feat(i18n): wire work case study page to translations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 15:00:40 -04:00
parent 901f76349a
commit 0189c56bec

View File

@@ -6,69 +6,38 @@ 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';
const BASE_URL = 'https://letsbe.biz';
// ─── Types ────────────────────────────────────────────────────────────────────
interface Project {
title: string;
subtitle: string;
description: string;
challenge: string;
approach: string;
outcome: string;
techStack: string[];
tags: string[];
image: string;
techStack: string[];
}
// ─── Slug-to-work-key mapping ─────────────────────────────────────────────────
const SLUG_TO_KEY: Record<string, string> = {
'monaco-ocean': 'monaco',
'port-nimara': 'portNimara',
'port-amador': 'portAmador',
}
// ─── 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'],
image: '/images/monaco_high_res.jpg',
techStack: ['Next.js', 'PostgreSQL', 'OpenAI API', 'Docker', 'Private Cloud'],
},
'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'],
image: '/images/anguilla.png',
techStack: ['Nuxt.js', 'Directus CMS', 'Node.js', 'Docker'],
},
'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'],
image: '/images/panama.png',
techStack: ['Next.js', 'Tailwind CSS', 'Framer Motion', 'Vercel'],
},
};
@@ -83,6 +52,9 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
const project = PROJECTS[slug];
if (!project) return {};
const workKey = SLUG_TO_KEY[slug];
if (!workKey) return {};
const t = await getTranslations({ locale, namespace: 'meta.work' });
const path = locale === 'en' ? `/work/${slug}` : `/${locale}/work/${slug}`;
@@ -161,12 +133,28 @@ export default async function CaseStudyPage({ params }: Props) {
const project = PROJECTS[slug];
if (!project) notFound();
const workKey = SLUG_TO_KEY[slug];
if (!workKey) notFound();
const t = await getTranslations({ locale, namespace: 'caseStudy' });
const tw = await getTranslations({ locale, namespace: 'work' });
// Get translated content
const title = tw(`projects.${workKey}.title`)
const tags = tw.raw(`projects.${workKey}.tags`) as string[]
const subtitle = t(`projects.${slug}.subtitle`)
const description = t(`projects.${slug}.description`)
const challenge = t(`projects.${slug}.challenge`)
const approach = t(`projects.${slug}.approach`)
const outcome = t(`projects.${slug}.outcome`)
const caseStudyJsonLd = {
'@context': 'https://schema.org',
'@type': 'CreativeWork',
name: project.title,
description: project.description,
name: title,
description: description,
image: `${BASE_URL}${project.image}`,
inLanguage: locale,
creator: {
'@type': 'Organization',
name: 'LetsBe.',
@@ -201,7 +189,7 @@ export default async function CaseStudyPage({ params }: Props) {
{/* Tags */}
<ScrollReveal variant="fadeIn">
<div className="flex flex-wrap items-center justify-center gap-2">
{project.tags.map((tag) => (
{tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center bg-white/15 backdrop-blur-sm text-white/90 text-[0.75rem] font-semibold px-3 py-1 rounded-full leading-none tracking-wide"
@@ -215,21 +203,21 @@ export default async function CaseStudyPage({ params }: Props) {
{/* Title */}
<ScrollReveal variant="fadeUp" delay={0.08}>
<h1 className="font-serif font-semibold text-white text-4xl md:text-5xl lg:text-[3.25rem] leading-[1.1] tracking-[-0.02em]">
{project.title}
{title}
</h1>
</ScrollReveal>
{/* Subtitle */}
<ScrollReveal variant="fadeUp" delay={0.16}>
<p className="label-md text-white/70 tracking-widest">
{project.subtitle}
{subtitle}
</p>
</ScrollReveal>
{/* Description */}
<ScrollReveal variant="fadeUp" delay={0.22}>
<p className="text-white/80 text-lg leading-relaxed max-w-2xl">
{project.description}
{description}
</p>
</ScrollReveal>
@@ -256,10 +244,10 @@ export default async function CaseStudyPage({ params }: Props) {
<div className="flex flex-col gap-16 md:gap-20">
<ContentSection
label="The Challenge"
label={t('labels.challenge')}
index="01"
heading="The problem we set out to solve"
body={project.challenge}
heading={t('labels.challengeHeading')}
body={challenge}
/>
{/* Divider */}
@@ -275,10 +263,10 @@ export default async function CaseStudyPage({ params }: Props) {
</ScrollReveal>
<ContentSection
label="Our Approach"
label={t('labels.approach')}
index="02"
heading="How we thought about it"
body={project.approach}
heading={t('labels.approachHeading')}
body={approach}
/>
{/* Divider */}
@@ -294,10 +282,10 @@ export default async function CaseStudyPage({ params }: Props) {
</ScrollReveal>
<ContentSection
label="The Outcome"
label={t('labels.outcome')}
index="03"
heading="What we delivered"
body={project.outcome}
heading={t('labels.outcomeHeading')}
body={outcome}
/>
</div>
@@ -311,7 +299,7 @@ export default async function CaseStudyPage({ params }: Props) {
<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>
<p className="label-md text-outline">{t('labels.builtWith')}</p>
<div className="flex flex-wrap gap-2">
{project.techStack.map((tech) => (
<span
@@ -341,20 +329,19 @@ export default async function CaseStudyPage({ params }: Props) {
<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">Your Turn</p>
<p className="label-md text-primary px-4">{t('labels.yourTurn')}</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 something like this?
{t('labels.ctaTitle')}
</h2>
<p className="text-outline text-lg leading-relaxed max-w-xl">
Every project starts with a conversation. Tell us what you&rsquo;re working on and
we&rsquo;ll figure out the best way to bring it to life.
{t('labels.ctaSubtitle')}
</p>
<Button variant="primary" size="lg" arrow href="/#configure">
Start your project
{t('labels.ctaButton')}
</Button>
</div>