fix(auth): update auth page components
This commit is contained in:
parent
71f90bcd4d
commit
82475e95d5
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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'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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user