style(landing): polish the public index, login and register screens
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Failing after 26s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / tests-ui (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Failing after 26s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / tests-ui (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped
- add theme-aware helpers: btn-brand, btn-glass-dark, auth-input, auth-label, auth-alert, explore-pill, avatar-tile, aurora blobs and a hero scroll cue - reorder backdrop-filter declarations so the glass blur survives the production CSS optimizer in modern Chromium - hero: aurora glow, frosted recent-users chip, premium CTA buttons and cue - explore nav: icon pills with hover arrows; features: gradient icon tiles - stats: single glass panel with column dividers; join CTA: aurora + ring - auth forms: visible labels, icon inputs, eye/password toggle, gradient submit and pill footer links - auth pages: gradient card frame, aurora accents on the intro panel and hover-lift avatar tiles; unified pill-shaped top bar - drop the hard-coded register banner image in favor of the framed card
This commit is contained in:
1 parent
5b2eb91c5c
commit
57710a7fe3
8 files changed
+879
-521
No files matched your search
+112
-122
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { KeyRound, Lock, UserRound } from "lucide-react";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { type FormEvent, useId, useState } from "react";
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
CaptchaWidget,
|
||||
readCaptchaToken,
|
||||
} from "@/components/auth/captcha-widget";
|
||||
import { PasswordToggle } from "@/components/auth/password-toggle";
|
||||
import Link from "@/components/link";
|
||||
import { signInWithTransientRetry } from "@/lib/auth/sign-in-retry";
|
||||
|
||||
@@ -49,7 +51,6 @@ export function LoginForm({
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
const showFooter = variant === "page";
|
||||
const labelled = variant === "compact";
|
||||
const lockCredentials = needs2fa ? "opacity-50 pointer-events-none" : "";
|
||||
|
||||
async function onSubmit(e: FormEvent<HTMLFormElement>) {
|
||||
@@ -103,8 +104,14 @@ export function LoginForm({
|
||||
<>
|
||||
{needs2fa && (
|
||||
<p
|
||||
className="animate-fade-in-up mb-4 text-sm font-semibold"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
className="animate-fade-in-up mb-4 rounded-xl border px-3 py-2.5 text-sm font-semibold"
|
||||
style={{
|
||||
color: "var(--color-text-readable)",
|
||||
background:
|
||||
"color-mix(in srgb, var(--color-primary) 10%, transparent)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 26%, transparent)",
|
||||
}}
|
||||
>
|
||||
{t("subtitle2fa")}
|
||||
</p>
|
||||
@@ -115,113 +122,89 @@ export function LoginForm({
|
||||
aria-label={t("signIn")}
|
||||
className="relative flex flex-col gap-4"
|
||||
>
|
||||
<div className={labelled ? "space-y-1" : lockCredentials}>
|
||||
<label
|
||||
htmlFor={`${fieldId}-username`}
|
||||
className={
|
||||
labelled
|
||||
? "block text-xs font-bold uppercase tracking-wider"
|
||||
: "sr-only"
|
||||
}
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
<div className={`space-y-1.5 ${lockCredentials}`}>
|
||||
<label htmlFor={`${fieldId}-username`} className="auth-label">
|
||||
{t("username")}
|
||||
</label>
|
||||
<input
|
||||
id={`${fieldId}-username`}
|
||||
name="username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder={t("usernamePlaceholder")}
|
||||
autoComplete="username"
|
||||
disabled={needs2fa}
|
||||
required
|
||||
className="input-glow w-full rounded-xl border-2 px-4 py-3 text-sm font-medium transition-all focus:outline-none disabled:opacity-50"
|
||||
style={{
|
||||
backgroundColor: "var(--color-background)",
|
||||
color: "var(--color-text-readable)",
|
||||
borderColor: needs2fa
|
||||
? "color-mix(in srgb, var(--color-text-muted) 15%, transparent)"
|
||||
: "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
|
||||
}}
|
||||
/>
|
||||
<div className="relative">
|
||||
<UserRound
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
/>
|
||||
<input
|
||||
id={`${fieldId}-username`}
|
||||
name="username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder={t("usernamePlaceholder")}
|
||||
autoComplete="username"
|
||||
disabled={needs2fa}
|
||||
required
|
||||
className="auth-input has-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${labelled ? "space-y-1" : lockCredentials} relative`}>
|
||||
<label
|
||||
htmlFor={`${fieldId}-password`}
|
||||
className={
|
||||
labelled
|
||||
? "block text-xs font-bold uppercase tracking-wider"
|
||||
: "sr-only"
|
||||
}
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
<div className={`space-y-1.5 ${lockCredentials}`}>
|
||||
<label htmlFor={`${fieldId}-password`} className="auth-label">
|
||||
{t("password")}
|
||||
</label>
|
||||
<input
|
||||
id={`${fieldId}-password`}
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t("passwordPlaceholder")}
|
||||
autoComplete="current-password"
|
||||
disabled={needs2fa}
|
||||
required
|
||||
className="input-glow w-full rounded-xl border-2 px-4 py-3 text-sm font-medium transition-all focus:outline-none disabled:opacity-50"
|
||||
style={{
|
||||
backgroundColor: "var(--color-background)",
|
||||
color: "var(--color-text-readable)",
|
||||
borderColor: needs2fa
|
||||
? "color-mix(in srgb, var(--color-text-muted) 15%, transparent)"
|
||||
: "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-sm font-bold opacity-60 transition-opacity hover:opacity-100"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
tabIndex={-1}
|
||||
aria-label={showPassword ? t("hidePassword") : t("showPassword")}
|
||||
aria-pressed={showPassword}
|
||||
>
|
||||
{showPassword ? t("hidePassword") : t("showPassword")}
|
||||
</button>
|
||||
<div className="relative">
|
||||
<Lock
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
/>
|
||||
<input
|
||||
id={`${fieldId}-password`}
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t("passwordPlaceholder")}
|
||||
autoComplete="current-password"
|
||||
disabled={needs2fa}
|
||||
required
|
||||
className="auth-input has-icon has-action"
|
||||
/>
|
||||
<PasswordToggle
|
||||
show={showPassword}
|
||||
onToggle={() => setShowPassword((v) => !v)}
|
||||
labelShow={t("showPassword")}
|
||||
labelHide={t("hidePassword")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{needs2fa ? (
|
||||
<div className={`animate-scale-in ${labelled ? "space-y-1" : ""}`}>
|
||||
<label
|
||||
htmlFor={`${fieldId}-2fa`}
|
||||
className={
|
||||
labelled
|
||||
? "block text-xs font-bold uppercase tracking-wider"
|
||||
: "sr-only"
|
||||
}
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
<div className="animate-scale-in space-y-1.5">
|
||||
<label htmlFor={`${fieldId}-2fa`} className="auth-label">
|
||||
{t("codePlaceholder")}
|
||||
</label>
|
||||
<input
|
||||
id={`${fieldId}-2fa`}
|
||||
name="code"
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder={t("codePlaceholder")}
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
className="input-glow w-full rounded-xl border-2 px-4 py-3 text-sm font-medium transition-all focus:outline-none"
|
||||
style={{
|
||||
backgroundColor: "var(--color-background)",
|
||||
color: "var(--color-text-readable)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 30%, transparent)",
|
||||
}}
|
||||
/>
|
||||
<div className="relative">
|
||||
<KeyRound
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2"
|
||||
style={{ color: "var(--color-primary)" }}
|
||||
/>
|
||||
<input
|
||||
id={`${fieldId}-2fa`}
|
||||
name="code"
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder={t("codePlaceholder")}
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
className="auth-input has-icon"
|
||||
style={{
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 45%, transparent)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<CaptchaWidget captcha={captcha} nonce={nonce} />
|
||||
@@ -231,8 +214,7 @@ export function LoginForm({
|
||||
<p
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
className="m-0 animate-fade-in-up text-center text-sm font-semibold"
|
||||
style={{ color: "var(--color-danger)" }}
|
||||
className="auth-alert animate-fade-in-up m-0"
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
@@ -241,13 +223,7 @@ export function LoginForm({
|
||||
<button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
className="btn-shine w-full cursor-pointer rounded-xl py-3.5 text-sm font-extrabold shadow-lg transition-all duration-200 hover:scale-[1.02] active:scale-95 disabled:opacity-60"
|
||||
style={{
|
||||
background: "var(--color-primary)",
|
||||
color: "var(--color-primary-foreground)",
|
||||
boxShadow:
|
||||
"0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)",
|
||||
}}
|
||||
className="btn-brand btn-shine w-full cursor-pointer py-3.5 text-sm font-extrabold uppercase tracking-wider disabled:opacity-60"
|
||||
>
|
||||
{pending ? (
|
||||
<span className="inline-flex items-center justify-center gap-2">
|
||||
@@ -283,27 +259,41 @@ export function LoginForm({
|
||||
</form>
|
||||
|
||||
{showFooter && (
|
||||
<p
|
||||
className="mt-6 text-center text-xs"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
<div
|
||||
className="mt-6 space-y-3 border-t pt-5"
|
||||
style={{
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-text-muted) 14%, transparent)",
|
||||
}}
|
||||
>
|
||||
{t("noAccount")}{" "}
|
||||
<Link
|
||||
href="/register"
|
||||
className="font-extrabold hover:underline"
|
||||
style={{ color: "var(--color-primary)" }}
|
||||
<p
|
||||
className="text-center text-xs"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{t("createOne")}
|
||||
</Link>{" "}
|
||||
·{" "}
|
||||
{t("noAccount")}{" "}
|
||||
<Link
|
||||
href="/register"
|
||||
className="font-extrabold transition-colors hover:underline"
|
||||
style={{ color: "var(--color-primary)" }}
|
||||
>
|
||||
{t("createOne")}
|
||||
</Link>
|
||||
</p>
|
||||
<Link
|
||||
href="/forgot"
|
||||
className="font-extrabold hover:underline"
|
||||
style={{ color: "var(--color-primary)" }}
|
||||
className="flex items-center justify-center gap-2 rounded-full border px-4 py-2.5 text-xs font-extrabold transition-all duration-200 hover:-translate-y-0.5"
|
||||
style={{
|
||||
color: "var(--color-primary)",
|
||||
background:
|
||||
"color-mix(in srgb, var(--color-primary) 8%, transparent)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 35%, transparent)",
|
||||
}}
|
||||
>
|
||||
<KeyRound aria-hidden="true" className="h-3.5 w-3.5" />
|
||||
{t("forgotPassword")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user