latest changes
This commit is contained in:
1 parent
56cd6fd058
commit
1de72863db
40 files changed
+3837
-568
No files matched your search
@@ -0,0 +1,303 @@
|
||||
"use client";
|
||||
|
||||
import { useState, FormEvent, ChangeEvent } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { AvatarCarousel } from "@/components/auth/avatar-carousel";
|
||||
|
||||
interface RegisterFormProps {
|
||||
initialFigures: string[];
|
||||
imager: string;
|
||||
hotelName: string;
|
||||
captcha: { provider: string; siteKey?: string };
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function RegisterForm({
|
||||
initialFigures,
|
||||
imager,
|
||||
hotelName,
|
||||
captcha,
|
||||
error,
|
||||
}: RegisterFormProps) {
|
||||
const t = useTranslations("pages.register");
|
||||
const router = useRouter();
|
||||
const showCaptcha = captcha.provider !== "none" && !!captcha.siteKey;
|
||||
const [selectedFigure, setSelectedFigure] = useState(initialFigures[0] || "");
|
||||
const [formData, setFormData] = useState({
|
||||
username: "",
|
||||
mail: "",
|
||||
password: "",
|
||||
password_confirmation: "",
|
||||
terms: false,
|
||||
beta_code: "",
|
||||
referral_code: "",
|
||||
});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: type === "checkbox" ? checked : value }));
|
||||
if (errors[name]) {
|
||||
setErrors((prev) => ({ ...prev, [name]: "" }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!formData.username.trim()) newErrors.username = t("usernameError");
|
||||
if (!formData.mail.trim()) newErrors.mail = t("emailError");
|
||||
if (!formData.password) newErrors.password = t("passwordError");
|
||||
if (formData.password !== formData.password_confirmation) {
|
||||
newErrors.password_confirmation = t("confirmPasswordError");
|
||||
}
|
||||
if (!formData.terms) newErrors.terms = t("termsError");
|
||||
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
...formData,
|
||||
look: selectedFigure,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
setErrors({ form: data.message || "Registration failed" });
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
router.push("/login?registered=1");
|
||||
} catch {
|
||||
setErrors({ form: "An error occurred. Please try again." });
|
||||
setIsSubmitting(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>
|
||||
<img src="/assets/images/FrankwithBag.gif" className="w-full h-full" style={{ objectFit: "contain", objectPosition: "center" }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} 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>
|
||||
)}
|
||||
|
||||
{/* 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)" }}>
|
||||
{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)",
|
||||
borderColor: "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
|
||||
}}
|
||||
required
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{errors.username && <p className="text-xs text-red-500">{errors.username}</p>}
|
||||
</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)" }}>
|
||||
{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)",
|
||||
borderColor: "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
|
||||
}}
|
||||
required
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{errors.mail && <p className="text-xs text-red-500">{errors.mail}</p>}
|
||||
</fieldset>
|
||||
</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)" }}>
|
||||
{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)",
|
||||
borderColor: "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
|
||||
}}
|
||||
required
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{errors.password && <p className="text-xs text-red-500">{errors.password}</p>}
|
||||
</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)" }}>
|
||||
{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)",
|
||||
borderColor: "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
|
||||
}}
|
||||
required
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{errors.password_confirmation && <p className="text-xs text-red-500">{errors.password_confirmation}</p>}
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Avatar Selection */}
|
||||
<div className="w-full rounded-md p-4" style={{
|
||||
backgroundColor: "color-mix(in srgb, var(--color-background) 50%, var(--color-surface))",
|
||||
border: "1px solid color-mix(in srgb, var(--color-text-muted) 15%, transparent)",
|
||||
}}>
|
||||
<legend className="text-sm font-semibold mb-3" style={{ color: "var(--color-text)" }}>
|
||||
{t("selectOutfit")}
|
||||
</legend>
|
||||
<AvatarCarousel
|
||||
figures={initialFigures}
|
||||
imager={imager}
|
||||
onSelect={setSelectedFigure}
|
||||
/>
|
||||
</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))",
|
||||
}}>
|
||||
<fieldset className="flex flex-wrap items-center w-full gap-3 text-sm">
|
||||
<input
|
||||
id="terms"
|
||||
name="terms"
|
||||
type="checkbox"
|
||||
className="w-4 h-4 border-gray-300 rounded focus:ring-0"
|
||||
style={{ borderColor: "color-mix(in srgb, var(--color-text-muted) 30%, transparent)" }}
|
||||
required
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<label htmlFor="terms" className="font-semibold" style={{ color: "var(--color-text)" }}>
|
||||
<a href="/help-center/rules" target="_blank" rel="noopener noreferrer" className="hover:underline" style={{ color: "var(--color-text)" }}>
|
||||
{t("termsAccept", { hotel: hotelName })}
|
||||
</a>
|
||||
</label>
|
||||
</fieldset>
|
||||
{errors.terms && <p className="text-xs text-red-500 italic">{errors.terms}</p>}
|
||||
</div>
|
||||
|
||||
{/* 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={isSubmitting}
|
||||
className="w-full rounded p-2 text-white font-semibold transition-all duration-200 text-base"
|
||||
style={{
|
||||
backgroundColor: "#27a44d",
|
||||
border: "2px solid #34c661",
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? t("creatingAccount") : t("createAccount")}
|
||||
</button>
|
||||
|
||||
{/* Social Login - optional */}
|
||||
{false && (
|
||||
<div className="pt-4 border-t" style={{ borderColor: "color-mix(in srgb, var(--color-text-muted) 15%, transparent)" }}>
|
||||
<p className="text-center text-sm mb-3" style={{ color: "var(--color-text-muted)" }}>
|
||||
Or register with
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Google, Discord, GitHub buttons would go here */}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
<Link href="/login" className="text-sm font-semibold hover:underline" style={{ color: "var(--color-primary)" }}>
|
||||
{t("alreadyHaveAccount")}
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user