Polish register, forgot password, settings, community pages with shadcn components; update admin articles with modern table
Local Build and Deploy / deploy (push) Successful in 1m0s

This commit is contained in:
openhands committed 2026-07-16 19:51:33 +02:00
1 parent cb4cdc4a17
commit d4ee1dc4a3
5 files changed
+479 -487

No files matched your search

+161 -276
View File
@@ -1,10 +1,13 @@
"use client";
import { Eye, EyeOff, Lock, Mail, User, UserPlus } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useTranslations } from "next-intl";
import { useActionState, useState } from "react";
import { register } from "@/actions/register";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
interface RegisterFormProps {
hotelName: string;
@@ -17,292 +20,174 @@ export function RegisterForm({ hotelName, captcha, error }: RegisterFormProps) {
const showCaptcha = captcha.provider !== "none" && !!captcha.siteKey;
const [serverError, formAction, isPending] = useActionState(register, null);
const [termsAccepted, setTermsAccepted] = useState(false);
const [showPassword, setShowPassword] = useState(false);
return (
<div className="mx-auto w-full max-w-[800px]">
{/* Header Banner */}
<div
className="relative overflow-hidden bg-center bg-cover rounded-t-lg"
style={{
backgroundImage: "url(/assets/images/mybobba_banner.png)",
height: "180px",
maxHeight: "150px",
}}
>
<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="absolute inset-0 rounded-full overflow-hidden"
style={{ zIndex: -1 }}
>
<div
className="w-full h-full"
style={{
background: "black",
filter: "blur(8px)",
transform: "scale(1.2)",
opacity: 0.6,
}}
/>
</div>
<Image
src="/assets/images/FrankwithBag.gif"
alt=""
fill
className="!static"
style={{
objectFit: "contain",
objectPosition: "center",
width: "100%",
height: "100%",
}}
/>
</div>
<div className="mx-auto w-full max-w-lg animate-fade-in">
<div className="text-center mb-8">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-[var(--color-primary)]/20 to-[var(--color-primary)]/5 shadow-lg">
<UserPlus className="h-8 w-8 text-[var(--color-primary)]" />
</div>
<h1 className="text-2xl font-extrabold tracking-tight m-0">
{t("title")}
</h1>
<p className="mt-1.5 text-sm text-[var(--color-text-muted-readable)]">
{t("subtitle", { hotel: hotelName })}
</p>
</div>
{/* Form */}
<form
action={formAction}
className="p-4 flex flex-col gap-6"
style={{
backgroundColor:
"color-mix(in srgb, var(--color-background) 50%, var(--color-surface))",
borderRadius: "0 0 8px 8px",
}}
>
{error && (
<div
className="p-3 rounded-md text-sm"
style={{ backgroundColor: "#ef4444", color: "white" }}
>
{error}
</div>
)}
{serverError && (
<div
className="p-3 rounded-md text-sm"
style={{ backgroundColor: "#ef4444", color: "white" }}
>
{serverError}
</div>
)}
<Card className="border-[var(--border-subtle)] shadow-elevated">
<CardHeader className="px-6 pt-6 pb-0">
<CardTitle className="text-base font-bold">
Create your account
</CardTitle>
<CardDescription>
Fill in the details below to join {hotelName}
</CardDescription>
</CardHeader>
<CardContent className="p-6">
<form action={formAction} className="space-y-5">
{(error || serverError) && (
<div className="rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 px-4 py-3">
<p className="text-sm font-medium text-red-600 dark:text-red-400 text-center m-0">
{error || serverError}
</p>
</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>
<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>
<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>
<div className="grid gap-4 sm:grid-cols-2">
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--color-text-muted)]" />
<input
id="username"
name="username"
type="text"
placeholder={t("usernamePlaceholder")}
autoComplete="username"
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] pl-10 pr-4 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
required
/>
</div>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--color-text-muted)]" />
<input
id="mail"
name="mail"
type="email"
placeholder={t("emailPlaceholder")}
autoComplete="email"
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] pl-10 pr-4 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
/>
</div>
</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>
<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>
</div>
{/* Terms */}
<div
className="rounded-md p-3 flex flex-col gap-3"
style={{
backgroundColor:
"color-mix(in srgb, var(--color-background) 50%, var(--color-surface))",
}}
>
<div className="flex items-center gap-3 text-sm">
<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"
style={{
borderColor: termsAccepted
? "#27a44d"
: "color-mix(in srgb, var(--color-text-muted) 30%, transparent)",
backgroundColor: termsAccepted ? "#27a44d" : "transparent",
}}
>
{termsAccepted && (
<svg
className="w-3 h-3 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
<div className="grid gap-4 sm:grid-cols-2">
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--color-text-muted)]" />
<input
id="password"
name="password"
type={showPassword ? "text" : "password"}
placeholder={t("passwordPlaceholder")}
autoComplete="new-password"
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] pl-10 pr-10 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)] hover:text-[var(--color-text-readable)]"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
)}
</button>
<input
type="hidden"
name="terms"
value={termsAccepted ? "1" : ""}
/>
<span
className="font-semibold cursor-pointer"
style={{ color: "var(--color-text-readable, var(--color-text))" }}
onClick={() => setTermsAccepted(!termsAccepted)}
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--color-text-muted)]" />
<input
id="password_confirmation"
name="password_confirmation"
type="password"
placeholder={t("confirmPasswordPlaceholder")}
autoComplete="new-password"
className="w-full rounded-xl border-2 border-[var(--input-border-color)] bg-[var(--color-background)] pl-10 pr-4 py-2.5 text-sm transition-all duration-200 focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
required
/>
</div>
</div>
{/* Terms */}
<div className="flex items-start gap-3 rounded-xl border border-[var(--border-subtle)] bg-[var(--color-background)] p-4">
<button
type="button"
role="checkbox"
aria-checked={termsAccepted}
onClick={() => setTermsAccepted(!termsAccepted)}
className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md border-2 transition-all"
style={{
borderColor: termsAccepted
? "var(--color-primary)"
: "var(--input-border-color)",
backgroundColor: termsAccepted
? "var(--color-primary)"
: "transparent",
}}
>
{termsAccepted && (
<svg className="h-3 w-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</button>
<input type="hidden" name="terms" value={termsAccepted ? "1" : ""} />
<label
className="text-sm cursor-pointer select-none"
style={{ color: "var(--color-text-readable)" }}
onClick={() => setTermsAccepted(!termsAccepted)}
>
{t("termsAccept", { hotel: hotelName })}
</label>
</div>
{/* Captcha */}
{showCaptcha && captcha.provider === "turnstile" && (
<div className="flex justify-center">
<div className="cf-turnstile" data-sitekey={captcha.siteKey} />
</div>
)}
{showCaptcha && captcha.provider === "recaptcha" && (
<div className="flex justify-center">
<div className="g-recaptcha" data-sitekey={captcha.siteKey} />
</div>
)}
<Button
type="submit"
disabled={isPending}
size="lg"
className="w-full font-bold text-base h-12 rounded-xl"
>
{t("termsAccept", { hotel: hotelName })}
</span>
</div>
</div>
{isPending ? (
<span className="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>
{/* 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} />
)}
{/* Submit */}
<button
type="submit"
disabled={isPending}
className="w-full rounded p-2 text-white font-semibold transition-all duration-200 text-base"
style={{
backgroundColor: "#27a44d",
border: "2px solid #34c661",
}}
>
{isPending ? t("creatingAccount") : 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))",
}}
>
{t("alreadyHaveAccount")}
</Link>
</div>
</form>
<div className="text-center">
<p className="text-sm text-[var(--color-text-muted-readable)]">
{t("alreadyHaveAccount")}{" "}
<Link href="/login" className="font-bold text-[var(--color-primary)] hover:underline">
{t("signIn")}
</Link>
</p>
</div>
</form>
</CardContent>
</Card>
</div>
);
}