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,420 @@
import { setRequestLocale } from 'next-intl/server';
import { Shield, PenTool, Users } from 'lucide-react';
import ScrollReveal from '@/components/ui/ScrollReveal';
import Button from '@/components/ui/Button';
import CornerBracket from '@/components/icons/CornerBracket';
import { routing } from '@/i18n/routing';
// ─── Static Generation ────────────────────────────────────────────────────────
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
// ─── Data ─────────────────────────────────────────────────────────────────────
const PILLARS = [
{
id: 'ownership',
Icon: Shield,
title: 'Ownership & Privacy',
description:
'We build on infrastructure you control. No vendor lock-in, no opaque SaaS dependencies quietly holding your data hostage. Your platform, your servers, your rules — backed by engineering that makes it maintainable.',
},
{
id: 'craftsmanship',
Icon: PenTool,
title: 'Craftsmanship',
description:
'The gap between a website that works and one that endures is craft. We sweat the typography, the transitions, the query performance, the edge cases. Every interface we ship is something we would be proud to sign.',
},
{
id: 'one-team',
Icon: Users,
title: 'One Team, Everything',
description:
'Strategy, design, engineering, infrastructure — under one roof, one point of contact, one shared standard of quality. No handoffs between agencies. No telephone-game briefs. Just people who care about the whole thing.',
},
];
// ─── Sub-components ───────────────────────────────────────────────────────────
function StoryGeometry() {
return (
<div
className="absolute inset-0 overflow-hidden rounded-xl"
aria-hidden="true"
>
{/* Primary gradient circle — top right */}
<div
className="absolute rounded-full"
style={{
width: '70%',
height: '70%',
top: '-15%',
right: '-15%',
background:
'radial-gradient(circle, rgba(91,164,217,0.10) 0%, rgba(0,100,148,0.05) 55%, transparent 100%)',
}}
/>
{/* Secondary circle — bottom left */}
<div
className="absolute rounded-full"
style={{
width: '55%',
height: '55%',
bottom: '-10%',
left: '-8%',
background:
'radial-gradient(circle, rgba(46,196,160,0.07) 0%, transparent 70%)',
}}
/>
{/* Diagonal grid texture */}
<div
className="absolute inset-0 opacity-[0.025]"
style={{
backgroundImage:
'repeating-linear-gradient(-45deg, var(--color-primary-dark) 0, var(--color-primary-dark) 1px, transparent 0, transparent 50%)',
backgroundSize: '28px 28px',
}}
/>
{/* Tilted rectangle — left center */}
<div
className="absolute rounded-lg"
style={{
width: '24%',
height: '34%',
top: '18%',
left: '6%',
border: '1.5px solid rgba(91,164,217,0.12)',
transform: 'rotate(-6deg)',
}}
/>
{/* Dot field — right center */}
<div
className="absolute"
style={{
width: '28%',
height: '28%',
top: '30%',
right: '8%',
backgroundImage:
'radial-gradient(circle, rgba(91,164,217,0.22) 1.5px, transparent 1.5px)',
backgroundSize: '10px 10px',
}}
/>
{/* Dashed arc */}
<svg
className="absolute"
style={{ top: '15%', left: '28%', opacity: 0.07 }}
width="160"
height="160"
viewBox="0 0 160 160"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="80"
cy="80"
r="68"
stroke="var(--color-primary-dark)"
strokeWidth="1"
strokeDasharray="7 5"
/>
</svg>
{/* Small accent square */}
<div
className="absolute rounded-sm"
style={{
width: '5%',
height: '5%',
bottom: '26%',
right: '26%',
background: 'rgba(91,164,217,0.18)',
transform: 'rotate(14deg)',
}}
/>
{/* Teal accent line — bottom */}
<div
className="absolute"
style={{
width: '30%',
height: '2px',
bottom: '14%',
left: '50%',
transform: 'translateX(-50%)',
background:
'linear-gradient(to right, transparent, rgba(46,196,160,0.35), transparent)',
}}
/>
</div>
);
}
function PillarCard({
Icon,
title,
description,
}: {
Icon: React.ElementType;
title: string;
description: string;
}) {
return (
<ScrollReveal variant="fadeUp">
<div className="bg-surface-high rounded-xl p-8 flex flex-col gap-5 h-full shadow-subtle">
{/* Icon */}
<div
className="w-11 h-11 rounded-xl flex items-center justify-center flex-shrink-0"
style={{ background: 'rgba(91,164,217,0.10)' }}
>
<Icon
size={20}
strokeWidth={1.75}
className="text-primary"
aria-hidden="true"
/>
</div>
{/* Text */}
<div className="flex flex-col gap-2.5">
<h3 className="font-serif text-xl font-semibold text-on-surface leading-snug tracking-[-0.02em]">
{title}
</h3>
<p className="text-outline text-sm leading-relaxed">{description}</p>
</div>
</div>
</ScrollReveal>
);
}
// ─── Page ─────────────────────────────────────────────────────────────────────
type Props = {
params: Promise<{ locale: string }>;
};
export default async function AboutPage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
return (
<main>
{/* ── 1. Hero ── */}
<section className="bg-surface py-32">
<div className="container mx-auto px-6">
<div className="max-w-4xl mx-auto flex flex-col items-center text-center gap-8">
<ScrollReveal variant="fadeIn">
<span className="label-md text-primary">About LetsBe.</span>
</ScrollReveal>
<ScrollReveal variant="fadeUp" delay={0.1}>
<h1 className="font-serif font-semibold text-on-surface text-5xl md:text-6xl lg:text-[4rem] leading-[1.05] tracking-[-0.03em]">
Digital Sovereignty
<br />
is not a luxury.
</h1>
</ScrollReveal>
<ScrollReveal variant="fadeUp" delay={0.2}>
<p className="text-outline text-xl leading-relaxed max-w-2xl">
We build digital platforms for businesses that refuse to compromise on ownership,
on quality, or on the partner they trust to build it with them.
</p>
</ScrollReveal>
</div>
</div>
</section>
{/* ── 2. Story ── */}
<section className="bg-surface-low py-24">
<div className="container mx-auto px-6">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-start">
{/* Left: Text */}
<div className="lg:col-span-5 flex flex-col gap-10">
<ScrollReveal variant="slideLeft" className="flex flex-col gap-4">
<span className="label-md text-primary">Our Story</span>
<h2 className="font-serif text-4xl md:text-[2.75rem] font-semibold text-on-surface leading-[1.1] tracking-[-0.02em]">
Born on the<br />
Côte d&rsquo;Azur.
</h2>
</ScrollReveal>
<ScrollReveal variant="fadeUp" delay={0.1} className="flex flex-col gap-5 text-outline leading-relaxed text-[0.9375rem]">
<p>
LetsBe. was founded on the French Riviera by a small team of engineers and
designers who shared a single conviction: that ambitious businesses deserve digital
infrastructure as carefully considered as the work they do.
</p>
<p>
We started by building for founders and institutions along the coast port
authorities, conservation organisations, maritime operators each one operating
in a context where reliability, elegance, and discretion were not optional extras.
Those early projects shaped everything we believe about what a digital partner
should be.
</p>
<p>
Today we work with clients across Europe and the Mediterranean on platforms that
are built to be owned, not rented. We do not believe in locking clients into
systems they cannot see, services they cannot leave, or vendors whose priorities
will drift from theirs. We build on open infrastructure, we document everything,
and we hand over codebases that outlast the engagement.
</p>
</ScrollReveal>
</div>
{/* Right: Decorative canvas */}
<div className="lg:col-span-7 relative">
<ScrollReveal variant="slideRight">
<div
className="relative bg-surface rounded-xl overflow-hidden"
style={{ minHeight: '460px' }}
>
<StoryGeometry />
{/* Inset rim */}
<div
className="absolute inset-0 rounded-xl pointer-events-none"
style={{
boxShadow: 'inset 0 0 0 1px rgba(194,199,206,0.22)',
}}
aria-hidden="true"
/>
</div>
</ScrollReveal>
{/* Floating pull quote */}
<ScrollReveal
variant="fadeUp"
delay={0.4}
className="absolute -bottom-8 -left-4 lg:-left-10 z-10 max-w-[320px] w-[calc(100%-2.5rem)] lg:max-w-[340px]"
>
<div className="bg-surface-high rounded-xl p-6 shadow-subtle relative">
<div className="absolute top-4 right-4">
<CornerBracket
size={28}
position="top-right"
color="var(--color-primary)"
/>
</div>
<blockquote className="font-serif italic text-lg text-on-surface leading-relaxed pr-8">
&ldquo;Build fewer things. Build them better. Build them to last.&rdquo;
</blockquote>
<div
className="w-8 h-px bg-primary/40 my-4"
aria-hidden="true"
/>
<p className="label-md text-outline">LetsBe. founding principle</p>
</div>
</ScrollReveal>
</div>
</div>
</div>
</section>
{/* ── 3. Three Pillars ── */}
<section className="bg-surface py-24 mt-8">
<div className="container mx-auto px-6">
<ScrollReveal variant="fadeUp" className="flex flex-col items-center text-center gap-4 mb-16">
<span className="label-md text-primary">Our Beliefs</span>
<h2 className="font-serif font-semibold text-on-surface text-4xl md:text-5xl leading-[1.1] tracking-[-0.02em] max-w-2xl">
What We Believe
</h2>
<p className="text-outline text-lg leading-relaxed max-w-xl mt-1">
Three principles that inform every decision we make, every line of code we write, and
every client relationship we enter.
</p>
</ScrollReveal>
<ScrollReveal variant="fadeUp" stagger className="grid grid-cols-1 md:grid-cols-3 gap-6">
{PILLARS.map(({ id, Icon, title, description }) => (
<PillarCard key={id} Icon={Icon} title={title} description={description} />
))}
</ScrollReveal>
</div>
</section>
{/* ── 4. Quote ── */}
<section
className="py-20"
style={{ backgroundColor: 'var(--color-navy)' }}
>
<div className="container mx-auto px-6">
<ScrollReveal variant="fadeUp">
<div className="max-w-3xl mx-auto flex flex-col items-center text-center gap-6">
{/* Decorative line */}
<div
className="w-10 h-px"
style={{ background: 'rgba(91,164,217,0.5)' }}
aria-hidden="true"
/>
<blockquote className="font-serif italic text-white text-3xl md:text-[2.25rem] leading-[1.3] tracking-[-0.02em]">
&ldquo;Our mission is to bring the precision of architecture to the fluidity of the
web.&rdquo;
</blockquote>
<p className="label-md text-white/40">
&mdash; LetsBe. founding philosophy
</p>
</div>
</ScrollReveal>
</div>
</section>
{/* ── 5. CTA ── */}
<section className="bg-surface py-16">
<div className="container mx-auto px-6">
<ScrollReveal variant="fadeUp">
<div className="max-w-3xl mx-auto flex flex-col items-center text-center gap-8">
<div className="flex flex-col gap-3 items-center">
<span className="label-md text-primary">Work With Us</span>
<h2 className="font-serif font-semibold text-on-surface text-3xl md:text-4xl leading-[1.1] tracking-[-0.02em]">
Let&rsquo;s build something together.
</h2>
<p className="text-outline text-lg leading-relaxed max-w-lg">
Whether you have a clear brief or an early-stage idea, we would be glad to talk
through what is possible.
</p>
</div>
<div className="flex flex-col sm:flex-row items-center gap-4">
<Button variant="primary" size="lg" arrow href="/#configure">
Start your project
</Button>
<Button
variant="secondary"
size="lg"
href="mailto:hello@letsbe.biz"
>
Book a call
</Button>
</div>
</div>
</ScrollReveal>
</div>
</section>
</main>
);
}

