diff --git a/src/App.tsx b/src/App.tsx index bf9fb78..52b8cd1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -58,6 +58,7 @@ const router = createBrowserRouter([ element: , children: [ { + loader: getServerSettings, path: "/aliases", element: , }, diff --git a/src/constants/values.ts b/src/constants/values.ts index 83df337..c78bd5c 100644 --- a/src/constants/values.ts +++ b/src/constants/values.ts @@ -1 +1,2 @@ export const MASTER_PASSWORD_LENGTH = 4096 +export const LOCAL_REGEX = /^[a-zA-Z0-9!#$%&‘*+–/=?^_`.{|}~-]{1,64}$/g diff --git a/src/route-widgets/AliasRoute/AliasListItem.tsx b/src/route-widgets/AliasRoute/AliasListItem.tsx deleted file mode 100644 index aa93279..0000000 --- a/src/route-widgets/AliasRoute/AliasListItem.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import {ReactElement} from "react" -import {MdOutlineMoreVert} from "react-icons/md" - -import { - IconButton, - ListItemButton, - ListItemSecondaryAction, - ListItemText, -} from "@mui/material" - -import {AliasList} from "~/server-types" - -export interface AliasListItemProps { - alias: AliasList -} - -export default function AliasListItem({ - alias, -}: AliasListItemProps): ReactElement { - const address = `${alias.local}@${alias.domain}` - - return ( - - - - - - - - - ) -} diff --git a/src/route-widgets/AliasesRoute/AliasListItem.tsx b/src/route-widgets/AliasesRoute/AliasListItem.tsx new file mode 100644 index 0000000..3910e5c --- /dev/null +++ b/src/route-widgets/AliasesRoute/AliasListItem.tsx @@ -0,0 +1,28 @@ +import {ReactElement} from "react" +import {FaHashtag, FaRandom} from "react-icons/fa" + +import {ListItemButton, ListItemIcon, ListItemText} from "@mui/material" + +import {AliasList, AliasType} from "~/server-types" + +export interface AliasListItemProps { + alias: AliasList +} + +const ALIAS_TYPE_ICON_MAP: Record = { + [AliasType.RANDOM]: , + [AliasType.CUSTOM]: , +} + +export default function AliasListItem({ + alias, +}: AliasListItemProps): ReactElement { + const address = `${alias.local}@${alias.domain}` + + return ( + + {ALIAS_TYPE_ICON_MAP[alias.type]} + + + ) +} diff --git a/src/route-widgets/AliasRoute/CreateRandomAliasButton.tsx b/src/route-widgets/AliasesRoute/CreateAliasButton.tsx similarity index 66% rename from src/route-widgets/AliasRoute/CreateRandomAliasButton.tsx rename to src/route-widgets/AliasesRoute/CreateAliasButton.tsx index 131b33d..73f26fc 100644 --- a/src/route-widgets/AliasRoute/CreateRandomAliasButton.tsx +++ b/src/route-widgets/AliasesRoute/CreateAliasButton.tsx @@ -15,20 +15,25 @@ import { } from "@mui/material" import {useMutation} from "@tanstack/react-query" -import {CreateAliasData, createAlias} from "~/apis" +import {createAlias} from "~/apis" import {Alias, AliasType} from "~/server-types" -export interface CreateRandomAliasButtonProps { - onCreated: (alias: Alias) => void +export interface CreateAliasButtonProps { + onRandomCreated: (alias: Alias) => void + onCustomCreated: () => void } -export default function CreateRandomAliasButton({ - onCreated, -}: CreateRandomAliasButtonProps): ReactElement { - const {mutate, isLoading} = useMutation( - createAlias, +export default function CreateAliasButton({ + onRandomCreated, + onCustomCreated, +}: CreateAliasButtonProps): ReactElement { + const {mutate, isLoading} = useMutation( + () => + createAlias({ + type: AliasType.RANDOM, + }), { - onSuccess: onCreated, + onSuccess: onRandomCreated, }, ) @@ -41,11 +46,7 @@ export default function CreateRandomAliasButton({ @@ -62,7 +63,12 @@ export default function CreateRandomAliasButton({ onClose={() => setAnchorElement(null)} > - + { + setAnchorElement(null) + onCustomCreated() + }} + > diff --git a/src/route-widgets/AliasesRoute/CustomAliasDialog.tsx b/src/route-widgets/AliasesRoute/CustomAliasDialog.tsx new file mode 100644 index 0000000..a9b23b0 --- /dev/null +++ b/src/route-widgets/AliasesRoute/CustomAliasDialog.tsx @@ -0,0 +1,164 @@ +import * as yup from "yup" +import {ReactElement} from "react" +import {useFormik} from "formik" +import {useLoaderData} from "react-router-dom" +import {AxiosError} from "axios" +import {TiCancel} from "react-icons/ti" +import {FaPen} from "react-icons/fa" + +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + InputAdornment, + TextField, + Typography, +} from "@mui/material" +import {useMutation} from "@tanstack/react-query" + +import {Alias, AliasType, ServerSettings} from "~/server-types" +import {CreateAliasData, createAlias} from "~/apis" +import {parseFastAPIError} from "~/utils" +import {LOCAL_REGEX} from "~/constants/values" +import {ErrorSnack, SuccessSnack} from "~/components" + +export interface CustomAliasDialogProps { + visible: boolean + onCreated: () => void + onClose: () => void +} + +interface Form { + local: string + + detail?: string +} + +export default function CustomAliasDialog({ + visible, + onCreated, + onClose, +}: CustomAliasDialogProps): ReactElement { + const serverSettings = useLoaderData() as ServerSettings + + const schema = yup.object().shape({ + local: yup + .string() + .matches(LOCAL_REGEX) + .required() + .min(1) + .max(64 - serverSettings.customAliasSuffixLength - 1), + }) + + const {mutateAsync, isLoading, isSuccess, reset} = useMutation< + Alias, + AxiosError, + Omit + >( + values => + // @ts-ignore + createAlias({ + type: AliasType.CUSTOM, + ...values, + }), + { + onSuccess: () => { + reset() + onCreated() + }, + }, + ) + + const formik = useFormik
({ + validationSchema: schema, + initialValues: { + local: "", + }, + onSubmit: async (values, {setErrors}) => { + try { + await mutateAsync({ + local: values.local, + }) + } catch (error) { + setErrors(parseFastAPIError(error as AxiosError)) + } + }, + }) + + return ( + <> + + + Create Custom Alias + + + You can define your own custom alias. Note that a + random suffix will be added at the end to avoid + duplicates. + + + + + + {Array( + serverSettings.customAliasSuffixLength, + ) + .fill("#") + .join("")} + + + @{serverSettings.mailDomain} + + + + ), + }} + /> + + + + + + + +
+ + + + ) +} diff --git a/src/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm.tsx b/src/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm.tsx index 6468c0a..45e2e84 100644 --- a/src/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm.tsx +++ b/src/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm.tsx @@ -95,7 +95,7 @@ export default function ConfirmCodeForm({ fullWidth name="code" id="code" - label="code" + label="Code" value={formik.values.code} onChange={formik.handleChange} disabled={formik.isSubmitting} diff --git a/src/routes/AliasesRoute.tsx b/src/routes/AliasesRoute.tsx index e2afce6..05c06c3 100644 --- a/src/routes/AliasesRoute.tsx +++ b/src/routes/AliasesRoute.tsx @@ -1,12 +1,13 @@ -import {ReactElement} from "react" +import {ReactElement, useState} from "react" import {AxiosError} from "axios" -import {Grid, List, Typography} from "@mui/material" +import {List} from "@mui/material" import {useQuery} from "@tanstack/react-query" import {AliasList, PaginationResult} from "~/server-types" -import AliasListItem from "~/route-widgets/AliasRoute/AliasListItem" -import CreateRandomAliasButton from "~/route-widgets/AliasRoute/CreateRandomAliasButton" +import AliasListItem from "~/route-widgets/AliasesRoute/AliasListItem" +import CreateAliasButton from "~/route-widgets/AliasesRoute/CreateAliasButton" +import CustomAliasDialog from "~/route-widgets/AliasesRoute/CustomAliasDialog" import QueryResult from "~/components/QueryResult" import SimplePage from "~/components/SimplePage" import getAliases from "~/apis/get-aliases" @@ -17,34 +18,38 @@ export default function AliasesRoute(): ReactElement { getAliases, ) + const [showCustomCreateDialog, setShowCustomCreateDialog] = + useState(false) + return ( - - - - - Random Aliases - - - - > query={query}> - {result => ( - - {result.items.map(alias => ( - - ))} - - )} - - - - query.refetch()} + <> + query.refetch()} + onCustomCreated={() => setShowCustomCreateDialog(true)} /> - - - + } + > + > query={query}> + {result => ( + + {result.items.map(alias => ( + + ))} + + )} + + + { + setShowCustomCreateDialog(false) + query.refetch() + }} + onClose={() => setShowCustomCreateDialog(false)} + /> + ) } diff --git a/src/server-types.ts b/src/server-types.ts index 9798ca8..9e5a095 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -74,6 +74,7 @@ export interface ServerSettings { emailLoginTokenChars: string emailLoginTokenLength: number emailResendWaitTime: number + customAliasSuffixLength: number } export interface Alias { @@ -95,6 +96,7 @@ export interface AliasList { domain: string local: string isActive: boolean + type: AliasType } export interface Report {