diff --git a/package.json b/package.json index c1864b5..c9ef1d4 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "devDependencies": { "@graphql-codegen/cli": "^1.17.10", "@graphql-codegen/typescript": "^1.17.10", + "eslint-plugin-react-hooks": "^4.2.0", "typescript-plugin-css-modules": "^2.7.0" } } diff --git a/public/index.html b/public/index.html index a02d5c6..8dc3367 100644 --- a/public/index.html +++ b/public/index.html @@ -10,33 +10,13 @@ content="QuestionForm is an open source alternative to Google Forms" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> - <!-- - manifest.json provides metadata used when your web app is installed on a - user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ - --> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> - <!-- - Notice the use of %PUBLIC_URL% in the tags above. - It will be replaced with the URL of the `public` folder during the build. - Only files inside the `public` folder can be referenced from the HTML. - Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will - work correctly both with client-side routing and a non-root public URL. - Learn how to configure a non-root public URL by running `npm run build`. - --> - <link rel="stylesheet" href="index.css" /> + <link rel="stylesheet" href="/index.css" /> <title>QuestionForm</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> - <!-- - This HTML file is a template. - If you open it directly in the browser, you will see an empty page. - You can add webfonts, meta tags, or analytics to this file. - The build step will place the bundled scripts into the <body> tag. - To begin the development, run `npm start` or `yarn start`. - To create a production bundle, use `npm run build` or `yarn build`. - --> </body> </html> diff --git a/src/components/App.tsx b/src/components/App.tsx index d63a876..170b013 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -3,40 +3,30 @@ import React from 'react' import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' import client from '../apollo' -import Context from '../context' -import { useUser } from '../hooks' -import Authorize from './Authorize' -import CreateForm from './CreateForm' -import DoForm from './DoForm' -import Home from './Home' -import Login from './Login' +import Authorize from '../views/Authorize' +import CreateForm from '../views/CreateForm' +import DoForm from '../views/DoForm' +import Home from '../views/Home' +import Login from '../views/Login' import Navbar from './Navbar' -import Register from './Register' -import UserPage from './UserPage' +import Register from '../views/Register' -const App: React.FC = () => { - const userContext = useUser() - - return ( - <div className="App"> - <ApolloProvider client={client}> - <Context.Provider value={userContext}> - <Router> - <Navbar /> - <Switch> - <Route path="/login" component={Login} /> - <Route path="/authorize" component={Authorize} /> - <Route path="/user" component={UserPage} /> - <Route path="/form/:id" component={DoForm} /> - <Route path="/create" component={CreateForm} /> - <Route path="/register" component={Register} /> - <Route exact path="/" component={Home} /> - </Switch> - </Router> - </Context.Provider> - </ApolloProvider> - </div> - ) -} +const App: React.FC = () => ( + <div className="App"> + <ApolloProvider client={client}> + <Router> + <Navbar /> + <Switch> + <Route path="/login" component={Login} /> + <Route path="/authorize" component={Authorize} /> + <Route path="/form/:id" component={DoForm} /> + <Route path="/create" component={CreateForm} /> + <Route path="/register" component={Register} /> + <Route exact path="/" component={Home} /> + </Switch> + </Router> + </ApolloProvider> + </div> +) export default App diff --git a/src/components/Card/index.tsx b/src/components/Card/index.tsx index 28c76cf..2428089 100644 --- a/src/components/Card/index.tsx +++ b/src/components/Card/index.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom' import styles from './main.module.css' -interface props { +interface ICardProps { title: string subtitle?: string icon?: React.Component @@ -11,7 +11,7 @@ interface props { id: number } -const Card: React.FC<props> = ({ title, subtitle, id }) => { +const Card: React.FC<ICardProps> = ({ title, subtitle, id }) => { return ( <Link to={`/form/${id}`} className={styles.card}> <h3>{title}</h3> diff --git a/src/components/DoForm/hooks.ts b/src/components/DoForm/hooks.ts deleted file mode 100644 index 5e73887..0000000 --- a/src/components/DoForm/hooks.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useState } from 'react' -import { ChoiseAnswer, InputAnswer } from '../../apollo/typeDefs.gen' - -export const useForm = (initialValue?: (InputAnswer | ChoiseAnswer)[]) => { - 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 deleted file mode 100644 index cfad12f..0000000 --- a/src/components/DoForm/index.tsx +++ /dev/null @@ -1,220 +0,0 @@ -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, - MutationFormSubmitArgs, - QueryFormArgs, - ServerAnswer, -} from '../../apollo/typeDefs.gen' -import Lists from './Lists' - -interface IFormQuery { - form: Form -} - -interface IFormSubmitMutation { - formSubmit: ServerAnswer -} - -const DoForm: React.FC = () => { - const { id: idString } = useParams<{ id: string }>() - - const id = parseInt(idString) - - const { data, error, loading, refetch: refetchForm } = useQuery< - IFormQuery, - QueryFormArgs - >(FORM, { - variables: { id }, - skip: isNaN(id), - }) - - const [ - doFormSubmit, - { error: submitError, data: submitData, loading: submitLoading }, - ] = useMutation<IFormSubmitMutation, MutationFormSubmitArgs>(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]) - - if (isNaN(id)) return <Redirect to="/" /> - - if (loading) return <div>Loading...</div> - if (error) return <div>{error.message}</div> - - const { form } = data! - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault() - - answers.forEach((el) => { - delete el.__typename - }) - - try { - const submitAnswers = JSON.stringify(answers) - - await doFormSubmit({ - variables: { - formId: id, - answers: submitAnswers, - }, - }) - await refetchForm() - } catch (err) {} - } - - 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.submissions ? ( - form.submissions.length > 0 ? ( - <div> - <h1>Submission{form.submissions.length > 1 && 's'}:</h1> - <ul> - {form.submissions.map((submission, submissionIndex) => ( - <li key={submissionIndex}> - <h2> - User:{' '} - {submission.user ? submission.user.name : 'No submitter'} - </h2> - <ul> - {submission.answers.map( - (answer: InputAnswer | ChoiseAnswer, answerIndex) => ( - <li key={answerIndex}> - <h3>{form.questions![answerIndex].title}</h3> - {answer.__typename === 'ChoiseAnswer' && ( - <h4> - { - (form.questions![ - answerIndex - ] as ChoisesQuestion).variants[ - answer.userChoise - ].text - } - </h4> - )} - {answer.__typename === 'InputAnswer' && ( - <h4>{answer.userInput}</h4> - )} - </li> - ) - )} - </ul> - </li> - ))} - </ul> - </div> - ) : ( - 'No submissions yet' - ) - ) : ( - <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/DoForm/Lists.tsx b/src/components/FormLists/index.tsx similarity index 87% rename from src/components/DoForm/Lists.tsx rename to src/components/FormLists/index.tsx index b92697e..2c8ffc5 100644 --- a/src/components/DoForm/Lists.tsx +++ b/src/components/FormLists/index.tsx @@ -1,13 +1,13 @@ import React from 'react' -interface IProps { +interface IListsProps { variants: { text: string }[] name: string type: 'CHECK' | 'CHOOSE' onChange: (num: number) => void } -const Lists: React.FC<IProps> = ({ variants, name, type, onChange }) => { +const Lists: React.FC<IListsProps> = ({ variants, name, type, onChange }) => { const inputType = (type === 'CHECK' && 'check') || (type === 'CHOOSE' && 'radio') || undefined diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index c0f0f1a..45c0a65 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -1,5 +1,6 @@ import React from 'react' import { Link } from 'react-router-dom' + import styles from './main.module.css' import logo from './logo.svg' diff --git a/src/components/QuestionsForm/hooks.ts b/src/components/QuestionsForm/hooks.ts new file mode 100644 index 0000000..556701b --- /dev/null +++ b/src/components/QuestionsForm/hooks.ts @@ -0,0 +1,34 @@ +import { useState, useCallback } from 'react' +import { AnswerT, QuestionT } from '../../types' +import { getInitialState } from './utils' + +export const useForm = () => { + const [answers, setAnswers] = useState<AnswerT[]>([]) + + const changeAnswer = useCallback( + (num: number) => (value: number | string) => + setAnswers((prev) => + 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 + }) + ), + [setAnswers] + ) + + const setInitialState = useCallback( + (questions: QuestionT[]) => { + const state = getInitialState(questions) + console.log('Setting state') + setAnswers(state) + }, + [setAnswers] + ) + + return { answers, changeAnswer, setInitialState } +} diff --git a/src/components/QuestionsForm/index.tsx b/src/components/QuestionsForm/index.tsx new file mode 100644 index 0000000..0918dfc --- /dev/null +++ b/src/components/QuestionsForm/index.tsx @@ -0,0 +1,118 @@ +import React, { FormEvent, useEffect } from 'react' +import { useMutation } from '@apollo/client' + +import { MutationFormSubmitArgs } from '../../apollo/typeDefs.gen' +import { FORMSUBMIT } from '../../apollo' +import Lists from '../FormLists' +import { useForm } from './hooks' +import { QuestionT } from '../../types' +import { RefetchQuestionsFT, IFormSubmitMutation } from './types' + +interface IQuestionsFormProps { + formId: number + questions: QuestionT[] + refetchQuestions: RefetchQuestionsFT +} + +const QuestionsForm: React.FC<IQuestionsFormProps> = ({ + formId, + questions, + refetchQuestions, +}) => { + const [ + doFormSubmit, + { error: submitError, data: submitData, loading: submitLoading }, + ] = useMutation<IFormSubmitMutation, MutationFormSubmitArgs>(FORMSUBMIT) + + const { + answers, + changeAnswer, + setInitialState: setInitialFromState, + } = useForm() + + useEffect(() => setInitialFromState(questions), [ + questions, + setInitialFromState, + ]) + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + + answers.forEach((el) => { + delete el.__typename + }) + + try { + const submitAnswers = JSON.stringify(answers) + + await doFormSubmit({ + variables: { + formId, + answers: submitAnswers, + }, + }) + await refetchQuestions() + } catch (err) {} + } + + return ( + <> + <form onSubmit={handleSubmit}> + <ul> + {questions.map((el: QuestionT) => { + if (el.__typename === 'InputQuestion') + return ( + <li key={el.number}> + <label> + {el.title} + <input + onChange={(e) => + changeAnswer(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 + ) + changeAnswer(el.number)(selectValue) + }} + name={el.title} + > + {el.variants.map((option, index) => ( + <option key={index}>{option.text}</option> + ))} + </select> + ) : ( + <Lists + variants={el.variants} + onChange={changeAnswer(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?.formSubmit.success && <p>Successfully uploaded</p>} + </> + ) +} + +export default QuestionsForm diff --git a/src/components/QuestionsForm/types.ts b/src/components/QuestionsForm/types.ts new file mode 100644 index 0000000..104a08b --- /dev/null +++ b/src/components/QuestionsForm/types.ts @@ -0,0 +1,14 @@ +import { ApolloQueryResult } from '@apollo/client' + +import { IFormQuery, AnswerT, QuestionT } from '../../types' +import { QueryFormArgs, ServerAnswer } from '../../apollo/typeDefs.gen' + +export type RefetchQuestionsFT = ( + variables?: Partial<QueryFormArgs> | undefined +) => Promise<ApolloQueryResult<IFormQuery>> + +export interface IFormSubmitMutation { + formSubmit: ServerAnswer +} + +export type GetInitialStateFT = (questions: QuestionT[]) => AnswerT[] diff --git a/src/components/QuestionsForm/utils.ts b/src/components/QuestionsForm/utils.ts new file mode 100644 index 0000000..5ffbf69 --- /dev/null +++ b/src/components/QuestionsForm/utils.ts @@ -0,0 +1,11 @@ +import { AnswerT } from '../../types' +import { GetInitialStateFT } from './types' + +export const getInitialState: GetInitialStateFT = (questions) => + questions.flatMap<AnswerT>((el) => { + if (el.__typename === 'ChoisesQuestion') + return [{ __typename: 'ChoiseAnswer', type: 'CHOISE', userChoise: -1 }] + if (el.__typename === 'InputQuestion') + return [{ __typename: 'InputAnswer', type: 'INPUT', userInput: '' }] + return [] + }) diff --git a/src/components/SubmissionsList/index.tsx b/src/components/SubmissionsList/index.tsx new file mode 100644 index 0000000..79e183d --- /dev/null +++ b/src/components/SubmissionsList/index.tsx @@ -0,0 +1,56 @@ +import React from 'react' + +import { + FormSubmission, + InputAnswer, + ChoiseAnswer, + ChoisesQuestion, + Question, +} from '../../apollo/typeDefs.gen' + +interface ISubmissionListProps { + submissions: FormSubmission[] + questions: Question[] +} + +const SubmissionList: React.FC<ISubmissionListProps> = ({ + submissions, + questions, +}) => { + return submissions.length > 0 ? ( + <ul> + {submissions.map((submission, submissionIndex) => ( + <li key={submissionIndex}> + <h2> + User: {submission.user ? submission.user.name : 'No submitter'} + </h2> + <ul> + {submission.answers.map( + (answer: InputAnswer | ChoiseAnswer, answerIndex) => ( + <li key={answerIndex}> + <h3>{questions![answerIndex].title}</h3> + {answer.__typename === 'ChoiseAnswer' && ( + <h4> + { + (questions![answerIndex] as ChoisesQuestion).variants[ + answer.userChoise + ].text + } + </h4> + )} + {answer.__typename === 'InputAnswer' && ( + <h4>{answer.userInput}</h4> + )} + </li> + ) + )} + </ul> + </li> + ))} + </ul> + ) : ( + <p>No submissions yet</p> + ) +} + +export default SubmissionList diff --git a/src/components/UserPage/index.tsx b/src/components/UserPage/index.tsx deleted file mode 100644 index 9635d05..0000000 --- a/src/components/UserPage/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useQuery } from '@apollo/client' -import React from 'react' -import { USER } from '../../apollo' -import { QueryUserArgs, User } from '../../apollo/typeDefs.gen' - -interface IUserQuery { - user: User -} - -const UserPage: React.FC = () => { - const { data, error, loading } = useQuery<IUserQuery, QueryUserArgs>(USER) - if (loading) return <p>Loading...</p> - - if (error) return <p>{error.message}</p> - - const { user } = data! - - return ( - <div> - <h1>Username: {user.name}</h1> - <h3>Email: {user.email}</h3> - <p>User ID: {user.id}</p> - {user.forms && ( - <> - <h2>Forms list</h2> - <ul> - {user.forms.map((form, index) => ( - <li key={index}> - <a href={`http://localhost:3000/form/${form.id}`}> - {form.title} - </a> - </li> - ))} - </ul> - </> - )} - </div> - ) -} - -export default UserPage diff --git a/src/context.ts b/src/context.ts deleted file mode 100644 index 089057d..0000000 --- a/src/context.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createContext } from 'react' - -export type UserType = { - id: number - email: string - name: string -} - -export type ContextType = { - user: UserType - setUser: (user: UserType) => void -} - -export const initialUser: UserType = { - email: '', - id: 0, - name: '' -} - -const initialState: ContextType = { - user: initialUser, - setUser: () => {} -} - -const Context = createContext<ContextType>(initialState) - -export default Context diff --git a/src/hooks.ts b/src/hooks.ts index 0f85206..2c98944 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,16 +1,4 @@ -import { useCallback, useState } from 'react' import { useLocation } from 'react-router-dom' -import { ContextType, initialUser, UserType } from './context' - -export const useUser = (): ContextType => { - const [user, internalSerUser] = useState<UserType>(initialUser) - - const setUser = useCallback((user: UserType): void => { - internalSerUser(user) - }, []) - - return { user, setUser } -} export const useURLQuery = () => { return new URLSearchParams(useLocation().search) diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..472f12f --- /dev/null +++ b/src/types.ts @@ -0,0 +1,15 @@ +import { + InputQuestion, + ChoisesQuestion, + InputAnswer, + ChoiseAnswer, + Form, +} from './apollo/typeDefs.gen' + +export type QuestionT = InputQuestion | ChoisesQuestion + +export type AnswerT = InputAnswer | ChoiseAnswer + +export interface IFormQuery { + form: Form +} diff --git a/src/components/Authorize/index.tsx b/src/views/Authorize/index.tsx similarity index 99% rename from src/components/Authorize/index.tsx rename to src/views/Authorize/index.tsx index dcd920f..f1e9309 100644 --- a/src/components/Authorize/index.tsx +++ b/src/views/Authorize/index.tsx @@ -1,5 +1,6 @@ import React from 'react' import { Redirect } from 'react-router-dom' + import { useURLQuery } from '../../hooks' const Authorize: React.FC = () => { diff --git a/src/components/CreateForm/hooks.ts b/src/views/CreateForm/hooks.ts similarity index 50% rename from src/components/CreateForm/hooks.ts rename to src/views/CreateForm/hooks.ts index 11da114..6dcec4a 100644 --- a/src/components/CreateForm/hooks.ts +++ b/src/views/CreateForm/hooks.ts @@ -1,72 +1,27 @@ -import { ApolloError, useMutation } from '@apollo/client' -import { ChangeEvent, FormEvent, useState } from 'react' +import { useMutation } from '@apollo/client' +import { useState } from 'react' + import { CREATEFORM } from '../../apollo' -import { MutationCreateFormArgs, ServerAnswer } from '../../apollo/typeDefs.gen' - -type FormQuestion<T extends string> = { - title: string - type: T - variants: string[] -} - -type Form<T extends string> = { - title: string - questions: FormQuestion<T>[] -} - -export type FormatQuestionsToSubmitType = <T extends string>( - questions: FormQuestion<T>[] -) => string - -interface ICreateFormMutation { - createForm: ServerAnswer -} - -type FormSubmitType = (e: FormEvent<HTMLFormElement>) => void - -type HandleFormTitleChangeType = (e: ChangeEvent<HTMLInputElement>) => void - -type CreateQuestionType<T extends string> = (type: T) => void - -type HandleQuestionTitleChangeType = ( - questionNumber: number, - e: ChangeEvent<HTMLInputElement> -) => void - -type AddVariantType = (questionNumber: number) => void - -type HandleAnswerVariantChangeType = ( - questionNumber: number, - variantNumber: number, - e: ChangeEvent<HTMLInputElement> -) => void - -type UseFormCreatorHookTurple<T extends string> = [ - Form<T>, - [ - FormSubmitType, - { - submitData: ICreateFormMutation | null | undefined - submitError: ApolloError | undefined - submitLoading: boolean - } - ], - { - handleFormTitleChange: HandleFormTitleChangeType - addQuestion: CreateQuestionType<T> - handleQuestionTitleChange: HandleQuestionTitleChangeType - handleAnswerVariantChange: HandleAnswerVariantChangeType - addVariant: AddVariantType - }, - () => void -] +import { MutationCreateFormArgs } from '../../apollo/typeDefs.gen' +import { + FormatQuestionsToSubmitFT, + UseFormCreatorHookTurpleT, + FormT, + ICreateFormMutation, + FormSubmitT, + HandleFormTitleChangeFT, + CreateQuestionFT, + HandleQuestionTitleChangeFT, + HandleAnswerVariantChangeFT, + AddVariantFT, +} from './types' const initialState = { title: '', questions: [] } export const useFormCreator = <T extends string>( - formatQuestionsToSubmit: FormatQuestionsToSubmitType -): UseFormCreatorHookTurple<T> => { - const [form, setState] = useState<Form<T>>(initialState) + formatQuestionsToSubmit: FormatQuestionsToSubmitFT +): UseFormCreatorHookTurpleT<T> => { + const [form, setState] = useState<FormT<T>>(initialState) const [ doFormCreation, @@ -78,26 +33,26 @@ export const useFormCreator = <T extends string>( }, }) - const formSubmit: FormSubmitType = async (e) => { + const formSubmit: FormSubmitT = async (e) => { e.preventDefault() try { await doFormCreation() } catch (err) {} } - const handleFormTitleChange: HandleFormTitleChangeType = (e) => { + const handleFormTitleChange: HandleFormTitleChangeFT = (e) => { const title = e.currentTarget.value setState((prev) => ({ ...prev, title })) } - const createQuestion: CreateQuestionType<T> = (type) => { + const createQuestion: CreateQuestionFT<T> = (type) => { setState(({ title, questions }) => ({ title, questions: questions.concat({ title: '', type, variants: [''] }), })) } - const handleQuestionTitleChange: HandleQuestionTitleChangeType = ( + const handleQuestionTitleChange: HandleQuestionTitleChangeFT = ( questionNumber, e ) => { @@ -110,7 +65,7 @@ export const useFormCreator = <T extends string>( })) } - const handleAnswerVariantChange: HandleAnswerVariantChangeType = ( + const handleAnswerVariantChange: HandleAnswerVariantChangeFT = ( questionNumber, variantNumber, e @@ -131,7 +86,7 @@ export const useFormCreator = <T extends string>( })) } - const addVariant: AddVariantType = (questionNumber) => { + const addVariant: AddVariantFT = (questionNumber) => { setState(({ title, questions }) => ({ title, questions: questions.map((el, index) => ({ diff --git a/src/components/CreateForm/index.tsx b/src/views/CreateForm/index.tsx similarity index 78% rename from src/components/CreateForm/index.tsx rename to src/views/CreateForm/index.tsx index 2df35e8..e109891 100644 --- a/src/components/CreateForm/index.tsx +++ b/src/views/CreateForm/index.tsx @@ -1,30 +1,8 @@ import React from 'react' -import { FormatQuestionsToSubmitType, useFormCreator } from './hooks' - -const creationsArray = [ - { title: 'Check', type: 'CHECK', enabled: false }, - { title: 'Input', type: 'INPUT', enabled: true }, - { title: 'Choose', type: 'CHOOSE', enabled: true }, - { title: 'Select', type: 'SELECT', enabled: true }, -] as const - -type QuestionTypes = 'CHECK' | 'INPUT' | 'CHOOSE' | 'SELECT' - -const formatQuestionsToSubmit: FormatQuestionsToSubmitType = (questions) => { - return JSON.stringify( - questions.map((question) => - question.type === 'INPUT' - ? { title: question.title } - : { - ...question, - variants: question.variants.map((variant) => ({ - text: variant, - })), - } - ) - ) -} +import { QuestionTypes } from './types' +import { useFormCreator } from './hooks' +import { creationsArray, formatQuestionsToSubmit } from './utils' const CreateForm: React.FC = () => { const [ @@ -44,8 +22,8 @@ const CreateForm: React.FC = () => { <> <form onSubmit={(e) => { - resetForm() formSubmit(e) + resetForm() }} > <label> @@ -126,10 +104,7 @@ const CreateForm: React.FC = () => { </fieldset> {submitLoading ? 'Loading...' : <input type="submit" value="Submit" />} </form> - {submitData && - submitData.createForm && - submitData.createForm.success && - 'Successfully uploaded'} + {submitData?.createForm.success && 'Successfully uploaded'} {submitError && submitError.message} </> ) diff --git a/src/views/CreateForm/types.ts b/src/views/CreateForm/types.ts new file mode 100644 index 0000000..b954215 --- /dev/null +++ b/src/views/CreateForm/types.ts @@ -0,0 +1,64 @@ +import { FormEvent, ChangeEvent } from 'react' +import { ApolloError } from '@apollo/client' + +import { ServerAnswer } from '../../apollo/typeDefs.gen' + +export type FormQuestionT<T extends string> = { + title: string + type: T + variants: string[] +} + +export type FormT<T extends string> = { + title: string + questions: FormQuestionT<T>[] +} + +export type FormatQuestionsToSubmitFT = <T extends string>( + questions: FormQuestionT<T>[] +) => string + +export interface ICreateFormMutation { + createForm: ServerAnswer +} + +export type FormSubmitT = (e: FormEvent<HTMLFormElement>) => void + +export type HandleFormTitleChangeFT = (e: ChangeEvent<HTMLInputElement>) => void + +export type CreateQuestionFT<T extends string> = (type: T) => void + +export type HandleQuestionTitleChangeFT = ( + questionNumber: number, + e: ChangeEvent<HTMLInputElement> +) => void + +export type AddVariantFT = (questionNumber: number) => void + +export type HandleAnswerVariantChangeFT = ( + questionNumber: number, + variantNumber: number, + e: ChangeEvent<HTMLInputElement> +) => void + +export type UseFormCreatorHookTurpleT<T extends string> = [ + FormT<T>, + [ + FormSubmitT, + { + submitData: ICreateFormMutation | null | undefined + submitError: ApolloError | undefined + submitLoading: boolean + } + ], + { + handleFormTitleChange: HandleFormTitleChangeFT + addQuestion: CreateQuestionFT<T> + handleQuestionTitleChange: HandleQuestionTitleChangeFT + handleAnswerVariantChange: HandleAnswerVariantChangeFT + addVariant: AddVariantFT + }, + () => void +] + +export type QuestionTypes = 'CHECK' | 'INPUT' | 'CHOOSE' | 'SELECT' diff --git a/src/views/CreateForm/utils.ts b/src/views/CreateForm/utils.ts new file mode 100644 index 0000000..dd3ec85 --- /dev/null +++ b/src/views/CreateForm/utils.ts @@ -0,0 +1,22 @@ +import { FormatQuestionsToSubmitFT } from './types' + +export const creationsArray = [ + { title: 'Check', type: 'CHECK', enabled: false }, + { title: 'Input', type: 'INPUT', enabled: true }, + { title: 'Choose', type: 'CHOOSE', enabled: true }, + { title: 'Select', type: 'SELECT', enabled: true }, +] as const + +export const formatQuestionsToSubmit: FormatQuestionsToSubmitFT = (questions) => + JSON.stringify( + questions.map((question) => + question.type === 'INPUT' + ? { title: question.title } + : { + ...question, + variants: question.variants.map((variant) => ({ + text: variant, + })), + } + ) + ) diff --git a/src/views/DoForm/hooks.ts b/src/views/DoForm/hooks.ts new file mode 100644 index 0000000..0159017 --- /dev/null +++ b/src/views/DoForm/hooks.ts @@ -0,0 +1,7 @@ +import { useParams } from 'react-router-dom' + +export const useId = () => { + const { id: idString } = useParams<{ id: string }>() + + return parseInt(idString) +} diff --git a/src/views/DoForm/index.tsx b/src/views/DoForm/index.tsx new file mode 100644 index 0000000..4aa688f --- /dev/null +++ b/src/views/DoForm/index.tsx @@ -0,0 +1,68 @@ +import { useQuery } from '@apollo/client' +import React from 'react' +import { Redirect } from 'react-router-dom' + +import { FORM } from '../../apollo' +import { QueryFormArgs } from '../../apollo/typeDefs.gen' +import SubmissionList from '../../components/SubmissionsList' +import styles from './main.module.css' +import QuestionsForm from '../../components/QuestionsForm' +import { IFormQuery } from '../../types' +import { useId } from './hooks' +import { getDateCreated } from './utils' + +const DoForm: React.FC = () => { + const id = useId() + + const { data, error, loading, refetch: refetchForm } = useQuery< + IFormQuery, + QueryFormArgs + >(FORM, { + variables: { id }, + skip: isNaN(id), + }) + + if (isNaN(id)) return <Redirect to="/" /> + + if (loading) return <div>Loading...</div> + if (error) return <div>{error.message}</div> + + const { form } = data! + + return ( + <div className={styles.container}> + <header className={styles.header}> + <h1 className={styles.formTitle}>{form.title}</h1> + <p className={styles.dateCreated}> + Published on {getDateCreated(form.dateCreated)} + </p> + <p className={styles.author}> + {'by ' + form.author?.name || 'No author'} + </p> + </header> + <main className={styles.main}> + <div className={styles.mainTop}></div> + {form.submissions ? ( + <> + <h1>Submissions</h1> + <SubmissionList + submissions={form.submissions} + questions={form.questions!} + /> + </> + ) : ( + <> + <h1>Questions</h1> + <QuestionsForm + formId={id} + questions={data!.form.questions!} + refetchQuestions={refetchForm} + /> + </> + )} + </main> + </div> + ) +} + +export default DoForm diff --git a/src/views/DoForm/main.module.css b/src/views/DoForm/main.module.css new file mode 100644 index 0000000..d3df2b3 --- /dev/null +++ b/src/views/DoForm/main.module.css @@ -0,0 +1,48 @@ +.container { + display: flex; + flex-direction: column; + min-height: calc(100vh - 4rem); +} + +.header { + padding: 2.3rem; + display: grid; + grid-template: + 'title title' auto + 'date author' auto / auto auto; +} + +.formTitle { + text-align: center; + grid-area: title; +} + +.dateCreated { + grid-area: date; +} + +.author { + text-align: right; + grid-area: author; +} + +.main { + background-color: #ffffff; + position: relative; + margin-top: 2.3rem; + flex-grow: 1; + padding: 2.3rem; + padding-top: 0; +} + +.mainTop { + height: 2.3rem; + border-top-left-radius: 20px; + border-top-right-radius: 20px; + width: 100%; + background-color: #ffffff; + position: absolute; + top: -2.3rem; + left: 0rem; + width: 100vw; +} diff --git a/src/views/DoForm/types.ts b/src/views/DoForm/types.ts new file mode 100644 index 0000000..b0d1f05 --- /dev/null +++ b/src/views/DoForm/types.ts @@ -0,0 +1 @@ +export type GetDateCreatedFT = (dateString: string) => string diff --git a/src/views/DoForm/utils.ts b/src/views/DoForm/utils.ts new file mode 100644 index 0000000..4717769 --- /dev/null +++ b/src/views/DoForm/utils.ts @@ -0,0 +1,7 @@ +import { GetDateCreatedFT } from './types' + +export const getDateCreated: GetDateCreatedFT = (dateString) => { + const date = new Date(dateString) + + return `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}` +} diff --git a/src/components/Home/index.tsx b/src/views/Home/index.tsx similarity index 97% rename from src/components/Home/index.tsx rename to src/views/Home/index.tsx index eb3aae2..0ba4cfd 100644 --- a/src/components/Home/index.tsx +++ b/src/views/Home/index.tsx @@ -1,12 +1,12 @@ import React from 'react' import { useQuery } from '@apollo/client' import { generateFromString } from 'generate-avatar' +import { Redirect } from 'react-router-dom' -import Card from '../Card' +import Card from '../../components/Card' import { USER } from '../../apollo' import { QueryUserArgs, User } from '../../apollo/typeDefs.gen' import styles from './main.module.css' -import { Redirect } from 'react-router-dom' interface IUserQuery { user: User diff --git a/src/components/Home/main.module.css b/src/views/Home/main.module.css similarity index 100% rename from src/components/Home/main.module.css rename to src/views/Home/main.module.css diff --git a/src/components/Login/index.tsx b/src/views/Login/index.tsx similarity index 100% rename from src/components/Login/index.tsx rename to src/views/Login/index.tsx diff --git a/src/components/Login/main.module.css b/src/views/Login/main.module.css similarity index 100% rename from src/components/Login/main.module.css rename to src/views/Login/main.module.css diff --git a/src/components/Login/meme.png b/src/views/Login/meme.png similarity index 100% rename from src/components/Login/meme.png rename to src/views/Login/meme.png diff --git a/src/components/Register/index.tsx b/src/views/Register/index.tsx similarity index 100% rename from src/components/Register/index.tsx rename to src/views/Register/index.tsx diff --git a/src/components/Register/meme.jpg b/src/views/Register/meme.jpg similarity index 100% rename from src/components/Register/meme.jpg rename to src/views/Register/meme.jpg