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 { client, type CaptchaResponse } from "@/client";
@ -8,10 +8,10 @@ import { Label } from "@/components/ui/label";
export function Divider() {
return (
<div className="my-5 flex items-center gap-4 text-xs font-medium uppercase text-zinc-400">
<div className="h-px flex-1 bg-zinc-200" />
<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-border" />
<span>or</span>
<div className="h-px flex-1 bg-zinc-200" />
<div className="h-px flex-1 bg-border" />
</div>
);
}
@ -19,15 +19,15 @@ export function Divider() {
export function SocialButtons() {
return (
<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" />
<span className="sr-only">Continue with Apple</span>
</Button>
<Button className="h-11 rounded-lg border-zinc-200 bg-white text-zinc-950 hover:bg-zinc-50" type="button" variant="outline">
<Globe className="size-5 text-red-500" aria-hidden="true" />
<Button className="h-11 rounded-xl border-border bg-background text-foreground hover:bg-muted" type="button" variant="outline">
<Globe className="size-5 text-destructive" aria-hidden="true" />
<span className="sr-only">Continue with Google</span>
</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" />
<span className="sr-only">Continue with Twitter</span>
</Button>
@ -39,17 +39,20 @@ export function PasswordInput({
id,
name,
placeholder,
autoComplete = "current-password",
}: {
id: string;
name: string;
placeholder: string;
autoComplete?: string;
}) {
const [visible, setVisible] = useState(false);
return (
<div className="relative">
<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}
name={name}
placeholder={placeholder}
@ -58,7 +61,7 @@ export function PasswordInput({
/>
<Button
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)}
type="button"
variant="ghost"
@ -77,7 +80,7 @@ export function CaptchaField({
const [captcha, setCaptcha] = useState<CaptchaResponse | null>(null);
const [loading, setLoading] = useState(false);
const loadCaptcha = async () => {
const loadCaptcha = useCallback(async () => {
setLoading(true);
try {
const response = await client.authCaptcha({
@ -93,11 +96,12 @@ export function CaptchaField({
} finally {
setLoading(false);
}
};
}, [onPublicKey]);
useEffect(() => {
void loadCaptcha();
}, []);
const timer = window.setTimeout(() => void loadCaptcha(), 0);
return () => window.clearTimeout(timer);
}, [loadCaptcha]);
const src = captcha?.base64?.startsWith("data:")
? captcha.base64
@ -105,26 +109,28 @@ export function CaptchaField({
return (
<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
</Label>
<div className="flex gap-3">
<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"
inputMode="text"
name="captcha"
placeholder="Enter captcha..."
placeholder="Enter captcha"
required
/>
<button
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}
onClick={() => void loadCaptcha()}
type="button"
>
{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" />
)}
@ -149,15 +155,17 @@ export function TextField({
}) {
return (
<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>
<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}
name={name}
placeholder={placeholder}
required
spellCheck={type === "email" || name === "username" ? false : undefined}
type={type}
/>
</div>
@ -167,10 +175,11 @@ export function TextField({
export function FormMessage({ children, tone }: { children: ReactNode; tone: "error" | "success" }) {
return (
<p
aria-live="polite"
className={
tone === "error"
? "rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600"
: "rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-700"
? "rounded-xl bg-destructive/10 px-3 py-2 text-sm text-destructive"
: "rounded-xl bg-primary/10 px-3 py-2 text-sm text-primary"
}
>
{children}

View File

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

View File

@ -5,8 +5,8 @@ import { useAuth } from "@/context/auth-context";
function AuthLogo() {
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="relative size-8 overflow-hidden rounded-sm bg-white/20">
<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-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 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" />
@ -17,10 +17,10 @@ function AuthLogo() {
export function AuthPanel({ children }: { children: ReactNode }) {
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">
<div className="pointer-events-none absolute inset-x-0 top-0 h-32 overflow-hidden rounded-t-lg">
<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-x-10 top-0 h-24 bg-gradient-to-b from-[oklch(0.40_0.12_290)/0.15] to-transparent blur-2xl" />
<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-2xl">
<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-primary/10 blur-2xl" />
</div>
{children}
</section>
@ -55,11 +55,11 @@ export default function AuthLayout() {
return (
<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="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-[radial-gradient(circle_at_center,rgba(134,59,255,0.15),transparent_50%)]" />
<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,var(--primary),transparent_55%)] opacity-10" />
{isLoading ? (
<div className="relative z-10 text-sm font-mono font-medium text-muted-foreground">
Loading...
Loading
</div>
) : (
<Outlet />

View File

@ -124,24 +124,24 @@ export default function LoginPage() {
id="username"
label="Username or E-Mail Address"
name="username"
placeholder="Enter your username or email..."
placeholder="Enter your username or email"
/>
<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
</Label>
<PasswordInput id="password" name="password" placeholder="Enter your password..." />
<PasswordInput id="password" name="password" placeholder="Enter your password" />
</div>
<CaptchaField onPublicKey={setPublicKey} />
<div className="flex items-center justify-between gap-4 text-sm">
<label className="flex items-center gap-3 font-medium text-zinc-950">
<Checkbox className="size-4 rounded-[5px] border-zinc-200" name="remember" />
<label className="flex items-center gap-3 font-medium text-foreground">
<Checkbox className="size-4 rounded-md border-border" name="remember" />
Remember me
</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?
</Link>
</div>
@ -149,17 +149,17 @@ export default function LoginPage() {
{error && <FormMessage tone="error">{error}</FormMessage>}
<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}
type="submit"
>
{submitting ? "Please wait..." : "Sign in"}
{submitting ? "Please wait" : "Sign in"}
</Button>
</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?{" "}
<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
</Link>
</footer>
@ -196,11 +196,11 @@ export default function LoginPage() {
<DialogFooter className="-mx-4 -mb-4">
<Button
className="h-10 bg-zinc-950 text-white hover:bg-zinc-800"
className="h-10"
disabled={twoFactorSubmitting}
type="submit"
>
{twoFactorSubmitting ? "Verifying..." : "Verify"}
{twoFactorSubmitting ? "Verifying" : "Verify"}
</Button>
</DialogFooter>
</form>

View File

@ -58,26 +58,26 @@ export default function RegisterPage() {
<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)}>
<TextField id="username" label="Username" name="username" placeholder="Enter your username..." />
<TextField id="username" label="Username" name="username" placeholder="Enter your username" />
<TextField
id="email"
label="E-Mail Address"
name="email"
placeholder="Enter your email..."
placeholder="Enter your email"
type="email"
/>
<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
</Label>
<PasswordInput id="password" name="password" placeholder="Enter your password..." />
<PasswordInput autoComplete="new-password" id="password" name="password" placeholder="Enter your password" />
</div>
<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
</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>
<CaptchaField onPublicKey={setPublicKey} />
@ -85,17 +85,17 @@ export default function RegisterPage() {
{error && <FormMessage tone="error">{error}</FormMessage>}
<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}
type="submit"
>
{submitting ? "Please wait..." : "Sign up"}
{submitting ? "Please wait" : "Sign up"}
</Button>
</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?{" "}
<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
</Link>
</footer>

View File

@ -40,29 +40,29 @@ export default function ResetPasswordPage() {
<form className="relative mt-6 space-y-4" onSubmit={(event) => void handleSubmit(event)}>
{!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">
<Label className="text-sm font-semibold text-zinc-950" htmlFor="password">
<Label className="text-sm font-semibold text-foreground" htmlFor="password">
Password
</Label>
<PasswordInput id="password" name="password" placeholder="Enter your password..." />
<PasswordInput autoComplete="new-password" id="password" name="password" placeholder="Enter your password" />
</div>
{error && <FormMessage tone="error">{error}</FormMessage>}
<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}
type="submit"
>
{submitting ? "Please wait..." : "Reset password"}
{submitting ? "Please wait" : "Reset password"}
</Button>
</form>
<footer className="mt-6 text-center text-base text-zinc-500">
<Link className="font-semibold text-zinc-950" to="/auth/login">
<footer className="mt-6 text-center text-base text-muted-foreground">
<Link className="font-semibold text-foreground transition-colors hover:text-primary" to="/auth/login">
Back to sign in
</Link>
</footer>