Fix migration connection exhaustion and polish auth pages
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m49s

- apply-migrations.ts: use single shared connection instead of one per operation
- Increase MariaDB max_connections from 151 to 300 in server config
- home-login-form.tsx: replace hardcoded gray colors with theme variables
- register-form.tsx: add password strength meter, spinner, theme-aligned inputs
- login-form.tsx: add Discord/Google SVG icons, spinner, smoother 2FA animation
- page/register/login: apply premium animations (float, stagger, glow, gradient)
This commit is contained in:
openhands committed 2026-07-24 11:30:58 +02:00
1 parent a9f1f4f99d
commit f7f6e09174
7 files changed
+394 -303

No files matched your search

+169 -151
View File
@@ -14,6 +14,20 @@ interface RegisterFormProps {
nonce?: string;
}
function passwordStrength(pw: string): { score: number; label: string; color: string } {
let score = 0;
if (pw.length >= 8) score += 1;
if (pw.length >= 12) score += 1;
if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) score += 1;
if (/\d/.test(pw)) score += 1;
if (/[^a-zA-Z0-9]/.test(pw)) score += 1;
if (score <= 1) return { score, label: "Weak", color: "#ef4444" };
if (score <= 2) return { score, label: "Fair", color: "#f59e0b" };
if (score <= 3) return { score, label: "Good", color: "#22c55e" };
return { score: 5, label: "Strong", color: "#16a34a" };
}
export function RegisterForm({
hotelName,
captcha,
@@ -24,6 +38,8 @@ export function RegisterForm({
const showCaptcha = captcha.provider !== "none" && !!captcha.siteKey;
const [serverError, formAction, isPending] = useActionState(register, null);
const [termsAccepted, setTermsAccepted] = useState(false);
const [password, setPassword] = useState("");
const strength = passwordStrength(password);
return (
<div className="mx-auto w-full max-w-[800px]">
@@ -45,7 +61,7 @@ export function RegisterForm({
) : null}
{/* Header Banner */}
<div
className="relative overflow-hidden bg-center bg-cover rounded-t-lg"
className="relative overflow-hidden bg-center bg-cover rounded-t-xl"
style={{
backgroundImage: "url(/assets/images/mybobba_banner.png)",
height: "180px",
@@ -54,7 +70,7 @@ export function RegisterForm({
>
<div className="absolute inset-0 z-10 bg-black/30" />
<div className="relative flex items-center justify-center w-full h-full z-20">
<div className="relative" style={{ width: "140px", height: "140px" }}>
<div className="relative animate-float" style={{ width: "140px", height: "140px" }}>
<div
className="absolute inset-0 rounded-full overflow-hidden"
style={{ zIndex: -1 }}
@@ -88,184 +104,178 @@ export function RegisterForm({
{/* Form */}
<form
action={formAction}
className="p-4 flex flex-col gap-6"
className="p-6 flex flex-col gap-6"
style={{
backgroundColor:
"color-mix(in srgb, var(--color-background) 50%, var(--color-surface))",
borderRadius: "0 0 8px 8px",
borderRadius: "0 0 12px 12px",
}}
>
{error && (
{(error || serverError) && (
<div
className="p-3 rounded-md text-sm"
style={{ backgroundColor: "#ef4444", color: "white" }}
className="animate-fade-in-up p-3 rounded-lg text-sm font-semibold text-white"
style={{ backgroundColor: "var(--color-danger)" }}
>
{error}
</div>
)}
{serverError && (
<div
className="p-3 rounded-md text-sm"
style={{ backgroundColor: "#ef4444", color: "white" }}
>
{serverError}
{error || serverError}
</div>
)}
{/* Username & Email */}
<div className="w-full flex flex-col md:flex-row gap-4">
<div className="w-full md:w-1/2">
<fieldset className="flex flex-col w-full gap-1">
<label
htmlFor="username"
className="block font-semibold text-sm"
style={{
color: "var(--color-text-readable, var(--color-text))",
}}
>
{t("username")}
</label>
<input
id="username"
name="username"
type="text"
placeholder={t("usernamePlaceholder")}
autoComplete="username"
className="focus:ring-0 border-4 rounded w-full text-sm px-3 py-2 mt-1"
style={{
backgroundColor: "var(--color-background)",
color: "var(--color-text-readable, var(--color-text))",
borderColor:
"color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
}}
required
/>
</fieldset>
<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="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="w-full md:w-1/2">
<fieldset className="flex flex-col w-full gap-1">
<label
htmlFor="mail"
className="block font-semibold text-sm"
style={{
color: "var(--color-text-readable, var(--color-text))",
}}
>
{t("email")}
</label>
<input
id="mail"
name="mail"
type="email"
placeholder={t("emailPlaceholder")}
autoComplete="email"
className="focus:ring-0 border-4 rounded w-full text-sm px-3 py-2 mt-1"
style={{
backgroundColor: "var(--color-background)",
color: "var(--color-text-readable, var(--color-text))",
borderColor:
"color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
}}
/>
</fieldset>
<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="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)",
}}
/>
<p
className="text-xs mt-1"
style={{ color: "var(--color-text-muted)" }}
>
Geen e-mail? Je kunt later via Discord verifiëren.
</p>
</div>
<p
className="text-xs mt-1"
style={{
color:
"var(--color-text-muted-readable, var(--color-text-muted))",
}}
>
Geen e-mail? Je kunt later via Discord verifiëren.
</p>
</div>
{/* Password & Confirm */}
<div className="w-full flex flex-col md:flex-row gap-4">
<div className="w-full md:w-1/2">
<fieldset className="flex flex-col w-full gap-1">
<label
htmlFor="password"
className="block font-semibold text-sm"
style={{
color: "var(--color-text-readable, var(--color-text))",
}}
>
{t("password")}
</label>
<input
id="password"
name="password"
type="password"
placeholder={t("passwordPlaceholder")}
autoComplete="new-password"
className="focus:ring-0 border-4 rounded w-full text-sm px-3 py-2 mt-1"
style={{
backgroundColor: "var(--color-background)",
color: "var(--color-text-readable, var(--color-text))",
borderColor:
"color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
}}
required
/>
</fieldset>
<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)" }}
>
{t("password")}
</label>
<input
id="password"
name="password"
type="password"
placeholder={t("passwordPlaceholder")}
autoComplete="new-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="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
/>
{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="w-full md:w-1/2">
<fieldset className="flex flex-col w-full gap-1">
<label
htmlFor="password_confirmation"
className="block font-semibold text-sm"
style={{
color: "var(--color-text-readable, var(--color-text))",
}}
>
{t("confirmPassword")}
</label>
<input
id="password_confirmation"
name="password_confirmation"
type="password"
placeholder={t("confirmPasswordPlaceholder")}
autoComplete="new-password"
className="focus:ring-0 border-4 rounded w-full text-sm px-3 py-2 mt-1"
style={{
backgroundColor: "var(--color-background)",
color: "var(--color-text-readable, var(--color-text))",
borderColor:
"color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
}}
required
/>
</fieldset>
<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>
<input
id="password_confirmation"
name="password_confirmation"
type="password"
placeholder={t("confirmPasswordPlaceholder")}
autoComplete="new-password"
className="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>
{/* Terms */}
<div
className="rounded-md p-3 flex flex-col gap-3"
className="rounded-xl p-4 flex flex-col gap-3"
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)",
}}
>
<div className="flex items-center gap-3 text-sm">
{/* biome-ignore lint/a11y/useSemanticElements: custom checkbox styled as button is intentional design */}
<button
type="button"
role="checkbox"
aria-checked={termsAccepted}
onClick={() => setTermsAccepted(!termsAccepted)}
className="w-5 h-5 rounded border-2 flex items-center justify-center shrink-0 transition-colors"
className="w-5 h-5 rounded border-2 flex items-center justify-center shrink-0 transition-all duration-200"
style={{
borderColor: termsAccepted
? "#27a44d"
? "var(--color-primary)"
: "color-mix(in srgb, var(--color-text-muted) 30%, transparent)",
backgroundColor: termsAccepted ? "#27a44d" : "transparent",
backgroundColor: termsAccepted
? "var(--color-primary)"
: "transparent",
}}
>
{termsAccepted && (
<svg
className="w-3 h-3 text-white"
className="w-3 h-3"
style={{ color: "var(--color-primary-foreground)" }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -287,7 +297,6 @@ export function RegisterForm({
name="terms"
value={termsAccepted ? "1" : ""}
/>
{/* biome-ignore lint/a11y/useSemanticElements: terms label mirrors the checkbox button */}
<span
role="checkbox"
aria-checked={termsAccepted}
@@ -298,8 +307,8 @@ export function RegisterForm({
setTermsAccepted(!termsAccepted);
}
}}
className="font-semibold cursor-pointer"
style={{ color: "var(--color-text-readable, var(--color-text))" }}
className="font-semibold cursor-pointer select-none"
style={{ color: "var(--color-text-readable)" }}
onClick={() => setTermsAccepted(!termsAccepted)}
>
{t("termsAccept", { hotel: hotelName })}
@@ -319,22 +328,31 @@ export function RegisterForm({
<button
type="submit"
disabled={isPending}
className="w-full rounded p-2 text-white font-semibold transition-all duration-200 text-base"
className="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"
style={{
backgroundColor: "#27a44d",
border: "2px solid #34c661",
background: "var(--color-primary)",
color: "var(--color-primary-foreground)",
boxShadow: "0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)",
}}
>
{isPending ? t("creatingAccount") : t("createAccount")}
{isPending ? (
<span className="inline-flex items-center justify-center gap-2">
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
{t("creatingAccount")}
</span>
) : (
t("createAccount")
)}
</button>
<div className="text-center">
<Link
href="/login"
className="text-sm font-semibold hover:underline"
style={{
color: "var(--color-primary-readable, var(--color-primary))",
}}
className="text-sm font-bold hover:underline transition-all"
style={{ color: "var(--color-primary-readable, var(--color-primary))" }}
>
{t("alreadyHaveAccount")}
</Link>