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:
@@ -2,28 +2,25 @@
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Calendar, Mail } from 'lucide-react';
|
||||
import { Calendar, Mail, RotateCcw } from 'lucide-react';
|
||||
import AnimatedCheckmark from '@/components/icons/AnimatedCheckmark';
|
||||
import Button from '@/components/ui/Button';
|
||||
import CalButton from '@/components/ui/CalButton';
|
||||
import type { WizardFormData } from './WizardContainer';
|
||||
|
||||
// ─── Brief Renderer ───────────────────────────────────────────────────────────
|
||||
// ─── Brief Renderer ──────────────────────────────────────────────────────────
|
||||
|
||||
function renderBrief(brief: string) {
|
||||
// Split on double newlines for paragraph blocks
|
||||
const blocks = brief.split('\n\n').filter(Boolean);
|
||||
|
||||
return blocks.map((block, blockIdx) => {
|
||||
const lines = block.split('\n').filter(Boolean);
|
||||
|
||||
// Detect a heading block (starts with **)
|
||||
const isSectionHeading =
|
||||
lines.length === 1 && lines[0].startsWith('**') && lines[0].endsWith('**');
|
||||
|
||||
if (isSectionHeading) {
|
||||
const text = lines[0].replace(/\*\*/g, '');
|
||||
// Top heading is larger
|
||||
if (blockIdx === 0) {
|
||||
return (
|
||||
<p key={blockIdx} className="font-semibold text-sm text-on-surface mb-0.5">
|
||||
@@ -38,16 +35,13 @@ function renderBrief(brief: string) {
|
||||
);
|
||||
}
|
||||
|
||||
// Separator line
|
||||
if (lines.length === 1 && lines[0] === '---') {
|
||||
return <hr key={blockIdx} className="border-outline-variant/30 my-3" />;
|
||||
}
|
||||
|
||||
// Body paragraph — inline bold rendering
|
||||
return (
|
||||
<div key={blockIdx} className="text-xs text-outline leading-relaxed">
|
||||
{lines.map((line, lineIdx) => {
|
||||
// Render **bold** inline
|
||||
const parts = line.split(/(\*\*[^*]+\*\*)/g);
|
||||
return (
|
||||
<p key={lineIdx} className={lineIdx > 0 ? 'mt-1' : ''}>
|
||||
@@ -68,37 +62,15 @@ function renderBrief(brief: string) {
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Cal.com Embed / Booking ──────────────────────────────────────────────────
|
||||
|
||||
function BookingSection() {
|
||||
return (
|
||||
<div className="rounded-xl bg-surface-low px-5 py-5 text-center">
|
||||
<div className="flex justify-center mb-3">
|
||||
<span className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<Calendar size={18} strokeWidth={1.5} className="text-primary-dark" />
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-on-surface mb-1">Book a Consultation</p>
|
||||
<p className="text-xs text-outline mb-4">30 minutes to discuss your brief with our team</p>
|
||||
<CalButton
|
||||
className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg text-sm font-medium text-white transition-all hover:-translate-y-px active:translate-y-0"
|
||||
style={{ background: 'linear-gradient(135deg, #006494, #5BA4D9)' }}
|
||||
>
|
||||
<Calendar size={16} />
|
||||
Book a Call
|
||||
</CalButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||
// ─── Main Component ──────────────────────────────────────────────────────────
|
||||
|
||||
interface StepCompleteProps {
|
||||
formData: WizardFormData;
|
||||
brief: string;
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
export default function StepComplete({ formData, brief }: StepCompleteProps) {
|
||||
export default function StepComplete({ formData, brief, onReset }: StepCompleteProps) {
|
||||
const t = useTranslations('configurator');
|
||||
|
||||
const displayEmail = formData.email || 'your inbox';
|
||||
@@ -106,9 +78,7 @@ export default function StepComplete({ formData, brief }: StepCompleteProps) {
|
||||
const containerVariants = {
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: {
|
||||
staggerChildren: 0.12,
|
||||
},
|
||||
transition: { staggerChildren: 0.12 },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -151,7 +121,7 @@ export default function StepComplete({ formData, brief }: StepCompleteProps) {
|
||||
className="rounded-xl bg-surface-high border border-outline-variant/40 px-5 py-5 shadow-card"
|
||||
>
|
||||
<p className="text-xs font-semibold uppercase tracking-label text-outline mb-3">
|
||||
Your project brief
|
||||
{t('complete.briefPreview')}
|
||||
</p>
|
||||
<div className="space-y-1 max-h-72 overflow-y-auto pr-1 scrollbar-thin">
|
||||
{renderBrief(brief)}
|
||||
@@ -161,16 +131,32 @@ export default function StepComplete({ formData, brief }: StepCompleteProps) {
|
||||
|
||||
{/* Booking */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<p className="text-xs font-semibold uppercase tracking-label text-outline mb-3">
|
||||
{t('complete.bookTitle')}
|
||||
</p>
|
||||
<BookingSection />
|
||||
<div className="rounded-xl bg-surface-low px-5 py-5 text-center">
|
||||
<div className="flex justify-center mb-3">
|
||||
<span className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<Calendar size={18} strokeWidth={1.5} className="text-primary-dark" />
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-on-surface mb-1">
|
||||
{t('complete.bookTitle')}
|
||||
</p>
|
||||
<p className="text-xs text-outline mb-4">
|
||||
{t('complete.bookSubtitle')}
|
||||
</p>
|
||||
<CalButton
|
||||
className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg text-sm font-medium text-white transition-all hover:-translate-y-px active:translate-y-0"
|
||||
style={{ background: 'linear-gradient(135deg, #006494, #5BA4D9)' }}
|
||||
>
|
||||
<Calendar size={16} />
|
||||
{t('complete.bookCall')}
|
||||
</CalButton>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Fallback contact */}
|
||||
<motion.div variants={itemVariants}>
|
||||
{/* Fallback contact + reset */}
|
||||
<motion.div variants={itemVariants} className="flex flex-col gap-3">
|
||||
<p className="text-center text-xs text-outline">
|
||||
Or reach us directly at{' '}
|
||||
{t('complete.reachDirectly')}{' '}
|
||||
<a
|
||||
href="mailto:hello@letsbe.biz"
|
||||
className="text-primary-dark underline underline-offset-2 hover:text-primary transition-colors"
|
||||
@@ -178,6 +164,17 @@ export default function StepComplete({ formData, brief }: StepCompleteProps) {
|
||||
hello@letsbe.biz
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{onReset && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onReset}
|
||||
className="flex items-center justify-center gap-1.5 text-xs text-outline hover:text-on-surface transition-colors mx-auto"
|
||||
>
|
||||
<RotateCcw size={12} />
|
||||
{t('startOver')}
|
||||
</button>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user