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 { Portal } from "@/components/utils/Portal";
|
||||||
import { Dialog, DialogButton, DialogButtonGroup, DialogHeadline, DialogSupportingText } from "@/components/ui/Dialog";
|
import { Dialog, DialogButton, DialogButtonGroup, DialogHeadline, DialogSupportingText } from "@/components/ui/Dialog";
|
||||||
import { FilledButton } from "@/components/ui/Buttons/FilledButton";
|
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 {
|
interface CaptchaSessionResponse {
|
||||||
g: string;
|
g: string;
|
||||||
@ -37,9 +44,9 @@ interface RegistrationFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
|
const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
|
||||||
const [username, setUsername] = useState("");
|
const [usernameInput, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [passwordInput, setPassword] = useState("");
|
||||||
const [nickname, setNickname] = useState("");
|
const [nicknameInput, setNickname] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
const [dialogContent, setDialogContent] = useState(<></>);
|
const [dialogContent, setDialogContent] = useState(<></>);
|
||||||
@ -68,6 +75,44 @@ const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const register = async () => {
|
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);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
if (!captchaSession?.g || !captchaSession?.n || !captchaSession?.t || !captchaSession?.id) {
|
if (!captchaSession?.g || !captchaSession?.n || !captchaSession?.t || !captchaSession?.id) {
|
||||||
@ -92,9 +137,9 @@ const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
|
|||||||
Authorization: `Bearer ${captchaResult.token}`
|
Authorization: `Bearer ${captchaResult.token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
username,
|
username: username,
|
||||||
password,
|
password: password,
|
||||||
nickname
|
nickname: nickname
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -124,7 +169,7 @@ const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
|
|||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
labelText="用户名"
|
labelText="用户名"
|
||||||
inputText={username}
|
inputText={usernameInput}
|
||||||
onInputTextChange={setUsername}
|
onInputTextChange={setUsername}
|
||||||
maxChar={50}
|
maxChar={50}
|
||||||
supportingText="*必填。用户名是唯一的,不区分大小写。"
|
supportingText="*必填。用户名是唯一的,不区分大小写。"
|
||||||
@ -132,40 +177,19 @@ const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
|
|||||||
<TextField
|
<TextField
|
||||||
labelText="密码"
|
labelText="密码"
|
||||||
type="password"
|
type="password"
|
||||||
inputText={password}
|
inputText={passwordInput}
|
||||||
onInputTextChange={setPassword}
|
onInputTextChange={setPassword}
|
||||||
supportingText="*必填。密码至少为 4 个字符。"
|
supportingText="*必填。密码至少为 4 个字符。"
|
||||||
maxChar={120}
|
maxChar={120}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
labelText="昵称"
|
labelText="昵称"
|
||||||
inputText={nickname}
|
inputText={nicknameInput}
|
||||||
onInputTextChange={setNickname}
|
onInputTextChange={setNickname}
|
||||||
supportingText="昵称可以重复。"
|
supportingText="昵称可以重复。"
|
||||||
maxChar={30}
|
maxChar={30}
|
||||||
/>
|
/>
|
||||||
<FilledButton
|
<FilledButton type="submit" disabled={loading}>
|
||||||
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}>
|
|
||||||
{!loading ? <span>注册</span> : <LoadingSpinner />}
|
{!loading ? <span>注册</span> : <LoadingSpinner />}
|
||||||
</FilledButton>
|
</FilledButton>
|
||||||
<Portal>
|
<Portal>
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"swr": "^2.3.3",
|
"swr": "^2.3.3",
|
||||||
|
"yup": "^1.6.1",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@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=="],
|
"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": ["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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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/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=="],
|
"@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;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OptionalChidrenProps {
|
type OptionalChidrenProps<T = React.HTMLAttributes<HTMLElement>> = T & {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
};
|
||||||
|
|
||||||
type DialogHeadlineProps = OptionalChidrenProps;
|
type HeadElementAttr = React.HTMLAttributes<HTMLHeadElement>;
|
||||||
type DialogSupportingTextProps = OptionalChidrenProps;
|
type DivElementAttr = React.HTMLAttributes<HTMLDivElement>;
|
||||||
type DialogButtonGroupProps = OptionalChidrenProps;
|
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>;
|
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DialogHeadline: React.FC<DialogHeadlineProps> = ({ children }: DialogHeadlineProps) => {
|
export const DialogHeadline: React.FC<DialogHeadlineProps> = ({
|
||||||
return <h2 className="text-2xl leading-8 text-on-surface dark:text-dark-on-surface">{children}</h2>;
|
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) => {
|
export const DialogSupportingText: React.FC<DialogSupportingTextProps> = ({
|
||||||
return <div className="mt-4 text-sm leading-5 mb-6">{children}</div>;
|
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) => {
|
export const DialogButton: React.FC<DialogButtonProps> = ({ children, onClick, ...rest }: DialogButtonProps) => {
|
||||||
return <TextButton onClick={onClick}>{children}</TextButton>;
|
return (
|
||||||
|
<TextButton onClick={onClick} {...rest}>
|
||||||
|
{children}
|
||||||
|
</TextButton>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DialogButtonGroup: React.FC<DialogButtonGroupProps> = ({ children }: DialogButtonGroupProps) => {
|
export const DialogButtonGroup: React.FC<DialogButtonGroupProps> = ({ children, ...rest }: DialogButtonGroupProps) => {
|
||||||
return <div className="flex justify-end gap-2">{children}</div>;
|
return (
|
||||||
|
<div className="flex justify-end gap-2" {...rest}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Dialog: React.FC<DialogProps> = ({ show, children }: DialogProps) => {
|
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)" }}
|
animate={{ opacity: 1, transform: "scale(1)" }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ ease: [0.31, 0.69, 0.3, 1.02], duration: 0.3 }}
|
transition={{ ease: [0.31, 0.69, 0.3, 1.02], duration: 0.3 }}
|
||||||
|
aria-modal="true"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
@ -14,7 +14,8 @@
|
|||||||
"next": "^15.1.8",
|
"next": "^15.1.8",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"swr": "^2.3.3"
|
"swr": "^2.3.3",
|
||||||
|
"yup": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
|
Loading…
Reference in New Issue
Block a user