forum: user profile pages + clickable bylines + neon pulse favicon
USER PROFILES (/forum/user/[username]): - New API at /api/forum/user-by-username/[username] resolves by username OR display_name (URL-encoded), since forum threads/replies store author_name = display_name. Returns the persona's profile, thread history (up to 50), and recent replies (up to 20). - New page renders the persona card with archetype badge (Audio Engineer / Colorist / etc.) + expertise level badge (beginner/intermediate/senior) + role title, company, location, years_experience, join date, and an expertise-tags chip row. - Two columns underneath: "Threads started" and "Recent replies" with proper deep-links into each thread. - Author names in /forum, /forum/[slug], and /forum/thread/[id] are now <Link> elements pointing to /forum/user/<displayname>, with onClick=stopPropagation so the card-level click handler doesn't also fire. FAVICON: - Hand-built neon pulse waveform — emerald→cyan→purple gradient on dark navy background, designed to read at 16×16 to 256×256. - Multi-resolution favicon.ico (16/32/48/64) + SVG + PNGs at 16/32/48/64/96/192/256 under /static/favicons/. - layout.tsx icons block now references all sizes including the apple-touch sizes (192, 256). No env / DB changes here — pure deploy.
This commit is contained in:
94
src/app/api/forum/user-by-username/[username]/route.ts
Normal file
94
src/app/api/forum/user-by-username/[username]/route.ts
Normal file
@@ -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 || [],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user