View File

@@ -0,0 +1,46 @@
import type { Metadata } from 'next'
import { NextIntlClientProvider } from 'next-intl'
import { getMessages, setRequestLocale } from 'next-intl/server'
import { notFound } from 'next/navigation'
import { routing } from '@/i18n/routing'
import Nav from '@/components/layout/Nav'
import Footer from '@/components/layout/Footer'
import '@/styles/globals.css'
export const metadata: Metadata = {
title: 'LetsBe. | Bespoke Digital Studio & AI Infrastructure',
description:
'Bespoke digital ecosystems, private infrastructure, and AI automation for ambitious businesses on the Côte d\'Azur.',
}
type Props = {
children: React.ReactNode
params: Promise<{ locale: string }>
}
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }))
}
export default async function LocaleLayout({ children, params }: Props) {
const { locale } = await params
if (!routing.locales.includes(locale as any)) {
notFound()
}
setRequestLocale(locale)
const messages = await getMessages()
return (
<html lang={locale} className="scroll-smooth">
<body className="font-sans text-on-surface bg-surface antialiased">
<NextIntlClientProvider messages={messages}>
<Nav />
{children}
<Footer />
</NextIntlClientProvider>
</body>
</html>
)
}

View File

@@ -0,0 +1,31 @@
import { setRequestLocale } from 'next-intl/server'
import Hero from '@/components/sections/Hero'
import TrustBar from '@/components/sections/TrustBar'
import ServicesOverview from '@/components/sections/ServicesOverview'
import Process from '@/components/sections/Process'
import SelectedWorks from '@/components/sections/SelectedWorks'
import Philosophy from '@/components/sections/Philosophy'
import Configurator from '@/components/sections/Configurator'
import CTABanner from '@/components/sections/CTABanner'
type Props = {
params: Promise<{ locale: string }>
}
export default async function HomePage({ params }: Props) {
const { locale } = await params
setRequestLocale(locale)
return (
<main>
<Hero />
<TrustBar />
<ServicesOverview />
<Configurator />
<Process />
<SelectedWorks />
<Philosophy />
<CTABanner />
</main>
)
}

