initial commit: rocket.new export of broadcastbeat

This commit is contained in:
Ryan Salazar
2026-05-07 16:39:17 +00:00
commit 70aa1ad46e
302 changed files with 60710 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
'use server';
import { NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
export async function GET() {
try {
const supabase = await createClient();
// ── Thread Growth (last 8 weeks) ──────────────────────────────────────
const now = new Date();
const weeklyGrowth: { week: string; threads: number; replies: number }[] = [];
for (let i = 7; i >= 0; i--) {
const weekStart = new Date(now);
weekStart.setDate(now.getDate() - i * 7 - 6);
weekStart.setHours(0, 0, 0, 0);
const weekEnd = new Date(now);
weekEnd.setDate(now.getDate() - i * 7);
weekEnd.setHours(23, 59, 59, 999);
const { count: threadCount } = await supabase
.from('forum_threads')
.select('*', { count: 'exact', head: true })
.gte('created_at', weekStart.toISOString())
.lte('created_at', weekEnd.toISOString());
const { count: replyCount } = await supabase
.from('forum_replies')
.select('*', { count: 'exact', head: true })
.gte('created_at', weekStart.toISOString())
.lte('created_at', weekEnd.toISOString());
const label = `W${8 - i}`;
weeklyGrowth.push({
week: label,
threads: threadCount ?? 0,
replies: replyCount ?? 0,
});
}
// ── Top Categories ────────────────────────────────────────────────────
const { data: categories } = await supabase
.from('forum_categories')
.select('id, name, icon, thread_count, post_count')
.order('thread_count', { ascending: false })
.limit(8);
// ── Active Users (last 30 days) ───────────────────────────────────────
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
const { data: activeThreadAuthors } = await supabase
.from('forum_threads')
.select('author_id, author_name, created_at')
.gte('created_at', thirtyDaysAgo)
.not('author_id', 'is', null);
const { data: activeReplyAuthors } = await supabase
.from('forum_replies')
.select('author_id, author_name, created_at')
.gte('created_at', thirtyDaysAgo)
.not('author_id', 'is', null);
// Aggregate user activity
const userActivityMap: Record<string, { author_id: string; author_name: string; threads: number; replies: number; total: number }> = {};
(activeThreadAuthors ?? []).forEach((t: any) => {
if (!t.author_id) return;
if (!userActivityMap[t.author_id]) {
userActivityMap[t.author_id] = { author_id: t.author_id, author_name: t.author_name || 'Anonymous', threads: 0, replies: 0, total: 0 };
}
userActivityMap[t.author_id].threads++;
userActivityMap[t.author_id].total++;
});
(activeReplyAuthors ?? []).forEach((r: any) => {
if (!r.author_id) return;
if (!userActivityMap[r.author_id]) {
userActivityMap[r.author_id] = { author_id: r.author_id, author_name: r.author_name || 'Anonymous', threads: 0, replies: 0, total: 0 };
}
userActivityMap[r.author_id].replies++;
userActivityMap[r.author_id].total++;
});
const activeUsers = Object.values(userActivityMap)
.sort((a, b) => b.total - a.total)
.slice(0, 10);
// ── Engagement Metrics ────────────────────────────────────────────────
const { count: totalThreads } = await supabase
.from('forum_threads')
.select('*', { count: 'exact', head: true });
const { count: totalReplies } = await supabase
.from('forum_replies')
.select('*', { count: 'exact', head: true });
const { data: viewData } = await supabase
.from('forum_threads')
.select('view_count, reply_count');
const totalViews = (viewData ?? []).reduce((sum: number, t: any) => sum + (t.view_count ?? 0), 0);
const avgRepliesPerThread = totalThreads && totalThreads > 0
? Math.round(((totalReplies ?? 0) / totalThreads) * 10) / 10
: 0;
// Threads with replies (engagement rate)
const threadsWithReplies = (viewData ?? []).filter((t: any) => (t.reply_count ?? 0) > 0).length;
const engagementRate = totalThreads && totalThreads > 0
? Math.round((threadsWithReplies / totalThreads) * 100)
: 0;
// New threads last 7 days
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
const { count: newThreadsWeek } = await supabase
.from('forum_threads')
.select('*', { count: 'exact', head: true })
.gte('created_at', sevenDaysAgo);
const { count: newRepliesWeek } = await supabase
.from('forum_replies')
.select('*', { count: 'exact', head: true })
.gte('created_at', sevenDaysAgo);
// ── Votes / Upvotes ───────────────────────────────────────────────────
const { count: totalVotes } = await supabase
.from('forum_votes')
.select('*', { count: 'exact', head: true });
// ── Moderation Stats ──────────────────────────────────────────────────
const { count: flaggedThreads } = await supabase
.from('forum_threads')
.select('*', { count: 'exact', head: true })
.eq('is_flagged', true);
const { count: lockedThreads } = await supabase
.from('forum_threads')
.select('*', { count: 'exact', head: true })
.eq('is_locked', true);
const { count: pinnedThreads } = await supabase
.from('forum_threads')
.select('*', { count: 'exact', head: true })
.eq('is_pinned', true);
const { count: featuredThreads } = await supabase
.from('forum_threads')
.select('*', { count: 'exact', head: true })
.eq('is_featured', true);
const { count: flaggedReplies } = await supabase
.from('forum_replies')
.select('*', { count: 'exact', head: true })
.eq('is_flagged', true);
const { count: hiddenReplies } = await supabase
.from('forum_replies')
.select('*', { count: 'exact', head: true })
.eq('is_hidden', true);
return NextResponse.json({
weeklyGrowth,
topCategories: categories ?? [],
activeUsers,
engagement: {
totalThreads: totalThreads ?? 0,
totalReplies: totalReplies ?? 0,
totalViews,
totalVotes: totalVotes ?? 0,
avgRepliesPerThread,
engagementRate,
newThreadsWeek: newThreadsWeek ?? 0,
newRepliesWeek: newRepliesWeek ?? 0,
},
moderation: {
flaggedThreads: flaggedThreads ?? 0,
lockedThreads: lockedThreads ?? 0,
pinnedThreads: pinnedThreads ?? 0,
featuredThreads: featuredThreads ?? 0,
flaggedReplies: flaggedReplies ?? 0,
hiddenReplies: hiddenReplies ?? 0,
},
});
} catch (err: any) {
return NextResponse.json({ error: err.message ?? 'Internal server error' }, { status: 500 });
}
}