update: the error dialog

This commit is contained in:
alikia2x (寒寒) 2025-05-31 11:47:45 +08:00
parent 96903dec2b
commit ae338f88ee
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
4 changed files with 111 additions and 46 deletions

View File

@ -9,6 +9,13 @@ import { ApiRequestError } from "@/lib/net";
import { Portal } from "@/components/utils/Portal";
import { Dialog, DialogButton, DialogButtonGroup, DialogHeadline, DialogSupportingText } from "@/components/ui/Dialog";
import { FilledButton } from "@/components/ui/Buttons/FilledButton";
import { string, object, ValidationError } from "yup";
const FormSchema = object({
username: string().required().max(50),
password: string().required().min(4).max(120),
nickname: string().optional().max(30)
});
interface CaptchaSessionResponse {
g: string;
@ -37,9 +44,9 @@ interface RegistrationFormProps {
}
const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [nickname, setNickname] = useState("");
const [usernameInput, setUsername] = useState("");
const [passwordInput, setPassword] = useState("");
const [nicknameInput, setNickname] = useState("");
const [loading, setLoading] = useState(false);
const [showDialog, setShowDialog] = useState(false);
const [dialogContent, setDialogContent] = useState(<></>);
@ -68,6 +75,44 @@ const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
};
const register = async () => {
let username: string | undefined;
let password: string | undefined;
let nickname: string | undefined;
try {
const formData = await FormSchema.validate({
username: usernameInput,
password: passwordInput,
nickname: nicknameInput
});
username = formData.username;
password = formData.password;
nickname = formData.nickname;
} catch (e) {
if (!(e instanceof ValidationError)) {
return;
}
setShowDialog(true);
setDialogContent(
<>
<DialogHeadline></DialogHeadline>
<DialogSupportingText>
<p></p>
<span>: </span>
<br />
<ol>
{e.errors.map((item, i) => {
return <li key={i}>{item}</li>;
})}
</ol>
</DialogSupportingText>
<DialogButtonGroup>
<DialogButton onClick={() => setShowDialog(false)}>Close</DialogButton>
</DialogButtonGroup>
</>
);
return;
}
setLoading(true);
try {
if (!captchaSession?.g || !captchaSession?.n || !captchaSession?.t || !captchaSession?.id) {
@ -92,9 +137,9 @@ const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
Authorization: `Bearer ${captchaResult.token}`
},
body: JSON.stringify({
username,
password,
nickname
username: username,
password: password,
nickname: nickname
})
});
@ -124,7 +169,7 @@ const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
>
<TextField
labelText="用户名"
inputText={username}
inputText={usernameInput}
onInputTextChange={setUsername}
maxChar={50}
supportingText="*必填。用户名是唯一的,不区分大小写。"
@ -132,40 +177,19 @@ const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
<TextField
labelText="密码"
type="password"
inputText={password}
inputText={passwordInput}
onInputTextChange={setPassword}
supportingText="*必填。密码至少为 4 个字符。"
maxChar={120}
/>
<TextField
labelText="昵称"
inputText={nickname}
inputText={nicknameInput}
onInputTextChange={setNickname}
supportingText="昵称可以重复。"
maxChar={30}
/>
<FilledButton
type="button"
onClick={() => {
setShowDialog(true);
setDialogContent(
<>
<DialogHeadline>Error</DialogHeadline>
<DialogSupportingText>
<p>Your operation frequency is too high. Please try again later. (RATE_LIMIT_EXCEED)</p>
</DialogSupportingText>
<DialogButtonGroup>
<DialogButton onClick={() => setShowDialog(false)}>Close</DialogButton>
</DialogButtonGroup>
</>
);
}}
size="m"
shape="square"
>
Show Dialog
</FilledButton>
<FilledButton type="submit" disabled={loading} tabIndex={1}>
<FilledButton type="submit" disabled={loading}>
{!loading ? <span></span> : <LoadingSpinner />}
</FilledButton>
<Portal>

View File

