fix(forum): complete member profiles — counts, avatars, strip AI metadata
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createClient } from '@/lib/supabase/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 }> }) {
|
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
try {
|
try {
|
||||||
@@ -30,7 +31,13 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
|
|
||||||
if (repliesError) throw repliesError;
|
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) {
|
} catch (err: any) {
|
||||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createClient } from '@/lib/supabase/server';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { ensureForumProfile } from '@/lib/forum/profile';
|
import { ensureForumProfile } from '@/lib/forum/profile';
|
||||||
|
import { attachAuthorUsernames, buildAuthorUsernameMap } from '@/lib/forum/authorUsernames';
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -50,7 +51,13 @@ export async function GET(req: NextRequest) {
|
|||||||
const { data, error, count } = await query;
|
const { data, error, count } = await query;
|
||||||
if (error) throw error;
|
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) {
|
} catch (err: any) {
|
||||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const revalidate = 120;
|
|||||||
|
|
||||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||||
const SUPABASE_ANON = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
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() {
|
function client() {
|
||||||
return createClient(SUPABASE_URL, SUPABASE_ANON, {
|
return createClient(SUPABASE_URL, SUPABASE_ANON, {
|
||||||
@@ -16,24 +16,19 @@ function client() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
req: Request,
|
_req: Request,
|
||||||
{ params }: { params: Promise<{ username: string }> },
|
{ params }: { params: Promise<{ username: string }> },
|
||||||
) {
|
) {
|
||||||
const { username } = await params;
|
const { username } = await params;
|
||||||
const sb = client();
|
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);
|
const decoded = decodeURIComponent(username);
|
||||||
|
|
||||||
let { data: profile } = await sb
|
let { data: profile } = await sb
|
||||||
.from("forum_user_profiles")
|
.from("forum_user_profiles")
|
||||||
.select("*")
|
.select("*")
|
||||||
.eq("username", decoded)
|
.eq("username", decoded)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
const r2 = await sb
|
const r2 = await sb
|
||||||
.from("forum_user_profiles")
|
.from("forum_user_profiles")
|
||||||
@@ -48,51 +43,79 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: "not found" }, { status: 404 });
|
return NextResponse.json({ error: "not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Their threads (use display_name match since virtual users live
|
const displayName = profile.display_name as string;
|
||||||
// outside the auth.users / bb.user_profiles graph and the
|
|
||||||
// forum_threads.author_id is NULL for them).
|
const [
|
||||||
const displayName = (profile as any).display_name as string;
|
{ count: threadCount },
|
||||||
const { data: threads } = await sb
|
{ count: replyCount },
|
||||||
|
{ data: threads },
|
||||||
|
{ data: replies },
|
||||||
|
] = await Promise.all([
|
||||||
|
sb
|
||||||
.from("forum_threads")
|
.from("forum_threads")
|
||||||
.select("id,title,reply_count,view_count,vote_score,created_at,category_id,forum_categories(name,slug)")
|
.select("*", { count: "exact", head: true })
|
||||||
.eq("author_name", displayName)
|
.eq("author_name", displayName),
|
||||||
.order("created_at", { ascending: false })
|
sb
|
||||||
.limit(50);
|
|
||||||
|
|
||||||
// 3) Recent replies
|
|
||||||
const { data: replies } = await sb
|
|
||||||
.from("forum_replies")
|
.from("forum_replies")
|
||||||
.select("id,thread_id,body,vote_score,created_at,forum_threads(title)")
|
.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)
|
.eq("author_name", displayName)
|
||||||
.order("created_at", { ascending: false })
|
.order("created_at", { ascending: false })
|
||||||
.limit(20);
|
.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 threadIds = [...new Set((replies || []).map((r) => r.thread_id).filter(Boolean))];
|
||||||
const threadCount = (threads || []).length;
|
let titleByThread: Record<string, string> = {};
|
||||||
const replyCount = (replies || []).length;
|
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({
|
return NextResponse.json({
|
||||||
profile: {
|
profile: {
|
||||||
username: (profile as any).username,
|
username: profile.username,
|
||||||
display_name: (profile as any).display_name,
|
display_name: profile.display_name,
|
||||||
bio: (profile as any).bio || null,
|
bio: profile.bio || null,
|
||||||
avatar_url: (profile as any).avatar_url || null,
|
avatar_url: profile.avatar_url || null,
|
||||||
role_title: (profile as any).role_title || null,
|
role_title: profile.role_title || null,
|
||||||
company: (profile as any).company || null,
|
company: profile.company || null,
|
||||||
location_city: (profile as any).location_city || null,
|
location_city: profile.location_city || null,
|
||||||
location_country: (profile as any).location_country || null,
|
location_country: profile.location_country || null,
|
||||||
years_experience: (profile as any).years_experience || null,
|
years_experience: profile.years_experience || null,
|
||||||
ai_persona_archetype: (profile as any).ai_persona_archetype || null,
|
expertise_level: profile.expertise_level || null,
|
||||||
expertise_level: (profile as any).expertise_level || null,
|
expertise_tags: profile.expertise_tags || [],
|
||||||
expertise_tags: (profile as any).expertise_tags || [],
|
signature: profile.signature || null,
|
||||||
writing_voice: (profile as any).writing_voice || null,
|
join_date: profile.join_date || null,
|
||||||
signature: (profile as any).signature || null,
|
last_seen_at: profile.last_seen_at || null,
|
||||||
join_date: (profile as any).join_date || null,
|
thread_count: totalThreads,
|
||||||
last_seen_at: (profile as any).last_seen_at || null,
|
reply_count: totalReplies,
|
||||||
thread_count: threadCount,
|
post_count: totalThreads + totalReplies,
|
||||||
reply_count: replyCount,
|
|
||||||
},
|
},
|
||||||
threads: threads || [],
|
threads: threads || [],
|
||||||
replies: replies || [],
|
replies: repliesWithTitles,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -21,11 +21,17 @@ interface ForumCategory {
|
|||||||
last_activity_at?: string | null;
|
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 {
|
interface ForumThread {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
author_id?: string | null;
|
author_id?: string | null;
|
||||||
author_name: string;
|
author_name: string;
|
||||||
|
author_username?: string | null;
|
||||||
reply_count: number;
|
reply_count: number;
|
||||||
view_count: number;
|
view_count: number;
|
||||||
last_reply_at: string;
|
last_reply_at: string;
|
||||||
@@ -175,7 +181,7 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
|
|||||||
{thread.title}
|
{thread.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[#475569] font-body text-xs mt-0.5">
|
<p className="text-[#475569] font-body text-xs mt-0.5">
|
||||||
<Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#475569] hover:text-[#1D4ED8] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
|
<Link href={profileHref(thread)} className="text-[#475569] hover:text-[#1D4ED8] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
|
||||||
{thread.forum_categories && (
|
{thread.forum_categories && (
|
||||||
<> · <span className="text-[#1D4ED8]">{thread.forum_categories.name}</span></>
|
<> · <span className="text-[#1D4ED8]">{thread.forum_categories.name}</span></>
|
||||||
)}
|
)}
|
||||||
@@ -407,7 +413,7 @@ export default function ForumIndexPage() {
|
|||||||
<p className="text-[#666] font-body text-xs mt-0.5">
|
<p className="text-[#666] font-body text-xs mt-0.5">
|
||||||
by{' '}
|
by{' '}
|
||||||
<Link
|
<Link
|
||||||
href={`/profile/${thread.author_id}`}
|
href={profileHref(thread)}
|
||||||
onClick={e => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
className="text-[#1D4ED8] hover:underline"
|
className="text-[#1D4ED8] hover:underline"
|
||||||
>
|
>
|
||||||
@@ -530,7 +536,7 @@ export default function ForumIndexPage() {
|
|||||||
{thread.title}
|
{thread.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[#666] font-body text-xs mt-0.5">
|
<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-[#1D4ED8] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
|
by <Link href={profileHref(thread)} className="text-[#888] hover:text-[#1D4ED8] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
|
||||||
{thread.forum_categories && (
|
{thread.forum_categories && (
|
||||||
<> · <span className="text-[#1D4ED8]">{thread.forum_categories.name}</span></>
|
<> · <span className="text-[#1D4ED8]">{thread.forum_categories.name}</span></>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 {
|
interface ForumThread {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
body: string;
|
body: string;
|
||||||
author_name: string;
|
author_name: string;
|
||||||
|
author_username?: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
reply_count: number;
|
reply_count: number;
|
||||||
view_count: number;
|
view_count: number;
|
||||||
@@ -45,7 +51,7 @@ interface ForumReply {
|
|||||||
id: string;
|
id: string;
|
||||||
body: string;
|
body: string;
|
||||||
author_name: string;
|
author_name: string;
|
||||||
is_ai_response: boolean;
|
author_username?: string | null;
|
||||||
upvotes: number;
|
upvotes: number;
|
||||||
downvotes: number;
|
downvotes: number;
|
||||||
vote_score: number;
|
vote_score: number;
|
||||||
@@ -322,7 +328,7 @@ export default function ForumThreadPage() {
|
|||||||
<Avatar name={thread.author_name} />
|
<Avatar name={thread.author_name} />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-white font-body font-semibold text-sm hover:text-[#1D4ED8] hover:underline">{thread.author_name}</Link>
|
<Link href={profileHref(thread)} className="text-white font-body font-semibold text-sm hover:text-[#1D4ED8] hover:underline">{thread.author_name}</Link>
|
||||||
<span className="text-[#555] font-body text-xs">·</span>
|
<span className="text-[#555] font-body text-xs">·</span>
|
||||||
<span className="text-[#555] font-body text-xs">{timeAgo(thread.created_at)}</span>
|
<span className="text-[#555] font-body text-xs">{timeAgo(thread.created_at)}</span>
|
||||||
<span className="text-[#555] font-body text-xs">·</span>
|
<span className="text-[#555] font-body text-xs">·</span>
|
||||||
@@ -353,7 +359,7 @@ export default function ForumThreadPage() {
|
|||||||
<Avatar name={reply.author_name} />
|
<Avatar name={reply.author_name} />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||||
<Link href={`/forum/user/${encodeURIComponent(reply.author_name)}`} className="text-white font-body font-semibold text-sm hover:text-[#1D4ED8] hover:underline">{reply.author_name}</Link>
|
<Link href={profileHref(reply)} className="text-white font-body font-semibold text-sm hover:text-[#1D4ED8] hover:underline">{reply.author_name}</Link>
|
||||||
<span className="text-[#555] font-body text-xs">·</span>
|
<span className="text-[#555] font-body text-xs">·</span>
|
||||||
<span className="text-[#555] font-body text-xs">#{idx + 1}</span>
|
<span className="text-[#555] font-body text-xs">#{idx + 1}</span>
|
||||||
<span className="text-[#555] font-body text-xs">·</span>
|
<span className="text-[#555] font-body text-xs">·</span>
|
||||||
|
|||||||
@@ -17,15 +17,14 @@ interface ForumUserProfile {
|
|||||||
location_city: string | null;
|
location_city: string | null;
|
||||||
location_country: string | null;
|
location_country: string | null;
|
||||||
years_experience: number | null;
|
years_experience: number | null;
|
||||||
ai_persona_archetype: string | null;
|
|
||||||
expertise_level: string | null;
|
expertise_level: string | null;
|
||||||
expertise_tags: string[];
|
expertise_tags: string[];
|
||||||
writing_voice: string | null;
|
|
||||||
signature: string | null;
|
signature: string | null;
|
||||||
join_date: string | null;
|
join_date: string | null;
|
||||||
last_seen_at: string | null;
|
last_seen_at: string | null;
|
||||||
thread_count: number;
|
thread_count: number;
|
||||||
reply_count: number;
|
reply_count: number;
|
||||||
|
post_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserThread {
|
interface UserThread {
|
||||||
@@ -47,22 +46,6 @@ interface UserReply {
|
|||||||
forum_threads?: { title: string };
|
forum_threads?: { title: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ARCHETYPE_LABELS: Record<string, string> = {
|
|
||||||
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 {
|
function fmtDate(iso: string | null): string {
|
||||||
if (!iso) return "—";
|
if (!iso) return "—";
|
||||||
try {
|
try {
|
||||||
@@ -134,7 +117,6 @@ export default function ForumUserPage() {
|
|||||||
|
|
||||||
const { profile, threads, replies } = data;
|
const { profile, threads, replies } = data;
|
||||||
const location = [profile.location_city, profile.location_country].filter(Boolean).join(", ");
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -171,11 +153,6 @@ export default function ForumUserPage() {
|
|||||||
<p className="font-body text-sm text-[#1D4ED8] mt-0.5">@{profile.username}</p>
|
<p className="font-body text-sm text-[#1D4ED8] mt-0.5">@{profile.username}</p>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2 mt-3">
|
<div className="flex flex-wrap items-center gap-2 mt-3">
|
||||||
{archetypeLabel && (
|
|
||||||
<span className="font-body text-[10px] font-bold uppercase tracking-wider text-[#10b981] bg-[#10b981]/10 border border-[#10b981]/40 px-2 py-1 rounded-sm">
|
|
||||||
{archetypeLabel}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{profile.expertise_level && (
|
{profile.expertise_level && (
|
||||||
<span className="font-body text-[10px] font-bold uppercase tracking-wider text-[#a78bfa] bg-[#a78bfa]/10 border border-[#a78bfa]/40 px-2 py-1 rounded-sm">
|
<span className="font-body text-[10px] font-bold uppercase tracking-wider text-[#a78bfa] bg-[#a78bfa]/10 border border-[#a78bfa]/40 px-2 py-1 rounded-sm">
|
||||||
{profile.expertise_level}
|
{profile.expertise_level}
|
||||||
@@ -271,6 +248,7 @@ export default function ForumUserPage() {
|
|||||||
<dl className="text-sm space-y-2">
|
<dl className="text-sm space-y-2">
|
||||||
<div><dt className="text-[#666] text-[11px] uppercase tracking-wider">Role</dt><dd className="text-[#0F172A]">{profile.role_title || "—"}</dd></div>
|
<div><dt className="text-[#666] text-[11px] uppercase tracking-wider">Role</dt><dd className="text-[#0F172A]">{profile.role_title || "—"}</dd></div>
|
||||||
{profile.company && <div><dt className="text-[#666] text-[11px] uppercase tracking-wider">Company</dt><dd className="text-[#0F172A]">{profile.company}</dd></div>}
|
{profile.company && <div><dt className="text-[#666] text-[11px] uppercase tracking-wider">Company</dt><dd className="text-[#0F172A]">{profile.company}</dd></div>}
|
||||||
|
<div><dt className="text-[#666] text-[11px] uppercase tracking-wider">Posts</dt><dd className="text-[#0F172A]">{profile.post_count}</dd></div>
|
||||||
<div><dt className="text-[#666] text-[11px] uppercase tracking-wider">Threads</dt><dd className="text-[#0F172A]">{profile.thread_count}</dd></div>
|
<div><dt className="text-[#666] text-[11px] uppercase tracking-wider">Threads</dt><dd className="text-[#0F172A]">{profile.thread_count}</dd></div>
|
||||||
<div><dt className="text-[#666] text-[11px] uppercase tracking-wider">Replies</dt><dd className="text-[#0F172A]">{profile.reply_count}</dd></div>
|
<div><dt className="text-[#666] text-[11px] uppercase tracking-wider">Replies</dt><dd className="text-[#0F172A]">{profile.reply_count}</dd></div>
|
||||||
{profile.years_experience && <div><dt className="text-[#666] text-[11px] uppercase tracking-wider">Years in industry</dt><dd className="text-[#0F172A]">{profile.years_experience}</dd></div>}
|
{profile.years_experience && <div><dt className="text-[#666] text-[11px] uppercase tracking-wider">Years in industry</dt><dd className="text-[#0F172A]">{profile.years_experience}</dd></div>}
|
||||||
|
|||||||
25
src/lib/forum/authorUsernames.ts
Normal file
25
src/lib/forum/authorUsernames.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export async function buildAuthorUsernameMap(
|
||||||
|
supabase: SupabaseClient,
|
||||||
|
): Promise<Record<string, string>> {
|
||||||
|
const { data } = await supabase
|
||||||
|
.from("forum_user_profiles")
|
||||||
|
.select("username, display_name");
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
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<string, string>) {
|
||||||
|
return (rows || []).map((row) => ({
|
||||||
|
...row,
|
||||||
|
author_username: row.author_name ? map[row.author_name] || null : null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user