Compare commits

...

20 Commits
v1.0 ... main

Author SHA1 Message Date
540c93236a
Updated packages and removed yarn.lock 2021-02-05 20:57:35 +05:00
dependabot[bot]
5a4795a66e
Bump ini from 1.3.5 to 1.3.8
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-13 07:03:18 +00:00
65a6ca3e03 Some deployment fixes 2020-11-11 19:12:15 +03:00
2634f64d1f
Added serve package 2020-11-11 00:03:10 +05:00
53ea63b35f
Removed development console.log's and added Dockerfile 2020-11-07 07:08:28 +05:00
19fa7fc4e5
Create LICENSE 2020-11-07 06:09:09 +05:00
6ef34c6ab8
Added styles for form creation page and all components in it. Some minor improvements in another pages 2020-11-07 06:07:27 +05:00
85d7e48e0d
Added styles for entrie form view: form submission and list submissions components. Some minor code refactors 2020-11-07 03:33:30 +05:00
c94dab4a9b
Merge branch 'main' of https://github.com/Dm1tr1y147/questionForm_frontend into main
I used dependabot to fix some dependencies errors and didn't commit them before local commits
2020-11-06 22:39:17 +05:00
eaba1665ce
Massive code refactor, added initial states to form submission component (named DoForm) 2020-11-06 22:38:28 +05:00
228ad884bc
Merge pull request from Dm1tr1y147/dependabot/npm_and_yarn/websocket-extensions-0.1.4
Bump websocket-extensions from 0.1.3 to 0.1.4
2020-11-05 02:01:23 +05:00
743742b757
Merge pull request from Dm1tr1y147/dependabot/npm_and_yarn/http-proxy-1.18.1
Bump http-proxy from 1.18.0 to 1.18.1
2020-11-05 02:00:01 +05:00
dependabot[bot]
46ffa05e14
Bump websocket-extensions from 0.1.3 to 0.1.4
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-04 16:59:14 +00:00
dependabot[bot]
c434429ed5
Bump http-proxy from 1.18.0 to 1.18.1
Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.18.0 to 1.18.1.
- [Release notes](https://github.com/http-party/node-http-proxy/releases)
- [Changelog](https://github.com/http-party/node-http-proxy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/http-party/node-http-proxy/compare/1.18.0...1.18.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-04 16:58:37 +00:00
6ff944f0ee
Merge pull request from Dm1tr1y147/dependabot/npm_and_yarn/elliptic-6.5.3
Bump elliptic from 6.5.2 to 6.5.3
2020-11-04 21:57:40 +05:00
dependabot[bot]
5be9ed5197
Bump elliptic from 6.5.2 to 6.5.3
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-04 14:03:07 +00:00
1060ea4e41
Fixed some styling errors, updated home page content and styles, added some changes into data fetching 2020-11-04 18:58:12 +05:00
3f580213fa
Created styles for login and register pages. Started styling home page 2020-11-03 22:24:07 +05:00
6c60520aae
Added registration page and functionality, improved error handling, fixed some critical security problems 2020-10-20 00:35:08 +05:00
b8450525a5
Added form submissions showing for submitted forms and authors instead of forms itself 2020-10-19 22:48:51 +05:00
68 changed files with 1971 additions and 11441 deletions

1
.dockerignore Normal file

@ -0,0 +1 @@
node_modules/

5
.gitignore vendored

@ -13,6 +13,7 @@
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
@ -23,4 +24,6 @@ yarn-debug.log*
yarn-error.log*
*.local*
*.gen*
*.gen*
yarn.lock

7
Dockerfile Normal file

@ -0,0 +1,7 @@
FROM node:alpine
WORKDIR /frontend
COPY package.json /frontend/package.json
RUN yarn
COPY . /frontend
RUN yarn codegen
RUN yarn build

21
LICENSE Normal file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Dmitriy Shishkov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -9,6 +9,7 @@
"@types/react-dom": "^16.9.0",
"@types/react-router-dom": "^5.1.6",
"@types/validator": "^13.1.0",
"generate-avatar": "^1.4.6",
"graphql": "^15.3.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
@ -19,7 +20,8 @@
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"codegen": "graphql-codegen --config codegen.yml"
"codegen": "graphql-codegen --config codegen.yml",
"serve": "serve -s build"
},
"eslintConfig": {
"extends": "react-app"
@ -38,6 +40,9 @@
},
"devDependencies": {
"@graphql-codegen/cli": "^1.17.10",
"@graphql-codegen/typescript": "^1.17.10"
"@graphql-codegen/typescript": "^1.17.10",
"eslint-plugin-react-hooks": "^4.2.0",
"serve": "^11.3.2",
"typescript-plugin-css-modules": "^2.7.0"
}
}

Binary file not shown.

After

(image error) Size: 16 KiB

Binary file not shown.

After

(image error) Size: 35 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

(image error) Size: 10 KiB

9
public/browserconfig.xml Normal file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png" />
<TileColor>#363645</TileColor>
</tile>
</msapplication>
</browserconfig>

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

(image error) Size: 959 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

(image error) Size: 1.8 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width: 48px  |  Height: 48px  |  Size: 15 KiB

45
public/index.css Normal file

@ -0,0 +1,45 @@
:root {
--backgroundColor: rgba(54, 54, 69, 0.05);
--containerColor: rgb(54, 54, 69);
--accentColor: rgb(109, 245, 119);
--accentShadowColor: rgba(109, 245, 119, 0.16);
--onAccentFontColor: #ffffff;
}
* {
margin: 0;
padding: 0;
-ms-overflow-style: none;
scrollbar-width: none;
box-sizing: border-box;
outline: none;
}
*::-webkit-scrollbar {
display: none;
}
body {
background-color: var(--backgroundColor);
min-height: 100vh;
color: var(--containerColor);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
input {
color: var(--containerColor);
}
#root {
height: 100vh;
}

@ -2,39 +2,43 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
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="apple-touch-icon"
sizes="180x180"
href="%PUBLIC_URL%/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/favicon-16x16.png"
/>
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
<link
rel="mask-icon"
href="%PUBLIC_URL%/safari-pinned-tab.svg"
color="#6df577"
/>
<meta name="msapplication-TileColor" content="#6df577" />
<meta name="theme-color" content="#363645" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" />
<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>

