a7b7a29ff2
Deploy / deploy (push) Successful in 40s
- 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>
155 lines
5.8 KiB
TypeScript
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>
|
|
);
|
|
}
|