diff --git a/.vscode/settings.json b/.vscode/settings.json index 896cb9470959cc24e9d343173561d9d728f3b206..d98ed346857ad58d2b4422a2764fcc38485c1fe3 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 46369ae68df115758c772580e6f148f3956380a8..7e2cadb0f5dc6cbde1bcb39b5382f7f88f349dde 100644 --- a/client/.eslintrc +++ b/client/.eslintrc @@ -1,57 +1,26 @@ { - "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": { + "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 0cc6d643f022f337aea01cf93a3eb4d23b6cb136..4ff5f098aabbbd3a4fe56615df8a56dad7ef48bc 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 1e8c051e2d67e0a74f4ef26ab737a7427cf23b52..c488843cc955c038ec74ccd1bf6b1c9e9def4a5f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,15 +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 a44422fdc2886602b51f32c09c3a11c3fca3a8b7..5ad533a6203b5dc1399fa89381bb13d3a35180a6 100644 --- a/client/src/Main.tsx +++ b/client/src/Main.tsx @@ -1,14 +1,24 @@ import React from 'react' import { BrowserRouter, Route, Switch } from 'react-router-dom' -import AdminView from './components/AdminView' -import LoginForm from './components/Login' +import AdminPage from './pages/admin/AdminPage' +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' +import ViewSelectPage from './pages/views/ViewSelectPage' const Main = () => { return ( <BrowserRouter> <Switch> - <Route exact path="/" component={LoginForm} /> - <Route path="/admin" component={AdminView} /> + <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} /> + <Route exact path="/view/audience" component={AudienceViewPage} /> </Switch> </BrowserRouter> ) diff --git a/client/src/components/CompetitionManager.tsx b/client/src/components/CompetitionManager.tsx deleted file mode 100644 index 2d140fa35968cb8b227e5802b426fea7ce4bb144..0000000000000000000000000000000000000000 --- 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/components/Login.tsx b/client/src/components/Login.tsx deleted file mode 100644 index 91af9b78d06d4bdde2d1b42939bad82fe0f1c968..0000000000000000000000000000000000000000 --- a/client/src/components/Login.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { Button, TextField } from '@material-ui/core' -import { 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 * as Yup from 'yup' -import { LoginModel } from '../interfaces/models' -import './Login.css' - -const styles = {} - -interface LoginState { - status: number - message: string -} - -interface LoginFormModel { - model: LoginModel - error?: string -} - -interface ServerResponse { - code: number - message: string -} - -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') - }) - .required(), - error: Yup.string().optional() -}) - -const handleSubmit = async ( - values: LoginFormModel, - actions: FormikHelpers<LoginFormModel> -) => { - 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 LoginForm: React.FC = (props) => { - const [serverState, setServerState] = useState<LoginFormModel>() - const initialValues: LoginFormModel = { model: { email: '', password: '' } } - return ( - <div className="login-page"> - <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 : '' - } - 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} - > - Submit - </Button> - {formik.errors.error ? ( - <Alert severity="error"> - <AlertTitle>Error</AlertTitle> - {formik.errors.error} - </Alert> - ) : ( - <div /> - )} - </form> - )} - </Formik> - </div> - ) -} - -export default withStyles(styles)(LoginForm) diff --git a/client/src/index.css b/client/src/index.css index ec2585e8c0bb8188184ed1e0703c4c8f2a8419b0..7323ae85c542d8da74f197f1395de258e8a4c673 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/src/interfaces/models.ts b/client/src/interfaces/models.ts index 292554f4ac8c62b8525f679c3d127accea65752e..f2bc0b12cf3151d9fde320d2a981a88859f664b1 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 +} diff --git a/client/src/components/AdminView.css b/client/src/pages/admin/AdminPage.css similarity index 56% rename from client/src/components/AdminView.css rename to client/src/pages/admin/AdminPage.css index 1ad0b7ee6dd4647d7f8f7d07b6853ed5ceac4026..471dc7d3d7a3b865f32c4e0d99b9bba73df2c5cb 100644 --- a/client/src/components/AdminView.css +++ b/client/src/pages/admin/AdminPage.css @@ -1,10 +1,9 @@ .background { - background: linear-gradient(to top, #efd5ff 0%, #3d55b3 100%); height: 100%; } .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.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 cdc460499c777bd4ee6a456b93d751272e98ebeb..b29b78f82bb88d7489a9a09ef071693acbfde654 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 79% rename from client/src/components/AdminView.tsx rename to client/src/pages/admin/AdminPage.tsx index 54551b640789e676461065c686d424bafa290efd..a95bf04ee0c8c50e298c859e345a153d93ce6d8a 100644 --- a/client/src/components/AdminView.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -9,16 +9,16 @@ 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' 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'] @@ -26,36 +26,34 @@ 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), + }, }) ) 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 /> @@ -70,7 +68,7 @@ const AdminView: React.FC = (props) => { className={(classes.drawer, 'background')} variant="permanent" classes={{ - paper: classes.drawerPaper + paper: classes.drawerPaper, }} anchor="left" > @@ -87,9 +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> ))} @@ -97,14 +93,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/pages/admin/components/CompetitionManager.css b/client/src/pages/admin/components/CompetitionManager.css new file mode 100644 index 0000000000000000000000000000000000000000..716dde28317c6d6da62f3ddcb72021cec5d31ed8 --- /dev/null +++ b/client/src/pages/admin/components/CompetitionManager.css @@ -0,0 +1,13 @@ +.top-bar { + display: flex; + justify-content: space-between; + align-items:flex-end; +} + +.new-competition-button { + margin-bottom: 8px !important; +} + +.remove-competition { + color:red !important; +} \ No newline at end of file diff --git a/client/src/components/CompetitionManager.test.tsx b/client/src/pages/admin/components/CompetitionManager.test.tsx similarity index 56% rename from client/src/components/CompetitionManager.test.tsx rename to client/src/pages/admin/components/CompetitionManager.test.tsx index b7156f680329a98c468dfa3a751448191a8b088b..2a92138fe288429a93c9a2d015af2d08cf93f58f 100644 --- a/client/src/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/admin/components/CompetitionManager.tsx b/client/src/pages/admin/components/CompetitionManager.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b2a717e30fe5b0595f6ebb3143387988a9476d7b --- /dev/null +++ b/client/src/pages/admin/components/CompetitionManager.tsx @@ -0,0 +1,206 @@ +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, id: number) { + return { name, region, year, id } +} + +const competitions = [ + 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 + .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 [searchInput, setSearchInput] = React.useState('') + 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) + } + + const onSearchChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { + setSearchInput(event.target.value) + } + + 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" onChange={onSearchChange} /> + </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 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 && nameOkay + }) + .map((row) => ( + <TableRow key={row.name}> + <TableCell scope="row"> + <Button color="primary" component={Link} to={`/competition-id=${row.id}`}> + {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 className="remove-competition" 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/components/Login.css b/client/src/pages/login/LoginPage.css similarity index 75% rename from client/src/components/Login.css rename to client/src/pages/login/LoginPage.css index 5a01047d29ff819bb1581e3434f2509620ab6aed..abefd48f971cdf9fdf2f072a552aaaf6a7b18713 100644 --- a/client/src/components/Login.css +++ b/client/src/pages/login/LoginPage.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.test.tsx b/client/src/pages/login/LoginPage.test.tsx similarity index 64% rename from client/src/components/Login.test.tsx rename to client/src/pages/login/LoginPage.test.tsx index 12a6c79c133e96598a6e583ddcbd555e1799df97..ae41ba025840c938774f018789438941ba368cdc 100644 --- a/client/src/components/Login.test.tsx +++ b/client/src/pages/login/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/login/LoginPage.tsx b/client/src/pages/login/LoginPage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..776e64b1b9582ddbd000470d63089994144d8777 --- /dev/null +++ b/client/src/pages/login/LoginPage.tsx @@ -0,0 +1,44 @@ +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 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 0000000000000000000000000000000000000000..4e966c895cd9deee7b738b6eaee9e979e60ddf0d --- /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/AdminLogin.tsx b/client/src/pages/login/components/AdminLogin.tsx new file mode 100644 index 0000000000000000000000000000000000000000..77dc2a4ccd1d500221cb4c5a3ce0f3a2972dbb7d --- /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.test.tsx b/client/src/pages/login/components/CompetitionLogin.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..29213c943578afb01b27199d0a3fcb5f2db44092 --- /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 />) +}) diff --git a/client/src/pages/login/components/CompetitionLogin.tsx b/client/src/pages/login/components/CompetitionLogin.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2450196d0cf5166a4d295417c69ef3654196552f --- /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 0000000000000000000000000000000000000000..e5f1366ef74167317e53f52b8ed9b804ce6531f0 --- /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/AudienceViewPage.tsx b/client/src/pages/views/AudienceViewPage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f0cd70bc18d1132c0054ff3242ef04e4a35e4fb0 --- /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 0000000000000000000000000000000000000000..0051152b6327bd9df8257733b4e8628be21bd0c0 --- /dev/null +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +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 0000000000000000000000000000000000000000..6a22cd4477e91a9a3d7f8d82bb36363c8c38a990 --- /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 0000000000000000000000000000000000000000..920c6a342fa49a6a2ce9d205effa14e6e0d74a7c --- /dev/null +++ b/client/src/pages/views/ViewSelectPage.css @@ -0,0 +1,20 @@ +.root { + display: flex; + justify-content: center; + margin-top: 12%; + height: 100%; +} + +.button-group { + 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 0000000000000000000000000000000000000000..81decf7cdfb52b9d586edd2e92922eba7f3f58a9 --- /dev/null +++ b/client/src/pages/views/ViewSelectPage.tsx @@ -0,0 +1,25 @@ +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"> + <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> + ) +} + +export default ViewSelectPage diff --git a/client/tsconfig.json b/client/tsconfig.json index a273b0cfc0e965c35524e3cd0d3574cbe1ad2d0d..7f162ff3f6dbd3b8e60e6de8503fac0937d8f423 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" ] }