View File

@@ -0,0 +1,174 @@
import { setRequestLocale } from 'next-intl/server';
import type { Metadata } from 'next';
import ServicesHero from '@/components/sections/services/ServicesHero';
import ServicePillar from '@/components/sections/services/ServicePillar';
import AILayer from '@/components/sections/services/AILayer';
import ServicesCTA from '@/components/sections/services/ServicesCTA';
// Icon names passed as strings — resolved in client component
// ─── Metadata ─────────────────────────────────────────────────────────────────
export const metadata: Metadata = {
title: 'Services | LetsBe. — Bespoke Digital Studio',
description:
'From bespoke web design to private infrastructure and AI automation — three pillars of digital excellence, engineered for ambitious businesses.',
};
// ─── Service data ──────────────────────────────────────────────────────────────
export const SERVICE_PILLARS = [
{
id: 'design-development',
numeral: '01',
title: 'Web Design & Development',
description:
'Your digital presence is not a template waiting to be filled — it is an engineered expression of your brand. We design and build bespoke websites and web applications from a blank canvas, crafting every interaction, every transition, and every responsive breakpoint with intention. Our work is fast by architecture, not by accident: semantic markup, optimised asset pipelines, and edge-deployed rendering that scores at the top of the Core Web Vitals chart. Whether you need a high-conversion marketing site, a complex SaaS application, or a multi-region e-commerce platform, we deliver a digital product that is built to last and built to grow.',
background: 'bg-surface' as const,
features: [
{
icon: 'Palette',
title: 'Bespoke UI/UX Design',
description:
'Custom Figma-to-code workflows. Every layout, component, and motion decision serves your brand — never a theme, never a shortcut.',
},
{
icon: 'Globe',
title: 'Modern Web Applications',
description:
'React, Next.js, and edge-deployed architecture for SPAs, SSR, and full-stack applications that scale without compromise.',
},
{
icon: 'ShoppingCart',
title: 'E-commerce & Platforms',
description:
'Headless storefronts, custom checkout flows, and multi-currency platforms engineered for high-volume retail.',
},
{
icon: 'Zap',
title: 'Performance Optimisation',
description:
'LCP under one second, zero layout shift. We audit, refactor, and rebuild performance into the foundation of your product.',
},
],
},
{
id: 'custom-systems',
numeral: '02',
title: 'Custom Systems',
description:
'Off-the-shelf software makes assumptions about your business. We don\'t. When your operations outgrow spreadsheets and generic SaaS platforms, we build the exact system your team needs — with the data model, the permission structure, and the workflow logic that reflects how you actually work. From CRM platforms tailored to your sales cycle, to internal management tools that replace three different subscriptions, to API architecture that connects your existing stack into a coherent whole — every system we build is owned by you, documented in full, and maintained without vendor dependency.',
background: 'bg-surface-low' as const,
features: [
{
icon: 'Database',
title: 'CRM & Management',
description:
'Purpose-built relationship and pipeline management with the fields, views, and automation rules your team will actually use.',
},
{
icon: 'Code2',
title: 'Bespoke Software',
description:
'Full-stack business applications: from quoting and booking platforms to complex multi-tenant SaaS products.',
},
{
icon: 'GitBranch',
title: 'API Architecture',
description:
'RESTful and GraphQL APIs, webhook integrations, and the middleware layer that makes your disparate tools speak the same language.',
},
{
icon: 'Wrench',
title: 'Internal Tooling',
description:
'Admin dashboards, reporting portals, and workflow automation that give your team unfair operational advantages.',
},
],
},
{
id: 'infrastructure',
numeral: '03',
title: 'Digital Infrastructure',
description:
'Data sovereignty is not a feature — it is a right. We provision and manage private cloud infrastructure that removes your reliance on opaque hyperscalers and shared-hosting providers. Your data lives where you say it lives, on servers you control, with access policies your legal team can actually review. We handle the full infrastructure lifecycle: dedicated server provisioning, containerised deployments with Docker and Kubernetes, automated backup strategies, TLS certificate management, and 24/7 uptime monitoring. When the European data-residency audit arrives, you will be the only business in the room with a clean answer.',
background: 'bg-surface' as const,
features: [
{
icon: 'Server',
title: 'Dedicated Hosting',
description:
'Private VPS and bare-metal environments in EU data centres — no shared neighbours, no noisy-neighbour risk.',
},
{
icon: 'Shield',
title: 'Data Sovereignty',
description:
'GDPR-aligned architecture with data-residency guarantees, audit trails, and full client ownership of storage and credentials.',
},
{
icon: 'Lock',
title: 'Security Hardening',
description:
'WAF configuration, DDoS mitigation, secrets management, and penetration-testing-ready hardened deployment pipelines.',
},
{
icon: 'Settings',
title: 'DevOps & Maintenance',
description:
'CI/CD pipelines, container orchestration, rolling deployments, and proactive monitoring so your team ships without fear.',
},
],
},
] as const;
// ─── AI Layer data ─────────────────────────────────────────────────────────────
export const AI_CAPABILITIES = [
{
id: 'ai-teammate',
title: 'AI Teammate',
description:
'An operational assistant embedded directly into your workflow. Retrieves and summarises emails, drafts responses, executes tasks across connected tools, and surfaces the information your team needs before they know they need it.',
},
{
id: 'customer-facing-ai',
title: 'Customer-Facing AI',
description:
'Intelligent conversational interfaces that handle enquiries, qualify leads, make product recommendations, and personalise the user journey — at scale, around the clock, without extra headcount.',
},
{
id: 'data-intelligence',
title: 'Data Intelligence',
description:
'Automated analytics pipelines, predictive modelling, and scheduled reporting that transform the data your systems collect into decisions your leadership team can act on.',
},
] as const;
// ─── Page ──────────────────────────────────────────────────────────────────────
type Props = {
params: Promise<{ locale: string }>;
};
export default async function ServicesPage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
return (
<main>
<ServicesHero />
{SERVICE_PILLARS.map((pillar, index) => (
<ServicePillar
key={pillar.id}
pillar={pillar}
index={index}
/>
))}
<AILayer capabilities={AI_CAPABILITIES} />
<ServicesCTA />
</main>
);
}

