Inscription self-service et quota IA exprimé en questions
- Parcours d'inscription /[lang]/signup. - Quota IA : « req/mois » remplacé par « questions de visiteurs / mois » en 4 langues — le backend compte des tokens, pas des requêtes, et la vitrine annonçait encore l'ancienne unité. - .env.production suivi volontairement : il ne contient qu'une URL publique (préfixe NEXT_PUBLIC_), d'où l'exception dans .gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4849249c55
commit
d91cf7f02c
1
.env.production
Normal file
1
.env.production
Normal file
@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_MANAGER_SERVICE_URL=https://api.myinfomate.be
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -32,6 +32,7 @@ yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.production
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
@ -479,9 +479,6 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-6 items-stretch pt-6">
|
||||
<Reveal delay={0}>
|
||||
<div className="flex flex-col h-full bg-white rounded-3xl border border-slate-200 p-8 shadow-sm hover:shadow-md transition-shadow relative">
|
||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
|
||||
<span className="bg-slate-800 text-white text-xs font-extrabold px-4 py-1.5 rounded-full uppercase tracking-wider whitespace-nowrap">{t.pricing.comingSoon}</span>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<p className="text-sm font-bold text-slate-400 uppercase tracking-widest mb-2">Essentiel</p>
|
||||
<span className="inline-flex items-center gap-1 text-xs font-semibold px-2.5 py-1 rounded-full bg-slate-100 text-slate-500 mb-3">
|
||||
@ -516,13 +513,14 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<a href="#contact" className="block w-full py-3 rounded-2xl border-2 border-slate-200 text-slate-700 font-bold text-sm hover:border-primary hover:text-primary transition-all text-center">
|
||||
{t.pricing.ctaStart}
|
||||
<a href={`/${lang}/signup`} className="block w-full py-3 rounded-2xl border-2 border-slate-200 text-slate-700 font-bold text-sm hover:border-primary hover:text-primary transition-all text-center">
|
||||
{t.pricing.ctaStartTrial}
|
||||
</a>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={60}>
|
||||
<div className="flex flex-col h-full bg-white rounded-3xl border-2 border-primary p-8 shadow-xl shadow-primary/10 relative">
|
||||
<div className="animated-card-border relative p-[2px] rounded-3xl shadow-xl shadow-primary/10 h-full">
|
||||
<div className="flex flex-col h-full bg-white rounded-[22px] p-8 relative">
|
||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
|
||||
<span className="bg-primary text-slate-900 text-xs font-extrabold px-4 py-1.5 rounded-full uppercase tracking-wider">{t.pricing.recommended}</span>
|
||||
</div>
|
||||
@ -571,6 +569,7 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
{t.pricing.ctaStart}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={120}>
|
||||
<div className="flex flex-col h-full bg-gradient-to-br from-slate-900 to-slate-800 rounded-3xl p-8 shadow-xl">
|
||||
@ -611,7 +610,7 @@ export default function Home({ lang }: { lang: Language }) {
|
||||
<li className="flex items-start gap-3 text-sm text-slate-300">
|
||||
<span className="material-symbols-outlined text-primary text-base shrink-0 mt-0.5">check_circle</span>
|
||||
<span>
|
||||
{t.pricing.features.ai} — 2 000 {t.pricing.features.reqPerMonth}
|
||||
{t.pricing.features.ai} — ≈ 2 000 {t.pricing.features.questionsPerMonth}
|
||||
<span className="block text-xs text-slate-500 mt-0.5">{t.pricing.features.aiNeedMore} → Enterprise</span>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@ -404,9 +404,6 @@ export default function SegmentPageClient({ data, lang }: { data: Segment; lang:
|
||||
{/* Essentiel */}
|
||||
<Reveal delay={0}>
|
||||
<div className="flex flex-col h-full bg-white rounded-3xl border border-slate-200 p-8 shadow-sm hover:shadow-md transition-shadow relative">
|
||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
|
||||
<span className="bg-slate-800 text-white text-xs font-extrabold px-4 py-1.5 rounded-full uppercase tracking-wider whitespace-nowrap">{t.pricing.comingSoon}</span>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<p className="text-sm font-bold text-slate-400 uppercase tracking-widest mb-2">Essentiel</p>
|
||||
<span className="inline-flex items-center gap-1 text-xs font-semibold px-2.5 py-1 rounded-full bg-slate-100 text-slate-500 mb-3">
|
||||
@ -441,9 +438,9 @@ export default function SegmentPageClient({ data, lang }: { data: Segment; lang:
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button onClick={scrollToContact} className="w-full py-3 rounded-2xl border-2 border-slate-200 text-slate-700 font-bold text-sm hover:border-primary hover:text-primary transition-all">
|
||||
{t.pricing.ctaStart}
|
||||
</button>
|
||||
<a href={`/${lang}/signup`} className="block w-full py-3 rounded-2xl border-2 border-slate-200 text-slate-700 font-bold text-sm hover:border-primary hover:text-primary transition-all text-center">
|
||||
{t.pricing.ctaStartTrial}
|
||||
</a>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
@ -540,7 +537,7 @@ export default function SegmentPageClient({ data, lang }: { data: Segment; lang:
|
||||
<li className="flex items-start gap-3 text-sm text-slate-300">
|
||||
<span className="material-symbols-outlined text-primary text-base shrink-0 mt-0.5">check_circle</span>
|
||||
<span>
|
||||
{t.pricing.features.ai} — 2 000 {t.pricing.features.reqPerMonth}
|
||||
{t.pricing.features.ai} — ≈ 2 000 {t.pricing.features.questionsPerMonth}
|
||||
<span className="block text-xs text-slate-500 mt-0.5">{t.pricing.features.aiNeedMore} → Enterprise</span>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
463
src/app/[lang]/signup/SignupClient.tsx
Normal file
463
src/app/[lang]/signup/SignupClient.tsx
Normal file
@ -0,0 +1,463 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import translations, { Language } from '@/data/translations';
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_MANAGER_SERVICE_URL ?? '';
|
||||
|
||||
const COUNTRIES = [
|
||||
{ code: 'BE', label: { fr: 'Belgique', en: 'Belgium', nl: 'België', de: 'Belgien' } },
|
||||
{ code: 'FR', label: { fr: 'France', en: 'France', nl: 'Frankrijk', de: 'Frankreich' } },
|
||||
{ code: 'LU', label: { fr: 'Luxembourg', en: 'Luxembourg', nl: 'Luxemburg', de: 'Luxemburg' } },
|
||||
{ code: 'NL', label: { fr: 'Pays-Bas', en: 'Netherlands', nl: 'Nederland', de: 'Niederlande' } },
|
||||
{ code: 'DE', label: { fr: 'Allemagne', en: 'Germany', nl: 'Duitsland', de: 'Deutschland' } },
|
||||
{ code: 'CH', label: { fr: 'Suisse (hors UE)', en: 'Switzerland (non-EU)', nl: 'Zwitserland (niet-EU)', de: 'Schweiz (nicht EU)' } },
|
||||
];
|
||||
|
||||
type SlugStatus = 'idle' | 'checking' | 'available' | 'taken' | 'short';
|
||||
type VatStatus = { kind: 'idle' | 'checking' | 'zero' | 'std' | 'err' | 'unavailable'; message: string };
|
||||
type FormState = 'idle' | 'loading' | 'success' | 'error';
|
||||
|
||||
function sanitizeSlug(value: string) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-/, '');
|
||||
}
|
||||
|
||||
export default function SignupClient({ lang }: { lang: Language }) {
|
||||
const t = translations[lang].signup;
|
||||
|
||||
const [orgName, setOrgName] = useState('');
|
||||
const [slug, setSlug] = useState('');
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [address, setAddress] = useState('');
|
||||
const [zip, setZip] = useState('');
|
||||
const [city, setCity] = useState('');
|
||||
const [country, setCountry] = useState('BE');
|
||||
const [vat, setVat] = useState('');
|
||||
const [consent, setConsent] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const [slugStatus, setSlugStatus] = useState<SlugStatus>('idle');
|
||||
const [vatStatus, setVatStatus] = useState<VatStatus>({ kind: 'idle', message: '' });
|
||||
const [formState, setFormState] = useState<FormState>('idle');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [result, setResult] = useState<{ webUrl: string; managerAppUrl: string; trialEndsAt: string } | null>(null);
|
||||
|
||||
const slugTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const vatTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (slugTimer.current) clearTimeout(slugTimer.current);
|
||||
const clean = sanitizeSlug(slug);
|
||||
if (!clean) {
|
||||
setSlugStatus('idle');
|
||||
return;
|
||||
}
|
||||
if (clean.length < 3) {
|
||||
setSlugStatus('short');
|
||||
return;
|
||||
}
|
||||
setSlugStatus('checking');
|
||||
slugTimer.current = setTimeout(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/onboarding/check-slug/${clean}`);
|
||||
const data = await res.json();
|
||||
setSlugStatus(data.available ? 'available' : 'taken');
|
||||
} catch {
|
||||
setSlugStatus('idle');
|
||||
}
|
||||
}, 600);
|
||||
return () => { if (slugTimer.current) clearTimeout(slugTimer.current); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [slug]);
|
||||
|
||||
useEffect(() => {
|
||||
if (vatTimer.current) clearTimeout(vatTimer.current);
|
||||
|
||||
if (country === 'CH') {
|
||||
setVatStatus({ kind: 'zero', message: t.vatZero });
|
||||
return;
|
||||
}
|
||||
if (!vat.trim()) {
|
||||
setVatStatus({ kind: 'std', message: `${t.vatNoNumber} — ${t.vatNoNumberHint}` });
|
||||
return;
|
||||
}
|
||||
|
||||
setVatStatus({ kind: 'checking', message: t.vatChecking });
|
||||
vatTimer.current = setTimeout(async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${API_BASE}/api/onboarding/validate-vat?country=${encodeURIComponent(country)}&vatNumber=${encodeURIComponent(vat)}`,
|
||||
{ method: 'POST' }
|
||||
);
|
||||
const data = await res.json();
|
||||
if (data.valid === null) {
|
||||
setVatStatus({ kind: 'unavailable', message: t.vatUnavailable });
|
||||
} else if (data.valid === false) {
|
||||
setVatStatus({ kind: 'err', message: t.vatInvalid });
|
||||
} else {
|
||||
setVatStatus({ kind: data.vatRate === 0 ? 'zero' : 'std', message: data.vatRate === 0 ? t.vatZero : t.vatDomestic });
|
||||
}
|
||||
} catch {
|
||||
setVatStatus({ kind: 'unavailable', message: t.vatUnavailable });
|
||||
}
|
||||
}, 500);
|
||||
return () => { if (vatTimer.current) clearTimeout(vatTimer.current); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [vat, country]);
|
||||
|
||||
const trialEndDateLabel = () => {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() + 14);
|
||||
return d.toLocaleDateString(lang === 'fr' ? 'fr-BE' : lang === 'nl' ? 'nl-BE' : lang === 'de' ? 'de-DE' : 'en-GB', {
|
||||
day: 'numeric', month: 'long',
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const clean = sanitizeSlug(slug);
|
||||
|
||||
if (!orgName.trim() || !firstName.trim() || !lastName.trim() || !email.trim() || password.length < 8 || !clean || !consent) {
|
||||
return;
|
||||
}
|
||||
if (slugStatus === 'taken') return;
|
||||
|
||||
setFormState('loading');
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/onboarding/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
organizationName: orgName.trim(),
|
||||
firstName: firstName.trim(),
|
||||
lastName: lastName.trim(),
|
||||
email: email.trim(),
|
||||
password,
|
||||
slug: clean,
|
||||
billingAddress: [address, zip, city].filter(Boolean).join(', ') || null,
|
||||
billingCountry: country,
|
||||
vatNumber: vat.trim() || null,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.status === 409) {
|
||||
setSlugStatus('taken');
|
||||
setFormState('idle');
|
||||
return;
|
||||
}
|
||||
if (!res.ok) throw new Error('registration failed');
|
||||
|
||||
const data = await res.json();
|
||||
setResult(data);
|
||||
setFormState('success');
|
||||
} catch {
|
||||
setErrorMessage(t.errorGeneric);
|
||||
setFormState('error');
|
||||
}
|
||||
};
|
||||
|
||||
if (formState === 'success' && result) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#e9eef5] flex items-center justify-center p-4 sm:p-6">
|
||||
<div className="w-full max-w-md bg-white rounded-3xl shadow-2xl p-10 text-center">
|
||||
<div className="w-20 h-20 bg-emerald-50 text-emerald-500 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<span className="material-symbols-outlined text-5xl">task_alt</span>
|
||||
</div>
|
||||
<h2 className="text-2xl font-extrabold text-slate-900 mb-2">{t.successTitle}</h2>
|
||||
<p className="text-slate-500 mb-6">{t.successDesc}</p>
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-2xl divide-y divide-slate-200 mb-6 text-left">
|
||||
<div className="p-4">
|
||||
<div className="text-xs text-slate-400 font-semibold">{t.recapUrlLabel}</div>
|
||||
<div className="text-sm font-bold text-primary">{result.webUrl}</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="text-xs text-slate-400 font-semibold">{t.recapTrialLabel}</div>
|
||||
<div className="text-sm font-bold text-slate-900">{trialEndDateLabel()}</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="text-xs text-slate-400 font-semibold">{t.recapEmailLabel}</div>
|
||||
<div className="text-sm font-bold text-slate-900">{email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={result.managerAppUrl}
|
||||
className="block w-full py-4 rounded-2xl bg-slate-900 text-white font-extrabold text-sm hover:bg-slate-800 transition-all"
|
||||
>
|
||||
{t.goToApp}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const missing = !orgName.trim() || !firstName.trim() || !lastName.trim() || !email.trim() || password.length < 8 || !sanitizeSlug(slug) || !consent;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#e9eef5] p-4 sm:p-6 lg:p-10 flex items-center justify-center">
|
||||
<div className="w-full max-w-6xl grid grid-cols-1 lg:grid-cols-[0.85fr_1fr] bg-white rounded-[2rem] overflow-hidden shadow-2xl">
|
||||
|
||||
{/* Brand panel — always dark, a deliberate brand block independent of page theme */}
|
||||
<div className="relative isolate overflow-hidden bg-[#0a1222] text-white p-10 lg:p-11 flex flex-col gap-7">
|
||||
<div className="absolute -z-10 w-[480px] h-[480px] -top-40 -right-48 rounded-full bg-primary/40 blur-3xl animate-float" />
|
||||
<div className="absolute -z-10 w-[420px] h-[420px] -bottom-44 -left-40 rounded-full bg-accent-violet/40 blur-3xl animate-float" style={{ animationDelay: '2s' }} />
|
||||
|
||||
<a href={`/${lang}`} className="flex items-center gap-2.5 font-extrabold text-lg">
|
||||
<span className="w-8 h-8 rounded-[10px] bg-gradient-to-br from-primary via-accent-violet to-accent-orange flex items-center justify-center shadow-lg shadow-primary/30">
|
||||
<span className="w-2.5 h-2.5 rounded-[4px] bg-[#0a1222]" />
|
||||
</span>
|
||||
MyInfoMate
|
||||
</a>
|
||||
|
||||
<span className="inline-flex items-center gap-2 self-start bg-primary/10 border border-primary/30 text-primary text-xs font-bold uppercase tracking-wider px-3.5 py-1.5 rounded-full">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '14px' }}>stars</span>
|
||||
{t.planBadge}
|
||||
</span>
|
||||
|
||||
<h1 className="text-3xl lg:text-4xl font-extrabold leading-tight text-balance">
|
||||
<span className="gradient-text">{t.heroTitle}</span>
|
||||
</h1>
|
||||
|
||||
<div className="flex items-baseline gap-2 -mt-2">
|
||||
<span className="text-4xl font-extrabold">39 €</span>
|
||||
<span className="text-slate-400 text-sm font-semibold">{t.priceSuffix}</span>
|
||||
</div>
|
||||
|
||||
<span className="inline-flex items-center gap-2 self-start bg-emerald-500/15 border border-emerald-400/30 text-emerald-300 text-sm font-bold px-4 py-2 rounded-xl">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '18px' }}>check_circle</span>
|
||||
{t.trialBadge}
|
||||
</span>
|
||||
|
||||
<ul className="flex flex-col gap-3 mt-1">
|
||||
{[t.feature1, t.feature2, t.feature3, t.feature4].map((f) => (
|
||||
<li key={f} className="flex items-start gap-3 text-sm text-slate-200">
|
||||
<span className="flex-none w-5 h-5 rounded-md bg-primary/15 text-primary flex items-center justify-center mt-0.5">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '13px' }}>check</span>
|
||||
</span>
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="mt-auto pt-6 border-t border-white/10 flex flex-col gap-4">
|
||||
<p className="text-xs text-slate-400 leading-relaxed flex gap-2">
|
||||
<span className="material-symbols-outlined flex-none" style={{ fontSize: '15px' }}>info</span>
|
||||
{t.watermarkNote}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2 text-xs font-semibold text-slate-400">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '13px' }}>shield</span>
|
||||
{t.trustHosting}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '13px' }}>close</span>
|
||||
{t.trustCancel}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '13px' }}>support_agent</span>
|
||||
{t.trustSupport}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form panel — always light */}
|
||||
<div className="p-8 sm:p-10 lg:p-12">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-2xl font-extrabold text-slate-900">{t.formTitle}</h2>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
{t.alreadyAccount}{' '}
|
||||
<a href="https://manager.myinfomate.be" className="text-primary font-bold hover:underline">{t.loginLink}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<fieldset className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[11px] font-extrabold uppercase tracking-wider text-slate-400">{t.sectionOrg}</span>
|
||||
<span className="flex-1 h-px bg-slate-200" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.orgNameLabel}</label>
|
||||
<input
|
||||
type="text" required value={orgName} onChange={(e) => setOrgName(e.target.value)}
|
||||
placeholder={t.orgNamePlaceholder}
|
||||
className="w-full px-5 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.slugLabel}</label>
|
||||
<div className={`flex items-stretch border rounded-2xl bg-slate-50 overflow-hidden transition-all ${slugStatus === 'checking' ? 'border-slate-200' : slugStatus === 'available' ? 'border-emerald-400 ring-2 ring-emerald-100' : slugStatus === 'taken' || slugStatus === 'short' ? 'border-red-300 ring-2 ring-red-50' : 'border-slate-200'}`}>
|
||||
<span className="flex items-center pl-4 pr-1 text-sm text-slate-500 font-semibold whitespace-nowrap">{t.slugPrefix}</span>
|
||||
<input
|
||||
type="text" required value={slug} spellCheck={false}
|
||||
onChange={(e) => setSlug(sanitizeSlug(e.target.value))}
|
||||
placeholder={t.slugPlaceholder}
|
||||
className="flex-1 min-w-0 px-1 py-3.5 bg-transparent outline-none font-bold text-slate-900"
|
||||
/>
|
||||
<span className="flex items-center px-3">
|
||||
{slugStatus === 'checking' && <span className="w-4 h-4 rounded-full border-2 border-slate-300 border-t-primary animate-spin" />}
|
||||
{slugStatus === 'available' && <span className="material-symbols-outlined text-emerald-500" style={{ fontSize: '20px' }}>check_circle</span>}
|
||||
{(slugStatus === 'taken' || slugStatus === 'short') && <span className="material-symbols-outlined text-red-500" style={{ fontSize: '20px' }}>cancel</span>}
|
||||
</span>
|
||||
</div>
|
||||
<p className={`text-xs font-medium ml-1 ${slugStatus === 'available' ? 'text-emerald-600' : slugStatus === 'taken' || slugStatus === 'short' ? 'text-red-500' : 'text-slate-400'}`}>
|
||||
{slugStatus === 'checking' && t.slugHintChecking}
|
||||
{slugStatus === 'available' && `${t.slugHintAvailable} app.myinfomate.be/${sanitizeSlug(slug)}`}
|
||||
{slugStatus === 'taken' && t.slugHintTaken}
|
||||
{slugStatus === 'short' && t.slugHintTooShort}
|
||||
{slugStatus === 'idle' && t.slugHintDefault}
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[11px] font-extrabold uppercase tracking-wider text-slate-400">{t.sectionContact}</span>
|
||||
<span className="flex-1 h-px bg-slate-200" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.firstNameLabel}</label>
|
||||
<input type="text" required value={firstName} onChange={(e) => setFirstName(e.target.value)} placeholder={t.firstNamePlaceholder}
|
||||
className="w-full px-5 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.lastNameLabel}</label>
|
||||
<input type="text" required value={lastName} onChange={(e) => setLastName(e.target.value)} placeholder={t.lastNamePlaceholder}
|
||||
className="w-full px-5 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.emailLabel}</label>
|
||||
<input type="email" required value={email} onChange={(e) => setEmail(e.target.value)} placeholder={t.emailPlaceholder}
|
||||
className="w-full px-5 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.passwordLabel}</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'} required value={password} onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t.passwordPlaceholder}
|
||||
className="w-full px-5 py-3.5 pr-12 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all"
|
||||
/>
|
||||
<button type="button" onClick={() => setShowPassword((v) => !v)} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 p-1.5">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '20px' }}>{showPassword ? 'visibility_off' : 'visibility'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[11px] font-extrabold uppercase tracking-wider text-slate-400">{t.sectionBilling}</span>
|
||||
<span className="flex-1 h-px bg-slate-200" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.addressLabel}</label>
|
||||
<input type="text" value={address} onChange={(e) => setAddress(e.target.value)} placeholder={t.addressPlaceholder}
|
||||
className="w-full px-5 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.zipLabel}</label>
|
||||
<input type="text" value={zip} onChange={(e) => setZip(e.target.value)} placeholder={t.zipPlaceholder}
|
||||
className="w-full px-5 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.cityLabel}</label>
|
||||
<input type="text" value={city} onChange={(e) => setCity(e.target.value)} placeholder={t.cityPlaceholder}
|
||||
className="w-full px-5 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.countryLabel}</label>
|
||||
<select value={country} onChange={(e) => setCountry(e.target.value)}
|
||||
className="w-full px-5 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all appearance-none cursor-pointer">
|
||||
{COUNTRIES.map((c) => <option key={c.code} value={c.code}>{c.label[lang]}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-bold text-slate-700 ml-1">{t.vatLabel} <span className="text-slate-400 font-medium">— {t.vatOptional}</span></label>
|
||||
<input type="text" value={vat} spellCheck={false} onChange={(e) => setVat(e.target.value)} placeholder={t.vatPlaceholder}
|
||||
className="w-full px-5 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all" />
|
||||
</div>
|
||||
</div>
|
||||
{vatStatus.kind !== 'idle' && (
|
||||
<div className={`flex items-center gap-2.5 px-4 py-3 rounded-xl text-sm font-semibold ${
|
||||
vatStatus.kind === 'zero' ? 'bg-emerald-50 text-emerald-700' :
|
||||
vatStatus.kind === 'err' ? 'bg-red-50 text-red-600' :
|
||||
vatStatus.kind === 'unavailable' ? 'bg-amber-50 text-amber-700' :
|
||||
'bg-primary/10 text-slate-700'
|
||||
}`}>
|
||||
{vatStatus.kind === 'checking'
|
||||
? <span className="w-4 h-4 rounded-full border-2 border-slate-300 border-t-primary animate-spin flex-none" />
|
||||
: <span className="material-symbols-outlined flex-none" style={{ fontSize: '18px' }}>
|
||||
{vatStatus.kind === 'zero' ? 'check_circle' : vatStatus.kind === 'err' ? 'error' : vatStatus.kind === 'unavailable' ? 'warning' : 'info'}
|
||||
</span>}
|
||||
{vatStatus.message}
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<label className="flex items-start gap-3 text-sm text-slate-500">
|
||||
<input type="checkbox" required checked={consent} onChange={(e) => setConsent(e.target.checked)}
|
||||
className="w-5 h-5 mt-0.5 flex-none accent-primary rounded cursor-pointer" />
|
||||
<span>
|
||||
{t.consentPrefix}{' '}
|
||||
<a href={`/${lang}#`} className="text-primary font-bold hover:underline">{t.consentTerms}</a>{' '}
|
||||
{t.consentAnd}{' '}
|
||||
<a href="/confidentialite" className="text-primary font-bold hover:underline">{t.consentPrivacy}</a>{' '}
|
||||
{t.consentSuffix}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{formState === 'error' && (
|
||||
<div className="p-4 bg-red-50 text-red-600 rounded-xl text-sm font-medium border border-red-100">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{slugStatus === 'taken' && (
|
||||
<div className="p-4 bg-red-50 text-red-600 rounded-xl text-sm font-medium border border-red-100">
|
||||
{t.errorSlugTaken}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={formState === 'loading' || missing}
|
||||
className={`w-full py-4 bg-primary text-slate-900 font-extrabold rounded-2xl shadow-xl shadow-primary/25 hover:brightness-105 transition-all flex items-center justify-center gap-2.5 ${
|
||||
formState === 'loading' || missing ? 'opacity-60 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
{formState === 'loading' ? (
|
||||
<>
|
||||
<span className="w-5 h-5 border-2 border-slate-900/30 border-t-slate-900 rounded-full animate-spin" />
|
||||
{t.submitting}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t.submitCta}
|
||||
<span className="material-symbols-outlined">arrow_forward</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<p className="text-center text-xs text-slate-400 flex items-center justify-center gap-1.5">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '14px' }}>lock</span>
|
||||
{t.noCardNote}
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
src/app/[lang]/signup/page.tsx
Normal file
54
src/app/[lang]/signup/page.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { LOCALES, LOCALE_HTML_LANG, DEFAULT_LOCALE, isLocale, type Locale } from '@/i18n';
|
||||
import SignupClient from './SignupClient';
|
||||
|
||||
const META_BY_LOCALE: Record<Locale, { title: string; description: string }> = {
|
||||
fr: {
|
||||
title: 'Créer mon compte — Plan Essentiel | MyInfoMate',
|
||||
description: "Démarrez votre essai gratuit de 14 jours sur MyInfoMate, sans carte bancaire. Votre app visiteur web en quelques minutes.",
|
||||
},
|
||||
en: {
|
||||
title: 'Create my account — Essentiel plan | MyInfoMate',
|
||||
description: 'Start your 14-day free trial on MyInfoMate, no card required. Your web visitor app in minutes.',
|
||||
},
|
||||
nl: {
|
||||
title: 'Mijn account aanmaken — Plan Essentieel | MyInfoMate',
|
||||
description: 'Start uw gratis proefperiode van 14 dagen op MyInfoMate, zonder kredietkaart. Uw webbezoekersapp in enkele minuten.',
|
||||
},
|
||||
de: {
|
||||
title: 'Konto erstellen — Essentiel-Plan | MyInfoMate',
|
||||
description: 'Starten Sie Ihre 14-tägige kostenlose Testphase bei MyInfoMate, ohne Kreditkarte. Ihre Web-Besucher-App in wenigen Minuten.',
|
||||
},
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return LOCALES.map((lang) => ({ lang }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ lang: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { lang } = await params;
|
||||
if (!isLocale(lang)) return {};
|
||||
const m = META_BY_LOCALE[lang];
|
||||
const languages = Object.fromEntries(LOCALES.map((l) => [LOCALE_HTML_LANG[l], `/${l}/signup`]));
|
||||
return {
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
robots: { index: false, follow: true },
|
||||
alternates: {
|
||||
canonical: `/${lang}/signup`,
|
||||
languages: { ...languages, 'x-default': `/${DEFAULT_LOCALE}/signup` },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function SignupPage({ params }: { params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params;
|
||||
if (!isLocale(lang)) notFound();
|
||||
|
||||
return <SignupClient lang={lang} />;
|
||||
}
|
||||
@ -69,6 +69,26 @@
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@property --border-angle {
|
||||
syntax: '<angle>';
|
||||
inherits: false;
|
||||
initial-value: 0deg;
|
||||
}
|
||||
|
||||
@keyframes spin-border {
|
||||
to { --border-angle: 360deg; }
|
||||
}
|
||||
|
||||
@utility animated-card-border {
|
||||
background: conic-gradient(
|
||||
from var(--border-angle),
|
||||
transparent 20%,
|
||||
#0df2df 50%,
|
||||
transparent 80%
|
||||
);
|
||||
animation: spin-border 4s linear infinite;
|
||||
}
|
||||
|
||||
@utility scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
@ -194,6 +194,77 @@ const translations = {
|
||||
messagePlaceholder: 'Parlez-nous de votre projet...',
|
||||
submitButton: 'Envoyer ma demande',
|
||||
},
|
||||
signup: {
|
||||
planBadge: 'Plan Essentiel',
|
||||
heroTitle: 'Votre lieu culturel, en ligne en quelques minutes.',
|
||||
priceSuffix: '/ mois HTVA',
|
||||
trialBadge: "14 jours d'essai gratuit — sans carte bancaire",
|
||||
feature1: 'App visiteur web, ouverte par QR code — aucune installation',
|
||||
feature2: 'Contenus, agenda, plans et sections illimités',
|
||||
feature3: 'Multilingue — vos visiteurs choisissent leur langue',
|
||||
feature4: 'Vos couleurs et votre logo, votre adresse dédiée',
|
||||
watermarkNote: "Pendant l'essai, vos pages visiteur affichent un léger filigrane « Aperçu ». Il disparaît dès l'activation de votre abonnement.",
|
||||
trustHosting: 'Hébergé en Europe',
|
||||
trustCancel: 'Résiliable à tout moment',
|
||||
trustSupport: 'Support en français',
|
||||
formTitle: 'Créer mon compte',
|
||||
alreadyAccount: 'Vous avez déjà un espace ?',
|
||||
loginLink: 'Se connecter',
|
||||
sectionOrg: 'Votre organisation',
|
||||
orgNameLabel: "Nom de l'organisation",
|
||||
orgNamePlaceholder: 'Musée des Beaux-Arts',
|
||||
slugLabel: 'Votre adresse dédiée',
|
||||
slugPrefix: 'app.myinfomate.be/',
|
||||
slugPlaceholder: 'votre-musee',
|
||||
slugHintDefault: "Lettres minuscules, chiffres et tirets — c'est l'URL que vos visiteurs scanneront.",
|
||||
slugHintChecking: 'Vérification de la disponibilité…',
|
||||
slugHintTaken: 'Cette adresse est déjà utilisée — essayez un autre nom.',
|
||||
slugHintAvailable: 'Disponible !',
|
||||
slugHintTooShort: 'Au moins 3 caractères.',
|
||||
sectionContact: 'Vos coordonnées',
|
||||
firstNameLabel: 'Prénom',
|
||||
firstNamePlaceholder: 'Camille',
|
||||
lastNameLabel: 'Nom',
|
||||
lastNamePlaceholder: 'Durand',
|
||||
emailLabel: 'E-mail professionnel',
|
||||
emailPlaceholder: 'camille@votre-musee.be',
|
||||
passwordLabel: 'Mot de passe',
|
||||
passwordPlaceholder: 'Au moins 8 caractères',
|
||||
sectionBilling: 'Facturation',
|
||||
addressLabel: 'Adresse',
|
||||
addressPlaceholder: 'Rue de la Culture 12',
|
||||
zipLabel: 'Code postal',
|
||||
zipPlaceholder: '1000',
|
||||
cityLabel: 'Ville',
|
||||
cityPlaceholder: 'Bruxelles',
|
||||
countryLabel: 'Pays',
|
||||
vatLabel: 'N° de TVA',
|
||||
vatOptional: 'optionnel',
|
||||
vatPlaceholder: 'BE 0123.456.789',
|
||||
vatChecking: 'Vérification du numéro de TVA…',
|
||||
vatUnavailable: 'Service de validation TVA indisponible — TVA belge appliquée, à revalider ultérieurement.',
|
||||
vatNoNumber: 'TVA belge 21% appliquée',
|
||||
vatNoNumberHint: "Ajoutez un n° de TVA valide pour l'exonération intracommunautaire.",
|
||||
vatDomestic: 'TVA belge 21% appliquée',
|
||||
vatZero: 'TVA intracommunautaire — 0%',
|
||||
vatInvalid: 'Numéro de TVA invalide',
|
||||
consentPrefix: "J'accepte les",
|
||||
consentTerms: 'conditions générales',
|
||||
consentAnd: 'et la',
|
||||
consentPrivacy: 'politique de confidentialité',
|
||||
consentSuffix: 'de MyInfoMate.',
|
||||
submitCta: 'Démarrer mon essai gratuit',
|
||||
submitting: 'Création de votre espace…',
|
||||
noCardNote: "Aucun prélèvement pendant l'essai. Nous ne demandons pas de carte.",
|
||||
errorSlugTaken: 'Cette adresse est prise — choisissez-en une autre.',
|
||||
errorGeneric: 'Une erreur est survenue, merci de réessayer.',
|
||||
successTitle: 'Bienvenue à bord !',
|
||||
successDesc: 'Votre espace est prêt. Un e-mail de confirmation vient de partir.',
|
||||
recapUrlLabel: 'Votre adresse visiteur',
|
||||
recapTrialLabel: 'Essai gratuit',
|
||||
recapEmailLabel: 'E-mail de confirmation',
|
||||
goToApp: 'Accéder à mon espace',
|
||||
},
|
||||
pricing: {
|
||||
sectionLabel: 'Tarifs',
|
||||
sectionTitle: 'Un plan pour chaque lieu',
|
||||
@ -205,8 +276,8 @@ const translations = {
|
||||
startingFrom: 'À partir de',
|
||||
recommended: 'Recommandé',
|
||||
ctaStart: 'Demander une démo',
|
||||
ctaStartTrial: 'Démarrer mon essai gratuit',
|
||||
ctaContact: 'Nous contacter',
|
||||
comingSoon: 'Bientôt disponible',
|
||||
enterprisePrice: 'Sur devis',
|
||||
enterpriseDesc: 'Pour les organisations multi-sites ou aux besoins spécifiques (développements custom, accompagnement, SLA renforcé).',
|
||||
multiYearLabel: 'Budget pluriannuel ?',
|
||||
@ -224,7 +295,7 @@ const translations = {
|
||||
pushNotif: 'Notifications push',
|
||||
stats: 'Stats de visite',
|
||||
ai: 'Assistant IA visiteur',
|
||||
reqPerMonth: 'req/mois',
|
||||
questionsPerMonth: 'questions de visiteurs / mois',
|
||||
statsBasic: '30 jours',
|
||||
statsAdvanced: 'Illimitées',
|
||||
webDisplayUrl: 'Affichage web sur URL dédiée',
|
||||
@ -455,6 +526,77 @@ const translations = {
|
||||
messagePlaceholder: 'Tell us about your project...',
|
||||
submitButton: 'Send my request',
|
||||
},
|
||||
signup: {
|
||||
planBadge: 'Essentiel plan',
|
||||
heroTitle: 'Your cultural venue, online in minutes.',
|
||||
priceSuffix: '/ month excl. VAT',
|
||||
trialBadge: '14-day free trial — no card required',
|
||||
feature1: 'Web visitor app, opened via QR code — no installation',
|
||||
feature2: 'Unlimited content, agenda, maps and sections',
|
||||
feature3: 'Multilingual — your visitors choose their language',
|
||||
feature4: 'Your colors and logo, your dedicated address',
|
||||
watermarkNote: 'During the trial, your visitor pages show a light "Preview" watermark. It disappears as soon as your subscription is activated.',
|
||||
trustHosting: 'Hosted in Europe',
|
||||
trustCancel: 'Cancel anytime',
|
||||
trustSupport: 'Support in French',
|
||||
formTitle: 'Create my account',
|
||||
alreadyAccount: 'Already have a workspace?',
|
||||
loginLink: 'Log in',
|
||||
sectionOrg: 'Your organization',
|
||||
orgNameLabel: 'Organization name',
|
||||
orgNamePlaceholder: 'Museum of Fine Arts',
|
||||
slugLabel: 'Your dedicated address',
|
||||
slugPrefix: 'app.myinfomate.be/',
|
||||
slugPlaceholder: 'your-museum',
|
||||
slugHintDefault: "Lowercase letters, numbers and dashes — this is the URL your visitors will scan.",
|
||||
slugHintChecking: 'Checking availability…',
|
||||
slugHintTaken: 'This address is already taken — try another name.',
|
||||
slugHintAvailable: 'Available!',
|
||||
slugHintTooShort: 'At least 3 characters.',
|
||||
sectionContact: 'Your contact details',
|
||||
firstNameLabel: 'First name',
|
||||
firstNamePlaceholder: 'Camille',
|
||||
lastNameLabel: 'Last name',
|
||||
lastNamePlaceholder: 'Durand',
|
||||
emailLabel: 'Work email',
|
||||
emailPlaceholder: 'camille@your-museum.com',
|
||||
passwordLabel: 'Password',
|
||||
passwordPlaceholder: 'At least 8 characters',
|
||||
sectionBilling: 'Billing',
|
||||
addressLabel: 'Address',
|
||||
addressPlaceholder: '12 Culture Street',
|
||||
zipLabel: 'Postal code',
|
||||
zipPlaceholder: '1000',
|
||||
cityLabel: 'City',
|
||||
cityPlaceholder: 'Brussels',
|
||||
countryLabel: 'Country',
|
||||
vatLabel: 'VAT number',
|
||||
vatOptional: 'optional',
|
||||
vatPlaceholder: 'BE 0123.456.789',
|
||||
vatChecking: 'Checking VAT number…',
|
||||
vatUnavailable: 'VAT validation service unavailable — Belgian VAT applied, to be re-validated later.',
|
||||
vatNoNumber: '21% Belgian VAT applied',
|
||||
vatNoNumberHint: 'Add a valid VAT number for intra-community exemption.',
|
||||
vatDomestic: '21% Belgian VAT applied',
|
||||
vatZero: 'Intra-community VAT — 0%',
|
||||
vatInvalid: 'Invalid VAT number',
|
||||
consentPrefix: 'I accept the',
|
||||
consentTerms: 'terms of service',
|
||||
consentAnd: 'and the',
|
||||
consentPrivacy: 'privacy policy',
|
||||
consentSuffix: 'of MyInfoMate.',
|
||||
submitCta: 'Start my free trial',
|
||||
submitting: 'Creating your workspace…',
|
||||
noCardNote: 'No charge during the trial. We never ask for a card.',
|
||||
errorSlugTaken: 'This address is taken — choose another one.',
|
||||
errorGeneric: 'Something went wrong, please try again.',
|
||||
successTitle: 'Welcome aboard!',
|
||||
successDesc: 'Your workspace is ready. A confirmation email has just been sent.',
|
||||
recapUrlLabel: 'Your visitor address',
|
||||
recapTrialLabel: 'Free trial',
|
||||
recapEmailLabel: 'Confirmation email',
|
||||
goToApp: 'Go to my workspace',
|
||||
},
|
||||
pricing: {
|
||||
sectionLabel: 'Pricing',
|
||||
sectionTitle: 'A plan for every venue',
|
||||
@ -466,8 +608,8 @@ const translations = {
|
||||
startingFrom: 'Starting from',
|
||||
recommended: 'Recommended',
|
||||
ctaStart: 'Request a demo',
|
||||
ctaStartTrial: 'Start my free trial',
|
||||
ctaContact: 'Contact us',
|
||||
comingSoon: 'Coming soon',
|
||||
enterprisePrice: 'Custom quote',
|
||||
enterpriseDesc: 'For multi-site organizations or specific needs (custom development, onboarding, enhanced SLA).',
|
||||
multiYearLabel: 'Multi-year budget?',
|
||||
@ -485,7 +627,7 @@ const translations = {
|
||||
pushNotif: 'Push notifications',
|
||||
stats: 'Visit statistics',
|
||||
ai: 'AI visitor assistant',
|
||||
reqPerMonth: 'req/month',
|
||||
questionsPerMonth: 'visitor questions / month',
|
||||
statsBasic: '30 days',
|
||||
statsAdvanced: 'Unlimited',
|
||||
webDisplayUrl: 'Web display on dedicated URL',
|
||||
@ -716,6 +858,77 @@ const translations = {
|
||||
messagePlaceholder: 'Vertel ons over uw project...',
|
||||
submitButton: 'Mijn aanvraag verzenden',
|
||||
},
|
||||
signup: {
|
||||
planBadge: 'Plan Essentieel',
|
||||
heroTitle: 'Uw culturele locatie, online in enkele minuten.',
|
||||
priceSuffix: '/ maand excl. BTW',
|
||||
trialBadge: '14 dagen gratis proberen — geen kredietkaart nodig',
|
||||
feature1: 'Web-bezoekersapp, geopend via QR-code — geen installatie',
|
||||
feature2: 'Onbeperkte inhoud, agenda, plannen en secties',
|
||||
feature3: 'Meertalig — uw bezoekers kiezen hun taal',
|
||||
feature4: 'Uw kleuren en logo, uw eigen adres',
|
||||
watermarkNote: 'Tijdens de proefperiode tonen uw bezoekerspagina\'s een licht "Preview"-watermerk. Dit verdwijnt zodra uw abonnement actief is.',
|
||||
trustHosting: 'Gehost in Europa',
|
||||
trustCancel: 'Op elk moment opzegbaar',
|
||||
trustSupport: 'Ondersteuning in het Frans',
|
||||
formTitle: 'Mijn account aanmaken',
|
||||
alreadyAccount: 'Heeft u al een omgeving?',
|
||||
loginLink: 'Inloggen',
|
||||
sectionOrg: 'Uw organisatie',
|
||||
orgNameLabel: 'Naam van de organisatie',
|
||||
orgNamePlaceholder: 'Museum voor Schone Kunsten',
|
||||
slugLabel: 'Uw eigen adres',
|
||||
slugPrefix: 'app.myinfomate.be/',
|
||||
slugPlaceholder: 'uw-museum',
|
||||
slugHintDefault: 'Kleine letters, cijfers en streepjes — dit is de URL die uw bezoekers zullen scannen.',
|
||||
slugHintChecking: 'Beschikbaarheid controleren…',
|
||||
slugHintTaken: 'Dit adres is al in gebruik — probeer een andere naam.',
|
||||
slugHintAvailable: 'Beschikbaar!',
|
||||
slugHintTooShort: 'Minstens 3 tekens.',
|
||||
sectionContact: 'Uw gegevens',
|
||||
firstNameLabel: 'Voornaam',
|
||||
firstNamePlaceholder: 'Camille',
|
||||
lastNameLabel: 'Naam',
|
||||
lastNamePlaceholder: 'Durand',
|
||||
emailLabel: 'Professioneel e-mailadres',
|
||||
emailPlaceholder: 'camille@uw-museum.be',
|
||||
passwordLabel: 'Wachtwoord',
|
||||
passwordPlaceholder: 'Minstens 8 tekens',
|
||||
sectionBilling: 'Facturatie',
|
||||
addressLabel: 'Adres',
|
||||
addressPlaceholder: 'Cultuurstraat 12',
|
||||
zipLabel: 'Postcode',
|
||||
zipPlaceholder: '1000',
|
||||
cityLabel: 'Stad',
|
||||
cityPlaceholder: 'Brussel',
|
||||
countryLabel: 'Land',
|
||||
vatLabel: 'BTW-nummer',
|
||||
vatOptional: 'optioneel',
|
||||
vatPlaceholder: 'BE 0123.456.789',
|
||||
vatChecking: 'BTW-nummer controleren…',
|
||||
vatUnavailable: 'BTW-validatiedienst niet beschikbaar — Belgische BTW toegepast, later opnieuw te valideren.',
|
||||
vatNoNumber: '21% Belgische BTW toegepast',
|
||||
vatNoNumberHint: 'Voeg een geldig BTW-nummer toe voor intracommunautaire vrijstelling.',
|
||||
vatDomestic: '21% Belgische BTW toegepast',
|
||||
vatZero: 'Intracommunautaire BTW — 0%',
|
||||
vatInvalid: 'Ongeldig BTW-nummer',
|
||||
consentPrefix: 'Ik aanvaard de',
|
||||
consentTerms: 'algemene voorwaarden',
|
||||
consentAnd: 'en het',
|
||||
consentPrivacy: 'privacybeleid',
|
||||
consentSuffix: 'van MyInfoMate.',
|
||||
submitCta: 'Start mijn gratis proefperiode',
|
||||
submitting: 'Uw omgeving wordt aangemaakt…',
|
||||
noCardNote: 'Geen afschrijving tijdens de proefperiode. Wij vragen nooit om een kaart.',
|
||||
errorSlugTaken: 'Dit adres is al bezet — kies een andere.',
|
||||
errorGeneric: 'Er is iets misgegaan, probeer het opnieuw.',
|
||||
successTitle: 'Welkom aan boord!',
|
||||
successDesc: 'Uw omgeving is klaar. Er is zojuist een bevestigingsmail verstuurd.',
|
||||
recapUrlLabel: 'Uw bezoekersadres',
|
||||
recapTrialLabel: 'Gratis proefperiode',
|
||||
recapEmailLabel: 'Bevestigingsmail',
|
||||
goToApp: 'Naar mijn omgeving',
|
||||
},
|
||||
pricing: {
|
||||
sectionLabel: 'Tarieven',
|
||||
sectionTitle: 'Een plan voor elke locatie',
|
||||
@ -727,8 +940,8 @@ const translations = {
|
||||
startingFrom: 'Vanaf',
|
||||
recommended: 'Aanbevolen',
|
||||
ctaStart: 'Demo aanvragen',
|
||||
ctaStartTrial: 'Start mijn gratis proefperiode',
|
||||
ctaContact: 'Neem contact op',
|
||||
comingSoon: 'Binnenkort beschikbaar',
|
||||
enterprisePrice: 'Op maat',
|
||||
enterpriseDesc: 'Voor organisaties met meerdere locaties of specifieke behoeften (maatwerk ontwikkeling, begeleiding, verbeterde SLA).',
|
||||
multiYearLabel: 'Meerjarig budget?',
|
||||
@ -746,7 +959,7 @@ const translations = {
|
||||
pushNotif: 'Pushmeldingen',
|
||||
stats: 'Bezoekstatistieken',
|
||||
ai: 'AI-bezoekersassistent',
|
||||
reqPerMonth: 'req/maand',
|
||||
questionsPerMonth: 'bezoekersvragen / maand',
|
||||
statsBasic: '30 dagen',
|
||||
statsAdvanced: 'Onbeperkt',
|
||||
webDisplayUrl: 'Webweergave op toegewijd URL',
|
||||
@ -977,6 +1190,77 @@ const translations = {
|
||||
messagePlaceholder: 'Erzählen Sie uns von Ihrem Projekt...',
|
||||
submitButton: 'Meine Anfrage senden',
|
||||
},
|
||||
signup: {
|
||||
planBadge: 'Essentiel-Plan',
|
||||
heroTitle: 'Ihr Kulturort, online in wenigen Minuten.',
|
||||
priceSuffix: '/ Monat zzgl. MwSt.',
|
||||
trialBadge: '14 Tage kostenlos testen — keine Kreditkarte nötig',
|
||||
feature1: 'Web-Besucher-App, per QR-Code geöffnet — keine Installation',
|
||||
feature2: 'Unbegrenzte Inhalte, Agenda, Pläne und Sektionen',
|
||||
feature3: 'Mehrsprachig — Ihre Besucher wählen ihre Sprache',
|
||||
feature4: 'Ihre Farben und Ihr Logo, Ihre eigene Adresse',
|
||||
watermarkNote: 'Während der Testphase zeigen Ihre Besucherseiten ein leichtes "Vorschau"-Wasserzeichen. Es verschwindet, sobald Ihr Abonnement aktiviert ist.',
|
||||
trustHosting: 'Gehostet in Europa',
|
||||
trustCancel: 'Jederzeit kündbar',
|
||||
trustSupport: 'Support auf Französisch',
|
||||
formTitle: 'Konto erstellen',
|
||||
alreadyAccount: 'Haben Sie bereits einen Arbeitsbereich?',
|
||||
loginLink: 'Anmelden',
|
||||
sectionOrg: 'Ihre Organisation',
|
||||
orgNameLabel: 'Name der Organisation',
|
||||
orgNamePlaceholder: 'Museum der Schönen Künste',
|
||||
slugLabel: 'Ihre eigene Adresse',
|
||||
slugPrefix: 'app.myinfomate.be/',
|
||||
slugPlaceholder: 'ihr-museum',
|
||||
slugHintDefault: 'Kleinbuchstaben, Zahlen und Bindestriche — dies ist die URL, die Ihre Besucher scannen werden.',
|
||||
slugHintChecking: 'Verfügbarkeit wird geprüft…',
|
||||
slugHintTaken: 'Diese Adresse ist bereits vergeben — wählen Sie einen anderen Namen.',
|
||||
slugHintAvailable: 'Verfügbar!',
|
||||
slugHintTooShort: 'Mindestens 3 Zeichen.',
|
||||
sectionContact: 'Ihre Kontaktdaten',
|
||||
firstNameLabel: 'Vorname',
|
||||
firstNamePlaceholder: 'Camille',
|
||||
lastNameLabel: 'Nachname',
|
||||
lastNamePlaceholder: 'Durand',
|
||||
emailLabel: 'Geschäftliche E-Mail',
|
||||
emailPlaceholder: 'camille@ihr-museum.de',
|
||||
passwordLabel: 'Passwort',
|
||||
passwordPlaceholder: 'Mindestens 8 Zeichen',
|
||||
sectionBilling: 'Rechnungsstellung',
|
||||
addressLabel: 'Adresse',
|
||||
addressPlaceholder: 'Kulturstraße 12',
|
||||
zipLabel: 'Postleitzahl',
|
||||
zipPlaceholder: '1000',
|
||||
cityLabel: 'Stadt',
|
||||
cityPlaceholder: 'Brüssel',
|
||||
countryLabel: 'Land',
|
||||
vatLabel: 'USt-IdNr.',
|
||||
vatOptional: 'optional',
|
||||
vatPlaceholder: 'BE 0123.456.789',
|
||||
vatChecking: 'USt-IdNr. wird geprüft…',
|
||||
vatUnavailable: 'USt-Validierungsdienst nicht verfügbar — belgische MwSt. angewendet, später erneut zu validieren.',
|
||||
vatNoNumber: '21% belgische MwSt. angewendet',
|
||||
vatNoNumberHint: 'Fügen Sie eine gültige USt-IdNr. für die innergemeinschaftliche Befreiung hinzu.',
|
||||
vatDomestic: '21% belgische MwSt. angewendet',
|
||||
vatZero: 'Innergemeinschaftliche MwSt. — 0%',
|
||||
vatInvalid: 'Ungültige USt-IdNr.',
|
||||
consentPrefix: 'Ich akzeptiere die',
|
||||
consentTerms: 'Allgemeinen Geschäftsbedingungen',
|
||||
consentAnd: 'und die',
|
||||
consentPrivacy: 'Datenschutzrichtlinie',
|
||||
consentSuffix: 'von MyInfoMate.',
|
||||
submitCta: 'Kostenlos testen',
|
||||
submitting: 'Ihr Arbeitsbereich wird erstellt…',
|
||||
noCardNote: 'Keine Abbuchung während der Testphase. Wir fragen nie nach einer Karte.',
|
||||
errorSlugTaken: 'Diese Adresse ist vergeben — wählen Sie eine andere.',
|
||||
errorGeneric: 'Etwas ist schiefgelaufen, bitte versuchen Sie es erneut.',
|
||||
successTitle: 'Willkommen an Bord!',
|
||||
successDesc: 'Ihr Arbeitsbereich ist bereit. Eine Bestätigungs-E-Mail wurde soeben versendet.',
|
||||
recapUrlLabel: 'Ihre Besucheradresse',
|
||||
recapTrialLabel: 'Kostenlose Testphase',
|
||||
recapEmailLabel: 'Bestätigungs-E-Mail',
|
||||
goToApp: 'Zu meinem Arbeitsbereich',
|
||||
},
|
||||
pricing: {
|
||||
sectionLabel: 'Preise',
|
||||
sectionTitle: 'Ein Plan für jeden Standort',
|
||||
@ -988,8 +1272,8 @@ const translations = {
|
||||
startingFrom: 'Ab',
|
||||
recommended: 'Empfohlen',
|
||||
ctaStart: 'Demo anfordern',
|
||||
ctaStartTrial: 'Kostenlos testen',
|
||||
ctaContact: 'Kontaktieren Sie uns',
|
||||
comingSoon: 'Demnächst verfügbar',
|
||||
enterprisePrice: 'Auf Anfrage',
|
||||
enterpriseDesc: 'Für Organisationen mit mehreren Standorten oder spezifischen Anforderungen (individuelle Entwicklung, Begleitung, erweiterter SLA).',
|
||||
multiYearLabel: 'Mehrjähriges Budget?',
|
||||
@ -1007,7 +1291,7 @@ const translations = {
|
||||
pushNotif: 'Push-Benachrichtigungen',
|
||||
stats: 'Besucherstatistiken',
|
||||
ai: 'KI-Besucherassistent',
|
||||
reqPerMonth: 'Req/Monat',
|
||||
questionsPerMonth: 'Besucherfragen / Monat',
|
||||
statsBasic: '30 Tage',
|
||||
statsAdvanced: 'Unbegrenzt',
|
||||
webDisplayUrl: 'Web-Anzeige auf dedizierter URL',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user