fix the contract view and edit

This commit is contained in:
root
2026-05-15 11:34:29 -04:00
committed by Administrator
parent f317fa12dd
commit 593ff202a3
13 changed files with 2331 additions and 381 deletions
@@ -3,6 +3,7 @@
import Link from 'next/link'
import { useState } from 'react'
import { useSearchParams } from 'next/navigation'
import { useDashboardI18n } from '@/components/I18nProvider'
import { apiFetch } from '@/lib/api'
import PublicShell from '@/components/layout/PublicShell'
import { marketplaceUrl } from '@/lib/urls'
@@ -14,9 +15,15 @@ type SignupForm = {
password: string
confirmPassword: string
companyName: string
companyPhone: string
legalForm: string
registrationNumber: string
streetAddress: string
city: string
country: string
zipCode: string
companyPhone: string
fax: string
yearsActive: string
plan: 'STARTER' | 'GROWTH' | 'PRO'
billingPeriod: 'MONTHLY' | 'ANNUAL'
currency: 'MAD' | 'USD' | 'EUR'
@@ -42,7 +49,11 @@ type SignupResponse = {
} | null
}
const LEGAL_FORM_OPTIONS = ['SARL', 'SARL AU', 'SA', 'SAS', 'AUTO_ENTREPRENEUR', 'EI', 'OTHER'] as const
const YEARS_ACTIVE_OPTIONS = ['LESS_THAN_1', 'ONE_TO_FIVE', 'MORE_THAN_5'] as const
export default function SignUpPage() {
const { language } = useDashboardI18n()
const searchParams = useSearchParams()
const initialPlan = normalizePlan(searchParams.get('plan'))
const initialBillingPeriod = normalizeBillingPeriod(searchParams.get('billing'))
@@ -58,23 +69,266 @@ export default function SignUpPage() {
password: '',
confirmPassword: '',
companyName: '',
companyPhone: '',
legalForm: '',
registrationNumber: '',
streetAddress: '',
city: '',
country: '',
zipCode: '',
companyPhone: '',
fax: '',
yearsActive: '',
plan: initialPlan,
billingPeriod: initialBillingPeriod,
currency: initialCurrency,
paymentProvider: 'AMANPAY',
})
const isArabic = language === 'ar'
const dict = {
en: {
brand: 'RentalDriveGo',
pageTitle: 'Launch your rental workspace',
pageSubtitle: 'Create the user account, complete the company information, and launch your workspace.',
steps: ['Account User', 'Company info', 'Plan', 'Verify'],
ownerTitle: 'Account User',
ownerBody: 'Create the primary user account that will manage this company workspace.',
partnerTitle: 'Company info',
partnerBody: 'Complete the company information required to create the workspace.',
planTitle: 'Choose your plan',
planBody: 'Your workspace starts immediately on a 14-day free trial in your preferred billing currency.',
reviewTitle: 'Review and launch',
reviewBody: 'We will create the company workspace immediately and start the 14-day trial with the plan you selected.',
firstName: 'Manager/Owner first name',
lastName: 'Manager/Owner last name',
ownerEmail: 'Manager/Owner email',
password: 'Password',
confirmPassword: 'Confirm password',
passwordHint: 'Use at least 8 characters for your company owner account.',
companyName: 'Company Name',
legalForm: 'Legal form',
registrationNumber: 'Commercial Registry (RC)',
streetAddress: 'Street Address',
city: 'City',
country: 'Country',
zipCode: 'Zip code',
companyPhone: 'Phone',
fax: 'Fax (optional)',
yearsActive: 'Years active',
continue: 'Continue',
back: 'Back',
working: 'Working…',
createWorkspace: 'Create workspace',
workspaceReady: 'Workspace ready',
workspaceReadyBody: 'is now active and the owner email',
enterDashboard: 'Enter dashboard',
signInAnotherDevice: 'Sign in on another device',
reviewWorkspace: 'Workspace',
reviewPlan: 'Plan',
reviewOwnerEmail: 'Owner email',
reviewPhone: 'Phone',
invalidPassword: 'Choose a password with at least 8 characters.',
passwordMismatch: 'Passwords do not match.',
missingRequired: 'Fill in all required fields.',
companyFallback: 'Your company',
couldNotCreate: 'Could not create workspace',
legalFormOptions: {
'': 'Select legal form',
SARL: 'SARL',
'SARL AU': 'SARL AU',
SA: 'SA',
SAS: 'SAS',
AUTO_ENTREPRENEUR: 'Auto-entrepreneur',
EI: 'Sole proprietorship',
OTHER: 'Other',
} as Record<string, string>,
yearsActiveOptions: {
'': 'Select years active',
LESS_THAN_1: 'Less than 1 year',
ONE_TO_FIVE: '1 to 5 years',
MORE_THAN_5: 'More than 5 years',
} as Record<string, string>,
planDescriptions: {
STARTER: 'Core fleet, offers, and bookings.',
GROWTH: 'Featured marketplace placement and more room to scale.',
PRO: 'Full white-label and premium controls.',
} as Record<SignupForm['plan'], string>,
billingPeriodOptions: {
MONTHLY: 'Monthly',
ANNUAL: 'Annual',
} as Record<SignupForm['billingPeriod'], string>,
paymentProviderOptions: {
AMANPAY: 'AmanPay',
PAYPAL: 'PayPal',
} as Record<SignupForm['paymentProvider'], string>,
},
fr: {
brand: 'RentalDriveGo',
pageTitle: 'Lancez votre espace de location',
pageSubtitle: 'Créez le compte utilisateur, complétez les informations de lentreprise et lancez votre espace.',
steps: ['Compte utilisateur', 'Infos entreprise', 'Plan', 'Vérifier'],
ownerTitle: 'Compte utilisateur',
ownerBody: 'Créez le compte utilisateur principal qui gérera cet espace entreprise.',
partnerTitle: 'Infos entreprise',
partnerBody: 'Complétez les informations de lentreprise requises pour créer lespace.',
planTitle: 'Choisissez votre formule',
planBody: 'Votre espace démarre immédiatement avec un essai gratuit de 14 jours dans la devise choisie.',
reviewTitle: 'Vérifier et lancer',
reviewBody: 'Nous allons créer immédiatement lespace entreprise et démarrer lessai de 14 jours avec la formule choisie.',
firstName: 'Prénom du gérant/propriétaire',
lastName: 'Nom du gérant/propriétaire',
ownerEmail: 'E-mail du gérant/propriétaire',
password: 'Mot de passe',
confirmPassword: 'Confirmer le mot de passe',
passwordHint: 'Utilisez au moins 8 caractères pour le compte propriétaire.',
companyName: 'Nom de lentreprise',
legalForm: 'Forme juridique',
registrationNumber: 'Registre du commerce (RC)',
streetAddress: 'Adresse',
city: 'Ville',
country: 'Pays',
zipCode: 'Code postal',
companyPhone: 'Téléphone',
fax: 'Fax (optionnel)',
yearsActive: 'Années dactivité',
continue: 'Continuer',
back: 'Retour',
working: 'Traitement…',
createWorkspace: 'Créer lespace',
workspaceReady: 'Espace prêt',
workspaceReadyBody: 'est maintenant actif et le-mail propriétaire',
enterDashboard: 'Entrer dans le dashboard',
signInAnotherDevice: 'Se connecter sur un autre appareil',
reviewWorkspace: 'Espace',
reviewPlan: 'Formule',
reviewOwnerEmail: 'E-mail propriétaire',
reviewPhone: 'Téléphone',
invalidPassword: 'Choisissez un mot de passe dau moins 8 caractères.',
passwordMismatch: 'Les mots de passe ne correspondent pas.',
missingRequired: 'Renseignez tous les champs obligatoires.',
companyFallback: 'Votre entreprise',
couldNotCreate: 'Impossible de créer lespace',
legalFormOptions: {
'': 'Sélectionner la forme juridique',
SARL: 'SARL',
'SARL AU': 'SARL AU',
SA: 'SA',
SAS: 'SAS',
AUTO_ENTREPRENEUR: 'Auto-entrepreneur',
EI: 'Entreprise individuelle',
OTHER: 'Autre',
} as Record<string, string>,
yearsActiveOptions: {
'': 'Sélectionner les années dactivité',
LESS_THAN_1: 'Moins de 1 an',
ONE_TO_FIVE: '1 an à 5 ans',
MORE_THAN_5: 'Plus > 5 ans',
} as Record<string, string>,
planDescriptions: {
STARTER: 'Flotte, offres et réservations essentielles.',
GROWTH: 'Mise en avant marketplace et plus de marge pour évoluer.',
PRO: 'White-label complet et contrôles premium.',
} as Record<SignupForm['plan'], string>,
billingPeriodOptions: {
MONTHLY: 'Mensuel',
ANNUAL: 'Annuel',
} as Record<SignupForm['billingPeriod'], string>,
paymentProviderOptions: {
AMANPAY: 'AmanPay',
PAYPAL: 'PayPal',
} as Record<SignupForm['paymentProvider'], string>,
},
ar: {
brand: 'RentalDriveGo',
pageTitle: 'أطلق مساحة التأجير الخاصة بك',
pageSubtitle: 'أنشئ حساب المستخدم، وأكمل معلومات الشركة، ثم أطلق مساحة العمل.',
steps: ['حساب المستخدم', 'معلومات الشركة', 'الخطة', 'المراجعة'],
ownerTitle: 'حساب المستخدم',
ownerBody: 'أنشئ حساب المستخدم الرئيسي الذي سيدير مساحة عمل الشركة.',
partnerTitle: 'معلومات الشركة',
partnerBody: 'أكمل معلومات الشركة المطلوبة لإنشاء مساحة العمل.',
planTitle: 'اختر خطتك',
planBody: 'تبدأ المساحة فوراً بفترة تجريبية مجانية لمدة 14 يوماً بالعملة التي تفضلها.',
reviewTitle: 'راجع وأطلق',
reviewBody: 'سننشئ مساحة الشركة فوراً ونبدأ الفترة التجريبية لمدة 14 يوماً بالخطة التي اخترتها.',
firstName: 'الاسم الأول للمدير/المالك',
lastName: 'اسم العائلة للمدير/المالك',
ownerEmail: 'بريد المدير/المالك الإلكتروني',
password: 'كلمة المرور',
confirmPassword: 'تأكيد كلمة المرور',
passwordHint: 'استخدم 8 أحرف على الأقل لحساب مالك الشركة.',
companyName: 'اسم الشركة',
legalForm: 'الشكل القانوني',
registrationNumber: 'السجل التجاري (RC)',
streetAddress: 'عنوان الشارع',
city: 'المدينة',
country: 'الدولة',
zipCode: 'الرمز البريدي',
companyPhone: 'الهاتف',
fax: 'الفاكس (اختياري)',
yearsActive: 'سنوات النشاط',
continue: 'متابعة',
back: 'رجوع',
working: 'جارٍ التنفيذ…',
createWorkspace: 'إنشاء المساحة',
workspaceReady: 'المساحة جاهزة',
workspaceReadyBody: 'أصبحت الآن نشطة وتم تسجيل بريد المالك',
enterDashboard: 'الدخول إلى لوحة التحكم',
signInAnotherDevice: 'تسجيل الدخول على جهاز آخر',
reviewWorkspace: 'المساحة',
reviewPlan: 'الخطة',
reviewOwnerEmail: 'بريد المالك',
reviewPhone: 'الهاتف',
invalidPassword: 'اختر كلمة مرور من 8 أحرف على الأقل.',
passwordMismatch: 'كلمتا المرور غير متطابقتين.',
missingRequired: 'أكمل جميع الحقول الإلزامية.',
companyFallback: 'شركتك',
couldNotCreate: 'تعذر إنشاء المساحة',
legalFormOptions: {
'': 'اختر الشكل القانوني',
SARL: 'شركة ذات مسؤولية محدودة',
'SARL AU': 'شركة ذات مسؤولية محدودة لشخص واحد',
SA: 'شركة مساهمة',
SAS: 'شركة مساهمة مبسطة',
AUTO_ENTREPRENEUR: 'مقاول ذاتي',
EI: 'مؤسسة فردية',
OTHER: 'أخرى',
} as Record<string, string>,
yearsActiveOptions: {
'': 'اختر سنوات النشاط',
LESS_THAN_1: 'أقل من سنة',
ONE_TO_FIVE: 'من سنة إلى 5 سنوات',
MORE_THAN_5: 'أكثر من 5 سنوات',
} as Record<string, string>,
planDescriptions: {
STARTER: 'الأساسيات للأسطول والعروض والحجوزات.',
GROWTH: 'ظهور مميز في السوق ومساحة أكبر للنمو.',
PRO: 'تحكم كامل وواجهة بيضاء متقدمة.',
} as Record<SignupForm['plan'], string>,
billingPeriodOptions: {
MONTHLY: 'شهري',
ANNUAL: 'سنوي',
} as Record<SignupForm['billingPeriod'], string>,
paymentProviderOptions: {
AMANPAY: 'AmanPay',
PAYPAL: 'PayPal',
} as Record<SignupForm['paymentProvider'], string>,
},
}[language]
async function submitSignup() {
if (form.password.length < 8) {
setError('Choose a password with at least 8 characters.')
setError(dict.invalidPassword)
return
}
if (form.password !== form.confirmPassword) {
setError('Passwords do not match.')
setError(dict.passwordMismatch)
return
}
if (!hasRequiredPartnerFields(form)) {
setError(dict.missingRequired)
return
}
@@ -90,9 +344,15 @@ export default function SignUpPage() {
email: form.email,
password: form.password,
companyName: form.companyName,
companyPhone: form.companyPhone || undefined,
city: form.city || undefined,
country: form.country || undefined,
legalForm: form.legalForm,
registrationNumber: form.registrationNumber,
streetAddress: form.streetAddress,
city: form.city,
country: form.country,
zipCode: form.zipCode,
companyPhone: form.companyPhone,
fax: form.fax || undefined,
yearsActive: form.yearsActive,
plan: form.plan,
billingPeriod: form.billingPeriod,
currency: form.currency,
@@ -103,10 +363,10 @@ export default function SignUpPage() {
setCompletedSignup({
companyName: form.companyName,
email: form.email,
emailWarning: getEmailWarning(response.emailDelivery),
emailWarning: getEmailWarning(response.emailDelivery, language),
})
} catch (err) {
setError(getErrorMessage(err, 'Could not create workspace'))
setError(getErrorMessage(err, dict.couldNotCreate))
} finally {
setLoading(false)
}
@@ -117,11 +377,11 @@ export default function SignUpPage() {
<PublicShell>
<main className="flex-1 px-4 py-16">
<div className="mx-auto max-w-2xl rounded-[2rem] border border-slate-200 bg-white p-10 shadow-sm">
<Link href={marketplaceUrl} className="text-xs font-semibold uppercase tracking-[0.24em] text-blue-600">RentalDriveGo</Link>
<h1 className="mt-4 text-4xl font-black tracking-tight text-slate-900">Workspace ready</h1>
<Link href={marketplaceUrl} className="text-xs font-semibold uppercase tracking-[0.24em] text-blue-600">{dict.brand}</Link>
<h1 className="mt-4 text-4xl font-black tracking-tight text-slate-900">{dict.workspaceReady}</h1>
<p className="mt-4 text-base leading-7 text-slate-600">
<span className="font-semibold text-slate-900">{completedSignup.companyName}</span> is now active and the owner email
<span className="font-semibold text-slate-900"> {completedSignup.email}</span> has been registered.
<span className="font-semibold text-slate-900">{completedSignup.companyName}</span> {dict.workspaceReadyBody}
<span className="font-semibold text-slate-900"> {completedSignup.email}</span>.
</p>
{completedSignup.emailWarning ? (
<div className="mt-6 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
@@ -130,10 +390,10 @@ export default function SignUpPage() {
) : null}
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<Link href="/dashboard" className="btn-primary justify-center">
Enter dashboard
{dict.enterDashboard}
</Link>
<Link href="/sign-in" className="btn-secondary justify-center">
Sign in on another device
{dict.signInAnotherDevice}
</Link>
</div>
</div>
@@ -145,15 +405,15 @@ export default function SignUpPage() {
return (
<PublicShell>
<main className="px-4 py-12">
<div className="mx-auto max-w-3xl space-y-8">
<div className="text-center">
<Link href={marketplaceUrl} className="text-xs font-semibold uppercase tracking-[0.24em] text-blue-600">RentalDriveGo</Link>
<h1 className="mt-3 text-5xl font-black tracking-tight text-slate-900">Launch your rental workspace</h1>
<p className="mt-4 text-base leading-7 text-slate-600">Create the owner account and start your 14-day free trial.</p>
<div className="mx-auto max-w-4xl space-y-8">
<div className={`text-center ${isArabic ? 'rtl' : ''}`}>
<Link href={marketplaceUrl} className="text-xs font-semibold uppercase tracking-[0.24em] text-blue-600">{dict.brand}</Link>
<h1 className="mt-3 text-5xl font-black tracking-tight text-slate-900">{dict.pageTitle}</h1>
<p className="mt-4 text-base leading-7 text-slate-600">{dict.pageSubtitle}</p>
</div>
<div className="grid gap-3 sm:grid-cols-4">
{['Account', 'Company', 'Plan', 'Verify'].map((label, index) => (
{dict.steps.map((label, index) => (
<div key={label} className={`rounded-full px-4 py-2 text-center text-sm font-semibold ${index + 1 <= step ? 'bg-slate-900 text-white' : 'border border-slate-200 bg-white text-slate-500'}`}>
{label}
</div>
@@ -165,74 +425,89 @@ export default function SignUpPage() {
{step === 1 ? (
<div className="space-y-5">
<SectionIntro title="Owner account" body="This becomes the primary owner account for the company workspace." />
<SectionIntro title={dict.ownerTitle} body={dict.ownerBody} />
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label="First name" value={form.firstName} onChange={(value) => setForm((current) => ({ ...current, firstName: value }))} />
<LabeledInput label="Last name" value={form.lastName} onChange={(value) => setForm((current) => ({ ...current, lastName: value }))} />
<LabeledInput label={dict.firstName} required maxLength={80} value={form.firstName} onChange={(value) => setForm((current) => ({ ...current, firstName: normalizeTitleCase(value) }))} />
<LabeledInput label={dict.lastName} required maxLength={80} value={form.lastName} onChange={(value) => setForm((current) => ({ ...current, lastName: normalizeTitleCase(value) }))} />
</div>
<LabeledInput label="Email" type="email" value={form.email} onChange={(value) => setForm((current) => ({ ...current, email: value }))} />
<LabeledInput label={dict.ownerEmail} required type="email" maxLength={254} value={form.email} onChange={(value) => setForm((current) => ({ ...current, email: normalizeEmail(value) }))} />
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label="Password" type="password" value={form.password} onChange={(value) => setForm((current) => ({ ...current, password: value }))} />
<LabeledInput label="Confirm password" type="password" value={form.confirmPassword} onChange={(value) => setForm((current) => ({ ...current, confirmPassword: value }))} />
<LabeledInput label={dict.password} required type="password" maxLength={128} value={form.password} onChange={(value) => setForm((current) => ({ ...current, password: value }))} />
<LabeledInput label={dict.confirmPassword} required type="password" maxLength={128} value={form.confirmPassword} onChange={(value) => setForm((current) => ({ ...current, confirmPassword: value }))} />
</div>
<p className="text-xs text-slate-500">Use at least 8 characters for your company owner account.</p>
<p className="text-xs text-slate-500">{dict.passwordHint}</p>
<div className="flex justify-end">
<button type="button" onClick={() => setStep(2)} className="btn-primary">Continue</button>
<button type="button" onClick={() => setStep(2)} className="btn-primary">{dict.continue}</button>
</div>
</div>
) : null}
{step === 2 ? (
<div className="space-y-5">
<SectionIntro title="Company details" body="Well use these details for your trial workspace and public marketplace profile." />
<LabeledInput label="Company name" value={form.companyName} onChange={(value) => setForm((current) => ({ ...current, companyName: value }))} />
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label="Phone" value={form.companyPhone} onChange={(value) => setForm((current) => ({ ...current, companyPhone: value }))} />
<LabeledInput label="Country" value={form.country} onChange={(value) => setForm((current) => ({ ...current, country: value }))} />
<SectionIntro title={dict.partnerTitle} body={dict.partnerBody} />
<div className="space-y-4">
<LabeledInput label={dict.companyName} required maxLength={120} value={form.companyName} onChange={(value) => setForm((current) => ({ ...current, companyName: normalizeTitleCase(value) }))} />
<LabeledSelect label={dict.legalForm} required value={form.legalForm} options={['', ...LEGAL_FORM_OPTIONS]} labels={dict.legalFormOptions} onChange={(value) => setForm((current) => ({ ...current, legalForm: value }))} />
<LabeledInput label={dict.registrationNumber} required maxLength={120} value={form.registrationNumber} onChange={(value) => setForm((current) => ({ ...current, registrationNumber: normalizeTitleCase(value) }))} />
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label={dict.companyPhone} required maxLength={80} value={form.companyPhone} onChange={(value) => setForm((current) => ({ ...current, companyPhone: value }))} />
<LabeledInput label={dict.fax} maxLength={80} value={form.fax} onChange={(value) => setForm((current) => ({ ...current, fax: value }))} />
</div>
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label={dict.streetAddress} required maxLength={200} value={form.streetAddress} onChange={(value) => setForm((current) => ({ ...current, streetAddress: normalizeUpper(value) }))} />
<LabeledInput label={dict.city} required maxLength={120} value={form.city} onChange={(value) => setForm((current) => ({ ...current, city: normalizeUpper(value) }))} />
</div>
<div className="grid gap-4 sm:grid-cols-2">
<LabeledInput label={dict.country} required maxLength={120} value={form.country} onChange={(value) => setForm((current) => ({ ...current, country: normalizeUpper(value) }))} />
<LabeledInput label={dict.zipCode} required maxLength={40} value={form.zipCode} onChange={(value) => setForm((current) => ({ ...current, zipCode: value.toUpperCase() }))} />
</div>
<LabeledSelect label={dict.yearsActive} required value={form.yearsActive} options={['', ...YEARS_ACTIVE_OPTIONS]} labels={dict.yearsActiveOptions} onChange={(value) => setForm((current) => ({ ...current, yearsActive: value }))} />
</div>
<LabeledInput label="City" value={form.city} onChange={(value) => setForm((current) => ({ ...current, city: value }))} />
<NavActions onBack={() => setStep(1)} onNext={() => setStep(3)} />
<NavActions onBack={() => setStep(1)} onNext={() => setStep(3)} nextLabel={dict.continue} backLabel={dict.back} loadingLabel={dict.working} />
</div>
) : null}
{step === 3 ? (
<div className="space-y-5">
<SectionIntro title="Choose your plan" body="Your workspace starts immediately on a 14-day free trial in your preferred billing currency." />
<SectionIntro title={dict.planTitle} body={dict.planBody} />
<div className="grid gap-4 sm:grid-cols-3">
{['STARTER', 'GROWTH', 'PRO'].map((plan) => (
{(['STARTER', 'GROWTH', 'PRO'] as const).map((plan) => (
<button
key={plan}
type="button"
onClick={() => setForm((current) => ({ ...current, plan: plan as SignupForm['plan'] }))}
onClick={() => setForm((current) => ({ ...current, plan }))}
className={`rounded-3xl border p-5 text-left ${form.plan === plan ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-200 bg-white text-slate-900'}`}
>
<p className="text-xs font-semibold uppercase tracking-[0.16em]">{plan}</p>
<p className="mt-3 text-sm opacity-80">{plan === 'STARTER' ? 'Core fleet, offers, and bookings.' : plan === 'GROWTH' ? 'Featured marketplace placement and more room to scale.' : 'Full white-label and premium controls.'}</p>
<p className="mt-3 text-sm opacity-80">{dict.planDescriptions[plan]}</p>
</button>
))}
</div>
<div className="grid gap-4 sm:grid-cols-3">
<LabeledSelect label="Billing period" value={form.billingPeriod} options={['MONTHLY', 'ANNUAL']} onChange={(value) => setForm((current) => ({ ...current, billingPeriod: value as SignupForm['billingPeriod'] }))} />
<LabeledSelect label="Currency" value={form.currency} options={['MAD', 'USD', 'EUR']} onChange={(value) => setForm((current) => ({ ...current, currency: value as SignupForm['currency'] }))} />
<LabeledSelect label="Primary provider" value={form.paymentProvider} options={['AMANPAY', 'PAYPAL']} onChange={(value) => setForm((current) => ({ ...current, paymentProvider: value as SignupForm['paymentProvider'] }))} />
<LabeledSelect label={language === 'fr' ? 'Période de facturation' : language === 'ar' ? 'فترة الفوترة' : 'Billing period'} value={form.billingPeriod} options={['MONTHLY', 'ANNUAL']} labels={dict.billingPeriodOptions} onChange={(value) => setForm((current) => ({ ...current, billingPeriod: value as SignupForm['billingPeriod'] }))} />
<LabeledSelect label={language === 'fr' ? 'Devise' : language === 'ar' ? 'العملة' : 'Currency'} value={form.currency} options={['MAD', 'USD', 'EUR']} onChange={(value) => setForm((current) => ({ ...current, currency: value as SignupForm['currency'] }))} />
<LabeledSelect label={language === 'fr' ? 'Prestataire principal' : language === 'ar' ? 'مزود الدفع الرئيسي' : 'Primary provider'} value={form.paymentProvider} options={['AMANPAY', 'PAYPAL']} labels={dict.paymentProviderOptions} onChange={(value) => setForm((current) => ({ ...current, paymentProvider: value as SignupForm['paymentProvider'] }))} />
</div>
<NavActions onBack={() => setStep(2)} onNext={() => setStep(4)} />
<NavActions onBack={() => setStep(2)} onNext={() => setStep(4)} nextLabel={dict.continue} backLabel={dict.back} loadingLabel={dict.working} />
</div>
) : null}
{step === 4 ? (
<div className="space-y-5">
<SectionIntro title="Review and launch" body="Well create the company workspace immediately and start the 14-day trial with the plan you selected." />
<SectionIntro title={dict.reviewTitle} body={dict.reviewBody} />
<div className="rounded-3xl border border-slate-200 bg-slate-50 p-5 text-sm text-slate-600">
<p><span className="font-semibold text-slate-900">Workspace:</span> {form.companyName || 'Your company'}</p>
<p className="mt-2"><span className="font-semibold text-slate-900">Plan:</span> {form.plan} · {form.billingPeriod} · {form.currency}</p>
<p className="mt-2"><span className="font-semibold text-slate-900">Owner email:</span> {form.email || '—'}</p>
<p><span className="font-semibold text-slate-900">{dict.reviewWorkspace}:</span> {form.companyName || dict.companyFallback}</p>
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewPlan}:</span> {form.plan} · {dict.billingPeriodOptions[form.billingPeriod]} · {form.currency}</p>
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewOwnerEmail}:</span> {form.email || '—'}</p>
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewPhone}:</span> {form.companyPhone || '—'}</p>
</div>
<NavActions
onBack={() => setStep(3)}
onNext={submitSignup}
loading={loading}
nextLabel="Create workspace"
nextLabel={dict.createWorkspace}
backLabel={dict.back}
loadingLabel={dict.working}
/>
</div>
) : null}
@@ -243,6 +518,20 @@ export default function SignUpPage() {
)
}
function hasRequiredPartnerFields(form: SignupForm) {
return [
form.companyName,
form.legalForm,
form.registrationNumber,
form.streetAddress,
form.city,
form.country,
form.zipCode,
form.companyPhone,
form.yearsActive,
].every((value) => value.trim().length > 0)
}
function SectionIntro({ title, body }: { title: string; body: string }) {
return (
<div>
@@ -252,12 +541,16 @@ function SectionIntro({ title, body }: { title: string; body: string }) {
)
}
function getEmailWarning(emailDelivery: SignupResponse['emailDelivery']) {
function getEmailWarning(emailDelivery: SignupResponse['emailDelivery'], language: 'en' | 'fr' | 'ar') {
if (!emailDelivery) return null
if (emailDelivery.success) return null
if (emailDelivery.attempted) {
if (language === 'fr') return `Le compte a été créé, mais le-mail de confirmation na pas pu être envoyé${emailDelivery.error ? ` : ${emailDelivery.error}` : '.'}`
if (language === 'ar') return `تم إنشاء الحساب، لكن تعذر إرسال رسالة التأكيد${emailDelivery.error ? `: ${emailDelivery.error}` : '.'}`
return `The account was created, but the confirmation email could not be delivered${emailDelivery.error ? `: ${emailDelivery.error}` : '.'}`
}
if (language === 'fr') return 'Le compte a été créé, mais lenvoi de-mails nest pas encore configuré sur cet environnement.'
if (language === 'ar') return 'تم إنشاء الحساب، لكن إرسال البريد الإلكتروني غير مهيأ بعد في هذه البيئة.'
return 'The account was created, but email sending is not configured on this environment yet.'
}
@@ -266,38 +559,101 @@ function getErrorMessage(error: unknown, fallback: string) {
return fallback
}
function LabeledInput({ label, value, onChange, type = 'text' }: { label: string; value: string; onChange: (value: string) => void; type?: string }) {
function normalizeTitleCase(value: string) {
if (!value) return value
const trimmedStart = value.replace(/^\s+/, '')
if (!trimmedStart) return ''
return trimmedStart.charAt(0).toUpperCase() + trimmedStart.slice(1).toLowerCase()
}
function normalizeEmail(value: string) {
return value.replace(/\s+/g, '').toLowerCase()
}
function normalizeUpper(value: string) {
return value.replace(/^\s+/, '').toUpperCase()
}
function LabeledInput({
label,
value,
onChange,
type = 'text',
required = false,
maxLength,
}: {
label: string
value: string
onChange: (value: string) => void
type?: string
required?: boolean
maxLength?: number
}) {
return (
<label className="block">
<span className="mb-1.5 block text-sm font-medium text-slate-700">{label}</span>
<input type={type} className="input-field" value={value} onChange={(event) => onChange(event.target.value)} />
<span className="mb-1.5 block text-sm font-medium text-slate-700">
{label}
{required ? <span className="ml-1 text-red-600">*</span> : null}
</span>
<input type={type} maxLength={maxLength} className="input-field" value={value} onChange={(event) => onChange(event.target.value)} />
</label>
)
}
function LabeledSelect({ label, value, options, onChange }: { label: string; value: string; options: string[]; onChange: (value: string) => void }) {
function LabeledSelect({
label,
value,
options,
onChange,
labels,
required = false,
}: {
label: string
value: string
options: readonly string[]
onChange: (value: string) => void
labels?: Record<string, string>
required?: boolean
}) {
return (
<label className="block">
<span className="mb-1.5 block text-sm font-medium text-slate-700">{label}</span>
<span className="mb-1.5 block text-sm font-medium text-slate-700">
{label}
{required ? <span className="ml-1 text-red-600">*</span> : null}
</span>
<select className="input-field" value={value} onChange={(event) => onChange(event.target.value)}>
{options.map((option) => (
<option key={option} value={option}>{option}</option>
<option key={option || 'empty'} value={option}>{labels?.[option] ?? option}</option>
))}
</select>
</label>
)
}
function NavActions({ onBack, onNext, loading = false, nextLabel = 'Continue' }: { onBack?: () => void; onNext: () => void; loading?: boolean; nextLabel?: string }) {
function NavActions({
onBack,
onNext,
loading = false,
nextLabel = 'Continue',
backLabel = 'Back',
loadingLabel = 'Working…',
}: {
onBack?: () => void
onNext: () => void
loading?: boolean
nextLabel?: string
backLabel?: string
loadingLabel?: string
}) {
return (
<div className="flex gap-3">
{onBack ? (
<button type="button" onClick={onBack} className="btn-secondary flex-1 justify-center">
Back
{backLabel}
</button>
) : null}
<button type="button" onClick={onNext} disabled={loading} className="btn-primary flex-1 justify-center disabled:cursor-not-allowed disabled:opacity-60">
{loading ? 'Working…' : nextLabel}
{loading ? loadingLabel : nextLabel}
</button>
</div>
)