Massive code refactor, added initial states to form submission component (named DoForm)
This commit is contained in:
parent
1060ea4e41
commit
eaba1665ce
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 (
|
||||
const App: React.FC = () => (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default App
|
||||
|
@ -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>
|
||||
|
@ -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]
|
||||
}
|
@ -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
|
@ -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
|
||||
|
@ -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'
|
||||
|
||||
|
34
src/components/QuestionsForm/hooks.ts
Normal file
34
src/components/QuestionsForm/hooks.ts
Normal file
@ -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 }
|
||||
}
|
118
src/components/QuestionsForm/index.tsx
Normal file
118
src/components/QuestionsForm/index.tsx
Normal file
@ -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
|
14
src/components/QuestionsForm/types.ts
Normal file
14
src/components/QuestionsForm/types.ts
Normal file
@ -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[]
|
11
src/components/QuestionsForm/utils.ts
Normal file
11
src/components/QuestionsForm/utils.ts
Normal file
@ -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 []
|
||||
})
|
56
src/components/SubmissionsList/index.tsx
Normal file
56
src/components/SubmissionsList/index.tsx
Normal file
@ -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
|
@ -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
|
@ -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
|
12
src/hooks.ts
12
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)
|
||||
|
15
src/types.ts
Normal file
15
src/types.ts
Normal file
@ -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
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import { Redirect } from 'react-router-dom'
|
||||
|
||||
import { useURLQuery } from '../../hooks'
|
||||
|
||||
const Authorize: React.FC = () => {
|
@ -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) => ({
|
@ -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}
|
||||
</>
|
||||
)
|
64
src/views/CreateForm/types.ts
Normal file
64
src/views/CreateForm/types.ts
Normal file
@ -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'
|
22
src/views/CreateForm/utils.ts
Normal file
22
src/views/CreateForm/utils.ts
Normal file
@ -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,
|
||||
})),
|
||||
}
|
||||
)
|
||||
)
|
7
src/views/DoForm/hooks.ts
Normal file
7
src/views/DoForm/hooks.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
export const useId = () => {
|
||||
const { id: idString } = useParams<{ id: string }>()
|
||||
|
||||
return parseInt(idString)
|
||||
}
|
68
src/views/DoForm/index.tsx
Normal file
68
src/views/DoForm/index.tsx
Normal file
@ -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
|
48
src/views/DoForm/main.module.css
Normal file
48
src/views/DoForm/main.module.css
Normal file
@ -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;
|
||||
}
|
1
src/views/DoForm/types.ts
Normal file
1
src/views/DoForm/types.ts
Normal file
@ -0,0 +1 @@
|
||||
export type GetDateCreatedFT = (dateString: string) => string
|
7
src/views/DoForm/utils.ts
Normal file
7
src/views/DoForm/utils.ts
Normal file
@ -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()}`
|
||||
}
|
@ -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
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
Loading…
x
Reference in New Issue
Block a user