Phase 6C featured carousel + Phase 2 event tag chips
FeaturedCarouselServer (RSC) pulls 5 most recent featured=true rows from
bb.ai_rewritten_articles UNION bb.wp_imported_posts, renders into the
client FeaturedCarousel component: 4s auto-advance (pause on hover),
chevron + dot nav + NN/NN counter, [ID xxxx] / [CATEGORY] badges,
serif headline, lede, optional neural-summary box with corner brackets +
confidence + entity chips, byline with initials avatar. Replaces the
FeaturedBento on the homepage.
Seeded bb.wp_imported_posts.featured = TRUE on 5 recent posts so the
carousel renders out-of-the-box (Amagi, Riedel, LiveU BroadcastAsia,
Cobalt Best-of-Show, Leader MPTS).
Phase 2 UI:
EventTagPicker (client) — chip multi-select reading bb.events,
writing to bb.article_events via
/api/admin/review-queue/[id]/events.
article_table=ai_rewritten for now;
future submission-form integration will
cover other tables.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
101
src/components/FeaturedCarouselServer.tsx
Normal file
101
src/components/FeaturedCarouselServer.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import { rewriteLegacyImageUrl } from "@/lib/legacy-image";
|
||||
import FeaturedCarousel from "./FeaturedCarousel";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
function initials(name: string): string {
|
||||
const parts = name.split(/\s+/).filter(Boolean);
|
||||
if (parts.length === 0) return "??";
|
||||
const first = parts[0][0] || "";
|
||||
const last = parts.length > 1 ? parts[parts.length - 1][0] || "" : "";
|
||||
return (first + last).toUpperCase();
|
||||
}
|
||||
|
||||
function readTime(text: string | null | undefined): number {
|
||||
if (!text) return 1;
|
||||
const words = text.replace(/<[^>]+>/g, " ").split(/\s+/).filter(Boolean).length;
|
||||
return Math.max(1, Math.round(words / 200));
|
||||
}
|
||||
|
||||
function agoMinutes(iso: string | null): number {
|
||||
if (!iso) return 0;
|
||||
return Math.max(0, Math.floor((Date.now() - new Date(iso).getTime()) / 60000));
|
||||
}
|
||||
|
||||
export default async function FeaturedCarouselServer() {
|
||||
const sb = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
db: { schema: (process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb") as "public" },
|
||||
auth: { persistSession: false },
|
||||
}
|
||||
);
|
||||
|
||||
const [wp, ai] = await Promise.all([
|
||||
sb
|
||||
.from("wp_imported_posts")
|
||||
.select("wp_id, wp_slug, title, excerpt, content, featured_image, author_name, category, wp_published_at, neural_summary")
|
||||
.eq("status", "published")
|
||||
.eq("featured", true)
|
||||
.order("wp_published_at", { ascending: false })
|
||||
.limit(5),
|
||||
sb
|
||||
.from("ai_rewritten_articles")
|
||||
.select("id, slug, title, excerpt, body, category, published_at, neural_summary, persona:ai_personas(name, beat)")
|
||||
.eq("status", "published")
|
||||
.eq("featured", true)
|
||||
.order("published_at", { ascending: false, nullsFirst: false })
|
||||
.limit(5),
|
||||
]);
|
||||
|
||||
const slides: any[] = [];
|
||||
|
||||
for (const r of (ai.data || []) as any[]) {
|
||||
const persona = Array.isArray(r.persona) ? r.persona[0] : r.persona;
|
||||
const author = persona?.name || "Staff Reporter";
|
||||
slides.push({
|
||||
id: r.id,
|
||||
slug: r.slug,
|
||||
title: r.title,
|
||||
excerpt: r.excerpt || "",
|
||||
image: null,
|
||||
category: r.category || "News",
|
||||
author,
|
||||
author_initials: initials(author),
|
||||
author_beat: persona?.beat || null,
|
||||
date: r.published_at,
|
||||
read_min: readTime(r.body),
|
||||
ago_min: agoMinutes(r.published_at),
|
||||
neural_summary: r.neural_summary || null,
|
||||
});
|
||||
}
|
||||
|
||||
for (const r of (wp.data || []) as any[]) {
|
||||
const image = r.featured_image ? rewriteLegacyImageUrl(r.featured_image) : null;
|
||||
const author = r.author_name || "Staff Reporter";
|
||||
slides.push({
|
||||
id: String(r.wp_id),
|
||||
slug: r.wp_slug,
|
||||
title: r.title,
|
||||
excerpt: r.excerpt || "",
|
||||
image,
|
||||
category: r.category || "News",
|
||||
author,
|
||||
author_initials: initials(author),
|
||||
author_beat: null,
|
||||
date: r.wp_published_at,
|
||||
read_min: readTime(r.content),
|
||||
ago_min: agoMinutes(r.wp_published_at),
|
||||
neural_summary: r.neural_summary || null,
|
||||
});
|
||||
}
|
||||
|
||||
// Cap to 5 most recent across both sources
|
||||
slides.sort((a, b) => new Date(b.date || 0).getTime() - new Date(a.date || 0).getTime());
|
||||
const top5 = slides.slice(0, 5);
|
||||
|
||||
if (top5.length === 0) return null;
|
||||
return <FeaturedCarousel slides={top5} />;
|
||||
}
|
||||
Reference in New Issue
Block a user