diff --git a/src/app/(site)/login/page.tsx b/src/app/(site)/login/page.tsx
index 95b61337..101f1952 100644
--- a/src/app/(site)/login/page.tsx
+++ b/src/app/(site)/login/page.tsx
@@ -1,15 +1,15 @@
import { desc, eq } from "drizzle-orm";
import type { Metadata } from "next";
import { headers } from "next/headers";
-import Image from "next/image";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
+import { AuthAlert } from "@/components/auth/auth-alert";
import {
AuthPageFrame,
AuthUsersCards,
} from "@/components/auth/auth-page-frame";
+import { AuthPromoCard } from "@/components/auth/auth-promo-card";
import { LoginForm } from "@/components/auth/login-form";
-import { SurfaceCard } from "@/components/surface-card";
import { auth } from "@/lib/auth";
import { safeRedirectPath } from "@/lib/auth/safe-redirect";
import { cached } from "@/lib/cache";
@@ -90,22 +90,16 @@ export default async function LoginPage({
// exists.
const notice =
sp.registered === "1" ? (
-
+
{t("registeredSuccess")}
-
+
) : sp.reset === "1" ? (
// `?reset=1` comes from a successful password reset; saying so matters
// because the visitor just changed their password and a silent form
// reads like the reset failed.
-
+
{t("passwordChanged")}
-
+
) : undefined;
return (
@@ -127,89 +121,11 @@ export default async function LoginPage({
/>
}
>
-
-
-
-
-
-
-
-
-
-
-
- {t("usersOnline", { count: online })}
-
-
- {t("welcomeBack")}
-
-
- {t("welcomeBackSub", { hotelName })}
-
-
-
+
{/* ── Top bar ── */}
-
-
-
- {logo ? (
-
- ) : (
-
-
-
- )}
-
- {hotelName}
-
-
-
-
-
-
+
{/* ── Cinematic hero ── */}
diff --git a/src/app/(site)/register/page.tsx b/src/app/(site)/register/page.tsx
index f23b753f..7da8a818 100644
--- a/src/app/(site)/register/page.tsx
+++ b/src/app/(site)/register/page.tsx
@@ -1,15 +1,14 @@
import { desc, eq } from "drizzle-orm";
import type { Metadata } from "next";
import { headers } from "next/headers";
-import Image from "next/image";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import {
AuthPageFrame,
AuthUsersCards,
} from "@/components/auth/auth-page-frame";
+import { AuthPromoCard } from "@/components/auth/auth-promo-card";
import { RegisterForm } from "@/components/auth/register-form";
-import { SurfaceCard } from "@/components/surface-card";
import { auth } from "@/lib/auth";
import { cached } from "@/lib/cache";
@@ -86,96 +85,15 @@ export default async function RegisterPage() {
/>
}
>
-
-
-
-
-
-
-
-
-
-
-
-
- {tpr("usersOnline", { count: online })}
-
-
- {tpr("title")} —{" "}
- {hotelName}
-
-
- {tpr("subtitle")}
-
-
-
-
-
+
+ {tpr("title")} — {hotelName}
+ >
+ }
+ subtitle={tpr("subtitle")}
+ />
+ {children}
+
+ );
+}
diff --git a/src/components/auth/auth-promo-card.tsx b/src/components/auth/auth-promo-card.tsx
new file mode 100644
index 00000000..234b3faf
--- /dev/null
+++ b/src/components/auth/auth-promo-card.tsx
@@ -0,0 +1,107 @@
+import Image from "next/image";
+import type { ReactNode } from "react";
+import { SurfaceCard } from "@/components/surface-card";
+
+interface AuthPromoCardProps {
+ /** Fully formatted "N users online" label. */
+ onlineLabel: string;
+ title: ReactNode;
+ subtitle: ReactNode;
+}
+
+/**
+ * The single promo panel both `/login` and `/register` show above the form:
+ * Frank, the online badge and a short welcome. Keeping it in one place means
+ * the two auth pages can never drift apart visually again.
+ */
+export function AuthPromoCard({
+ onlineLabel,
+ title,
+ subtitle,
+}: AuthPromoCardProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {onlineLabel}
+
+
+ {title}
+
+
+ {subtitle}
+
+
+
+ );
+}
diff --git a/src/components/auth/login-form.tsx b/src/components/auth/login-form.tsx
index ec0cefe7..a0c50d6a 100644
--- a/src/components/auth/login-form.tsx
+++ b/src/components/auth/login-form.tsx
@@ -3,8 +3,9 @@
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";
+import { type FormEvent, useEffect, useId, useRef, useState } from "react";
import { precheckLogin } from "@/actions/auth-precheck";
+import { AuthAlert } from "@/components/auth/auth-alert";
import {
type CaptchaPublicConfig,
CaptchaWidget,
@@ -51,8 +52,17 @@ export function LoginForm({
const [unverified, setUnverified] = useState(false);
const [pending, setPending] = useState(false);
const [locked, setLocked] = useState(false);
+ const codeRef = useRef(null);
+
+ // The 2FA field only exists after the pre-check asks for a code; move focus
+ // there automatically so the visitor can keep typing without reaching for
+ // the mouse (the lint rule rules out a bare `autoFocus`).
+ useEffect(() => {
+ if (needs2fa) codeRef.current?.focus();
+ }, [needs2fa]);
const showFooter = variant === "page";
+ const compact = variant === "compact";
const lockCredentials =
needs2fa || locked ? "opacity-50 pointer-events-none" : "";
@@ -115,7 +125,9 @@ export function LoginForm({
<>
{needs2fa && (
@@ -151,8 +163,10 @@ export function LoginForm({
onChange={(e) => setUsername(e.target.value)}
placeholder={t("usernamePlaceholder")}
autoComplete="username"
- disabled={needs2fa}
+ disabled={needs2fa || pending}
required
+ aria-invalid={error ? true : undefined}
+ aria-describedby={error ? `${fieldId}-error` : undefined}
className="auth-input has-icon"
/>
@@ -176,8 +190,10 @@ export function LoginForm({
onChange={(e) => setPassword(e.target.value)}
placeholder={t("passwordPlaceholder")}
autoComplete="current-password"
- disabled={needs2fa}
+ disabled={needs2fa || pending}
required
+ aria-invalid={error ? true : undefined}
+ aria-describedby={error ? `${fieldId}-error` : undefined}
className="auth-input has-icon has-action"
/>
)}
- {error && (
-
- {error}
-
- )}
+ {error && {error} }
{unverified && (
{pending ? (
diff --git a/src/components/auth/register-form.tsx b/src/components/auth/register-form.tsx
index 82fe35c7..d6f3aaab 100644
--- a/src/components/auth/register-form.tsx
+++ b/src/components/auth/register-form.tsx
@@ -18,6 +18,7 @@ import {
type RegisterState,
register,
} from "@/actions/register";
+import { AuthAlert } from "@/components/auth/auth-alert";
import { PasswordToggle } from "@/components/auth/password-toggle";
import Link from "@/components/link";
import { signInWithTransientRetry } from "@/lib/auth/sign-in-retry";
@@ -183,15 +184,7 @@ export function RegisterForm({
}}
className="flex flex-col gap-6"
>
- {errorMessage && (
-
- {errorMessage}
-
- )}
+ {errorMessage && {errorMessage} }
{referralCode && (