From 9c513a4fbc541c1a152e957bab7e2641bea85f52 Mon Sep 17 00:00:00 2001 From: Stefan Kairinos Date: Wed, 19 Jun 2024 14:31:16 +0100 Subject: [PATCH] fix: auth flow (#43) * otp bypass token * otp bypass token * fix: simplify page components * comment out unused lines * add router components * simplified text fields * clean up utils * rename to schema * fix import * fix import * fix import * create router utils * delete index * fix paths * fix props * do not set default value * comment out broken imports * comment out broken import * support helper text * fix auth factor type * add otp field * login path props * fix: hooks page auth flow * pass in session metadata * add placeholders * first name field * decode cookie value * add user type * set session user type * fix use session required * remove duplicate logic * fix imports * feedback --- src/api/index.ts | 2 + src/api/models.ts | 10 +- src/components/ClickableTooltip.tsx | 2 +- src/components/Image.tsx | 2 +- src/components/form/AutocompleteField.tsx | 2 +- src/components/form/CheckboxField.tsx | 2 +- src/components/form/DateField.tsx | 215 ------------------- src/components/form/EmailField.tsx | 18 +- src/components/form/FirstNameField.tsx | 29 +++ src/components/form/Form.tsx | 12 +- src/components/form/OtpField.tsx | 28 +++ src/components/form/PasswordField.tsx | 23 +- src/components/form/SubmitButton.tsx | 16 +- src/components/form/TextField.tsx | 248 ++++------------------ src/components/form/index.ts | 56 ----- src/components/form/index.tsx | 34 +++ src/components/page/Banner.tsx | 34 +-- src/components/page/Container.tsx | 46 ---- src/components/page/Notification.tsx | 15 +- src/components/page/Page.tsx | 75 +++++++ src/components/page/Section.tsx | 26 +-- src/components/page/TabBar.tsx | 9 +- src/components/page/index.ts | 26 +-- src/components/router/Link.tsx | 15 ++ src/components/router/LinkButton.tsx | 12 ++ src/components/router/LinkListItem.tsx | 12 ++ src/components/router/LinkTab.tsx | 12 ++ src/components/router/index.tsx | 15 ++ src/hooks/auth.tsx | 84 ++++++++ src/{hooks.ts => hooks/general.ts} | 26 --- src/hooks/index.ts | 24 +++ src/hooks/router.ts | 31 +++ src/theme/ThemedBox.tsx | 2 +- src/theme/components/MuiButton.ts | 2 +- src/theme/components/MuiLink.ts | 2 +- src/theme/components/MuiListItemText.ts | 2 +- src/theme/components/MuiMenuItem.ts | 2 +- src/theme/components/MuiTable.ts | 2 +- src/theme/components/MuiTableBody.ts | 2 +- src/theme/components/MuiTableHead.ts | 2 +- src/theme/components/MuiTextField.ts | 2 +- src/theme/components/_components.ts | 6 +- src/utils/{rtkQuery.ts => api.ts} | 0 src/utils/{formik.ts => form.ts} | 21 +- src/utils/general.ts | 29 --- src/utils/index.ts | 32 --- src/utils/jsCookie.ts | 21 -- src/utils/router.ts | 28 +++ src/utils/{yup.ts => schema.ts} | 0 src/utils/{materialUI.tsx => theme.tsx} | 0 50 files changed, 565 insertions(+), 751 deletions(-) delete mode 100644 src/components/form/DateField.tsx create mode 100644 src/components/form/FirstNameField.tsx create mode 100644 src/components/form/OtpField.tsx delete mode 100644 src/components/form/index.ts create mode 100644 src/components/form/index.tsx delete mode 100644 src/components/page/Container.tsx create mode 100644 src/components/page/Page.tsx create mode 100644 src/components/router/Link.tsx create mode 100644 src/components/router/LinkButton.tsx create mode 100644 src/components/router/LinkListItem.tsx create mode 100644 src/components/router/LinkTab.tsx create mode 100644 src/components/router/index.tsx create mode 100644 src/hooks/auth.tsx rename src/{hooks.ts => hooks/general.ts} (81%) create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/router.ts rename src/utils/{rtkQuery.ts => api.ts} (100%) rename src/utils/{formik.ts => form.ts} (76%) delete mode 100644 src/utils/index.ts delete mode 100644 src/utils/jsCookie.ts create mode 100644 src/utils/router.ts rename src/utils/{yup.ts => schema.ts} (100%) rename src/utils/{materialUI.tsx => theme.tsx} (100%) diff --git a/src/api/index.ts b/src/api/index.ts index 95ebff09..4b9faa3e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -3,6 +3,7 @@ import endpoints from "./endpoints" import type { AuthFactor, Class, + OtpBypassToken, School, Student, Teacher, @@ -18,6 +19,7 @@ export { urls, type AuthFactor, type Class, + type OtpBypassToken, type School, type Student, type Teacher, diff --git a/src/api/models.ts b/src/api/models.ts index 0bc1d24b..6835e44a 100644 --- a/src/api/models.ts +++ b/src/api/models.ts @@ -1,5 +1,5 @@ +import type { Model } from "../utils/api" import type { CountryIsoCodes, UkCounties } from "../utils/general" -import type { Model } from "../utils/rtkQuery" export type User = Model< number, @@ -63,3 +63,11 @@ export type AuthFactor = Model< type: "otp" } > + +export type OtpBypassToken = Model< + number, + { + user: number + token: string + } +> diff --git a/src/components/ClickableTooltip.tsx b/src/components/ClickableTooltip.tsx index 0820703d..593f38f0 100644 --- a/src/components/ClickableTooltip.tsx +++ b/src/components/ClickableTooltip.tsx @@ -1,7 +1,7 @@ import { Tooltip, type TooltipProps } from "@mui/material" import React from "react" -import { wrap } from "../utils" +import { wrap } from "../utils/general" export interface ClickableTooltipProps extends TooltipProps {} diff --git a/src/components/Image.tsx b/src/components/Image.tsx index d5d2795a..a83c655d 100644 --- a/src/components/Image.tsx +++ b/src/components/Image.tsx @@ -1,7 +1,7 @@ import { Box, type BoxProps } from "@mui/material" import type React from "react" -import { openInNewTab } from "../utils" +import { openInNewTab } from "../utils/general" export interface ImageProps extends Omit { alt: string diff --git a/src/components/form/AutocompleteField.tsx b/src/components/form/AutocompleteField.tsx index f4e6c10e..625bd9f6 100644 --- a/src/components/form/AutocompleteField.tsx +++ b/src/components/form/AutocompleteField.tsx @@ -13,7 +13,7 @@ import React from "react" import { flushSync } from "react-dom" import { string as YupString, ValidationError as YupValidationError } from "yup" -import { wrap } from "../../utils" +import { wrap } from "../../utils/general" import ClickableTooltip from "../ClickableTooltip" export interface AutocompleteFieldProps< diff --git a/src/components/form/CheckboxField.tsx b/src/components/form/CheckboxField.tsx index e979251d..6fc6cf38 100644 --- a/src/components/form/CheckboxField.tsx +++ b/src/components/form/CheckboxField.tsx @@ -16,7 +16,7 @@ import React from "react" import { BooleanSchema, ValidationError, bool as YupBool } from "yup" import { form as formTypography } from "../../theme/typography" -import { wrap } from "../../utils" +import { wrap } from "../../utils/general" import ClickableTooltip from "../ClickableTooltip" export interface CheckboxFieldProps diff --git a/src/components/form/DateField.tsx b/src/components/form/DateField.tsx deleted file mode 100644 index 48032799..00000000 --- a/src/components/form/DateField.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { - FormHelperText, - Unstable_Grid2 as Grid, - MenuItem, - Select, - type FormHelperTextProps, - type SelectChangeEvent, - type SelectProps, -} from "@mui/material" -import { Field, type FieldConfig, type FieldProps } from "formik" -import React from "react" - -import { form as formTypography } from "../../theme/typography" -import { MIN_DATE } from "../../utils/general" - -const monthOptions = [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", -] - -export interface DateFieldProps { - name?: string - required?: boolean - previousYears?: number - helperText?: string - formHelperTextProps?: FormHelperTextProps -} - -const DateField: React.FC = ({ - name = "date", - required = false, - previousYears = 150, - helperText, - formHelperTextProps, -}) => { - const [day, setDay] = React.useState(0) - const [month, setMonth] = React.useState(0) - const [year, setYear] = React.useState(0) - const [isDateValid, setIsDateValid] = React.useState(true) - const menuMaxHeight = 400 - - const fieldConfig: FieldConfig = { - type: "date", - name, - validate: (value: Date): void | string => { - if (required && value.getTime() === MIN_DATE.getTime()) { - return "date required" - } - }, - } - - return ( - - {({ form }: FieldProps) => { - // TODO: simplify this component and relocate this effect. - // eslint-disable-next-line react-hooks/rules-of-hooks - React.useEffect(() => { - const date = - [day, month, year].includes(0) || !isDateValid - ? MIN_DATE - : new Date(year, month - 1, day) - - form.setFieldValue(name, date, true) - }, [form]) - - function getLastDay(month: number, year: number): number { - return new Date(year, month, 0).getDate() - } - - function dispatchSelectChangeEvent( - dispatch: React.Dispatch>, - ) { - return (event: SelectChangeEvent) => { - const value = Number(event.target.value) - - let [_day, _month, _year] = [day, month, year] - switch (dispatch) { - case setDay: - _day = value - break - case setMonth: - _month = value - break - case setYear: - _year = value - break - } - - if (_day !== 0 && _month !== 0 && _year !== 0) { - if (_day > getLastDay(_month, _year)) { - setIsDateValid(false) - } else { - setIsDateValid(true) - } - } - - dispatch(value) - } - } - - const dayOptions = Array.from(Array(31).keys()).map(day => day + 1) - - const yearOptions = Array.from(Array(previousYears).keys()) - .map(year => year + 1 - previousYears + new Date().getFullYear()) - .reverse() - - const commonSelectProps: SelectProps = { - style: { backgroundColor: "white", width: "100%" }, - size: "small", - } - - return ( - - {helperText !== undefined && helperText !== "" && ( - - - {helperText} - - - )} - - - - - - - - - - {!isDateValid && ( - - - Invalid date - - - )} - - ) - }} - - ) -} - -export default DateField diff --git a/src/components/form/EmailField.tsx b/src/components/form/EmailField.tsx index 65a6057d..22fcbea0 100644 --- a/src/components/form/EmailField.tsx +++ b/src/components/form/EmailField.tsx @@ -1,25 +1,27 @@ -import type React from "react" -import { InputAdornment } from "@mui/material" import { EmailOutlined as EmailOutlinedIcon } from "@mui/icons-material" +import { InputAdornment } from "@mui/material" +import type { FC } from "react" import { string as YupString } from "yup" import TextField, { type TextFieldProps } from "./TextField" -export interface EmailFieldProps extends Omit { - name?: string -} +export type EmailFieldProps = Omit & + Partial> -const EmailField: React.FC = ({ +const EmailField: FC = ({ name = "email", + label = "Email address", + placeholder = "Enter your email address", InputProps = {}, - validate = YupString().email(), ...otherTextFieldProps }) => { return ( diff --git a/src/components/form/FirstNameField.tsx b/src/components/form/FirstNameField.tsx new file mode 100644 index 00000000..143933a9 --- /dev/null +++ b/src/components/form/FirstNameField.tsx @@ -0,0 +1,29 @@ +import type { FC } from "react" +import { string as YupString } from "yup" + +import TextField, { type TextFieldProps } from "./TextField" + +export type FirstNameFieldProps = Omit< + TextFieldProps, + "type" | "name" | "schema" +> & + Partial> + +const FirstNameField: FC = ({ + name = "first_name", + label = "First name", + placeholder = "Enter your first name", + ...otherTextFieldProps +}) => { + return ( + + ) +} + +export default FirstNameField diff --git a/src/components/form/Form.tsx b/src/components/form/Form.tsx index 3ae7feb7..ee6a59d2 100644 --- a/src/components/form/Form.tsx +++ b/src/components/form/Form.tsx @@ -1,17 +1,14 @@ -import { Stack, type StackProps } from "@mui/material" import { Formik, Form as FormikForm, type FormikConfig, + type FormikErrors, type FormikValues, } from "formik" -export interface FormProps extends FormikConfig { - stackProps?: Omit -} +export interface FormProps extends FormikConfig {} const Form = ({ - stackProps, children, ...otherFormikProps }: FormProps): JSX.Element => { @@ -19,9 +16,7 @@ const Form = ({ {formik => ( - - {typeof children === "function" ? children(formik) : children} - + {typeof children === "function" ? children(formik) : children} )} @@ -29,3 +24,4 @@ const Form = ({ } export default Form +export { type FormikErrors as FormErrors } diff --git a/src/components/form/OtpField.tsx b/src/components/form/OtpField.tsx new file mode 100644 index 00000000..67f396b8 --- /dev/null +++ b/src/components/form/OtpField.tsx @@ -0,0 +1,28 @@ +import { type FC } from "react" +import { string as YupString } from "yup" + +import TextField, { type TextFieldProps } from "./TextField" + +export type OtpFieldProps = Omit< + TextFieldProps, + "name" | "schema" | "required" +> & + Partial> + +const OtpField: FC = ({ + name = "otp", + label = "OTP", + placeholder = "Enter your OTP", + ...otherTextFieldProps +}) => ( + +) + +export default OtpField diff --git a/src/components/form/PasswordField.tsx b/src/components/form/PasswordField.tsx index 63b19bfc..e92a4449 100644 --- a/src/components/form/PasswordField.tsx +++ b/src/components/form/PasswordField.tsx @@ -1,26 +1,31 @@ -import type React from "react" -import { InputAdornment } from "@mui/material" import { Security as SecurityIcon } from "@mui/icons-material" +import { InputAdornment } from "@mui/material" +import type { FC } from "react" import { string as YupString } from "yup" import TextField, { type TextFieldProps } from "./TextField" -export interface PasswordFieldProps - extends Omit { - name?: string -} +export type PasswordFieldProps = Omit< + TextFieldProps, + "type" | "name" | "schema" +> & + Partial> -const PasswordField: React.FC = ({ +const PasswordField: FC = ({ name = "password", + label = "Password", + placeholder = "Enter your password", + schema = YupString(), InputProps = {}, - validate = YupString(), ...otherTextFieldProps }) => { return ( diff --git a/src/components/form/SubmitButton.tsx b/src/components/form/SubmitButton.tsx index c54416a2..2359f929 100644 --- a/src/components/form/SubmitButton.tsx +++ b/src/components/form/SubmitButton.tsx @@ -1,15 +1,13 @@ -import type React from "react" -import { Button, type ButtonProps, Stack, type StackProps } from "@mui/material" +import { Button, type ButtonProps } from "@mui/material" import { Field, type FieldProps, type FormikProps } from "formik" +import type { FC } from "react" export interface SubmitButtonProps extends Omit { - stackProps?: Omit disabled?: (form: FormikProps) => boolean } -const SubmitButton: React.FC = ({ - stackProps, +const SubmitButton: FC = ({ children = "Submit", disabled = form => !(form.dirty && form.isValid), ...otherButtonProps @@ -17,11 +15,9 @@ const SubmitButton: React.FC = ({ return ( {({ form }: FieldProps) => ( - - - + )} ) diff --git a/src/components/form/TextField.tsx b/src/components/form/TextField.tsx index b754a631..3fc9b42f 100644 --- a/src/components/form/TextField.tsx +++ b/src/components/form/TextField.tsx @@ -1,231 +1,63 @@ -import { ErrorOutline as ErrorOutlineIcon } from "@mui/icons-material" import { - InputAdornment, TextField as MuiTextField, type TextFieldProps as MuiTextFieldProps, } from "@mui/material" -import { - Field, - type FieldConfig, - type FieldProps, - type FieldValidator, -} from "formik" -import React from "react" -import { - Schema, - ValidationError, - array as YupArray, - string as YupString, - type AnyObject, - type ArraySchema, - type StringSchema, -} from "yup" - -import { wrap } from "../../utils" -import ClickableTooltip from "../ClickableTooltip" - -type StringArraySchema = ArraySchema< - Array | undefined, - AnyObject, - "", - "" -> -type Validate = FieldValidator | StringSchema | StringArraySchema -type Split = string | RegExp - -type BaseTextFieldProps = Omit & { +import { Field, type FieldConfig, type FieldProps } from "formik" +import type { FC } from "react" +import { type StringSchema } from "yup" + +import { schemaToFieldValidator } from "../../utils/form" + +export type TextFieldProps = Omit< + MuiTextFieldProps, + | "name" + | "id" + | "value" + | "onChange" + | "onBlur" + | "error" + | "defaultValue" + | "helperText" +> & { name: string + schema: StringSchema } -type RepeatTextFieldProps = BaseTextFieldProps & { - repeat?: Array< - Omit & { - inheritProps?: boolean - } - > -} - -export type TextFieldProps = - RepeatTextFieldProps & - (SingleValue extends true - ? { validate?: FieldValidator | StringSchema } - : { - validate?: FieldValidator | StringArraySchema - split: Split - }) - -// Internal TextField. -const _TextField: React.FC< - BaseTextFieldProps & { - validate: Validate - split?: Split - } -> = ({ - validate, - split, +// https://formik.org/docs/examples/with-material-ui +const TextField: FC = ({ name, + schema, type = "text", - InputProps = {}, - onKeyUp, - onBlur, + required = false, ...otherTextFieldProps }) => { + if (required) schema = schema.required() + const fieldConfig: FieldConfig = { name, type, - validate: async value => { - if (validate instanceof Schema) { - try { - await validate.validate(value) - } catch (error) { - if (error instanceof ValidationError) { - return error.errors[0] - } - throw error - } - } else if (validate !== undefined) { - return await validate(value) - } - }, + validate: schemaToFieldValidator(schema), } return ( - {({ meta, form }: FieldProps) => { - // TODO: simplify this component and remove this state. - // eslint-disable-next-line react-hooks/rules-of-hooks - const [showError, setShowError] = React.useState(false) - - let { endAdornment, ...otherInputProps } = InputProps - - if (showError && meta.error !== undefined && meta.error !== "") { - endAdornment = ( - <> - {endAdornment} - - - - - - - ) - } - - onKeyUp = wrap( - { - after: (event: React.KeyboardEvent) => { - let value: string | string[] = ( - event.target as HTMLTextAreaElement - ).value - if (split !== undefined) value = value.split(split) - form.setFieldValue(name, value, true) - }, - }, - onKeyUp, - ) - - onBlur = wrap( - { - after: () => { - setShowError(true) - }, - }, - onBlur, - ) - - return ( - - ) - }} - - ) -} - -interface ITextField { - // eslint-disable-next-line @typescript-eslint/prefer-function-type - ( - props: TextFieldProps, - context?: any, - ): React.ReactElement | null -} - -const TextField: ITextField & ITextField = ({ - validate, - split, - required = false, - name, - onKeyUp, - repeat = [], - ...otherTextFieldProps -}: RepeatTextFieldProps & { - validate?: Validate - split?: Split -}) => { - const [validateRepeat, setValidateRepeat] = React.useState(YupString()) - - if (validate === undefined) { - validate = split === undefined ? YupString() : YupArray().of(YupString()) - } - - if (required && validate instanceof Schema) { - validate = validate.required() - } - - if (repeat.length > 0) { - onKeyUp = wrap( - { - after: (event: React.KeyboardEvent) => { - setValidateRepeat( - YupString().test( - `matches-${name}`, - `doesn't match ${name}`, - repeatValue => { - const value = (event.target as HTMLTextAreaElement).value - return value === repeatValue - }, - ), - ) - }, - }, - onKeyUp, - ) - } - - return ( - <> - {/* TODO: simplify this component and remove this sub component. */} - {/* eslint-disable-next-line react/jsx-pascal-case */} - <_TextField - validate={validate} - split={split} - name={name} - onKeyUp={onKeyUp} - {...otherTextFieldProps} - /> - {repeat.map(({ name, inheritProps = true, ...repeatTextFieldProps }) => ( - // TODO: simplify this component and remove this sub component. - // eslint-disable-next-line react/jsx-pascal-case - <_TextField - key={name} + {({ form }: FieldProps) => ( + - ))} - + )} + ) } diff --git a/src/components/form/index.ts b/src/components/form/index.ts deleted file mode 100644 index dcec87e1..00000000 --- a/src/components/form/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { type FormikErrors } from "formik" - -import AutocompleteField, { - type AutocompleteFieldProps, -} from "./AutocompleteField" -import CheckboxField, { type CheckboxFieldProps } from "./CheckboxField" -import DateField, { type DateFieldProps } from "./DateField" -import EmailField, { type EmailFieldProps } from "./EmailField" -import Form, { type FormProps } from "./Form" -import PasswordField, { type PasswordFieldProps } from "./PasswordField" -import SubmitButton, { type SubmitButtonProps } from "./SubmitButton" -import TextField, { type TextFieldProps } from "./TextField" - -export { - type FormikErrors as FormErrors, - AutocompleteField, - type AutocompleteFieldProps, - CheckboxField, - type CheckboxFieldProps, - DateField, - type DateFieldProps, - EmailField, - type EmailFieldProps, - Form, - type FormProps, - PasswordField, - type PasswordFieldProps, - SubmitButton, - type SubmitButtonProps, - TextField, - type TextFieldProps, -} - -// TODO: Replace the above with the below. -// export type { -// FormikErrors as FormErrors, -// AutocompleteFieldProps, -// CheckboxFieldProps, -// EmailFieldProps, -// ContainerProps, -// PasswordFieldProps, -// SubmitButtonProps, -// TextFieldProps -// }; - -// const Form = { -// AutocompleteField, -// CheckboxField, -// EmailField, -// Container, -// PasswordField, -// SubmitButton, -// TextField -// }; - -// export default Form; diff --git a/src/components/form/index.tsx b/src/components/form/index.tsx new file mode 100644 index 00000000..7fd758d8 --- /dev/null +++ b/src/components/form/index.tsx @@ -0,0 +1,34 @@ +// TODO: fix this broken import +// import AutocompleteField, { +// type AutocompleteFieldProps, +// } from "./AutocompleteField" +import CheckboxField, { type CheckboxFieldProps } from "./CheckboxField" +import EmailField, { type EmailFieldProps } from "./EmailField" +import FirstNameField, { type FirstNameFieldProps } from "./FirstNameField" +import Form, { type FormErrors, type FormProps } from "./Form" +import OtpField, { type OtpFieldProps } from "./OtpField" +import PasswordField, { type PasswordFieldProps } from "./PasswordField" +import SubmitButton, { type SubmitButtonProps } from "./SubmitButton" +import TextField, { type TextFieldProps } from "./TextField" + +export { + // AutocompleteField, + CheckboxField, + EmailField, + FirstNameField, + Form, + OtpField, + PasswordField, + SubmitButton, + TextField, + type CheckboxFieldProps, + type EmailFieldProps, + // type AutocompleteFieldProps, + type FirstNameFieldProps, + type FormErrors, + type FormProps, + type OtpFieldProps, + type PasswordFieldProps, + type SubmitButtonProps, + type TextFieldProps, +} diff --git a/src/components/page/Banner.tsx b/src/components/page/Banner.tsx index 9e14528b..00a9f0ac 100644 --- a/src/components/page/Banner.tsx +++ b/src/components/page/Banner.tsx @@ -1,8 +1,8 @@ +import { Button, Stack, Typography, type ButtonProps } from "@mui/material" import type React from "react" -import { Typography, Stack, Button, type ButtonProps } from "@mui/material" +// import { primary, secondary, tertiary } from "../../theme/colors" import palette from "../../theme/palette" -import { primary, secondary, tertiary } from "../../theme/colors" import Image, { type ImageProps } from "../Image" import Section from "./Section" @@ -26,22 +26,26 @@ const Banner: React.FC = ({ // @ts-expect-error guaranteed to be in palette const contrastText = palette[bgcolor].contrastText - let _bgcolor: string - switch (bgcolor) { - case "primary": - _bgcolor = primary[500] - break - case "secondary": - _bgcolor = secondary[500] - break - case "tertiary": - _bgcolor = tertiary[500] - break - } + // let _bgcolor: string + // switch (bgcolor) { + // case "primary": + // _bgcolor = primary[500] + // break + // case "secondary": + // _bgcolor = secondary[500] + // break + // case "tertiary": + // _bgcolor = tertiary[500] + // break + // } return ( <> -
+
-} - -export interface ContainerProps extends Omit {} - -const Container: React.FC = ({ - children, - ...otherGridProps -}) => { - const location = useLocation() - const childrenArray = React.Children.toArray(children) - - if (location.state !== null) { - const state: ContainerState = location.state - - if (Array.isArray(state.notifications)) { - state.notifications - .filter(notification => "props" in notification) - .forEach((notification, index) => { - childrenArray.splice( - notification.index ?? index, - 0, - , - ) - }) - } - } - - return ( - - {childrenArray} - - ) -} - -export default Container diff --git a/src/components/page/Notification.tsx b/src/components/page/Notification.tsx index f0ec202a..a4f628e5 100644 --- a/src/components/page/Notification.tsx +++ b/src/components/page/Notification.tsx @@ -1,10 +1,10 @@ -import React from "react" -import { Stack, Typography, IconButton } from "@mui/material" import { - InfoOutlined as InfoOutlinedIcon, - ErrorOutline as ErrorOutlineIcon, CloseOutlined as CloseOutlinedIcon, + ErrorOutline as ErrorOutlineIcon, + InfoOutlined as InfoOutlinedIcon, } from "@mui/icons-material" +import { IconButton, Stack, Typography } from "@mui/material" +import React from "react" import palette from "../../theme/palette" import Section from "./Section" @@ -37,9 +37,10 @@ const Notification: React.FC = ({ return (
diff --git a/src/components/page/Page.tsx b/src/components/page/Page.tsx new file mode 100644 index 00000000..d12285aa --- /dev/null +++ b/src/components/page/Page.tsx @@ -0,0 +1,75 @@ +import { Children } from "react" +import { useLocation, type Location } from "react-router-dom" + +import { + useSession, + type SessionMetadata, + type UseSessionChildren, + type UseSessionChildrenFunction, + type UseSessionOptions, +} from "../../hooks/auth" +import Notification, { type NotificationProps } from "./Notification" + +export type PageState = { + notifications: Array<{ + index?: number + props: NotificationProps + }> +} + +export interface PageProps< + SessionUserType extends SessionMetadata["user_type"] | undefined, +> { + children: UseSessionChildren + session?: UseSessionOptions +} + +const Page = < + SessionUserType extends SessionMetadata["user_type"] | undefined = undefined, +>({ + children, + session, +}: PageProps): JSX.Element => { + const { state } = useLocation() as Location + + return ( + <> + {useSession((metadata?: SessionMetadata) => { + if (typeof children === "function") { + children = metadata + ? (children as UseSessionChildrenFunction)(metadata) + : (children as UseSessionChildrenFunction)(metadata) + } + + const childrenArray = Children.toArray(children) + + if ( + typeof state === "object" && + state !== null && + "notifications" in state && + Array.isArray(state.notifications) && + state.notifications.every( + (notification: unknown) => + typeof notification === "object" && + notification !== null && + "props" in notification, + ) + ) { + ;(state.notifications as PageState["notifications"]).forEach( + (notification, index) => { + childrenArray.splice( + notification.index ?? index, + 0, + , + ) + }, + ) + } + + return childrenArray + }, session)} + + ) +} + +export default Page diff --git a/src/components/page/Section.tsx b/src/components/page/Section.tsx index b7e9c965..38ad79f0 100644 --- a/src/components/page/Section.tsx +++ b/src/components/page/Section.tsx @@ -1,26 +1,10 @@ -import type React from "react" -import { - Unstable_Grid2 as Grid, - type Grid2Props, - Container, - type ContainerProps, -} from "@mui/material" +import { Container, type ContainerProps } from "@mui/material" +import type { FC } from "react" -export interface SectionProps extends ContainerProps { - children: React.ReactNode - gridProps?: Omit -} +export interface SectionProps extends ContainerProps {} -const Section: React.FC = ({ - gridProps, - children, - ...containerProps -}) => { - return ( - - {children} - - ) +const Section: FC = containerProps => { + return } export default Section diff --git a/src/components/page/TabBar.tsx b/src/components/page/TabBar.tsx index 339aca45..2584055c 100644 --- a/src/components/page/TabBar.tsx +++ b/src/components/page/TabBar.tsx @@ -13,8 +13,7 @@ import React from "react" import { generatePath, useNavigate, useParams } from "react-router-dom" import { object as YupObject, string as YupString } from "yup" -import { primary } from "../../theme/colors" -import { tryValidateSync } from "../../utils/yup" +import { tryValidateSync } from "../../utils/schema" import Section from "./Section" export interface TabBarProps { @@ -64,7 +63,8 @@ const TabBar: React.FC = ({ return ( <>
@@ -78,7 +78,8 @@ const TabBar: React.FC = ({
diff --git a/src/components/page/index.ts b/src/components/page/index.ts index cbb5919a..224365f7 100644 --- a/src/components/page/index.ts +++ b/src/components/page/index.ts @@ -1,27 +1,19 @@ import Banner, { type BannerProps } from "./Banner" -import Container, { - type ContainerProps, - type ContainerState, -} from "./Container" import Notification, { type NotificationProps } from "./Notification" +import Page, { type PageProps, type PageState } from "./Page" import Section, { type SectionProps } from "./Section" import TabBar, { type TabBarProps } from "./TabBar" -export type { - BannerProps, - ContainerProps, - ContainerState, - NotificationProps, - SectionProps, - TabBarProps, -} - -const Page = { +export { Banner, - Container, Notification, + Page, Section, TabBar, + type BannerProps, + type NotificationProps, + type PageProps, + type PageState, + type SectionProps, + type TabBarProps, } - -export default Page diff --git a/src/components/router/Link.tsx b/src/components/router/Link.tsx new file mode 100644 index 00000000..dc4befcc --- /dev/null +++ b/src/components/router/Link.tsx @@ -0,0 +1,15 @@ +import { Link as MuiLink, type LinkProps as MuiLinkProps } from "@mui/material" +import type { FC } from "react" +import { + Link as RouterLink, + type LinkProps as RouterLinkProps, +} from "react-router-dom" + +export type LinkProps = Omit & RouterLinkProps + +// https://mui.com/material-ui/integrations/routing/#link +const Link: FC = props => { + return +} + +export default Link diff --git a/src/components/router/LinkButton.tsx b/src/components/router/LinkButton.tsx new file mode 100644 index 00000000..a8ee95d8 --- /dev/null +++ b/src/components/router/LinkButton.tsx @@ -0,0 +1,12 @@ +import { Button, type ButtonProps } from "@mui/material" +import type { FC } from "react" +import { Link, type LinkProps } from "react-router-dom" + +export type LinkButtonProps = Omit & LinkProps + +// https://mui.com/material-ui/integrations/routing/#button +const LinkButton: FC = props => { + return