View File

@@ -0,0 +1,293 @@
import { notFound } from 'next/navigation';
import { setRequestLocale } from 'next-intl/server';
import { routing } from '@/i18n/routing';
import ScrollReveal from '@/components/ui/ScrollReveal';
import Button from '@/components/ui/Button';
import CornerBracket from '@/components/icons/CornerBracket';
import Chip from '@/components/ui/Chip';
// ─── Types ────────────────────────────────────────────────────────────────────
interface Project {
title: string;
subtitle: string;
description: string;
challenge: string;
approach: string;
outcome: string;
techStack: string[];
tags: string[];
}
// ─── Data (will come from Payload CMS) ────────────────────────────────────────
const PROJECTS: Record<string, Project> = {
'monaco-ocean': {
title: 'Monaco Ocean Protection Challenge',
subtitle: 'AI-Powered Judging & Analytics Platform',
description:
"A comprehensive judging and analytics system with advanced AI jury integration for one of the Mediterranean's most prestigious conservation events.",
challenge:
'The Monaco Ocean Protection Challenge needed a modern platform to manage submissions, coordinate judges across time zones, and provide AI-assisted evaluation of conservation proposals — all while maintaining the prestige and security expected of a Monaco institution.',
approach:
'We built a custom platform from the ground up using Next.js and a private PostgreSQL infrastructure. The AI jury module uses natural language processing to pre-screen submissions and generate summary reports, while human judges retain full control over final decisions.',
outcome:
"The platform processed over 200 submissions in its first season, reducing judge workload by 40% through AI-assisted pre-screening. The client praised the system's reliability and the elegance of its interface.",
techStack: ['Next.js', 'PostgreSQL', 'OpenAI API', 'Docker', 'Private Cloud'],
tags: ['AI Integration', 'Platform'],
},
'port-nimara': {
title: 'Port Nimara',
subtitle: 'Maritime Digital Hub',
description: 'Scalable digital hub for maritime logistics.',
challenge:
'Port Nimara needed a modern digital presence that could serve as both a marketing website and an operational hub for berth inquiries, event management, and partner communications.',
approach:
'We designed and developed a performant Nuxt.js application with a headless CMS for content management, integrated with their existing maritime scheduling systems via custom API middleware.',
outcome:
'The new platform increased online berth inquiries by 3x and provided the port authority with real-time content management capabilities they previously lacked.',
techStack: ['Nuxt.js', 'Directus CMS', 'Node.js', 'Docker'],
tags: ['Website', 'Infrastructure'],
},
'port-amador': {
title: 'Port Amador',
subtitle: 'Premium Nautical Experience',
description: 'Premium digital experience for elite nautical services.',
challenge:
'Port Amador required a luxury-grade digital experience that matched the exclusivity of their nautical services, with multi-language support and seamless booking integration.',
approach:
'We crafted a bespoke website with cinematic imagery, smooth animations, and an integrated booking flow. The site was built on modern web technologies with a focus on performance and SEO for the competitive luxury maritime market.',
outcome:
"The redesigned platform elevated Port Amador's digital presence to match their premium positioning, with a 60% improvement in page load times and significantly increased organic traffic.",
techStack: ['Next.js', 'Tailwind CSS', 'Framer Motion', 'Vercel'],
tags: ['Website', 'Infrastructure'],
},
};
// ─── Static Generation ────────────────────────────────────────────────────────
export function generateStaticParams() {
const slugs = Object.keys(PROJECTS);
const allParams: { locale: string; slug: string }[] = [];
for (const locale of routing.locales) {
for (const slug of slugs) {
allParams.push({ locale, slug });
}
}
return allParams;
}
// ─── Sub-components ───────────────────────────────────────────────────────────
function ContentSection({
label,
index,
heading,
body,
}: {
label: string;
index: string;
heading: string;
body: string;
}) {
return (
<ScrollReveal variant="fadeUp" className="flex flex-col gap-4">
<div className="flex items-center gap-3">
<span className="label-md text-outline/50">{index}</span>
<span className="label-md text-primary">{label}</span>
</div>
<h3 className="font-serif text-2xl md:text-3xl font-semibold text-on-surface leading-snug tracking-[-0.02em]">
{heading}
</h3>
<p className="text-outline leading-relaxed text-base md:text-[1.0625rem]">{body}</p>
</ScrollReveal>
);
}
// ─── Page ─────────────────────────────────────────────────────────────────────
type Props = {
params: Promise<{ locale: string; slug: string }>;
};
export default async function CaseStudyPage({ params }: Props) {
const { locale, slug } = await params;
setRequestLocale(locale);
const project = PROJECTS[slug];
if (!project) notFound();
return (
<main>
{/* ── Hero ── */}
<section className="bg-surface-low py-24">
<div className="container mx-auto px-6">
<div className="max-w-3xl mx-auto flex flex-col items-center text-center gap-6">
{/* Tags */}
<ScrollReveal variant="fadeIn">
<div className="flex flex-wrap items-center justify-center gap-2">
{project.tags.map((tag) => (
<Chip key={tag} size="md">
{tag}
</Chip>
))}
</div>
</ScrollReveal>
{/* Title */}
<ScrollReveal variant="fadeUp" delay={0.08}>
<h1 className="font-serif font-semibold text-on-surface text-4xl md:text-5xl lg:text-[3.25rem] leading-[1.1] tracking-[-0.02em]">
{project.title}
</h1>
</ScrollReveal>
{/* Subtitle */}
<ScrollReveal variant="fadeUp" delay={0.16}>
<p className="label-md text-primary tracking-widest">
{project.subtitle}
</p>
</ScrollReveal>
{/* Description */}
<ScrollReveal variant="fadeUp" delay={0.22}>
<p className="text-outline text-lg leading-relaxed max-w-2xl">
{project.description}
</p>
</ScrollReveal>
</div>
</div>
</section>
{/* ── Case Study Content ── */}
<section className="bg-surface py-20 md:py-28">
<div className="container mx-auto px-6">
<div className="max-w-3xl mx-auto">
{/* Decorative corner brackets framing the content area */}
<ScrollReveal variant="fadeIn" className="relative">
<div className="absolute -top-6 -left-6 md:-left-10" aria-hidden="true">
<CornerBracket size={36} position="top-left" color="var(--color-primary)" />
</div>
<div className="absolute -top-6 -right-6 md:-right-10" aria-hidden="true">
<CornerBracket size={36} position="top-right" color="var(--color-primary)" />
</div>
</ScrollReveal>
{/* Sections */}
<div className="flex flex-col gap-16 md:gap-20">
<ContentSection
label="The Challenge"
index="01"
heading="The problem we set out to solve"
body={project.challenge}
/>
{/* Divider */}
<ScrollReveal variant="fadeIn">
<div
className="w-full h-px"
style={{
background:
'linear-gradient(to right, transparent, var(--color-outline-variant), transparent)',
}}
aria-hidden="true"
/>
</ScrollReveal>
<ContentSection
label="Our Approach"
index="02"
heading="How we thought about it"
body={project.approach}
/>
{/* Divider */}
<ScrollReveal variant="fadeIn">
<div
className="w-full h-px"
style={{
background:
'linear-gradient(to right, transparent, var(--color-outline-variant), transparent)',
}}
aria-hidden="true"
/>
</ScrollReveal>
<ContentSection
label="The Outcome"
index="03"
heading="What we delivered"
body={project.outcome}
/>
</div>
</div>
</div>
</section>
{/* ── Tech Stack ── */}
<section className="bg-surface-low py-12">
<div className="container mx-auto px-6">
<div className="max-w-3xl mx-auto">
<ScrollReveal variant="fadeUp" className="flex flex-col gap-5">
<p className="label-md text-outline">Built with</p>
<div className="flex flex-wrap gap-2">
{project.techStack.map((tech) => (
<span
key={tech}
className="inline-flex items-center px-4 py-2 rounded-xl bg-surface-high text-on-surface text-sm font-medium shadow-card"
>
{tech}
</span>
))}
</div>
</ScrollReveal>
</div>
</div>
</section>
{/* ── CTA ── */}
<section className="bg-surface py-20 md:py-24">
<div className="container mx-auto px-6">
<ScrollReveal variant="fadeUp">
<div className="max-w-3xl mx-auto flex flex-col items-center text-center gap-8">
{/* Decorative corner bracket */}
<div className="relative">
<div className="absolute -top-3 -left-3" aria-hidden="true">
<CornerBracket size={24} position="top-left" color="var(--color-teal)" />
</div>
<div className="absolute -top-3 -right-3" aria-hidden="true">
<CornerBracket size={24} position="top-right" color="var(--color-teal)" />
</div>
<p className="label-md text-primary px-4">Next Step</p>
</div>
<h2 className="font-serif font-semibold text-on-surface text-3xl md:text-4xl leading-[1.1] tracking-[-0.02em]">
Ready to build your own landmark?
</h2>
<p className="text-outline text-lg leading-relaxed max-w-xl">
Every project we take on is a collaboration built on trust, precision, and a shared
belief that great digital work is never accidental.
</p>
<Button variant="primary" size="lg" arrow href="/#configure">
Start your project
</Button>
</div>
</ScrollReveal>
</div>
</section>
</main>
);
}