@ -9,6 +9,7 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"swr": "^2.3.3",
"yup": "^1.6.1",
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@ -213,6 +214,8 @@
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
"property-expr": ["property-expr@2.0.6", "", {}, "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
@ -239,8 +242,14 @@
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
"tiny-case": ["tiny-case@1.0.3", "", {}, "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="],
"toposort": ["toposort@2.0.2", "", {}, "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
@ -249,6 +258,8 @@
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"yup": ["yup@1.6.1", "", { "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", "toposort": "^2.0.2", "type-fest": "^2.19.0" } }, "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],

View File

@ -18,32 +18,60 @@ interface DialogProps {
children?: React.ReactNode;
}
interface OptionalChidrenProps {
type OptionalChidrenProps<T = React.HTMLAttributes<HTMLElement>> = T & {
children?: React.ReactNode;
}
};
type DialogHeadlineProps = OptionalChidrenProps;
type DialogSupportingTextProps = OptionalChidrenProps;
type DialogButtonGroupProps = OptionalChidrenProps;
type HeadElementAttr = React.HTMLAttributes<HTMLHeadElement>;
type DivElementAttr = React.HTMLAttributes<HTMLDivElement>;
type ButtonElementAttr = React.HTMLAttributes<HTMLButtonElement>;
interface DialogButtonProps extends OptionalChidrenProps {
type DialogHeadlineProps = OptionalChidrenProps<HeadElementAttr>;
type DialogSupportingTextProps = OptionalChidrenProps<DivElementAttr>;
type DialogButtonGroupProps = OptionalChidrenProps<DivElementAttr>;
interface DialogButtonProps extends OptionalChidrenProps<ButtonElementAttr> {
onClick?: React.MouseEventHandler<HTMLButtonElement>;
}
export const DialogHeadline: React.FC<DialogHeadlineProps> = ({ children }: DialogHeadlineProps) => {
return <h2 className="text-2xl leading-8 text-on-surface dark:text-dark-on-surface">{children}</h2>;
export const DialogHeadline: React.FC<DialogHeadlineProps> = ({
children,
className,
...rest
}: DialogHeadlineProps) => {
return (
<h2 className={"text-2xl leading-8 text-on-surface dark:text-dark-on-surface " + className || ""} {...rest}>
{children}
</h2>
);
};
export const DialogSupportingText: React.FC<DialogSupportingTextProps> = ({ children }: DialogHeadlineProps) => {
return <div className="mt-4 text-sm leading-5 mb-6">{children}</div>;
export const DialogSupportingText: React.FC<DialogSupportingTextProps> = ({
children,
className,
...rest
}: DialogHeadlineProps) => {
return (
<div className={"mt-4 text-sm leading-5 mb-6 " + className || ""} {...rest}>
{children}
</div>
);
};
export const DialogButton: React.FC<DialogButtonProps> = ({ children, onClick }: DialogButtonProps) => {
return <TextButton onClick={onClick}>{children}</TextButton>;
export const DialogButton: React.FC<DialogButtonProps> = ({ children, onClick, ...rest }: DialogButtonProps) => {
return (
<TextButton onClick={onClick} {...rest}>
{children}
</TextButton>
);
};
export const DialogButtonGroup: React.FC<DialogButtonGroupProps> = ({ children }: DialogButtonGroupProps) => {
return <div className="flex justify-end gap-2">{children}</div>;
export const DialogButtonGroup: React.FC<DialogButtonGroupProps> = ({ children, ...rest }: DialogButtonGroupProps) => {
return (
<div className="flex justify-end gap-2" {...rest}>
{children}
</div>
);
};
export const Dialog: React.FC<DialogProps> = ({ show, children }: DialogProps) => {
@ -67,6 +95,7 @@ export const Dialog: React.FC<DialogProps> = ({ show, children }: DialogProps) =
animate={{ opacity: 1, transform: "scale(1)" }}
exit={{ opacity: 0 }}
transition={{ ease: [0.31, 0.69, 0.3, 1.02], duration: 0.3 }}
aria-modal="true"
>
{children}
</motion.div>

View File

@ -14,7 +14,8 @@
"next": "^15.1.8",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"swr": "^2.3.3"
"swr": "^2.3.3",
"yup": "^1.6.1"
},
"devDependencies": {
"typescript": "^5",