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:
464
src/components/sections/SelectedWorks.tsx
Normal file
464
src/components/sections/SelectedWorks.tsx
Normal file
@@ -0,0 +1,464 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
staggerContainerWide,
|
||||
slideLeftVariants,
|
||||
fadeVariants,
|
||||
viewportOnce,
|
||||
} from '@/lib/animations';
|
||||
import { Link } from '@/i18n/navigation';
|
||||
import { Lock, Clock, ArrowRight } from 'lucide-react';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface Project {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
slug: string;
|
||||
featured?: boolean;
|
||||
}
|
||||
|
||||
interface ComingSoonItem {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
// ─── 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'],
|
||||
slug: 'monaco-ocean',
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
title: 'Port Nimara',
|
||||
description: 'Scalable digital hub for maritime logistics.',
|
||||
tags: ['Website', 'Infrastructure'],
|
||||
slug: 'port-nimara',
|
||||
},
|
||||
{
|
||||
title: 'Port Amador',
|
||||
description: 'Premium digital experience for elite nautical services.',
|
||||
tags: ['Website', 'Infrastructure'],
|
||||
slug: 'port-amador',
|
||||
},
|
||||
];
|
||||
|
||||
const COMING_SOON: ComingSoonItem[] = [
|
||||
{ title: 'Confidential Riviera Project', subtitle: 'Coming Soon' },
|
||||
{ title: 'Sophia Antipolis AI Startup', subtitle: 'Launching Q4' },
|
||||
];
|
||||
|
||||
// ─── Animation Variants ───────────────────────────────────────────────────────
|
||||
|
||||
const ease = [0.16, 1, 0.3, 1] as const;
|
||||
|
||||
const featuredCardVariants = {
|
||||
hidden: { opacity: 0, y: 48 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.7, ease },
|
||||
},
|
||||
};
|
||||
|
||||
const smallCardVariants = {
|
||||
hidden: { opacity: 0, y: 32 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.6, ease },
|
||||
},
|
||||
};
|
||||
|
||||
const comingSoonVariants = {
|
||||
hidden: { opacity: 0, y: 24 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.55, ease },
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Geometric Placeholder ────────────────────────────────────────────────────
|
||||
|
||||
function GeometricPlaceholder({
|
||||
variant = 'featured',
|
||||
className,
|
||||
}: {
|
||||
variant?: 'featured' | 'small';
|
||||
className?: string;
|
||||
}) {
|
||||
const isFeatured = variant === 'featured';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative overflow-hidden bg-gradient-to-br from-primary-dark/90 to-primary/70',
|
||||
className,
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Abstract geometric shapes */}
|
||||
<div className="absolute inset-0">
|
||||
{/* Large circle top-right */}
|
||||
<div
|
||||
className="absolute rounded-full bg-white/[0.06]"
|
||||
style={{
|
||||
width: isFeatured ? '55%' : '70%',
|
||||
height: isFeatured ? '55%' : '70%',
|
||||
top: '-15%',
|
||||
right: '-10%',
|
||||
}}
|
||||
/>
|
||||
{/* Medium circle bottom-left */}
|
||||
<div
|
||||
className="absolute rounded-full bg-white/[0.08]"
|
||||
style={{
|
||||
width: isFeatured ? '40%' : '50%',
|
||||
height: isFeatured ? '40%' : '50%',
|
||||
bottom: '-20%',
|
||||
left: '-5%',
|
||||
}}
|
||||
/>
|
||||
{/* Diagonal stripes overlay */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.04]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'repeating-linear-gradient(-45deg, #fff 0, #fff 1px, transparent 0, transparent 50%)',
|
||||
backgroundSize: isFeatured ? '28px 28px' : '20px 20px',
|
||||
}}
|
||||
/>
|
||||
{/* Small accent rectangle */}
|
||||
<div
|
||||
className="absolute bg-white/[0.12] rounded-sm"
|
||||
style={{
|
||||
width: isFeatured ? '18%' : '24%',
|
||||
height: isFeatured ? '28%' : '36%',
|
||||
bottom: '18%',
|
||||
right: '15%',
|
||||
transform: 'rotate(-6deg)',
|
||||
}}
|
||||
/>
|
||||
{/* Thin horizontal line accent */}
|
||||
<div
|
||||
className="absolute bg-white/20 rounded-full"
|
||||
style={{
|
||||
width: isFeatured ? '30%' : '40%',
|
||||
height: '1px',
|
||||
top: '38%',
|
||||
left: '10%',
|
||||
}}
|
||||
/>
|
||||
{/* Grid-dot accent */}
|
||||
<div
|
||||
className="absolute opacity-[0.07]"
|
||||
style={{
|
||||
width: isFeatured ? '25%' : '30%',
|
||||
height: isFeatured ? '25%' : '30%',
|
||||
top: '50%',
|
||||
right: '22%',
|
||||
backgroundImage: 'radial-gradient(circle, #fff 1px, transparent 1px)',
|
||||
backgroundSize: '8px 8px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Bottom gradient fade */}
|
||||
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-primary-dark/50 to-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Tag Chip ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function TagChip({ label }: { label: string }) {
|
||||
return (
|
||||
<span className="inline-flex items-center bg-primary/10 text-primary-dark text-xs font-medium px-2.5 py-1 rounded-full leading-none">
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Featured Card ────────────────────────────────────────────────────────────
|
||||
|
||||
function FeaturedCard({ project, readLabel }: { project: Project; readLabel: string }) {
|
||||
return (
|
||||
<motion.article
|
||||
variants={featuredCardVariants}
|
||||
className={cn(
|
||||
'group relative flex flex-col bg-surface-high rounded-2xl overflow-hidden',
|
||||
'shadow-subtle',
|
||||
'transition-shadow duration-300 hover:shadow-[0_24px_48px_rgba(25,28,29,0.10)]',
|
||||
)}
|
||||
>
|
||||
{/* Geometric image placeholder */}
|
||||
<GeometricPlaceholder
|
||||
variant="featured"
|
||||
className="w-full aspect-[16/9] md:aspect-[2/1]"
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex flex-col flex-1 p-7 gap-4">
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.tags.map((tag) => (
|
||||
<TagChip key={tag} label={tag} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-serif text-2xl font-semibold text-on-surface leading-snug">
|
||||
{project.title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-sm text-outline leading-relaxed flex-1">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
{/* CTA */}
|
||||
<Link
|
||||
href={`/work/${project.slug}`}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 text-sm font-medium text-primary-dark',
|
||||
'transition-gap duration-200 group/link',
|
||||
'mt-1',
|
||||
)}
|
||||
>
|
||||
<span className="underline underline-offset-4 decoration-primary/40 group-hover/link:decoration-primary-dark transition-colors duration-200">
|
||||
{readLabel}
|
||||
</span>
|
||||
<ArrowRight
|
||||
size={14}
|
||||
className="transition-transform duration-200 group-hover/link:translate-x-1"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</motion.article>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Small Card ───────────────────────────────────────────────────────────────
|
||||
|
||||
function SmallCard({ project, readLabel }: { project: Project; readLabel: string }) {
|
||||
return (
|
||||
<motion.article
|
||||
variants={smallCardVariants}
|
||||
className={cn(
|
||||
'group relative flex flex-col bg-surface-high rounded-xl overflow-hidden',
|
||||
'shadow-card',
|
||||
'transition-all duration-300',
|
||||
'hover:shadow-subtle',
|
||||
)}
|
||||
>
|
||||
{/* Geometric placeholder — grayscale to color on hover */}
|
||||
<div className="relative overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'transition-all duration-500',
|
||||
'grayscale group-hover:grayscale-0',
|
||||
'opacity-80 group-hover:opacity-100',
|
||||
)}
|
||||
>
|
||||
<GeometricPlaceholder
|
||||
variant="small"
|
||||
className="w-full aspect-[16/7]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex flex-col flex-1 p-5 gap-3">
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{project.tags.map((tag) => (
|
||||
<TagChip key={tag} label={tag} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-serif text-lg font-semibold text-on-surface leading-snug">
|
||||
{project.title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-xs text-outline leading-relaxed flex-1">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
{/* CTA */}
|
||||
<Link
|
||||
href={`/work/${project.slug}`}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-medium text-primary-dark group/link"
|
||||
>
|
||||
<span className="underline underline-offset-4 decoration-primary/40 group-hover/link:decoration-primary-dark transition-colors duration-200">
|
||||
{readLabel}
|
||||
</span>
|
||||
<ArrowRight
|
||||
size={12}
|
||||
className="transition-transform duration-200 group-hover/link:translate-x-0.5"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</motion.article>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Coming Soon Card ─────────────────────────────────────────────────────────
|
||||
|
||||
function ComingSoonCard({ item }: { item: ComingSoonItem }) {
|
||||
const isConfidential = item.subtitle === 'Coming Soon';
|
||||
const Icon = isConfidential ? Lock : Clock;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
variants={comingSoonVariants}
|
||||
className={cn(
|
||||
'relative flex flex-col items-center justify-center gap-4',
|
||||
'rounded-xl p-8 min-h-[160px]',
|
||||
'border border-dashed border-outline-variant/30',
|
||||
'coming-soon-card',
|
||||
'overflow-hidden',
|
||||
)}
|
||||
>
|
||||
{/* Subtle animated pulse overlay */}
|
||||
<div
|
||||
className="absolute inset-0 rounded-xl opacity-0 coming-soon-pulse"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center gap-3 text-center">
|
||||
<Icon
|
||||
size={20}
|
||||
className="text-outline/40"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<div>
|
||||
<p className="font-serif text-base font-medium text-on-surface/40 leading-snug">
|
||||
{item.title}
|
||||
</p>
|
||||
<p className="label-md text-outline/50 mt-1">{item.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||
|
||||
export default function SelectedWorks() {
|
||||
const t = useTranslations();
|
||||
|
||||
const featuredProject = PROJECTS.find((p) => p.featured)!;
|
||||
const secondaryProjects = PROJECTS.filter((p) => !p.featured);
|
||||
|
||||
return (
|
||||
<section id="work" className="bg-surface-low py-24">
|
||||
<style>{`
|
||||
@keyframes dashed-drift {
|
||||
0% { background-position: 0 0, 100% 0, 100% 100%, 0 100%; }
|
||||
100% { background-position: 100% 0, 100% 100%, 0 100%, 0 0; }
|
||||
}
|
||||
@keyframes coming-soon-fade {
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
.coming-soon-pulse {
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(91, 164, 217, 0.04) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
animation: coming-soon-fade 4s ease-in-out infinite;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="container mx-auto px-6">
|
||||
|
||||
{/* ── Section Header ── */}
|
||||
<motion.div
|
||||
variants={slideLeftVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewportOnce}
|
||||
className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-14"
|
||||
>
|
||||
<div>
|
||||
<p className="label-md text-primary mb-2">{t('work.eyebrow')}</p>
|
||||
<h2 className="font-serif text-4xl md:text-5xl font-semibold text-on-surface leading-[1.1] tracking-[-0.02em]">
|
||||
{t('work.title')}
|
||||
</h2>
|
||||
</div>
|
||||
<motion.div
|
||||
variants={fadeVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewportOnce}
|
||||
>
|
||||
<Link
|
||||
href="/work"
|
||||
className="inline-flex items-center gap-2 text-sm font-medium text-outline hover:text-primary-dark transition-colors duration-200 group"
|
||||
>
|
||||
<span>View all work</span>
|
||||
<ArrowRight
|
||||
size={14}
|
||||
className="transition-transform duration-200 group-hover:translate-x-1"
|
||||
/>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* ── Primary Grid: Featured + 2 Small ── */}
|
||||
<motion.div
|
||||
variants={staggerContainerWide}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewportOnce}
|
||||
className="grid grid-cols-1 md:grid-cols-12 gap-5 mb-5"
|
||||
>
|
||||
{/* Featured card — 8 cols */}
|
||||
<div className="md:col-span-8">
|
||||
<FeaturedCard
|
||||
project={featuredProject}
|
||||
readLabel={t('work.readCaseStudy')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Secondary column — 4 cols, 2 stacked */}
|
||||
<div className="md:col-span-4 flex flex-col gap-5">
|
||||
{secondaryProjects.map((project) => (
|
||||
<SmallCard
|
||||
key={project.slug}
|
||||
project={project}
|
||||
readLabel={t('work.readCaseStudy')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* ── Coming Soon Row ── */}
|
||||
<motion.div
|
||||
variants={staggerContainerWide}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewportOnce}
|
||||
className="grid grid-cols-1 sm:grid-cols-2 gap-5"
|
||||
>
|
||||
{COMING_SOON.map((item) => (
|
||||
<ComingSoonCard key={item.title} item={item} />
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user