diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..4fc1d92bfc87de8bab2d806efca8c1e6114b07d0 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Debug CRA Tests", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts", + "args": ["test", "--runInBand", "--no-cache", "--watchAll=false"], + "cwd": "${workspaceRoot}", + "protocol": "inspector", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "env": { "CI": "true" }, + "disableOptimisticBPs": true + } + ] +} diff --git a/README.md b/README.md index 6017106ae4f8fd3ebb1776acae45dd027b602bdb..01731c749d795b65abf923f9836afa15d5945775 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +## Color Theme: + +https://www.fu-berlin.de/sites/corporate-design/grundlagen/farben/index.html + This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template. ## Available Scripts diff --git a/package-lock.json b/package-lock.json index 21fde50ec5942558e9f4ff3fe5bc4fbc8b156e9b..5b2d1a9087e59146caacf949a7c2ff65c04b14cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7891,16 +7891,11 @@ "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, "history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.0.0.tgz", + "integrity": "sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==", "requires": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" + "@babel/runtime": "^7.7.6" } }, "hmac-drbg": { @@ -14445,6 +14440,21 @@ "react-is": "^16.6.0", "tiny-invariant": "^1.0.2", "tiny-warning": "^1.0.0" + }, + "dependencies": { + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + } } }, "react-router-dom": { @@ -14459,6 +14469,21 @@ "react-router": "5.2.0", "tiny-invariant": "^1.0.2", "tiny-warning": "^1.0.0" + }, + "dependencies": { + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + } } }, "react-scripts": { diff --git a/package.json b/package.json index ba6508f73fe2419635ea73ad74abdcd851d85dd3..201a3f95a54b495f31a97a243d497a1726bd15cc 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@types/react": "^16.9.0", "@types/react-dom": "^16.9.0", "@types/react-redux": "^7.1.7", + "history": "^5.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-redux": "^7.2.0", @@ -25,6 +26,7 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", + "test:debug": "react-scripts --inspect-brk test --runInBand --no-cache", "eject": "react-scripts eject", "lint": "eslint . -c .eslint-rc.js", "lint-fix": "eslint . -c .eslint-rc.js --fix", diff --git a/src/App.css b/src/App.css index b4d6bf7278923d22c651287f74a61c3c713e0d0c..d81843d65ecc573bfd05e00abfb3e76c44cafccd 100644 --- a/src/App.css +++ b/src/App.css @@ -1,3 +1,11 @@ +html, +body { + margin: 0; + padding: 0; + width: 100vw; + min-height: 100vh; +} + #login-view { display: flex; justify-content: center; diff --git a/src/App.test.tsx b/src/App.test.tsx index c2b185148d663a660dd63ea5393257d680c8939b..62271ab3516b22a5cabfb098d9de1f18ddd7c12e 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,15 +1,52 @@ import React from 'react' -import { render } from '@testing-library/react' +import { act, render, RenderResult, wait } from '@testing-library/react' import { Provider } from 'react-redux' import { store } from './redux/store' import App from './App' +import { MemoryRouter, Route } from 'react-router' +import { Location } from 'history' -test('renders learn react link', () => { - const { getByText } = render( +test('renders user page', () => { + const renderedApp = render( <Provider store={store}> - <App /> + <MemoryRouter> + <App /> + </MemoryRouter> </Provider> ) - expect(getByText('UniSport')).toBeInTheDocument() + expect(renderedApp.getByText('Unisport User Forntend')).toBeInTheDocument() +}) + +test('admin page navigation', async () => { + let renderedApp: RenderResult + let testLocation: Location + act(() => { + renderedApp = render( + <Provider store={store}> + <MemoryRouter initialEntries={['/admin']}> + <App /> + <Route + path="*" + render={({ history, location }) => { + testLocation = location + return null + }} + /> + </MemoryRouter> + </Provider> + ) + }) + + await wait() + + let drawer = renderedApp!.container.querySelector('#admin-drawer') + expect(drawer).not.toBeNull() + + act(() => { + const btn = renderedApp.getByText('Sportarten') + btn.click() + }) + await wait() + expect(testLocation!?.pathname).toBe('/admin/sportarten') }) diff --git a/src/App.tsx b/src/App.tsx index e95077ca742677faaf9966d5cf347a047fa06963..a49749414e4cbcdfa8a8b213d85c9aa7ef206113 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,23 +1,43 @@ +import { createMuiTheme, Paper, useMediaQuery } from '@material-ui/core' +import { ThemeProvider } from '@material-ui/styles' import React, { Suspense } from 'react' -import { BrowserRouter as Router, Switch, Route } from 'react-router-dom' +import { Switch, Route } from 'react-router-dom' import UserHome from './components/user/homeComponent' const AdminComponent = React.lazy(() => import('./components/admin')) function App() { + // use browser history by default, can be overwritten for testing + // code from https://material-ui.com/customization/palette/ + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)') + + const theme = React.useMemo( + () => + createMuiTheme({ + palette: { + type: prefersDarkMode ? 'dark' : 'light' + } + }), + [prefersDarkMode] + ) + return ( - <Router> - <Switch> - <Route path="/admin"> - <Suspense fallback={<div>Loading AdminComponent...</div>}> - <AdminComponent /> - </Suspense> - </Route> - <Route path=""> - <UserHome /> - </Route> - </Switch> - </Router> + <ThemeProvider theme={theme}> + <Paper style={{ width: '100vw', minHeight: '100vh' }}> + <Switch> + <Route path="/admin"> + <Suspense + fallback={<div>Loading AdminComponent...</div>} + > + <AdminComponent /> + </Suspense> + </Route> + <Route path=""> + <UserHome /> + </Route> + </Switch> + </Paper> + </ThemeProvider> ) } diff --git a/src/components/admin/Navbar.test.tsx b/src/components/admin/Navbar.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0da4ba3f3da3d9df96e901b4ed21cc3ca01fee2f --- /dev/null +++ b/src/components/admin/Navbar.test.tsx @@ -0,0 +1,23 @@ +import { render, wait } from '@testing-library/react' +import { createMemoryHistory } from 'history' +import React from 'react' +import { Router } from 'react-router' +import { Navbar } from './navbar' + +describe('admin/Navbar', () => { + it('should route to correct paths', async () => { + const history = createMemoryHistory({ + initialEntries: ['/admin/sportarten'] + }) + const renderedNavbar = render( + <Router history={history}> + <Navbar></Navbar> + </Router> + ) + + await wait() + + let drawer = renderedNavbar.container.querySelector('#admin-drawer') + expect(drawer).not.toBeNull() + }) +}) diff --git a/src/components/admin/fragen/Question.tsx b/src/components/admin/fragen/Question.tsx index 897a7e11db08e3569c4689d4bd746f072ac9b0c0..5b43910f2b5ed3d1acf0d589a217afd68796669b 100644 --- a/src/components/admin/fragen/Question.tsx +++ b/src/components/admin/fragen/Question.tsx @@ -34,7 +34,7 @@ type QuestionProp = { activ: boolean } -export const Question: React.FC<QuestionProp> = ({ activ }) => { +export const Question: React.FC<QuestionProp> = ({ activ }: QuestionProp) => { const classes = useStyles() const [open, setOpen] = useState(false) return ( @@ -73,7 +73,7 @@ export const Question: React.FC<QuestionProp> = ({ activ }) => { <List dense={true}> {languages.map((entry) => { return ( - <ListItem> + <ListItem key={entry}> <ListItemText>{entry}</ListItemText>{' '} <TextField id="outlined-basic" diff --git a/src/components/admin/fragen/QuestionList.tsx b/src/components/admin/fragen/QuestionList.tsx index af415f656c91edfa8315ed70679c9a15aa278f79..b07d81778d7c26e8aee1afb2b0d6f4797c43d5ad 100644 --- a/src/components/admin/fragen/QuestionList.tsx +++ b/src/components/admin/fragen/QuestionList.tsx @@ -19,8 +19,10 @@ export const QuestionList: React.FC = () => { <div> <h2>Aktive Fragen:</h2> <List> - {ar.map(() => ( - <div> + {ar.map(( + data // TODO: better key + ) => ( + <div key={data}> <ListItem> <Question activ={true} /> </ListItem> @@ -30,8 +32,10 @@ export const QuestionList: React.FC = () => { </List> <h2>Inaktive Fragen:</h2> <List> - {ar.map(() => ( - <div> + {ar.map(( + data // TODO: better key + ) => ( + <div key={data}> <ListItem> <Question activ={false} /> </ListItem> diff --git a/src/components/admin/navbar.tsx b/src/components/admin/navbar.tsx index 69f589cc7aa1957589db2022a601e64509fc57d4..1a65f79669a6bd6bde8a096756f01f21985113ef 100644 --- a/src/components/admin/navbar.tsx +++ b/src/components/admin/navbar.tsx @@ -1,4 +1,9 @@ -import { Link, useRouteMatch } from 'react-router-dom' +import { + useRouteMatch, + LinkProps as RouterLinkProps, + useLocation, + NavLink +} from 'react-router-dom' import React from 'react' import Drawer from '@material-ui/core/Drawer' import List from '@material-ui/core/List' @@ -9,56 +14,97 @@ import Filter3Icon from '@material-ui/icons/Filter3' import Filter4Icon from '@material-ui/icons/Filter4' import Filter5Icon from '@material-ui/icons/Filter5' import Filter6Icon from '@material-ui/icons/Filter6' -import Divider from "@material-ui/core/Divider"; +import Divider from '@material-ui/core/Divider' +import { ListItemIcon, ListItemText } from '@material-ui/core' -const drawerWidth = 180 +// from https://material-ui.com/guides/composition/#link +interface ListItemLinkProps { + icon: React.ReactElement + primary: string + to: string +} + +const ListItemLink: React.FC<ListItemLinkProps> = ( + props: ListItemLinkProps +) => { + const { icon, primary, to } = props + + const renderLink = React.useMemo( + () => + // inner function must have a name + React.forwardRef<any, Omit<RouterLinkProps, 'to'>>( + function RenderedLink(itemProps, ref) { + return <NavLink to={to} ref={ref} {...itemProps} /> + } + ), + [to] + ) + + renderLink.displayName = 'RenderLink' + + let location = useLocation() + + return ( + <li> + <ListItem + button + component={renderLink} + selected={location.pathname === to} + > + <ListItemIcon>{icon}</ListItemIcon> + <ListItemText primary={primary} /> + </ListItem> + </li> + ) +} + +const drawerWidth = '220px' export const Navbar: React.FC = () => { let { url } = useRouteMatch() + return ( - <Drawer variant="permanent" style={{ width: drawerWidth }}> - <List> - <ListItem> - <Link to={`${url}/sportarten`}> - {' '} - <Filter1Icon /> Sportarten{' '} - </Link> - </ListItem> + <Drawer + id="admin-drawer" + variant="permanent" + style={{ width: drawerWidth }} + > + <List style={{ width: drawerWidth }}> + <ListItemLink + to={`${url}/sportarten`} + primary="Sportarten" + icon={<Filter1Icon />} + /> <Divider /> - <ListItem> - <Link to={`${url}/statistiken`}> - {' '} - <Filter2Icon /> Statistik{' '} - </Link> - </ListItem> + <ListItemLink + to={`${url}/statistiken`} + primary="Statistik" + icon={<Filter2Icon />} + /> <Divider /> - <ListItem> - <Link to={`${url}/fragen`}> - {' '} - <Filter3Icon /> Fragen{' '} - </Link> - </ListItem> + <ListItemLink + to={`${url}/fragen`} + primary="Fragen" + icon={<Filter3Icon />} + /> <Divider /> - <ListItem> - <Link to={`${url}/synchronisieren`}> - {' '} - <Filter4Icon /> Synchronisieren{' '} - </Link> - </ListItem> + <ListItemLink + to={`${url}/synchronisieren`} + primary="Synchronisieren" + icon={<Filter4Icon />} + /> <Divider /> - <ListItem> - <Link to={`${url}/snacks`}> - {' '} - <Filter5Icon /> Snacks & Co{' '} - </Link> - </ListItem> + <ListItemLink + to={`${url}/snacks`} + primary="Snacks & Co" + icon={<Filter5Icon />} + /> <Divider /> - <ListItem> - <Link to={`${url}/reinfolge`}> - {' '} - <Filter6Icon /> Reinfolge{' '} - </Link> - </ListItem> + <ListItemLink + to={`${url}/reinfolge`} + primary="Reinfolge" + icon={<Filter6Icon />} + /> <Divider /> </List> </Drawer> diff --git a/src/components/admin/snack & co/Snack.tsx b/src/components/admin/snack & co/Snack.tsx index 7ed20650669f10b390acb85a9c07e1d04f15cb52..bef0848f8f5608822995ce3f9c77a39487036b9f 100644 --- a/src/components/admin/snack & co/Snack.tsx +++ b/src/components/admin/snack & co/Snack.tsx @@ -15,7 +15,7 @@ type SnackProp = { titel: string } -export const Snack: React.FC<SnackProp> = ({ typ, titel }) => { +export const Snack: React.FC<SnackProp> = ({ typ, titel }: SnackProp) => { return ( <Grid container direction="row"> <Button> diff --git a/src/components/user/homeComponent.tsx b/src/components/user/homeComponent.tsx index 6ddbcb0d9d95bafc52acee0ec86cd5049a3ebccd..98795659d4cfc9d9b8b30abc03baf2fd7f26b960 100644 --- a/src/components/user/homeComponent.tsx +++ b/src/components/user/homeComponent.tsx @@ -1,10 +1,11 @@ import React from 'react' import Button from '@material-ui/core/Button' +import { Typography } from '@material-ui/core' const UserHome = () => { return ( <div> - <h1 style={{ color: 'green' }}>UniSport</h1> + <Typography>Unisport User Forntend</Typography> Placeholder for the page that will allow the user to start the quiz <Button variant="contained" color="primary"> Hello World :D diff --git a/src/index.tsx b/src/index.tsx index 99d4acbcaf05e12a49bfe0da9126a4244fb6436f..9f97e7985f6ec7599ec3b7a7c99594f85987a47e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,11 +5,14 @@ import { store } from './redux/store' import { Provider } from 'react-redux' import * as serviceWorker from './serviceWorker' import './App.css' +import { BrowserRouter } from 'react-router-dom' ReactDOM.render( <React.StrictMode> <Provider store={store}> - <App /> + <BrowserRouter> + <App /> + </BrowserRouter> </Provider> </React.StrictMode>, document.getElementById('root')