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:
@@ -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’re working on and
|
||||
we’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>
|
||||
|
||||
Reference in New Issue
Block a user