Files
avbeat-com/src/components/TrendsSidebar.tsx
Claude (Phase B) 09a24be3de trends sidebar cleanup: minimal display + beige/navy palette
Per Ryan 2026-05-15:
- Removed Vector pass footer (no audience value)
- Removed PHASE 6b comment
- Removed corner brackets + monitoring subtitle + rank prefix
- Top 5 entities now show as Sony +12 / Riedel +8 — entity name
  serif + count mono, right-aligned
- Background #F5EFEB (beige; #F5EFFEB had 7 hex chars, interpreted as
  #F5EFEB) via new --color-trends-bg token
- Text #2F4F5B (navy) via new --color-trends-text token
- Tokens added to redesign-tokens.css palette section

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:53:51 +00:00

110 lines
3.7 KiB
TypeScript

import { createClient } from "@supabase/supabase-js";
export const dynamic = "force-dynamic";
export const revalidate = 21600; // 6h
interface Article {
title: string;
excerpt: string | null;
tags: string[] | null;
published_at: string | null;
}
function detectTrends(recent: Article[], baseline: Article[]) {
const recentCounts: Record<string, number> = {};
for (const a of recent) for (const t of a.tags || []) {
const k = t.toLowerCase();
recentCounts[k] = (recentCounts[k] || 0) + 1;
}
const baseCounts: Record<string, number> = {};
for (const a of baseline) for (const t of a.tags || []) {
const k = t.toLowerCase();
baseCounts[k] = (baseCounts[k] || 0) + 1;
}
const scored = Object.entries(recentCounts).map(([k, c]) => {
const baseline_per_day = (baseCounts[k] || 0) / 30;
const recent_per_day = c / 7;
const lift = baseline_per_day > 0 ? recent_per_day / baseline_per_day : recent_per_day * 3;
return { entity: k, lift, count: c };
});
scored.sort((a, b) => b.lift - a.lift);
return scored.slice(0, 5);
}
function titleCase(s: string): string {
return s.replace(/\b\w/g, (c) => c.toUpperCase());
}
export default async function TrendsSidebar() {
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 since7d = new Date(Date.now() - 7 * 86400000).toISOString();
const since30d = new Date(Date.now() - 30 * 86400000).toISOString();
const [recent, baseline] = await Promise.all([
sb
.from("wp_imported_posts")
.select("title, excerpt, tags, wp_published_at")
.eq("status", "published")
.gte("wp_published_at", since7d)
.order("wp_published_at", { ascending: false })
.limit(200),
sb
.from("wp_imported_posts")
.select("title, excerpt, tags, wp_published_at")
.eq("status", "published")
.gte("wp_published_at", since30d)
.lt("wp_published_at", since7d)
.order("wp_published_at", { ascending: false })
.limit(800),
]);
const recentArts: Article[] = (recent.data || []).map((r: any) => ({
title: r.title, excerpt: r.excerpt, tags: r.tags, published_at: r.wp_published_at,
}));
const baselineArts: Article[] = (baseline.data || []).map((r: any) => ({
title: r.title, excerpt: r.excerpt, tags: r.tags, published_at: r.wp_published_at,
}));
const trends = detectTrends(recentArts, baselineArts);
return (
<aside
className="rounded p-4"
style={{
background: "var(--color-trends-bg, #F5EFEB)",
color: "var(--color-trends-text, #2F4F5B)",
borderRadius: "var(--border-radius-md, 6px)",
}}
aria-label="BB AI detected trends"
>
<div className="flex items-center justify-between mb-3">
<h2 className="font-serif text-base font-semibold">BB AI · detected trends</h2>
<span className="inline-flex items-center gap-1.5 text-[10px] font-mono uppercase tracking-wider opacity-70">
<span className="bb-pulse-dot" style={{ background: "var(--color-trends-text, #2F4F5B)" }} />
live
</span>
</div>
<ul className="space-y-1.5">
{trends.length === 0 && (
<li className="text-xs opacity-60">No trend data yet.</li>
)}
{trends.map((t) => (
<li key={t.entity} className="flex items-center justify-between text-sm">
<span className="truncate font-serif">{titleCase(t.entity)}</span>
<span className="font-mono text-[12px] opacity-80">
+{Math.round((t.lift - 1) * 100) || t.count}
</span>
</li>
))}
</ul>
</aside>
);
}