From 1585b87a1d9cc91bb6f0a036f2ca3ee6d21ffac1 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Tue, 2 Mar 2021 15:05:51 +0100 Subject: [PATCH 01/10] #21: Added competition login --- client/src/components/Login.css | 3 +- client/src/components/Login.tsx | 150 +++++++++++++++++++++++++++----- client/src/interfaces/models.ts | 6 +- 3 files changed, 134 insertions(+), 25 deletions(-) diff --git a/client/src/components/Login.css b/client/src/components/Login.css index 5a01047d..abefd48f 100644 --- a/client/src/components/Login.css +++ b/client/src/components/Login.css @@ -1,10 +1,11 @@ .login-page { display: flex; + height: 100%; justify-content: center; + align-items: center; } .login-form { display: flex; flex-direction: column; - width: 250px; } diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index 91af9b78..5df0e3d0 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -1,22 +1,22 @@ -import { Button, TextField } from '@material-ui/core' -import { withStyles } from '@material-ui/core/styles' +import { AppBar, Button, Tab, Tabs, TextField } from '@material-ui/core' +import { makeStyles, Theme, withStyles } from '@material-ui/core/styles' import { Alert, AlertTitle } from '@material-ui/lab' import axios from 'axios' import { Formik, FormikHelpers } from 'formik' -import React, { useState } from 'react' +import React from 'react' import * as Yup from 'yup' -import { LoginModel } from '../interfaces/models' +import { AccountLoginModel, CompetitionLoginModel } from '../interfaces/models' import './Login.css' const styles = {} -interface LoginState { - status: number - message: string +interface AccountLoginFormModel { + model: AccountLoginModel + error?: string } -interface LoginFormModel { - model: LoginModel +interface CompetitionLoginFormModel { + model: CompetitionLoginModel error?: string } @@ -25,21 +25,32 @@ interface ServerResponse { message: string } -const schema: Yup.SchemaOf<LoginFormModel> = Yup.object({ +const accountSchema: Yup.SchemaOf<AccountLoginFormModel> = Yup.object({ model: Yup.object() .shape({ email: Yup.string().email('Email inte giltig').required('Email krävs'), password: Yup.string() .required('Lösenord krävs') - .min(6, 'Lösenord måste vara minst 6 karaktärer') + .min(6, 'Lösenord måste vara minst 6 tecken') + }) + .required(), + error: Yup.string().optional() +}) + +const competitionSchema: Yup.SchemaOf<CompetitionLoginFormModel> = Yup.object({ + model: Yup.object() + .shape({ + code: Yup.string() + .required('Mata in kod') + .min(6, 'Koden måste vara minst 6 karaktärer') }) .required(), error: Yup.string().optional() }) -const handleSubmit = async ( - values: LoginFormModel, - actions: FormikHelpers<LoginFormModel> +const handleAccountSubmit = async ( + values: AccountLoginFormModel, + actions: FormikHelpers<AccountLoginFormModel> ) => { await axios .post<ServerResponse>(`users/login`, values.model) @@ -54,15 +65,41 @@ const handleSubmit = async ( }) } -const LoginForm: React.FC = (props) => { - const [serverState, setServerState] = useState<LoginFormModel>() - const initialValues: LoginFormModel = { model: { email: '', password: '' } } - return ( - <div className="login-page"> +const handleCompetitionSubmit = async ( + values: CompetitionLoginFormModel, + actions: FormikHelpers<CompetitionLoginFormModel> +) => { + await axios + .post<ServerResponse>(`users/login`, values.model) + .then((res) => { + actions.resetForm() + }) + .catch(({ response }) => { + actions.setFieldError('error', response.data.message) + }) + .finally(() => { + actions.setSubmitting(false) + }) +} + +interface TabPanelProps { + activeTab: number +} + +function LoginTab(props: TabPanelProps) { + const accountInitialValues: AccountLoginFormModel = { + model: { email: '', password: '' } + } + const competitionInitialValues: CompetitionLoginFormModel = { + model: { code: '' } + } + const { activeTab } = props + if (activeTab === 0) { + return ( <Formik - initialValues={initialValues} - validationSchema={schema} - onSubmit={handleSubmit} + initialValues={accountInitialValues} + validationSchema={accountSchema} + onSubmit={handleAccountSubmit} > {(formik) => ( <form onSubmit={formik.handleSubmit} className="login-form"> @@ -98,7 +135,7 @@ const LoginForm: React.FC = (props) => { color="primary" disabled={!formik.isValid} > - Submit + Login </Button> {formik.errors.error ? ( <Alert severity="error"> @@ -111,6 +148,73 @@ const LoginForm: React.FC = (props) => { </form> )} </Formik> + ) + } + return ( + <Formik + initialValues={competitionInitialValues} + validationSchema={competitionSchema} + onSubmit={handleCompetitionSubmit} + > + {(formik) => ( + <form onSubmit={formik.handleSubmit} className="login-form"> + <TextField + label="Tävlingskod" + name="model.code" + helperText={ + formik.touched.model?.code ? formik.errors.model?.code : '' + } + error={Boolean(formik.errors.model?.code)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + margin="normal" + /> + <Button + type="submit" + fullWidth + variant="contained" + color="primary" + disabled={!formik.isValid} + > + Login + </Button> + {formik.errors.error ? ( + <Alert severity="error"> + <AlertTitle>Error</AlertTitle> + {formik.errors.error} + </Alert> + ) : ( + <div /> + )} + </form> + )} + </Formik> + ) +} + +const useStyles = makeStyles((theme: Theme) => ({ + root: { + backgroundColor: theme.palette.background.paper + } +})) + +const LoginForm: React.FC = (props) => { + const classes = useStyles() + const [loginTab, setLoginTab] = React.useState(0) + return ( + <div className="login-page"> + <div className={classes.root}> + <AppBar position="static"> + <Tabs + value={loginTab} + onChange={(event, selectedTab) => setLoginTab(selectedTab)} + > + <Tab label="Konto" id="simple-tab-0" /> + <Tab label="Tävling" id="simple-tab-1" /> + </Tabs> + </AppBar> + <LoginTab activeTab={loginTab} /> + </div> </div> ) } diff --git a/client/src/interfaces/models.ts b/client/src/interfaces/models.ts index 292554f4..f2bc0b12 100644 --- a/client/src/interfaces/models.ts +++ b/client/src/interfaces/models.ts @@ -1,4 +1,8 @@ -export interface LoginModel { +export interface AccountLoginModel { email: string password: string } + +export interface CompetitionLoginModel { + code: string +} -- GitLab From 0bbb3513855ea3342ab6795f5d0642928e2f13b1 Mon Sep 17 00:00:00 2001 From: Victor <viclo211@student.liu.se> Date: Tue, 2 Mar 2021 15:28:37 +0100 Subject: [PATCH 02/10] Change eslint and prettier settings --- client/.eslintrc | 78 +++++++++-------------------- client/.prettierrc | 13 ++--- client/src/App.tsx | 5 +- client/src/components/AdminView.css | 4 +- client/src/components/AdminView.tsx | 29 ++++------- client/src/components/Login.tsx | 35 +++---------- client/src/index.css | 8 ++- client/tsconfig.json | 15 ++++-- 8 files changed, 64 insertions(+), 123 deletions(-) diff --git a/client/.eslintrc b/client/.eslintrc index 46369ae6..b83ef431 100644 --- a/client/.eslintrc +++ b/client/.eslintrc @@ -1,57 +1,27 @@ { - "env": { - "browser": true, - "es6": true, - "node": true - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "project": [ - "tsconfig.json" - ], - "ecmaVersion": 2021, - "sourceType": "module" - }, - "settings": { - "react": { - "version": "detect" - } - }, - "extends": [ - "airbnb-typescript", - "plugin:@typescript-eslint/recommended", - "plugin:react/recommended", - "prettier/@typescript-eslint", - "plugin:prettier/recommended" - ], - "rules": { - - "semi": "off", - "react/jsx-one-expression-per-line": "off", - - "prettier/prettier": [ - "error", - { - "endOfLine": "auto" - } - ], - "jsx-a11y/label-has-associated-control": [ - 2, - { - "labelComponents": [ - "CustomInputLabel" - ], - "labelAttributes": [ - "label" - ], - "controlComponents": [ - "CustomInput" - ], - "depth": 3 - } - ] + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2021, + "sourceType": "module", + "project": [ + "tsconfig.json" + ] + }, + "ecmaFeatures": { + "jsx": true + }, + "settings": { + "react": { + "version": "detect" } + }, + "extends": [ + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "prettier/@typescript-eslint", + "plugin:prettier/recommended" + ], + "rules": { + "prettier/prettier": ["warn"] + } } \ No newline at end of file diff --git a/client/.prettierrc b/client/.prettierrc index 0cc6d643..4ff5f098 100644 --- a/client/.prettierrc +++ b/client/.prettierrc @@ -1,7 +1,8 @@ { - "semi": false, - "trailingComma": "none", - "singleQuote": true, - "printWidth": 80, - "endOfLine": "lf" - } \ No newline at end of file + "semi": false, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 120, + "tabWidth": 2, + "endOfLine": "lf" +} diff --git a/client/src/App.tsx b/client/src/App.tsx index 1e8c051e..105a44d5 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -5,10 +5,7 @@ import Main from './Main' const App: React.FC = () => { return ( <div className="wrapper"> - <link - rel="stylesheet" - href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" - /> + <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> <Main /> </div> ) diff --git a/client/src/components/AdminView.css b/client/src/components/AdminView.css index 1ad0b7ee..a310f123 100644 --- a/client/src/components/AdminView.css +++ b/client/src/components/AdminView.css @@ -4,7 +4,7 @@ } .top-bar { - display:flex; + display: flex; justify-content: space-between; align-items: flex-start; -} \ No newline at end of file +} diff --git a/client/src/components/AdminView.tsx b/client/src/components/AdminView.tsx index 54551b64..1b4ad513 100644 --- a/client/src/components/AdminView.tsx +++ b/client/src/components/AdminView.tsx @@ -9,7 +9,7 @@ import { ListItemIcon, ListItemText, Toolbar, - Typography + Typography, } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import DashboardIcon from '@material-ui/icons/Dashboard' @@ -26,27 +26,27 @@ const menuItems = ['Startsida', 'Regioner', 'Användare', 'Tävlingshanterare'] const useStyles = makeStyles((theme: Theme) => createStyles({ root: { - display: 'flex' + display: 'flex', }, appBar: { width: `calc(100% - ${drawerWidth}px)`, - marginLeft: drawerWidth + marginLeft: drawerWidth, }, drawer: { width: drawerWidth, flexShrink: 0, - marginRight: drawerWidth + marginRight: drawerWidth, }, drawerPaper: { - width: drawerWidth + width: drawerWidth, }, // necessary for content to be below app bar toolbar: theme.mixins.toolbar, content: { flexGrow: 1, backgroundColor: theme.palette.background.default, - paddingLeft: theme.spacing(30) - } + paddingLeft: theme.spacing(30), + }, }) ) @@ -70,7 +70,7 @@ const AdminView: React.FC = (props) => { className={(classes.drawer, 'background')} variant="permanent" classes={{ - paper: classes.drawerPaper + paper: classes.drawerPaper, }} anchor="left" > @@ -87,9 +87,7 @@ const AdminView: React.FC = (props) => { selected={index === openIndex} onClick={() => setOpenIndex(index)} > - <ListItemIcon> - {text === 'Dashboard' ? <DashboardIcon /> : <MailIcon />} - </ListItemIcon> + <ListItemIcon>{text === 'Dashboard' ? <DashboardIcon /> : <MailIcon />}</ListItemIcon> <ListItemText primary={text} /> </ListItem> ))} @@ -97,14 +95,7 @@ const AdminView: React.FC = (props) => { <Divider /> <List> <ListItem> - <Button - component={Link} - to="/" - type="submit" - fullWidth - variant="contained" - color="primary" - > + <Button component={Link} to="/" type="submit" fullWidth variant="contained" color="primary"> Logga ut </Button> </ListItem> diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index 91af9b78..0b504f24 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -29,18 +29,13 @@ const schema: Yup.SchemaOf<LoginFormModel> = Yup.object({ model: Yup.object() .shape({ email: Yup.string().email('Email inte giltig').required('Email krävs'), - password: Yup.string() - .required('Lösenord krävs') - .min(6, 'Lösenord måste vara minst 6 karaktärer') + password: Yup.string().required('Lösenord krävs').min(6, 'Lösenord måste vara minst 6 karaktärer'), }) .required(), - error: Yup.string().optional() + error: Yup.string().optional(), }) -const handleSubmit = async ( - values: LoginFormModel, - actions: FormikHelpers<LoginFormModel> -) => { +const handleSubmit = async (values: LoginFormModel, actions: FormikHelpers<LoginFormModel>) => { await axios .post<ServerResponse>(`users/login`, values.model) .then((res) => { @@ -59,19 +54,13 @@ const LoginForm: React.FC = (props) => { const initialValues: LoginFormModel = { model: { email: '', password: '' } } return ( <div className="login-page"> - <Formik - initialValues={initialValues} - validationSchema={schema} - onSubmit={handleSubmit} - > + <Formik initialValues={initialValues} validationSchema={schema} onSubmit={handleSubmit}> {(formik) => ( <form onSubmit={formik.handleSubmit} className="login-form"> <TextField label="Email Adress" name="model.email" - helperText={ - formik.touched.model?.email ? formik.errors.model?.email : '' - } + helperText={formik.touched.model?.email ? formik.errors.model?.email : ''} error={Boolean(formik.errors.model?.email)} onChange={formik.handleChange} onBlur={formik.handleBlur} @@ -81,23 +70,13 @@ const LoginForm: React.FC = (props) => { label="Lösenord" name="model.password" type="password" - helperText={ - formik.touched.model?.password - ? formik.errors.model?.password - : '' - } + helperText={formik.touched.model?.password ? formik.errors.model?.password : ''} error={Boolean(formik.errors.model?.password)} onChange={formik.handleChange} onBlur={formik.handleBlur} margin="normal" /> - <Button - type="submit" - fullWidth - variant="contained" - color="primary" - disabled={!formik.isValid} - > + <Button type="submit" fullWidth variant="contained" color="primary" disabled={!formik.isValid}> Submit </Button> {formik.errors.error ? ( diff --git a/client/src/index.css b/client/src/index.css index ec2585e8..7323ae85 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,13 +1,11 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; + 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; + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } diff --git a/client/tsconfig.json b/client/tsconfig.json index a273b0cf..7f162ff3 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,26 +1,31 @@ { "compilerOptions": { + "rootDir": "./src", + "outDir": "./build", + "esModuleInterop": true, + "jsx": "react-jsx", "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], - "allowJs": true, "skipLibCheck": true, - "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx" + "allowJs": true, + "noFallthroughCasesInSwitch": true }, "include": [ - "src" + "./src/**/*" + ], + "exclude": [ + "build" ] } -- GitLab From caf343aa89d4a34a0bf10a76f2e5e07c2bcd6fad Mon Sep 17 00:00:00 2001 From: Victor <viclo211@student.liu.se> Date: Tue, 2 Mar 2021 15:40:55 +0100 Subject: [PATCH 03/10] #21: Remove depracted option and add prettierrc path --- .vscode/settings.json | 1 + client/.eslintrc | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 896cb947..d98ed346 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,6 +22,7 @@ "eslint.options": { "configFile":"./.eslintrc" }, + "prettier.configPath": "./client/.prettierrc", //git "git.ignoreLimitWarning": true, //language specific diff --git a/client/.eslintrc b/client/.eslintrc index b83ef431..7e2cadb0 100644 --- a/client/.eslintrc +++ b/client/.eslintrc @@ -1,7 +1,6 @@ { "parser": "@typescript-eslint/parser", "parserOptions": { - "ecmaVersion": 2021, "sourceType": "module", "project": [ "tsconfig.json" -- GitLab From dd5ee6927815a0730a8047338b7c510635feeaf0 Mon Sep 17 00:00:00 2001 From: Victor <viclo211@student.liu.se> Date: Tue, 2 Mar 2021 20:32:10 +0100 Subject: [PATCH 04/10] #21: Cleanup login code --- client/src/components/Login.tsx | 151 +++++++++++++------------------- 1 file changed, 62 insertions(+), 89 deletions(-) diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index 9912b7b1..1cda8296 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -1,8 +1,7 @@ import { AppBar, Button, Tab, Tabs, TextField } from '@material-ui/core' import { makeStyles, Theme, withStyles } from '@material-ui/core/styles' -import { Alert, AlertTitle } from '@material-ui/lab' import axios from 'axios' -import { Formik, FormikHelpers } from 'formik' +import { FieldAttributes, Form, Formik, FormikHelpers, useField } from 'formik' import React from 'react' import * as Yup from 'yup' import { AccountLoginModel, CompetitionLoginModel } from '../interfaces/models' @@ -25,23 +24,13 @@ interface ServerResponse { message: string } -const accountSchema: Yup.SchemaOf<AccountLoginFormModel> = Yup.object({ - model: Yup.object() - .shape({ - email: Yup.string().email('Email inte giltig').required('Email krävs'), - password: Yup.string().required('Lösenord krävs').min(6, 'Lösenord måste vara minst 6 karaktärer'), - }) - .required(), - error: Yup.string().optional(), +const accountLoginSchema: Yup.SchemaOf<AccountLoginModel> = Yup.object({ + email: Yup.string().required('Emailadress krävs').email('Ogiltig emailadress'), + password: Yup.string().required('Lösenord krävs').min(8, 'Lösenord måste vara minst 6 tecken'), }) -const competitionSchema: Yup.SchemaOf<CompetitionLoginFormModel> = Yup.object({ - model: Yup.object() - .shape({ - code: Yup.string().required('Mata in kod').min(6, 'Koden måste vara minst 6 karaktärer'), - }) - .required(), - error: Yup.string().optional(), +const competitionSchema: Yup.SchemaOf<CompetitionLoginModel> = Yup.object({ + code: Yup.string().required('Mata in kod').length(4, 'Koden måste vara 4 tecken'), }) const handleAccountSubmit = async (values: AccountLoginFormModel, actions: FormikHelpers<AccountLoginFormModel>) => { @@ -79,86 +68,70 @@ interface TabPanelProps { activeTab: number } +const CustomInputField = (props: FieldAttributes<any>) => { + const [field, meta] = useField<Record<string, unknown>>(props) + const errorText = meta.error && meta.touched ? meta.error : '' + return <TextField {...field} {...props} helperText={errorText} type="input" margin="normal"></TextField> +} + function LoginTab(props: TabPanelProps) { - const accountInitialValues: AccountLoginFormModel = { - model: { email: '', password: '' }, + const accountInitialValues: AccountLoginModel = { + email: '', + password: '', } - const competitionInitialValues: CompetitionLoginFormModel = { - model: { code: '' }, + const competitionInitalValues: CompetitionLoginModel = { + code: '', } const { activeTab } = props if (activeTab === 0) { return ( - <Formik initialValues={accountInitialValues} validationSchema={accountSchema} onSubmit={handleAccountSubmit}> - {(formik) => ( - <form onSubmit={formik.handleSubmit} className="login-form"> - <TextField - label="Email Adress" - name="model.email" - helperText={formik.touched.model?.email ? formik.errors.model?.email : ''} - error={Boolean(formik.errors.model?.email)} - onChange={formik.handleChange} - onBlur={formik.handleBlur} - margin="normal" - /> - <TextField - label="Lösenord" - name="model.password" - type="password" - helperText={formik.touched.model?.password ? formik.errors.model?.password : ''} - error={Boolean(formik.errors.model?.password)} - onChange={formik.handleChange} - onBlur={formik.handleBlur} - margin="normal" - /> - <Button type="submit" fullWidth variant="contained" color="primary" disabled={!formik.isValid}> - Logga in - </Button> - {formik.errors.error ? ( - <Alert severity="error"> - <AlertTitle>Error</AlertTitle> - {formik.errors.error} - </Alert> - ) : ( - <div /> - )} - </form> - )} - </Formik> + <div> + <Formik + initialValues={accountInitialValues} + validationSchema={accountLoginSchema} + onSubmit={(values: AccountLoginModel) => { + console.log('Login') + }} + > + {(formik) => ( + <Form className="login-form"> + <CustomInputField name="email" label="Emailadress" error={'email' in formik.errors}></CustomInputField> + <CustomInputField name="password" label="Lösenord" error={'password' in formik.errors}></CustomInputField> + <Button type="submit" fullWidth variant="contained" color="primary" disabled={!formik.isValid}> + Logga in + </Button> + {/* <pre>{JSON.stringify(formik.values, null, 2)}</pre> + <pre>{JSON.stringify(formik.errors, null, 2)}</pre> */} + </Form> + )} + </Formik> + </div> ) - } - return ( - <Formik - initialValues={competitionInitialValues} - validationSchema={competitionSchema} - onSubmit={handleCompetitionSubmit} - > - {(formik) => ( - <form onSubmit={formik.handleSubmit} className="login-form"> - <TextField - label="Tävlingskod" - name="model.code" - helperText={formik.touched.model?.code ? formik.errors.model?.code : ''} - error={Boolean(formik.errors.model?.code)} - onChange={formik.handleChange} - onBlur={formik.handleBlur} - margin="normal" - /> - <Button type="submit" fullWidth variant="contained" color="primary" disabled={!formik.isValid}> - Login - </Button> - {formik.errors.error ? ( - <Alert severity="error"> - <AlertTitle>Error</AlertTitle> - {formik.errors.error} - </Alert> - ) : ( - <div /> + } else if (activeTab === 1) { + return ( + <div> + <Formik + initialValues={competitionInitalValues} + validationSchema={competitionSchema} + onSubmit={(values: CompetitionLoginModel) => { + console.log('Connect') + }} + > + {(formik) => ( + <Form className="login-form"> + <CustomInputField name="code" label="Tävlingskod" error={'code' in formik.errors}></CustomInputField> + <Button type="submit" fullWidth variant="contained" color="primary" disabled={!formik.isValid}> + Anslut till tävling + </Button> + {/* <pre>values: {JSON.stringify(formik.values, null, 2)}</pre> + <pre>errors: {JSON.stringify(formik.errors, null, 2)}</pre> */} + </Form> )} - </form> - )} - </Formik> - ) + </Formik> + </div> + ) + } + return <div></div> } const useStyles = makeStyles((theme: Theme) => ({ -- GitLab From ca1285b679942e38060c0215931984e7b4594c3e Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Wed, 3 Mar 2021 12:08:13 +0100 Subject: [PATCH 05/10] #21: Added view selection and pages for individual views --- client/src/Main.tsx | 12 +- client/src/components/AdminView.tsx | 4 +- client/src/components/Login.tsx | 161 --------------- .../Login.css => pages/LoginPage.css} | 0 .../LoginPage.test.tsx} | 4 +- client/src/pages/LoginPage.tsx | 192 ++++++++++++++++++ client/src/pages/views/AudienceViewPage.tsx | 7 + client/src/pages/views/JudgeViewPage.tsx | 8 + .../src/pages/views/ParticipantViewPage.tsx | 7 + client/src/pages/views/ViewSelectPage.css | 21 ++ client/src/pages/views/ViewSelectPage.tsx | 23 +++ 11 files changed, 271 insertions(+), 168 deletions(-) delete mode 100644 client/src/components/Login.tsx rename client/src/{components/Login.css => pages/LoginPage.css} (100%) rename client/src/{components/Login.test.tsx => pages/LoginPage.test.tsx} (64%) create mode 100644 client/src/pages/LoginPage.tsx create mode 100644 client/src/pages/views/AudienceViewPage.tsx create mode 100644 client/src/pages/views/JudgeViewPage.tsx create mode 100644 client/src/pages/views/ParticipantViewPage.tsx create mode 100644 client/src/pages/views/ViewSelectPage.css create mode 100644 client/src/pages/views/ViewSelectPage.tsx diff --git a/client/src/Main.tsx b/client/src/Main.tsx index a44422fd..33bfc5f2 100644 --- a/client/src/Main.tsx +++ b/client/src/Main.tsx @@ -1,14 +1,22 @@ import React from 'react' import { BrowserRouter, Route, Switch } from 'react-router-dom' import AdminView from './components/AdminView' -import LoginForm from './components/Login' +import LoginPage from './pages/LoginPage' +import AudienceViewPage from './pages/views/AudienceViewPage' +import JudgeViewPage from './pages/views/JudgeViewPage' +import ParticipantViewPage from './pages/views/ParticipantViewPage' +import ViewSelectPage from './pages/views/ViewSelectPage' const Main = () => { return ( <BrowserRouter> <Switch> - <Route exact path="/" component={LoginForm} /> + <Route exact path="/" component={LoginPage} /> <Route path="/admin" component={AdminView} /> + <Route exact path="/view" component={ViewSelectPage} /> + <Route exact path="/view/participant" component={ParticipantViewPage} /> + <Route exact path="/view/judge" component={JudgeViewPage} /> + <Route exact path="/view/audience" component={AudienceViewPage} /> </Switch> </BrowserRouter> ) diff --git a/client/src/components/AdminView.tsx b/client/src/components/AdminView.tsx index 1b4ad513..59f8f67f 100644 --- a/client/src/components/AdminView.tsx +++ b/client/src/components/AdminView.tsx @@ -53,9 +53,7 @@ const useStyles = makeStyles((theme: Theme) => const AdminView: React.FC = (props) => { const classes = useStyles() const [openIndex, setOpenIndex] = React.useState(0) - const match = useRouteMatch() - console.log(match) - const { path, url } = match + const { path, url } = useRouteMatch() return ( <div className={classes.root}> <CssBaseline /> diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx deleted file mode 100644 index 1cda8296..00000000 --- a/client/src/components/Login.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { AppBar, Button, Tab, Tabs, TextField } from '@material-ui/core' -import { makeStyles, Theme, withStyles } from '@material-ui/core/styles' -import axios from 'axios' -import { FieldAttributes, Form, Formik, FormikHelpers, useField } from 'formik' -import React from 'react' -import * as Yup from 'yup' -import { AccountLoginModel, CompetitionLoginModel } from '../interfaces/models' -import './Login.css' - -const styles = {} - -interface AccountLoginFormModel { - model: AccountLoginModel - error?: string -} - -interface CompetitionLoginFormModel { - model: CompetitionLoginModel - error?: string -} - -interface ServerResponse { - code: number - message: string -} - -const accountLoginSchema: Yup.SchemaOf<AccountLoginModel> = Yup.object({ - email: Yup.string().required('Emailadress krävs').email('Ogiltig emailadress'), - password: Yup.string().required('Lösenord krävs').min(8, 'Lösenord måste vara minst 6 tecken'), -}) - -const competitionSchema: Yup.SchemaOf<CompetitionLoginModel> = Yup.object({ - code: Yup.string().required('Mata in kod').length(4, 'Koden måste vara 4 tecken'), -}) - -const handleAccountSubmit = async (values: AccountLoginFormModel, actions: FormikHelpers<AccountLoginFormModel>) => { - await axios - .post<ServerResponse>(`users/login`, values.model) - .then((res) => { - actions.resetForm() - }) - .catch(({ response }) => { - actions.setFieldError('error', response.data.message) - }) - .finally(() => { - actions.setSubmitting(false) - }) -} - -const handleCompetitionSubmit = async ( - values: CompetitionLoginFormModel, - actions: FormikHelpers<CompetitionLoginFormModel> -) => { - await axios - .post<ServerResponse>(`users/login`, values.model) - .then((res) => { - actions.resetForm() - }) - .catch(({ response }) => { - actions.setFieldError('error', response.data.message) - }) - .finally(() => { - actions.setSubmitting(false) - }) -} - -interface TabPanelProps { - activeTab: number -} - -const CustomInputField = (props: FieldAttributes<any>) => { - const [field, meta] = useField<Record<string, unknown>>(props) - const errorText = meta.error && meta.touched ? meta.error : '' - return <TextField {...field} {...props} helperText={errorText} type="input" margin="normal"></TextField> -} - -function LoginTab(props: TabPanelProps) { - const accountInitialValues: AccountLoginModel = { - email: '', - password: '', - } - const competitionInitalValues: CompetitionLoginModel = { - code: '', - } - const { activeTab } = props - if (activeTab === 0) { - return ( - <div> - <Formik - initialValues={accountInitialValues} - validationSchema={accountLoginSchema} - onSubmit={(values: AccountLoginModel) => { - console.log('Login') - }} - > - {(formik) => ( - <Form className="login-form"> - <CustomInputField name="email" label="Emailadress" error={'email' in formik.errors}></CustomInputField> - <CustomInputField name="password" label="Lösenord" error={'password' in formik.errors}></CustomInputField> - <Button type="submit" fullWidth variant="contained" color="primary" disabled={!formik.isValid}> - Logga in - </Button> - {/* <pre>{JSON.stringify(formik.values, null, 2)}</pre> - <pre>{JSON.stringify(formik.errors, null, 2)}</pre> */} - </Form> - )} - </Formik> - </div> - ) - } else if (activeTab === 1) { - return ( - <div> - <Formik - initialValues={competitionInitalValues} - validationSchema={competitionSchema} - onSubmit={(values: CompetitionLoginModel) => { - console.log('Connect') - }} - > - {(formik) => ( - <Form className="login-form"> - <CustomInputField name="code" label="Tävlingskod" error={'code' in formik.errors}></CustomInputField> - <Button type="submit" fullWidth variant="contained" color="primary" disabled={!formik.isValid}> - Anslut till tävling - </Button> - {/* <pre>values: {JSON.stringify(formik.values, null, 2)}</pre> - <pre>errors: {JSON.stringify(formik.errors, null, 2)}</pre> */} - </Form> - )} - </Formik> - </div> - ) - } - return <div></div> -} - -const useStyles = makeStyles((theme: Theme) => ({ - root: { - backgroundColor: theme.palette.background.paper, - }, -})) - -const LoginForm: React.FC = (props) => { - const classes = useStyles() - const [loginTab, setLoginTab] = React.useState(0) - return ( - <div className="login-page"> - <div className={classes.root}> - <AppBar position="static"> - <Tabs value={loginTab} onChange={(event, selectedTab) => setLoginTab(selectedTab)}> - <Tab label="Konto" id="simple-tab-0" /> - <Tab label="Tävling" id="simple-tab-1" /> - </Tabs> - </AppBar> - <LoginTab activeTab={loginTab} /> - </div> - </div> - ) -} - -export default withStyles(styles)(LoginForm) diff --git a/client/src/components/Login.css b/client/src/pages/LoginPage.css similarity index 100% rename from client/src/components/Login.css rename to client/src/pages/LoginPage.css diff --git a/client/src/components/Login.test.tsx b/client/src/pages/LoginPage.test.tsx similarity index 64% rename from client/src/components/Login.test.tsx rename to client/src/pages/LoginPage.test.tsx index 12a6c79c..ae41ba02 100644 --- a/client/src/components/Login.test.tsx +++ b/client/src/pages/LoginPage.test.tsx @@ -1,7 +1,7 @@ import { render } from '@testing-library/react' import React from 'react' -import LoginForm from './Login' +import LoginPage from './LoginPage' it('renders login form', () => { - render(<LoginForm />) + render(<LoginPage />) }) diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx new file mode 100644 index 00000000..ec3757e4 --- /dev/null +++ b/client/src/pages/LoginPage.tsx @@ -0,0 +1,192 @@ +import { AppBar, Button, Tab, Tabs, TextField } from '@material-ui/core' +import { makeStyles, Theme, withStyles } from '@material-ui/core/styles' +import { Alert, AlertTitle } from '@material-ui/lab' +import axios from 'axios' +import { Formik, FormikHelpers } from 'formik' +import React from 'react' +import * as Yup from 'yup' +import { AccountLoginModel, CompetitionLoginModel } from '../interfaces/models' +import './LoginPage.css' + +const styles = {} + +interface AccountLoginFormModel { + model: AccountLoginModel + error?: string +} + +interface CompetitionLoginFormModel { + model: CompetitionLoginModel + error?: string +} + +interface ServerResponse { + code: number + message: string +} + +const accountSchema: Yup.SchemaOf<AccountLoginFormModel> = Yup.object({ + model: Yup.object() + .shape({ + email: Yup.string().email('Email inte giltig').required('Email krävs'), + password: Yup.string().required('Lösenord krävs').min(6, 'Lösenord måste vara minst 6 tecken'), + }) + .required(), + error: Yup.string().optional(), +}) + +const competitionSchema: Yup.SchemaOf<CompetitionLoginFormModel> = Yup.object({ + model: Yup.object() + .shape({ + code: Yup.string().required('Mata in kod').min(6, 'Koden måste vara minst 6 karaktärer'), + }) + .required(), + error: Yup.string().optional(), +}) + +const handleAccountSubmit = async (values: AccountLoginFormModel, actions: FormikHelpers<AccountLoginFormModel>) => { + await axios + .post<ServerResponse>(`users/login`, values.model) + .then((res) => { + actions.resetForm() + }) + .catch(({ response }) => { + actions.setFieldError('error', response.data.message) + }) + .finally(() => { + actions.setSubmitting(false) + }) +} + +const handleCompetitionSubmit = async ( + values: CompetitionLoginFormModel, + actions: FormikHelpers<CompetitionLoginFormModel> +) => { + await axios + .post<ServerResponse>(`users/login`, values.model) + .then((res) => { + actions.resetForm() + }) + .catch(({ response }) => { + actions.setFieldError('error', response.data.message) + }) + .finally(() => { + actions.setSubmitting(false) + }) +} + +interface TabPanelProps { + activeTab: number +} + +function LoginTab(props: TabPanelProps) { + const accountInitialValues: AccountLoginFormModel = { + model: { email: '', password: '' }, + } + const competitionInitialValues: CompetitionLoginFormModel = { + model: { code: '' }, + } + const { activeTab } = props + if (activeTab === 0) { + return ( + <Formik initialValues={accountInitialValues} validationSchema={accountSchema} onSubmit={handleAccountSubmit}> + {(formik) => ( + <form onSubmit={formik.handleSubmit} className="login-form"> + <TextField + label="Email Adress" + name="model.email" + helperText={formik.touched.model?.email ? formik.errors.model?.email : ''} + error={Boolean(formik.errors.model?.email)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + margin="normal" + /> + <TextField + label="Lösenord" + name="model.password" + type="password" + helperText={formik.touched.model?.password ? formik.errors.model?.password : ''} + error={Boolean(formik.errors.model?.password)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + margin="normal" + /> + <Button type="submit" fullWidth variant="contained" color="primary" disabled={!formik.isValid}> + Logga in + </Button> + {formik.errors.error ? ( + <Alert severity="error"> + <AlertTitle>Error</AlertTitle> + {formik.errors.error} + </Alert> + ) : ( + <div /> + )} + </form> + )} + </Formik> + ) + } + return ( + <Formik + initialValues={competitionInitialValues} + validationSchema={competitionSchema} + onSubmit={handleCompetitionSubmit} + > + {(formik) => ( + <form onSubmit={formik.handleSubmit} className="login-form"> + <TextField + label="Tävlingskod" + name="model.code" + helperText={formik.touched.model?.code ? formik.errors.model?.code : ''} + error={Boolean(formik.errors.model?.code)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + margin="normal" + /> + <Button type="submit" fullWidth variant="contained" color="primary" disabled={!formik.isValid}> + Anslut till tävling + </Button> + {formik.errors.error ? ( + <Alert severity="error"> + <AlertTitle>Error</AlertTitle> + {formik.errors.error} + </Alert> + ) : ( + <div /> + )} + </form> + )} + </Formik> + ) +} + +const useStyles = makeStyles((theme: Theme) => ({ + root: { + backgroundColor: theme.palette.background.paper, + }, +})) + +const LoginPage: React.FC = (props) => { + const classes = useStyles() + const [loginTab, setLoginTab] = React.useState(0) + return ( + <div className="login-page"> + <div className={classes.root}> + <AppBar position="static"> + <Tabs value={loginTab} onChange={(event, selectedTab) => setLoginTab(selectedTab)}> + <Tab label="Konto" id="simple-tab-0" /> + <Tab label="Tävling" id="simple-tab-1" /> + </Tabs> + </AppBar> + <LoginTab activeTab={loginTab} /> + </div> + </div> + ) +} + +export default withStyles(styles)(LoginPage) + +// TODO: Values carry over from email address to code and vice versa +// TODO: Errors in email address causes error messages in password field +// TODO: Login button is enabled by default but should be turned off as now values have been entered yet diff --git a/client/src/pages/views/AudienceViewPage.tsx b/client/src/pages/views/AudienceViewPage.tsx new file mode 100644 index 00000000..f0cd70bc --- /dev/null +++ b/client/src/pages/views/AudienceViewPage.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +const AudienceViewPage: React.FC = (props) => { + return <div>Publik</div> +} + +export default AudienceViewPage diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx new file mode 100644 index 00000000..5498ff8c --- /dev/null +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import './ViewSelectPage.css' + +const JudgeViewPage: React.FC = (props) => { + return <div>Judge</div> +} + +export default JudgeViewPage diff --git a/client/src/pages/views/ParticipantViewPage.tsx b/client/src/pages/views/ParticipantViewPage.tsx new file mode 100644 index 00000000..6a22cd44 --- /dev/null +++ b/client/src/pages/views/ParticipantViewPage.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +const ParticipantViewPage: React.FC = (props) => { + return <div>Deltagare</div> +} + +export default ParticipantViewPage diff --git a/client/src/pages/views/ViewSelectPage.css b/client/src/pages/views/ViewSelectPage.css new file mode 100644 index 00000000..3a684a19 --- /dev/null +++ b/client/src/pages/views/ViewSelectPage.css @@ -0,0 +1,21 @@ +html, +body { + display: flex; + justify-content: center; + margin-top: 15%; + height: 100%; +} + +.root { + display: flex; + flex-direction: column; + justify-content: space-between; + width: max-content; + height: 140px; + margin-left: auto; + margin-right: auto; +} + +.view-button { + width: 100%; +} diff --git a/client/src/pages/views/ViewSelectPage.tsx b/client/src/pages/views/ViewSelectPage.tsx new file mode 100644 index 00000000..249dd87c --- /dev/null +++ b/client/src/pages/views/ViewSelectPage.tsx @@ -0,0 +1,23 @@ +import Button from '@material-ui/core/Button' +import React from 'react' +import { Link, useRouteMatch } from 'react-router-dom' +import './ViewSelectPage.css' + +const ViewSelectPage: React.FC = (props) => { + const { path, url } = useRouteMatch() + return ( + <div className="root"> + <Button className="view-button" color="primary" variant="contained" component={Link} to={`${url}/participant`}> + Deltagarvy + </Button> + <Button className="view-button" color="primary" variant="contained" component={Link} to={`${url}/audience`}> + Åskådarvy + </Button> + <Button className="view-button" color="primary" variant="contained" component={Link} to={`${url}/judge`}> + Domarvy + </Button> + </div> + ) +} + +export default ViewSelectPage -- GitLab From 2aed5820b139b3ec8d0ce462586fcb36ea6a9fc4 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Wed, 3 Mar 2021 17:05:16 +0100 Subject: [PATCH 06/10] Add competition list --- client/src/Main.tsx | 4 +- client/src/components/CompetitionManager.tsx | 12 -- client/src/pages/LoginPage.tsx | 23 +- .../admin/AdminPage.css} | 0 .../admin/AdminPage.test.tsx} | 4 +- .../admin/AdminPage.tsx} | 8 +- .../admin/components/CompetitionManager.css | 9 + .../components/CompetitionManager.test.tsx | 0 .../admin/components/CompetitionManager.tsx | 197 ++++++++++++++++++ .../admin}/components/Regions.test.tsx | 0 .../{ => pages/admin}/components/Regions.tsx | 0 client/src/pages/views/JudgeViewPage.tsx | 1 + client/src/pages/views/ViewSelectPage.css | 7 +- client/src/pages/views/ViewSelectPage.tsx | 20 +- 14 files changed, 243 insertions(+), 42 deletions(-) delete mode 100644 client/src/components/CompetitionManager.tsx rename client/src/{components/AdminView.css => pages/admin/AdminPage.css} (100%) rename client/src/{components/AdminView.test.tsx => pages/admin/AdminPage.test.tsx} (79%) rename client/src/{components/AdminView.tsx => pages/admin/AdminPage.tsx} (93%) create mode 100644 client/src/pages/admin/components/CompetitionManager.css rename client/src/{ => pages/admin}/components/CompetitionManager.test.tsx (100%) create mode 100644 client/src/pages/admin/components/CompetitionManager.tsx rename client/src/{ => pages/admin}/components/Regions.test.tsx (100%) rename client/src/{ => pages/admin}/components/Regions.tsx (100%) diff --git a/client/src/Main.tsx b/client/src/Main.tsx index 33bfc5f2..0bae997f 100644 --- a/client/src/Main.tsx +++ b/client/src/Main.tsx @@ -1,6 +1,6 @@ import React from 'react' import { BrowserRouter, Route, Switch } from 'react-router-dom' -import AdminView from './components/AdminView' +import AdminPage from './pages/admin/AdminPage' import LoginPage from './pages/LoginPage' import AudienceViewPage from './pages/views/AudienceViewPage' import JudgeViewPage from './pages/views/JudgeViewPage' @@ -12,7 +12,7 @@ const Main = () => { <BrowserRouter> <Switch> <Route exact path="/" component={LoginPage} /> - <Route path="/admin" component={AdminView} /> + <Route path="/admin" component={AdminPage} /> <Route exact path="/view" component={ViewSelectPage} /> <Route exact path="/view/participant" component={ParticipantViewPage} /> <Route exact path="/view/judge" component={JudgeViewPage} /> diff --git a/client/src/components/CompetitionManager.tsx b/client/src/components/CompetitionManager.tsx deleted file mode 100644 index 2d140fa3..00000000 --- a/client/src/components/CompetitionManager.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Typography } from '@material-ui/core' -import React from 'react' - -const CompetitionManager: React.FC = (props) => { - return ( - <Typography variant="h1" noWrap> - Tävlingshanterare - </Typography> - ) -} - -export default CompetitionManager diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index ec3757e4..d2fc3848 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -7,7 +7,6 @@ import React from 'react' import * as Yup from 'yup' import { AccountLoginModel, CompetitionLoginModel } from '../interfaces/models' import './LoginPage.css' - const styles = {} interface AccountLoginFormModel { @@ -38,7 +37,7 @@ const accountSchema: Yup.SchemaOf<AccountLoginFormModel> = Yup.object({ const competitionSchema: Yup.SchemaOf<CompetitionLoginFormModel> = Yup.object({ model: Yup.object() .shape({ - code: Yup.string().required('Mata in kod').min(6, 'Koden måste vara minst 6 karaktärer'), + code: Yup.string().required('Mata in kod').min(6, 'Koden måste vara minst 6 tecken'), }) .required(), error: Yup.string().optional(), @@ -62,8 +61,9 @@ const handleCompetitionSubmit = async ( values: CompetitionLoginFormModel, actions: FormikHelpers<CompetitionLoginFormModel> ) => { + console.log(values.model) await axios - .post<ServerResponse>(`users/login`, values.model) + .post<ServerResponse>(`users/login`, { code: values.model.code }) .then((res) => { actions.resetForm() }) @@ -96,7 +96,7 @@ function LoginTab(props: TabPanelProps) { label="Email Adress" name="model.email" helperText={formik.touched.model?.email ? formik.errors.model?.email : ''} - error={Boolean(formik.errors.model?.email)} + error={Boolean(formik.touched.model?.email && formik.errors.model?.email)} onChange={formik.handleChange} onBlur={formik.handleBlur} margin="normal" @@ -106,12 +106,18 @@ function LoginTab(props: TabPanelProps) { name="model.password" type="password" helperText={formik.touched.model?.password ? formik.errors.model?.password : ''} - error={Boolean(formik.errors.model?.password)} + error={Boolean(formik.touched.model?.password && formik.errors.model?.password)} onChange={formik.handleChange} onBlur={formik.handleBlur} margin="normal" /> - <Button type="submit" fullWidth variant="contained" color="primary" disabled={!formik.isValid}> + <Button + type="submit" + fullWidth + variant="contained" + color="primary" + disabled={!formik.isValid || !formik.touched.model?.email || !formik.touched.model?.email} + > Logga in </Button> {formik.errors.error ? ( @@ -138,8 +144,8 @@ function LoginTab(props: TabPanelProps) { <TextField label="Tävlingskod" name="model.code" - helperText={formik.touched.model?.code ? formik.errors.model?.code : ''} - error={Boolean(formik.errors.model?.code)} + helperText={formik.touched.model?.code && formik.touched.model?.code ? formik.errors.model?.code : ''} + error={Boolean(formik.touched.model?.code && formik.errors.model?.code)} onChange={formik.handleChange} onBlur={formik.handleBlur} margin="normal" @@ -189,4 +195,3 @@ export default withStyles(styles)(LoginPage) // TODO: Values carry over from email address to code and vice versa // TODO: Errors in email address causes error messages in password field -// TODO: Login button is enabled by default but should be turned off as now values have been entered yet diff --git a/client/src/components/AdminView.css b/client/src/pages/admin/AdminPage.css similarity index 100% rename from client/src/components/AdminView.css rename to client/src/pages/admin/AdminPage.css diff --git a/client/src/components/AdminView.test.tsx b/client/src/pages/admin/AdminPage.test.tsx similarity index 79% rename from client/src/components/AdminView.test.tsx rename to client/src/pages/admin/AdminPage.test.tsx index cdc46049..b29b78f8 100644 --- a/client/src/components/AdminView.test.tsx +++ b/client/src/pages/admin/AdminPage.test.tsx @@ -1,12 +1,12 @@ import { render } from '@testing-library/react' import React from 'react' import { BrowserRouter } from 'react-router-dom' -import AdminView from './AdminView' +import AdminPage from './AdminPage' it('renders admin view', () => { render( <BrowserRouter> - <AdminView /> + <AdminPage /> </BrowserRouter> ) }) diff --git a/client/src/components/AdminView.tsx b/client/src/pages/admin/AdminPage.tsx similarity index 93% rename from client/src/components/AdminView.tsx rename to client/src/pages/admin/AdminPage.tsx index 59f8f67f..a95bf04e 100644 --- a/client/src/components/AdminView.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -16,9 +16,9 @@ import DashboardIcon from '@material-ui/icons/Dashboard' import MailIcon from '@material-ui/icons/Mail' import React from 'react' import { Link, Route, Switch, useRouteMatch } from 'react-router-dom' -import './AdminView.css' -import CompetitionManager from './CompetitionManager' -import Regions from './Regions' +import './AdminPage.css' +import CompetitionManager from './components/CompetitionManager' +import Regions from './components/Regions' const drawerWidth = 240 const menuItems = ['Startsida', 'Regioner', 'Användare', 'Tävlingshanterare'] @@ -85,7 +85,7 @@ const AdminView: React.FC = (props) => { selected={index === openIndex} onClick={() => setOpenIndex(index)} > - <ListItemIcon>{text === 'Dashboard' ? <DashboardIcon /> : <MailIcon />}</ListItemIcon> + <ListItemIcon>{index === 0 ? <DashboardIcon /> : <MailIcon />}</ListItemIcon> <ListItemText primary={text} /> </ListItem> ))} diff --git a/client/src/pages/admin/components/CompetitionManager.css b/client/src/pages/admin/components/CompetitionManager.css new file mode 100644 index 00000000..51817660 --- /dev/null +++ b/client/src/pages/admin/components/CompetitionManager.css @@ -0,0 +1,9 @@ +.top-bar { + display: flex; + justify-content: space-between; + align-items: center; +} + +.new-competition-button { + margin-bottom: 15px; +} diff --git a/client/src/components/CompetitionManager.test.tsx b/client/src/pages/admin/components/CompetitionManager.test.tsx similarity index 100% rename from client/src/components/CompetitionManager.test.tsx rename to client/src/pages/admin/components/CompetitionManager.test.tsx diff --git a/client/src/pages/admin/components/CompetitionManager.tsx b/client/src/pages/admin/components/CompetitionManager.tsx new file mode 100644 index 00000000..160b32e4 --- /dev/null +++ b/client/src/pages/admin/components/CompetitionManager.tsx @@ -0,0 +1,197 @@ +import { Button, Menu } from '@material-ui/core' +import FormControl from '@material-ui/core/FormControl' +import InputBase from '@material-ui/core/InputBase' +import InputLabel from '@material-ui/core/InputLabel' +import MenuItem from '@material-ui/core/MenuItem' +import Paper from '@material-ui/core/Paper' +import Select from '@material-ui/core/Select' +import { createStyles, makeStyles, Theme, withStyles } from '@material-ui/core/styles' +import Table from '@material-ui/core/Table' +import TableBody from '@material-ui/core/TableBody' +import TableCell from '@material-ui/core/TableCell' +import TableContainer from '@material-ui/core/TableContainer' +import TableHead from '@material-ui/core/TableHead' +import TableRow from '@material-ui/core/TableRow' +import MoreHorizIcon from '@material-ui/icons/MoreHoriz' +import React from 'react' +import { Link } from 'react-router-dom' +import './CompetitionManager.css' + +const BootstrapInput = withStyles((theme: Theme) => + createStyles({ + root: { + 'label + &': { + marginTop: theme.spacing(3), + }, + }, + input: { + borderRadius: 4, + position: 'relative', + backgroundColor: theme.palette.background.paper, + border: '1px solid #ced4da', + fontSize: 16, + padding: '10px 26px 10px 12px', + transition: theme.transitions.create(['border-color', 'box-shadow']), + '&:focus': { + borderRadius: 4, + borderColor: '#80bdff', + boxShadow: '0 0 0 0.2rem rgba(0,123,255,.25)', + }, + }, + }) +)(InputBase) + +function createCompetition(name: string, region: string, year: number) { + return { name, region, year } +} + +const competitions = [ + createCompetition('Tävling 1', 'Stockholm', 2021), + createCompetition('Tävling 2', 'Stockholm', 2020), + createCompetition('Tävling 3', 'Sala', 2020), + createCompetition('Tävling 4', 'Sundsvall', 2020), + createCompetition('Tävling 5', 'Linköping', 2020), + createCompetition('Tävling 6', 'Linköping', 2020), + createCompetition('Tävling 7', 'Sala', 2019), + createCompetition('Tävling 8', 'Stockholm', 2019), + createCompetition('Tävling 9', 'Stockholm', 2019), + createCompetition('Tävling 10', 'Lidköping', 2019), + createCompetition('Tävling 11', 'Stockholm', 2019), + createCompetition('Tävling 12', 'Sala', 2018), + createCompetition('Tävling 13', 'Tornby', 2018), +] + +const regions = competitions + .map((competition) => competition.region) + .filter((competition, index, self) => self.indexOf(competition) === index) + +const years = competitions + .map((competition) => competition.year) + .filter((competition, index, self) => self.indexOf(competition) === index) + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + table: { + width: 1500, // TODO: Shrink table when smaller screen + }, + margin: { + margin: theme.spacing(1), + }, + }) +) + +const CompetitionManager: React.FC = (props) => { + const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) + const classes = useStyles() + const yearInitialValue = 0 + const regionInitialValue = '' + const noFilterText = 'Alla' + const [year, setYear] = React.useState(yearInitialValue) + const [region, setRegion] = React.useState(regionInitialValue) + const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + return ( + <div> + <div className="top-bar"> + <div> + <FormControl className={classes.margin}> + <InputLabel shrink id="demo-customized-textbox"> + Sök + </InputLabel> + <BootstrapInput id="demo-customized-textbox" /> + </FormControl> + <FormControl className={classes.margin}> + <InputLabel shrink id="demo-customized-select-native"> + Region + </InputLabel> + <Select + labelId="demo-customized-select-label" + id="demo-customized-select" + value={region === regionInitialValue ? noFilterText : region} + input={<BootstrapInput />} + > + <MenuItem value={noFilterText} onClick={() => setRegion(regionInitialValue)}> + {noFilterText} + </MenuItem> + {regions.map((text, index) => ( + <MenuItem key={text} value={text} onClick={() => setRegion(text)}> + {text} + </MenuItem> + ))} + </Select> + </FormControl> + <FormControl className={classes.margin}> + <InputLabel shrink id="demo-customized-select-label"> + År + </InputLabel> + <Select + id="demo-customized-select" + value={year === yearInitialValue ? noFilterText : year} + input={<BootstrapInput />} + > + <MenuItem value={noFilterText} onClick={() => setYear(yearInitialValue)}> + {noFilterText} + </MenuItem> + {years.map((year, index) => ( + <MenuItem key={year} value={year} onClick={() => setYear(year)}> + {year} + </MenuItem> + ))} + </Select> + </FormControl> + </div> + <Button color="secondary" variant="contained" className="new-competition-button"> + Ny Tävling + </Button> + </div> + <TableContainer component={Paper}> + <Table className={classes.table} aria-label="simple table"> + <TableHead> + <TableRow> + <TableCell>Namn</TableCell> + <TableCell align="right">Region</TableCell> + <TableCell align="right">År</TableCell> + <TableCell align="right"></TableCell> + </TableRow> + </TableHead> + <TableBody> + {competitions + .filter((row) => { + const yearOkay = year == yearInitialValue || row.year == year + const regionOkay = region == regionInitialValue || row.region == region + return yearOkay && regionOkay + }) + .map((row) => ( + <TableRow key={row.name}> + <TableCell scope="row"> + <Button color="primary" className="aa" component={Link} to="/123"> + {row.name} + </Button> + </TableCell> + <TableCell align="right">{row.region}</TableCell> + <TableCell align="right">{row.year}</TableCell> + <TableCell align="right"> + <Button onClick={handleClick}> + <MoreHorizIcon /> + </Button> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </TableContainer> + <Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}> + <MenuItem onClick={handleClose}>Starta</MenuItem> + <MenuItem onClick={handleClose}>Duplicera</MenuItem> + <MenuItem onClick={handleClose}>Ta bort</MenuItem> + </Menu> + </div> + ) +} + +export default CompetitionManager diff --git a/client/src/components/Regions.test.tsx b/client/src/pages/admin/components/Regions.test.tsx similarity index 100% rename from client/src/components/Regions.test.tsx rename to client/src/pages/admin/components/Regions.test.tsx diff --git a/client/src/components/Regions.tsx b/client/src/pages/admin/components/Regions.tsx similarity index 100% rename from client/src/components/Regions.tsx rename to client/src/pages/admin/components/Regions.tsx diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx index 5498ff8c..c6c65d24 100644 --- a/client/src/pages/views/JudgeViewPage.tsx +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -3,6 +3,7 @@ import './ViewSelectPage.css' const JudgeViewPage: React.FC = (props) => { return <div>Judge</div> + return <div>Judge</div> } export default JudgeViewPage diff --git a/client/src/pages/views/ViewSelectPage.css b/client/src/pages/views/ViewSelectPage.css index 3a684a19..920c6a34 100644 --- a/client/src/pages/views/ViewSelectPage.css +++ b/client/src/pages/views/ViewSelectPage.css @@ -1,12 +1,11 @@ -html, -body { +.root { display: flex; justify-content: center; - margin-top: 15%; + margin-top: 12%; height: 100%; } -.root { +.button-group { display: flex; flex-direction: column; justify-content: space-between; diff --git a/client/src/pages/views/ViewSelectPage.tsx b/client/src/pages/views/ViewSelectPage.tsx index 249dd87c..81decf7c 100644 --- a/client/src/pages/views/ViewSelectPage.tsx +++ b/client/src/pages/views/ViewSelectPage.tsx @@ -7,15 +7,17 @@ const ViewSelectPage: React.FC = (props) => { const { path, url } = useRouteMatch() return ( <div className="root"> - <Button className="view-button" color="primary" variant="contained" component={Link} to={`${url}/participant`}> - Deltagarvy - </Button> - <Button className="view-button" color="primary" variant="contained" component={Link} to={`${url}/audience`}> - Åskådarvy - </Button> - <Button className="view-button" color="primary" variant="contained" component={Link} to={`${url}/judge`}> - Domarvy - </Button> + <div className="button-group"> + <Button className="view-button" color="primary" variant="contained" component={Link} to={`${url}/participant`}> + Deltagarvy + </Button> + <Button className="view-button" color="primary" variant="contained" component={Link} to={`${url}/audience`}> + Åskådarvy + </Button> + <Button className="view-button" color="primary" variant="contained" component={Link} to={`${url}/judge`}> + Domarvy + </Button> + </div> </div> ) } -- GitLab From 75ab5952ff1f8d05e4f3317cebaa2be476c9c417 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Thu, 4 Mar 2021 17:07:19 +0100 Subject: [PATCH 07/10] Fix login page, theme color and competition search --- client/src/App.tsx | 16 +- client/src/Main.tsx | 4 +- client/src/pages/LoginPage.tsx | 197 ------------------ client/src/pages/admin/AdminPage.css | 1 - .../admin/components/CompetitionManager.css | 8 +- .../admin/components/CompetitionManager.tsx | 47 +++-- client/src/pages/{ => login}/LoginPage.css | 0 .../src/pages/{ => login}/LoginPage.test.tsx | 0 client/src/pages/login/LoginPage.tsx | 47 +++++ .../src/pages/login/components/AdminLogin.tsx | 94 +++++++++ .../login/components/CompetitionLogin.tsx | 85 ++++++++ .../PresentationEditorPage.tsx | 14 ++ client/src/pages/views/JudgeViewPage.tsx | 2 - 13 files changed, 291 insertions(+), 224 deletions(-) delete mode 100644 client/src/pages/LoginPage.tsx rename client/src/pages/{ => login}/LoginPage.css (100%) rename client/src/pages/{ => login}/LoginPage.test.tsx (100%) create mode 100644 client/src/pages/login/LoginPage.tsx create mode 100644 client/src/pages/login/components/AdminLogin.tsx create mode 100644 client/src/pages/login/components/CompetitionLogin.tsx create mode 100644 client/src/pages/presentationEditor/PresentationEditorPage.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 105a44d5..c488843c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,12 +1,24 @@ +import { createMuiTheme, ThemeProvider } from '@material-ui/core' import React from 'react' import './App.css' import Main from './Main' +const theme = createMuiTheme({ + palette: { + primary: { + // Purple and green play nicely together. + main: '#6200EE', + }, + }, +}) + const App: React.FC = () => { return ( <div className="wrapper"> - <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> - <Main /> + <ThemeProvider theme={theme}> + <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> + <Main /> + </ThemeProvider> </div> ) } diff --git a/client/src/Main.tsx b/client/src/Main.tsx index 0bae997f..5ad533a6 100644 --- a/client/src/Main.tsx +++ b/client/src/Main.tsx @@ -1,7 +1,8 @@ import React from 'react' import { BrowserRouter, Route, Switch } from 'react-router-dom' import AdminPage from './pages/admin/AdminPage' -import LoginPage from './pages/LoginPage' +import LoginPage from './pages/login/LoginPage' +import PresentationEditorPage from './pages/presentationEditor/PresentationEditorPage' import AudienceViewPage from './pages/views/AudienceViewPage' import JudgeViewPage from './pages/views/JudgeViewPage' import ParticipantViewPage from './pages/views/ParticipantViewPage' @@ -13,6 +14,7 @@ const Main = () => { <Switch> <Route exact path="/" component={LoginPage} /> <Route path="/admin" component={AdminPage} /> + <Route path="/competition-id=:id" component={PresentationEditorPage} /> <Route exact path="/view" component={ViewSelectPage} /> <Route exact path="/view/participant" component={ParticipantViewPage} /> <Route exact path="/view/judge" component={JudgeViewPage} /> diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx deleted file mode 100644 index d2fc3848..00000000 --- a/client/src/pages/LoginPage.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import { AppBar, Button, Tab, Tabs, TextField } from '@material-ui/core' -import { makeStyles, Theme, withStyles } from '@material-ui/core/styles' -import { Alert, AlertTitle } from '@material-ui/lab' -import axios from 'axios' -import { Formik, FormikHelpers } from 'formik' -import React from 'react' -import * as Yup from 'yup' -import { AccountLoginModel, CompetitionLoginModel } from '../interfaces/models' -import './LoginPage.css' -const styles = {} - -interface AccountLoginFormModel { - model: AccountLoginModel - error?: string -} - -interface CompetitionLoginFormModel { - model: CompetitionLoginModel - error?: string -} - -interface ServerResponse { - code: number - message: string -} - -const accountSchema: Yup.SchemaOf<AccountLoginFormModel> = Yup.object({ - model: Yup.object() - .shape({ - email: Yup.string().email('Email inte giltig').required('Email krävs'), - password: Yup.string().required('Lösenord krävs').min(6, 'Lösenord måste vara minst 6 tecken'), - }) - .required(), - error: Yup.string().optional(), -}) - -const competitionSchema: Yup.SchemaOf<CompetitionLoginFormModel> = Yup.object({ - model: Yup.object() - .shape({ - code: Yup.string().required('Mata in kod').min(6, 'Koden måste vara minst 6 tecken'), - }) - .required(), - error: Yup.string().optional(), -}) - -const handleAccountSubmit = async (values: AccountLoginFormModel, actions: FormikHelpers<AccountLoginFormModel>) => { - await axios - .post<ServerResponse>(`users/login`, values.model) - .then((res) => { - actions.resetForm() - }) - .catch(({ response }) => { - actions.setFieldError('error', response.data.message) - }) - .finally(() => { - actions.setSubmitting(false) - }) -} - -const handleCompetitionSubmit = async ( - values: CompetitionLoginFormModel, - actions: FormikHelpers<CompetitionLoginFormModel> -) => { - console.log(values.model) - await axios - .post<ServerResponse>(`users/login`, { code: values.model.code }) - .then((res) => { - actions.resetForm() - }) - .catch(({ response }) => { - actions.setFieldError('error', response.data.message) - }) - .finally(() => { - actions.setSubmitting(false) - }) -} - -interface TabPanelProps { - activeTab: number -} - -function LoginTab(props: TabPanelProps) { - const accountInitialValues: AccountLoginFormModel = { - model: { email: '', password: '' }, - } - const competitionInitialValues: CompetitionLoginFormModel = { - model: { code: '' }, - } - const { activeTab } = props - if (activeTab === 0) { - return ( - <Formik initialValues={accountInitialValues} validationSchema={accountSchema} onSubmit={handleAccountSubmit}> - {(formik) => ( - <form onSubmit={formik.handleSubmit} className="login-form"> - <TextField - label="Email Adress" - name="model.email" - helperText={formik.touched.model?.email ? formik.errors.model?.email : ''} - error={Boolean(formik.touched.model?.email && formik.errors.model?.email)} - onChange={formik.handleChange} - onBlur={formik.handleBlur} - margin="normal" - /> - <TextField - label="Lösenord" - name="model.password" - type="password" - helperText={formik.touched.model?.password ? formik.errors.model?.password : ''} - error={Boolean(formik.touched.model?.password && formik.errors.model?.password)} - onChange={formik.handleChange} - onBlur={formik.handleBlur} - margin="normal" - /> - <Button - type="submit" - fullWidth - variant="contained" - color="primary" - disabled={!formik.isValid || !formik.touched.model?.email || !formik.touched.model?.email} - > - Logga in - </Button> - {formik.errors.error ? ( - <Alert severity="error"> - <AlertTitle>Error</AlertTitle> - {formik.errors.error} - </Alert> - ) : ( - <div /> - )} - </form> - )} - </Formik> - ) - } - return ( - <Formik - initialValues={competitionInitialValues} - validationSchema={competitionSchema} - onSubmit={handleCompetitionSubmit} - > - {(formik) => ( - <form onSubmit={formik.handleSubmit} className="login-form"> - <TextField - label="Tävlingskod" - name="model.code" - helperText={formik.touched.model?.code && formik.touched.model?.code ? formik.errors.model?.code : ''} - error={Boolean(formik.touched.model?.code && formik.errors.model?.code)} - onChange={formik.handleChange} - onBlur={formik.handleBlur} - margin="normal" - /> - <Button type="submit" fullWidth variant="contained" color="primary" disabled={!formik.isValid}> - Anslut till tävling - </Button> - {formik.errors.error ? ( - <Alert severity="error"> - <AlertTitle>Error</AlertTitle> - {formik.errors.error} - </Alert> - ) : ( - <div /> - )} - </form> - )} - </Formik> - ) -} - -const useStyles = makeStyles((theme: Theme) => ({ - root: { - backgroundColor: theme.palette.background.paper, - }, -})) - -const LoginPage: React.FC = (props) => { - const classes = useStyles() - const [loginTab, setLoginTab] = React.useState(0) - return ( - <div className="login-page"> - <div className={classes.root}> - <AppBar position="static"> - <Tabs value={loginTab} onChange={(event, selectedTab) => setLoginTab(selectedTab)}> - <Tab label="Konto" id="simple-tab-0" /> - <Tab label="Tävling" id="simple-tab-1" /> - </Tabs> - </AppBar> - <LoginTab activeTab={loginTab} /> - </div> - </div> - ) -} - -export default withStyles(styles)(LoginPage) - -// TODO: Values carry over from email address to code and vice versa -// TODO: Errors in email address causes error messages in password field diff --git a/client/src/pages/admin/AdminPage.css b/client/src/pages/admin/AdminPage.css index a310f123..471dc7d3 100644 --- a/client/src/pages/admin/AdminPage.css +++ b/client/src/pages/admin/AdminPage.css @@ -1,5 +1,4 @@ .background { - background: linear-gradient(to top, #efd5ff 0%, #3d55b3 100%); height: 100%; } diff --git a/client/src/pages/admin/components/CompetitionManager.css b/client/src/pages/admin/components/CompetitionManager.css index 51817660..716dde28 100644 --- a/client/src/pages/admin/components/CompetitionManager.css +++ b/client/src/pages/admin/components/CompetitionManager.css @@ -1,9 +1,13 @@ .top-bar { display: flex; justify-content: space-between; - align-items: center; + align-items:flex-end; } .new-competition-button { - margin-bottom: 15px; + margin-bottom: 8px !important; } + +.remove-competition { + color:red !important; +} \ No newline at end of file diff --git a/client/src/pages/admin/components/CompetitionManager.tsx b/client/src/pages/admin/components/CompetitionManager.tsx index 160b32e4..b2a717e3 100644 --- a/client/src/pages/admin/components/CompetitionManager.tsx +++ b/client/src/pages/admin/components/CompetitionManager.tsx @@ -41,24 +41,24 @@ const BootstrapInput = withStyles((theme: Theme) => }) )(InputBase) -function createCompetition(name: string, region: string, year: number) { - return { name, region, year } +function createCompetition(name: string, region: string, year: number, id: number) { + return { name, region, year, id } } const competitions = [ - createCompetition('Tävling 1', 'Stockholm', 2021), - createCompetition('Tävling 2', 'Stockholm', 2020), - createCompetition('Tävling 3', 'Sala', 2020), - createCompetition('Tävling 4', 'Sundsvall', 2020), - createCompetition('Tävling 5', 'Linköping', 2020), - createCompetition('Tävling 6', 'Linköping', 2020), - createCompetition('Tävling 7', 'Sala', 2019), - createCompetition('Tävling 8', 'Stockholm', 2019), - createCompetition('Tävling 9', 'Stockholm', 2019), - createCompetition('Tävling 10', 'Lidköping', 2019), - createCompetition('Tävling 11', 'Stockholm', 2019), - createCompetition('Tävling 12', 'Sala', 2018), - createCompetition('Tävling 13', 'Tornby', 2018), + createCompetition('Tävling 1', 'Stockholm', 2021, 1), + createCompetition('Tävling 2', 'Stockholm', 2020, 2), + createCompetition('Tävling 3', 'Sala', 2020, 3), + createCompetition('Tävling 4', 'Sundsvall', 2020, 4), + createCompetition('Tävling 5', 'Linköping', 2020, 5), + createCompetition('Tävling 6', 'Linköping', 2020, 6), + createCompetition('Tävling 7', 'Sala', 2019, 7), + createCompetition('Tävling 8', 'Stockholm', 2019, 8), + createCompetition('Tävling 9', 'Stockholm', 2019, 9), + createCompetition('Tävling 10', 'Lidköping', 2019, 10), + createCompetition('Tävling 11', 'Stockholm', 2019, 11), + createCompetition('Tävling 12', 'Sala', 2018, 12), + createCompetition('Tävling 13', 'Tornby', 2018, 13), ] const regions = competitions @@ -86,6 +86,7 @@ const CompetitionManager: React.FC = (props) => { const yearInitialValue = 0 const regionInitialValue = '' const noFilterText = 'Alla' + const [searchInput, setSearchInput] = React.useState('') const [year, setYear] = React.useState(yearInitialValue) const [region, setRegion] = React.useState(regionInitialValue) const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { @@ -95,6 +96,11 @@ const CompetitionManager: React.FC = (props) => { const handleClose = () => { setAnchorEl(null) } + + const onSearchChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { + setSearchInput(event.target.value) + } + return ( <div> <div className="top-bar"> @@ -103,7 +109,7 @@ const CompetitionManager: React.FC = (props) => { <InputLabel shrink id="demo-customized-textbox"> Sök </InputLabel> - <BootstrapInput id="demo-customized-textbox" /> + <BootstrapInput id="demo-customized-textbox" onChange={onSearchChange} /> </FormControl> <FormControl className={classes.margin}> <InputLabel shrink id="demo-customized-select-native"> @@ -162,14 +168,15 @@ const CompetitionManager: React.FC = (props) => { <TableBody> {competitions .filter((row) => { + const nameOkay = row.name.match(RegExp(searchInput, 'i')) //Makes sure name matches search input case insensitively const yearOkay = year == yearInitialValue || row.year == year const regionOkay = region == regionInitialValue || row.region == region - return yearOkay && regionOkay + return yearOkay && regionOkay && nameOkay }) .map((row) => ( <TableRow key={row.name}> <TableCell scope="row"> - <Button color="primary" className="aa" component={Link} to="/123"> + <Button color="primary" component={Link} to={`/competition-id=${row.id}`}> {row.name} </Button> </TableCell> @@ -188,7 +195,9 @@ const CompetitionManager: React.FC = (props) => { <Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}> <MenuItem onClick={handleClose}>Starta</MenuItem> <MenuItem onClick={handleClose}>Duplicera</MenuItem> - <MenuItem onClick={handleClose}>Ta bort</MenuItem> + <MenuItem className="remove-competition" onClick={handleClose}> + Ta bort + </MenuItem> </Menu> </div> ) diff --git a/client/src/pages/LoginPage.css b/client/src/pages/login/LoginPage.css similarity index 100% rename from client/src/pages/LoginPage.css rename to client/src/pages/login/LoginPage.css diff --git a/client/src/pages/LoginPage.test.tsx b/client/src/pages/login/LoginPage.test.tsx similarity index 100% rename from client/src/pages/LoginPage.test.tsx rename to client/src/pages/login/LoginPage.test.tsx diff --git a/client/src/pages/login/LoginPage.tsx b/client/src/pages/login/LoginPage.tsx new file mode 100644 index 00000000..abb2e181 --- /dev/null +++ b/client/src/pages/login/LoginPage.tsx @@ -0,0 +1,47 @@ +import { AppBar, Tab, Tabs } from '@material-ui/core' +import { makeStyles, Theme } from '@material-ui/core/styles' +import React from 'react' +import AdminLogin from './components/AdminLogin' +import CompetitionLogin from './components/CompetitionLogin' +import './LoginPage.css' + +interface TabPanelProps { + activeTab: number +} + +function LoginContent(props: TabPanelProps) { + const { activeTab } = props + if (activeTab === 0) { + return <AdminLogin /> + } + return <CompetitionLogin /> +} + +const useStyles = makeStyles((theme: Theme) => ({ + root: { + backgroundColor: theme.palette.background.paper, + }, +})) + +const LoginPage: React.FC = (props) => { + const classes = useStyles() + const [loginTab, setLoginTab] = React.useState(0) + return ( + <div className="login-page"> + <div className={classes.root}> + <AppBar position="static"> + <Tabs value={loginTab} onChange={(event, selectedTab) => setLoginTab(selectedTab)}> + <Tab label="Konto" id="simple-tab-0" /> + <Tab label="Tävling" id="simple-tab-1" /> + </Tabs> + </AppBar> + <LoginContent activeTab={loginTab} /> + </div> + </div> + ) +} + +export default LoginPage + +// TODO: Values carry over from email address to code and vice versa +// TODO: Errors in email address causes error messages in password field diff --git a/client/src/pages/login/components/AdminLogin.tsx b/client/src/pages/login/components/AdminLogin.tsx new file mode 100644 index 00000000..77dc2a4c --- /dev/null +++ b/client/src/pages/login/components/AdminLogin.tsx @@ -0,0 +1,94 @@ +import { Button, TextField } from '@material-ui/core' +import { Alert, AlertTitle } from '@material-ui/lab' +import axios from 'axios' +import { Formik, FormikHelpers } from 'formik' +import React from 'react' +import * as Yup from 'yup' +import { AccountLoginModel } from '../../../interfaces/models' + +interface AccountLoginFormModel { + model: AccountLoginModel + error?: string +} + +interface ServerResponse { + code: number + message: string +} + +const accountSchema: Yup.SchemaOf<AccountLoginFormModel> = Yup.object({ + model: Yup.object() + .shape({ + email: Yup.string().email('Email inte giltig').required('Email krävs'), + password: Yup.string().required('Lösenord krävs').min(6, 'Lösenord måste vara minst 6 tecken'), + }) + .required(), + error: Yup.string().optional(), +}) + +const handleAccountSubmit = async (values: AccountLoginFormModel, actions: FormikHelpers<AccountLoginFormModel>) => { + await axios + .post<ServerResponse>(`users/login`, values.model) + .then((res) => { + actions.resetForm() + }) + .catch(({ response }) => { + console.log(response.data.message) + actions.setFieldError('error', response.data.message) + }) + .finally(() => { + actions.setSubmitting(false) + }) +} + +const AdminLogin: React.FC = (props) => { + const accountInitialValues: AccountLoginFormModel = { + model: { email: '', password: '' }, + } + return ( + <Formik initialValues={accountInitialValues} validationSchema={accountSchema} onSubmit={handleAccountSubmit}> + {(formik) => ( + <form onSubmit={formik.handleSubmit} className="login-form"> + <TextField + label="Email Adress" + name="model.email" + helperText={formik.touched.model?.email ? formik.errors.model?.email : ''} + error={Boolean(formik.touched.model?.email && formik.errors.model?.email)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + margin="normal" + /> + <TextField + label="Lösenord" + name="model.password" + type="password" + helperText={formik.touched.model?.password ? formik.errors.model?.password : ''} + error={Boolean(formik.touched.model?.password && formik.errors.model?.password)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + margin="normal" + /> + <Button + type="submit" + fullWidth + variant="contained" + color="primary" + disabled={!formik.isValid || !formik.touched.model?.email || !formik.touched.model?.email} + > + Logga in + </Button> + {formik.errors.error ? ( + <Alert severity="error"> + <AlertTitle>Error</AlertTitle> + {formik.errors.error} + </Alert> + ) : ( + <div /> + )} + </form> + )} + </Formik> + ) +} + +export default AdminLogin diff --git a/client/src/pages/login/components/CompetitionLogin.tsx b/client/src/pages/login/components/CompetitionLogin.tsx new file mode 100644 index 00000000..2450196d --- /dev/null +++ b/client/src/pages/login/components/CompetitionLogin.tsx @@ -0,0 +1,85 @@ +import { Button, TextField } from '@material-ui/core' +import { Alert, AlertTitle } from '@material-ui/lab' +import axios from 'axios' +import { Formik, FormikHelpers } from 'formik' +import React from 'react' +import * as Yup from 'yup' +import { CompetitionLoginModel } from '../../../interfaces/models' + +interface CompetitionLoginFormModel { + model: CompetitionLoginModel + error?: string +} + +interface ServerResponse { + code: number + message: string +} + +const competitionSchema: Yup.SchemaOf<CompetitionLoginFormModel> = Yup.object({ + model: Yup.object() + .shape({ + code: Yup.string().required('Mata in kod').min(6, 'Koden måste vara minst 6 tecken'), + }) + .required(), + error: Yup.string().optional(), +}) + +const handleCompetitionSubmit = async ( + values: CompetitionLoginFormModel, + actions: FormikHelpers<CompetitionLoginFormModel> +) => { + console.log(values.model) + await axios + .post<ServerResponse>(`users/login`, { code: values.model.code }) + .then((res) => { + actions.resetForm() + }) + .catch(({ response }) => { + console.log(response.data.message) + actions.setFieldError('error', response.data.message) + }) + .finally(() => { + actions.setSubmitting(false) + }) +} + +const CompetitionLogin: React.FC = (props) => { + const competitionInitialValues: CompetitionLoginFormModel = { + model: { code: '' }, + } + return ( + <Formik + initialValues={competitionInitialValues} + validationSchema={competitionSchema} + onSubmit={handleCompetitionSubmit} + > + {(formik) => ( + <form onSubmit={formik.handleSubmit} className="login-form"> + <TextField + label="Tävlingskod" + name="model.code" + helperText={formik.touched.model?.code && formik.touched.model?.code ? formik.errors.model?.code : ''} + error={Boolean(formik.touched.model?.code && formik.errors.model?.code)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + margin="normal" + /> + <Button type="submit" fullWidth variant="contained" color="primary" disabled={!formik.isValid}> + Anslut till tävling + </Button> + {formik.errors.error ? ( + <Alert severity="error"> + <AlertTitle>Error</AlertTitle> + {formik.errors.error} + </Alert> + ) : ( + <div /> + )} + </form> + )} + </Formik> + ) +} + +export default CompetitionLogin diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx new file mode 100644 index 00000000..e5f1366e --- /dev/null +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -0,0 +1,14 @@ +import { Typography } from '@material-ui/core' +import React from 'react' +import { useParams } from 'react-router-dom' + +interface CompetitionParams { + id: string +} + +const PresentationEditorPage: React.FC = (props) => { + const params: CompetitionParams = useParams() + return <Typography variant="h1">tävling: {params.id}</Typography> +} + +export default PresentationEditorPage diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx index c6c65d24..0051152b 100644 --- a/client/src/pages/views/JudgeViewPage.tsx +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -1,9 +1,7 @@ import React from 'react' -import './ViewSelectPage.css' const JudgeViewPage: React.FC = (props) => { return <div>Judge</div> - return <div>Judge</div> } export default JudgeViewPage -- GitLab From 1b1ee897b955befbff03bb36515fc5aa74f7092e Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Thu, 4 Mar 2021 17:09:25 +0100 Subject: [PATCH 08/10] remove todo --- client/src/pages/login/LoginPage.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/src/pages/login/LoginPage.tsx b/client/src/pages/login/LoginPage.tsx index abb2e181..776e64b1 100644 --- a/client/src/pages/login/LoginPage.tsx +++ b/client/src/pages/login/LoginPage.tsx @@ -42,6 +42,3 @@ const LoginPage: React.FC = (props) => { } export default LoginPage - -// TODO: Values carry over from email address to code and vice versa -// TODO: Errors in email address causes error messages in password field -- GitLab From abe2d723648381db4e131ba28df49795928c67c1 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Thu, 4 Mar 2021 17:44:31 +0100 Subject: [PATCH 09/10] fix tests not compiling --- .../src/pages/admin/components/CompetitionManager.test.tsx | 7 ++++++- client/src/pages/login/components/AdminLogin.test.tsx | 7 +++++++ .../src/pages/login/components/CompetitionLogin.test.tsx | 7 +++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 client/src/pages/login/components/AdminLogin.test.tsx create mode 100644 client/src/pages/login/components/CompetitionLogin.test.tsx diff --git a/client/src/pages/admin/components/CompetitionManager.test.tsx b/client/src/pages/admin/components/CompetitionManager.test.tsx index b7156f68..2a92138f 100644 --- a/client/src/pages/admin/components/CompetitionManager.test.tsx +++ b/client/src/pages/admin/components/CompetitionManager.test.tsx @@ -1,7 +1,12 @@ import { render } from '@testing-library/react' import React from 'react' +import { BrowserRouter } from 'react-router-dom' import CompetitionManager from './CompetitionManager' it('renders competition manager', () => { - render(<CompetitionManager />) + render( + <BrowserRouter> + <CompetitionManager /> + </BrowserRouter> + ) }) diff --git a/client/src/pages/login/components/AdminLogin.test.tsx b/client/src/pages/login/components/AdminLogin.test.tsx new file mode 100644 index 00000000..4e966c89 --- /dev/null +++ b/client/src/pages/login/components/AdminLogin.test.tsx @@ -0,0 +1,7 @@ +import { render } from '@testing-library/react' +import React from 'react' +import AdminLogin from './AdminLogin' + +it('renders admin login', () => { + render(<AdminLogin />) +}) diff --git a/client/src/pages/login/components/CompetitionLogin.test.tsx b/client/src/pages/login/components/CompetitionLogin.test.tsx new file mode 100644 index 00000000..29213c94 --- /dev/null +++ b/client/src/pages/login/components/CompetitionLogin.test.tsx @@ -0,0 +1,7 @@ +import { render } from '@testing-library/react' +import React from 'react' +import CompetitionLogin from './CompetitionLogin' + +it('renders competition login', () => { + render(<CompetitionLogin />) +}) -- GitLab From af44ab0b11ec8c2e42564c683caf0dad9611541c Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Mon, 8 Mar 2021 08:30:01 +0100 Subject: [PATCH 10/10] Added structure for presentation editor --- client/src/App.tsx | 5 +- client/src/Main.tsx | 2 +- .../admin/components/CompetitionManager.tsx | 2 +- .../PresentationEditorPage.css | 23 ++++ .../PresentationEditorPage.tsx | 118 +++++++++++++++++- .../components/CompetitionSettings.tsx | 70 +++++++++++ .../components/SettingsPanel.tsx | 36 ++++++ 7 files changed, 251 insertions(+), 5 deletions(-) create mode 100644 client/src/pages/presentationEditor/PresentationEditorPage.css create mode 100644 client/src/pages/presentationEditor/components/CompetitionSettings.tsx create mode 100644 client/src/pages/presentationEditor/components/SettingsPanel.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index c488843c..c80013fd 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,4 +1,5 @@ import { createMuiTheme, ThemeProvider } from '@material-ui/core' +import { teal } from '@material-ui/core/colors' import React from 'react' import './App.css' import Main from './Main' @@ -6,9 +7,11 @@ import Main from './Main' const theme = createMuiTheme({ palette: { primary: { - // Purple and green play nicely together. main: '#6200EE', }, + secondary: { + main: teal.A400, + }, }, }) diff --git a/client/src/Main.tsx b/client/src/Main.tsx index 5ad533a6..568d1010 100644 --- a/client/src/Main.tsx +++ b/client/src/Main.tsx @@ -14,7 +14,7 @@ const Main = () => { <Switch> <Route exact path="/" component={LoginPage} /> <Route path="/admin" component={AdminPage} /> - <Route path="/competition-id=:id" component={PresentationEditorPage} /> + <Route path="/editor/competition-id=:id" component={PresentationEditorPage} /> <Route exact path="/view" component={ViewSelectPage} /> <Route exact path="/view/participant" component={ParticipantViewPage} /> <Route exact path="/view/judge" component={JudgeViewPage} /> diff --git a/client/src/pages/admin/components/CompetitionManager.tsx b/client/src/pages/admin/components/CompetitionManager.tsx index b2a717e3..a85378ab 100644 --- a/client/src/pages/admin/components/CompetitionManager.tsx +++ b/client/src/pages/admin/components/CompetitionManager.tsx @@ -176,7 +176,7 @@ const CompetitionManager: React.FC = (props) => { .map((row) => ( <TableRow key={row.name}> <TableCell scope="row"> - <Button color="primary" component={Link} to={`/competition-id=${row.id}`}> + <Button color="primary" component={Link} to={`/editor/competition-id=${row.id}`}> {row.name} </Button> </TableCell> diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.css b/client/src/pages/presentationEditor/PresentationEditorPage.css new file mode 100644 index 00000000..2d3d142b --- /dev/null +++ b/client/src/pages/presentationEditor/PresentationEditorPage.css @@ -0,0 +1,23 @@ +.toolbar-container { + display:flex; + justify-content: space-between; +} + +.view-button { + margin-right: 8px; +} + +.view-button-group { + display: flex; + flex-direction: row; +} + +.slide-list-item { + text-align: center !important; + height: 60px; +} + +.right-drawer-tab { + height: 64px; + min-width: 130px !important; +} \ No newline at end of file diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index e5f1366e..c48e8958 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -1,14 +1,128 @@ -import { Typography } from '@material-ui/core' +import { Button, Divider, Typography } from '@material-ui/core' +import AppBar from '@material-ui/core/AppBar' +import CssBaseline from '@material-ui/core/CssBaseline' +import Drawer from '@material-ui/core/Drawer' +import List from '@material-ui/core/List' +import ListItem from '@material-ui/core/ListItem' +import ListItemText from '@material-ui/core/ListItemText' +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' +import Toolbar from '@material-ui/core/Toolbar' import React from 'react' import { useParams } from 'react-router-dom' +import SettingsPanel from './components/SettingsPanel' +import './PresentationEditorPage.css' + +function createSlide(name: string) { + return { name } +} + +const slides = [ + createSlide('Sida 1'), + createSlide('Sida 2'), + createSlide('Sida 3'), + createSlide('Sida 4'), + createSlide('Sida 5'), + createSlide('Sida 6'), + createSlide('Sida 7'), +] +const leftDrawerWidth = 150 +const rightDrawerWidth = 390 + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + display: 'flex', + }, + appBar: { + width: `calc(100% - ${rightDrawerWidth}px)`, + marginLeft: leftDrawerWidth, + marginRight: rightDrawerWidth, + }, + leftDrawer: { + width: leftDrawerWidth, + flexShrink: 0, + position: 'relative', + zIndex: 1, + }, + rightDrawer: { + width: rightDrawerWidth, + flexShrink: 0, + }, + leftDrawerPaper: { + width: leftDrawerWidth, + }, + rightDrawerPaper: { + width: rightDrawerWidth, + }, + // necessary for content to be below app bar + toolbar: theme.mixins.toolbar, + content: { + flexGrow: 1, + backgroundColor: theme.palette.background.default, + padding: theme.spacing(3), + }, + }) +) interface CompetitionParams { id: string } const PresentationEditorPage: React.FC = (props) => { + const classes = useStyles() const params: CompetitionParams = useParams() - return <Typography variant="h1">tävling: {params.id}</Typography> + return ( + <div className={classes.root}> + <CssBaseline /> + <AppBar position="fixed" className={classes.appBar}> + <Toolbar className="toolbar-container"> + <Typography variant="h6" noWrap> + Tävling nr: {params.id} + </Typography> + <div className="view-button-group"> + <Button className="view-button" variant="contained" color="secondary"> + Åskådarvy + </Button> + <Button className="view-button" variant="contained" color="secondary"> + Deltagarvy + </Button> + <Button className="view-button" variant="contained" color="secondary"> + Domarvy + </Button> + </div> + </Toolbar> + </AppBar> + <Drawer + className={classes.leftDrawer} + variant="permanent" + classes={{ + paper: classes.leftDrawerPaper, + }} + anchor="left" + > + <div className={classes.toolbar} /> + <Divider /> + <List> + {slides.map((slide, index) => ( + <ListItem className="slide-list-item" divider button key={slide.name}> + <ListItemText primary={slide.name} /> + </ListItem> + ))} + </List> + </Drawer> + <div className={classes.toolbar} /> + <Drawer + className={classes.rightDrawer} + variant="permanent" + classes={{ + paper: classes.rightDrawerPaper, + }} + anchor="right" + > + <SettingsPanel></SettingsPanel> + </Drawer> + </div> + ) } export default PresentationEditorPage diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx new file mode 100644 index 00000000..72478634 --- /dev/null +++ b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx @@ -0,0 +1,70 @@ +import { Button, Divider, List, ListItem, ListItemText, TextField } from '@material-ui/core' +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' +import CloseIcon from '@material-ui/icons/Close' +import React, { useState } from 'react' + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + textInputContainer: { + '& > *': { + margin: theme.spacing(1), + width: '100%', + }, + }, + textInput: { + margin: theme.spacing(2), + width: '87%', + }, + textCenter: { + textAlign: 'center', + }, + center: { + display: 'flex', + justifyContent: 'center', + }, + }) +) +interface TeamListItemProps { + name: string +} + +const CompetitionSettings: React.FC = (props) => { + const classes = useStyles() + const initialList = [ + { id: '1', name: 'Lag1' }, + { id: '2', name: 'Lag2' }, + { id: '3', name: 'Lag3' }, + ] + const handleClick = (id: string) => { + setTeams(teams.filter((item) => item.id !== id)) //Will not be done like this when api is used + } + const [teams, setTeams] = useState(initialList) + return ( + <div className={classes.textInputContainer}> + <form noValidate autoComplete="off"> + <TextField className={classes.textInput} id="outlined-basic" label="Tävlingsnamn" variant="outlined" /> + <Divider /> + <TextField className={classes.textInput} id="outlined-basic" label="Stad" variant="outlined" /> + </form> + <List> + <Divider /> + <ListItem> + <ListItemText className={classes.textCenter} primary="Lag" /> + </ListItem> + {teams.map((team) => ( + <div key={team.id}> + <ListItem divider button> + <ListItemText primary={team.name} /> + <CloseIcon onClick={() => handleClick(team.id)} /> + </ListItem> + </div> + ))} + <ListItem className={classes.center} button> + <Button>Lägg till lag</Button> + </ListItem> + </List> + </div> + ) +} + +export default CompetitionSettings diff --git a/client/src/pages/presentationEditor/components/SettingsPanel.tsx b/client/src/pages/presentationEditor/components/SettingsPanel.tsx new file mode 100644 index 00000000..946befe7 --- /dev/null +++ b/client/src/pages/presentationEditor/components/SettingsPanel.tsx @@ -0,0 +1,36 @@ +import { Tab, Tabs } from '@material-ui/core' +import AppBar from '@material-ui/core/AppBar' +import React from 'react' +import CompetitionSettings from './CompetitionSettings' + +interface TabPanelProps { + activeTab: number +} + +function TabContent(props: TabPanelProps) { + const { activeTab } = props + if (activeTab === 0) { + return <CompetitionSettings /> + } else if (activeTab === 1) { + return <div>2</div> + } + return <div>3</div> +} + +const SettingsPanel: React.FC = (props) => { + const [activeTab, setActiveTab] = React.useState(0) + return ( + <div> + <AppBar position="static"> + <Tabs value={activeTab} onChange={(event, val) => setActiveTab(val)} aria-label="simple tabs example"> + <Tab className="right-drawer-tab" label="Tävling" /> + <Tab className="right-drawer-tab" label="Sida" /> + <Tab className="right-drawer-tab" label="Stil" /> + </Tabs> + </AppBar> + <TabContent activeTab={activeTab} /> + </div> + ) +} + +export default SettingsPanel -- GitLab