Skip to content
Snippets Groups Projects
Commit 11b9712a authored by Albin Henriksson's avatar Albin Henriksson Committed by Victor Löfgren
Browse files

Resolve "Add competition login view"

parent 1c6ccf23
No related branches found
No related tags found
1 merge request!24Resolve "Add competition login view"
Pipeline #36080 passed
Showing
with 358 additions and 239 deletions
......@@ -22,6 +22,7 @@
"eslint.options": {
"configFile":"./.eslintrc"
},
"prettier.configPath": "./client/.prettierrc",
//git
"git.ignoreLimitWarning": true,
//language specific
......
{
"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
{
"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"
}
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>
)
}
......
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>
)
......
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
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)
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;
}
export interface LoginModel {
export interface AccountLoginModel {
email: string
password: string
}
export interface CompetitionLoginModel {
code: string
}
.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
}
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>
)
})
......@@ -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>
......
.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
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>
)
})
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
.login-page {
display: flex;
height: 100%;
justify-content: center;
align-items: center;
}
.login-form {
display: flex;
flex-direction: column;
width: 250px;
}
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 />)
})
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment