diff options
| -rw-r--r-- | package-lock.json | 12 | ||||
| -rw-r--r-- | package.json | 18 | ||||
| -rw-r--r-- | src/App.js | 93 | ||||
| -rw-r--r-- | src/App.test.js | 8 | ||||
| -rw-r--r-- | src/CustomThemeProvider.js | 45 | ||||
| -rw-r--r-- | src/components/NavBar/index.js | 100 | ||||
| -rw-r--r-- | src/hooks/useLoginState.js | 33 | ||||
| -rw-r--r-- | src/pages/Login/index.js | 52 | ||||
| -rw-r--r-- | src/pages/NewSurvey/index.js | 3 | ||||
| -rw-r--r-- | src/pages/SurveyAssignees/index.js | 3 | ||||
| -rw-r--r-- | src/pages/SurveyResults/index.js | 3 | ||||
| -rw-r--r-- | src/pages/Surveys/index.js | 3 | ||||
| -rw-r--r-- | src/pages/Users/index.js | 3 | 
13 files changed, 349 insertions, 27 deletions
| diff --git a/package-lock.json b/package-lock.json index ccbce15..c75f8f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,25 @@        "name": "repeated-surveyer-frontend",        "version": "0.1.0",        "dependencies": { +        "@emotion/react": "^11.11.1", +        "@emotion/styled": "^11.11.0", +        "@mui/icons-material": "^5.14.6", +        "@mui/material": "^5.14.6",          "@testing-library/jest-dom": "^5.17.0",          "@testing-library/react": "^13.4.0",          "@testing-library/user-event": "^13.5.0",          "react": "^18.2.0",          "react-dom": "^18.2.0", +        "react-router-dom": "^6.15.0",          "react-scripts": "5.0.1",          "web-vitals": "^2.1.4" +      }, +      "devDependencies": { +        "eslint": "^8.48.0", +        "eslint-config-prettier": "^9.0.0", +        "eslint-plugin-prettier": "^5.0.0", +        "eslint-plugin-react": "^7.33.2", +        "prettier": "^3.0.2"        }      },      "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index 1fc68ff..6ca7e08 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,16 @@    "version": "0.1.0",    "private": true,    "dependencies": { +    "@emotion/react": "^11.11.1", +    "@emotion/styled": "^11.11.0", +    "@mui/icons-material": "^5.14.6", +    "@mui/material": "^5.14.6",      "@testing-library/jest-dom": "^5.17.0",      "@testing-library/react": "^13.4.0",      "@testing-library/user-event": "^13.5.0",      "react": "^18.2.0",      "react-dom": "^18.2.0", +    "react-router-dom": "^6.15.0",      "react-scripts": "5.0.1",      "web-vitals": "^2.1.4"    }, @@ -23,6 +28,12 @@        "react-app/jest"      ]    }, +  "prettier": { +    "singleQuote": true, +    "semi": true, +    "tabWidth": 2, +    "useTabs": false +  },    "browserslist": {      "production": [        ">0.2%", @@ -34,5 +45,12 @@        "last 1 firefox version",        "last 1 safari version"      ] +  }, +  "devDependencies": { +    "eslint": "^8.48.0", +    "eslint-config-prettier": "^9.0.0", +    "eslint-plugin-prettier": "^5.0.0", +    "eslint-plugin-react": "^7.33.2", +    "prettier": "^3.0.2"    }  } @@ -1,25 +1,80 @@ -import logo from './logo.svg';  import './App.css'; +import { RouterProvider, createBrowserRouter, Navigate } from 'react-router-dom'; +import Login from './pages/Login'; +import Surveys from './pages/Surveys'; +import NewSurvey from './pages/NewSurvey'; +import SurveyResults from './pages/SurveyResults'; +import SurveyAssignees from './pages/SurveyAssignees'; +import Users from './pages/Users'; +import NavBar from './components/NavBar'; +import useLoginState from './hooks/useLoginState'; +import CssBaseline from '@mui/material/CssBaseline'; +import CustomThemeProvider from './CustomThemeProvider'; -function App() { +function routes({ login, logout, isLoggedIn }) { +   +  function withNavBar(component) { +    const navbarLinks = [ +      { label: 'Surveys', link: '/surveys' }, +      { label: 'New Survey', link: '/surveys/new' }, +      { label: 'Users', link: '/users' }, +    ]; +    return (<> +      <NavBar isLoggedIn={isLoggedIn} pages={navbarLinks} logout={logout} /> +      {component} +    </>); +  } + +  if (!isLoggedIn) { +    return ([ +      { +        path: '*', +        element: <Login login={login}/>, +      }, +    ]); +  } else { +    return ([ +      { +        path: '/', +        element: <Navigate to={{ pathname: '/surveys' }} />, +      }, +      { +        path: '/surveys', +        element: withNavBar(<Surveys />), +      }, +      { +        path: '/surveys/new', +        element: withNavBar(<NewSurvey />), +      }, +      { +        path: '/surveys/:surveyId/results', +        element: withNavBar(<SurveyResults />), +      }, +      { +        path: '/surveys/:surveyId/assignees', +        element: withNavBar(<SurveyAssignees />), +      }, +      { +        path: '/users', +        element: withNavBar(<Users />), +      }, +    ]); +  } + +} + +export default function App() { +  const { login, logout, isLoggedIn } = useLoginState(); +  const currentRoutes = routes({isLoggedIn, logout, login});    return ( -    <div className="App"> -      <header className="App-header"> -        <img src={logo} className="App-logo" alt="logo" /> -        <p> -          Edit <code>src/App.js</code> and save to reload. -        </p> -        <a -          className="App-link" -          href="https://reactjs.org" -          target="_blank" -          rel="noopener noreferrer" -        > -          Learn React -        </a> -      </header> -    </div> +    <> +      <CssBaseline /> +      <CustomThemeProvider> +        <RouterProvider router={ +          createBrowserRouter(currentRoutes) +        }/> +      </CustomThemeProvider> +    </>    );  } -export default App; diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index 1f03afe..0000000 --- a/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { -  render(<App />); -  const linkElement = screen.getByText(/learn react/i); -  expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/CustomThemeProvider.js b/src/CustomThemeProvider.js new file mode 100644 index 0000000..f3f4a26 --- /dev/null +++ b/src/CustomThemeProvider.js @@ -0,0 +1,45 @@ +import {ThemeProvider} from '@mui/material'; +import {createTheme} from '@mui/material'; + +export default function CustomThemeProvider({children}) { +  console.log('theme', theme); +  return <ThemeProvider theme={theme}> +    { children } +  </ThemeProvider>; +} + +const themeOptions = { +  palette: { +    mode: 'light', +    primary: { +      main: '#0b0b14', +    }, +    secondary: { +      main: '#9c27b0', +    }, +  }, +  overrides: { +    MuiAppBar: { +      colorInherit: { +        backgroundColor: '#689f38', +        color: '#fff', +      }, +    }, +    MuiButton: { +      root: { +        background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)', +        border: 0, +        borderRadius: 3, +        boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)', +        color: 'white', +        height: 48, +        padding: '0 30px', +      }, +    }, +  }, +}; + + +const theme = createTheme(themeOptions); + + diff --git a/src/components/NavBar/index.js b/src/components/NavBar/index.js new file mode 100644 index 0000000..0bbc550 --- /dev/null +++ b/src/components/NavBar/index.js @@ -0,0 +1,100 @@ +import {useState} from 'react'; +import { Link } from 'react-router-dom'; +import {AppBar, Box, Button, Drawer, IconButton, List, ListItem, ListItemButton, ListItemText, Toolbar} from '@mui/material'; +import {Menu} from '@mui/icons-material'; + +export default function NavBar({ isLoggedIn, pages, logout }) { +  const drawerWidth = 200; + +  const [mobileOpen, setMobileOpen] = useState(false); + +  const handleDrawerToggle = () => { +    setMobileOpen(!mobileOpen); +  }; + +  return isLoggedIn && +    <Box sx={{ display: 'flex' }}> +      <AppBar +        component='nav' +        sx={{position: 'initial'}}> +        <Toolbar> +          <IconButton +            color="inherit" +            aria-label="open drawer" +            edge="start" +            onClick={handleDrawerToggle} +            sx={{ mr: 2, display: { sm: 'none' } }} +          > +            <Menu /> +          </IconButton> +          <Box sx={{ display: { xs: 'none', sm: 'block' } }}> +            {pages.map(p =>  +              p.hidden ||  +<Link key={p.link} to={p.link} style={{textDecorationLine:'none'}}> +  <Button key={p.label} sx={{ textDecorationLine:'none', color: '#ffffff' }}> +    {p.label} +  </Button> +</Link> +            )} +            <Button onClick={logout} sx={{color:'#ffffff'}}> +            Logout +            </Button> +          </Box> +        </Toolbar> +      </AppBar> + + +      <Box component="nav"> +        <Drawer +          sx={{ +            width: drawerWidth, +            flexShrink: 0, +            '& .MuiDrawer-paper': { +              width: drawerWidth, +              boxSizing: 'border-box', +            }, + +            display: { xs: 'block', sm: 'none' }, +          }} +          variant='temporary' +          anchor='left' +          open={mobileOpen} +          onClose={handleDrawerToggle} +          ModalProps={{ +            keepMounted:true, +          }} +        > +          <Box onClick={handleDrawerToggle} sx={{ textAlign: 'center' }}> +            <List> +              {pages.map(({label, link, hidden}) => ( +                hidden ? <div key={link}></div> : +                  <ListItem key={link} disablePadding> + +                    <Link to={link} style={{textDecorationLine:'none', width: '100%'}}> +                      <ListItemButton +                        key={label} +                        sx={{ +                          width:'100%', +                          color: 'primary.main', +                          textAlign: 'left', +                        }} +                      > +                        <ListItemText primary={label} sx={{ width:'100%'}} /> +                      </ListItemButton> +                    </Link> +                  </ListItem> +              ))} +              <ListItem disablePadding> +                <ListItemButton +                  onClick={logout} +                  sx={{width:'100%', color: 'primary.main' }} +                > +                  <ListItemText primary={'Logout'}/> +                </ListItemButton> +              </ListItem> +            </List> +          </Box> +        </Drawer> +      </Box> +    </Box>; +} diff --git a/src/hooks/useLoginState.js b/src/hooks/useLoginState.js new file mode 100644 index 0000000..b7c7221 --- /dev/null +++ b/src/hooks/useLoginState.js @@ -0,0 +1,33 @@ +import {useState} from 'react'; + +export default function useLoginState() { +  const [userInfo, setUserInfoState] = useState( +    localStorage.userInfo ? +      JSON.parse(localStorage.userInfo) : +      {} +  ); + +  function setUserInfo(userInfo) { +    setUserInfoState(userInfo); +    localStorage.userInfo = JSON.stringify(userInfo || {}); +  } + +  async function login(username, password) { +    console.log(`logging in: ${username}, ${password}`); +    // const userInfo = await api.login() +    const userInfo = { +      username +    }; +    console.log('Login success'); +    setUserInfo(userInfo); +    return userInfo; +  }  + +  function logout() { +    setUserInfo({}); +  } + +  const isLoggedIn = !!userInfo?.username; +   +  return {userInfo, isLoggedIn, login, logout}; +} diff --git a/src/pages/Login/index.js b/src/pages/Login/index.js new file mode 100644 index 0000000..1735d8e --- /dev/null +++ b/src/pages/Login/index.js @@ -0,0 +1,52 @@ +import React, { useState } from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import Button from '@mui/material/Button'; +import {Stack} from '@mui/system'; + +export default function Login({ login }) { +  const [user, setUser] = useState(''); +  const [password, setPassword] = useState(''); +  const [loginFailed, setLoginFailed] = useState(false); + +  const getApiKey = () => { +    login(user, password) +      .then(ok => { +        setLoginFailed(!ok); +      } +      ) +      .catch(console.error); +  }; + +  return ( +    <Box +      component="form" +      sx={{ +        '& > :not(style)': { m: 1 }, +        flexGrow: 1, +      }} +      noValidate +      autoComplete="off" +    > +      <Stack spacing={1} maxWidth='20em'> +        <h2>Repeated Surveyer</h2> +        <h3>Login</h3> +        <TextField +          label='Email Address' +          type='text' +          value={user} +          onChange={e => setUser(e.target.value.trim())} +        /> +        <TextField +          label='Password' +          type='password' +          value={password} +          onChange={e => setPassword(e.target.value)} +        /> +        <Button onClick={() => getApiKey()}>Login</Button> +        {loginFailed && <b>Login Failed</b>} +      </Stack> +    </Box> +  ); +} + diff --git a/src/pages/NewSurvey/index.js b/src/pages/NewSurvey/index.js new file mode 100644 index 0000000..a4b711a --- /dev/null +++ b/src/pages/NewSurvey/index.js @@ -0,0 +1,3 @@ +export default function NewSurvey() { +  return <>NewSurvey</>; +} diff --git a/src/pages/SurveyAssignees/index.js b/src/pages/SurveyAssignees/index.js new file mode 100644 index 0000000..3623217 --- /dev/null +++ b/src/pages/SurveyAssignees/index.js @@ -0,0 +1,3 @@ +export default function SurveyAssignees() { +  return <>SurveyAssignees</>; +} diff --git a/src/pages/SurveyResults/index.js b/src/pages/SurveyResults/index.js new file mode 100644 index 0000000..82d41c6 --- /dev/null +++ b/src/pages/SurveyResults/index.js @@ -0,0 +1,3 @@ +export default function SurveyResults() { +  return <>SurveyResults</>; +} diff --git a/src/pages/Surveys/index.js b/src/pages/Surveys/index.js new file mode 100644 index 0000000..69e854f --- /dev/null +++ b/src/pages/Surveys/index.js @@ -0,0 +1,3 @@ +export default function Surveys() { +  return <>Surveys</>; +} diff --git a/src/pages/Users/index.js b/src/pages/Users/index.js new file mode 100644 index 0000000..43afe5d --- /dev/null +++ b/src/pages/Users/index.js @@ -0,0 +1,3 @@ +export default function Users() { +  return <>Users</>; +} | 
