homepage: neon dual-speed ticker + emerald/cyan palette pass

- New DualSpeedTicker component (CSS keyframes, GPU-accelerated,
  pause on hover, 28px rows, 17s BEAT / 40s FORUM, refresh 5min)
- /api/ticker/beat-news: last 7 days from bb.native_articles
- /api/ticker/forum-posts: latest threads + replies interleaved
- Mounted at the bottom of Header so every page gets it
- Scoped .bb-neon palette on homepage root: section heads +
  text-shadow glow, neon-tinted card borders, emerald→cyan button
  gradient, cyan hover state on links, subscribe boxes neon-bordered
- CSS-only — no layout changes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ryan Salazar
2026-05-21 23:58:12 +00:00
parent 5eaf64925f
commit aee67ef9f6
6 changed files with 490 additions and 1 deletions

View File

@@ -0,0 +1,46 @@
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
export const runtime = 'nodejs';
export const revalidate = 300; // 5 minutes
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const SUPABASE_ANON = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export async function GET() {
const sb = createClient(SUPABASE_URL, SUPABASE_ANON, {
db: { schema: 'bb' },
auth: { persistSession: false },
});
const cutoff = new Date(Date.now() - 7 * 86400000).toISOString();
const { data, error } = await sb
.from('native_articles')
.select('id, slug, title, category, published_at')
.eq('status', 'published')
.gte('published_at', cutoff)
.order('published_at', { ascending: false })
.limit(30);
if (error) {
return NextResponse.json({ error: error.message, items: [] }, { status: 500 });
}
const items = (data || []).map((a) => ({
id: a.id,
href: `/articles/${a.slug}`,
title: a.title,
category: a.category || null,
published_at: a.published_at,
}));
return NextResponse.json(
{ items },
{
headers: {
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600',
},
}
);
}

View File

@@ -0,0 +1,90 @@
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
export const runtime = 'nodejs';
export const revalidate = 300; // 5 minutes
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const SUPABASE_ANON = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
interface TickerItem {
id: string;
href: string;
title: string;
kind: 'thread' | 'reply';
author_name: string | null;
created_at: string;
}
export async function GET() {
const sb = createClient(SUPABASE_URL, SUPABASE_ANON, {
db: { schema: 'bb' },
auth: { persistSession: false },
});
// Newest threads + newest replies, interleaved by timestamp.
const [threadsRes, repliesRes] = await Promise.all([
sb
.from('forum_threads')
.select('id, title, author_name, created_at')
.order('created_at', { ascending: false })
.limit(20),
sb
.from('forum_replies')
.select('id, body, thread_id, author_name, created_at, forum_threads(title)')
.order('created_at', { ascending: false })
.limit(20),
]);
if (threadsRes.error && repliesRes.error) {
return NextResponse.json(
{ error: threadsRes.error?.message || repliesRes.error?.message, items: [] },
{ status: 500 }
);
}
const items: TickerItem[] = [];
for (const t of threadsRes.data || []) {
items.push({
id: t.id,
href: `/forum/thread/${t.id}`,
title: t.title,
kind: 'thread',
author_name: t.author_name,
created_at: t.created_at,
});
}
for (const r of (repliesRes.data || []) as Array<{
id: string;
body: string;
thread_id: string;
author_name: string | null;
created_at: string;
forum_threads?: { title: string } | { title: string }[] | null;
}>) {
const parent =
Array.isArray(r.forum_threads) ? r.forum_threads[0] : r.forum_threads;
const parentTitle = parent?.title || 'forum reply';
items.push({
id: r.id,
href: `/forum/thread/${r.thread_id}`,
title: `Re: ${parentTitle}`,
kind: 'reply',
author_name: r.author_name,
created_at: r.created_at,
});
}
items.sort((a, b) => (a.created_at < b.created_at ? 1 : -1));
return NextResponse.json(
{ items: items.slice(0, 30) },
{
headers: {
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600',
},
}
);
}