Files
avbeat-com/src/components/FeaturedCarouselServer.tsx
Claude (Phase B) 4546d5be3b 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>
2026-05-15 08:20:49 +00:00

102 lines
3.3 KiB
TypeScript

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} />;
}