/** * Article comments API. * * GET /api/comments?article_slug= → list, threaded by parent * POST /api/comments → create (auth required) * * Reads: public — anyone can fetch. * Writes: signed-in users only. We assert via supabase auth and use the * user's forum_user_profile for the denormalized display fields so the * client can render avatar/name without an extra join per comment. */ import { NextRequest, NextResponse } from "next/server"; import { createClient } from "@/lib/supabase/server"; export const dynamic = "force-dynamic"; interface Row { id: string; article_slug: string; parent_comment_id: string | null; user_id: string | null; content: string; author_username: string | null; author_display_name: string | null; author_avatar_url: string | null; is_ai_seeded: boolean; upvotes: number; downvotes: number; vote_score: number; status: string; created_at: string; updated_at: string; } export async function GET(req: NextRequest) { const slug = new URL(req.url).searchParams.get("article_slug"); if (!slug) return NextResponse.json({ error: "article_slug required" }, { status: 400 }); const sb = await createClient(); const { data, error } = await sb .from("article_comments") .select("*") .eq("article_slug", slug) .eq("status", "published") .order("vote_score", { ascending: false }) .order("created_at", { ascending: true }) .limit(500); if (error) return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ comments: data || [] }); } export async function POST(req: NextRequest) { const sb = await createClient(); const { data: { user } } = await sb.auth.getUser(); if (!user) { return NextResponse.json( { error: "Sign in to comment", redirectTo: "/forum/login" }, { status: 401 }, ); } let body: { article_slug?: string; content?: string; parent_comment_id?: string | null }; try { body = await req.json(); } catch { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); } const slug = (body.article_slug || "").trim(); const content = (body.content || "").trim(); if (!slug || !content) { return NextResponse.json({ error: "article_slug and content required" }, { status: 400 }); } if (content.length > 5000) { return NextResponse.json({ error: "Comment too long (5000 char max)" }, { status: 400 }); } // Pull the user's forum profile so we have display name + avatar for // denormalized rendering (saves a per-comment join later). const { data: prof } = await sb .from("forum_user_profiles") .select("username, display_name, avatar_url") .eq("user_id", user.id) .maybeSingle(); const ins = { article_slug: slug, user_id: user.id, content, parent_comment_id: body.parent_comment_id || null, is_ai_seeded: false, author_username: prof?.username || null, author_display_name: prof?.display_name || user.email?.split("@")[0] || "Member", author_avatar_url: prof?.avatar_url || null, }; const { data, error } = await sb .from("article_comments") .insert(ins) .select() .single(); if (error) return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ comment: data }); }