Files
ttvfx/app/(site)/work/[slug]/page.tsx
T
twotalesanimation a7b7a29ff2
Deploy / deploy (push) Successful in 40s
Clere Radiance: 30s spot, product viz R&D loop
- Main video: 30s CTA spot (ProRes master re-encoded to 7.4 MB H.264)
- New 'loops' project field: silent autoplaying clips with a caption;
  used for the square product visualisation R&D render (1.9 MB)
- Poster and gallery stills from the spot; summary/credits updated
- Ignore .mov masters in content/

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:58:14 +02:00

155 lines
5.8 KiB
TypeScript

import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import Frame from "@/components/Frame";
import Laurel from "@/components/Laurel";
import { awardFor, projects, site } from "@/content/site";
export const dynamicParams = false;
export function generateStaticParams() {
return projects.map((p) => ({ slug: p.slug }));
}
export async function generateMetadata({ params }: PageProps<"/work/[slug]">): Promise<Metadata> {
const { slug } = await params;
const project = projects.find((p) => p.slug === slug);
if (!project) return {};
return { title: project.title, description: project.summary };
}
function Video({ src, title, poster }: { src: string; title: string; poster?: string }) {
if (/vimeo\.com|youtube\.com|youtu\.be/.test(src)) {
return (
<iframe
src={src}
title={title}
className="aspect-video w-full"
allow="autoplay; fullscreen; picture-in-picture"
allowFullScreen
/>
);
}
return <video src={src} poster={poster} className="w-full bg-black" controls playsInline preload="metadata" />;
}
export default async function ProjectPage({ params }: PageProps<"/work/[slug]">) {
const { slug } = await params;
const index = projects.findIndex((p) => p.slug === slug);
if (index === -1) notFound();
const project = projects[index];
const next = projects[(index + 1) % projects.length];
const award = awardFor(project.slug);
const meta = [
{ k: "Client", v: project.client },
{ k: "Year", v: String(project.year) },
{ k: "Type", v: project.category },
];
return (
<article className="pt-28 md:pt-36">
<div className="mx-auto max-w-7xl px-5 md:px-8">
<Link
href="/work/"
className="-my-3.5 inline-block py-3.5 font-mono text-xs tracking-[0.2em] text-muted uppercase transition-colors hover:text-fg"
>
← All work
</Link>
<h1 className="mt-8 font-display text-[clamp(2rem,8.5vw,6rem)] leading-[0.9] font-thin break-words uppercase">{project.title}</h1>
{award && (
<div className="mt-8 inline-flex items-center gap-4 border border-accent/40 bg-accent/[0.06] py-3 pr-6 pl-3">
<Laurel className="h-12 w-12 shrink-0 text-accent" />
<div>
<p className="font-mono text-[10px] tracking-[0.25em] text-accent uppercase">
{award.edition} {award.body} · {award.result}
</p>
<p className="mt-1 text-sm md:text-base">{award.category}</p>
</div>
</div>
)}
</div>
<div className="mx-auto mt-12 max-w-7xl px-5 md:px-8">
{project.video ? <Video src={project.video} title={project.title} poster={project.image} /> : <Frame project={project} priority />}
</div>
{project.gallery && (
<div className="mx-auto mt-2 grid max-w-7xl gap-2 px-5 md:grid-cols-2 md:px-8">
{project.gallery.map((src) => (
// eslint-disable-next-line @next/next/no-img-element
<img key={src} src={src} alt={`${project.title} still`} loading="lazy" className="w-full" />
))}
</div>
)}
{project.loops?.map((loop) => (
<section
key={loop.src}
className="mx-auto mt-16 grid max-w-7xl items-center gap-8 px-5 md:mt-24 md:grid-cols-2 md:gap-16 md:px-8"
>
<video
src={loop.src}
poster={loop.poster}
autoPlay
muted
loop
playsInline
preload="metadata"
className="w-full bg-ink-2"
style={{ aspectRatio: loop.aspect }}
/>
<div>
<p className="font-mono text-xs tracking-[0.3em] text-accent uppercase">{loop.title}</p>
<p className="mt-4 text-lg leading-relaxed font-light text-muted md:text-xl">{loop.caption}</p>
</div>
</section>
))}
<div className="mx-auto grid max-w-7xl gap-12 px-5 py-20 md:grid-cols-[1fr_2fr] md:px-8 md:py-28">
<dl className="grid grid-cols-2 gap-6 self-start md:grid-cols-1">
{meta.map((m) => (
<div key={m.k}>
<dt className="font-mono text-[11px] tracking-[0.2em] text-muted uppercase">{m.k}</dt>
<dd className="mt-1 text-lg">{m.v}</dd>
</div>
))}
</dl>
<div>
<p className="text-xl leading-relaxed font-light md:text-2xl">{project.summary}</p>
<ul className="mt-10 flex flex-wrap gap-2">
{project.credits.map((c) => (
<li
key={c}
className="border border-line px-3 py-1.5 font-mono text-[11px] tracking-[0.15em] text-muted uppercase"
>
{c}
</li>
))}
</ul>
<a
href={`mailto:${site.email}?subject=${encodeURIComponent(`Enquiry: ${project.title}`)}`}
className="mt-9 inline-block py-3 font-mono text-xs tracking-[0.2em] text-accent uppercase hover:text-fg"
>
Talk to us about a project like this →
</a>
</div>
</div>
<Link href={`/work/${next.slug}/`} className="group block border-t border-line">
<div className="mx-auto flex max-w-7xl items-center justify-between gap-6 px-5 py-14 md:px-8 md:py-20">
<div className="min-w-0">
<p className="font-mono text-xs tracking-[0.3em] text-muted uppercase">Next project</p>
<p className="mt-3 font-display text-3xl font-thin break-words uppercase transition-colors group-hover:text-accent md:text-6xl">
{next.title}
</p>
</div>
<span className="shrink-0 font-display text-4xl font-thin transition-transform group-hover:translate-x-2 md:text-6xl">
→
</span>
</div>
</Link>
</article>
);
}