forum: drop Recent Threads widget, surface last post inline on category cards
Per Ryan — category drill-down already shows newest-first ordering so the standalone Recent Threads list was redundant. Each category card now shows "Last post: <title> · <relative time>" below the description so glanceable activity stays on the landing page. Categories API joins one latest thread per category in a single round-trip (in_ + limit + group in memory) instead of N+1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,13 +4,42 @@ import { createClient } from '@/lib/supabase/server';
|
||||
export async function GET() {
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
const { data, error } = await supabase
|
||||
const { data: cats, error } = await supabase
|
||||
.from('forum_categories')
|
||||
.select('*')
|
||||
.order('display_order', { ascending: true });
|
||||
|
||||
if (error) throw error;
|
||||
return NextResponse.json({ categories: data });
|
||||
|
||||
// Latest thread per category — joined client-side so we keep one
|
||||
// round-trip per category instead of N+1. Each cat ends up with
|
||||
// last_thread_title + last_activity_at attached.
|
||||
const catIds = (cats || []).map((c: any) => c.id);
|
||||
let lastByCat: Record<string, { title: string; at: string }> = {};
|
||||
if (catIds.length) {
|
||||
const { data: latest } = await supabase
|
||||
.from('forum_threads')
|
||||
.select('id, category_id, title, last_reply_at, created_at')
|
||||
.in('category_id', catIds)
|
||||
.order('last_reply_at', { ascending: false, nullsFirst: false })
|
||||
.limit(2000);
|
||||
for (const t of latest || []) {
|
||||
const k = t.category_id as string;
|
||||
if (!lastByCat[k]) {
|
||||
lastByCat[k] = {
|
||||
title: t.title,
|
||||
at: t.last_reply_at || t.created_at,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const enriched = (cats || []).map((c: any) => ({
|
||||
...c,
|
||||
last_thread_title: lastByCat[c.id]?.title || null,
|
||||
last_activity_at: lastByCat[c.id]?.at || null,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ categories: enriched });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ interface ForumCategory {
|
||||
thread_count: number;
|
||||
post_count: number;
|
||||
display_order: number;
|
||||
last_thread_title?: string | null;
|
||||
last_activity_at?: string | null;
|
||||
}
|
||||
|
||||
interface ForumThread {
|
||||
@@ -549,38 +551,10 @@ export default function ForumIndexPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recent Threads — newest first across all categories */}
|
||||
{!loading && !error && threads.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-white font-heading font-bold text-base mb-3">Recent Threads</h2>
|
||||
<div className="space-y-1">
|
||||
{threads.slice(0, 12).map((thread) => (
|
||||
<Link
|
||||
key={thread.id}
|
||||
href={`/forum/thread/${thread.id}`}
|
||||
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#3b82f6] rounded-lg px-4 py-3 transition-all group"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-white font-body font-semibold text-sm group-hover:text-[#3b82f6] transition-colors truncate">
|
||||
{thread.title}
|
||||
</h3>
|
||||
<p className="text-[#666] font-body text-xs mt-0.5">
|
||||
by <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#888] hover:text-[#3b82f6] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
|
||||
{thread.forum_categories && (
|
||||
<> · <span className="text-[#3b82f6]">{thread.forum_categories.name}</span></>
|
||||
)}
|
||||
{' · '}{timeAgo(thread.last_reply_at || thread.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-right hidden sm:block">
|
||||
<div className="text-[#aaa] font-body text-sm font-semibold">{thread.reply_count}</div>
|
||||
<div className="text-[#555] font-body text-xs">replies</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* "Recent Threads" widget removed per Ryan — users can drill
|
||||
into a category to see latest-first ordering. Each category
|
||||
card now surfaces the last-post title + timestamp inline so
|
||||
the homepage glance still shows activity. */}
|
||||
|
||||
{!loading && !error && (
|
||||
<>
|
||||
@@ -598,6 +572,15 @@ export default function ForumIndexPage() {
|
||||
{cat.name}
|
||||
</h2>
|
||||
<p className="text-[#888] font-body text-sm mt-0.5 truncate">{cat.description}</p>
|
||||
{cat.last_thread_title && (
|
||||
<p className="text-[10px] text-[#666] font-body mt-1.5 truncate">
|
||||
<span className="text-[#555]">Last post:</span>{' '}
|
||||
<span className="text-[#aaa]">{cat.last_thread_title}</span>
|
||||
{cat.last_activity_at && (
|
||||
<span className="text-[#555]"> · {timeAgo(cat.last_activity_at)}</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-right hidden sm:block">
|
||||
<div className="text-white font-body font-semibold text-sm">{cat.thread_count}</div>
|
||||
|
||||
Reference in New Issue
Block a user