View File

@@ -0,0 +1,216 @@
import { NextRequest, NextResponse } from 'next/server';
// ─── Types ────────────────────────────────────────────────────────────────────
interface ConfigureRequestBody {
services: string[];
aiEnabled: boolean;
aiType: string | null;
industry: string | null;
scope: string;
timeline: string | null;
name: string;
company: string;
email: string;
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
function formatServicesList(services: string[]): string {
if (services.length === 0) return 'digital services';
if (services.length === 1) return services[0];
if (services.length === 2) return `${services[0]} and ${services[1]}`;
const last = services[services.length - 1];
const rest = services.slice(0, -1);
return `${rest.join(', ')}, and ${last}`;
}
function formatTimeline(timeline: string | null): string {
switch (timeline) {
case 'asap':
return 'as soon as possible';
case '1-3months':
return 'within the next 13 months';
case '3-6months':
return 'over a 36 month horizon';
case 'exploring':
return 'at a pace that suits your strategic planning';
default:
return 'within a timeline to be agreed upon';
}
}
function formatIndustry(industry: string | null): string {
switch (industry) {
case 'maritime':
return 'Maritime & Yachting';
case 'hospitality':
return 'Hospitality';
case 'technology':
return 'Technology';
case 'realestate':
return 'Real Estate';
case 'finance':
return 'Finance';
case 'ngo':
return 'NGO & Nonprofit';
case 'other':
return 'your sector';
default:
return 'your industry';
}
}
function formatAIType(aiType: string | null): string {
switch (aiType) {
case 'teammate':
return 'an internal AI teammate that augments your team\'s workflow';
case 'customer-facing':
return 'a customer-facing AI layer to enhance client interactions';
case 'data-intelligence':
return 'a data intelligence system that surfaces actionable insights from your data';
case 'notsure':
return 'an AI integration strategy tailored to your specific use case (to be defined during discovery)';
default:
return 'intelligent automation';
}
}
function generateMockBrief(body: ConfigureRequestBody): string {
const {
services,
aiEnabled,
aiType,
industry,
scope,
timeline,
name,
company,
} = body;
const servicesList = formatServicesList(services);
const industryLabel = formatIndustry(industry);
const timelineStr = formatTimeline(timeline);
const displayCompany = company.trim() || 'your organisation';
const displayName = name.split(' ')[0] || 'there';
const hasWeb = services.some((s) =>
s.toLowerCase().includes('web') || s.toLowerCase().includes('design'),
);
const hasSystems = services.some((s) =>
s.toLowerCase().includes('system') || s.toLowerCase().includes('cog') || s.toLowerCase().includes('custom'),
);
const hasInfra = services.some((s) =>
s.toLowerCase().includes('infra') || s.toLowerCase().includes('server'),
);
let webSection = '';
if (hasWeb) {
webSection = `
**Design & Development**
We will design and develop a bespoke digital presence for ${displayCompany} — starting from a clean slate, not a template. Expect a modern, responsive interface built on a headless architecture with exceptional performance scores (Lighthouse 95+). The design will reflect your brand positioning within the ${industryLabel} sector, with full accessibility compliance and SEO-optimised markup from day one.
`;
}
let systemsSection = '';
if (hasSystems) {
systemsSection = `
**Custom Systems**
We will architect and build a purpose-made internal system tailored to ${displayCompany}'s operational workflows. This includes a custom data model, role-based access control, and integrations with your existing toolchain. No generic SaaS — every logic rule and every interface is written to match how your team actually works.
`;
}
let infraSection = '';
if (hasInfra) {
infraSection = `
**Digital Infrastructure**
We will provision a dedicated, private cloud environment for ${displayCompany} — giving your team full data sovereignty. This includes containerised deployments, automated backups, uptime monitoring, and a managed CI/CD pipeline. Your data remains yours, stored in compliant European infrastructure.
`;
}
let aiSection = '';
if (aiEnabled && aiType) {
aiSection = `
**AI Integration**
Beyond the core build, we will layer in ${formatAIType(aiType)}. This is not a bolted-on chatbot — it is a deeply integrated capability that evolves alongside your digital ecosystem. The AI layer will be scoped precisely during our discovery sessions to ensure maximum return on investment.
`;
}
let scopeSection = '';
if (scope && scope.trim().length > 0) {
scopeSection = `
**Your Goals**
You've shared the following context: "${scope.trim()}" — we've taken note of this and will frame our initial discovery session around these priorities.
`;
}
return `**Project Brief for ${displayCompany}**
Prepared for: ${name}
Date: ${new Date().toLocaleDateString('en-GB', { year: 'numeric', month: 'long', day: 'numeric' })}
---
**Overview**
Hi ${displayName}, based on your requirements for ${servicesList} within the ${industryLabel} sector, we have prepared this preliminary brief to guide our first conversation.
LetsBe. will approach ${displayCompany}'s project as a complete digital ecosystem — not a collection of disconnected deliverables. Every component we build is designed to work in concert, giving you a unified platform that you own and control entirely.
${webSection}${systemsSection}${infraSection}${aiSection}${scopeSection}
**Recommended Approach**
We propose a phased engagement beginning with a structured Discovery sprint (23 sessions) to map your requirements, data flows, and technical constraints before any code is written. This protects your investment and ensures we build exactly what you need — nothing more, nothing less.
**Timeline**
Based on your preference, we will plan to deliver this project ${timelineStr}. A detailed project roadmap with milestones will be shared following the Discovery phase.
**Next Steps**
1. Book a 30-minute introductory call with our team
2. We'll share a detailed scope document within 48 hours of that call
3. Discovery sprint begins — at no obligation
We look forward to building something exceptional together.
— The LetsBe. Team`;
}
// ─── Route Handler ────────────────────────────────────────────────────────────
export async function POST(request: NextRequest) {
try {
const body = (await request.json()) as ConfigureRequestBody;
// Validate required fields
if (!body.services || body.services.length === 0) {
return NextResponse.json(
{ success: false, error: 'At least one service must be selected.' },
{ status: 400 },
);
}
if (!body.name || body.name.trim().length < 2) {
return NextResponse.json(
{ success: false, error: 'A valid name is required.' },
{ status: 400 },
);
}
if (!body.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) {
return NextResponse.json(
{ success: false, error: 'A valid email address is required.' },
{ status: 400 },
);
}
// Generate the brief
const brief = generateMockBrief(body);
return NextResponse.json({ success: true, brief });
} catch {
return NextResponse.json(
{ success: false, error: 'An unexpected error occurred. Please try again.' },
{ status: 500 },
);
}
}