polish: complete copy rewrite + i18n fixes + dead link cleanup
All checks were successful
Build & Push / build-and-push (push) Successful in 1m47s
All checks were successful
Build & Push / build-and-push (push) Successful in 1m47s
- Rewrite all site copy based on founder interview: honest, outcome-focused, premium but approachable tone - Hero: "Websites, software, and infrastructure — designed and built entirely around you." - Services: "Design. Build. Run." with clear pillars (Websites, Custom Software, Infrastructure) - Philosophy: "Your tools should belong to you." — removed Kubernetes reference - Trust bar: concrete value props (Built From Scratch, You Own Everything, One Team, AI) - CTA: "Ready to build something?" — direct, no fluff - Projects: honest descriptions of what was actually delivered - Fix all French translation gaps: hero accent word, configurator cards, project descriptions, footer links, philosophy quote - Fix dead links: footer service links → /#services, work/about → anchors - Restore case study links (dynamic route exists) - Fix mobile hero padding (pt-40 → pt-24) - AI narrative positioned honestly as add-on capability Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,31 +15,28 @@ import type { StepProps } from './WizardContainer';
|
||||
interface ServiceOption {
|
||||
id: string;
|
||||
icon: React.ElementType;
|
||||
title: string;
|
||||
description: string;
|
||||
titleKey: string;
|
||||
descriptionKey: string;
|
||||
}
|
||||
|
||||
const SERVICES: ServiceOption[] = [
|
||||
{
|
||||
id: 'web',
|
||||
icon: Globe,
|
||||
title: 'Web Design & Development',
|
||||
description:
|
||||
'Bespoke websites and web applications built from scratch — pixel-perfect design, blazing performance, and clean code.',
|
||||
titleKey: 'services.web.title',
|
||||
descriptionKey: 'services.web.description',
|
||||
},
|
||||
{
|
||||
id: 'systems',
|
||||
icon: Cog,
|
||||
title: 'Custom Systems',
|
||||
description:
|
||||
'Purpose-built CRMs, internal tools, and business platforms crafted to match exactly how your team works.',
|
||||
titleKey: 'services.systems.title',
|
||||
descriptionKey: 'services.systems.description',
|
||||
},
|
||||
{
|
||||
id: 'infrastructure',
|
||||
icon: Server,
|
||||
title: 'Digital Infrastructure',
|
||||
description:
|
||||
'Private cloud hosting, data sovereignty, DevOps pipelines, and security hardening — your stack, fully owned.',
|
||||
titleKey: 'services.infrastructure.title',
|
||||
descriptionKey: 'services.infrastructure.description',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -78,9 +75,11 @@ interface ServiceCardProps {
|
||||
option: ServiceOption;
|
||||
selected: boolean;
|
||||
onToggle: () => void;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
function ServiceCard({ option, selected, onToggle }: ServiceCardProps) {
|
||||
function ServiceCard({ option, selected, onToggle, title, description }: ServiceCardProps) {
|
||||
const Icon = option.icon;
|
||||
|
||||
return (
|
||||
@@ -117,9 +116,9 @@ function ServiceCard({ option, selected, onToggle }: ServiceCardProps) {
|
||||
selected ? 'text-primary-dark' : 'text-on-surface',
|
||||
)}
|
||||
>
|
||||
{option.title}
|
||||
{title}
|
||||
</p>
|
||||
<p className="text-xs text-outline leading-relaxed">{option.description}</p>
|
||||
<p className="text-xs text-outline leading-relaxed">{description}</p>
|
||||
</div>
|
||||
|
||||
{/* Checkbox */}
|
||||
@@ -160,9 +159,11 @@ function ServiceCard({ option, selected, onToggle }: ServiceCardProps) {
|
||||
interface AIToggleProps {
|
||||
enabled: boolean;
|
||||
onToggle: () => void;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
function AIToggle({ enabled, onToggle }: AIToggleProps) {
|
||||
function AIToggle({ enabled, onToggle, label, description }: AIToggleProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -192,10 +193,10 @@ function AIToggle({ enabled, onToggle }: AIToggleProps) {
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Enhance with AI
|
||||
{label}
|
||||
</span>
|
||||
<p className="text-xs text-outline mt-0.5">
|
||||
We layer intelligent automation into every system we build.
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
{/* Switch */}
|
||||
@@ -269,6 +270,8 @@ export default function StepServices({ formData, setFormData, onNext }: StepProp
|
||||
option={option}
|
||||
selected={formData.services.includes(option.id)}
|
||||
onToggle={() => toggleService(option.id)}
|
||||
title={t(option.titleKey)}
|
||||
description={t(option.descriptionKey)}
|
||||
/>
|
||||
))}
|
||||
{/* Empty-state hint */}
|
||||
@@ -281,7 +284,7 @@ export default function StepServices({ formData, setFormData, onNext }: StepProp
|
||||
transition={{ duration: 0.2 }}
|
||||
className="text-xs text-outline/60 text-center pt-1 pb-0.5 select-none"
|
||||
>
|
||||
Select at least one service to continue
|
||||
{t('selectService')}
|
||||
</motion.p>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@@ -289,7 +292,12 @@ export default function StepServices({ formData, setFormData, onNext }: StepProp
|
||||
|
||||
{/* AI Toggle */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<AIToggle enabled={formData.aiEnabled} onToggle={toggleAI} />
|
||||
<AIToggle
|
||||
enabled={formData.aiEnabled}
|
||||
onToggle={toggleAI}
|
||||
label={t('aiToggle')}
|
||||
description={t('aiDescription')}
|
||||
/>
|
||||
|
||||
{/* AI type chips — stagger in */}
|
||||
<AnimatePresence>
|
||||
|
||||
@@ -8,17 +8,17 @@ import CalButton from '@/components/ui/CalButton'
|
||||
// ── Static link data ─────────────────────────────────────────────────────────
|
||||
|
||||
const SERVICE_LINKS = [
|
||||
{ label: 'Design & Development', href: '/services#design-development' },
|
||||
{ label: 'Custom Systems', href: '/services#custom-systems' },
|
||||
{ label: 'Digital Infrastructure', href: '/services#infrastructure' },
|
||||
{ label: 'AI & Automation', href: '/services#ai-automation' },
|
||||
{ labelKey: 'serviceLinks.designDev', href: '/#services' },
|
||||
{ labelKey: 'serviceLinks.customSystems', href: '/#services' },
|
||||
{ labelKey: 'serviceLinks.infrastructure', href: '/#services' },
|
||||
{ labelKey: 'serviceLinks.aiAutomation', href: '/#services' },
|
||||
] as const
|
||||
|
||||
// Studio links use nav translation keys where they exist (process, work, about)
|
||||
// Studio links use nav translation keys where they exist
|
||||
const STUDIO_NAV_LINKS = [
|
||||
{ navKey: 'process' as const, href: '/#process' },
|
||||
{ navKey: 'work' as const, href: '/work' },
|
||||
{ navKey: 'about' as const, href: '/about' },
|
||||
{ navKey: 'work' as const, href: '/#work' },
|
||||
{ navKey: 'about' as const, href: '/#about' },
|
||||
] as const
|
||||
|
||||
function LinkedInIcon({ className }: { className?: string }) {
|
||||
@@ -131,10 +131,10 @@ export default function Footer() {
|
||||
<div>
|
||||
<FooterHeading>{t('services')}</FooterHeading>
|
||||
<ul className="flex flex-col gap-3" role="list">
|
||||
{SERVICE_LINKS.map(({ label, href }) => (
|
||||
<li key={label}>
|
||||
{SERVICE_LINKS.map(({ labelKey, href }) => (
|
||||
<li key={labelKey}>
|
||||
<InternalLink href={href} className={linkClass}>
|
||||
{label}
|
||||
{t(labelKey)}
|
||||
</InternalLink>
|
||||
</li>
|
||||
))}
|
||||
@@ -155,7 +155,7 @@ export default function Footer() {
|
||||
{/* Contact — separate entry pointing to configure section */}
|
||||
<li>
|
||||
<InternalLink href="/#configure" className={linkClass}>
|
||||
Contact
|
||||
{tNav('configure')}
|
||||
</InternalLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function Configurator() {
|
||||
className="flex flex-col gap-3 pt-2"
|
||||
>
|
||||
<p className="text-xs font-semibold uppercase tracking-label text-outline/70">
|
||||
How it works
|
||||
{t('howItWorks')}
|
||||
</p>
|
||||
{/* Vertical accent line + steps */}
|
||||
<div className="flex gap-4">
|
||||
@@ -99,7 +99,7 @@ export default function Configurator() {
|
||||
className="pt-1 flex items-center gap-2"
|
||||
>
|
||||
<ShieldCheck size={14} strokeWidth={1.75} className="text-primary flex-shrink-0" aria-hidden="true" />
|
||||
<p className="text-xs text-outline">No commitment required</p>
|
||||
<p className="text-xs text-outline">{t('noCommitment')}</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -79,10 +79,9 @@ const rightColumnVariant = {
|
||||
export default function Hero() {
|
||||
const t = useTranslations('hero');
|
||||
|
||||
// Split the raw title on the {exclusively} placeholder
|
||||
// Expected translation: "Built {exclusively} for ambitious brands."
|
||||
// Split the raw title on the {accentWord} placeholder
|
||||
const rawTitle: string = t.raw('title');
|
||||
const parts = rawTitle.split('{exclusively}');
|
||||
const parts = rawTitle.split('{accentWord}');
|
||||
const before = parts[0] ?? '';
|
||||
const after = parts[1] ?? '';
|
||||
|
||||
@@ -99,7 +98,7 @@ export default function Hero() {
|
||||
: [];
|
||||
const allWords: WordItem[] = [
|
||||
...beforeWords,
|
||||
{ type: 'accent', text: 'exclusively' },
|
||||
{ type: 'accent', text: t('accentWord') },
|
||||
...afterWords,
|
||||
];
|
||||
|
||||
@@ -128,7 +127,7 @@ export default function Hero() {
|
||||
<div className="relative z-10 w-full max-w-screen-xl mx-auto px-6 lg:px-12 xl:px-16 flex flex-col lg:flex-row lg:items-center min-h-screen">
|
||||
|
||||
{/* ── LEFT COLUMN — text content (55% on desktop) ─────────────── */}
|
||||
<div className="flex-1 lg:max-w-[58%] flex flex-col justify-center pt-40 pb-16 lg:pt-28 lg:pb-0">
|
||||
<div className="flex-1 lg:max-w-[58%] flex flex-col justify-center pt-24 pb-16 lg:pt-28 lg:pb-0">
|
||||
|
||||
{/* Headline — word-by-word stagger */}
|
||||
<motion.h1
|
||||
@@ -136,7 +135,7 @@ export default function Hero() {
|
||||
variants={headlineContainer}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
aria-label={rawTitle.replace('{exclusively}', 'exclusively')}
|
||||
aria-label={rawTitle.replace('{accentWord}', t('accentWord'))}
|
||||
>
|
||||
{allWords.map((word, i) =>
|
||||
word.type === 'accent' ? (
|
||||
|
||||
@@ -421,7 +421,7 @@ export default function Philosophy() {
|
||||
/>
|
||||
|
||||
{/* Attribution */}
|
||||
<p className="label-md text-outline">Founded on the Côte d’Azur</p>
|
||||
<p className="label-md text-outline">{t('philosophy.foundedLocation')}</p>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -15,46 +15,44 @@ import { Lock, Clock, ArrowRight } from 'lucide-react';
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface Project {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
/** i18n key under "work.projects" (e.g. "monaco") */
|
||||
i18nKey: string;
|
||||
slug: string;
|
||||
/** number of tags to resolve from the translation array */
|
||||
tagCount: number;
|
||||
featured?: boolean;
|
||||
}
|
||||
|
||||
interface ComingSoonItem {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
/** i18n key under "work.comingSoonProjects" (e.g. "riviera") */
|
||||
i18nKey: string;
|
||||
confidential?: boolean;
|
||||
}
|
||||
|
||||
// ─── Data ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
const PROJECTS: Project[] = [
|
||||
{
|
||||
title: 'Monaco Ocean Protection Challenge',
|
||||
description:
|
||||
"A comprehensive judging and analytics system with advanced AI jury integration for one of the Mediterranean's most prestigious conservation events.",
|
||||
tags: ['AI Integration', 'Platform'],
|
||||
i18nKey: 'monaco',
|
||||
slug: 'monaco-ocean',
|
||||
tagCount: 2,
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
title: 'Port Nimara',
|
||||
description: 'Scalable digital hub for maritime logistics.',
|
||||
tags: ['Website', 'Infrastructure'],
|
||||
i18nKey: 'portNimara',
|
||||
slug: 'port-nimara',
|
||||
tagCount: 2,
|
||||
},
|
||||
{
|
||||
title: 'Port Amador',
|
||||
description: 'Premium digital experience for elite nautical services.',
|
||||
tags: ['Website', 'Infrastructure'],
|
||||
i18nKey: 'portAmador',
|
||||
slug: 'port-amador',
|
||||
tagCount: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const COMING_SOON: ComingSoonItem[] = [
|
||||
{ title: 'Confidential Riviera Project', subtitle: 'Coming Soon' },
|
||||
{ title: 'Sophia Antipolis AI Startup', subtitle: 'Launching Q4' },
|
||||
{ i18nKey: 'riviera', confidential: true },
|
||||
{ i18nKey: 'sophia' },
|
||||
];
|
||||
|
||||
// ─── Animation Variants ───────────────────────────────────────────────────────
|
||||
@@ -371,7 +369,11 @@ function TagChip({ label, showDot = false }: { label: string; showDot?: boolean
|
||||
|
||||
// ─── Featured Card ────────────────────────────────────────────────────────────
|
||||
|
||||
function FeaturedCard({ project, readLabel }: { project: Project; readLabel: string }) {
|
||||
function FeaturedCard({ project, readLabel, t }: { project: Project; readLabel: string; t: (key: string) => string }) {
|
||||
const tags = Array.from({ length: project.tagCount }, (_, i) =>
|
||||
t(`work.projects.${project.i18nKey}.tags.${i}`),
|
||||
);
|
||||
|
||||
return (
|
||||
<motion.article
|
||||
variants={featuredCardVariants}
|
||||
@@ -392,24 +394,24 @@ function FeaturedCard({ project, readLabel }: { project: Project; readLabel: str
|
||||
<div className="flex flex-col flex-1 p-7 gap-4">
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.tags.map((tag, i) => (
|
||||
{tags.map((tag, i) => (
|
||||
<TagChip key={tag} label={tag} showDot={i > 0} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-serif text-2xl font-semibold text-on-surface leading-snug">
|
||||
{project.title}
|
||||
{t(`work.projects.${project.i18nKey}.title`)}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-sm text-outline leading-relaxed flex-1">
|
||||
{project.description}
|
||||
{t(`work.projects.${project.i18nKey}.description`)}
|
||||
</p>
|
||||
|
||||
{/* CTA */}
|
||||
<Link
|
||||
href={`/work/${project.slug}`}
|
||||
href={`/work/${project.slug}` as any}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 text-sm font-medium text-primary-dark',
|
||||
'mt-1 group/link',
|
||||
@@ -435,8 +437,11 @@ const SLUG_TO_VARIANT: Record<string, 'nimara' | 'amador'> = {
|
||||
'port-amador': 'amador',
|
||||
};
|
||||
|
||||
function SmallCard({ project, readLabel }: { project: Project; readLabel: string }) {
|
||||
function SmallCard({ project, readLabel, t }: { project: Project; readLabel: string; t: (key: string) => string }) {
|
||||
const cardVariant = SLUG_TO_VARIANT[project.slug] ?? 'nimara';
|
||||
const tags = Array.from({ length: project.tagCount }, (_, i) =>
|
||||
t(`work.projects.${project.i18nKey}.tags.${i}`),
|
||||
);
|
||||
|
||||
return (
|
||||
<motion.article
|
||||
@@ -469,24 +474,24 @@ function SmallCard({ project, readLabel }: { project: Project; readLabel: string
|
||||
<div className="flex flex-col flex-1 p-5 gap-3">
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{project.tags.map((tag, i) => (
|
||||
{tags.map((tag, i) => (
|
||||
<TagChip key={tag} label={tag} showDot={i > 0} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-serif text-lg font-semibold text-on-surface leading-snug">
|
||||
{project.title}
|
||||
{t(`work.projects.${project.i18nKey}.title`)}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-xs text-outline leading-relaxed flex-1">
|
||||
{project.description}
|
||||
{t(`work.projects.${project.i18nKey}.description`)}
|
||||
</p>
|
||||
|
||||
{/* CTA */}
|
||||
<Link
|
||||
href={`/work/${project.slug}`}
|
||||
href={`/work/${project.slug}` as any}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-medium text-primary-dark group/link"
|
||||
>
|
||||
<span className="relative after:absolute after:bottom-0 after:left-0 after:h-px after:w-full after:bg-primary-dark after:origin-left after:scale-x-100 after:transition-transform after:duration-200">
|
||||
@@ -504,9 +509,8 @@ function SmallCard({ project, readLabel }: { project: Project; readLabel: string
|
||||
|
||||
// ─── Coming Soon Card ─────────────────────────────────────────────────────────
|
||||
|
||||
function ComingSoonCard({ item }: { item: ComingSoonItem }) {
|
||||
const isConfidential = item.subtitle === 'Coming Soon';
|
||||
const Icon = isConfidential ? Lock : Clock;
|
||||
function ComingSoonCard({ item, t }: { item: ComingSoonItem; t: (key: string) => string }) {
|
||||
const Icon = item.confidential ? Lock : Clock;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -533,9 +537,11 @@ function ComingSoonCard({ item }: { item: ComingSoonItem }) {
|
||||
/>
|
||||
<div>
|
||||
<p className="font-serif text-base font-medium text-on-surface/60 leading-snug">
|
||||
{item.title}
|
||||
{t(`work.comingSoonProjects.${item.i18nKey}.title`)}
|
||||
</p>
|
||||
<p className="label-md text-outline/70 mt-1">
|
||||
{t(`work.comingSoonProjects.${item.i18nKey}.subtitle`)}
|
||||
</p>
|
||||
<p className="label-md text-outline/70 mt-1">{item.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -609,6 +615,7 @@ export default function SelectedWorks() {
|
||||
<FeaturedCard
|
||||
project={featuredProject}
|
||||
readLabel={t('work.readCaseStudy')}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -619,6 +626,7 @@ export default function SelectedWorks() {
|
||||
key={project.slug}
|
||||
project={project}
|
||||
readLabel={t('work.readCaseStudy')}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -633,7 +641,7 @@ export default function SelectedWorks() {
|
||||
className="grid grid-cols-1 sm:grid-cols-2 gap-5"
|
||||
>
|
||||
{COMING_SOON.map((item) => (
|
||||
<ComingSoonCard key={item.title} item={item} />
|
||||
<ComingSoonCard key={item.i18nKey} item={item} t={t} />
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user