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