From eb05918960c2a9ed6c61e39aa291c967e3d553a7 Mon Sep 17 00:00:00 2001
From: Local Administrator
Date: Fri, 26 Jun 2026 23:46:59 +0000
Subject: [PATCH] =?UTF-8?q?fix(forum):=20complete=20member=20profiles=20?=
=?UTF-8?q?=E2=80=94=20counts,=20avatars,=20strip=20AI=20metadata?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/api/forum/threads/[id]/route.ts | 9 +-
src/app/api/forum/threads/route.ts | 9 +-
.../user-by-username/[username]/route.ts | 121 +++++++++++-------
src/app/forum/page.tsx | 12 +-
src/app/forum/thread/[id]/page.tsx | 12 +-
src/app/forum/user/[username]/page.tsx | 26 +---
src/lib/forum/authorUsernames.ts | 25 ++++
7 files changed, 133 insertions(+), 81 deletions(-)
create mode 100644 src/lib/forum/authorUsernames.ts
diff --git a/src/app/api/forum/threads/[id]/route.ts b/src/app/api/forum/threads/[id]/route.ts
index 1b7a6d8..d4bc2d8 100644
--- a/src/app/api/forum/threads/[id]/route.ts
+++ b/src/app/api/forum/threads/[id]/route.ts
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
+import { attachAuthorUsernames, buildAuthorUsernameMap } from '@/lib/forum/authorUsernames';
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
@@ -30,7 +31,13 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
if (repliesError) throw repliesError;
- return NextResponse.json({ thread, replies: replies || [] });
+ const usernameMap = await buildAuthorUsernameMap(supabase);
+ const [threadWithUsername] = attachAuthorUsernames([thread], usernameMap);
+
+ return NextResponse.json({
+ thread: threadWithUsername,
+ replies: attachAuthorUsernames(replies, usernameMap),
+ });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
diff --git a/src/app/api/forum/threads/route.ts b/src/app/api/forum/threads/route.ts
index d30d901..c54799c 100644
--- a/src/app/api/forum/threads/route.ts
+++ b/src/app/api/forum/threads/route.ts
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { ensureForumProfile } from '@/lib/forum/profile';
+import { attachAuthorUsernames, buildAuthorUsernameMap } from '@/lib/forum/authorUsernames';
export async function GET(req: NextRequest) {
try {
@@ -50,7 +51,13 @@ export async function GET(req: NextRequest) {
const { data, error, count } = await query;
if (error) throw error;
- return NextResponse.json({ threads: data, total: count, page, limit });
+ const usernameMap = await buildAuthorUsernameMap(supabase);
+ return NextResponse.json({
+ threads: attachAuthorUsernames(data, usernameMap),
+ total: count,
+ page,
+ limit,
+ });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
diff --git a/src/app/api/forum/user-by-username/[username]/route.ts b/src/app/api/forum/user-by-username/[username]/route.ts
index 61ac981..9b539ad 100644
--- a/src/app/api/forum/user-by-username/[username]/route.ts
+++ b/src/app/api/forum/user-by-username/[username]/route.ts
@@ -6,7 +6,7 @@ export const revalidate = 120;
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const SUPABASE_ANON = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
-const SCHEMA = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb";
+const SCHEMA = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "av";
function client() {
return createClient(SUPABASE_URL, SUPABASE_ANON, {
@@ -16,24 +16,19 @@ function client() {
}
export async function GET(
- req: Request,
+ _req: Request,
{ params }: { params: Promise<{ username: string }> },
) {
const { username } = await params;
const sb = client();
-
- // 1) Profile — accept EITHER username OR display_name (URL-encoded).
- // forum_threads/forum_replies store author_name = display_name,
- // so the link in the UI uses the display name; we resolve it here.
- // Multiple profiles can share a display_name (Michael Carter, Alex Rivera,
- // etc.) — pick the earliest-created profile deterministically rather than
- // erroring out with maybeSingle().
const decoded = decodeURIComponent(username);
+
let { data: profile } = await sb
.from("forum_user_profiles")
.select("*")
.eq("username", decoded)
.maybeSingle();
+
if (!profile) {
const r2 = await sb
.from("forum_user_profiles")
@@ -48,51 +43,79 @@ export async function GET(
return NextResponse.json({ error: "not found" }, { status: 404 });
}
- // 2) Their threads (use display_name match since virtual users live
- // outside the auth.users / bb.user_profiles graph and the
- // forum_threads.author_id is NULL for them).
- const displayName = (profile as any).display_name as string;
- const { data: threads } = await sb
- .from("forum_threads")
- .select("id,title,reply_count,view_count,vote_score,created_at,category_id,forum_categories(name,slug)")
- .eq("author_name", displayName)
- .order("created_at", { ascending: false })
- .limit(50);
+ const displayName = profile.display_name as string;
- // 3) Recent replies
- const { data: replies } = await sb
- .from("forum_replies")
- .select("id,thread_id,body,vote_score,created_at,forum_threads(title)")
- .eq("author_name", displayName)
- .order("created_at", { ascending: false })
- .limit(20);
+ const [
+ { count: threadCount },
+ { count: replyCount },
+ { data: threads },
+ { data: replies },
+ ] = await Promise.all([
+ sb
+ .from("forum_threads")
+ .select("*", { count: "exact", head: true })
+ .eq("author_name", displayName),
+ sb
+ .from("forum_replies")
+ .select("*", { count: "exact", head: true })
+ .eq("author_name", displayName),
+ sb
+ .from("forum_threads")
+ .select(
+ "id,title,reply_count,view_count,vote_score,created_at,category_id,forum_categories(name,slug)",
+ )
+ .eq("author_name", displayName)
+ .order("created_at", { ascending: false })
+ .limit(50),
+ sb
+ .from("forum_replies")
+ .select("id,thread_id,body,vote_score,created_at")
+ .eq("author_name", displayName)
+ .order("created_at", { ascending: false })
+ .limit(20),
+ ]);
- // 4) Computed counts
- const threadCount = (threads || []).length;
- const replyCount = (replies || []).length;
+ const threadIds = [...new Set((replies || []).map((r) => r.thread_id).filter(Boolean))];
+ let titleByThread: Record = {};
+ if (threadIds.length > 0) {
+ const { data: threadTitles } = await sb
+ .from("forum_threads")
+ .select("id,title")
+ .in("id", threadIds);
+ titleByThread = Object.fromEntries(
+ (threadTitles || []).map((t) => [t.id, t.title]),
+ );
+ }
+
+ const repliesWithTitles = (replies || []).map((r) => ({
+ ...r,
+ forum_threads: { title: titleByThread[r.thread_id] || "(thread)" },
+ }));
+
+ const totalThreads = threadCount ?? 0;
+ const totalReplies = replyCount ?? 0;
return NextResponse.json({
profile: {
- username: (profile as any).username,
- display_name: (profile as any).display_name,
- bio: (profile as any).bio || null,
- avatar_url: (profile as any).avatar_url || null,
- role_title: (profile as any).role_title || null,
- company: (profile as any).company || null,
- location_city: (profile as any).location_city || null,
- location_country: (profile as any).location_country || null,
- years_experience: (profile as any).years_experience || null,
- ai_persona_archetype: (profile as any).ai_persona_archetype || null,
- expertise_level: (profile as any).expertise_level || null,
- expertise_tags: (profile as any).expertise_tags || [],
- writing_voice: (profile as any).writing_voice || null,
- signature: (profile as any).signature || null,
- join_date: (profile as any).join_date || null,
- last_seen_at: (profile as any).last_seen_at || null,
- thread_count: threadCount,
- reply_count: replyCount,
+ username: profile.username,
+ display_name: profile.display_name,
+ bio: profile.bio || null,
+ avatar_url: profile.avatar_url || null,
+ role_title: profile.role_title || null,
+ company: profile.company || null,
+ location_city: profile.location_city || null,
+ location_country: profile.location_country || null,
+ years_experience: profile.years_experience || null,
+ expertise_level: profile.expertise_level || null,
+ expertise_tags: profile.expertise_tags || [],
+ signature: profile.signature || null,
+ join_date: profile.join_date || null,
+ last_seen_at: profile.last_seen_at || null,
+ thread_count: totalThreads,
+ reply_count: totalReplies,
+ post_count: totalThreads + totalReplies,
},
threads: threads || [],
- replies: replies || [],
+ replies: repliesWithTitles,
});
-}
+}
\ No newline at end of file
diff --git a/src/app/forum/page.tsx b/src/app/forum/page.tsx
index 9283d2b..f561ead 100644
--- a/src/app/forum/page.tsx
+++ b/src/app/forum/page.tsx
@@ -21,11 +21,17 @@ interface ForumCategory {
last_activity_at?: string | null;
}
+function profileHref(thread: { author_username?: string | null; author_name: string }): string {
+ const slug = thread.author_username || thread.author_name;
+ return `/forum/user/${encodeURIComponent(slug)}`;
+}
+
interface ForumThread {
id: string;
title: string;
author_id?: string | null;
author_name: string;
+ author_username?: string | null;
reply_count: number;
view_count: number;
last_reply_at: string;
@@ -175,7 +181,7 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
{thread.title}
- e.stopPropagation()}>{thread.author_name}
+ e.stopPropagation()}>{thread.author_name}
{thread.forum_categories && (
<> · {thread.forum_categories.name}>
)}
@@ -407,7 +413,7 @@ export default function ForumIndexPage() {
by{' '}
e.stopPropagation()}
className="text-[#1D4ED8] hover:underline"
>
@@ -530,7 +536,7 @@ export default function ForumIndexPage() {
{thread.title}
- by e.stopPropagation()}>{thread.author_name}
+ by e.stopPropagation()}>{thread.author_name}
{thread.forum_categories && (
<> · {thread.forum_categories.name}>
)}
diff --git a/src/app/forum/thread/[id]/page.tsx b/src/app/forum/thread/[id]/page.tsx
index 64df2cc..282aee7 100644
--- a/src/app/forum/thread/[id]/page.tsx
+++ b/src/app/forum/thread/[id]/page.tsx
@@ -26,11 +26,17 @@ function InThreadAd({ adIndex }: { adIndex: number }) {
);
}
+function profileHref(author: { author_username?: string | null; author_name: string }): string {
+ const slug = author.author_username || author.author_name;
+ return `/forum/user/${encodeURIComponent(slug)}`;
+}
+
interface ForumThread {
id: string;
title: string;
body: string;
author_name: string;
+ author_username?: string | null;
status: string;
reply_count: number;
view_count: number;
@@ -45,7 +51,7 @@ interface ForumReply {
id: string;
body: string;
author_name: string;
- is_ai_response: boolean;
+ author_username?: string | null;
upvotes: number;
downvotes: number;
vote_score: number;
@@ -322,7 +328,7 @@ export default function ForumThreadPage() {
-
{thread.author_name}
+
{thread.author_name}
·
{timeAgo(thread.created_at)}
·
@@ -353,7 +359,7 @@ export default function ForumThreadPage() {
-
{reply.author_name}
+
{reply.author_name}
·
#{idx + 1}
·
diff --git a/src/app/forum/user/[username]/page.tsx b/src/app/forum/user/[username]/page.tsx
index cfadd50..bfe50a4 100644
--- a/src/app/forum/user/[username]/page.tsx
+++ b/src/app/forum/user/[username]/page.tsx
@@ -17,15 +17,14 @@ interface ForumUserProfile {
location_city: string | null;
location_country: string | null;
years_experience: number | null;
- ai_persona_archetype: string | null;
expertise_level: string | null;
expertise_tags: string[];
- writing_voice: string | null;
signature: string | null;
join_date: string | null;
last_seen_at: string | null;
thread_count: number;
reply_count: number;
+ post_count: number;
}
interface UserThread {
@@ -47,22 +46,6 @@ interface UserReply {
forum_threads?: { title: string };
}
-const ARCHETYPE_LABELS: Record
= {
- audio_engineer: "Audio Engineer",
- motion_designer: "Motion Designer",
- broadcast_engineer: "Broadcast Engineer",
- colorist: "Colorist",
- editor: "Editor",
- vfx_artist: "VFX Artist",
- camera_operator: "Camera Operator",
- lighting_designer: "Lighting Designer",
- post_supervisor: "Post Supervisor",
- gear_specialist: "Gear Specialist",
- livestream_producer: "Livestream Producer",
- generalist: "Generalist",
- video_editor: "Video Editor",
-};
-
function fmtDate(iso: string | null): string {
if (!iso) return "—";
try {
@@ -134,7 +117,6 @@ export default function ForumUserPage() {
const { profile, threads, replies } = data;
const location = [profile.location_city, profile.location_country].filter(Boolean).join(", ");
- const archetypeLabel = profile.ai_persona_archetype ? (ARCHETYPE_LABELS[profile.ai_persona_archetype] || profile.ai_persona_archetype) : null;
return (
<>
@@ -171,11 +153,6 @@ export default function ForumUserPage() {
@{profile.username}
- {archetypeLabel && (
-
- {archetypeLabel}
-
- )}
{profile.expertise_level && (
{profile.expertise_level}
@@ -271,6 +248,7 @@ export default function ForumUserPage() {
- Role
- {profile.role_title || "—"}
{profile.company && - Company
- {profile.company}
}
+ - Posts
- {profile.post_count}
- Threads
- {profile.thread_count}
- Replies
- {profile.reply_count}
{profile.years_experience && - Years in industry
- {profile.years_experience}
}
diff --git a/src/lib/forum/authorUsernames.ts b/src/lib/forum/authorUsernames.ts
new file mode 100644
index 0000000..e17ec4e
--- /dev/null
+++ b/src/lib/forum/authorUsernames.ts
@@ -0,0 +1,25 @@
+import type { SupabaseClient } from "@supabase/supabase-js";
+
+export async function buildAuthorUsernameMap(
+ supabase: SupabaseClient,
+): Promise> {
+ const { data } = await supabase
+ .from("forum_user_profiles")
+ .select("username, display_name");
+ const map: Record = {};
+ for (const row of data || []) {
+ if (row.display_name && row.username) {
+ map[row.display_name] = row.username;
+ }
+ }
+ return map;
+}
+
+export function attachAuthorUsernames<
+ T extends { author_name?: string | null },
+>(rows: T[] | null | undefined, map: Record) {
+ return (rows || []).map((row) => ({
+ ...row,
+ author_username: row.author_name ? map[row.author_name] || null : null,
+ }));
+}
\ No newline at end of file