465 lines
15 KiB
TypeScript
465 lines
15 KiB
TypeScript
|
|
'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>
|
||
|
|
);
|
||
|
|
}
|