feat(auth): unify and polish login, register and homepage auth UI

Login and register each carried their own copy of the promo panel and the
error banner, so the two pages drifted apart and every caller had to wire
role="alert" by hand. Extract a shared AuthPromoCard and AuthAlert, use them
in both forms and for the login notices, and link errors to their fields via
aria-invalid / aria-describedby.

Focus now lands on the 2FA code field the moment it appears, the credential
fields lock while a request is in flight, and invalid inputs get a danger
tint. The homepage top bar was a byte-for-byte duplicate of AuthTopBar; it
now renders the shared component, and the compact homepage login uses the
tighter spacing its comment already promised.
This commit is contained in:
openhands
2026-10-18 18:49:18 +02:00
parent 2249303dbd
commit c6e5e0b1a9
8 changed files with 215 additions and 296 deletions
+4 -85
View File
@@ -7,16 +7,14 @@ import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import type { CSSProperties, ReactNode } from "react";
import { AnimatedCounter } from "@/components/animated-counter";
import { AuthTopBar } from "@/components/auth/auth-top-bar";
import { LoginForm } from "@/components/auth/login-form";
import { Clock } from "@/components/clock";
import { LanguageSwitcher } from "@/components/language-switcher";
import Link from "@/components/link";
import { LiveOnlineCounter } from "@/components/live-online-counter";
import { Reveal } from "@/components/motion-reveal";
import { PublicLoadError } from "@/components/public/load-error";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { SpotlightCard } from "@/components/spotlight-card";
import { ThemeSwitcher } from "@/components/theme-switcher";
import { TypewriterText } from "@/components/typewriter-text";
import { auth } from "@/lib/auth";
import { cached } from "@/lib/cache";
@@ -41,7 +39,6 @@ import {
ICON_CAMERA,
ICON_CATALOG,
ICON_FRIENDS,
ICON_LIGHTHOUSE,
ICON_NAV_GOODY,
ICON_NAV_ME,
} from "@/lib/site-icons";
@@ -406,87 +403,9 @@ async function Home() {
return (
<div className="flex w-full flex-col gap-14 pb-16 pt-2 md:gap-20 md:pb-24">
{/* ── Top bar ── */}
<Reveal className="mx-auto w-full max-w-7xl">
<div
className="flex items-center justify-between rounded-full border px-3 py-2.5 sm:px-5"
style={{
background:
"linear-gradient(90deg, color-mix(in srgb, var(--color-surface) 62%, transparent), color-mix(in srgb, var(--color-primary) 7%, transparent))",
backdropFilter: "blur(14px)",
WebkitBackdropFilter: "blur(14px)",
borderColor:
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
boxShadow:
"0 1px 3px rgba(0,0,0,0.04), 0 0 0 0.5px color-mix(in srgb, var(--color-text-muted) 6%, transparent)",
}}
>
<div className="flex items-center gap-2 sm:gap-3">
{logo ? (
<Image
src={logo}
alt={hotelName}
width={100}
height={28}
className="shrink-0 object-contain"
unoptimized
style={{ maxHeight: 26, width: "auto", height: "auto" }}
/>
) : (
<div
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full"
style={{
background:
"linear-gradient(135deg, var(--color-primary), color-mix(in srgb, var(--color-accent) 60%, var(--color-primary)))",
boxShadow:
"0 4px 16px -4px color-mix(in srgb, var(--color-primary) 60%, transparent)",
}}
>
<Image
src={ICON_LIGHTHOUSE}
alt=""
width={18}
height={18}
className="shrink-0"
unoptimized
style={{ filter: "brightness(0) invert(1)" }}
/>
</div>
)}
<span
className="hidden text-sm font-extrabold uppercase tracking-wide sm:inline"
style={{ color: "var(--color-text-readable)" }}
>
{hotelName}
</span>
</div>
<div
className="flex items-center gap-1 sm:gap-1.5 rounded-full px-1.5 py-1"
style={{
background:
"color-mix(in srgb, var(--color-text-muted) 5%, transparent)",
}}
>
<LanguageSwitcher />
<div
className="hidden w-px h-5 sm:block"
style={{
background:
"color-mix(in srgb, var(--color-text-muted) 12%, transparent)",
}}
/>
<Clock />
<div
className="w-px h-5"
style={{
background:
"color-mix(in srgb, var(--color-text-muted) 12%, transparent)",
}}
/>
<ThemeSwitcher />
</div>
</div>
</Reveal>
<div className="mx-auto w-full max-w-7xl">
<AuthTopBar hotelName={hotelName} logo={logo} />
</div>
{/* ── Cinematic hero ── */}
<Reveal className="-mx-3 sm:-mx-4 md:-mx-6">
<div className="relative overflow-hidden">