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:
Thomas Fransolet 2026-08-11 10:48:54 +02:00
parent 4849249c55
commit d91cf7f02c
8 changed files with 841 additions and 22 deletions

1
.env.production Normal file
View File

@ -0,0 +1 @@
NEXT_PUBLIC_MANAGER_SERVICE_URL=https://api.myinfomate.be

1
.gitignore vendored
View File

@ -32,6 +32,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed) # env files (can opt-in for committing if needed)
.env* .env*
!.env.production
# vercel # vercel
.vercel .vercel

View File

@ -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"> <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-6 items-stretch pt-6">
<Reveal delay={0}> <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="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"> <div className="mb-6">
<p className="text-sm font-bold text-slate-400 uppercase tracking-widest mb-2">Essentiel</p> <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"> <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> </li>
))} ))}
</ul> </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"> <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.ctaStart} {t.pricing.ctaStartTrial}
</a> </a>
</div> </div>
</Reveal> </Reveal>
<Reveal delay={60}> <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"> <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> <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> </div>
@ -571,6 +569,7 @@ export default function Home({ lang }: { lang: Language }) {
{t.pricing.ctaStart} {t.pricing.ctaStart}
</a> </a>
</div> </div>
</div>
</Reveal> </Reveal>
<Reveal delay={120}> <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"> <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"> <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 className="material-symbols-outlined text-primary text-base shrink-0 mt-0.5">check_circle</span>
<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 className="block text-xs text-slate-500 mt-0.5">{t.pricing.features.aiNeedMore} Enterprise</span>
</span> </span>
</li> </li>

View File

@ -404,9 +404,6 @@ export default function SegmentPageClient({ data, lang }: { data: Segment; lang:
{/* Essentiel */} {/* Essentiel */}
<Reveal delay={0}> <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="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"> <div className="mb-6">
<p className="text-sm font-bold text-slate-400 uppercase tracking-widest mb-2">Essentiel</p> <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"> <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> </li>
))} ))}
</ul> </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"> <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.ctaStart} {t.pricing.ctaStartTrial}
</button> </a>
</div> </div>
</Reveal> </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"> <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 className="material-symbols-outlined text-primary text-base shrink-0 mt-0.5">check_circle</span>
<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 className="block text-xs text-slate-500 mt-0.5">{t.pricing.features.aiNeedMore} Enterprise</span>
</span> </span>
</li> </li>

View 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&nbsp;</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>
);
}

View 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} />;
}

View File