@ -8,18 +8,18 @@
"type": "image/x-icon"
},
{
"src": "logo192.png",
"src": "android-chrome-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"src": "android-chrome-512x512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"start_url": "/",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
"theme_color": "#6df577",
"background_color": "#363645"
}

BIN
public/mstile-150x150.png Normal file

Binary file not shown.

After

(image error) Size: 11 KiB

@ -0,0 +1,34 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2265 5108 c-44 -5 -134 -20 -200 -33 -514 -104 -948 -336 -1315
-705 -370 -371 -602 -803 -705 -1315 -34 -172 -45 -289 -45 -495 0 -206 11
-323 45 -495 103 -512 335 -946 705 -1315 356 -357 770 -584 1263 -694 338
-76 753 -76 1097 0 261 58 590 191 802 325 164 104 314 225 458 369 357 356
584 770 694 1263 56 251 73 597 41 857 -95 768 -529 1449 -1191 1868 -304 192
-708 331 -1076 371 -115 13 -459 12 -573 -1z m690 -902 c173 -37 291 -78 450
-156 311 -154 568 -407 724 -715 137 -272 201 -616 172 -925 -39 -400 -186
-725 -448 -987 l-82 -83 32 -23 c126 -93 243 -139 367 -142 l85 -2 3 -148 3
-148 -103 6 c-216 13 -391 84 -559 228 l-56 48 -29 -20 c-61 -42 -245 -129
-344 -164 -57 -19 -160 -48 -229 -62 -117 -25 -144 -27 -371 -27 -229 -1 -253
1 -371 26 -547 118 -982 448 -1219 925 -230 464 -222 1033 21 1503 226 437
648 751 1155 860 163 34 219 39 454 35 195 -3 241 -7 345 -29z"/>
<path d="M2420 3939 c-634 -61 -1129 -503 -1251 -1119 -26 -133 -35 -315 -20
-436 12 -94 35 -212 44 -220 2 -2 28 4 58 15 80 27 224 59 341 76 136 19 434
19 585 0 436 -56 820 -243 1257 -614 l100 -84 77 81 c175 185 296 418 346 670
24 122 24 394 -1 517 -56 282 -178 513 -374 709 -238 237 -534 372 -889 406
-120 11 -149 11 -273 -1z"/>
<path d="M1715 1973 c-105 -8 -202 -27 -289 -54 -53 -16 -96 -32 -96 -35 0
-12 36 -73 72 -122 175 -237 424 -415 717 -512 160 -54 277 -72 451 -73 251
-1 449 48 648 159 l64 36 -34 29 c-425 370 -784 533 -1248 569 -130 10 -199
10 -285 3z"/>
</g>
</svg>

After

(image error) Size: 1.9 KiB

19
public/site.webmanifest Normal file

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#363645",
"background_color": "#363645",
"display": "standalone"
}

