Complete UI professional makeover: premium design, animations, consistent shadcn components, polished admin dashboard, modern login/auth pages, enhanced navigation
Local Build and Deploy / deploy (push) Successful in 46s
Local Build and Deploy / deploy (push) Successful in 46s
This commit is contained in:
1 parent
ac1b412756
commit
3c4aa50fea
12 files changed
+773
-780
No files matched your search
+151
-103
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { KeyRound, Lock, Mail } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { type FormEvent, useState } from "react";
|
||||
import { precheckLogin } from "@/actions/auth-precheck";
|
||||
import { ContentCard } from "@/components/public/ui";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export default function LoginPage() {
|
||||
const t = useTranslations("pages.login");
|
||||
@@ -51,108 +53,154 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<main style={{ maxWidth: 420, margin: "2rem auto" }}>
|
||||
<ContentCard
|
||||
icon={needs2fa ? "🔒" : "🔑"}
|
||||
title={t("title")}
|
||||
subtitle={needs2fa ? t("subtitle2fa") : t("subtitle")}
|
||||
>
|
||||
<form onSubmit={onSubmit} style={{ display: "grid", gap: "0.7rem" }}>
|
||||
<input
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder={t("usernamePlaceholder")}
|
||||
autoComplete="username"
|
||||
disabled={needs2fa}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t("passwordPlaceholder")}
|
||||
autoComplete="current-password"
|
||||
disabled={needs2fa}
|
||||
/>
|
||||
{needs2fa ? (
|
||||
<input
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder={t("codePlaceholder")}
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
// biome-ignore lint/a11y/noAutofocus: focus the only relevant field in the 2FA step
|
||||
autoFocus
|
||||
/>
|
||||
) : null}
|
||||
<button type="submit" className="btn btn-primary" disabled={pending}>
|
||||
{pending ? t("pleaseWait") : needs2fa ? t("verify") : t("signIn")}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{!needs2fa ? (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
margin: "1rem 0",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 1,
|
||||
background: "var(--border-color)",
|
||||
}}
|
||||
/>
|
||||
<span className="muted">{t("or")}</span>
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 1,
|
||||
background: "var(--border-color)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "grid", gap: "0.5rem" }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline"
|
||||
onClick={() => signIn("discord", { callbackUrl: "/" })}
|
||||
>
|
||||
{t("continueWithDiscord")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline"
|
||||
onClick={() => signIn("google", { callbackUrl: "/" })}
|
||||
>
|
||||
{t("continueWithGoogle")}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<p
|
||||
style={{
|
||||
color: "var(--color-danger)",
|
||||
textAlign: "center",
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
<div className="flex min-h-[70vh] items-center justify-center px-4 py-12">
|
||||
<div className="w-full max-w-md animate-fade-in">
|
||||
<div className="text-center mb-8">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-[var(--color-primary)]/20 to-[var(--color-primary)]/5 shadow-lg">
|
||||
<Lock className="h-8 w-8 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-extrabold tracking-tight m-0">
|
||||
{t("title")}
|
||||
</h1>
|
||||
<p className="mt-1.5 text-sm text-[var(--color-text-muted-readable)]">
|
||||
{needs2fa ? t("subtitle2fa") : t("subtitle")}
|
||||
</p>
|
||||
) : null}
|
||||
<p
|
||||
className="muted"
|
||||
style={{ textAlign: "center", marginBottom: 0, marginTop: "1rem" }}
|
||||
>
|
||||
{t("noAccount")} <Link href="/register">{t("createOne")}</Link> ·{" "}
|
||||
<Link href="/forgot">{t("forgotPassword")}</Link>
|
||||
</p>
|
||||
</ContentCard>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<Card className="border-[var(--border-subtle)] shadow-elevated">
|
||||
<CardHeader className="px-6 pt-6 pb-0">
|
||||
<CardTitle className="text-base font-bold">
|
||||
{needs2fa ? t("subtitle2fa") : t("subtitle")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{needs2fa ? "Enter your authentication code" : "Sign in to your account"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--color-text-muted)]" />
|
||||
<input
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder={t("usernamePlaceholder")}
|
||||
autoComplete="username"
|
||||
disabled={needs2fa}
|
||||
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] px-10 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<KeyRound className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--color-text-muted)]" />
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t("passwordPlaceholder")}
|
||||
autoComplete="current-password"
|
||||
disabled={needs2fa}
|
||||
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] px-10 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
|
||||
/>
|
||||
</div>
|
||||
{needs2fa ? (
|
||||
<input
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder={t("codePlaceholder")}
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
autoFocus
|
||||
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] px-4 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 px-4 py-3">
|
||||
<p className="text-sm font-medium text-red-600 dark:text-red-400 text-center m-0">
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
size="lg"
|
||||
className="w-full font-bold text-base h-12 rounded-xl"
|
||||
>
|
||||
{pending ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
{t("pleaseWait")}
|
||||
</span>
|
||||
) : needs2fa ? t("verify") : t("signIn")}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{!needs2fa ? (
|
||||
<>
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-[var(--border-subtle)]" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-[var(--color-card)] px-3 text-[var(--color-text-muted-readable)] font-bold">
|
||||
{t("or")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="w-full font-semibold h-11 rounded-xl"
|
||||
onClick={() => signIn("discord", { callbackUrl: "/" })}
|
||||
>
|
||||
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189z" />
|
||||
</svg>
|
||||
{t("continueWithDiscord")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="w-full font-semibold h-11 rounded-xl"
|
||||
onClick={() => signIn("google", { callbackUrl: "/" })}
|
||||
>
|
||||
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4" />
|
||||
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
|
||||
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
|
||||
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
|
||||
</svg>
|
||||
{t("continueWithGoogle")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-[var(--color-text-muted-readable)]">
|
||||
{t("noAccount")}{" "}
|
||||
<Link href="/register" className="font-bold text-[var(--color-primary)] hover:underline">
|
||||
{t("createOne")}
|
||||
</Link>
|
||||
<span className="mx-2 text-[var(--border-subtle)]">·</span>
|
||||
<Link href="/forgot" className="font-bold text-[var(--color-primary)] hover:underline">
|
||||
{t("forgotPassword")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user