disclosure-bureau/web/components/iconic-cases.tsx

120 lines
4.8 KiB
TypeScript
Raw Normal View History

W5.6 (Phase 3D): Iconic Cases — curated rail on homepage 8 hand-picked UFO incidents that any enthusiast recognises: - Kenneth Arnold 1947 (the genesis — "flying saucers" coined) - Roswell 1947 (the original Army press release) - Maury Island 1947 (Puget Sound, slag drop, FBI plane crash) - Mantell 1948 (first known UFO casualty) - Rendlesham Forest 1980 (USAF security police, deputy commander tape) - Phoenix Lights 1997 (V-formation across Arizona, governor's reversal) - Nimitz Tic-Tac 2004 (USS Nimitz F/A-18, gun-cam released 2017) - Green Fireballs Sandia 1948 (copper salts, nuclear-site overflights) Each case has: - Bilingual hand-written editorial blurb (PT-BR + EN, no LLM) - Painterly editorial hero illustration at 2K - Year + tag chips (military / civilian / modern / early / naval / aviation / mass-sighting) - Link to /e/events/<id> when indexed in corpus, /search?q=... when not, /c/<slug> for the green-fireballs narrative case Components: lib/iconic-cases.ts — IconicCase type + ICONIC_CASES editorial list components/iconic-cases.tsx — magazine grid: first two as wide hero pair, rest as 3-up tiles below. Hover scale on images, gradient overlays, aspect-16/11 hero + 16/10 compact, lazy-loaded. app/page.tsx — inserted between <FeaturedCase /> and <PortalGrid /> 4 new hero illustrations generated this session: - iconic-nimitz-tic-tac-2004.png (Nano Banana Pro) - iconic-phoenix-lights-1997.png (Nano Banana Pro) - iconic-rendlesham-forest-1980.png (gpt-image-1.5 via Codex) - iconic-mantell-1948.png (Nano Banana Pro) Per user direction: mixed generators (Nano Banana primary, Codex co-pilot) so the homepage has stylistic variety while keeping the painterly editorial register. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:50:52 +00:00
/**
* IconicCases the curated cover-story rail.
*
* Magazine grid of hand-picked iconic UFO incidents with painterly hero
* illustrations. Sits high on the homepage so the casual reader lands on
* recognisable history within two seconds: Roswell, Nimitz, Phoenix,
* Rendlesham.
*/
import Link from "next/link";
import { ICONIC_CASES, type IconicCase } from "@/lib/iconic-cases";
const ART_BASE = "/api/static/processing/case-art";
export function IconicCases({ locale }: { locale: "pt-br" | "en" }) {
const cases = ICONIC_CASES;
if (cases.length === 0) return null;
// Split: first 2 go into a wide hero pair, the rest tile below in a 3-up grid.
const [first, second, ...rest] = cases;
return (
<section className="mx-auto max-w-7xl px-4 md:px-8 py-12 md:py-16">
<div className="flex items-end justify-between mb-6 flex-wrap gap-3">
<div>
<div className="text-[10px] font-mono uppercase tracking-[0.18em] text-[#5a6678] mb-2">
{locale === "en" ? "// The iconic record" : "// Os clássicos da divulgação"}
</div>
<h2 className="font-display text-2xl md:text-4xl text-[#e7ecf3] tracking-tight">
{locale === "en"
? "The cases every UFO enthusiast knows"
: "Os casos que todo entusiasta UFO conhece"}
</h2>
</div>
<div className="text-[11px] font-mono text-[#5a6678]">
{cases.length} {locale === "en" ? "stories" : "histórias"}
</div>
</div>
{/* Hero pair */}
<div className="grid md:grid-cols-2 gap-4 md:gap-5 mb-4 md:mb-5">
{[first, second].filter(Boolean).map((c) => (
<HeroCard key={c.slug} c={c} locale={locale} />
))}
</div>
{/* Rest grid */}
{rest.length > 0 && (
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-5">
{rest.map((c) => <CompactCard key={c.slug} c={c} locale={locale} />)}
</div>
)}
</section>
);
}
function HeroCard({ c, locale }: { c: IconicCase; locale: "pt-br" | "en" }) {
const title = locale === "pt-br" ? c.title_pt_br : c.title_en;
const blurb = locale === "pt-br" ? c.blurb_pt_br : c.blurb_en;
return (
<Link
href={c.link}
className="group relative block rounded-2xl overflow-hidden border border-[rgba(224,192,128,0.15)] bg-[#0d1220] aspect-[16/11]"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`${ART_BASE}/${c.image}`}
alt={title}
loading="lazy"
className="absolute inset-0 w-full h-full object-cover group-hover:scale-[1.03] transition-transform duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#0a0e1a] via-[#0a0e1a]/40 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-r from-[#0a0e1a]/60 via-transparent to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-5 md:p-7">
<div className="text-[10px] font-mono uppercase tracking-[0.18em] text-[#e0c080] mb-2">
{c.year} · {c.tags.slice(0, 3).join(" · ")}
</div>
<h3 className="font-display text-2xl md:text-3xl text-white leading-tight mb-2 group-hover:text-[#e0c080] transition-colors">
{title}
</h3>
<p className="text-[13px] md:text-sm text-[#cbd2dd] leading-relaxed line-clamp-3 max-w-prose">
{blurb}
</p>
</div>
</Link>
);
}
function CompactCard({ c, locale }: { c: IconicCase; locale: "pt-br" | "en" }) {
const title = locale === "pt-br" ? c.title_pt_br : c.title_en;
const blurb = locale === "pt-br" ? c.blurb_pt_br : c.blurb_en;
return (
<Link
href={c.link}
className="group block rounded-xl overflow-hidden border border-[rgba(224,192,128,0.15)] bg-[#0d1220] hover:border-[#e0c080]/50 transition-all"
>
<div className="relative aspect-[16/10] overflow-hidden">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`${ART_BASE}/${c.image}`}
alt={title}
loading="lazy"
className="absolute inset-0 w-full h-full object-cover group-hover:scale-[1.05] transition-transform duration-500"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#0a0e1a]/60 via-transparent to-transparent" />
<div className="absolute top-3 left-3 px-2 py-0.5 rounded bg-[#0a0e1a]/80 text-[10px] font-mono text-[#e0c080]">
{c.year}
</div>
</div>
<div className="p-4 md:p-5">
<h3 className="font-display text-lg md:text-xl text-[#e7ecf3] leading-snug mb-2 group-hover:text-[#e0c080] transition-colors">
{title}
</h3>
<p className="text-[12px] text-[#9aa6b8] leading-relaxed line-clamp-3">
{blurb}
</p>
</div>
</Link>
);
}