@ -69,6 +69,26 @@
animation: float 6s ease-in-out infinite; 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 { @utility scrollbar-hide {
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;

View File

@ -194,6 +194,77 @@ const translations = {
messagePlaceholder: 'Parlez-nous de votre projet...', messagePlaceholder: 'Parlez-nous de votre projet...',
submitButton: 'Envoyer ma demande', 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: { pricing: {
sectionLabel: 'Tarifs', sectionLabel: 'Tarifs',
sectionTitle: 'Un plan pour chaque lieu', sectionTitle: 'Un plan pour chaque lieu',
@ -205,8 +276,8 @@ const translations = {
startingFrom: 'À partir de', startingFrom: 'À partir de',
recommended: 'Recommandé', recommended: 'Recommandé',
ctaStart: 'Demander une démo', ctaStart: 'Demander une démo',
ctaStartTrial: 'Démarrer mon essai gratuit',
ctaContact: 'Nous contacter', ctaContact: 'Nous contacter',
comingSoon: 'Bientôt disponible',
enterprisePrice: 'Sur devis', enterprisePrice: 'Sur devis',
enterpriseDesc: 'Pour les organisations multi-sites ou aux besoins spécifiques (développements custom, accompagnement, SLA renforcé).', enterpriseDesc: 'Pour les organisations multi-sites ou aux besoins spécifiques (développements custom, accompagnement, SLA renforcé).',
multiYearLabel: 'Budget pluriannuel ?', multiYearLabel: 'Budget pluriannuel ?',
@ -224,7 +295,7 @@ const translations = {
pushNotif: 'Notifications push', pushNotif: 'Notifications push',
stats: 'Stats de visite', stats: 'Stats de visite',
ai: 'Assistant IA visiteur', ai: 'Assistant IA visiteur',
reqPerMonth: 'req/mois', questionsPerMonth: 'questions de visiteurs / mois',
statsBasic: '30 jours', statsBasic: '30 jours',
statsAdvanced: 'Illimitées', statsAdvanced: 'Illimitées',
webDisplayUrl: 'Affichage web sur URL dédiée', webDisplayUrl: 'Affichage web sur URL dédiée',
@ -455,6 +526,77 @@ const translations = {
messagePlaceholder: 'Tell us about your project...', messagePlaceholder: 'Tell us about your project...',
submitButton: 'Send my request', 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: { pricing: {
sectionLabel: 'Pricing', sectionLabel: 'Pricing',
sectionTitle: 'A plan for every venue', sectionTitle: 'A plan for every venue',
@ -466,8 +608,8 @@ const translations = {
startingFrom: 'Starting from', startingFrom: 'Starting from',
recommended: 'Recommended', recommended: 'Recommended',
ctaStart: 'Request a demo', ctaStart: 'Request a demo',
ctaStartTrial: 'Start my free trial',
ctaContact: 'Contact us', ctaContact: 'Contact us',
comingSoon: 'Coming soon',
enterprisePrice: 'Custom quote', enterprisePrice: 'Custom quote',
enterpriseDesc: 'For multi-site organizations or specific needs (custom development, onboarding, enhanced SLA).', enterpriseDesc: 'For multi-site organizations or specific needs (custom development, onboarding, enhanced SLA).',
multiYearLabel: 'Multi-year budget?', multiYearLabel: 'Multi-year budget?',
@ -485,7 +627,7 @@ const translations = {
pushNotif: 'Push notifications', pushNotif: 'Push notifications',
stats: 'Visit statistics', stats: 'Visit statistics',
ai: 'AI visitor assistant', ai: 'AI visitor assistant',
reqPerMonth: 'req/month', questionsPerMonth: 'visitor questions / month',
statsBasic: '30 days', statsBasic: '30 days',
statsAdvanced: 'Unlimited', statsAdvanced: 'Unlimited',
webDisplayUrl: 'Web display on dedicated URL', webDisplayUrl: 'Web display on dedicated URL',
@ -716,6 +858,77 @@ const translations = {
messagePlaceholder: 'Vertel ons over uw project...', messagePlaceholder: 'Vertel ons over uw project...',
submitButton: 'Mijn aanvraag verzenden', 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: { pricing: {
sectionLabel: 'Tarieven', sectionLabel: 'Tarieven',
sectionTitle: 'Een plan voor elke locatie', sectionTitle: 'Een plan voor elke locatie',
@ -727,8 +940,8 @@ const translations = {
startingFrom: 'Vanaf', startingFrom: 'Vanaf',
recommended: 'Aanbevolen', recommended: 'Aanbevolen',
ctaStart: 'Demo aanvragen', ctaStart: 'Demo aanvragen',
ctaStartTrial: 'Start mijn gratis proefperiode',
ctaContact: 'Neem contact op', ctaContact: 'Neem contact op',
comingSoon: 'Binnenkort beschikbaar',
enterprisePrice: 'Op maat', enterprisePrice: 'Op maat',
enterpriseDesc: 'Voor organisaties met meerdere locaties of specifieke behoeften (maatwerk ontwikkeling, begeleiding, verbeterde SLA).', enterpriseDesc: 'Voor organisaties met meerdere locaties of specifieke behoeften (maatwerk ontwikkeling, begeleiding, verbeterde SLA).',
multiYearLabel: 'Meerjarig budget?', multiYearLabel: 'Meerjarig budget?',
@ -746,7 +959,7 @@ const translations = {
pushNotif: 'Pushmeldingen', pushNotif: 'Pushmeldingen',
stats: 'Bezoekstatistieken', stats: 'Bezoekstatistieken',
ai: 'AI-bezoekersassistent', ai: 'AI-bezoekersassistent',
reqPerMonth: 'req/maand', questionsPerMonth: 'bezoekersvragen / maand',
statsBasic: '30 dagen', statsBasic: '30 dagen',
statsAdvanced: 'Onbeperkt', statsAdvanced: 'Onbeperkt',
webDisplayUrl: 'Webweergave op toegewijd URL', webDisplayUrl: 'Webweergave op toegewijd URL',
@ -977,6 +1190,77 @@ const translations = {
messagePlaceholder: 'Erzählen Sie uns von Ihrem Projekt...', messagePlaceholder: 'Erzählen Sie uns von Ihrem Projekt...',
submitButton: 'Meine Anfrage senden', 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: { pricing: {
sectionLabel: 'Preise', sectionLabel: 'Preise',
sectionTitle: 'Ein Plan für jeden Standort', sectionTitle: 'Ein Plan für jeden Standort',
@ -988,8 +1272,8 @@ const translations = {
startingFrom: 'Ab', startingFrom: 'Ab',
recommended: 'Empfohlen', recommended: 'Empfohlen',
ctaStart: 'Demo anfordern', ctaStart: 'Demo anfordern',
ctaStartTrial: 'Kostenlos testen',
ctaContact: 'Kontaktieren Sie uns', ctaContact: 'Kontaktieren Sie uns',
comingSoon: 'Demnächst verfügbar',
enterprisePrice: 'Auf Anfrage', enterprisePrice: 'Auf Anfrage',
enterpriseDesc: 'Für Organisationen mit mehreren Standorten oder spezifischen Anforderungen (individuelle Entwicklung, Begleitung, erweiterter SLA).', enterpriseDesc: 'Für Organisationen mit mehreren Standorten oder spezifischen Anforderungen (individuelle Entwicklung, Begleitung, erweiterter SLA).',
multiYearLabel: 'Mehrjähriges Budget?', multiYearLabel: 'Mehrjähriges Budget?',
@ -1007,7 +1291,7 @@ const translations = {
pushNotif: 'Push-Benachrichtigungen', pushNotif: 'Push-Benachrichtigungen',
stats: 'Besucherstatistiken', stats: 'Besucherstatistiken',
ai: 'KI-Besucherassistent', ai: 'KI-Besucherassistent',
reqPerMonth: 'Req/Monat', questionsPerMonth: 'Besucherfragen / Monat',
statsBasic: '30 Tage', statsBasic: '30 Tage',
statsAdvanced: 'Unbegrenzt', statsAdvanced: 'Unbegrenzt',
webDisplayUrl: 'Web-Anzeige auf dedizierter URL', webDisplayUrl: 'Web-Anzeige auf dedizierter URL',