From 077f146eda0740c10889c123799ce79a0e514ef2 Mon Sep 17 00:00:00 2001 From: Ryan Salazar Date: Thu, 28 May 2026 22:14:50 +0000 Subject: [PATCH] forum: drop Recent Threads widget, surface last post inline on category cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: · <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> --- src/app/api/forum/categories/route.ts | 33 +++++++++++++++++-- src/app/forum/page.tsx | 47 +++++++++------------------ 2 files changed, 46 insertions(+), 34 deletions(-) diff --git a/src/app/api/forum/categories/route.ts b/src/app/api/forum/categories/route.ts index 0ad7cd4..b73adfc 100644 --- a/src/app/api/forum/categories/route.ts +++ b/src/app/api/forum/categories/route.ts @@ -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 }); } diff --git a/src/app/forum/page.tsx b/src/app/forum/page.tsx index 6b2ea0e..ebb4fde 100644 --- a/src/app/forum/page.tsx +++ b/src/app/forum/page.tsx @@ -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>