forum: fix API + UI column-name mismatch against bb.forum_threads

The schema has upvotes / downvotes / vote_score; API + UI were
querying/reading upvote_count / downvote_count (non-existent
columns). Threads existed in the DB but didn't render.

- /api/forum/threads route.ts:
  - 'top-voted' sort: order by vote_score (was upvote_count)
  - 'newest' default sort: forum_threads has no 'status' column,
    so order pinned-first via is_pinned, then last_reply_at desc
- UI ForumThread interface + components: rename
  upvote_count → upvotes, add downvotes + vote_score so the
  vote display in the thread cards and the admin moderation
  list pulls live data from the existing columns.

After this lands and the deploy completes, the 50 seeded
threads + the 9 ai-seeded new threads + 253 backfilled replies
already in the DB will all render at /forum.
This commit is contained in:
Ryan Salazar
2026-05-20 08:34:05 +00:00
parent 4e11438963
commit dc166dfb10
4 changed files with 16 additions and 10 deletions

View File

@@ -14,7 +14,7 @@ interface ForumThread {
body: string;
reply_count: number;
view_count: number;
upvote_count: number;
upvotes: number;
is_pinned: boolean;
is_featured: boolean;
is_locked: boolean;

View File

@@ -31,7 +31,9 @@ export async function GET(req: NextRequest) {
}
if (sort === 'top-voted') {
query = query.order('upvote_count', { ascending: false }).order('created_at', { ascending: false });
// The schema has upvotes / downvotes / vote_score (no upvote_count).
// Use vote_score so downvotes properly demote a thread.
query = query.order('vote_score', { ascending: false }).order('created_at', { ascending: false });
} else if (sort === 'most-viewed') {
query = query.order('view_count', { ascending: false }).order('created_at', { ascending: false });
} else if (sort === 'most-replied') {
@@ -39,8 +41,9 @@ export async function GET(req: NextRequest) {
} else if (sort === 'unanswered') {
query = query.eq('reply_count', 0).order('created_at', { ascending: false });
} else {
// newest — default
query = query.order('status', { ascending: false }).order('last_reply_at', { ascending: false });
// newest — default. forum_threads has no 'status' column; sort
// pinned-first then by recent activity.
query = query.order('is_pinned', { ascending: false }).order('last_reply_at', { ascending: false, nullsFirst: false });
}
const { data, error, count } = await query;

View File

@@ -22,7 +22,7 @@ interface ForumThread {
status: string;
reply_count: number;
view_count: number;
upvote_count?: number;
upvotes?: number;
last_reply_at: string;
created_at: string;
is_ai_seeded: boolean;
@@ -71,7 +71,7 @@ function TopThreadsWidget({ categoryId }: { categoryId: string }) {
];
const getMetricValue = (thread: ForumThread) => {
if (activeMetric === 'upvotes') return { value: thread.upvote_count ?? 0, label: 'votes', color: 'text-[#22c55e]' };
if (activeMetric === 'upvotes') return { value: thread.upvotes ?? 0, label: 'votes', color: 'text-[#22c55e]' };
if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#3b82f6]' };
return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' };
};
@@ -451,7 +451,7 @@ export default function ForumCategoryPage() {
</div>
{sort === 'top-voted' && (
<div className="flex-shrink-0 text-right hidden md:block">
<div className="text-[#22c55e] font-body text-sm font-semibold">+{thread.upvote_count ?? 0}</div>
<div className="text-[#22c55e] font-body text-sm font-semibold">+{thread.upvotes ?? 0}</div>
<div className="text-[#555] font-body text-xs">votes</div>
</div>
)}

View File

@@ -22,12 +22,15 @@ interface ForumCategory {
interface ForumThread {
id: string;
title: string;
author_id?: string | null;
author_name: string;
reply_count: number;
view_count: number;
last_reply_at: string;
created_at: string;
upvote_count?: number;
upvotes?: number;
downvotes?: number;
vote_score?: number;
forum_categories?: { name: string; slug: string };
}
@@ -77,7 +80,7 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
];
const getMetricValue = (thread: ForumThread) => {
if (activeMetric === 'upvotes') return { value: thread.upvote_count ?? 0, label: 'votes', color: 'text-[#22c55e]' };
if (activeMetric === 'upvotes') return { value: thread.upvotes ?? 0, label: 'votes', color: 'text-[#22c55e]' };
if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#3b82f6]' };
return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' };
};
@@ -493,7 +496,7 @@ export default function ForumIndexPage() {
</div>
{sort === 'top-voted' && (
<div className="flex-shrink-0 text-right hidden md:block">
<div className="text-[#22c55e] font-body text-sm font-semibold">+{thread.upvote_count ?? 0}</div>
<div className="text-[#22c55e] font-body text-sm font-semibold">+{thread.upvotes ?? 0}</div>
<div className="text-[#555] font-body text-xs">votes</div>
</div>
)}