Deux courbes partagées dans lib/motion.ts, pour que le CSS et Framer aient le même feeling, et quatre primitives que Tailwind ne sait pas exprimer : reflet de bouton, halo qui suit le curseur, soulignement qui se dessine, secousse d'erreur. Nouveaux composants : Stagger (les collections se distribuent une par une), Spotlight (trouve son parent tout seul, donc les cartes restent des composants serveur), ScrollProgress, BackToTop, Parallax, Spinner, CheckMark. Reveal accepte maintenant une direction. Côté rendu : le trait de nav glisse d'un onglet à l'autre, les cartes se soulèvent, le hero réagit au scroll, les piliers entrent en zig-zag, la lightbox glisse dans le sens demandé et les formulaires répondent. prefers-reduced-motion est respecté partout : Framer coupe via useReducedMotion, le CSS via la media query déjà en place. Le commit embarque aussi la réorganisation des composants par domaine qui était en cours dans l'arbre de travail — les deux étaient trop imbriquées pour être séparées proprement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+27
-353
@@ -1,364 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import { games } from "@/lib/games";
|
||||
|
||||
const featured = games[0];
|
||||
|
||||
// Screenshots WIP du projet.
|
||||
// Pour en ajouter : dépose l'image dans /public/image/Emberwild/
|
||||
// puis renseigne `src` (ex: "/image/Emberwild/screen-01.jpg").
|
||||
// Laisse `src: null` pour afficher un emplacement placeholder.
|
||||
const screenshots: { src: string | null; label: string; span: string }[] = [
|
||||
{
|
||||
src: "/image/Games/Emberwild/Capture_1.png",
|
||||
label: "In-game",
|
||||
span: "col-span-2 row-span-2",
|
||||
},
|
||||
{
|
||||
src: "/image/Games/Emberwild/Capture_2.png",
|
||||
label: "Environment",
|
||||
span: "",
|
||||
},
|
||||
{
|
||||
src: "/image/Games/Emberwild/Capture_3.png",
|
||||
label: "Exploration",
|
||||
span: "",
|
||||
},
|
||||
];
|
||||
import { HomeHero } from "./components/home/HomeHero";
|
||||
import { Statement } from "./components/home/Statement";
|
||||
import { FeaturedGame } from "./components/home/FeaturedGame";
|
||||
import { StudioIntro } from "./components/home/StudioIntro";
|
||||
import { Dispatch } from "./components/home/Dispatch";
|
||||
import { featuredGame } from "@/lib/games";
|
||||
import { getAllPosts } from "@/lib/devblog";
|
||||
|
||||
/**
|
||||
* Home page.
|
||||
*
|
||||
* Read top to bottom it answers four questions in order: what is this
|
||||
* (hero), why should I care (statement), what are you making (featured game),
|
||||
* who are you (studio), how do I follow it (dispatch). Each band changes
|
||||
* rhythm — full-bleed, quiet, card, split, conversion — so the page never
|
||||
* reads as one long column of identical text blocks.
|
||||
*
|
||||
* Game data reads from `lib/games.ts`, so the home page can never advertise
|
||||
* something the game page doesn't show. Screenshots deliberately live only on
|
||||
* /games/[slug] — the home page teases the game, the game page shows it.
|
||||
*/
|
||||
export default function Home() {
|
||||
const latest = getAllPosts()[0] ?? null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* HERO */}
|
||||
<section className="relative isolate min-h-screen flex items-center justify-center overflow-hidden">
|
||||
{/* Background video */}
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="auto"
|
||||
aria-hidden
|
||||
className="absolute inset-0 z-0 w-full h-full object-cover"
|
||||
>
|
||||
<source src="/Video/Header_Video.mp4" type="video/mp4" />
|
||||
</video>
|
||||
{/* Dark overlay for text readability */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 z-10 bg-background/60"
|
||||
/>
|
||||
{/* Vignette / radial fade to deepen the edges */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(ellipse at 50% 50%, rgba(10,10,10,0) 30%, rgba(10,10,10,0.85) 100%)",
|
||||
}}
|
||||
/>
|
||||
{/* Grain / noise overlay via CSS gradient */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 z-10 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"repeating-linear-gradient(0deg, #fff 0, #fff 1px, transparent 1px, transparent 3px)",
|
||||
}}
|
||||
/>
|
||||
<HomeHero />
|
||||
|
||||
<div className="relative z-20 max-w-5xl w-full px-6 lg:px-10 flex flex-col items-center text-center pt-20">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.92, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
transition={{ duration: 1.1, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="mb-10"
|
||||
>
|
||||
<Image
|
||||
src="/image/Studio/Logo Flat.png"
|
||||
alt="Highland Games Studio"
|
||||
width={180}
|
||||
height={120}
|
||||
priority
|
||||
className="drop-shadow-[0_0_40px_rgba(196,184,150,0.15)]"
|
||||
/>
|
||||
</motion.div>
|
||||
<Statement />
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.9, delay: 0.3, ease: "easeOut" }}
|
||||
className="font-display text-6xl sm:text-7xl lg:text-8xl tracking-wider leading-[0.95] text-balance"
|
||||
>
|
||||
HIGHLAND GAMES
|
||||
<br />
|
||||
<span className="text-accent">STUDIO</span>
|
||||
</motion.h1>
|
||||
{featuredGame && <FeaturedGame game={featuredGame} />}
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.9, delay: 0.6, ease: "easeOut" }}
|
||||
className="mt-8 text-lg sm:text-xl text-foreground-muted tracking-wide max-w-xl text-balance"
|
||||
>
|
||||
Where summits become worlds.
|
||||
</motion.p>
|
||||
<StudioIntro />
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.9, delay: 0.9, ease: "easeOut" }}
|
||||
className="mt-12 flex flex-col sm:flex-row gap-4"
|
||||
>
|
||||
<Link
|
||||
href="/games"
|
||||
className="px-8 py-3 bg-accent text-background font-display tracking-widest text-sm hover:bg-foreground hover:scale-[1.03] transition duration-200"
|
||||
>
|
||||
EXPLORE OUR GAMES
|
||||
</Link>
|
||||
<Link
|
||||
href="/studio"
|
||||
className="px-8 py-3 border border-border text-foreground font-display tracking-widest text-sm hover:border-accent hover:text-accent hover:scale-[1.03] transition duration-200"
|
||||
>
|
||||
MEET THE STUDIO
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Scroll indicator - positioned relative to the section */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.5, duration: 1 }}
|
||||
className="absolute z-20 bottom-10 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 text-foreground-muted text-xs tracking-widest"
|
||||
>
|
||||
<span className="font-display">SCROLL</span>
|
||||
<motion.div
|
||||
animate={{ y: [0, 8, 0] }}
|
||||
transition={{ duration: 1.8, repeat: Infinity, ease: "easeInOut" }}
|
||||
className="w-px h-8 bg-foreground-muted"
|
||||
/>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* STUDIO INTRO */}
|
||||
<section className="py-24 px-6 lg:px-10">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
— THE STUDIO
|
||||
</p>
|
||||
<h2 className="font-display text-4xl sm:text-5xl lg:text-6xl tracking-wider leading-tight mb-8">
|
||||
Worlds beyond
|
||||
<br />
|
||||
the summit.
|
||||
</h2>
|
||||
<p className="text-lg text-foreground-muted leading-relaxed max-w-2xl">
|
||||
We build worlds where every horizon hides a story. Hand-crafted
|
||||
games made to be lived in, not just played through. Each one a
|
||||
different summit, a different climb.
|
||||
</p>
|
||||
<Link
|
||||
href="/studio"
|
||||
className="inline-block mt-8 font-display text-sm tracking-widest text-accent hover:text-foreground transition-colors"
|
||||
>
|
||||
LEARN MORE →
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FEATURED GAME */}
|
||||
<section className="py-24 px-6 lg:px-10 border-t border-border">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
— IN DEVELOPMENT
|
||||
</p>
|
||||
<h2 className="font-display text-4xl sm:text-5xl lg:text-6xl tracking-wider mb-12">
|
||||
OUR FIRST WORLD
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.9, delay: 0.1 }}
|
||||
>
|
||||
<Link
|
||||
href="/games/emberwild"
|
||||
className="group relative aspect-[16/9] bg-surface border border-border overflow-hidden block hover:border-accent transition-colors duration-500"
|
||||
>
|
||||
{featured?.cover ? (
|
||||
<Image
|
||||
src={featured.cover}
|
||||
alt={featured.title}
|
||||
fill
|
||||
sizes="(max-width: 1152px) 100vw, 1152px"
|
||||
className="object-cover group-hover:scale-105 transition-transform duration-700 ease-out"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Image
|
||||
src="/image/Studio/Logo Flat.png"
|
||||
alt="Game placeholder"
|
||||
width={120}
|
||||
height={80}
|
||||
className="opacity-20 group-hover:opacity-30 group-hover:scale-105 transition-all duration-700 ease-out"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 bg-accent/0 group-hover:bg-accent/[0.03] transition-colors duration-500"
|
||||
/>
|
||||
<div className="absolute bottom-0 left-0 right-0 p-8 bg-gradient-to-t from-background via-background/70 to-transparent">
|
||||
<p className="font-display text-xs tracking-[0.3em] text-accent mb-2">
|
||||
SURVIVAL · CRAFTING · EXPLORATION
|
||||
</p>
|
||||
<h3 className="font-display text-3xl sm:text-4xl tracking-wider mb-4 group-hover:text-accent transition-colors duration-300">
|
||||
PROJECT ONE
|
||||
</h3>
|
||||
<p className="text-foreground-muted max-w-xl mb-6">
|
||||
Inspired by Raft and Aloft. Build, explore, and survive across
|
||||
shifting horizons. A new survival adventure, currently in early
|
||||
development.
|
||||
</p>
|
||||
<span className="inline-flex items-center gap-2 font-display text-sm tracking-widest text-foreground group-hover:text-accent transition-colors">
|
||||
LEARN MORE
|
||||
<span className="inline-block transition-transform duration-300 group-hover:translate-x-1">
|
||||
→
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* GALLERY */}
|
||||
<section className="py-24 px-6 lg:px-10 border-t border-border">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
— GLIMPSES
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-12">
|
||||
<h2 className="font-display text-4xl sm:text-5xl lg:text-6xl tracking-wider">
|
||||
FROM THE WORKSHOP
|
||||
</h2>
|
||||
<p className="text-sm text-foreground-muted max-w-sm">
|
||||
Raw captures straight from development. Everything here is work
|
||||
in progress and will keep evolving.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4 auto-rows-[160px] sm:auto-rows-[220px]">
|
||||
{screenshots.map((shot, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, scale: 0.96 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true, margin: "-60px" }}
|
||||
transition={{ duration: 0.6, delay: i * 0.08 }}
|
||||
className={`group relative overflow-hidden rounded-2xl border border-border bg-surface ${shot.span}`}
|
||||
>
|
||||
{shot.src ? (
|
||||
<Image
|
||||
src={shot.src}
|
||||
alt={shot.label}
|
||||
fill
|
||||
sizes="(max-width: 1024px) 50vw, 33vw"
|
||||
className="object-cover transition-transform duration-700 ease-out group-hover:scale-105"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-3">
|
||||
<Image
|
||||
src="/image/Studio/Logo Flat.png"
|
||||
alt=""
|
||||
width={64}
|
||||
height={44}
|
||||
className="opacity-[0.12]"
|
||||
/>
|
||||
<span className="font-display text-[11px] tracking-[0.3em] text-foreground-muted uppercase">
|
||||
{shot.label}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Subtle gradient + label on hover for real screenshots */}
|
||||
{shot.src && (
|
||||
<>
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 bg-gradient-to-t from-background/70 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
||||
/>
|
||||
<span className="absolute bottom-3 left-4 font-display text-sm tracking-widest text-foreground opacity-0 translate-y-2 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-500">
|
||||
{shot.label}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* WIP badge */}
|
||||
<div className="absolute top-3 left-3 rounded-full border border-border/70 bg-background/70 px-3 py-1 backdrop-blur-sm">
|
||||
<span className="font-display text-[10px] tracking-[0.2em] text-accent">
|
||||
WIP
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* DEVBLOG TEASER */}
|
||||
<section className="py-24 px-6 lg:px-10 border-t border-border">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
|
||||
— DEVBLOG
|
||||
</p>
|
||||
<h2 className="font-display text-4xl sm:text-5xl tracking-wider mb-6">
|
||||
FOLLOW THE JOURNEY
|
||||
</h2>
|
||||
<p className="text-lg text-foreground-muted leading-relaxed max-w-2xl mx-auto mb-8">
|
||||
Behind-the-scenes updates, design decisions, and milestones from
|
||||
the studio. Every step of the climb, documented.
|
||||
</p>
|
||||
<Link
|
||||
href="/devblog"
|
||||
className="inline-block px-8 py-3 border border-border text-foreground font-display tracking-widest text-sm hover:border-accent hover:text-accent hover:scale-[1.03] transition duration-200"
|
||||
>
|
||||
READ THE DEVBLOG
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
<Dispatch latest={latest} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user