Merge backend + frontend #1
@@ -9,9 +9,12 @@
 | 
				
			|||||||
### Dependencies
 | 
					### Dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- React
 | 
					- React
 | 
				
			||||||
- React helment async: manage changes to document head.
 | 
					- React helment async: manage changes to document head
 | 
				
			||||||
- Material UI
 | 
					- React router dom: route management
 | 
				
			||||||
 | 
					- Material UI: UI styled components
 | 
				
			||||||
- Roboto font
 | 
					- Roboto font
 | 
				
			||||||
 | 
					- Formik: form management
 | 
				
			||||||
 | 
					- Yup: validate input data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Frontend Technical Write-up
 | 
					### Frontend Technical Write-up
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4842
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4842
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -3,35 +3,40 @@
 | 
				
			|||||||
  "version": "0.1.0",
 | 
					  "version": "0.1.0",
 | 
				
			||||||
  "private": true,
 | 
					  "private": true,
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@emotion/react": "^11.11.1",
 | 
					    "@emotion/react": "^11.9.3",
 | 
				
			||||||
    "@emotion/styled": "^11.11.0",
 | 
					    "@emotion/styled": "^11.9.3",
 | 
				
			||||||
    "@fontsource/roboto": "^5.0.5",
 | 
					    "@fontsource/roboto": "^4.5.7",
 | 
				
			||||||
    "@mui/icons-material": "^5.14.1",
 | 
					    "@mui/icons-material": "^5.8.4",
 | 
				
			||||||
    "@mui/lab": "^5.0.0-alpha.137",
 | 
					    "@mui/lab": "^5.0.0-alpha.91",
 | 
				
			||||||
    "@mui/material": "^5.14.1",
 | 
					    "@mui/material": "^5.9.1",
 | 
				
			||||||
    "@testing-library/jest-dom": "^5.17.0",
 | 
					    "@mui/x-date-pickers": "^5.0.0-beta.2",
 | 
				
			||||||
    "@testing-library/react": "^13.4.0",
 | 
					    "@testing-library/jest-dom": "^5.16.4",
 | 
				
			||||||
 | 
					    "@testing-library/react": "^13.3.0",
 | 
				
			||||||
    "@testing-library/user-event": "^13.5.0",
 | 
					    "@testing-library/user-event": "^13.5.0",
 | 
				
			||||||
    "@types/jest": "^27.5.2",
 | 
					    "@types/jest": "^27.5.2",
 | 
				
			||||||
    "@types/node": "^16.18.38",
 | 
					    "@types/node": "^16.11.45",
 | 
				
			||||||
    "@types/react": "^18.2.15",
 | 
					    "@types/react": "^18.0.15",
 | 
				
			||||||
 | 
					    "@types/react-dom": "^18.0.6",
 | 
				
			||||||
 | 
					    "date-fns": "^2.29.1",
 | 
				
			||||||
 | 
					    "formik": "^2.2.9",
 | 
				
			||||||
    "react": "^18.2.0",
 | 
					    "react": "^18.2.0",
 | 
				
			||||||
    "react-dom": "^18.2.0",
 | 
					    "react-dom": "^18.2.0",
 | 
				
			||||||
    "react-helmet-async": "^1.3.0",
 | 
					    "react-helmet-async": "^1.3.0",
 | 
				
			||||||
    "react-router-dom": "^6.14.2",
 | 
					    "react-router-dom": "^6.3.0",
 | 
				
			||||||
    "react-scripts": "^5.0.1",
 | 
					    "react-scripts": "5.0.1",
 | 
				
			||||||
    "typescript": "^4.9.5",
 | 
					    "typescript": "^4.7.4",
 | 
				
			||||||
    "web-vitals": "^2.1.4",
 | 
					    "web-vitals": "^2.1.4",
 | 
				
			||||||
    "webpack": ">=5.76.0"
 | 
					    "yup": "^0.32.11",
 | 
				
			||||||
 | 
					    "zxcvbn": "^4.4.2"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "analyze": "npm run build && source-map-explorer \"build/static/js/*.js\"",
 | 
				
			||||||
 | 
					    "format": "eslint --fix \"src/**/*.{ts,tsx}\" && prettier --parser typescript --write \"src/**/*.{ts,tsx}\"",
 | 
				
			||||||
 | 
					    "lint": " eslint \"src/**/*.{ts,tsx}\" && prettier --parser typescript --list-different  \"src/**/*.{ts,tsx}\"",
 | 
				
			||||||
    "start": "react-scripts start",
 | 
					    "start": "react-scripts start",
 | 
				
			||||||
    "build": "react-scripts build",
 | 
					    "build": "react-scripts build",
 | 
				
			||||||
    "test": "react-scripts test",
 | 
					    "test": "react-scripts test",
 | 
				
			||||||
    "eject": "react-scripts eject",
 | 
					    "eject": "react-scripts eject"
 | 
				
			||||||
    "analyze": "npm run build && source-map-explorer \"build/static/js/*.js\"",
 | 
					 | 
				
			||||||
    "format": "eslint --fix \"src/**/*.{ts,tsx}\" && prettier --parser typescript --write \"src/**/*.{ts,tsx}\"",
 | 
					 | 
				
			||||||
    "lint": "eslint \"src/**/*.{ts,tsx}\" && prettier --parser typescript --list-different  \"src/**/*.{ts,tsx}\""
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "eslintConfig": {
 | 
					  "eslintConfig": {
 | 
				
			||||||
    "extends": [
 | 
					    "extends": [
 | 
				
			||||||
@@ -61,14 +66,13 @@
 | 
				
			|||||||
    ]
 | 
					    ]
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
 | 
					    "@types/zxcvbn": "^4.4.1",
 | 
				
			||||||
    "@types/react-dom": "^18.2.7",
 | 
					    "eslint": "^8.20.0",
 | 
				
			||||||
    "eslint": "^8.45.0",
 | 
					    "eslint-config-prettier": "^8.5.0",
 | 
				
			||||||
    "eslint-config-prettier": "^8.8.0",
 | 
					    "eslint-import-resolver-typescript": "^3.3.0",
 | 
				
			||||||
    "eslint-import-resolver-typescript": "^3.5.5",
 | 
					    "eslint-plugin-import": "^2.26.0",
 | 
				
			||||||
    "eslint-plugin-import": "^2.27.5",
 | 
					    "prettier": "^2.7.1",
 | 
				
			||||||
    "prettier": "^3.0.0",
 | 
					    "source-map-explorer": "^2.5.2"
 | 
				
			||||||
    "source-map-explorer": "^2.5.3"
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "prettier": {
 | 
					  "prettier": {
 | 
				
			||||||
    "trailingComma": "all"
 | 
					    "trailingComma": "all"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,9 @@
 | 
				
			|||||||
import { BrowserRouter, Routes } from "react-router-dom";
 | 
					import { BrowserRouter, Routes } from "react-router-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Router = () => {
 | 
					const Router = () => (
 | 
				
			||||||
  <BrowserRouter>
 | 
					  <BrowserRouter>
 | 
				
			||||||
    <Routes>{}</Routes>
 | 
					    <Routes>{}</Routes>
 | 
				
			||||||
  </BrowserRouter>;
 | 
					  </BrowserRouter>
 | 
				
			||||||
};
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default Router;
 | 
					export default Router;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										38
									
								
								frontend/src/components/CheckboxField.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								frontend/src/components/CheckboxField.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					import Checkbox from "@mui/material/Checkbox";
 | 
				
			||||||
 | 
					import FormControl from "@mui/material/FormControl";
 | 
				
			||||||
 | 
					import FormControlLabel from "@mui/material/FormControlLabel";
 | 
				
			||||||
 | 
					import FormHelperText from "@mui/material/FormHelperText";
 | 
				
			||||||
 | 
					import { FieldHookConfig, useField } from "formik";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { combineHelperText } from "src/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type IProps = FieldHookConfig<boolean> & {
 | 
				
			||||||
 | 
					  fullWidth?: boolean;
 | 
				
			||||||
 | 
					  helperText?: string;
 | 
				
			||||||
 | 
					  label: string;
 | 
				
			||||||
 | 
					  required?: boolean;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const CheckboxField = (props: IProps) => {
 | 
				
			||||||
 | 
					  const [field, meta] = useField<boolean>(props);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <FormControl
 | 
				
			||||||
 | 
					      component="fieldset"
 | 
				
			||||||
 | 
					      error={Boolean(meta.error) && meta.touched}
 | 
				
			||||||
 | 
					      fullWidth={props.fullWidth}
 | 
				
			||||||
 | 
					      margin="normal"
 | 
				
			||||||
 | 
					      required={props.required}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <FormControlLabel
 | 
				
			||||||
 | 
					        control={<Checkbox {...field} checked={field.value} />}
 | 
				
			||||||
 | 
					        label={props.label}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <FormHelperText>
 | 
				
			||||||
 | 
					        {combineHelperText(props.helperText, meta)}
 | 
				
			||||||
 | 
					      </FormHelperText>
 | 
				
			||||||
 | 
					    </FormControl>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default CheckboxField;
 | 
				
			||||||
							
								
								
									
										30
									
								
								frontend/src/components/DateField.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								frontend/src/components/DateField.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					import TextField, { TextFieldProps } from "@mui/material/TextField";
 | 
				
			||||||
 | 
					import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
 | 
				
			||||||
 | 
					import { DatePicker } from "@mui/x-date-pickers/DatePicker";
 | 
				
			||||||
 | 
					import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
 | 
				
			||||||
 | 
					import { FieldHookConfig, useField } from "formik";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { combineHelperText } from "src/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DateField = (props: FieldHookConfig<Date | null> & TextFieldProps) => {
 | 
				
			||||||
 | 
					  const [field, meta, helpers] = useField<Date | null>(props);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <LocalizationProvider dateAdapter={AdapterDateFns}>
 | 
				
			||||||
 | 
					      <DatePicker
 | 
				
			||||||
 | 
					        label={props.label}
 | 
				
			||||||
 | 
					        value={field.value}
 | 
				
			||||||
 | 
					        onChange={(newValue) => helpers.setValue(newValue)}
 | 
				
			||||||
 | 
					        renderInput={(params) => (
 | 
				
			||||||
 | 
					          <TextField
 | 
				
			||||||
 | 
					            fullWidth={props.fullWidth}
 | 
				
			||||||
 | 
					            {...params}
 | 
				
			||||||
 | 
					            helperText={combineHelperText(props.helperText, meta)}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </LocalizationProvider>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default DateField;
 | 
				
			||||||
							
								
								
									
										22
									
								
								frontend/src/components/EmailField.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								frontend/src/components/EmailField.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					import TextField, { TextFieldProps } from "@mui/material/TextField";
 | 
				
			||||||
 | 
					import { FieldHookConfig, useField } from "formik";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { combineHelperText } from "src/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const EmailField = (props: FieldHookConfig<string> & TextFieldProps) => {
 | 
				
			||||||
 | 
					  const [field, meta] = useField<string>(props);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <TextField
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					      autoComplete="email"
 | 
				
			||||||
 | 
					      error={Boolean(meta.error) && meta.touched}
 | 
				
			||||||
 | 
					      helperText={combineHelperText(props.helperText, meta)}
 | 
				
			||||||
 | 
					      margin="normal"
 | 
				
			||||||
 | 
					      type="email"
 | 
				
			||||||
 | 
					      {...field}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default EmailField;
 | 
				
			||||||
							
								
								
									
										43
									
								
								frontend/src/components/FormActions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								frontend/src/components/FormActions.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					import Button from "@mui/material/Button";
 | 
				
			||||||
 | 
					import LoadingButton from "@mui/lab/LoadingButton";
 | 
				
			||||||
 | 
					import Stack from "@mui/material/Stack";
 | 
				
			||||||
 | 
					import { Link } from "react-router-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ILink {
 | 
				
			||||||
 | 
					  label: string;
 | 
				
			||||||
 | 
					  to: string;
 | 
				
			||||||
 | 
					  state?: any;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IProps {
 | 
				
			||||||
 | 
					  disabled: boolean;
 | 
				
			||||||
 | 
					  isSubmitting: boolean;
 | 
				
			||||||
 | 
					  label: string;
 | 
				
			||||||
 | 
					  links?: ILink[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FormActions = ({ disabled, isSubmitting, label, links }: IProps) => (
 | 
				
			||||||
 | 
					  <Stack direction="row" spacing={1} sx={{ marginTop: 2 }}>
 | 
				
			||||||
 | 
					    <LoadingButton
 | 
				
			||||||
 | 
					      disabled={disabled}
 | 
				
			||||||
 | 
					      loading={isSubmitting}
 | 
				
			||||||
 | 
					      type="submit"
 | 
				
			||||||
 | 
					      variant="contained"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {label}
 | 
				
			||||||
 | 
					    </LoadingButton>
 | 
				
			||||||
 | 
					    {(links ?? []).map(({ label, to, state }) => (
 | 
				
			||||||
 | 
					      <Button
 | 
				
			||||||
 | 
					        component={Link}
 | 
				
			||||||
 | 
					        key={to}
 | 
				
			||||||
 | 
					        state={state}
 | 
				
			||||||
 | 
					        to={to}
 | 
				
			||||||
 | 
					        variant="outlined"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {label}
 | 
				
			||||||
 | 
					      </Button>
 | 
				
			||||||
 | 
					    ))}
 | 
				
			||||||
 | 
					  </Stack>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default FormActions;
 | 
				
			||||||
							
								
								
									
										19
									
								
								frontend/src/components/LazyPasswordWithStrengthField.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/src/components/LazyPasswordWithStrengthField.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					import { TextFieldProps } from "@mui/material/TextField";
 | 
				
			||||||
 | 
					import { lazy, Suspense } from "react";
 | 
				
			||||||
 | 
					import { FieldHookConfig } from "formik";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import PasswordField from "src/components/PasswordField";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PasswordWithStrengthField = lazy(
 | 
				
			||||||
 | 
					  () => import("src/components/PasswordWithStrengthField"),
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const LazyPasswordWithStrengthField = (
 | 
				
			||||||
 | 
					  props: FieldHookConfig<string> & TextFieldProps,
 | 
				
			||||||
 | 
					) => (
 | 
				
			||||||
 | 
					  <Suspense fallback={<PasswordField {...props} />}>
 | 
				
			||||||
 | 
					    <PasswordWithStrengthField {...props} />
 | 
				
			||||||
 | 
					  </Suspense>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default LazyPasswordWithStrengthField;
 | 
				
			||||||
							
								
								
									
										39
									
								
								frontend/src/components/PasswordField.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								frontend/src/components/PasswordField.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					import IconButton from "@mui/material/IconButton";
 | 
				
			||||||
 | 
					import InputAdornment from "@mui/material/InputAdornment";
 | 
				
			||||||
 | 
					import TextField, { TextFieldProps } from "@mui/material/TextField";
 | 
				
			||||||
 | 
					import Visibility from "@mui/icons-material/Visibility";
 | 
				
			||||||
 | 
					import VisibilityOff from "@mui/icons-material/VisibilityOff";
 | 
				
			||||||
 | 
					import { FieldHookConfig, useField } from "formik";
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { combineHelperText } from "src/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PasswordField = (props: FieldHookConfig<string> & TextFieldProps) => {
 | 
				
			||||||
 | 
					  const [field, meta] = useField<string>(props);
 | 
				
			||||||
 | 
					  const [showPassword, setShowPassword] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <TextField
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					      InputProps={{
 | 
				
			||||||
 | 
					        endAdornment: (
 | 
				
			||||||
 | 
					          <InputAdornment position="end">
 | 
				
			||||||
 | 
					            <IconButton
 | 
				
			||||||
 | 
					              onClick={() => setShowPassword((value) => !value)}
 | 
				
			||||||
 | 
					              tabIndex={-1}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {showPassword ? <Visibility /> : <VisibilityOff />}
 | 
				
			||||||
 | 
					            </IconButton>
 | 
				
			||||||
 | 
					          </InputAdornment>
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      error={Boolean(meta.error) && meta.touched}
 | 
				
			||||||
 | 
					      helperText={combineHelperText(props.helperText, meta)}
 | 
				
			||||||
 | 
					      margin="normal"
 | 
				
			||||||
 | 
					      type={showPassword ? "text" : "password"}
 | 
				
			||||||
 | 
					      {...field}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default PasswordField;
 | 
				
			||||||
							
								
								
									
										58
									
								
								frontend/src/components/PasswordWithStrengthField.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								frontend/src/components/PasswordWithStrengthField.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					import LinearProgress from "@mui/material/LinearProgress";
 | 
				
			||||||
 | 
					import { TextFieldProps } from "@mui/material/TextField";
 | 
				
			||||||
 | 
					import { FieldHookConfig, useField } from "formik";
 | 
				
			||||||
 | 
					import zxcvbn from "zxcvbn";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import PasswordField from "src/components/PasswordField";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const scoreToDisplay = (score: number) => {
 | 
				
			||||||
 | 
					  let progressColor = "other.red";
 | 
				
			||||||
 | 
					  let helperText = "Weak";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  switch (score) {
 | 
				
			||||||
 | 
					    case 25:
 | 
				
			||||||
 | 
					      progressColor = "other.pink";
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 50:
 | 
				
			||||||
 | 
					      progressColor = "other.orange";
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 75:
 | 
				
			||||||
 | 
					      progressColor = "other.yellow";
 | 
				
			||||||
 | 
					      helperText = "Good";
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 100:
 | 
				
			||||||
 | 
					      progressColor = "other.green";
 | 
				
			||||||
 | 
					      helperText = "Strong";
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return [progressColor, helperText];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PasswordWithStrengthField = (
 | 
				
			||||||
 | 
					  props: FieldHookConfig<string> & TextFieldProps,
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
					  const [field] = useField<string>(props);
 | 
				
			||||||
 | 
					  const result = zxcvbn(field.value ?? "");
 | 
				
			||||||
 | 
					  const score = (result.score * 100) / 4;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [progressColor, helperText] = scoreToDisplay(score);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <PasswordField {...props} helperText={helperText} />
 | 
				
			||||||
 | 
					      <LinearProgress
 | 
				
			||||||
 | 
					        sx={{
 | 
				
			||||||
 | 
					          "& .MuiLinearProgress-barColorPrimary": {
 | 
				
			||||||
 | 
					            backgroundColor: progressColor,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          backgroundColor: "action.selected",
 | 
				
			||||||
 | 
					          margin: "0 4px 24px 4px",
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        value={score}
 | 
				
			||||||
 | 
					        variant="determinate"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default PasswordWithStrengthField;
 | 
				
			||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import { useContext } from "react";
 | 
					import { useContext } from "react";
 | 
				
			||||||
import { Navigate, useLocation } from "react-router-dom";
 | 
					import { Navigate, useLocation } from "react-router-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { AuthContext } from "src/AuthContext";
 | 
					import { AuthContext } from "src/AuthContext";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IProps {
 | 
					interface IProps {
 | 
				
			||||||
@@ -8,13 +9,11 @@ interface IProps {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const RequireAuth = ({ children }: IProps) => {
 | 
					const RequireAuth = ({ children }: IProps) => {
 | 
				
			||||||
  const { authenticated } = useContext(AuthContext);
 | 
					  const { authenticated } = useContext(AuthContext);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const location = useLocation();
 | 
					  const location = useLocation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (authenticated) {
 | 
					  if (authenticated) {
 | 
				
			||||||
    return <>{children}</>;
 | 
					    return <>{children}</>;
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    // re-route user back to login page if not logged in
 | 
					 | 
				
			||||||
    return <Navigate state={{ from: location }} to="/login/" />;
 | 
					    return <Navigate state={{ from: location }} to="/login/" />;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,8 +4,6 @@ import { useLocation } from "react-router";
 | 
				
			|||||||
const ScrollToTop = () => {
 | 
					const ScrollToTop = () => {
 | 
				
			||||||
  const { pathname } = useLocation();
 | 
					  const { pathname } = useLocation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // pathname changes => scroll to top
 | 
					 | 
				
			||||||
  // scrolling only on navigation
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    window.scrollTo(0, 0);
 | 
					    window.scrollTo(0, 0);
 | 
				
			||||||
  }, [pathname]);
 | 
					  }, [pathname]);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										21
									
								
								frontend/src/components/TextField.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								frontend/src/components/TextField.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					import MUITextField, { TextFieldProps } from "@mui/material/TextField";
 | 
				
			||||||
 | 
					import { FieldHookConfig, useField } from "formik";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { combineHelperText } from "src/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TextField = (props: FieldHookConfig<string> & TextFieldProps) => {
 | 
				
			||||||
 | 
					  const [field, meta] = useField<string>(props);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <MUITextField
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					      error={Boolean(meta.error) && meta.touched}
 | 
				
			||||||
 | 
					      helperText={combineHelperText(props.helperText, meta)}
 | 
				
			||||||
 | 
					      margin="normal"
 | 
				
			||||||
 | 
					      type="text"
 | 
				
			||||||
 | 
					      {...field}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default TextField;
 | 
				
			||||||
@@ -8,7 +8,7 @@ interface IProps {
 | 
				
			|||||||
const Title = ({ title }: IProps) => (
 | 
					const Title = ({ title }: IProps) => (
 | 
				
			||||||
  <>
 | 
					  <>
 | 
				
			||||||
    <Helmet>
 | 
					    <Helmet>
 | 
				
			||||||
      <title>Todo | {title}</title>
 | 
					      <title>Tozo | {title}</title>
 | 
				
			||||||
    </Helmet>
 | 
					    </Helmet>
 | 
				
			||||||
    <Typography component="h1" variant="h5">
 | 
					    <Typography component="h1" variant="h5">
 | 
				
			||||||
      {title}
 | 
					      {title}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										21
									
								
								frontend/src/utils.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								frontend/src/utils.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					import { FieldMetaProps } from "formik";
 | 
				
			||||||
 | 
					import React from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const combineHelperText = <T,>(
 | 
				
			||||||
 | 
					  helperText: React.ReactNode | string | undefined,
 | 
				
			||||||
 | 
					  meta: FieldMetaProps<T>,
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
					  if (Boolean(meta.error) && meta.touched) {
 | 
				
			||||||
 | 
					    if (typeof helperText === "string") {
 | 
				
			||||||
 | 
					      return `${meta.error}. ${helperText ?? ""}`;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          {meta.error}. {helperText}
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return helperText;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
		Reference in New Issue
	
	Block a user