diff --git a/public/favicon.ico b/public/favicon.ico index 4eb44dd..2744e3b 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/static/favicons/favicon-16.png b/public/static/favicons/favicon-16.png new file mode 100644 index 0000000..f86d26c Binary files /dev/null and b/public/static/favicons/favicon-16.png differ diff --git a/public/static/favicons/favicon-192.png b/public/static/favicons/favicon-192.png new file mode 100644 index 0000000..1f3b4cb Binary files /dev/null and b/public/static/favicons/favicon-192.png differ diff --git a/public/static/favicons/favicon-256.png b/public/static/favicons/favicon-256.png new file mode 100644 index 0000000..80e5881 Binary files /dev/null and b/public/static/favicons/favicon-256.png differ diff --git a/public/static/favicons/favicon-32.png b/public/static/favicons/favicon-32.png new file mode 100644 index 0000000..df578f6 Binary files /dev/null and b/public/static/favicons/favicon-32.png differ diff --git a/public/static/favicons/favicon-48.png b/public/static/favicons/favicon-48.png new file mode 100644 index 0000000..7307e00 Binary files /dev/null and b/public/static/favicons/favicon-48.png differ diff --git a/public/static/favicons/favicon-64.png b/public/static/favicons/favicon-64.png new file mode 100644 index 0000000..04b4b6b Binary files /dev/null and b/public/static/favicons/favicon-64.png differ diff --git a/public/static/favicons/favicon-96.png b/public/static/favicons/favicon-96.png new file mode 100644 index 0000000..ff49d1f Binary files /dev/null and b/public/static/favicons/favicon-96.png differ diff --git a/public/static/favicons/favicon.svg b/public/static/favicons/favicon.svg new file mode 100644 index 0000000..0952fd4 --- /dev/null +++ b/public/static/favicons/favicon.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/api/forum/user-by-username/[username]/route.ts b/src/app/api/forum/user-by-username/[username]/route.ts new file mode 100644 index 0000000..a70950a --- /dev/null +++ b/src/app/api/forum/user-by-username/[username]/route.ts @@ -0,0 +1,94 @@ +import { NextResponse } from "next/server"; +import { createClient } from "@supabase/supabase-js"; + +export const runtime = "nodejs"; +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"; + +function client() { + return createClient(SUPABASE_URL, SUPABASE_ANON, { + db: { schema: SCHEMA as "public" }, + auth: { persistSession: false }, + }); +} + +export async function GET( + 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. + 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") + .select("*") + .eq("display_name", decoded) + .maybeSingle(); + profile = r2.data; + } + + if (!profile) { + 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); + + // 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); + + // 4) Computed counts + const threadCount = (threads || []).length; + const replyCount = (replies || []).length; + + 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, + }, + threads: threads || [], + replies: replies || [], + }); +} diff --git a/src/app/forum/[slug]/page.tsx b/src/app/forum/[slug]/page.tsx index 0c5bc52..37ffd3d 100644 --- a/src/app/forum/[slug]/page.tsx +++ b/src/app/forum/[slug]/page.tsx @@ -144,7 +144,7 @@ function TopThreadsWidget({ categoryId }: { categoryId: string }) { {thread.title}

- {thread.author_name} + e.stopPropagation()}>{thread.author_name} {' · '}{timeAgo(thread.created_at)}

@@ -441,7 +441,7 @@ export default function ForumCategoryPage() {

- by {thread.author_name} + by e.stopPropagation()}>{thread.author_name} {' · '}{timeAgo(thread.created_at)}

diff --git a/src/app/forum/page.tsx b/src/app/forum/page.tsx index 2a49009..bc41473 100644 --- a/src/app/forum/page.tsx +++ b/src/app/forum/page.tsx @@ -153,7 +153,7 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) { {thread.title}

- {thread.author_name} + e.stopPropagation()}>{thread.author_name} {thread.forum_categories && ( <> · {thread.forum_categories.name} )} @@ -483,7 +483,7 @@ export default function ForumIndexPage() { {thread.title}

- by {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 0316ab2..99ccf2a 100644 --- a/src/app/forum/thread/[id]/page.tsx +++ b/src/app/forum/thread/[id]/page.tsx @@ -354,7 +354,7 @@ export default function ForumThreadPage() {

- {thread.author_name} + {thread.author_name} · {timeAgo(thread.created_at)} · @@ -389,7 +389,7 @@ export default function ForumThreadPage() {
- {reply.author_name} + {reply.author_name} {reply.is_ai_response && ( AI Assistant diff --git a/src/app/forum/user/[username]/page.tsx b/src/app/forum/user/[username]/page.tsx new file mode 100644 index 0000000..ff364c8 --- /dev/null +++ b/src/app/forum/user/[username]/page.tsx @@ -0,0 +1,264 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { useParams } from "next/navigation"; +import Link from "next/link"; +import Header from "@/components/Header"; +import Footer from "@/components/Footer"; +import AppImage from "@/components/ui/AppImage"; + +interface ForumUserProfile { + username: string; + display_name: string; + bio: string | null; + avatar_url: string | null; + role_title: string | null; + company: string | null; + 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; +} + +interface UserThread { + id: string; + title: string; + reply_count: number; + view_count: number; + vote_score: number; + created_at: string; + forum_categories?: { name: string; slug: string }; +} + +interface UserReply { + id: string; + thread_id: string; + body: string; + vote_score: number; + created_at: string; + 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 { + return new Date(iso).toLocaleDateString("en-US", { month: "long", year: "numeric" }); + } catch { return iso.slice(0, 10); } +} + +export default function ForumUserPage() { + const params = useParams(); + const username = params?.username as string; + const [data, setData] = useState<{ profile: ForumUserProfile; threads: UserThread[]; replies: UserReply[] } | null>(null); + const [loading, setLoading] = useState(true); + const [notFound, setNotFound] = useState(false); + + useEffect(() => { + if (!username) return; + setLoading(true); + fetch(`/api/forum/user-by-username/${encodeURIComponent(username)}`, { cache: "no-store" }) + .then(async (r) => { + if (r.status === 404) { setNotFound(true); return null; } + if (!r.ok) return null; + return r.json(); + }) + .then((d) => { if (d) setData(d); }) + .finally(() => setLoading(false)); + }, [username]); + + if (loading) { + return ( + <> +
+
+
+

Loading profile…

+
+
+