feat: complete agency site build (Phases 1-7)

Full Next.js 16 + Payload CMS 3.x agency site with:
- Homepage: Hero, TrustBar, Services, Configurator wizard, Process,
  Selected Works, Philosophy, CTA Banner
- Sub-pages: /services (3 pillars + AI Layer), /work/[slug] (case
  studies), /about (philosophy + story)
- Configurator: 3-step wizard with AI brief generation API
- i18n: Full EN/FR bilingual with next-intl
- Design system: Cormorant Garamond + Inter, celestial blue palette,
  glassmorphism nav, Framer Motion animations
- Payload CMS collections: Projects, Services, Submissions, Media

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 20:37:38 +01:00
commit a1f9eca76c
64 changed files with 15810 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
'use client';
import { useTranslations } from 'next-intl';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
import Button from '@/components/ui/Button';
import Chip from '@/components/ui/Chip';
import ProgressBar from './ProgressBar';
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' },
];
// ─── Component ────────────────────────────────────────────────────────────────
export default function StepDetails({ formData, setFormData, onNext, onBack }: StepProps) {
const t = useTranslations('configurator');
const selectIndustry = (id: string) => {
setFormData((prev) => ({
...prev,
industry: prev.industry === id ? null : id,
}));
};
const selectTimeline = (id: string) => {
setFormData((prev) => ({
...prev,
timeline: prev.timeline === id ? null : id,
}));
};
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')}
</h3>
<p className="mt-1 text-sm text-outline">{t('step2.subtitle')}</p>
</div>
{/* Industry */}
<div className="flex flex-col gap-2.5">
<label className="text-xs font-semibold uppercase tracking-label text-outline">
Your industry
</label>
<div className="flex flex-wrap gap-2">
{INDUSTRIES.map((option, index) => (
<motion.div
key={option.id}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{
delay: index * 0.04,
duration: 0.3,
ease: [0.16, 1, 0.3, 1],
}}
>
<Chip
active={formData.industry === option.id}
onClick={() => selectIndustry(option.id)}
>
{option.label}
</Chip>
</motion.div>
))}
</div>
</div>
{/* Scope / Goals */}
<div className="flex flex-col gap-2">
<label
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>
</label>
<textarea
id="scope-textarea"
value={formData.scope}
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…"
rows={4}
className={cn(
'w-full resize-none rounded-xl border border-outline-variant/60 bg-surface-high',
'px-4 py-3 text-sm text-on-surface placeholder:text-outline/50',
'focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary',
'transition-colors duration-200',
'leading-relaxed',
)}
/>
</div>
{/* Timeline */}
<div className="flex flex-col gap-2.5">
<label className="text-xs font-semibold uppercase tracking-label text-outline">
Timeline
</label>
<div className="flex flex-wrap gap-2">
{TIMELINES.map((option, index) => (
<motion.div
key={option.id}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{
delay: index * 0.05,
duration: 0.3,
ease: [0.16, 1, 0.3, 1],
}}
>
<Chip
active={formData.timeline === option.id}
onClick={() => selectTimeline(option.id)}
>
{option.label}
</Chip>
</motion.div>
))}
</div>
</div>
{/* Navigation */}
<div className="flex gap-3">
<Button variant="ghost" onClick={onBack} className="flex-shrink-0">
{t('back')}
</Button>
<Button
variant="primary"
arrow
disabled={!canProceed}
onClick={onNext}
className="flex-1"
>
{t('nextStep')}
</Button>
</div>
</div>
);
}