Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
540c93236a | |||
|
5a4795a66e | ||
65a6ca3e03 | |||
2634f64d1f | |||
53ea63b35f | |||
19fa7fc4e5 | |||
6ef34c6ab8 | |||
85d7e48e0d | |||
c94dab4a9b | |||
eaba1665ce | |||
228ad884bc | |||
743742b757 | |||
|
46ffa05e14 | ||
|
c434429ed5 | ||
6ff944f0ee | |||
|
5be9ed5197 | ||
1060ea4e41 | |||
3f580213fa | |||
6c60520aae | |||
b8450525a5 |
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
5
.gitignore
vendored
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
@ -23,4 +24,6 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
*.local*
|
*.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-dom": "^16.9.0",
|
||||||
"@types/react-router-dom": "^5.1.6",
|
"@types/react-router-dom": "^5.1.6",
|
||||||
"@types/validator": "^13.1.0",
|
"@types/validator": "^13.1.0",
|
||||||
|
"generate-avatar": "^1.4.6",
|
||||||
"graphql": "^15.3.0",
|
"graphql": "^15.3.0",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
@ -19,7 +20,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"codegen": "graphql-codegen --config codegen.yml"
|
"codegen": "graphql-codegen --config codegen.yml",
|
||||||
|
"serve": "serve -s build"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
@ -38,6 +40,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^1.17.10",
|
"@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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
public/apple-touch-icon.png
Normal file
After Width: | Height: | 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
After Width: | Height: | Size: 959 B |
BIN
public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
public/favicon.ico
Normal file
After Width: | Height: | 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">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="QuestionForm is an open source alternative to Google Forms"
|
content="QuestionForm is an open source alternative to Google Forms"
|
||||||
/>
|
/>
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<link
|
||||||
<!--
|
rel="apple-touch-icon"
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
sizes="180x180"
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
href="%PUBLIC_URL%/apple-touch-icon.png"
|
||||||
-->
|
/>
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link
|
||||||
<!--
|
rel="icon"
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
type="image/png"
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
sizes="32x32"
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
href="%PUBLIC_URL%/favicon-32x32.png"
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
/>
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
<link
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
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>
|
<title>QuestionForm</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -8,18 +8,18 @@
|
|||||||
"type": "image/x-icon"
|
"type": "image/x-icon"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "logo192.png",
|
"src": "android-chrome-192x192.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "logo512.png",
|
"src": "android-chrome-512x512.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "512x512"
|
"sizes": "512x512"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": ".",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#6df577",
|
||||||
"background_color": "#ffffff"
|
"background_color": "#363645"
|
||||||
}
|
}
|
||||||
|
BIN
public/mstile-150x150.png
Normal file
After Width: | Height: | Size: 11 KiB |
34
public/safari-pinned-tab.svg
Normal file
@ -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 Width: | Height: | 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
|
title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
submissions {
|
||||||
|
answers {
|
||||||
|
... on InputAnswer {
|
||||||
|
type
|
||||||
|
userInput
|
||||||
|
}
|
||||||
|
... on ChoiseAnswer {
|
||||||
|
type
|
||||||
|
userChoise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
date
|
||||||
|
user {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
title
|
title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,12 +57,21 @@ const USER = gql`
|
|||||||
query User {
|
query User {
|
||||||
user {
|
user {
|
||||||
email
|
email
|
||||||
id
|
|
||||||
name
|
|
||||||
forms {
|
forms {
|
||||||
id
|
id
|
||||||
|
submissions {
|
||||||
|
id
|
||||||
|
}
|
||||||
title
|
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 { setContext } from '@apollo/client/link/context'
|
||||||
|
import { onError } from '@apollo/client/link/error'
|
||||||
|
|
||||||
export * from './defs'
|
export * from './defs'
|
||||||
|
|
||||||
const httpLink = createHttpLink({
|
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: {
|
// fetchOptions: {
|
||||||
// mode: 'no-cors'
|
// mode: 'no-cors'
|
||||||
// }
|
// }
|
||||||
@ -17,14 +23,25 @@ const authLink = setContext((_, { headers }) => {
|
|||||||
return {
|
return {
|
||||||
headers: {
|
headers: {
|
||||||
...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({
|
const client = new ApolloClient({
|
||||||
link: authLink.concat(httpLink),
|
link: from([errorLink, authLink, httpLink]),
|
||||||
cache: new InMemoryCache()
|
cache: new InMemoryCache(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export default client
|
export default client
|
||||||
|
@ -15,7 +15,7 @@ type Form {
|
|||||||
author: User
|
author: User
|
||||||
dateCreated: String!
|
dateCreated: String!
|
||||||
id: Int!
|
id: Int!
|
||||||
questions: [Question!]!
|
questions: [Question!]
|
||||||
submissions: [FormSubmission!]
|
submissions: [FormSubmission!]
|
||||||
title: String!
|
title: String!
|
||||||
}
|
}
|
||||||
@ -42,9 +42,11 @@ type InputQuestion implements Question {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FormSubmission {
|
type FormSubmission {
|
||||||
|
user: User
|
||||||
answers: [Answer!]!
|
answers: [Answer!]!
|
||||||
date: String!
|
date: String!
|
||||||
id: Int!
|
id: Int!
|
||||||
|
form: Form
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Answer {
|
interface Answer {
|
||||||
@ -77,6 +79,7 @@ type User {
|
|||||||
forms: [Form!]
|
forms: [Form!]
|
||||||
id: Int!
|
id: Int!
|
||||||
name: String!
|
name: String!
|
||||||
|
formSubmissions: [FormSubmission!]
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverAnswer {
|
type serverAnswer {
|
||||||
|
@ -3,34 +3,30 @@ import React from 'react'
|
|||||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
|
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
|
||||||
|
|
||||||
import client from '../apollo'
|
import client from '../apollo'
|
||||||
import Context from '../context'
|
import Authorize from '../views/Authorize'
|
||||||
import { useUser } from '../hooks'
|
import CreateForm from '../views/CreateForm'
|
||||||
import Authorize from './Authorize'
|
import DoForm from '../views/DoForm'
|
||||||
import CreateForm from './CreateForm'
|
import Home from '../views/Home'
|
||||||
import DoForm from './DoForm'
|
import Login from '../views/Login'
|
||||||
import Login from './Login'
|
import Navbar from './Navbar'
|
||||||
import UserPage from './UserPage'
|
import Register from '../views/Register'
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => (
|
||||||
const userContext = useUser()
|
<div className="App">
|
||||||
|
<ApolloProvider client={client}>
|
||||||
return (
|
<Router>
|
||||||
<div className="App">
|
<Navbar />
|
||||||
<ApolloProvider client={client}>
|
<Switch>
|
||||||
<Context.Provider value={userContext}>
|
<Route path="/login" component={Login} />
|
||||||
<Router>
|
<Route path="/authorize" component={Authorize} />
|
||||||
<Switch>
|
<Route path="/form/:id" component={DoForm} />
|
||||||
<Route path="/login" component={Login} />
|
<Route path="/create" component={CreateForm} />
|
||||||
<Route path="/authorize" component={Authorize} />
|
<Route path="/register" component={Register} />
|
||||||
<Route path="/user" component={UserPage} />
|
<Route exact path="/" component={Home} />
|
||||||
<Route path="/form/:id" component={DoForm} />
|
</Switch>
|
||||||
<Route path="/create" component={CreateForm} />
|
</Router>
|
||||||
</Switch>
|
</ApolloProvider>
|
||||||
</Router>
|
</div>
|
||||||
</Context.Provider>
|
)
|
||||||
</ApolloProvider>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
23
src/components/Card/index.tsx
Normal file
@ -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
|
12
src/components/Card/main.module.css
Normal file
@ -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'
|
import React from 'react'
|
||||||
|
|
||||||
interface IProps {
|
import styles from './main.module.css'
|
||||||
|
|
||||||
|
interface IListsProps {
|
||||||
variants: { text: string }[]
|
variants: { text: string }[]
|
||||||
name: string
|
name: string
|
||||||
type: 'CHECK' | 'CHOOSE'
|
type: 'CHECK' | 'CHOOSE'
|
||||||
onChange: (num: number) => void
|
onChange: (num: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Lists: React.FC<IProps> = ({ variants, name, type, onChange }) => {
|
const Lists: React.FC<IListsProps> = ({ variants, name, type, onChange }) => {
|
||||||
const inputType =
|
const inputType =
|
||||||
(type === 'CHECK' && 'check') || (type === 'CHOOSE' && 'radio') || undefined
|
(type === 'CHECK' && 'check') || (type === 'CHOOSE' && 'radio') || undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={styles.variantsList}>
|
||||||
{variants.map((el, index) => (
|
{variants.map((el, index) => (
|
||||||
<label key={index}>
|
<label key={index}>
|
||||||
<input
|
<input
|
||||||
@ -25,6 +27,7 @@ const Lists: React.FC<IProps> = ({ variants, name, type, onChange }) => {
|
|||||||
type={inputType}
|
type={inputType}
|
||||||
name={name}
|
name={name}
|
||||||
value={el.text}
|
value={el.text}
|
||||||
|
className={styles.selector}
|
||||||
/>
|
/>
|
||||||
{el.text}
|
{el.text}
|
||||||
</label>
|
</label>
|
14
src/components/FormLists/main.module.css
Normal file
@ -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
|
|
15
src/components/Navbar/index.tsx
Normal file
@ -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
|
73
src/components/Navbar/logo.svg
Normal file
@ -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 Width: | Height: | Size: 3.8 KiB |
16
src/components/Navbar/main.module.css
Normal file
@ -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;
|
||||||
|
}
|
33
src/components/QuestionsForm/hooks.ts
Normal file
@ -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 }
|
||||||
|
}
|
130
src/components/QuestionsForm/index.tsx
Normal file
@ -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
|
53
src/components/QuestionsForm/main.module.css
Normal file
@ -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;
|
||||||
|
}
|
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
@ -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 []
|
||||||
|
})
|
25
src/components/SubmissionsList/empty.svg
Normal file
@ -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 Width: | Height: | Size: 969 B |
68
src/components/SubmissionsList/index.tsx
Normal file
@ -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
|
48
src/components/SubmissionsList/main.module.css
Normal file
@ -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
|
|
12
src/hooks.ts
@ -1,16 +1,4 @@
|
|||||||
import { useCallback, useState } from 'react'
|
|
||||||
import { useLocation } from 'react-router-dom'
|
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 = () => {
|
export const useURLQuery = () => {
|
||||||
return new URLSearchParams(useLocation().search)
|
return new URLSearchParams(useLocation().search)
|
||||||
|
@ -1,13 +1,3 @@
|
|||||||
body {
|
.App {
|
||||||
margin: 0;
|
height: 100vh;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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 React from 'react'
|
||||||
import { Redirect } from 'react-router-dom'
|
import { Redirect } from 'react-router-dom'
|
||||||
|
|
||||||
import { useURLQuery } from '../../hooks'
|
import { useURLQuery } from '../../hooks'
|
||||||
|
|
||||||
const Authorize: React.FC = () => {
|
const Authorize: React.FC = () => {
|
125
src/views/CreateForm/hooks.ts
Normal file
@ -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,
|
||||||
|
]
|
||||||
|
}
|
154
src/views/CreateForm/index.tsx
Normal file
@ -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
|
150
src/views/CreateForm/main.module.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
67
src/views/CreateForm/types.ts
Normal file
@ -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'
|
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
@ -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
@ -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
|
55
src/views/DoForm/main.module.css
Normal file
@ -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
|
132
src/views/Home/main.module.css
Normal file
@ -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
|
94
src/views/Login/main.module.css
Normal file
@ -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
After Width: | Height: | Size: 87 KiB |
73
src/views/Register/index.tsx
Normal file
@ -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
After Width: | Height: | Size: 61 KiB |
@ -13,7 +13,8 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react"
|
"jsx": "react",
|
||||||
|
"plugins": [{ "name": "typescript-plugin-css-modules" }]
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|