Merge backend + frontend #1
@@ -9,9 +9,12 @@
 | 
			
		||||
### Dependencies
 | 
			
		||||
 | 
			
		||||
- React
 | 
			
		||||
- React helment async: manage changes to document head.
 | 
			
		||||
- Material UI
 | 
			
		||||
- React helment async: manage changes to document head
 | 
			
		||||
- React router dom: route management
 | 
			
		||||
- Material UI: UI styled components
 | 
			
		||||
- Roboto font
 | 
			
		||||
- Formik: form management
 | 
			
		||||
- Yup: validate input data
 | 
			
		||||
 | 
			
		||||
### 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",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@emotion/react": "^11.11.1",
 | 
			
		||||
    "@emotion/styled": "^11.11.0",
 | 
			
		||||
    "@fontsource/roboto": "^5.0.5",
 | 
			
		||||
    "@mui/icons-material": "^5.14.1",
 | 
			
		||||
    "@mui/lab": "^5.0.0-alpha.137",
 | 
			
		||||
    "@mui/material": "^5.14.1",
 | 
			
		||||
    "@testing-library/jest-dom": "^5.17.0",
 | 
			
		||||
    "@testing-library/react": "^13.4.0",
 | 
			
		||||
    "@emotion/react": "^11.9.3",
 | 
			
		||||
    "@emotion/styled": "^11.9.3",
 | 
			
		||||
    "@fontsource/roboto": "^4.5.7",
 | 
			
		||||
    "@mui/icons-material": "^5.8.4",
 | 
			
		||||
    "@mui/lab": "^5.0.0-alpha.91",
 | 
			
		||||
    "@mui/material": "^5.9.1",
 | 
			
		||||
    "@mui/x-date-pickers": "^5.0.0-beta.2",
 | 
			
		||||
    "@testing-library/jest-dom": "^5.16.4",
 | 
			
		||||
    "@testing-library/react": "^13.3.0",
 | 
			
		||||
    "@testing-library/user-event": "^13.5.0",
 | 
			
		||||
    "@types/jest": "^27.5.2",
 | 
			
		||||
    "@types/node": "^16.18.38",
 | 
			
		||||
    "@types/react": "^18.2.15",
 | 
			
		||||
    "@types/node": "^16.11.45",
 | 
			
		||||
    "@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-dom": "^18.2.0",
 | 
			
		||||
    "react-helmet-async": "^1.3.0",
 | 
			
		||||
    "react-router-dom": "^6.14.2",
 | 
			
		||||
    "react-scripts": "^5.0.1",
 | 
			
		||||
    "typescript": "^4.9.5",
 | 
			
		||||
    "react-router-dom": "^6.3.0",
 | 
			
		||||
    "react-scripts": "5.0.1",
 | 
			
		||||
    "typescript": "^4.7.4",
 | 
			
		||||
    "web-vitals": "^2.1.4",
 | 
			
		||||
    "webpack": ">=5.76.0"
 | 
			
		||||
    "yup": "^0.32.11",
 | 
			
		||||
    "zxcvbn": "^4.4.2"
 | 
			
		||||
  },
 | 
			
		||||
  "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",
 | 
			
		||||
    "build": "react-scripts build",
 | 
			
		||||
    "test": "react-scripts test",
 | 
			
		||||
    "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}\""
 | 
			
		||||
    "eject": "react-scripts eject"
 | 
			
		||||
  },
 | 
			
		||||
  "eslintConfig": {
 | 
			
		||||
    "extends": [
 | 
			
		||||
@@ -61,14 +66,13 @@
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
 | 
			
		||||
    "@types/react-dom": "^18.2.7",
 | 
			
		||||
    "eslint": "^8.45.0",
 | 
			
		||||
    "eslint-config-prettier": "^8.8.0",
 | 
			
		||||
    "eslint-import-resolver-typescript": "^3.5.5",
 | 
			
		||||
    "eslint-plugin-import": "^2.27.5",
 | 
			
		||||
    "prettier": "^3.0.0",
 | 
			
		||||
    "source-map-explorer": "^2.5.3"
 | 
			
		||||
    "@types/zxcvbn": "^4.4.1",
 | 
			
		||||
    "eslint": "^8.20.0",
 | 
			
		||||
    "eslint-config-prettier": "^8.5.0",
 | 
			
		||||
    "eslint-import-resolver-typescript": "^3.3.0",
 | 
			
		||||
    "eslint-plugin-import": "^2.26.0",
 | 
			
		||||
    "prettier": "^2.7.1",
 | 
			
		||||
    "source-map-explorer": "^2.5.2"
 | 
			
		||||
  },
 | 
			
		||||
  "prettier": {
 | 
			
		||||
    "trailingComma": "all"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import { BrowserRouter, Routes } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
const Router = () => {
 | 
			
		||||
const Router = () => (
 | 
			
		||||
  <BrowserRouter>
 | 
			
		||||
    <Routes>{}</Routes>
 | 
			
		||||
  </BrowserRouter>;
 | 
			
		||||
};
 | 
			
		||||
  </BrowserRouter>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
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 { Navigate, useLocation } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
import { AuthContext } from "src/AuthContext";
 | 
			
		||||
 | 
			
		||||
interface IProps {
 | 
			
		||||
@@ -8,13 +9,11 @@ interface IProps {
 | 
			
		||||
 | 
			
		||||
const RequireAuth = ({ children }: IProps) => {
 | 
			
		||||
  const { authenticated } = useContext(AuthContext);
 | 
			
		||||
 | 
			
		||||
  const location = useLocation();
 | 
			
		||||
 | 
			
		||||
  if (authenticated) {
 | 
			
		||||
    return <>{children}</>;
 | 
			
		||||
  } else {
 | 
			
		||||
    // re-route user back to login page if not logged in
 | 
			
		||||
    return <Navigate state={{ from: location }} to="/login/" />;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,6 @@ import { useLocation } from "react-router";
 | 
			
		||||
const ScrollToTop = () => {
 | 
			
		||||
  const { pathname } = useLocation();
 | 
			
		||||
 | 
			
		||||
  // pathname changes => scroll to top
 | 
			
		||||
  // scrolling only on navigation
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    window.scrollTo(0, 0);
 | 
			
		||||
  }, [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) => (
 | 
			
		||||
  <>
 | 
			
		||||
    <Helmet>
 | 
			
		||||
      <title>Todo | {title}</title>
 | 
			
		||||
      <title>Tozo | {title}</title>
 | 
			
		||||
    </Helmet>
 | 
			
		||||
    <Typography component="h1" variant="h5">
 | 
			
		||||
      {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