feat: configurator overhaul — full i18n, AI brief generation, redesigned UI

Configurator:
- Full i18n: industries, timelines, AI types, field labels, error messages,
  complete screen — all translated to French
- Real AI brief generation via OpenRouter (DeepSeek V3.2) with fallback template
- Fixed email notification bug (was sending to client instead of admin)
- Added wizard reset capability ("Start Over" button on success screen)
- Redesigned section shell with refined card, accent line, step indicators
- All step components use translation keys instead of hardcoded strings

Hero:
- Tighter spacing to keep CTAs above the fold
- Reduced bottom gradient height

Footer:
- Removed GitHub link
- Legal name in copyright
- "American-founded" location text

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 17:52:09 +01:00
parent 4aa357a999
commit acefb70b68
12 changed files with 532 additions and 459 deletions

View File

@@ -10,32 +10,8 @@ import type { StepProps } from './WizardContainer';
// ─── Data ─────────────────────────────────────────────────────────────────────
interface IndustryOption {
id: string;
label: string;
}
const INDUSTRIES: IndustryOption[] = [
{ id: 'maritime', label: 'Maritime / Yachting' },
{ id: 'hospitality', label: 'Hospitality' },
{ id: 'technology', label: 'Technology' },
{ id: 'realestate', label: 'Real Estate' },
{ id: 'finance', label: 'Finance' },
{ id: 'ngo', label: 'NGO / Nonprofit' },
{ id: 'other', label: 'Other' },
];
interface TimelineOption {
id: string;
label: string;
}
const TIMELINES: TimelineOption[] = [
{ id: 'asap', label: 'ASAP' },
{ id: '1-3months', label: '13 months' },
{ id: '3-6months', label: '36 months' },
{ id: 'exploring', label: 'Just exploring' },
];
const INDUSTRY_IDS = ['maritime', 'hospitality', 'technology', 'realestate', 'finance', 'ngo', 'other'] as const;
const TIMELINE_IDS = ['asap', '1-3months', '3-6months', 'exploring'] as const;
// ─── Component ────────────────────────────────────────────────────────────────
@@ -56,14 +32,10 @@ export default function StepDetails({ formData, setFormData, onNext, onBack }: S
}));
};
const canProceed = true; // Step 2 fields are optional
return (
<div className="flex flex-col gap-6">
{/* Progress */}
<ProgressBar currentStep={2} />
{/* Heading */}
<div>
<h3 className="font-serif text-2xl font-semibold tracking-headline text-on-surface">
{t('step2.title')}
@@ -74,12 +46,12 @@ export default function StepDetails({ formData, setFormData, onNext, onBack }: S
{/* Industry */}
<div className="flex flex-col gap-2.5">
<label className="text-xs font-semibold uppercase tracking-label text-outline">
Your industry
{t('fields.industry')}
</label>
<div className="flex flex-wrap gap-2">
{INDUSTRIES.map((option, index) => (
{INDUSTRY_IDS.map((id, index) => (
<motion.div
key={option.id}
key={id}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{
@@ -89,10 +61,10 @@ export default function StepDetails({ formData, setFormData, onNext, onBack }: S
}}
>
<Chip
active={formData.industry === option.id}
onClick={() => selectIndustry(option.id)}
active={formData.industry === id}
onClick={() => selectIndustry(id)}
>
{option.label}
{t(`industries.${id}`)}
</Chip>
</motion.div>
))}
@@ -105,8 +77,10 @@ export default function StepDetails({ formData, setFormData, onNext, onBack }: S
htmlFor="scope-textarea"
className="text-xs font-semibold uppercase tracking-label text-outline"
>
What are you looking to achieve?
<span className="ml-1.5 normal-case font-normal text-outline/70">(optional)</span>
{t('fields.scope')}
<span className="ml-1.5 normal-case font-normal text-outline/70">
{t('fields.scopeOptional')}
</span>
</label>
<textarea
id="scope-textarea"
@@ -114,7 +88,7 @@ export default function StepDetails({ formData, setFormData, onNext, onBack }: S
onChange={(e) =>
setFormData((prev) => ({ ...prev, scope: e.target.value }))
}
placeholder="e.g. We need to replace our current booking system and improve the client-facing experience…"
placeholder={t('fields.scopePlaceholder')}
rows={4}
className={cn(
'w-full resize-none rounded-xl border border-outline-variant/60 bg-surface-high',
@@ -129,12 +103,12 @@ export default function StepDetails({ formData, setFormData, onNext, onBack }: S
{/* Timeline */}
<div className="flex flex-col gap-2.5">
<label className="text-xs font-semibold uppercase tracking-label text-outline">
Timeline
{t('fields.timeline')}
</label>
<div className="flex flex-wrap gap-2">
{TIMELINES.map((option, index) => (
{TIMELINE_IDS.map((id, index) => (
<motion.div
key={option.id}
key={id}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{
@@ -144,10 +118,10 @@ export default function StepDetails({ formData, setFormData, onNext, onBack }: S
}}
>
<Chip
active={formData.timeline === option.id}
onClick={() => selectTimeline(option.id)}
active={formData.timeline === id}
onClick={() => selectTimeline(id)}
>
{option.label}
{t(`timelines.${id}`)}
</Chip>
</motion.div>
))}
@@ -162,7 +136,6 @@ export default function StepDetails({ formData, setFormData, onNext, onBack }: S
<Button
variant="primary"
arrow
disabled={!canProceed}
onClick={onNext}
className="flex-1"
>