Compare commits
No commits in common. "main" and "v1.0" have entirely different histories.
@ -1 +0,0 @@
|
|||||||
node_modules/
|
|
3
.gitignore
vendored
@ -13,7 +13,6 @@
|
|||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
@ -25,5 +24,3 @@ yarn-error.log*
|
|||||||
|
|
||||||
*.local*
|
*.local*
|
||||||
*.gen*
|
*.gen*
|
||||||
|
|
||||||
yarn.lock
|
|
@ -1,7 +0,0 @@
|
|||||||
FROM node:alpine
|
|
||||||
WORKDIR /frontend
|
|
||||||
COPY package.json /frontend/package.json
|
|
||||||
RUN yarn
|
|
||||||
COPY . /frontend
|
|
||||||
RUN yarn codegen
|
|
||||||
RUN yarn build
|
|
21
LICENSE
@ -1,21 +0,0 @@
|
|||||||
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,7 +9,6 @@
|
|||||||
"@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",
|
||||||
@ -20,8 +19,7 @@
|
|||||||
"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"
|
||||||
@ -40,9 +38,6 @@
|
|||||||
},
|
},
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 10 KiB |
@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square150x150logo src="/mstile-150x150.png" />
|
|
||||||
<TileColor>#363645</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
Before Width: | Height: | Size: 959 B |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 15 KiB |
@ -1,45 +0,0 @@
|
|||||||
: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,43 +2,39 @@
|
|||||||
<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
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
rel="apple-touch-icon"
|
<!--
|
||||||
sizes="180x180"
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
href="%PUBLIC_URL%/apple-touch-icon.png"
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
/>
|
-->
|
||||||
<link
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
rel="icon"
|
<!--
|
||||||
type="image/png"
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
sizes="32x32"
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
href="%PUBLIC_URL%/favicon-32x32.png"
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
/>
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
<link
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
rel="icon"
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
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": "android-chrome-192x192.png",
|
"src": "logo192.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "android-chrome-512x512.png",
|
"src": "logo512.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "512x512"
|
"sizes": "512x512"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": "/",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"theme_color": "#6df577",
|
"theme_color": "#000000",
|
||||||
"background_color": "#363645"
|
"background_color": "#ffffff"
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
@ -1,34 +0,0 @@
|
|||||||
<?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>
|
|
Before Width: | Height: | Size: 1.9 KiB |
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"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,22 +32,6 @@ const FORM = gql`
|
|||||||
title
|
title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
submissions {
|
|
||||||
answers {
|
|
||||||
... on InputAnswer {
|
|
||||||
type
|
|
||||||
userInput
|
|
||||||
}
|
|
||||||
... on ChoiseAnswer {
|
|
||||||
type
|
|
||||||
userChoise
|
|
||||||
}
|
|
||||||
}
|
|
||||||
date
|
|
||||||
user {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
title
|
title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,21 +41,12 @@ 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@ -92,12 +67,4 @@ const CREATEFORM = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const REGISTER = gql`
|
export { LOGIN, FORM, USER, FORMSUBMIT, CREATEFORM }
|
||||||
mutation Register($email: String!, $name: String!) {
|
|
||||||
register(email: $email, name: $name) {
|
|
||||||
success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export { LOGIN, FORM, USER, FORMSUBMIT, CREATEFORM, REGISTER }
|
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
import {
|
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client'
|
||||||
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:
|
uri: process.env.GRAPHQL_URL || process.env.REACT_APP_GRAPHQL_URL || undefined
|
||||||
process.env.GRAPHQL_URL || process.env.REACT_APP_GRAPHQL_URL || undefined,
|
|
||||||
// fetchOptions: {
|
// fetchOptions: {
|
||||||
// mode: 'no-cors'
|
// mode: 'no-cors'
|
||||||
// }
|
// }
|
||||||
@ -23,25 +17,14 @@ 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: from([errorLink, authLink, httpLink]),
|
link: authLink.concat(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,11 +42,9 @@ 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 {
|
||||||
@ -79,7 +77,6 @@ type User {
|
|||||||
forms: [Form!]
|
forms: [Form!]
|
||||||
id: Int!
|
id: Int!
|
||||||
name: String!
|
name: String!
|
||||||
formSubmissions: [FormSubmission!]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverAnswer {
|
type serverAnswer {
|
||||||
|
@ -3,30 +3,34 @@ 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 Authorize from '../views/Authorize'
|
import Context from '../context'
|
||||||
import CreateForm from '../views/CreateForm'
|
import { useUser } from '../hooks'
|
||||||
import DoForm from '../views/DoForm'
|
import Authorize from './Authorize'
|
||||||
import Home from '../views/Home'
|
import CreateForm from './CreateForm'
|
||||||
import Login from '../views/Login'
|
import DoForm from './DoForm'
|
||||||
import Navbar from './Navbar'
|
import Login from './Login'
|
||||||
import Register from '../views/Register'
|
import UserPage from './UserPage'
|
||||||
|
|
||||||
const App: React.FC = () => (
|
const App: React.FC = () => {
|
||||||
|
const userContext = useUser()
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
|
<Context.Provider value={userContext}>
|
||||||
<Router>
|
<Router>
|
||||||
<Navbar />
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/login" component={Login} />
|
<Route path="/login" component={Login} />
|
||||||
<Route path="/authorize" component={Authorize} />
|
<Route path="/authorize" component={Authorize} />
|
||||||
|
<Route path="/user" component={UserPage} />
|
||||||
<Route path="/form/:id" component={DoForm} />
|
<Route path="/form/:id" component={DoForm} />
|
||||||
<Route path="/create" component={CreateForm} />
|
<Route path="/create" component={CreateForm} />
|
||||||
<Route path="/register" component={Register} />
|
|
||||||
<Route exact path="/" component={Home} />
|
|
||||||
</Switch>
|
</Switch>
|
||||||
</Router>
|
</Router>
|
||||||
|
</Context.Provider>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
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 = () => {
|
@ -1,23 +0,0 @@
|
|||||||
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
|
|
@ -1,12 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
156
src/components/CreateForm/hooks.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
129
src/components/CreateForm/index.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
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,20 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import styles from './main.module.css'
|
interface IProps {
|
||||||
|
|
||||||
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<IListsProps> = ({ variants, name, type, onChange }) => {
|
const Lists: React.FC<IProps> = ({ 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 className={styles.variantsList}>
|
<div>
|
||||||
{variants.map((el, index) => (
|
{variants.map((el, index) => (
|
||||||
<label key={index}>
|
<label key={index}>
|
||||||
<input
|
<input
|
||||||
@ -27,7 +25,6 @@ const Lists: React.FC<IListsProps> = ({ 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>
|
28
src/components/DoForm/hooks.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { ChoiseAnswer, InputAnswer } from '../../apollo/typeDefs.gen'
|
||||||
|
|
||||||
|
export const useForm = (initialValue?: (InputAnswer | ChoiseAnswer)[]) => {
|
||||||
|
console.log(initialValue, 'Inside hook')
|
||||||
|
|
||||||
|
const [answers, setAnswer] = useState<(InputAnswer | ChoiseAnswer)[]>(
|
||||||
|
initialValue || []
|
||||||
|
)
|
||||||
|
|
||||||
|
const answerChange = (num: number) => {
|
||||||
|
return (value: number | string) => {
|
||||||
|
setAnswer((prev) => {
|
||||||
|
return prev.map((el, index) => {
|
||||||
|
if (index === num) {
|
||||||
|
if (el.__typename === 'ChoiseAnswer' && typeof value === 'number')
|
||||||
|
return { ...el, userChoise: value }
|
||||||
|
if (el.__typename === 'InputAnswer' && typeof value === 'string')
|
||||||
|
return { ...el, userInput: value }
|
||||||
|
}
|
||||||
|
return el
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [answers, answerChange]
|
||||||
|
}
|
171
src/components/DoForm/index.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import { useMutation, useQuery } from '@apollo/client'
|
||||||
|
import React, { FormEvent, useEffect, useState } from 'react'
|
||||||
|
import { Redirect, useParams } from 'react-router-dom'
|
||||||
|
import { FORM, FORMSUBMIT } from '../../apollo'
|
||||||
|
import {
|
||||||
|
ChoiseAnswer,
|
||||||
|
ChoisesQuestion,
|
||||||
|
Form,
|
||||||
|
InputAnswer,
|
||||||
|
InputQuestion,
|
||||||
|
QueryFormArgs,
|
||||||
|
} from '../../apollo/typeDefs.gen'
|
||||||
|
import Lists from './Lists'
|
||||||
|
|
||||||
|
interface IFormQuery {
|
||||||
|
form: Form
|
||||||
|
}
|
||||||
|
|
||||||
|
const DoForm: React.FC = () => {
|
||||||
|
const { id: idString } = useParams<{ id: string }>()
|
||||||
|
|
||||||
|
const id = parseInt(idString)
|
||||||
|
|
||||||
|
const { data, error, loading } = useQuery<IFormQuery, QueryFormArgs>(FORM, {
|
||||||
|
variables: { id },
|
||||||
|
skip: isNaN(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
const [
|
||||||
|
doFormSubmit,
|
||||||
|
{ error: submitError, data: submitData, loading: submitLoading },
|
||||||
|
] = useMutation(FORMSUBMIT)
|
||||||
|
|
||||||
|
const [answers, setAnswer] = useState<(InputAnswer | ChoiseAnswer)[]>([])
|
||||||
|
|
||||||
|
const getInitialState = (data: IFormQuery) => {
|
||||||
|
if (data && data.form) {
|
||||||
|
return data.form.questions.flatMap<InputAnswer | ChoiseAnswer>(
|
||||||
|
(el: InputQuestion | ChoisesQuestion) => {
|
||||||
|
if (el.__typename === 'ChoisesQuestion')
|
||||||
|
return [
|
||||||
|
{ __typename: 'ChoiseAnswer', type: 'CHOISE', userChoise: -1 },
|
||||||
|
]
|
||||||
|
if (el.__typename === 'InputQuestion')
|
||||||
|
return [{ __typename: 'InputAnswer', type: 'INPUT', userInput: '' }]
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
const initialState = getInitialState(data)
|
||||||
|
setAnswer(initialState)
|
||||||
|
}
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
useEffect(() => console.log(answers), [answers])
|
||||||
|
|
||||||
|
if (isNaN(id)) return <Redirect to="/" />
|
||||||
|
|
||||||
|
if (loading) return <div>Loading...</div>
|
||||||
|
if (error) return <div>{error.message}</div>
|
||||||
|
|
||||||
|
const { form } = data!
|
||||||
|
|
||||||
|
const handleSubmit = (e: FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
console.log('Submited form:', answers)
|
||||||
|
|
||||||
|
answers.forEach((el) => {
|
||||||
|
delete el.__typename
|
||||||
|
})
|
||||||
|
|
||||||
|
const submitAnswers = JSON.stringify(answers)
|
||||||
|
|
||||||
|
console.log('Filtered answers: ', submitAnswers)
|
||||||
|
|
||||||
|
doFormSubmit({
|
||||||
|
variables: {
|
||||||
|
formId: id,
|
||||||
|
answers: submitAnswers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const answerChange = (num: number) => {
|
||||||
|
return (value: number | string) => {
|
||||||
|
setAnswer((prev) => {
|
||||||
|
return prev.map((el, index) => {
|
||||||
|
if (index === num) {
|
||||||
|
if (el.__typename === 'ChoiseAnswer' && typeof value === 'number')
|
||||||
|
return { ...el, userChoise: value }
|
||||||
|
if (el.__typename === 'InputAnswer' && typeof value === 'string')
|
||||||
|
return { ...el, userInput: value }
|
||||||
|
}
|
||||||
|
return el
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>{form.title}</h1>
|
||||||
|
<p>{form.dateCreated}</p>
|
||||||
|
<h3>{form.author?.name || 'No author'}</h3>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<ul>
|
||||||
|
{form.questions.map((el: InputQuestion | ChoisesQuestion) => {
|
||||||
|
if (el.__typename === 'InputQuestion')
|
||||||
|
return (
|
||||||
|
<li key={el.number}>
|
||||||
|
<label>
|
||||||
|
{el.title}
|
||||||
|
<input
|
||||||
|
onChange={(e) =>
|
||||||
|
answerChange(el.number)(e.currentTarget.value)
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
if (el.__typename === 'ChoisesQuestion')
|
||||||
|
return (
|
||||||
|
<li key={el.number}>
|
||||||
|
<label>
|
||||||
|
{el.title}
|
||||||
|
{el.type === 'SELECT' ? (
|
||||||
|
<select
|
||||||
|
onChange={(e) => {
|
||||||
|
const selectValue = el.variants.findIndex(
|
||||||
|
(val) => val.text === e.currentTarget.value
|
||||||
|
)
|
||||||
|
answerChange(el.number)(selectValue)
|
||||||
|
}}
|
||||||
|
name={el.title}
|
||||||
|
>
|
||||||
|
{el.variants.map((option, index) => (
|
||||||
|
<option key={index}>{option.text}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<Lists
|
||||||
|
variants={el.variants}
|
||||||
|
onChange={answerChange(el.number)}
|
||||||
|
name={el.title}
|
||||||
|
type={el.type}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
return <li>Unknown question type</li>
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
{submitLoading ? <p>Uploading...</p> : <input type="submit" />}
|
||||||
|
</form>
|
||||||
|
{submitError && <p>{submitError.message}</p>}
|
||||||
|
{submitData && submitData.formSubmit && submitData.formSubmit.success && (
|
||||||
|
<p>Successfully uploaded</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DoForm
|
@ -1,14 +0,0 @@
|
|||||||
.selector {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.variantsList {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (orientation: portrait) {
|
|
||||||
.variantsList {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
35
src/components/Login/index.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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
|
@ -1,15 +0,0 @@
|
|||||||
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
|
|
@ -1,73 +0,0 @@
|
|||||||
<?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>
|
|
Before Width: | Height: | Size: 3.8 KiB |
@ -1,16 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
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 }
|
|
||||||
}
|
|
@ -1,130 +0,0 @@
|
|||||||
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
|
|
@ -1,53 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
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[]
|
|
@ -1,11 +0,0 @@
|
|||||||
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 []
|
|
||||||
})
|
|
@ -1,25 +0,0 @@
|
|||||||
<?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>
|
|
Before Width: | Height: | Size: 969 B |
@ -1,68 +0,0 @@
|
|||||||
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
|
|
@ -1,48 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
41
src/components/UserPage/index.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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
|
27
src/context.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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,4 +1,16 @@
|
|||||||
|
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,3 +1,13 @@
|
|||||||
.App {
|
body {
|
||||||
height: 100vh;
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
}
|
}
|
||||||
|
22
src/types.ts
@ -1,22 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import { GetDateCreatedFT } from './types'
|
|
||||||
|
|
||||||
export const getDateCreated: GetDateCreatedFT = (dateString) => {
|
|
||||||
const date = new Date(dateString)
|
|
||||||
|
|
||||||
return `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`
|
|
||||||
}
|
|
@ -1,125 +0,0 @@
|
|||||||
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,
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,154 +0,0 @@
|
|||||||
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
|
|
@ -1,150 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
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'
|
|
@ -1,22 +0,0 @@
|
|||||||
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,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
@ -1,7 +0,0 @@
|
|||||||
import { useParams } from 'react-router-dom'
|
|
||||||
|
|
||||||
export const useId = () => {
|
|
||||||
const { id: idString } = useParams<{ id: string }>()
|
|
||||||
|
|
||||||
return parseInt(idString)
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
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
|
|
@ -1,55 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
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
|
|
@ -1,132 +0,0 @@
|
|||||||
.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%;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
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
|
|
@ -1,7 +0,0 @@
|
|||||||
import { LogOutFT } from './types'
|
|
||||||
|
|
||||||
export const logOut: LogOutFT = (refetch, history) => {
|
|
||||||
localStorage.removeItem('token')
|
|
||||||
refetch()
|
|
||||||
history.push('/')
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
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
|
|
@ -1,94 +0,0 @@
|
|||||||
.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%;
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 87 KiB |
@ -1,73 +0,0 @@
|
|||||||
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
|
|
Before Width: | Height: | Size: 61 KiB |
@ -13,8 +13,7 @@
|
|||||||
"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"]
|
||||||
}
|
}
|
||||||
|