refactor(auth): share the auth landing and extract AuthField
CI / check (push) Successful in 36s
CI / tests-imaging-browsers (push) Successful in 1m36s
CI / tests-integration (push) Successful in 1m52s
CI / tests-unit (push) Successful in 1m57s
CI / tests-quality (push) Successful in 52s
CI / tests-ui (push) Successful in 4m46s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 3m40s
CI / check (push) Successful in 36s
CI / tests-imaging-browsers (push) Successful in 1m36s
CI / tests-integration (push) Successful in 1m52s
CI / tests-unit (push) Successful in 1m57s
CI / tests-quality (push) Successful in 52s
CI / tests-ui (push) Successful in 4m46s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 3m40s
The login and register pages were near-identical after the previous cleanup, and both forms repeated the same labelled, icon-prefixed input seven times. Move the data loading and page shell into auth-landing.tsx (loadAuthLanding + AuthLandingView) and the input markup into a single AuthField, so the pages become thin compositions and every field shares one focus, icon and aria implementation. The numbered section headers collapse into AuthSectionHeader. No behaviour change.
This commit is contained in:
1 parent
c6e5e0b1a9
commit
aef0939ee9
6 files changed
+436
-393
No files matched your search
@@ -4,6 +4,7 @@ import {
|
||||
Check,
|
||||
KeyRound,
|
||||
Lock,
|
||||
type LucideIcon,
|
||||
Mail,
|
||||
ShieldCheck,
|
||||
UserRound,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
register,
|
||||
} from "@/actions/register";
|
||||
import { AuthAlert } from "@/components/auth/auth-alert";
|
||||
import { AuthField } from "@/components/auth/auth-field";
|
||||
import { PasswordToggle } from "@/components/auth/password-toggle";
|
||||
import Link from "@/components/link";
|
||||
import { signInWithTransientRetry } from "@/lib/auth/sign-in-retry";
|
||||
@@ -51,6 +53,51 @@ function passwordStrength(
|
||||
return { score: 5, label: t("strengthStrong"), color: "#16a34a" };
|
||||
}
|
||||
|
||||
/** The numbered section rule above each group of fields ("01 Account", ...). */
|
||||
function AuthSectionHeader({
|
||||
icon: Icon,
|
||||
label,
|
||||
step,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
step: string;
|
||||
}) {
|
||||
const accent = "var(--color-primary-readable, var(--color-primary))";
|
||||
return (
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg"
|
||||
style={{
|
||||
background:
|
||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
||||
boxShadow:
|
||||
"inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 22%, transparent)",
|
||||
}}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" style={{ color: accent }} />
|
||||
</span>
|
||||
<span
|
||||
className="text-[11px] font-black uppercase tracking-widest"
|
||||
style={{ color: accent }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<span
|
||||
className="h-px flex-1"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(90deg, color-mix(in srgb, var(--color-text-muted) 18%, transparent), transparent)",
|
||||
}}
|
||||
/>
|
||||
<span aria-hidden="true" className="number-mark text-lg">
|
||||
{step}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RegisterForm({
|
||||
hotelName,
|
||||
captcha,
|
||||
@@ -205,156 +252,61 @@ export function RegisterForm({
|
||||
|
||||
{/* Username & Email */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg"
|
||||
style={{
|
||||
background:
|
||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
||||
boxShadow:
|
||||
"inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 22%, transparent)",
|
||||
}}
|
||||
>
|
||||
<UserRound
|
||||
className="h-3.5 w-3.5"
|
||||
style={{
|
||||
color: "var(--color-primary-readable, var(--color-primary))",
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="text-[11px] font-black uppercase tracking-widest"
|
||||
style={{
|
||||
color: "var(--color-primary-readable, var(--color-primary))",
|
||||
}}
|
||||
>
|
||||
{t("accountDetails")}
|
||||
</span>
|
||||
<span
|
||||
className="h-px flex-1"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(90deg, color-mix(in srgb, var(--color-text-muted) 18%, transparent), transparent)",
|
||||
}}
|
||||
/>
|
||||
<span aria-hidden="true" className="number-mark text-lg">
|
||||
01
|
||||
</span>
|
||||
</div>
|
||||
<AuthSectionHeader
|
||||
icon={UserRound}
|
||||
label={t("accountDetails")}
|
||||
step="01"
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 stagger-children">
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="username" className="auth-label">
|
||||
{t("username")}
|
||||
</label>
|
||||
<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="username"
|
||||
name="username"
|
||||
type="text"
|
||||
placeholder={t("usernamePlaceholder")}
|
||||
autoComplete="username"
|
||||
className="auth-input has-icon"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="mail" className="auth-label">
|
||||
{t("email")}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail
|
||||
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="mail"
|
||||
name="mail"
|
||||
type="email"
|
||||
placeholder={t("emailPlaceholder")}
|
||||
autoComplete="email"
|
||||
className="auth-input has-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AuthField
|
||||
id="username"
|
||||
name="username"
|
||||
label={t("username")}
|
||||
icon={UserRound}
|
||||
placeholder={t("usernamePlaceholder")}
|
||||
autoComplete="username"
|
||||
required
|
||||
/>
|
||||
<AuthField
|
||||
id="mail"
|
||||
name="mail"
|
||||
label={t("email")}
|
||||
icon={Mail}
|
||||
type="email"
|
||||
placeholder={t("emailPlaceholder")}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password & Confirm */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg"
|
||||
style={{
|
||||
background:
|
||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
||||
boxShadow:
|
||||
"inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 22%, transparent)",
|
||||
}}
|
||||
>
|
||||
<KeyRound
|
||||
className="h-3.5 w-3.5"
|
||||
style={{
|
||||
color: "var(--color-primary-readable, var(--color-primary))",
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="text-[11px] font-black uppercase tracking-widest"
|
||||
style={{
|
||||
color: "var(--color-primary-readable, var(--color-primary))",
|
||||
}}
|
||||
>
|
||||
{t("credentials")}
|
||||
</span>
|
||||
<span
|
||||
className="h-px flex-1"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(90deg, color-mix(in srgb, var(--color-text-muted) 18%, transparent), transparent)",
|
||||
}}
|
||||
/>
|
||||
<span aria-hidden="true" className="number-mark text-lg">
|
||||
02
|
||||
</span>
|
||||
</div>
|
||||
<AuthSectionHeader
|
||||
icon={KeyRound}
|
||||
label={t("credentials")}
|
||||
step="02"
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 stagger-children">
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="password" className="auth-label">
|
||||
{t("password")}
|
||||
</label>
|
||||
<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="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder={t("passwordPlaceholder")}
|
||||
autoComplete="new-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="auth-input has-icon has-action"
|
||||
required
|
||||
/>
|
||||
<AuthField
|
||||
id="password"
|
||||
name="password"
|
||||
label={t("password")}
|
||||
icon={Lock}
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={setPassword}
|
||||
placeholder={t("passwordPlaceholder")}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
trailing={
|
||||
<PasswordToggle
|
||||
show={showPassword}
|
||||
onToggle={() => setShowPassword((v) => !v)}
|
||||
labelShow={t("showPassword")}
|
||||
labelHide={t("hidePassword")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{password.length > 0 && (
|
||||
<div className="animate-fade-in-up space-y-1.5 pt-0.5">
|
||||
<div className="flex gap-1.5">
|
||||
@@ -410,35 +362,27 @@ export function RegisterForm({
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="password_confirmation" className="auth-label">
|
||||
{t("confirmPassword")}
|
||||
</label>
|
||||
<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="password_confirmation"
|
||||
name="password_confirmation"
|
||||
type={showConfirm ? "text" : "password"}
|
||||
placeholder={t("confirmPasswordPlaceholder")}
|
||||
autoComplete="new-password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="auth-input has-icon has-action"
|
||||
required
|
||||
/>
|
||||
</AuthField>
|
||||
<AuthField
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
label={t("confirmPassword")}
|
||||
icon={Lock}
|
||||
type={showConfirm ? "text" : "password"}
|
||||
value={confirmPassword}
|
||||
onChange={setConfirmPassword}
|
||||
placeholder={t("confirmPasswordPlaceholder")}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
trailing={
|
||||
<PasswordToggle
|
||||
show={showConfirm}
|
||||
onToggle={() => setShowConfirm((v) => !v)}
|
||||
labelShow={t("showPassword")}
|
||||
labelHide={t("hidePassword")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{mismatch && (
|
||||
<p
|
||||
className="animate-fade-in-up text-xs font-bold"
|
||||
@@ -447,7 +391,7 @@ export function RegisterForm({
|
||||
{t("passwordsDontMatch")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</AuthField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user