diff --git a/.gitignore b/.gitignore index 6b26568..e00931e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -*.local* \ No newline at end of file +*.local* +*.gen* \ No newline at end of file diff --git a/codegen.yml b/codegen.yml new file mode 100644 index 0000000..2d64352 --- /dev/null +++ b/codegen.yml @@ -0,0 +1,13 @@ +overwrite: true +schema: 'src/apollo/typeDefs.gql' +documents: null +generates: + src/apollo/typeDefs.gen.ts: + config: + useIndexSignature: true + wrapFieldDefinitions: true + enumsAsTypes: true + declarationKind: 'interface' + + plugins: + - 'typescript' diff --git a/package.json b/package.json index e645900..a45f10e 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ }, "scripts": { "start": "react-scripts start", - "build": "react-scripts build" + "build": "react-scripts build", + "codegen": "graphql-codegen --config codegen.yml" }, "eslintConfig": { "extends": "react-app" @@ -34,5 +35,9 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "@graphql-codegen/cli": "^1.17.10", + "@graphql-codegen/typescript": "^1.17.10" } } diff --git a/src/apollo/defs.ts b/src/apollo/defs.ts index 0d05073..0639a2d 100644 --- a/src/apollo/defs.ts +++ b/src/apollo/defs.ts @@ -11,7 +11,27 @@ const LOGIN = gql` const FORM = gql` query Form($id: Int!) { form(id: $id) { + author { + email + id + name + } + dateCreated id + questions { + number + ... on ChoisesQuestion { + title + type + variants { + text + } + } + ... on InputQuestion { + number + title + } + } title } } @@ -27,4 +47,12 @@ const USER = gql` } ` -export { LOGIN, FORM, USER } +const FORMSUBMIT = gql` + mutation FormSubmit($formId: Int!, $answers: String!) { + formSubmit(formId: $formId, answers: $answers) { + success + } + } +` + +export { LOGIN, FORM, USER, FORMSUBMIT } diff --git a/src/apollo/typeDefs.gql b/src/apollo/typeDefs.gql new file mode 100644 index 0000000..dbcb68d --- /dev/null +++ b/src/apollo/typeDefs.gql @@ -0,0 +1,84 @@ +type Query { + form(id: Int!): Form + forms: [Form!]! + user(id: Int): User +} + +type Mutation { + createForm(title: String!, questions: String!): serverAnswer + formSubmit(formId: Int!, answers: String!): serverAnswer + login(email: String!): serverAnswer + register(name: String!, email: String!): serverAnswer +} + +type Form { + author: User + dateCreated: String! + id: Int! + questions: [Question!]! + submissions: [FormSubmission!] + title: String! +} + +interface Question { + number: Int! + title: String! +} + +type ChoisesQuestion implements Question { + number: Int! + title: String! + type: ChoiseType! + variants: [Variant!]! +} + +type Variant { + text: String! +} + +type InputQuestion implements Question { + number: Int! + title: String! +} + +type FormSubmission { + answers: [Answer!]! + date: String! + id: Int! +} + +interface Answer { + type: AnswerType! +} + +type InputAnswer implements Answer { + type: AnswerType! + userInput: String +} + +type ChoiseAnswer implements Answer { + type: AnswerType! + userChoise: Int! +} + +enum ChoiseType { + CHECK + CHOOSE + SELECT +} + +enum AnswerType { + CHOISE + INPUT +} + +type User { + email: String! + forms: [Form!] + id: Int! + name: String! +} + +type serverAnswer { + success: Boolean! +} diff --git a/src/components/App.tsx b/src/components/App.tsx index a39064a..6d9c1bf 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -6,36 +6,10 @@ import client from '../apollo' import Context from '../context' import { useUser } from '../hooks' import Authorize from './Authorize' +import DoForm from './DoForm' import Login from './Login' import UserPage from './UserPage' -// const TestComponent: React.FC = () => { -// const { loading, error, data } = useQuery(FORM, { -// variables: { -// id: 1 -// } -// }) - -// const { user } = useContext(Context) - -// const [doLogin] = useMutation(LOGIN) - -// if (loading) return <p>Loading...</p> -// if (error) return <p>Error :(</p> - -// return ( -// <div> -// <button -// onClick={() => doLogin({ variables: { email: 'test@test.test' } })} -// > -// Click! -// </button> -// {user.id} -// {data.form.id} -// </div> -// ) -// } - const App: React.FC = () => { const userContext = useUser() @@ -48,6 +22,7 @@ const App: React.FC = () => { <Route path="/login" component={Login} /> <Route path="/authorize" component={Authorize} /> <Route path="/user" component={UserPage} /> + <Route path="/form/:id" component={DoForm} /> </Switch> </Router> </Context.Provider> diff --git a/src/components/DoForm/Lists.tsx b/src/components/DoForm/Lists.tsx new file mode 100644 index 0000000..b92697e --- /dev/null +++ b/src/components/DoForm/Lists.tsx @@ -0,0 +1,36 @@ +import React from 'react' + +interface IProps { + variants: { text: string }[] + name: string + type: 'CHECK' | 'CHOOSE' + onChange: (num: number) => void +} + +const Lists: React.FC<IProps> = ({ variants, name, type, onChange }) => { + const inputType = + (type === 'CHECK' && 'check') || (type === 'CHOOSE' && 'radio') || undefined + + return ( + <div> + {variants.map((el, index) => ( + <label key={index}> + <input + onChange={(e) => { + const selectValue = variants.findIndex( + (val) => val.text === e.currentTarget.value + ) + onChange(selectValue) + }} + type={inputType} + name={name} + value={el.text} + /> + {el.text} + </label> + ))} + </div> + ) +} + +export default Lists diff --git a/src/components/DoForm/hooks.ts b/src/components/DoForm/hooks.ts new file mode 100644 index 0000000..00f5524 --- /dev/null +++ b/src/components/DoForm/hooks.ts @@ -0,0 +1,28 @@ +import { useState } from 'react' +import { ChoiseAnswer, InputAnswer } from '../../apollo/typeDefs.gen' + +export const useForm = (initialValue?: (InputAnswer | ChoiseAnswer)[]) => { + console.log(initialValue, 'Inside hook') + + const [answers, setAnswer] = useState<(InputAnswer | ChoiseAnswer)[]>( + initialValue || [] + ) + + const answerChange = (num: number) => { + return (value: number | string) => { + setAnswer((prev) => { + return prev.map((el, index) => { + if (index === num) { + if (el.__typename === 'ChoiseAnswer' && typeof value === 'number') + return { ...el, userChoise: value } + if (el.__typename === 'InputAnswer' && typeof value === 'string') + return { ...el, userInput: value } + } + return el + }) + }) + } + } + + return [answers, answerChange] +} diff --git a/src/components/DoForm/index.tsx b/src/components/DoForm/index.tsx new file mode 100644 index 0000000..fe15db2 --- /dev/null +++ b/src/components/DoForm/index.tsx @@ -0,0 +1,171 @@ +import { useMutation, useQuery } from '@apollo/client' +import React, { FormEvent, useEffect, useState } from 'react' +import { Redirect, useParams } from 'react-router-dom' +import { FORM, FORMSUBMIT } from '../../apollo' +import { + ChoiseAnswer, + ChoisesQuestion, + Form, + InputAnswer, + InputQuestion, + QueryFormArgs, +} from '../../apollo/typeDefs.gen' +import Lists from './Lists' + +interface IFormQuery { + form: Form +} + +const DoForm: React.FC = () => { + const { id: idString } = useParams<{ id: string }>() + + const id = parseInt(idString) + + const { data, error, loading } = useQuery<IFormQuery, QueryFormArgs>(FORM, { + variables: { id }, + skip: isNaN(id), + }) + + const [ + doFormSubmit, + { error: submitError, data: submitData, loading: submitLoading }, + ] = useMutation(FORMSUBMIT) + + const [answers, setAnswer] = useState<(InputAnswer | ChoiseAnswer)[]>([]) + + const getInitialState = (data: IFormQuery) => { + if (data && data.form) { + return data.form.questions.flatMap<InputAnswer | ChoiseAnswer>( + (el: InputQuestion | ChoisesQuestion) => { + if (el.__typename === 'ChoisesQuestion') + return [ + { __typename: 'ChoiseAnswer', type: 'CHOISE', userChoise: -1 }, + ] + if (el.__typename === 'InputQuestion') + return [{ __typename: 'InputAnswer', type: 'INPUT', userInput: '' }] + return [] + } + ) + } + return [] + } + + useEffect(() => { + if (data) { + const initialState = getInitialState(data) + setAnswer(initialState) + } + }, [data]) + + useEffect(() => console.log(answers), [answers]) + + if (isNaN(id)) return <Redirect to="/" /> + + if (loading) return <div>Loading...</div> + if (error) return <div>{error.message}</div> + + const { form } = data! + + const handleSubmit = (e: FormEvent) => { + e.preventDefault() + console.log('Submited form:', answers) + + answers.forEach((el) => { + delete el.__typename + }) + + const submitAnswers = JSON.stringify(answers) + + console.log('Filtered answers: ', submitAnswers) + + doFormSubmit({ + variables: { + formId: id, + answers: submitAnswers, + }, + }) + } + + const answerChange = (num: number) => { + return (value: number | string) => { + setAnswer((prev) => { + return prev.map((el, index) => { + if (index === num) { + if (el.__typename === 'ChoiseAnswer' && typeof value === 'number') + return { ...el, userChoise: value } + if (el.__typename === 'InputAnswer' && typeof value === 'string') + return { ...el, userInput: value } + } + return el + }) + }) + } + } + + return ( + <div> + <h1>{form.title}</h1> + <p>{form.dateCreated}</p> + <h3>{form.author?.name || 'No author'}</h3> + + <form onSubmit={handleSubmit}> + <ul> + {form.questions.map((el: InputQuestion | ChoisesQuestion) => { + if (el.__typename === 'InputQuestion') + return ( + <li key={el.number}> + <label> + {el.title} + <input + onChange={(e) => + answerChange(el.number)(e.currentTarget.value) + } + type="text" + /> + </label> + </li> + ) + if (el.__typename === 'ChoisesQuestion') + return ( + <li key={el.number}> + <label> + {el.title} + {el.type === 'SELECT' ? ( + <select + onChange={(e) => { + const selectValue = el.variants.findIndex( + (val) => val.text === e.currentTarget.value + ) + answerChange(el.number)(selectValue) + }} + name={el.title} + > + {el.variants.map((option, index) => ( + <option key={index}>{option.text}</option> + ))} + </select> + ) : ( + <Lists + variants={el.variants} + onChange={answerChange(el.number)} + name={el.title} + type={el.type} + /> + )} + </label> + </li> + ) + return <li>Unknown question type</li> + })} + </ul> + {submitLoading ? <p>Uploading...</p> : <input type="submit" />} + </form> + {submitError && <p>{submitError.message}</p>} + {submitData && submitData.formSubmit && submitData.formSubmit.success && ( + <p>Successfully uploaded</p> + )} + </div> + ) +} + +export default DoForm diff --git a/src/components/UserPage/index.tsx b/src/components/UserPage/index.tsx index 2a62f60..4bd5c57 100644 --- a/src/components/UserPage/index.tsx +++ b/src/components/UserPage/index.tsx @@ -8,15 +8,15 @@ const UserPage: React.FC = () => { if (error) return <p>{error.message}</p> - console.log(Object.entries(data.user)) + const { name, email, id } = data.user - const user = Object.entries(data.user).map((el, index) => ( - <li key={index}> - {el[0]}: {el[1]} - </li> - )) - - return <ul>{user}</ul> + return ( + <div> + <h1>Username: {name}</h1> + <h3>Email: {email}</h3> + <p>User ID: {id}</p> + </div> + ) } export default UserPage