fix(auth): update auth page components

This commit is contained in:
zhenyi 2026-05-31 13:11:37 +08:00
parent 71f90bcd4d
commit 82475e95d5
6 changed files with 75 additions and 66 deletions

View File

@ -1,4 +1,4 @@
import { useEffect, useState, type ReactNode } from "react"; import { useCallback, useEffect, useState, type ReactNode } from "react";
import { Apple, Eye, EyeOff, Globe, RotateCw, Send } from "lucide-react"; import { Apple, Eye, EyeOff, Globe, RotateCw, Send } from "lucide-react";
import { client, type CaptchaResponse } from "@/client"; import { client, type CaptchaResponse } from "@/client";
@ -8,10 +8,10 @@ import { Label } from "@/components/ui/label";
export function Divider() { export function Divider() {
return ( return (
<div className="my-5 flex items-center gap-4 text-xs font-medium uppercase text-zinc-400"> <div className="my-5 flex items-center gap-4 text-xs font-medium uppercase text-muted-foreground/60">
<div className="h-px flex-1 bg-zinc-200" /> <div className="h-px flex-1 bg-border" />
<span>or</span> <span>or</span>
<div className="h-px flex-1 bg-zinc-200" /> <div className="h-px flex-1 bg-border" />
</div> </div>
); );
} }
@ -19,15 +19,15 @@ export function Divider() {
export function SocialButtons() { export function SocialButtons() {
return ( return (
<div className="mt-6 grid grid-cols-3 gap-3"> <div className="mt-6 grid grid-cols-3 gap-3">
<Button className="h-11 rounded-lg border-zinc-200 bg-white text-zinc-950 hover:bg-zinc-50" type="button" variant="outline"> <Button className="h-11 rounded-xl border-border bg-background text-foreground hover:bg-muted" type="button" variant="outline">
<Apple className="size-5 fill-current" aria-hidden="true" /> <Apple className="size-5 fill-current" aria-hidden="true" />
<span className="sr-only">Continue with Apple</span> <span className="sr-only">Continue with Apple</span>
</Button> </Button>
<Button className="h-11 rounded-lg border-zinc-200 bg-white text-zinc-950 hover:bg-zinc-50" type="button" variant="outline"> <Button className="h-11 rounded-xl border-border bg-background text-foreground hover:bg-muted" type="button" variant="outline">
<Globe className="size-5 text-red-500" aria-hidden="true" /> <Globe className="size-5 text-destructive" aria-hidden="true" />
<span className="sr-only">Continue with Google</span> <span className="sr-only">Continue with Google</span>
</Button> </Button>
<Button className="h-11 rounded-lg border-zinc-200 bg-white text-sky-500 hover:bg-zinc-50" type="button" variant="outline"> <Button className="h-11 rounded-xl border-border bg-background text-primary hover:bg-muted" type="button" variant="outline">
<Send className="size-5 fill-current" aria-hidden="true" /> <Send className="size-5 fill-current" aria-hidden="true" />
<span className="sr-only">Continue with Twitter</span> <span className="sr-only">Continue with Twitter</span>
</Button> </Button>
@ -39,17 +39,20 @@ export function PasswordInput({
id, id,
name, name,
placeholder, placeholder,
autoComplete = "current-password",
}: { }: {
id: string; id: string;
name: string; name: string;
placeholder: string; placeholder: string;
autoComplete?: string;
}) { }) {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
return ( return (
<div className="relative"> <div className="relative">
<Input <Input
className="h-11 rounded-lg border-zinc-200 pr-11 text-sm placeholder:text-zinc-400 focus-visible:ring-zinc-200" className="h-11 rounded-xl border-input bg-background pr-11 text-sm placeholder:text-muted-foreground/50 focus-visible:ring-ring/20"
autoComplete={autoComplete}
id={id} id={id}
name={name} name={name}
placeholder={placeholder} placeholder={placeholder}
@ -58,7 +61,7 @@ export function PasswordInput({
/> />
<Button <Button
aria-label={visible ? "Hide password" : "Show password"} aria-label={visible ? "Hide password" : "Show password"}
className="absolute right-1.5 top-1/2 size-9 -translate-y-1/2 text-zinc-500 hover:bg-transparent" className="absolute right-1.5 top-1/2 size-9 -translate-y-1/2 text-muted-foreground hover:bg-transparent"
onClick={() => setVisible((current) => !current)} onClick={() => setVisible((current) => !current)}
type="button" type="button"
variant="ghost" variant="ghost"
@ -77,7 +80,7 @@ export function CaptchaField({
const [captcha, setCaptcha] = useState<CaptchaResponse | null>(null); const [captcha, setCaptcha] = useState<CaptchaResponse | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const loadCaptcha = async () => { const loadCaptcha = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const response = await client.authCaptcha({ const response = await client.authCaptcha({
@ -93,11 +96,12 @@ export function CaptchaField({
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [onPublicKey]);
useEffect(() => { useEffect(() => {
void loadCaptcha(); const timer = window.setTimeout(() => void loadCaptcha(), 0);
}, []); return () => window.clearTimeout(timer);
}, [loadCaptcha]);
const src = captcha?.base64?.startsWith("data:") const src = captcha?.base64?.startsWith("data:")
? captcha.base64 ? captcha.base64
@ -105,26 +109,28 @@ export function CaptchaField({
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-semibold text-zinc-950" htmlFor="captcha"> <Label className="text-sm font-semibold text-foreground" htmlFor="captcha">
Captcha Captcha
</Label> </Label>
<div className="flex gap-3"> <div className="flex gap-3">
<Input <Input
className="h-11 rounded-lg border-zinc-200 text-sm placeholder:text-zinc-400 focus-visible:ring-zinc-200" autoComplete="off"
className="h-11 rounded-xl border-input bg-background text-sm placeholder:text-muted-foreground/50 focus-visible:ring-ring/20"
id="captcha" id="captcha"
inputMode="text"
name="captcha" name="captcha"
placeholder="Enter captcha..." placeholder="Enter captcha"
required required
/> />
<button <button
aria-label="Refresh captcha" aria-label="Refresh captcha"
className="grid h-11 w-30 shrink-0 place-items-center rounded-lg border border-zinc-200 bg-zinc-50 text-zinc-500" className="grid h-11 w-30 shrink-0 place-items-center rounded-xl border border-border bg-muted/40 text-muted-foreground"
disabled={loading} disabled={loading}
onClick={() => void loadCaptcha()} onClick={() => void loadCaptcha()}
type="button" type="button"
> >
{captcha ? ( {captcha ? (
<img alt="Captcha" className="h-9 max-w-28 object-contain" src={src} /> <img alt="Captcha" className="h-9 max-w-28 object-contain" height={36} src={src} width={112} />
) : ( ) : (
<RotateCw className="size-4" /> <RotateCw className="size-4" />
)} )}
@ -149,15 +155,17 @@ export function TextField({
}) { }) {
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-semibold text-zinc-950" htmlFor={id}> <Label className="text-sm font-semibold text-foreground" htmlFor={id}>
{label} {label}
</Label> </Label>
<Input <Input
className="h-11 rounded-lg border-zinc-200 text-sm placeholder:text-zinc-400 focus-visible:ring-zinc-200" autoComplete={type === "email" ? "email" : name === "username" ? "username" : "off"}
className="h-11 rounded-xl border-input bg-background text-sm placeholder:text-muted-foreground/50 focus-visible:ring-ring/20"
id={id} id={id}
name={name} name={name}
placeholder={placeholder} placeholder={placeholder}
required required
spellCheck={type === "email" || name === "username" ? false : undefined}
type={type} type={type}
/> />
</div> </div>
@ -167,10 +175,11 @@ export function TextField({
export function FormMessage({ children, tone }: { children: ReactNode; tone: "error" | "success" }) { export function FormMessage({ children, tone }: { children: ReactNode; tone: "error" | "success" }) {
return ( return (
<p <p
aria-live="polite"
className={ className={
tone === "error" tone === "error"
? "rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600" ? "rounded-xl bg-destructive/10 px-3 py-2 text-sm text-destructive"
: "rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-700" : "rounded-xl bg-primary/10 px-3 py-2 text-sm text-primary"
} }
> >
{children} {children}

View File

@ -41,7 +41,7 @@ export default function ForgotPasswordPage() {
id="email" id="email"
label="E-Mail Address" label="E-Mail Address"
name="email" name="email"
placeholder="Enter your email..." placeholder="Enter your email"
type="email" type="email"
/> />
@ -49,16 +49,16 @@ export default function ForgotPasswordPage() {
{success && <FormMessage tone="success">{success}</FormMessage>} {success && <FormMessage tone="success">{success}</FormMessage>}
<Button <Button
className="h-11 w-full rounded-lg bg-zinc-950 text-base text-white shadow-inner shadow-white/10 hover:bg-zinc-800" className="h-11 w-full rounded-xl text-base shadow-inner shadow-primary-foreground/10"
disabled={submitting} disabled={submitting}
type="submit" type="submit"
> >
{submitting ? "Please wait..." : "Send reset link"} {submitting ? "Please wait" : "Send reset link"}
</Button> </Button>
</form> </form>
<footer className="mt-6 text-center text-base text-zinc-500"> <footer className="mt-6 text-center text-base text-muted-foreground">
<Link className="font-semibold text-zinc-950" to="/auth/login"> <Link className="font-semibold text-foreground transition-colors hover:text-primary" to="/auth/login">
Back to sign in Back to sign in
</Link> </Link>
</footer> </footer>

View File

@ -5,8 +5,8 @@ import { useAuth } from "@/context/auth-context";
function AuthLogo() { function AuthLogo() {
return ( return (
<div className="mx-auto grid size-12 place-items-center rounded-lg bg-gradient-to-b from-[oklch(0.45_0.24_290)] to-[oklch(0.55_0.24_290)] shadow-[0_14px_30px_rgba(134,59,255,0.24)]"> <div className="mx-auto grid size-12 place-items-center rounded-2xl bg-primary text-primary-foreground shadow-lg shadow-primary/20">
<div className="relative size-8 overflow-hidden rounded-sm bg-white/20"> <div className="relative size-8 overflow-hidden rounded-lg bg-primary-foreground/20">
<span className="absolute left-1.5 top-2 h-1 w-6 rounded-full bg-white" /> <span className="absolute left-1.5 top-2 h-1 w-6 rounded-full bg-white" />
<span className="absolute left-1 top-3.5 h-1 w-7 rounded-full bg-white/90" /> <span className="absolute left-1 top-3.5 h-1 w-7 rounded-full bg-white/90" />
<span className="absolute left-2 top-5 h-1 w-6 rounded-full bg-white/80" /> <span className="absolute left-2 top-5 h-1 w-6 rounded-full bg-white/80" />
@ -17,10 +17,10 @@ function AuthLogo() {
export function AuthPanel({ children }: { children: ReactNode }) { export function AuthPanel({ children }: { children: ReactNode }) {
return ( return (
<section className="relative z-10 w-full max-w-[460px] rounded-lg bg-card px-6 py-8 shadow-[0_24px_64px_rgba(134,59,255,0.08)] sm:px-9"> <section className="relative z-10 w-full max-w-[460px] rounded-2xl border border-border/50 bg-card px-6 py-8 shadow-xl shadow-primary/[0.04] sm:px-9">
<div className="pointer-events-none absolute inset-x-0 top-0 h-32 overflow-hidden rounded-t-lg"> <div className="pointer-events-none absolute inset-x-0 top-0 h-32 overflow-hidden rounded-t-2xl">
<div className="absolute inset-0 bg-[linear-gradient(rgba(134,59,255,0.10)_1px,transparent_1px),linear-gradient(90deg,rgba(134,59,255,0.10)_1px,transparent_1px)] bg-[size:38px_38px] [mask-image:linear-gradient(to_bottom,black,transparent)]" /> <div className="absolute inset-0 bg-[linear-gradient(var(--border)_1px,transparent_1px),linear-gradient(90deg,var(--border)_1px,transparent_1px)] bg-[size:38px_38px] opacity-40 [mask-image:linear-gradient(to_bottom,black,transparent)]" />
<div className="absolute inset-x-10 top-0 h-24 bg-gradient-to-b from-[oklch(0.40_0.12_290)/0.15] to-transparent blur-2xl" /> <div className="absolute inset-x-10 top-0 h-24 bg-primary/10 blur-2xl" />
</div> </div>
{children} {children}
</section> </section>
@ -55,11 +55,11 @@ export default function AuthLayout() {
return ( return (
<main className="min-h-svh bg-background px-4 py-6 text-foreground"> <main className="min-h-svh bg-background px-4 py-6 text-foreground">
<div className="relative mx-auto grid min-h-[calc(100svh-3rem)] w-full max-w-5xl place-items-center overflow-hidden"> <div className="relative mx-auto grid min-h-[calc(100svh-3rem)] w-full max-w-5xl place-items-center overflow-hidden">
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(rgba(134,59,255,0.05)_1px,transparent_1px),linear-gradient(90deg,rgba(134,59,255,0.05)_1px,transparent_1px)] bg-[size:72px_72px] [mask-image:radial-gradient(circle_at_center,black,transparent_70%)]" /> <div className="pointer-events-none absolute inset-0 bg-[linear-gradient(var(--border)_1px,transparent_1px),linear-gradient(90deg,var(--border)_1px,transparent_1px)] bg-[size:72px_72px] opacity-35 [mask-image:radial-gradient(circle_at_center,black,transparent_70%)]" />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(134,59,255,0.15),transparent_50%)]" /> <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_center,var(--primary),transparent_55%)] opacity-10" />
{isLoading ? ( {isLoading ? (
<div className="relative z-10 text-sm font-mono font-medium text-muted-foreground"> <div className="relative z-10 text-sm font-mono font-medium text-muted-foreground">
Loading... Loading
</div> </div>
) : ( ) : (
<Outlet /> <Outlet />

View File

@ -124,24 +124,24 @@ export default function LoginPage() {
id="username" id="username"
label="Username or E-Mail Address" label="Username or E-Mail Address"
name="username" name="username"
placeholder="Enter your username or email..." placeholder="Enter your username or email"
/> />
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-semibold text-zinc-950" htmlFor="password"> <Label className="text-sm font-semibold text-foreground" htmlFor="password">
Password Password
</Label> </Label>
<PasswordInput id="password" name="password" placeholder="Enter your password..." /> <PasswordInput id="password" name="password" placeholder="Enter your password" />
</div> </div>
<CaptchaField onPublicKey={setPublicKey} /> <CaptchaField onPublicKey={setPublicKey} />
<div className="flex items-center justify-between gap-4 text-sm"> <div className="flex items-center justify-between gap-4 text-sm">
<label className="flex items-center gap-3 font-medium text-zinc-950"> <label className="flex items-center gap-3 font-medium text-foreground">
<Checkbox className="size-4 rounded-[5px] border-zinc-200" name="remember" /> <Checkbox className="size-4 rounded-md border-border" name="remember" />
Remember me Remember me
</label> </label>
<Link className="font-medium text-zinc-600 underline underline-offset-4" to="/auth/forgot-password"> <Link className="font-medium text-muted-foreground underline underline-offset-4 transition-colors hover:text-foreground" to="/auth/forgot-password">
Forgot password? Forgot password?
</Link> </Link>
</div> </div>
@ -149,17 +149,17 @@ export default function LoginPage() {
{error && <FormMessage tone="error">{error}</FormMessage>} {error && <FormMessage tone="error">{error}</FormMessage>}
<Button <Button
className="h-11 w-full rounded-lg bg-zinc-950 text-base text-white shadow-inner shadow-white/10 hover:bg-zinc-800" className="h-11 w-full rounded-xl text-base shadow-inner shadow-primary-foreground/10"
disabled={submitting} disabled={submitting}
type="submit" type="submit"
> >
{submitting ? "Please wait..." : "Sign in"} {submitting ? "Please wait" : "Sign in"}
</Button> </Button>
</form> </form>
<footer className="mt-6 text-center text-base text-zinc-500"> <footer className="mt-6 text-center text-base text-muted-foreground">
Don&apos;t have an account yet?{" "} Don&apos;t have an account yet?{" "}
<Link className="font-semibold text-zinc-950" to="/auth/register"> <Link className="font-semibold text-foreground transition-colors hover:text-primary" to="/auth/register">
Sign Up Sign Up
</Link> </Link>
</footer> </footer>
@ -196,11 +196,11 @@ export default function LoginPage() {
<DialogFooter className="-mx-4 -mb-4"> <DialogFooter className="-mx-4 -mb-4">
<Button <Button
className="h-10 bg-zinc-950 text-white hover:bg-zinc-800" className="h-10"
disabled={twoFactorSubmitting} disabled={twoFactorSubmitting}
type="submit" type="submit"
> >
{twoFactorSubmitting ? "Verifying..." : "Verify"} {twoFactorSubmitting ? "Verifying" : "Verify"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View File

@ -58,26 +58,26 @@ export default function RegisterPage() {
<AuthHeader description="Please enter your details to sign up." title="Create account" /> <AuthHeader description="Please enter your details to sign up." title="Create account" />
<form className="relative space-y-4 mt-10" onSubmit={(event) => void handleSubmit(event)}> <form className="relative space-y-4 mt-10" onSubmit={(event) => void handleSubmit(event)}>
<TextField id="username" label="Username" name="username" placeholder="Enter your username..." /> <TextField id="username" label="Username" name="username" placeholder="Enter your username" />
<TextField <TextField
id="email" id="email"
label="E-Mail Address" label="E-Mail Address"
name="email" name="email"
placeholder="Enter your email..." placeholder="Enter your email"
type="email" type="email"
/> />
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-semibold text-zinc-950" htmlFor="password"> <Label className="text-sm font-semibold text-foreground" htmlFor="password">
Password Password
</Label> </Label>
<PasswordInput id="password" name="password" placeholder="Enter your password..." /> <PasswordInput autoComplete="new-password" id="password" name="password" placeholder="Enter your password" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-semibold text-zinc-950" htmlFor="confirmPassword"> <Label className="text-sm font-semibold text-foreground" htmlFor="confirmPassword">
Confirm Password Confirm Password
</Label> </Label>
<PasswordInput id="confirmPassword" name="confirmPassword" placeholder="Enter your password again..." /> <PasswordInput autoComplete="new-password" id="confirmPassword" name="confirmPassword" placeholder="Enter your password again" />
</div> </div>
<CaptchaField onPublicKey={setPublicKey} /> <CaptchaField onPublicKey={setPublicKey} />
@ -85,17 +85,17 @@ export default function RegisterPage() {
{error && <FormMessage tone="error">{error}</FormMessage>} {error && <FormMessage tone="error">{error}</FormMessage>}
<Button <Button
className="h-11 w-full rounded-lg bg-zinc-950 text-base text-white shadow-inner shadow-white/10 hover:bg-zinc-800" className="h-11 w-full rounded-xl text-base shadow-inner shadow-primary-foreground/10"
disabled={submitting} disabled={submitting}
type="submit" type="submit"
> >
{submitting ? "Please wait..." : "Sign up"} {submitting ? "Please wait" : "Sign up"}
</Button> </Button>
</form> </form>
<footer className="mt-6 text-center text-base text-zinc-500"> <footer className="mt-6 text-center text-base text-muted-foreground">
Already have an account?{" "} Already have an account?{" "}
<Link className="font-semibold text-zinc-950" to="/auth/login"> <Link className="font-semibold text-foreground transition-colors hover:text-primary" to="/auth/login">
Sign In Sign In
</Link> </Link>
</footer> </footer>

View File

@ -40,29 +40,29 @@ export default function ResetPasswordPage() {
<form className="relative mt-6 space-y-4" onSubmit={(event) => void handleSubmit(event)}> <form className="relative mt-6 space-y-4" onSubmit={(event) => void handleSubmit(event)}>
{!searchParams.get("token") && ( {!searchParams.get("token") && (
<TextField id="token" label="Reset Token" name="token" placeholder="Enter your reset token..." /> <TextField id="token" label="Reset Token" name="token" placeholder="Enter your reset token" />
)} )}
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-semibold text-zinc-950" htmlFor="password"> <Label className="text-sm font-semibold text-foreground" htmlFor="password">
Password Password
</Label> </Label>
<PasswordInput id="password" name="password" placeholder="Enter your password..." /> <PasswordInput autoComplete="new-password" id="password" name="password" placeholder="Enter your password" />
</div> </div>
{error && <FormMessage tone="error">{error}</FormMessage>} {error && <FormMessage tone="error">{error}</FormMessage>}
<Button <Button
className="h-11 w-full rounded-lg bg-zinc-950 text-base text-white shadow-inner shadow-white/10 hover:bg-zinc-800" className="h-11 w-full rounded-xl text-base shadow-inner shadow-primary-foreground/10"
disabled={submitting} disabled={submitting}
type="submit" type="submit"
> >
{submitting ? "Please wait..." : "Reset password"} {submitting ? "Please wait" : "Reset password"}
</Button> </Button>
</form> </form>
<footer className="mt-6 text-center text-base text-zinc-500"> <footer className="mt-6 text-center text-base text-muted-foreground">
<Link className="font-semibold text-zinc-950" to="/auth/login"> <Link className="font-semibold text-foreground transition-colors hover:text-primary" to="/auth/login">
Back to sign in Back to sign in
</Link> </Link>
</footer> </footer>