Replace the four accent-family hex codes site-wide with the teal palette (keeps the dark theme; only swaps the accent family, not the dark base): #3b82f6 (accent primary CTA) → #5B7C8D (teal) [839×] #2563eb (accent-dark / hover) → #4A6473 (darker teal) [44×] #60a5fa (info link, lighter) → #8FB0C3 (mid teal) [68×] #1e3a5f (accent-muted bg) → #2F4F5F (navy) [44×] tailwind.config.js tokens updated to match (accent/accent-dark/accent-muted). Sweep also covers tailwind.css. 108 files touched. The dark base (#0d0d0d, #111, #161616, #1a1a1a etc.) is intentionally untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
176 lines
6.8 KiB
TypeScript
176 lines
6.8 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import Link from "next/link";
|
||
|
||
interface Slide {
|
||
id: string;
|
||
slug: string;
|
||
title: string;
|
||
excerpt: string;
|
||
image: string | null;
|
||
category: string;
|
||
author: string;
|
||
author_initials: string;
|
||
author_beat: string | null;
|
||
date: string;
|
||
read_min: number;
|
||
ago_min: number;
|
||
neural_summary: NeuralSummary | null;
|
||
}
|
||
|
||
interface NeuralSummary {
|
||
bullets?: string[];
|
||
confidence?: number;
|
||
entities?: string[];
|
||
}
|
||
|
||
export default function FeaturedCarousel({ slides }: { slides: Slide[] }) {
|
||
const [i, setI] = useState(0);
|
||
const [paused, setPaused] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (slides.length < 2 || paused) return;
|
||
const t = setInterval(() => setI((v) => (v + 1) % slides.length), 4000);
|
||
return () => clearInterval(t);
|
||
}, [slides.length, paused]);
|
||
|
||
if (slides.length === 0) return null;
|
||
const s = slides[i];
|
||
|
||
return (
|
||
<section
|
||
onMouseEnter={() => setPaused(true)}
|
||
onMouseLeave={() => setPaused(false)}
|
||
className="max-w-container mx-auto px-4 my-8"
|
||
aria-label="Featured editorial"
|
||
>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280]">
|
||
Featured · staff editorial
|
||
</h2>
|
||
<span className="inline-flex items-center gap-1.5 text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#8FB0C3)]">
|
||
<span className="bb-pulse-dot" /> auto · 4s
|
||
</span>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 rounded border border-[#252525] bg-[#0b0f17] overflow-hidden">
|
||
<div className="relative aspect-[16/10] bg-[#111]">
|
||
{s.image ? (
|
||
<img src={s.image} alt={s.title} className="w-full h-full object-cover" />
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center text-[#6b7280] font-mono text-xs">
|
||
no image
|
||
</div>
|
||
)}
|
||
<div className="absolute top-2 right-2 flex gap-1 text-[10px] font-mono">
|
||
<span className="bg-black/60 text-[var(--color-text-info,#8FB0C3)] px-1.5 py-0.5 rounded">
|
||
[ID {s.id.slice(0, 4).toUpperCase()}]
|
||
</span>
|
||
<span className="bg-black/60 text-white px-1.5 py-0.5 rounded uppercase">
|
||
[{s.category.replace(/\s+/g, "-")}]
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-5">
|
||
<div className="flex items-center gap-2 text-[10px] font-mono text-[#6b7280] mb-2">
|
||
<span>{s.category}</span>
|
||
<span>·</span>
|
||
<span>{s.ago_min}min ago</span>
|
||
<span>·</span>
|
||
<span>{s.read_min}min read</span>
|
||
</div>
|
||
|
||
<Link href={`/news/${s.slug}`} className="block group">
|
||
<h3 className="font-serif text-2xl leading-snug text-[#e5e7eb] group-hover:text-[var(--color-text-info,#8FB0C3)]">
|
||
{s.title}
|
||
</h3>
|
||
</Link>
|
||
<p className="font-serif text-[15px] text-[#b0b0b0] mt-3 line-clamp-3">
|
||
{s.excerpt}
|
||
</p>
|
||
|
||
{s.neural_summary && Array.isArray(s.neural_summary.bullets) && s.neural_summary.bullets.length > 0 && (
|
||
<div className="relative mt-4 p-3 border border-[#252525] rounded">
|
||
<span aria-hidden className="absolute top-1 left-1 w-2.5 h-2.5 border-t border-l border-[var(--color-text-info,#8FB0C3)]" />
|
||
<span aria-hidden className="absolute bottom-1 right-1 w-2.5 h-2.5 border-b border-r border-[var(--color-text-info,#8FB0C3)]" />
|
||
<div className="flex items-center justify-between mb-2">
|
||
<span className="text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#8FB0C3)]">
|
||
Neural summary
|
||
</span>
|
||
{typeof s.neural_summary.confidence === "number" && (
|
||
<span className="text-[10px] font-mono text-[#6b7280]">
|
||
conf {(s.neural_summary.confidence * 100).toFixed(0)}%
|
||
</span>
|
||
)}
|
||
</div>
|
||
<ul className="space-y-1 text-xs text-[#d1d5db]">
|
||
{s.neural_summary.bullets.slice(0, 3).map((b, idx) => (
|
||
<li key={idx} className="flex gap-2">
|
||
<span className="text-[var(--color-text-info,#8FB0C3)]">·</span>
|
||
<span>{b}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
{s.neural_summary.entities && s.neural_summary.entities.length > 0 && (
|
||
<div className="mt-2 flex flex-wrap gap-1">
|
||
{s.neural_summary.entities.slice(0, 5).map((e) => (
|
||
<span key={e} className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-[#1a1f2e] text-[var(--color-text-info,#8FB0C3)]">
|
||
{e}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
<div className="mt-4 pt-3 border-t border-[#252525] flex items-center gap-2">
|
||
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-[#1a1f2e] text-[var(--color-text-info,#8FB0C3)] text-xs font-semibold">
|
||
{s.author_initials}
|
||
</span>
|
||
<div>
|
||
<div className="text-sm text-[#e5e7eb]">{s.author}</div>
|
||
{s.author_beat && (
|
||
<div className="text-[10px] font-mono text-[#6b7280] uppercase tracking-wider">{s.author_beat}</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-4 flex items-center justify-between">
|
||
<div className="flex gap-1.5">
|
||
{slides.map((_, n) => (
|
||
<button
|
||
key={n}
|
||
onClick={() => setI(n)}
|
||
aria-label={`Slide ${n + 1}`}
|
||
className={`w-2 h-2 rounded-full transition-colors ${
|
||
n === i ? "bg-[var(--color-text-info,#8FB0C3)]" : "bg-[#374151]"
|
||
}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
<div className="flex items-center gap-3 text-xs font-mono text-[#6b7280]">
|
||
<button
|
||
onClick={() => setI((v) => (v - 1 + slides.length) % slides.length)}
|
||
aria-label="Previous"
|
||
className="hover:text-[var(--color-text-info,#8FB0C3)]"
|
||
>
|
||
‹
|
||
</button>
|
||
<span>{String(i + 1).padStart(2, "0")} / {String(slides.length).padStart(2, "0")}</span>
|
||
<button
|
||
onClick={() => setI((v) => (v + 1) % slides.length)}
|
||
aria-label="Next"
|
||
className="hover:text-[var(--color-text-info,#8FB0C3)]"
|
||
>
|
||
›
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|