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 ScrollReveal from '@/components/ui/ScrollReveal';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import CornerBracket from '@/components/icons/CornerBracket'; import CornerBracket from '@/components/icons/CornerBracket';
import Chip from '@/components/ui/Chip';
const BASE_URL = 'https://letsbe.biz'; const BASE_URL = 'https://letsbe.biz';
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────
interface Project { interface Project {
title: string;
subtitle: string;
description: string;
challenge: string;
approach: string;
outcome: string;
techStack: string[];
tags: string[];
image: 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) ──────────────────────────────────────── // ─── Data (will come from Payload CMS) ────────────────────────────────────────
const PROJECTS: Record<string, Project> = { const PROJECTS: Record<string, Project> = {
'monaco-ocean': { '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', image: '/images/monaco_high_res.jpg',
techStack: ['Next.js', 'PostgreSQL', 'OpenAI API', 'Docker', 'Private Cloud'],
}, },
'port-nimara': { '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', image: '/images/anguilla.png',
techStack: ['Nuxt.js', 'Directus CMS', 'Node.js', 'Docker'],
}, },
'port-amador': { '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', 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]; const project = PROJECTS[slug];
if (!project) return {}; if (!project) return {};
const workKey = SLUG_TO_KEY[slug];
if (!workKey) return {};
const t = await getTranslations({ locale, namespace: 'meta.work' }); const t = await getTranslations({ locale, namespace: 'meta.work' });
const path = locale === 'en' ? `/work/${slug}` : `/${locale}/work/${slug}`; const path = locale === 'en' ? `/work/${slug}` : `/${locale}/work/${slug}`;
@@ -161,12 +133,28 @@ export default async function CaseStudyPage({ params }: Props) {
const project = PROJECTS[slug]; const project = PROJECTS[slug];
if (!project) notFound(); 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 = { const caseStudyJsonLd = {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'CreativeWork', '@type': 'CreativeWork',
name: project.title, name: title,
description: project.description, description: description,
image: `${BASE_URL}${project.image}`, image: `${BASE_URL}${project.image}`,
inLanguage: locale,
creator: { creator: {
'@type': 'Organization', '@type': 'Organization',
name: 'LetsBe.', name: 'LetsBe.',
@@ -201,7 +189,7 @@ export default async function CaseStudyPage({ params }: Props) {
{/* Tags */} {/* Tags */}
<ScrollReveal variant="fadeIn"> <ScrollReveal variant="fadeIn">
<div className="flex flex-wrap items-center justify-center gap-2"> <div className="flex flex-wrap items-center justify-center gap-2">
{project.tags.map((tag) => ( {tags.map((tag) => (
<span <span
key={tag} 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" 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 */} {/* Title */}
<ScrollReveal variant="fadeUp" delay={0.08}> <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]"> <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> </h1>
</ScrollReveal> </ScrollReveal>
{/* Subtitle */} {/* Subtitle */}
<ScrollReveal variant="fadeUp" delay={0.16}> <ScrollReveal variant="fadeUp" delay={0.16}>
<p className="label-md text-white/70 tracking-widest"> <p className="label-md text-white/70 tracking-widest">
{project.subtitle} {subtitle}
</p> </p>
</ScrollReveal> </ScrollReveal>
{/* Description */} {/* Description */}
<ScrollReveal variant="fadeUp" delay={0.22}> <ScrollReveal variant="fadeUp" delay={0.22}>
<p className="text-white/80 text-lg leading-relaxed max-w-2xl"> <p className="text-white/80 text-lg leading-relaxed max-w-2xl">
{project.description} {description}
</p> </p>
</ScrollReveal> </ScrollReveal>
@@ -256,10 +244,10 @@ export default async function CaseStudyPage({ params }: Props) {
<div className="flex flex-col gap-16 md:gap-20"> <div className="flex flex-col gap-16 md:gap-20">
<ContentSection <ContentSection
label="The Challenge" label={t('labels.challenge')}
index="01" index="01"
heading="The problem we set out to solve" heading={t('labels.challengeHeading')}
body={project.challenge} body={challenge}
/> />
{/* Divider */} {/* Divider */}
@@ -275,10 +263,10 @@ export default async function CaseStudyPage({ params }: Props) {
</ScrollReveal> </ScrollReveal>
<ContentSection <ContentSection
label="Our Approach" label={t('labels.approach')}
index="02" index="02"
heading="How we thought about it" heading={t('labels.approachHeading')}
body={project.approach} body={approach}
/> />
{/* Divider */} {/* Divider */}
@@ -294,10 +282,10 @@ export default async function CaseStudyPage({ params }: Props) {
</ScrollReveal> </ScrollReveal>
<ContentSection <ContentSection
label="The Outcome" label={t('labels.outcome')}
index="03" index="03"
heading="What we delivered" heading={t('labels.outcomeHeading')}
body={project.outcome} body={outcome}
/> />
</div> </div>
@@ -311,7 +299,7 @@ export default async function CaseStudyPage({ params }: Props) {
<div className="container mx-auto px-6"> <div className="container mx-auto px-6">
<div className="max-w-3xl mx-auto"> <div className="max-w-3xl mx-auto">
<ScrollReveal variant="fadeUp" className="flex flex-col gap-5"> <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"> <div className="flex flex-wrap gap-2">
{project.techStack.map((tech) => ( {project.techStack.map((tech) => (
<span <span
@@ -341,20 +329,19 @@ export default async function CaseStudyPage({ params }: Props) {
<div className="absolute -top-3 -right-3" aria-hidden="true"> <div className="absolute -top-3 -right-3" aria-hidden="true">
<CornerBracket size={24} position="top-right" color="var(--color-teal)" /> <CornerBracket size={24} position="top-right" color="var(--color-teal)" />
</div> </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> </div>
<h2 className="font-serif font-semibold text-on-surface text-3xl md:text-4xl leading-[1.1] tracking-[-0.02em]"> <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> </h2>
<p className="text-outline text-lg leading-relaxed max-w-xl"> <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 {t('labels.ctaSubtitle')}
we&rsquo;ll figure out the best way to bring it to life.
</p> </p>
<Button variant="primary" size="lg" arrow href="/#configure"> <Button variant="primary" size="lg" arrow href="/#configure">
Start your project {t('labels.ctaButton')}
</Button> </Button>
</div> </div>