initial commit: rocket.new export of broadcastbeat
This commit is contained in:
186
src/app/api/admin/forum-stats/route.ts
Normal file
186
src/app/api/admin/forum-stats/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user