feat: prioritize game iframe, gated register terms and polished register page
Fetch the Nitro client iframe with high priority so the browser starts the game document before competing resources, and replace the bare /client spinner with a branded boot screen. Preconnect and eager loading were already in place; typing support for the iframe fetchPriority prop is added in a React type augmentation. Rework the register terms block into a single clickable accept control with a custom check state, error shake and inline hint, and dim the submit button until the terms are accepted. The register page gets labeled sections (account details / credentials), a corrected banner overlay, translated show/hide toggles and a captcha slot that reserves height to avoid layout shifts. English is the source of truth; other locales fall back to it.
This commit is contained in:
1 parent
7f39ba4257
commit
b59d6c21af
6 files changed
+374
-168
No files matched your search
@@ -383,6 +383,7 @@ export function ClientView({
|
||||
allow="autoplay; gamepad; fullscreen"
|
||||
referrerPolicy="no-referrer"
|
||||
loading="eager"
|
||||
fetchPriority={"high" as const}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,16 +1,48 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export default function ClientLoading() {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-center min-h-screen"
|
||||
style={{ background: "var(--color-background)" }}
|
||||
className="relative flex items-center justify-center min-h-screen overflow-hidden"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(ellipse at center, color-mix(in srgb, var(--color-surface) 80%, transparent) 0%, var(--color-background) 75%)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="animate-spin w-10 h-10 border-4 rounded-full"
|
||||
className="absolute inset-0 animate-pulse-glow"
|
||||
style={{
|
||||
borderColor: "var(--color-primary)",
|
||||
borderTopColor: "transparent",
|
||||
background:
|
||||
"radial-gradient(circle at 50% 45%, color-mix(in srgb, var(--color-primary) 12%, transparent) 0%, transparent 55%)",
|
||||
}}
|
||||
/>
|
||||
<div className="relative flex flex-col items-center gap-6">
|
||||
<div
|
||||
className="w-20 h-20 rounded-2xl flex items-center justify-center shadow-2xl"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(135deg, var(--color-primary) 0%, color-mix(in srgb, var(--color-primary) 70%, black) 100%)",
|
||||
boxShadow:
|
||||
"0 12px 40px color-mix(in srgb, var(--color-primary) 35%, transparent)",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/assets/images/icons/lighthouse.png"
|
||||
alt=""
|
||||
width={44}
|
||||
height={44}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="animate-spin w-9 h-9 border-[3px] rounded-full"
|
||||
style={{
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-text-muted) 18%, transparent)",
|
||||
borderTopColor: "var(--color-primary)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1701,6 +1701,28 @@ details[open] > summary .details-open\:rotate-180 {
|
||||
.animate-pulse-glow {
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
.animate-shake {
|
||||
animation: shake 0.45s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
}
|
||||
@keyframes shake {
|
||||
10%,
|
||||
90% {
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
20%,
|
||||
80% {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
30%,
|
||||
50%,
|
||||
70% {
|
||||
transform: translateX(-6px);
|
||||
}
|
||||
40%,
|
||||
60% {
|
||||
transform: translateX(6px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Glass morphism */
|
||||
.glass {
|
||||
|
||||
@@ -47,6 +47,8 @@ export function RegisterForm({
|
||||
FormData
|
||||
>(register, { error: null, ok: false });
|
||||
const [termsAccepted, setTermsAccepted] = useState(false);
|
||||
const [termsError, setTermsError] = useState(false);
|
||||
const termsRef = useRef<HTMLDivElement>(null);
|
||||
const [password, setPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
@@ -115,11 +117,16 @@ export function RegisterForm({
|
||||
className="relative overflow-hidden bg-center bg-cover rounded-t-xl"
|
||||
style={{
|
||||
backgroundImage: "url(/assets/images/mybobba_banner.png)",
|
||||
height: "180px",
|
||||
maxHeight: "150px",
|
||||
height: "160px",
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 z-10 bg-black/30" />
|
||||
<div
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(180deg, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0.55) 100%)",
|
||||
}}
|
||||
/>
|
||||
<div className="relative flex items-center justify-center w-full h-full z-20">
|
||||
<div
|
||||
className="relative animate-float"
|
||||
@@ -181,208 +188,334 @@ export function RegisterForm({
|
||||
)}
|
||||
|
||||
{/* Username & Email */}
|
||||
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-4 stagger-children">
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block text-xs font-bold uppercase tracking-wider"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{t("username")}
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
placeholder={t("usernamePlaceholder")}
|
||||
autoComplete="username"
|
||||
className="input-glow w-full rounded-xl border-2 px-4 py-3 text-sm font-medium transition-all focus:outline-none"
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="text-[11px] font-black uppercase tracking-widest"
|
||||
style={{
|
||||
backgroundColor: "var(--color-background)",
|
||||
color: "var(--color-text-readable)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
|
||||
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)",
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
htmlFor="mail"
|
||||
className="block text-xs font-bold uppercase tracking-wider"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{t("email")}
|
||||
</label>
|
||||
<input
|
||||
id="mail"
|
||||
name="mail"
|
||||
type="email"
|
||||
placeholder={t("emailPlaceholder")}
|
||||
autoComplete="email"
|
||||
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-text-muted) 25%, transparent)",
|
||||
}}
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 stagger-children">
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block text-xs font-bold uppercase tracking-wider"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{t("username")}
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
placeholder={t("usernamePlaceholder")}
|
||||
autoComplete="username"
|
||||
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-text-muted) 25%, transparent)",
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
htmlFor="mail"
|
||||
className="block text-xs font-bold uppercase tracking-wider"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{t("email")}
|
||||
</label>
|
||||
<input
|
||||
id="mail"
|
||||
name="mail"
|
||||
type="email"
|
||||
placeholder={t("emailPlaceholder")}
|
||||
autoComplete="email"
|
||||
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-text-muted) 25%, transparent)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password & Confirm */}
|
||||
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-4 stagger-children">
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-xs font-bold uppercase tracking-wider"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="text-[11px] font-black uppercase tracking-widest"
|
||||
style={{
|
||||
color: "var(--color-primary-readable, var(--color-primary))",
|
||||
}}
|
||||
>
|
||||
{t("password")}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder={t("passwordPlaceholder")}
|
||||
autoComplete="new-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
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-text-muted) 25%, transparent)",
|
||||
}}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-sm font-bold opacity-60 hover:opacity-100 transition-opacity"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? "Hide" : "Show"}
|
||||
</button>
|
||||
</div>
|
||||
{password.length > 0 && (
|
||||
<div className="animate-fade-in-up space-y-1">
|
||||
<div className="flex gap-1">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-1.5 flex-1 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
backgroundColor:
|
||||
i <= strength.score
|
||||
? strength.color
|
||||
: "color-mix(in srgb, var(--color-text-muted) 15%, transparent)",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p
|
||||
className="text-xs font-bold"
|
||||
style={{ color: strength.color }}
|
||||
>
|
||||
{strength.label}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{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)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
htmlFor="password_confirmation"
|
||||
className="block text-xs font-bold uppercase tracking-wider"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{t("confirmPassword")}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
type={showConfirm ? "text" : "password"}
|
||||
placeholder={t("confirmPasswordPlaceholder")}
|
||||
autoComplete="new-password"
|
||||
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-text-muted) 25%, transparent)",
|
||||
}}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfirm(!showConfirm)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-sm font-bold opacity-60 hover:opacity-100 transition-opacity"
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 stagger-children">
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-xs font-bold uppercase tracking-wider"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showConfirm ? "Hide" : "Show"}
|
||||
</button>
|
||||
{t("password")}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder={t("passwordPlaceholder")}
|
||||
autoComplete="new-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
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-text-muted) 25%, transparent)",
|
||||
}}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-sm font-bold opacity-60 hover:opacity-100 transition-opacity"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? t("hidePassword") : t("showPassword")}
|
||||
</button>
|
||||
</div>
|
||||
{password.length > 0 && (
|
||||
<div className="animate-fade-in-up space-y-1">
|
||||
<div className="flex gap-1">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-1.5 flex-1 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
backgroundColor:
|
||||
i <= strength.score
|
||||
? strength.color
|
||||
: "color-mix(in srgb, var(--color-text-muted) 15%, transparent)",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p
|
||||
className="text-xs font-bold"
|
||||
style={{ color: strength.color }}
|
||||
>
|
||||
{strength.label}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
htmlFor="password_confirmation"
|
||||
className="block text-xs font-bold uppercase tracking-wider"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{t("confirmPassword")}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
type={showConfirm ? "text" : "password"}
|
||||
placeholder={t("confirmPasswordPlaceholder")}
|
||||
autoComplete="new-password"
|
||||
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-text-muted) 25%, transparent)",
|
||||
}}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfirm(!showConfirm)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-sm font-bold opacity-60 hover:opacity-100 transition-opacity"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showConfirm ? t("hidePassword") : t("showPassword")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Terms */}
|
||||
<div
|
||||
className="rounded-xl p-4 flex gap-4"
|
||||
ref={termsRef}
|
||||
className={`rounded-xl p-4 flex items-center justify-between gap-4 transition-all duration-300 ${
|
||||
termsError ? "animate-shake" : ""
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor:
|
||||
"color-mix(in srgb, var(--color-background) 50%, var(--color-surface))",
|
||||
border:
|
||||
"1px solid color-mix(in srgb, var(--color-text-muted) 8%, transparent)",
|
||||
border: `1px solid ${
|
||||
termsError
|
||||
? "var(--color-danger)"
|
||||
: "color-mix(in srgb, var(--color-text-muted) 12%, transparent)"
|
||||
}`,
|
||||
boxShadow: termsError
|
||||
? "0 0 0 3px color-mix(in srgb, var(--color-danger) 14%, transparent)"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<label
|
||||
htmlFor="terms"
|
||||
className="group flex flex-1 items-center gap-3 cursor-pointer select-none"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="terms"
|
||||
name="terms"
|
||||
checked={termsAccepted}
|
||||
onChange={(e) => setTermsAccepted(e.target.checked)}
|
||||
className="w-5 h-5 rounded border-2 accent-(--color-primary) shrink-0 cursor-pointer"
|
||||
onChange={(e) => {
|
||||
setTermsAccepted(e.target.checked);
|
||||
if (e.target.checked) setTermsError(false);
|
||||
}}
|
||||
aria-invalid={termsError}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<label
|
||||
htmlFor="terms"
|
||||
className="font-medium text-sm text-[var(--color-text-readable)] cursor-pointer"
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg border-2 transition-all duration-300 group-hover:scale-105 group-hover:border-[var(--color-primary)] peer-focus-visible:ring-2 peer-focus-visible:ring-[var(--color-primary)]"
|
||||
style={{
|
||||
borderColor: termsError
|
||||
? "var(--color-danger)"
|
||||
: termsAccepted
|
||||
? "var(--color-primary)"
|
||||
: "color-mix(in srgb, var(--color-text-muted) 45%, transparent)",
|
||||
background: termsAccepted
|
||||
? "var(--color-primary)"
|
||||
: "transparent",
|
||||
boxShadow: termsAccepted
|
||||
? "0 0 18px color-mix(in srgb, var(--color-primary) 40%, transparent)"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="var(--color-primary-foreground)"
|
||||
strokeWidth="3.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={`transition-all duration-300 ${
|
||||
termsAccepted ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
||||
}`}
|
||||
>
|
||||
<title>{t("accepted")}</title>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
className="text-sm font-semibold leading-snug"
|
||||
style={{ color: "var(--color-text-readable)" }}
|
||||
>
|
||||
{t("termsAccept", { hotel: hotelName })}
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--color-text-muted)]">
|
||||
* Required for account creation
|
||||
</p>
|
||||
</span>
|
||||
</label>
|
||||
<span
|
||||
className="hidden sm:inline-flex shrink-0 items-center gap-1 rounded-full px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider transition-colors duration-300"
|
||||
style={{
|
||||
color: termsError
|
||||
? "var(--color-danger)"
|
||||
: termsAccepted
|
||||
? "var(--color-primary)"
|
||||
: "var(--color-text-muted)",
|
||||
background: termsError
|
||||
? "color-mix(in srgb, var(--color-danger) 10%, transparent)"
|
||||
: termsAccepted
|
||||
? "color-mix(in srgb, var(--color-primary) 12%, transparent)"
|
||||
: "color-mix(in srgb, var(--color-text-muted) 6%, transparent)",
|
||||
}}
|
||||
>
|
||||
{termsError || !termsAccepted ? t("termsRequired") : t("accepted")}
|
||||
</span>
|
||||
</div>
|
||||
{termsError && (
|
||||
<p
|
||||
className="animate-fade-in-up text-xs font-bold"
|
||||
style={{ color: "var(--color-danger)" }}
|
||||
>
|
||||
{t("termsRequired")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Captcha */}
|
||||
{showCaptcha && captcha.provider === "turnstile" && (
|
||||
<div className="cf-turnstile" data-sitekey={captcha.siteKey} />
|
||||
)}
|
||||
{showCaptcha && captcha.provider === "recaptcha" && (
|
||||
<div className="g-recaptcha" data-sitekey={captcha.siteKey} />
|
||||
)}
|
||||
{showCaptcha && captcha.provider === "hcaptcha" && (
|
||||
<div className="h-captcha" data-sitekey={captcha.siteKey} />
|
||||
{showCaptcha && (
|
||||
<div className="flex min-h-[70px] items-center justify-center">
|
||||
{captcha.provider === "turnstile" && (
|
||||
<div className="cf-turnstile" data-sitekey={captcha.siteKey} />
|
||||
)}
|
||||
{captcha.provider === "recaptcha" && (
|
||||
<div className="g-recaptcha" data-sitekey={captcha.siteKey} />
|
||||
)}
|
||||
{captcha.provider === "hcaptcha" && (
|
||||
<div className="h-captcha" data-sitekey={captcha.siteKey} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending || signingIn}
|
||||
className="btn-shine w-full rounded-xl font-extrabold text-sm py-3.5 transition-all duration-200 hover:scale-[1.02] active:scale-95 shadow-lg disabled:opacity-60"
|
||||
onClick={() => {
|
||||
if (!termsAccepted) {
|
||||
setTermsError(true);
|
||||
termsRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={`btn-shine w-full rounded-xl font-extrabold text-sm py-3.5 transition-all duration-200 hover:scale-[1.02] active:scale-95 shadow-lg disabled:opacity-60 ${
|
||||
termsAccepted ? "" : "opacity-70 saturate-[0.85] hover:scale-100"
|
||||
}`}
|
||||
style={{
|
||||
background: "var(--color-primary)",
|
||||
color: "var(--color-primary-foreground)",
|
||||
boxShadow:
|
||||
"0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)",
|
||||
boxShadow: termsAccepted
|
||||
? "0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)"
|
||||
: "0 2px 10px color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
||||
}}
|
||||
>
|
||||
{isPending || signingIn ? (
|
||||
|
||||
@@ -897,10 +897,15 @@
|
||||
"confirmPassword": "Repeat password",
|
||||
"usernamePlaceholder": "Username",
|
||||
"emailPlaceholder": "Email",
|
||||
"passwordPlaceholder": "Password (min 6 chars)",
|
||||
"passwordPlaceholder": "Password (min 8 chars)",
|
||||
"confirmPasswordPlaceholder": "Repeat password",
|
||||
"selectOutfit": "Select your outfit",
|
||||
"termsAccept": "I am 18+ and accept the {hotel} terms & rules.",
|
||||
"accepted": "Accepted & agreed",
|
||||
"showPassword": "Show",
|
||||
"hidePassword": "Hide",
|
||||
"accountDetails": "Account details",
|
||||
"credentials": "Credentials & security",
|
||||
"createAccount": "Create account",
|
||||
"creatingAccount": "Creating account...",
|
||||
"redirecting": "Redirecting to your account...",
|
||||
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import type { HTMLAttributes } from "react";
|
||||
|
||||
declare module "react" {
|
||||
interface IframeHTMLAttributes<T> extends HTMLAttributes<T> {
|
||||
/**
|
||||
* Browser support for `fetchpriority` on iframes exists (Chrome 126+,
|
||||
* Safari 17.5+, Firefox 129+); React forwards the `fetchPriority` prop
|
||||
* to a `fetchpriority` attribute so the game document is fetched with
|
||||
* high priority instead of competing with other resources.
|
||||
*/
|
||||
fetchPriority?: "high" | "low" | "auto";
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user