057d41e1c2
Build & Push / Pipeline Tests (push) Failing after 1m5s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Failing after 52s
Test / API Unit Tests (push) Has been skipped
Test / Homepage Unit Tests (push) Has been skipped
Test / Carplace Unit Tests (push) Has been skipped
Test / Admin Unit Tests (push) Has been skipped
Test / Dashboard Unit Tests (push) Has been skipped
Test / API Integration Tests (push) Has been skipped
569 lines
24 KiB
TypeScript
569 lines
24 KiB
TypeScript
"use client";
|
||
|
||
import Image from "next/image";
|
||
import { useEffect, useRef, useState } from "react";
|
||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||
import { apiFetch } from "@/lib/api";
|
||
import PublicShell from "@/components/layout/PublicShell";
|
||
import { useDashboardI18n } from "@/components/I18nProvider";
|
||
import { buildHomepageSignInPath } from "@/lib/dashboardPaths";
|
||
import { websiteUrl } from "@/lib/urls";
|
||
|
||
const DASHBOARD_LOGO_SRC = "/dashboard/rentaldrivego.png";
|
||
const SUBSCRIPTION_PLANS = ["STARTER", "GROWTH", "PRO"] as const;
|
||
|
||
type SubscriptionPlan = (typeof SUBSCRIPTION_PLANS)[number];
|
||
|
||
const PLAN_LABELS: Record<SubscriptionPlan, string> = {
|
||
STARTER: "Launch",
|
||
GROWTH: "Growth",
|
||
PRO: "Pro",
|
||
};
|
||
|
||
function isSubscriptionPlan(value: string | null): value is SubscriptionPlan {
|
||
return SUBSCRIPTION_PLANS.some((plan) => plan === value);
|
||
}
|
||
|
||
function notifyParent(message: Record<string, unknown>) {
|
||
if (typeof window === "undefined" || window.parent === window) return;
|
||
window.parent.postMessage(message, "*");
|
||
}
|
||
|
||
function withAuthQuery(
|
||
params: { embedded: boolean; language: "en" | "fr" | "ar"; theme: string },
|
||
) {
|
||
const href = buildHomepageSignInPath(undefined, {
|
||
locale: params.language,
|
||
theme: params.theme,
|
||
});
|
||
const url = new URL(href, "http://localhost");
|
||
|
||
if (params.embedded) url.searchParams.set("embedded", "1");
|
||
|
||
return `${url.pathname}${url.search}`;
|
||
}
|
||
|
||
export default function SignUpForm({
|
||
embedded = false,
|
||
}: {
|
||
embedded?: boolean;
|
||
}) {
|
||
const { language, theme, setLanguage, setTheme } = useDashboardI18n();
|
||
const pathname = usePathname();
|
||
const router = useRouter();
|
||
const searchParams = useSearchParams();
|
||
const requestedLanguage = searchParams.get("lang");
|
||
const requestedTheme = searchParams.get("theme");
|
||
const requestedPlan = searchParams.get("plan");
|
||
const selectedPlan = isSubscriptionPlan(requestedPlan) ? requestedPlan : null;
|
||
const initializedFromQuery = useRef(false);
|
||
const initialLanguage =
|
||
requestedLanguage === "en" ||
|
||
requestedLanguage === "fr" ||
|
||
requestedLanguage === "ar"
|
||
? requestedLanguage
|
||
: language;
|
||
|
||
const [firstName, setFirstName] = useState("");
|
||
const [lastName, setLastName] = useState("");
|
||
const [companyName, setCompanyName] = useState("");
|
||
const [email, setEmail] = useState("");
|
||
const [password, setPassword] = useState("");
|
||
const [confirmPassword, setConfirmPassword] = useState("");
|
||
const [showPassword, setShowPassword] = useState(false);
|
||
const [preferredLanguage, setPreferredLanguage] = useState<
|
||
"en" | "fr" | "ar"
|
||
>(initialLanguage);
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [success, setSuccess] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (initializedFromQuery.current) return;
|
||
|
||
if (
|
||
(requestedLanguage === "en" ||
|
||
requestedLanguage === "fr" ||
|
||
requestedLanguage === "ar") &&
|
||
requestedLanguage !== language
|
||
) {
|
||
setLanguage(requestedLanguage);
|
||
setPreferredLanguage(requestedLanguage);
|
||
}
|
||
|
||
if (
|
||
(requestedTheme === "light" || requestedTheme === "dark") &&
|
||
requestedTheme !== theme
|
||
) {
|
||
setTheme(requestedTheme);
|
||
}
|
||
|
||
initializedFromQuery.current = true;
|
||
}, [
|
||
language,
|
||
requestedLanguage,
|
||
requestedTheme,
|
||
setLanguage,
|
||
setTheme,
|
||
theme,
|
||
]);
|
||
|
||
useEffect(() => {
|
||
if (!initializedFromQuery.current) return;
|
||
if (language !== preferredLanguage) setPreferredLanguage(language);
|
||
}, [language, preferredLanguage]);
|
||
|
||
useEffect(() => {
|
||
if (!initializedFromQuery.current) return;
|
||
|
||
const params = new URLSearchParams(searchParams.toString());
|
||
let changed = false;
|
||
|
||
if (params.get("lang") !== preferredLanguage) {
|
||
params.set("lang", preferredLanguage);
|
||
changed = true;
|
||
}
|
||
|
||
if (params.get("theme") !== theme) {
|
||
params.set("theme", theme);
|
||
changed = true;
|
||
}
|
||
|
||
if (embedded && params.get("embedded") !== "1") {
|
||
params.set("embedded", "1");
|
||
changed = true;
|
||
}
|
||
|
||
if (!changed) return;
|
||
|
||
const nextQuery = params.toString();
|
||
router.replace(nextQuery ? `${pathname}?${nextQuery}` : pathname, {
|
||
scroll: false,
|
||
});
|
||
}, [embedded, pathname, preferredLanguage, router, searchParams, theme]);
|
||
|
||
useEffect(() => {
|
||
const currentPath =
|
||
window.location.pathname + (window.location.search || "");
|
||
notifyParent({ type: "rentaldrivego:embedded-path", path: currentPath });
|
||
}, [pathname, searchParams]);
|
||
|
||
const dict = {
|
||
en: {
|
||
title: "Create account",
|
||
subtitle: "Enter your details to create your workspace.",
|
||
firstName: "First name",
|
||
firstNamePlaceholder: "Aya",
|
||
lastName: "Last name",
|
||
lastNamePlaceholder: "Benali",
|
||
company: "Company",
|
||
companyPlaceholder: "Atlas Cars",
|
||
email: "Email",
|
||
emailPlaceholder: "you@company.com",
|
||
password: "Password",
|
||
passwordPlaceholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
|
||
confirmPassword: "Confirm password",
|
||
passwordHint: "Minimum 8 characters",
|
||
languageLabel: "Preferred language",
|
||
create: "Create account",
|
||
subscribeToPlan: "Subscribe to {plan}",
|
||
creating: "Creating\u2026",
|
||
alreadyHave: "Already have an account?",
|
||
signIn: "Sign in",
|
||
errorEmailTaken: "An account with this email already exists.",
|
||
errorPasswordMismatch: "Passwords do not match.",
|
||
errorGeneric: "Something went wrong. Please try again.",
|
||
successTitle: "Check your email",
|
||
successMessage:
|
||
"We sent a verification link to {email}. Click the link to activate your account, then sign in.",
|
||
successSignIn: "Go to sign in",
|
||
},
|
||
fr: {
|
||
title: "Cr\u00e9er un compte",
|
||
subtitle: "Saisissez vos informations pour cr\u00e9er votre espace.",
|
||
firstName: "Pr\u00e9nom",
|
||
firstNamePlaceholder: "Aya",
|
||
lastName: "Nom",
|
||
lastNamePlaceholder: "Benali",
|
||
company: "Entreprise",
|
||
companyPlaceholder: "Atlas Cars",
|
||
email: "Email",
|
||
emailPlaceholder: "vous@entreprise.com",
|
||
password: "Mot de passe",
|
||
passwordPlaceholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
|
||
confirmPassword: "Confirmer le mot de passe",
|
||
passwordHint: "Minimum 8 caract\u00e8res",
|
||
languageLabel: "Langue pr\u00e9f\u00e9r\u00e9e",
|
||
create: "Cr\u00e9er un compte",
|
||
subscribeToPlan: "S’abonner à {plan}",
|
||
creating: "Cr\u00e9ation\u2026",
|
||
alreadyHave: "Vous avez d\u00e9j\u00e0 un compte ?",
|
||
signIn: "Se connecter",
|
||
errorEmailTaken: "Un compte avec cet email existe d\u00e9j\u00e0.",
|
||
errorPasswordMismatch: "Les mots de passe ne correspondent pas.",
|
||
errorGeneric: "Une erreur est survenue. Veuillez r\u00e9essayer.",
|
||
successTitle: "V\u00e9rifiez votre email",
|
||
successMessage:
|
||
"Nous avons envoy\u00e9 un lien de v\u00e9rification \u00e0 {email}. Cliquez sur le lien pour activer votre compte, puis connectez-vous.",
|
||
successSignIn: "Aller \u00e0 la connexion",
|
||
},
|
||
ar: {
|
||
title:
|
||
"\u0623\u0646\u0634\u0626 \u062d\u0633\u0627\u0628\u064b\u0627",
|
||
subtitle:
|
||
"\u0623\u062f\u062e\u0644 \u0628\u064a\u0627\u0646\u0627\u062a\u0643 \u0644\u0625\u0646\u0634\u0627\u0621 \u0645\u0633\u0627\u062d\u0629 \u0639\u0645\u0644\u0643.",
|
||
firstName: "\u0627\u0644\u0627\u0633\u0645 \u0627\u0644\u0623\u0648\u0644",
|
||
firstNamePlaceholder: "Aya",
|
||
lastName: "\u0627\u0633\u0645 \u0627\u0644\u0639\u0627\u0626\u0644\u0629",
|
||
lastNamePlaceholder: "Benali",
|
||
company: "\u0627\u0644\u0634\u0631\u0643\u0629",
|
||
companyPlaceholder: "Atlas Cars",
|
||
email:
|
||
"\u0627\u0644\u0628\u0631\u064a\u062f \u0627\u0644\u0625\u0644\u0643\u062a\u0631\u0648\u0646\u064a",
|
||
emailPlaceholder: "you@company.com",
|
||
password: "\u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631",
|
||
passwordPlaceholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
|
||
confirmPassword:
|
||
"\u062a\u0623\u0643\u064a\u062f \u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631",
|
||
passwordHint:
|
||
"\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u062f\u0646\u0649 8 \u0623\u062d\u0631\u0641",
|
||
languageLabel:
|
||
"\u0627\u0644\u0644\u063a\u0629 \u0627\u0644\u0645\u0641\u0636\u0644\u0629",
|
||
create:
|
||
"\u0623\u0646\u0634\u0626 \u062d\u0633\u0627\u0628\u064b\u0627",
|
||
subscribeToPlan:
|
||
"\u0627\u0634\u062a\u0631\u0643 \u0641\u064a {plan}",
|
||
creating:
|
||
"\u062c\u0627\u0631\u064d \u0627\u0644\u0625\u0646\u0634\u0627\u0621\u2026",
|
||
alreadyHave:
|
||
"\u0647\u0644 \u0644\u062f\u064a\u0643 \u062d\u0633\u0627\u0628 \u0628\u0627\u0644\u0641\u0639\u0644\u061f",
|
||
signIn:
|
||
"\u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062f\u062e\u0648\u0644",
|
||
errorEmailTaken:
|
||
"\u064a\u0648\u062c\u062f \u062d\u0633\u0627\u0628 \u0628\u0647\u0630\u0627 \u0627\u0644\u0628\u0631\u064a\u062f \u0627\u0644\u0625\u0644\u0643\u062a\u0631\u0648\u0646\u064a \u0628\u0627\u0644\u0641\u0639\u0644.",
|
||
errorPasswordMismatch:
|
||
"\u0643\u0644\u0645\u062a\u0627 \u0627\u0644\u0645\u0631\u0648\u0631 \u063a\u064a\u0631 \u0645\u062a\u0637\u0627\u0628\u0642\u062a\u064a\u0646.",
|
||
errorGeneric:
|
||
"\u062d\u062f\u062b \u062e\u0637\u0623 \u0645\u0627. \u064a\u0631\u062c\u0649 \u0627\u0644\u0645\u062d\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649.",
|
||
successTitle:
|
||
"\u062a\u062d\u0642\u0642 \u0645\u0646 \u0628\u0631\u064a\u062f\u0643 \u0627\u0644\u0625\u0644\u0643\u062a\u0631\u0648\u0646\u064a",
|
||
successMessage:
|
||
"\u0623\u0631\u0633\u0644\u0646\u0627 \u0631\u0627\u0628\u0637 \u062a\u062d\u0642\u0642 \u0625\u0644\u0649 {email}. \u0627\u0646\u0642\u0631 \u0639\u0644\u0649 \u0627\u0644\u0631\u0627\u0628\u0637 \u0644\u062a\u0641\u0639\u064a\u0644 \u062d\u0633\u0627\u0628\u0643\u060c \u062b\u0645 \u0642\u0645 \u0628\u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062f\u062e\u0648\u0644.",
|
||
successSignIn:
|
||
"\u0627\u0644\u0630\u0647\u0627\u0628 \u0625\u0644\u0649 \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062f\u062e\u0648\u0644",
|
||
},
|
||
}[preferredLanguage];
|
||
|
||
const signInHref = withAuthQuery({
|
||
embedded,
|
||
language: preferredLanguage,
|
||
theme,
|
||
});
|
||
const submitLabel = selectedPlan
|
||
? dict.subscribeToPlan.replace("{plan}", PLAN_LABELS[selectedPlan])
|
||
: dict.create;
|
||
const textPattern = {
|
||
en: "[A-Za-z\\s'-]+",
|
||
fr: "[A-Za-zÀ-ÖØ-öø-ÿŒœ\\s'-]+",
|
||
ar: "[\\u0600-\\u06FF\\u0750-\\u077F\\s،؛؟ـ'-]+",
|
||
}[preferredLanguage];
|
||
|
||
async function handleSubmit(e: React.FormEvent) {
|
||
e.preventDefault();
|
||
if (password !== confirmPassword) {
|
||
setError(dict.errorPasswordMismatch);
|
||
return;
|
||
}
|
||
|
||
setLoading(true);
|
||
setError(null);
|
||
|
||
try {
|
||
await apiFetch("/auth/account/start", {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
firstName: firstName.trim(),
|
||
lastName: lastName.trim(),
|
||
companyName: companyName.trim(),
|
||
email: email.trim(),
|
||
password,
|
||
preferredLanguage,
|
||
...(selectedPlan ? { subscriptionPlan: selectedPlan } : {}),
|
||
}),
|
||
});
|
||
|
||
setLanguage(preferredLanguage);
|
||
document.cookie = `rentaldrivego-language=${preferredLanguage}; path=/; max-age=31536000; samesite=lax`;
|
||
setSuccess(true);
|
||
} catch (err: any) {
|
||
if (
|
||
err?.message?.includes("email_taken") ||
|
||
err?.code === "email_taken"
|
||
) {
|
||
setError(dict.errorEmailTaken);
|
||
} else {
|
||
setError(dict.errorGeneric);
|
||
}
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
if (success) {
|
||
return (
|
||
<PublicShell embedded={embedded} hideFooter hideHeaderActions>
|
||
<main className="flex flex-1 items-center justify-center px-4 py-12">
|
||
<div className="w-full max-w-md text-center">
|
||
<div className="flex justify-center mb-6">
|
||
<Image
|
||
src={DASHBOARD_LOGO_SRC}
|
||
alt="RentalDriveGo"
|
||
width={104}
|
||
height={104}
|
||
unoptimized
|
||
className="h-24 w-24 rounded-[1.75rem] border border-stone-200/80 bg-white/85 p-1.5 shadow-sm transition-colors dark:border-blue-800 dark:bg-blue-950/80"
|
||
/>
|
||
</div>
|
||
<div className="rounded-[2rem] border border-stone-200/80 bg-white/82 p-8 shadow-[0_30px_80px_rgba(28,25,23,0.08)] backdrop-blur dark:border-blue-900 dark:bg-blue-950/78">
|
||
<h1 className="text-2xl font-black tracking-[-0.03em] text-blue-950 dark:text-stone-50">
|
||
{dict.successTitle}
|
||
</h1>
|
||
<p className="mt-4 text-sm leading-7 text-stone-600 dark:text-stone-300">
|
||
{dict.successMessage.replace("{email}", email)}
|
||
</p>
|
||
<a
|
||
href={signInHref}
|
||
target={embedded ? "_self" : undefined}
|
||
className="mt-6 inline-flex justify-center rounded-full bg-orange-600 px-6 py-3 text-sm font-semibold text-white transition hover:bg-orange-700 dark:bg-orange-500 dark:hover:bg-orange-400"
|
||
>
|
||
{dict.successSignIn}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</PublicShell>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<PublicShell embedded={embedded} hideFooter hideHeaderActions>
|
||
<main className="flex flex-1 items-center justify-center px-4 py-12">
|
||
<div className="w-full max-w-lg">
|
||
<div className="mb-8 text-center">
|
||
<div className="flex justify-center">
|
||
<a href={websiteUrl} target="_top">
|
||
<Image
|
||
src={DASHBOARD_LOGO_SRC}
|
||
alt="RentalDriveGo"
|
||
width={104}
|
||
height={104}
|
||
unoptimized
|
||
className="h-24 w-24 rounded-[1.75rem] border border-stone-200/80 bg-white/85 p-1.5 shadow-sm transition-colors dark:border-blue-800 dark:bg-blue-950/80"
|
||
/>
|
||
</a>
|
||
</div>
|
||
<h1 className="mt-3 text-3xl font-black tracking-tight text-stone-900 dark:text-stone-100">
|
||
{dict.title}
|
||
</h1>
|
||
<p className="mt-2 text-sm text-stone-600 dark:text-stone-300">
|
||
{dict.subtitle}
|
||
</p>
|
||
</div>
|
||
|
||
<section className="rounded-[2rem] border border-stone-200/80 bg-white/82 p-8 shadow-[0_30px_80px_rgba(28,25,23,0.08)] backdrop-blur transition-colors dark:border-blue-900 dark:bg-blue-950/78 dark:shadow-[0_30px_80px_rgba(0,0,0,0.26)]">
|
||
{error ? (
|
||
<div className="mb-5 rounded-[1.5rem] border border-red-200/80 bg-red-50/90 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-300">
|
||
{error}
|
||
</div>
|
||
) : null}
|
||
|
||
<form onSubmit={handleSubmit} className="space-y-5">
|
||
<div className="grid gap-4 sm:grid-cols-2">
|
||
<div>
|
||
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">
|
||
{dict.firstName} <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
required
|
||
maxLength={50}
|
||
value={firstName}
|
||
onChange={(e) => setFirstName(e.target.value)}
|
||
placeholder={dict.firstNamePlaceholder}
|
||
autoFocus
|
||
dir={preferredLanguage === "ar" ? "rtl" : "ltr"}
|
||
pattern={textPattern}
|
||
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">
|
||
{dict.lastName} <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
required
|
||
maxLength={50}
|
||
value={lastName}
|
||
onChange={(e) => setLastName(e.target.value)}
|
||
placeholder={dict.lastNamePlaceholder}
|
||
dir={preferredLanguage === "ar" ? "rtl" : "ltr"}
|
||
pattern={textPattern}
|
||
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">
|
||
{dict.company} <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
required
|
||
minLength={2}
|
||
maxLength={120}
|
||
value={companyName}
|
||
onChange={(e) => setCompanyName(e.target.value)}
|
||
placeholder={dict.companyPlaceholder}
|
||
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">
|
||
{dict.email} <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="email"
|
||
required
|
||
maxLength={254}
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
placeholder={dict.emailPlaceholder}
|
||
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">
|
||
{dict.languageLabel} <span className="text-red-500">*</span>
|
||
</label>
|
||
<select
|
||
required
|
||
value={preferredLanguage}
|
||
onChange={(e) => setPreferredLanguage(e.target.value as "en" | "fr" | "ar")}
|
||
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100"
|
||
>
|
||
<option value="en">EN</option>
|
||
<option value="fr">FR</option>
|
||
<option value="ar">عربي</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">
|
||
{dict.password} <span className="text-red-500">*</span>
|
||
</label>
|
||
<div className="relative">
|
||
<input
|
||
type={showPassword ? "text" : "password"}
|
||
required
|
||
minLength={8}
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
placeholder={
|
||
"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"
|
||
}
|
||
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 pr-10 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowPassword((v) => !v)}
|
||
className="absolute inset-y-0 right-3 flex items-center text-stone-400 hover:text-stone-700 dark:text-stone-500 dark:hover:text-stone-200"
|
||
tabIndex={-1}
|
||
>
|
||
{showPassword ? (
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
className="h-5 w-5"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
strokeWidth={2}
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||
/>
|
||
</svg>
|
||
) : (
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
className="h-5 w-5"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
strokeWidth={2}
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||
/>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||
/>
|
||
</svg>
|
||
)}
|
||
</button>
|
||
</div>
|
||
<p className="mt-1 text-xs text-stone-400 dark:text-stone-500">
|
||
{dict.passwordHint}
|
||
</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">
|
||
{dict.confirmPassword} <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type={showPassword ? "text" : "password"}
|
||
required
|
||
minLength={8}
|
||
value={confirmPassword}
|
||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||
placeholder={dict.passwordPlaceholder}
|
||
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
|
||
/>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={loading}
|
||
className="inline-flex w-full justify-center rounded-full bg-orange-600 px-6 py-3 text-sm font-semibold text-white transition hover:bg-orange-700 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-orange-500 dark:text-white dark:hover:bg-orange-400"
|
||
>
|
||
{loading ? dict.creating : submitLabel}
|
||
</button>
|
||
</form>
|
||
|
||
<p className="mt-6 text-center text-sm text-stone-500 dark:text-stone-400">
|
||
{dict.alreadyHave}{" "}
|
||
<a
|
||
href={signInHref}
|
||
className="font-semibold text-orange-700 hover:text-orange-800 dark:text-orange-300 dark:hover:text-orange-200"
|
||
>
|
||
{dict.signIn}
|
||
</a>
|
||
</p>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
</PublicShell>
|
||
);
|
||
}
|