fix(forum): complete member profiles — counts, avatars, strip AI metadata

This commit is contained in:
2026-06-26 23:46:59 +00:00
parent 4acc068732
commit eb05918960
7 changed files with 133 additions and 81 deletions

View File

@@ -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<string, string> = {};
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,
});
}
}