import { useState, type FormEvent } from "react"; import { useNavigate, Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { useAuthStore } from "@/stores/authStore"; import { useToastStore } from "@/stores/toastStore"; import { ApiError, toErrorMessage } from "@/lib/api"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { Card } from "@/components/ui/Card"; const USERNAME_INVALID_CHARS_RE = /[^a-zA-Z0-9_]/; export function RegisterPage() { const { t } = useTranslation(); const navigate = useNavigate(); const register = useAuthStore((s) => s.register); const pushToast = useToastStore((s) => s.push); const [email, setEmail] = useState(""); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [passwordConfirm, setPasswordConfirm] = useState(""); const [submitting, setSubmitting] = useState(false); const [errors, setErrors] = useState>({}); const validate = (): boolean => { const next: Record = {}; if (!email.includes("@")) next.email = t("errors.validation"); if (username.trim().length < 3) next.username = t("errors.validation"); if (USERNAME_INVALID_CHARS_RE.test(username.trim())) { next.username = t("auth.username_invalid_chars"); } if (password.length < 8) next.password = t("errors.validation"); if (password !== passwordConfirm) next.password_confirm = t("errors.validation"); setErrors(next); return Object.keys(next).length === 0; }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); if (!validate()) return; setSubmitting(true); try { await register({ email: email.trim(), username: username.trim(), password, password_confirm: passwordConfirm, }); pushToast("success", t("auth.register_success")); navigate("/login"); } catch (err) { if (err instanceof ApiError && err.status === 422) { // FastAPI 422 validation error — extract field-level messages. const detail = err.details; const fieldMsgs: Record = {}; let generalMsg = ""; if (Array.isArray(detail)) { for (const item of detail) { if (item && typeof item === "object") { const obj = item as { loc?: unknown; msg?: string; message?: string }; const loc = Array.isArray(obj.loc) ? obj.loc.map((x) => String(x)).filter((x) => x !== "body" && x !== "query").join(".") : obj.loc != null ? String(obj.loc) : ""; const msg = obj.msg || obj.message || ""; if (loc) { fieldMsgs[loc] = msg || t("errors.validation"); } else { generalMsg = generalMsg ? `${generalMsg}; ${msg}` : msg; } } } } // If we have specific field messages, show them inline. if (Object.keys(fieldMsgs).length > 0) { setErrors((prev) => ({ ...prev, ...fieldMsgs })); } // If the username field failed validation, show the specific message. if (fieldMsgs.username || fieldMsgs["username"]) { pushToast("error", t("auth.username_invalid_chars")); } else { pushToast("error", generalMsg || t("auth.register_failed")); } } else { pushToast("error", toErrorMessage(err, t("auth.register_failed"))); } } finally { setSubmitting(false); } }; return (
setEmail(e.target.value)} autoComplete="email" required error={errors.email} /> setUsername(e.target.value)} autoComplete="username" required error={errors.username} hint={t("auth.username_hint")} /> setPassword(e.target.value)} autoComplete="new-password" required error={errors.password} /> setPasswordConfirm(e.target.value)} autoComplete="new-password" required error={errors.password_confirm} />

{t("auth.have_account")}

); }