@ -32,6 +32,22 @@ const FORM = gql`
title
}
}
submissions {
answers {
... on InputAnswer {
type
userInput
}
... on ChoiseAnswer {
type
userChoise
}
}
date
user {
name
}
}
title
}
}
@ -41,12 +57,21 @@ const USER = gql`
query User {
user {
email
id
name
forms {
id
submissions {
id
}
title
}
name
formSubmissions {
id
form {
title
id
}
}
}
}
`
@ -67,4 +92,12 @@ const CREATEFORM = gql`
}
`
export { LOGIN, FORM, USER, FORMSUBMIT, CREATEFORM }
const REGISTER = gql`
mutation Register($email: String!, $name: String!) {
register(email: $email, name: $name) {
success
}
}
`
export { LOGIN, FORM, USER, FORMSUBMIT, CREATEFORM, REGISTER }

@ -1,11 +1,17 @@
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client'
import {
ApolloClient,
createHttpLink,
from,
InMemoryCache,
} from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { onError } from '@apollo/client/link/error'
export * from './defs'
const httpLink = createHttpLink({
uri: process.env.GRAPHQL_URL || process.env.REACT_APP_GRAPHQL_URL || undefined
uri:
process.env.GRAPHQL_URL || process.env.REACT_APP_GRAPHQL_URL || undefined,
// fetchOptions: {
// mode: 'no-cors'
// }
@ -17,14 +23,25 @@ const authLink = setContext((_, { headers }) => {
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ''
}
authorization: token ? `Bearer ${token}` : '',
},
}
})
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.map(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
)
if (networkError) console.log(`[Network error]: ${networkError}`)
})
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
link: from([errorLink, authLink, httpLink]),
cache: new InMemoryCache(),
})
export default client

@ -15,7 +15,7 @@ type Form {
author: User
dateCreated: String!
id: Int!
questions: [Question!]!
questions: [Question!]
submissions: [FormSubmission!]
title: String!
}
@ -42,9 +42,11 @@ type InputQuestion implements Question {
}
type FormSubmission {
user: User
answers: [Answer!]!
date: String!
id: Int!
form: Form
}
interface Answer {
@ -77,6 +79,7 @@ type User {
forms: [Form!]
id: Int!
name: String!
formSubmissions: [FormSubmission!]
}
type serverAnswer {

@ -3,34 +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 Login from './Login'
import UserPage from './UserPage'
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 '../views/Register'
const App: React.FC = () => {
const userContext = useUser()
return (
<div className="App">
<ApolloProvider client={client}>
<Context.Provider value={userContext}>
<Router>
<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} />
</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

@ -0,0 +1,23 @@
import React from 'react'
import { Link } from 'react-router-dom'
import styles from './main.module.css'
interface ICardProps {
title: string
subtitle?: string
icon?: React.Component
iconCounter?: number
id: number
}
const Card: React.FC<ICardProps> = ({ title, subtitle, id }) => {
return (
<Link to={`/form/${id}`} className={styles.card}>
<h3>{title}</h3>
{subtitle ?? <h5>{subtitle}</h5>}
</Link>
)
}
export default Card

@ -0,0 +1,12 @@
.card {
display: block;
background-color: var(--accentColor);
padding: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border-radius: 5px;
box-shadow: 0 1px 6px 0 var(--accentShadowColor);
text-decoration: none;
color: var(--onAccentFontColor);
}

@ -1,156 +0,0 @@
import { ApolloError, useMutation } from '@apollo/client'
import { ChangeEvent, FormEvent, 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
}
]
export const useFormCreator = <T extends string>(
formatQuestionsToSubmit: FormatQuestionsToSubmitType
): UseFormCreatorHookTurple<T> => {
const [form, setState] = useState<Form<T>>({ title: '', questions: [] })
const [
doFormCreation,
{ error: submitError, data: submitData, loading: submitLoading },
] = useMutation<ICreateFormMutation, MutationCreateFormArgs>(CREATEFORM, {
variables: {
title: form.title,
questions: formatQuestionsToSubmit<T>(form.questions),
},
})
const formSubmit: FormSubmitType = (e) => {
e.preventDefault()
console.log({
title: form.title,
questions: formatQuestionsToSubmit<T>(form.questions),
})
doFormCreation()
}
const handleFormTitleChange: HandleFormTitleChangeType = (e) => {
const title = e.currentTarget.value
setState((prev) => ({ ...prev, title }))
}
const createQuestion: CreateQuestionType<T> = (type) => {
setState(({ title, questions }) => ({
title,
questions: questions.concat({ title: '', type, variants: [] }),
}))
}
const handleQuestionTitleChange: HandleQuestionTitleChangeType = (
questionNumber,
e
) => {
const questionTitle = e.currentTarget.value
setState(({ title, questions }) => ({
title,
questions: questions.map((el, index) =>
index === questionNumber ? { ...el, title: questionTitle } : el
),
}))
}
const handleAnswerVariantChange: HandleAnswerVariantChangeType = (
questionNumber,
variantNumber,
e
) => {
const variantText = e.currentTarget.value
setState(({ title, questions }) => ({
title,
questions: questions.map((question, questionIndex) =>
questionIndex === questionNumber
? {
...question,
variants: question.variants.map((variant, variantIndex) =>
variantIndex === variantNumber ? variantText : variant
),
}
: question
),
}))
}
const addVariant: AddVariantType = (questionNumber) => {
console.log()
setState(({ title, questions }) => ({
title,
questions: questions.map((el, index) => ({
...el,
variants:
index === questionNumber ? el.variants.concat('') : el.variants,
})),
}))
}
return [
form,
[formSubmit, { submitData, submitError, submitLoading }],
{
handleFormTitleChange,
addQuestion: createQuestion,
handleQuestionTitleChange,
handleAnswerVariantChange,
addVariant,
},
]
}

@ -1,129 +0,0 @@
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,
})),
}
)
)
}
const CreateForm: React.FC = () => {
const [
form,
[formSubmit, { submitData, submitError, submitLoading }],
{
addQuestion,
handleFormTitleChange,
handleQuestionTitleChange,
handleAnswerVariantChange,
addVariant,
},
] = useFormCreator<QuestionTypes>(formatQuestionsToSubmit)
return (
<>
<form onSubmit={formSubmit}>
<label>
Title:
<input
type="text"
name="Title"
value={form.title}
onChange={handleFormTitleChange}
/>
</label>
<fieldset>
<legend>Content</legend>
<ul>
{creationsArray.flatMap((questionType, index) =>
questionType.enabled
? [
<li key={index}>
<button
type="button"
onClick={() => addQuestion(questionType.type)}
>
{questionType.title}
</button>
</li>,
]
: []
)}
</ul>
<ul>
{form.questions.map((quesstion, questionIndex) => (
<li key={questionIndex}>
<p>{quesstion.type} question:</p>
<input
type="text"
name="questionTitle"
placeholder="Title"
value={quesstion.title}
onChange={(e) => handleQuestionTitleChange(questionIndex, e)}
/>
{(quesstion.type === 'CHECK' ||
quesstion.type === 'CHOOSE' ||
quesstion.type === 'SELECT') && (
<>
<ul>
{quesstion.variants.map((variant, variantIndex) => (
<li key={variantIndex}>
<input
placeholder="Variant"
type="text"
value={variant}
onChange={(e) =>
handleAnswerVariantChange(
questionIndex,
variantIndex,
e
)
}
/>
</li>
))}
</ul>
<button
type="button"
onClick={() => addVariant(questionIndex)}
>
+
</button>
</>
)}
</li>
))}
</ul>
</fieldset>
{submitLoading ? 'Loading...' : <input type="submit" value="Submit" />}
</form>
{submitData &&
submitData.createForm &&
submitData.createForm.success &&
'Successfully uploaded'}
{submitError && submitError.message}
</>
)
}
export default CreateForm

@ -1,28 +0,0 @@
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]
}

@ -1,171 +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,
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

@ -1,18 +1,20 @@
import React from 'react'
interface IProps {
import styles from './main.module.css'
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
return (
<div>
<div className={styles.variantsList}>
{variants.map((el, index) => (
<label key={index}>
<input
@ -25,6 +27,7 @@ const Lists: React.FC<IProps> = ({ variants, name, type, onChange }) => {
type={inputType}
name={name}
value={el.text}
className={styles.selector}
/>
{el.text}
</label>

@ -0,0 +1,14 @@
.selector {
margin-right: 0.5rem;
}
.variantsList {
display: flex;
gap: 0.5rem;
}
@media (orientation: portrait) {
.variantsList {
flex-direction: column;
}
}

@ -1,35 +0,0 @@
import { useMutation } from '@apollo/client'
import React, { ChangeEvent, FormEvent, useState } from 'react'
import { Redirect } from 'react-router-dom'
import { LOGIN } from '../../apollo'
const Login: React.FC = () => {
const [email, setEmail] = useState<string>('')
const [doLogin, { error, data }] = useMutation(LOGIN)
const handleFormSubmit = async (e: FormEvent) => {
e.preventDefault()
try {
await doLogin({ variables: { email } })
} catch (err) {}
}
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setEmail(e.currentTarget.value)
}
return (
<div>
<form onSubmit={handleFormSubmit}>
<input type="text" onChange={handleInputChange} />
<input type="submit" value="Login" />
</form>
{error && error.message}
{data && data.login && data.login.success && <Redirect to="/" />}
</div>
)
}
export default Login

@ -0,0 +1,15 @@
import React from 'react'
import { Link } from 'react-router-dom'
import styles from './main.module.css'
import logo from './logo.svg'
const Navbar: React.FC = () => (
<nav className={styles.nav}>
<Link to="/" className={styles.logo}>
<img src={logo} alt="" className={styles.logo} />
</Link>
</nav>
)
export default Navbar

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="512"
height="512"
viewBox="0 0 135.46666 135.46666"
version="1.1"
id="svg8"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
sodipodi:docname="logo.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="574.79706"
inkscape:cy="560"
inkscape:document-units="px"
inkscape:current-layer="text845"
inkscape:document-rotation="0"
showgrid="false"
units="px"
inkscape:window-width="1366"
inkscape:window-height="744"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:pagecheckerboard="true"
borderlayer="false" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="fill:#6df577;fill-opacity:1;stroke:#000000;stroke-width:0.184432;stroke-miterlimit:4;stroke-dasharray:none"
id="path18"
cx="67.73333"
cy="67.73333"
r="67.641113" />
<g
aria-label="Q"
id="text845"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:116.417px;line-height:1.25;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264583">
<path
d="m 112.61208,104.23006 c -0.6985,0.11642 -1.397,0.11642 -1.74625,0.11642 -3.60893,0 -6.86861,-1.28059 -11.176035,-4.423848 9.313355,-7.916356 14.319295,-19.092388 14.319295,-32.363926 0,-25.495323 -19.55806,-44.354877 -45.984719,-44.354877 -26.659494,0 -46.566801,19.092388 -46.566801,44.587711 0,24.796821 20.605809,44.47129 46.450384,44.47129 9.546194,0 18.510303,-2.56117 25.844574,-7.45068 5.937267,5.47159 10.826782,7.33427 18.859552,7.45068 z M 35.07836,85.603341 c 4.65668,-1.862672 8.032773,-2.444757 13.504372,-2.444757 14.668542,0 25.728157,4.540263 38.184776,15.949129 -5.82085,3.608927 -11.525283,5.238767 -18.859554,5.238767 -8.498442,0 -16.414798,-2.67759 -22.817733,-7.567107 -4.65668,-3.49251 -7.799939,-7.101437 -10.011861,-11.176032 z M 31.469433,78.26907 c -0.931336,-4.307429 -1.280587,-6.519352 -1.280587,-9.779028 0,-21.187894 16.298379,-37.369857 37.602691,-37.369857 21.187894,0 37.486273,15.832712 37.486273,36.438521 0,10.244696 -3.84176,18.859554 -11.758116,26.659493 -4.074596,-3.49251 -7.450688,-6.170101 -10.943198,-8.731275 -9.662611,-6.868603 -20.489392,-10.244696 -32.713178,-10.244696 -6.635768,0 -11.758117,0.814919 -18.393885,3.026842 z"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:116.417px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;stroke-width:0.264583"
id="path1581" />
</g>
</g>
</svg>

After

(image error) Size: 3.8 KiB

@ -0,0 +1,16 @@
.nav {
display: flex;
width: 100vw;
height: 4rem;
align-items: center;
justify-content: center;
background-color: var(--containerColor);
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
}
.logo {
display: block;
height: 2.7rem;
width: 2.7rem;
}

@ -0,0 +1,33 @@
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)
setAnswers(state)
},
[setAnswers]
)
return { answers, changeAnswer, setInitialState }
}

@ -0,0 +1,130 @@
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'
import styles from './main.module.css'
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} className={styles.question}>
<label
className={styles.questionTitle}
htmlFor={el.title.replace(' ', '_')}
>
{el.title}
</label>
<input
className={styles.textInput}
placeholder="Input"
name={el.title.replace(' ', '_')}
onChange={(e) =>
changeAnswer(el.number)(e.currentTarget.value)
}
type="text"
/>
</li>
)
if (el.__typename === 'ChoisesQuestion')
return (
<li key={el.number} className={styles.question}>
<label className={styles.questionTitle} htmlFor={el.title}>
{el.title}
</label>
{el.type === 'SELECT' ? (
<select
className={styles.select}
onChange={(e) => {
const selectValue = el.variants.findIndex(
(val) => val.text === e.currentTarget.value
)
changeAnswer(el.number)(selectValue)
}}
name={el.title.replace(' ', '_')}
>
{el.variants.map((option, index) => (
<option key={index}>{option.text}</option>
))}
</select>
) : (
<Lists
variants={el.variants}
onChange={changeAnswer(el.number)}
name={el.title.replace(' ', '_')}
type={el.type}
/>
)}
</li>
)
return <li className={styles.question}>Unknown question type</li>
})}
</ul>
{submitLoading ? (
<p>Uploading...</p>
) : (
<input className={styles.button} type="submit" />
)}
</form>
{submitError && <p>{submitError.message}</p>}
{submitData?.formSubmit.success && <p>Successfully uploaded</p>}
</>
)
}
export default QuestionsForm

@ -0,0 +1,53 @@
.question {
list-style: none;
padding-bottom: 1rem;
}
.questionTitle {
padding-bottom: 0.5rem;
display: block;
font-weight: bold;
font-size: 1.3rem;
}
.textInput {
height: 2.3rem;
border-radius: 100vh;
border: none;
outline: none;
font-size: 1.2rem;
padding: 0 0.7rem;
width: 100%;
border-bottom: 0.15rem var(--containerColor) solid;
transition: border 0.1s;
}
.textInput:focus {
border-bottom-width: 0rem;
border-top: 0.15rem var(--containerColor) solid;
}
.select {
padding: 0.5rem;
background: var(--accentColor);
border-radius: 20px;
color: #ffffff;
}
.button {
height: 2.3rem;
border-radius: 100vh;
border: none;
outline: none;
font-size: 1.2rem;
padding: 0 0.7rem;
width: 100%;
cursor: pointer;
background-color: var(--accentColor);
color: var(--onAccentFontColor);
box-shadow: 0 1px 6px 0 var(--accentShadowColor);
}
.button:active {
box-shadow: none;
}

@ -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[]

@ -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 []
})

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 32.055 32.055" xml:space="preserve">
<g>
<path style="fill:#363645" d="M3.968,12.061C1.775,12.061,0,13.835,0,16.027c0,2.192,1.773,3.967,3.968,3.967c2.189,0,3.966-1.772,3.966-3.967
C7.934,13.835,6.157,12.061,3.968,12.061z M16.233,12.061c-2.188,0-3.968,1.773-3.968,3.965c0,2.192,1.778,3.967,3.968,3.967
s3.97-1.772,3.97-3.967C20.201,13.835,18.423,12.061,16.233,12.061z M28.09,12.061c-2.192,0-3.969,1.774-3.969,3.967
c0,2.19,1.774,3.965,3.969,3.965c2.188,0,3.965-1.772,3.965-3.965S30.278,12.061,28.09,12.061z" />
</g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
</svg>

After

(image error) Size: 969 B

@ -0,0 +1,68 @@
import React from 'react'
import {
FormSubmission,
InputAnswer,
ChoiseAnswer,
ChoisesQuestion,
Question,
} from '../../apollo/typeDefs.gen'
import emptyIcon from './empty.svg'
import styles from './main.module.css'
import { getDateCreated } from '../../utils'
interface ISubmissionListProps {
submissions: FormSubmission[]
questions: Question[]
}
const SubmissionList: React.FC<ISubmissionListProps> = ({
submissions,
questions,
}) => {
return submissions.length > 0 ? (
<ul className={styles.container}>
{submissions.map((submission, submissionIndex) => (
<li className={styles.listItem} key={submissionIndex}>
<h2 className={styles.itemHeader}>
{`User ${
submission.user ? submission.user.name : 'No submitter'
} submitted on ${getDateCreated(submission.date)}:`}
</h2>
<ul>
{submission.answers.map(
(answer: InputAnswer | ChoiseAnswer, answerIndex) => (
<li key={answerIndex}>
<div className={styles.question}>
<h3>{questions[answerIndex].title}</h3>
{answer.__typename === 'ChoiseAnswer' && (
<p>
{
(questions[answerIndex] as ChoisesQuestion).variants[
answer.userChoise
].text
}
</p>
)}
{answer.__typename === 'InputAnswer' && (
<p>{answer.userInput}</p>
)}
</div>
</li>
)
)}
</ul>
</li>
))}
</ul>
) : (
<div className={styles.emptyContainer}>
<div className={styles.emptyIconContainer}>
<img className={styles.emptyIcon} src={emptyIcon} alt="Empty" />
</div>
<h3>No submissions yet :(</h3>
</div>
)
}
export default SubmissionList

@ -0,0 +1,48 @@
.container {
display: flex;
flex-direction: column;
gap: 2.3rem;
}
.listItem {
list-style-type: none;
border: 0.1rem var(--containerColor) solid;
border-radius: 20px;
padding: 2.3rem;
}
.itemHeader {
text-decoration: underline wavy;
text-underline-offset: 0.5rem;
padding-bottom: 2.3rem;
}
.question {
display: flex;
gap: 0.5rem;
padding: 0.5rem 0;
}
.emptyContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-grow: 1;
}
.emptyIconContainer {
height: 3.5rem;
overflow: hidden;
}
.emptyIcon {
height: 7rem;
margin-top: -1.75rem;
}
@media (orientation: portrait) {
.question {
flex-direction: column;
}
}

@ -1,41 +0,0 @@
import { useQuery } from '@apollo/client'
import React from 'react'
import { USER } from '../../apollo'
import { User } from '../../apollo/typeDefs.gen'
interface UserQuery {
user: User
}
const UserPage: React.FC = () => {
const { data, error, loading } = useQuery<UserQuery>(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

@ -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)

@ -1,13 +1,3 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
.App {
height: 100vh;
}

22
src/types.ts Normal file

@ -0,0 +1,22 @@
import {
InputQuestion,
ChoisesQuestion,
InputAnswer,
ChoiseAnswer,
Form,
User,
} from './apollo/typeDefs.gen'
export type QuestionT = InputQuestion | ChoisesQuestion
export type AnswerT = InputAnswer | ChoiseAnswer
export interface IFormQuery {
form: Form
}
export type GetDateCreatedFT = (dateString: string) => string
export interface IUserQuery {
user: User
}

7
src/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,5 +1,6 @@
import React from 'react'
import { Redirect } from 'react-router-dom'
import { useURLQuery } from '../../hooks'
const Authorize: React.FC = () => {

@ -0,0 +1,125 @@
import { useMutation } from '@apollo/client'
import { useState } from 'react'
import { CREATEFORM } from '../../apollo'
import { MutationCreateFormArgs } from '../../apollo/typeDefs.gen'
import {
FormatQuestionsToSubmitFT,
UseFormCreatorHookTurpleT,
FormT,
ICreateFormMutation,
FormSubmitT,
HandleFormTitleChangeFT,
CreateQuestionFT,
HandleQuestionTitleChangeFT,
HandleAnswerVariantChangeFT,
AddVariantFT,
RemoveQuestionFT,
} from './types'
const initialState = { title: '', questions: [] }
export const useFormCreator = <T extends string>(
formatQuestionsToSubmit: FormatQuestionsToSubmitFT
): UseFormCreatorHookTurpleT<T> => {
const [form, setState] = useState<FormT<T>>(initialState)
const [
doFormCreation,
{ error: submitError, data: submitData, loading: submitLoading },
] = useMutation<ICreateFormMutation, MutationCreateFormArgs>(CREATEFORM, {
variables: {
title: form.title,
questions: formatQuestionsToSubmit<T>(form.questions),
},
})
const formSubmit: FormSubmitT = async (e) => {
e.preventDefault()
try {
await doFormCreation()
} catch (err) {}
}
const handleFormTitleChange: HandleFormTitleChangeFT = (e) => {
const title = e.currentTarget.value
setState((prev) => ({ ...prev, title }))
}
const createQuestion: CreateQuestionFT<T> = (type) => {
setState(({ title, questions }) => ({
title,
questions: questions.concat({ title: '', type, variants: [''] }),
}))
}
const removeQuestion: RemoveQuestionFT = (number) => {
setState(({ title, questions }) => ({
title,
questions: questions.filter((_, index) => index !== number),
}))
}
const handleQuestionTitleChange: HandleQuestionTitleChangeFT = (
questionNumber,
e
) => {
const questionTitle = e.currentTarget.value
setState(({ title, questions }) => ({
title,
questions: questions.map((el, index) =>
index === questionNumber ? { ...el, title: questionTitle } : el
),
}))
}
const handleAnswerVariantChange: HandleAnswerVariantChangeFT = (
questionNumber,
variantNumber,
e
) => {
const variantText = e.currentTarget.value
setState(({ title, questions }) => ({
title,
questions: questions.map((question, questionIndex) =>
questionIndex === questionNumber
? {
...question,
variants: question.variants.map((variant, variantIndex) =>
variantIndex === variantNumber ? variantText : variant
),
}
: question
),
}))
}
const addVariant: AddVariantFT = (questionNumber) => {
setState(({ title, questions }) => ({
title,
questions: questions.map((el, index) => ({
...el,
variants:
index === questionNumber ? el.variants.concat('') : el.variants,
})),
}))
}
const resetForm = () => {
setState(initialState)
}
return [
form,
[formSubmit, { submitData, submitError, submitLoading }],
{
handleFormTitleChange,
addQuestion: createQuestion,
removeQuestion,
handleQuestionTitleChange,
handleAnswerVariantChange,
addVariant,
},
resetForm,
]
}

@ -0,0 +1,154 @@
import React from 'react'
import { QuestionTypes } from './types'
import { useFormCreator } from './hooks'
import { creationsArray, formatQuestionsToSubmit } from './utils'
import styles from './main.module.css'
const CreateForm: React.FC = () => {
const [
form,
[formSubmit, { submitData, submitError, submitLoading }],
{
addQuestion,
removeQuestion,
handleFormTitleChange,
handleQuestionTitleChange,
handleAnswerVariantChange,
addVariant,
},
resetForm,
] = useFormCreator<QuestionTypes>(formatQuestionsToSubmit)
return (
<div className={styles.container}>
<form
onSubmit={(e) => {
formSubmit(e)
resetForm()
}}
>
<div className={styles.pageHeader}>
<h1 className={styles.pageTitle}>Create form</h1>
<input
className={styles.textInput}
type="text"
name="Title"
placeholder="title"
value={form.title}
onChange={handleFormTitleChange}
required
/>
</div>
<div className={styles.mainFormContainer}>
<fieldset className={styles.mainForm}>
<div className={styles.mainFormTop} />
<legend className={styles.fieldsetTitle}>Content</legend>
<ul>
{form.questions.map((quesstion, questionIndex) => (
<li className={styles.questionToCreateLI} key={questionIndex}>
<div className={styles.questionHeader}>
<h3 className={styles.questionTitle}>
{quesstion.type} question:
</h3>
<input
className={[styles.textInput, styles.fullWidth].join(' ')}
required
type="text"
name="questionTitle"
placeholder="Title"
value={quesstion.title}
onChange={(e) =>
handleQuestionTitleChange(questionIndex, e)
}
/>
<button
className={styles.button}
onClick={(e) => {
e.preventDefault()
removeQuestion(questionIndex)
}}
>
x
</button>
</div>
{(quesstion.type === 'CHECK' ||
quesstion.type === 'CHOOSE' ||
quesstion.type === 'SELECT') && (
<>
<h4 className={styles.variantsHeader}>Variants</h4>
<ul>
{quesstion.variants.map((variant, variantIndex) => (
<li key={variantIndex}>
<input
className={[
styles.textInput,
styles.fullWidth,
styles.variantText,
].join(' ')}
required
placeholder="Variant"
type="text"
value={variant}
onChange={(e) =>
handleAnswerVariantChange(
questionIndex,
variantIndex,
e
)
}
/>
</li>
))}
</ul>
<button
type="button"
onClick={() => addVariant(questionIndex)}
className={[styles.button, styles.addVariant].join(' ')}
>
+
</button>
</>
)}
</li>
))}
</ul>
<ul>
{creationsArray.flatMap((questionType, index) =>
questionType.enabled
? [
<li className={styles.questionCreatorLI} key={index}>
<button
className={styles.button}
type="button"
onClick={() => addQuestion(questionType.type)}
>
+ {questionType.title}
</button>
</li>,
]
: []
)}
</ul>
</fieldset>
</div>
<div className={styles.submitContainer}>
{submitLoading ? (
'Loading...'
) : (
<input
className={[styles.button, styles.submitButton].join(' ')}
type="submit"
value="Submit"
/>
)}
</div>
</form>
{submitData?.createForm.success && 'Successfully uploaded'}
{submitError && submitError.message}
</div>
)
}
export default CreateForm

@ -0,0 +1,150 @@
.container {
height: calc(100vh - 4rem);
}
.container form {
height: 100%;
display: flex;
flex-direction: column;
}
.pageHeader {
display: flex;
gap: 1rem;
padding: 2.3rem;
}
.pageTitle {
margin-top: -0.3rem;
}
.textInput {
height: 2.3rem;
border-radius: 100vh;
border: none;
display: inline;
outline: none;
font-size: 1.2rem;
padding: 0 0.7rem;
border-bottom: 0.15rem var(--containerColor) solid;
transition: border 0.1s;
}
.textInput:focus {
border-bottom-width: 0rem;
border-top: 0.15rem var(--containerColor) solid;
}
.mainFormContainer {
margin-top: 2.3rem;
padding: 2.3rem;
padding-top: 0;
flex-grow: 1;
position: relative;
background: #ffffff;
}
.mainFormTop {
top: -2.3rem;
left: 0rem;
position: absolute;
height: 2.3rem;
width: 100%;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
background: #ffffff;
}
.mainForm {
border: none;
}
.fieldsetTitle {
font-size: 1.4rem;
font-weight: bold;
padding-bottom: 2rem;
}
.questionCreatorLI {
list-style: none;
display: inline;
padding-right: 0.5rem;
}
.button {
height: 2.3rem;
border-radius: 100vh;
border: none;
outline: none;
font-size: 1.2rem;
padding: 0 0.7rem;
cursor: pointer;
background-color: var(--accentColor);
color: var(--onAccentFontColor);
box-shadow: 0 1px 6px 0 var(--accentShadowColor);
}
.button:active {
box-shadow: none;
}
.questionToCreateLI {
margin-bottom: 2.3rem;
list-style: none;
border: 0.1rem var(--containerColor) solid;
border-radius: 20px;
padding: 2.3rem;
}
.questionHeader {
display: grid;
grid-template-columns: auto 1fr 2.3rem;
gap: 0.5rem;
}
.questionTitle {
line-height: 2.3rem;
}
.fullWidth {
width: 100%;
}
.variantsHeader {
margin: 1rem 0;
}
.variantText {
margin: 0.5rem;
}
.addVariant {
width: 2.3rem;
margin: auto;
display: block;
line-height: 2.3rem;
}
.submitContainer {
padding: 2.3rem;
background-color: #ffffff;
}
.submitButton {
width: 100%;
}
@media (orientation: portrait) {
.pageHeader {
flex-direction: column;
gap: 0.5rem;
}
.pageTitle {
margin-top: 0;
}
.questionHeader {
grid-template-columns: 1fr;
}
}

@ -0,0 +1,67 @@
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 RemoveQuestionFT = (number: number) => 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>
removeQuestion: RemoveQuestionFT
handleQuestionTitleChange: HandleQuestionTitleChangeFT
handleAnswerVariantChange: HandleAnswerVariantChangeFT
addVariant: AddVariantFT
},
() => void
]
export type QuestionTypes = 'CHECK' | 'INPUT' | 'CHOOSE' | 'SELECT'

@ -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,
})),
}
)
)

@ -0,0 +1,7 @@
import { useParams } from 'react-router-dom'
export const useId = () => {
const { id: idString } = useParams<{ id: string }>()
return parseInt(idString)
}

@ -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} />
{form.submissions ? (
<>
<h1 className={styles.mainHeader}>Submissions</h1>
<SubmissionList
submissions={form.submissions}
questions={form.questions!}
/>
</>
) : (
<>
<h1 className={styles.mainHeader}>Questions</h1>
<QuestionsForm
formId={id}
questions={data!.form.questions!}
refetchQuestions={refetchForm}
/>
</>
)}
</main>
</div>
)
}
export default DoForm

@ -0,0 +1,55 @@
.container {
display: flex;
flex-direction: column;
min-height: calc(100vh - 4rem);
}
.header {
padding: 2.3rem;
display: grid;
gap: 1rem;
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;
display: flex;
flex-direction: column;
}
.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;
}
.mainHeader {
padding-bottom: 2.3rem;
}

86
src/views/Home/index.tsx Normal file

@ -0,0 +1,86 @@
import React from 'react'
import { useQuery } from '@apollo/client'
import { generateFromString } from 'generate-avatar'
import { Redirect, useHistory, Link } from 'react-router-dom'
import Card from '../../components/Card'
import { USER } from '../../apollo'
import { QueryUserArgs } from '../../apollo/typeDefs.gen'
import styles from './main.module.css'
import { IUserQuery } from '../../types'
import { logOut } from './utils'
const Home: React.FC = () => {
let { data, error, loading, refetch } = useQuery<IUserQuery, QueryUserArgs>(
USER
)
const history = useHistory()
if (loading) return <p>Loading...</p>
if (error?.message === 'Authorization required')
return <Redirect to="/login" />
if (error) return <p>{error.message}</p>
const { user } = data!
const { forms, formSubmissions } = user
return (
<div className={styles.container}>
<div className={styles.userPad}>
<div className={styles.userCard}>
<img
className={styles.userPic}
src={`data:image/svg+xml;utf8,${generateFromString(user.email)}`}
alt="Userpic"
/>
<h1>{user.name}</h1>
<button
className={styles.button}
onClick={() => logOut(refetch, history)}
>
Log out
</button>
</div>
</div>
<div className={styles.leftPad}>
<h1>My forms</h1>
<ul className={styles.cardList}>
{forms!.map((form) => (
<li key={form.id}>
<Card title={form.title} id={form.id} />
</li>
))}
<Link className={styles.createNew} to="/create">
<span>Create new</span> <h3>+</h3>
</Link>
</ul>
</div>
<div className={styles.rightPad}>
<h1>My submissions</h1>
<ul className={styles.cardList}>
{formSubmissions
?.filter((submission) => Boolean(submission.form))
.map((submission) => (
<li key={submission.id}>
<Card
title={submission.form!.title}
id={submission.form!.id}
subtitle={
submission.user ? 'by ' + submission.user.name : undefined
}
/>
</li>
))}
</ul>
</div>
</div>
)
}
export default Home

@ -0,0 +1,132 @@
.container {
display: grid;
min-height: calc(100vh - 4rem);
grid-template-columns: 5fr 3fr;
grid-template-rows: auto 1fr;
grid-template-areas:
'left user'
'left right';
padding: 2.3rem;
}
.leftPad {
display: flex;
grid-area: left;
flex-direction: column;
align-items: center;
gap: 2.3rem;
padding: 0 2.3rem;
}
.leftPad > * {
width: 100%;
}
.cardList {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.7rem;
overflow-y: auto;
}
.createNew:hover {
border-color: var(--containerColor);
color: var(--containerColor);
}
.createNew {
padding: 1rem;
border-radius: 0.5rem;
border: 0.1rem var(--accentColor) solid;
text-decoration: none;
color: var(--accentColor);
display: flex;
gap: 0.5rem;
justify-content: center;
}
.createNew span {
font-weight: bold;
line-height: 1.17em;
}
.createNew h3 {
line-height: 1.17em;
}
.rightPad {
display: flex;
flex-direction: column;
align-items: center;
gap: 2.3rem;
padding-top: 2.3rem;
grid-area: right;
}
.rightPad > * {
width: 30vw;
}
.userPad {
grid-area: user;
padding: 0 2.3rem;
}
.userCard {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
background-color: #ffffff;
padding: 2.3rem;
padding-bottom: 1.6rem;
border-radius: 20px;
}
.userPic {
border-radius: 20px;
width: 100%;
}
.button {
height: 2.3rem;
border-radius: 100vh;
border: none;
outline: none;
font-size: 1.2rem;
padding: 0 0.7rem;
cursor: pointer;
width: 100%;
background-color: var(--accentColor);
color: var(--onAccentFontColor);
box-shadow: 0 1px 6px 0 var(--accentShadowColor);
}
.button:active {
box-shadow: none;
}
@media (orientation: portrait) {
.container {
grid-template-columns: auto;
grid-template-areas: 'user' 'left' 'right';
gap: 2.3rem;
}
.userPad {
padding: 0;
}
.leftPad {
padding: 0;
}
.rightPad {
padding-top: 0;
}
.rightPad > * {
width: 100%;
}
}

11
src/views/Home/types.ts Normal file

@ -0,0 +1,11 @@
import { QueryUserArgs } from '../../apollo/typeDefs.gen'
import { ApolloQueryResult } from '@apollo/client'
import { IUserQuery } from '../../types'
import { History } from 'history'
export type LogOutFT = (
refetch: (
variables?: Partial<QueryUserArgs> | undefined
) => Promise<ApolloQueryResult<IUserQuery>>,
history: History
) => void

7
src/views/Home/utils.ts Normal file

@ -0,0 +1,7 @@
import { LogOutFT } from './types'
export const logOut: LogOutFT = (refetch, history) => {
localStorage.removeItem('token')
refetch()
history.push('/')
}

77
src/views/Login/index.tsx Normal file

@ -0,0 +1,77 @@
import { useMutation } from '@apollo/client'
import React, { ChangeEvent, FormEvent, useState } from 'react'
import { LOGIN } from '../../apollo'
import { MutationLoginArgs, ServerAnswer } from '../../apollo/typeDefs.gen'
import styles from './main.module.css'
import meme from './meme.png'
import { Link } from 'react-router-dom'
interface ILoginMutation {
login: ServerAnswer
}
const Login: React.FC = () => {
const [email, setEmail] = useState<string>('')
const [doLogin, { error, data, loading }] = useMutation<
ILoginMutation,
MutationLoginArgs
>(LOGIN)
const handleFormSubmit = async (e: FormEvent) => {
e.preventDefault()
try {
await doLogin({ variables: { email } })
} catch (err) {}
}
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setEmail(e.currentTarget.value)
}
return (
<div className={styles.container}>
<div className={styles.formCard}>
<img
className={styles.img}
src={meme}
alt="You can't forget password if you don't have it"
/>
<form className={styles.form} onSubmit={handleFormSubmit}>
{data?.login.success ? (
<div>
<h1>
You will get <span className={styles.focus}>login link</span>{' '}
<br /> in your <span className={styles.focus}>mailbox</span>
</h1>
</div>
) : (
<>
<h1 className={styles.header}>
Log In / <Link to="/register">Register</Link>
</h1>
<input
required
className={styles.input}
name="email"
id="email"
type="email"
placeholder="email"
onChange={handleInputChange}
/>
{loading ? (
'Loading...'
) : (
<input type="submit" value="Login" className={styles.button} />
)}
{error && <p className={styles.errorMsg}>{error.message}</p>}
</>
)}
</form>
</div>
</div>
)
}
export default Login

@ -0,0 +1,94 @@
.container {
display: flex;
min-height: calc(100% - 4rem);
align-items: center;
justify-content: center;
padding: 2.3rem;
}
.formCard {
min-width: 50vw;
background-color: #ffffff;
border-radius: 20px;
display: grid;
grid-template-columns: 1fr 1fr;
max-width: 90vw;
overflow: hidden;
}
.img {
height: 70vh;
object-fit: contain;
border-radius: 20px;
}
.form {
padding: 20px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 5px;
}
.form input {
height: 2.3rem;
border-radius: 100vh;
border: none;
outline: none;
font-size: 1.2rem;
padding: 0 0.7rem;
width: 100%;
}
.form label {
padding: 0 0.7rem;
}
.header {
text-align: center;
}
.header a {
color: var(--accentColor);
}
.input {
border-bottom: 0.15rem var(--containerColor) solid !important;
transition: border 0.1s;
}
.input:focus {
border-bottom-width: 0rem !important;
border-top: 0.15rem var(--containerColor) solid !important;
}
.button {
cursor: pointer;
background-color: var(--accentColor);
color: var(--onAccentFontColor);
box-shadow: 0 1px 6px 0 var(--accentShadowColor);
}
.button:active {
box-shadow: none;
}
.errorMsg {
color: red;
}
.focus {
color: var(--accentColor);
}
@media (orientation: portrait) {
.formCard {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
min-height: calc(100% - 4rem);
}
.img {
height: initial;
width: 100%;
}
}

BIN
src/views/Login/meme.png Normal file

Binary file not shown.

After

(image error) Size: 87 KiB

@ -0,0 +1,73 @@
import { useMutation } from '@apollo/client'
import React, { FormEvent } from 'react'
import { Redirect, Link } from 'react-router-dom'
import { REGISTER } from '../../apollo'
import { MutationRegisterArgs, ServerAnswer } from '../../apollo/typeDefs.gen'
import styles from '../Login/main.module.css'
import meme from './meme.jpg'
interface IRegisterMutation {
register: ServerAnswer
}
const Register: React.FC = () => {
const [doRegister, { data, loading, error }] = useMutation<
IRegisterMutation,
MutationRegisterArgs
>(REGISTER)
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
try {
await doRegister({
variables: {
email: formData.get('email') as string,
name: formData.get('name') as string,
},
})
} catch (err) {}
}
return (
<div className={styles.container}>
<div className={styles.formCard}>
<img
className={styles.img}
src={meme}
alt='Questionform says: "Is mailbox a password?"'
/>
<form className={styles.form} onSubmit={handleSubmit}>
<h1 className={styles.header}>
Register / <Link to="/login">Log In</Link>
</h1>
<input
required
className={styles.input}
type="email"
name="email"
placeholder="email"
/>
<input
required
className={styles.input}
type="text"
name="name"
placeholder="username"
/>
{loading ? (
'Loading...'
) : (
<input className={styles.button} type="submit" value="Submit" />
)}
{error && <p className={styles.errorMsg}>{error.message}</p>}
{data?.register.success && <Redirect to="/" />}
</form>
</div>
</div>
)
}
export default Register

BIN
src/views/Register/meme.jpg Normal file

Binary file not shown.

After

(image error) Size: 61 KiB

@ -13,7 +13,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
"jsx": "react",
"plugins": [{ "name": "typescript-plugin-css-modules" }]
},
"include": ["src"]
}

10755
yarn.lock

File diff suppressed because it is too large Load Diff