update: the error dialog
This commit is contained in:
parent
96903dec2b
commit
ae338f88ee
@ -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>
|
||||
|
@ -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=="],
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user