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:
@@ -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: '1–3 months' },
|
||||
{ id: '3-6months', label: '3–6 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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user