mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 15:55:26 +02:00
improvements; added CustomAliasDialog.tsx
This commit is contained in:
parent
ca9eb78330
commit
3c9918e8b7
@ -58,6 +58,7 @@ const router = createBrowserRouter([
|
||||
element: <AuthenticatedRoute />,
|
||||
children: [
|
||||
{
|
||||
loader: getServerSettings,
|
||||
path: "/aliases",
|
||||
element: <AliasesRoute />,
|
||||
},
|
||||
|
@ -1 +1,2 @@
|
||||
export const MASTER_PASSWORD_LENGTH = 4096
|
||||
export const LOCAL_REGEX = /^[a-zA-Z0-9!#$%&‘*+–/=?^_`.{|}~-]{1,64}$/g
|
||||
|
@ -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 (
|
||||
<ListItemButton href={`/aliases/${btoa(address)}`}>
|
||||
<ListItemText primary={address} />
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end">
|
||||
<MdOutlineMoreVert />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
)
|
||||
}
|
28
src/route-widgets/AliasesRoute/AliasListItem.tsx
Normal file
28
src/route-widgets/AliasesRoute/AliasListItem.tsx
Normal file
@ -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, ReactElement> = {
|
||||
[AliasType.RANDOM]: <FaRandom />,
|
||||
[AliasType.CUSTOM]: <FaHashtag />,
|
||||
}
|
||||
|
||||
export default function AliasListItem({
|
||||
alias,
|
||||
}: AliasListItemProps): ReactElement {
|
||||
const address = `${alias.local}@${alias.domain}`
|
||||
|
||||
return (
|
||||
<ListItemButton href={`/aliases/${btoa(address)}`}>
|
||||
<ListItemIcon>{ALIAS_TYPE_ICON_MAP[alias.type]}</ListItemIcon>
|
||||
<ListItemText primary={address} />
|
||||
</ListItemButton>
|
||||
)
|
||||
}
|
@ -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<Alias, AxiosError, CreateAliasData>(
|
||||
createAlias,
|
||||
export default function CreateAliasButton({
|
||||
onRandomCreated,
|
||||
onCustomCreated,
|
||||
}: CreateAliasButtonProps): ReactElement {
|
||||
const {mutate, isLoading} = useMutation<Alias, AxiosError, void>(
|
||||
() =>
|
||||
createAlias({
|
||||
type: AliasType.RANDOM,
|
||||
}),
|
||||
{
|
||||
onSuccess: onCreated,
|
||||
onSuccess: onRandomCreated,
|
||||
},
|
||||
)
|
||||
|
||||
@ -41,11 +46,7 @@ export default function CreateRandomAliasButton({
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
startIcon={<BsArrowClockwise />}
|
||||
onClick={() =>
|
||||
mutate({
|
||||
type: AliasType.RANDOM,
|
||||
})
|
||||
}
|
||||
onClick={() => mutate()}
|
||||
>
|
||||
Create random alias
|
||||
</Button>
|
||||
@ -62,7 +63,12 @@ export default function CreateRandomAliasButton({
|
||||
onClose={() => setAnchorElement(null)}
|
||||
>
|
||||
<MenuList>
|
||||
<MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setAnchorElement(null)
|
||||
onCustomCreated()
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<FaPen />
|
||||
</ListItemIcon>
|
164
src/route-widgets/AliasesRoute/CustomAliasDialog.tsx
Normal file
164
src/route-widgets/AliasesRoute/CustomAliasDialog.tsx
Normal file
@ -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<CreateAliasData, "type">
|
||||
>(
|
||||
values =>
|
||||
// @ts-ignore
|
||||
createAlias({
|
||||
type: AliasType.CUSTOM,
|
||||
...values,
|
||||
}),
|
||||
{
|
||||
onSuccess: () => {
|
||||
reset()
|
||||
onCreated()
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const formik = useFormik<Form>({
|
||||
validationSchema: schema,
|
||||
initialValues: {
|
||||
local: "",
|
||||
},
|
||||
onSubmit: async (values, {setErrors}) => {
|
||||
try {
|
||||
await mutateAsync({
|
||||
local: values.local,
|
||||
})
|
||||
} catch (error) {
|
||||
setErrors(parseFastAPIError(error as AxiosError))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog onClose={onClose} open={visible} keepMounted={false}>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<DialogTitle>Create Custom Alias</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
You can define your own custom alias. Note that a
|
||||
random suffix will be added at the end to avoid
|
||||
duplicates.
|
||||
</DialogContentText>
|
||||
<Box paddingY={4}>
|
||||
<TextField
|
||||
key="local"
|
||||
fullWidth
|
||||
autoFocus
|
||||
name="local"
|
||||
id="local"
|
||||
label="Address"
|
||||
value={formik.values.local}
|
||||
onChange={formik.handleChange}
|
||||
disabled={formik.isSubmitting}
|
||||
error={
|
||||
formik.touched.local &&
|
||||
Boolean(formik.errors.local)
|
||||
}
|
||||
helperText={
|
||||
formik.touched.local && formik.errors.local
|
||||
}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Typography variant="body2">
|
||||
<span>
|
||||
{Array(
|
||||
serverSettings.customAliasSuffixLength,
|
||||
)
|
||||
.fill("#")
|
||||
.join("")}
|
||||
</span>
|
||||
<span>
|
||||
@{serverSettings.mailDomain}
|
||||
</span>
|
||||
</Typography>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} startIcon={<TiCancel />}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {}}
|
||||
disabled={isLoading}
|
||||
startIcon={<FaPen />}
|
||||
variant="contained"
|
||||
type="submit"
|
||||
>
|
||||
Create Alias
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
<ErrorSnack message={formik.errors.detail} />
|
||||
<SuccessSnack
|
||||
message={isSuccess && "Created Alias successfully!"}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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}
|
||||
|
@ -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<boolean>(false)
|
||||
|
||||
return (
|
||||
<SimplePage title="Aliases">
|
||||
<Grid container spacing={4} direction="column" alignItems="stretch">
|
||||
<Grid item>
|
||||
<Typography variant="h6" component="h2">
|
||||
Random Aliases
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<QueryResult<PaginationResult<AliasList>> query={query}>
|
||||
{result => (
|
||||
<List>
|
||||
{result.items.map(alias => (
|
||||
<AliasListItem
|
||||
key={alias.id}
|
||||
alias={alias}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</QueryResult>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<CreateRandomAliasButton
|
||||
onCreated={() => query.refetch()}
|
||||
<>
|
||||
<SimplePage
|
||||
title="Aliases"
|
||||
actions={
|
||||
<CreateAliasButton
|
||||
onRandomCreated={() => query.refetch()}
|
||||
onCustomCreated={() => setShowCustomCreateDialog(true)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</SimplePage>
|
||||
}
|
||||
>
|
||||
<QueryResult<PaginationResult<AliasList>> query={query}>
|
||||
{result => (
|
||||
<List>
|
||||
{result.items.map(alias => (
|
||||
<AliasListItem key={alias.id} alias={alias} />
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</QueryResult>
|
||||
</SimplePage>
|
||||
<CustomAliasDialog
|
||||
visible={showCustomCreateDialog}
|
||||
onCreated={() => {
|
||||
setShowCustomCreateDialog(false)
|
||||
query.refetch()
|
||||
}}
|
||||
onClose={() => setShowCustomCreateDialog(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user