articles: discussion threads + Related Forum Posts sidebar
ArticleComments component renders under every news + articles detail page. Anonymous visitors see all existing comments and a Sign-in CTA; signed-in members get a composer, threaded replies, and up/down votes. AI personas are tagged with a small AI chip so readers know what they're interacting with — same persona TZ rules from #63 apply. Schema extensions on bb.article_comments: parent_comment_id self-FK for one-level threading, denormalized author fields, vote counters, is_ai_seeded flag, status enum. user_id relaxed to nullable so AI rows can exist without a real user_profile; CHECK constraint enforces 'real user OR is_ai_seeded' so anonymous comments can never sneak through. forum_votes.target_type check expanded to include 'comment' — same polymorphic vote table powers thread/reply/comment votes. New Related Forum Posts sidebar on both /articles/[slug] and /news/[slug]. getRelatedForumThreads() does title-keyword ILIKE OR against forum_threads, ranked by reply_count + recency, with recently- active fallback so the box is never empty. 6 threads per article. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
72
src/app/api/comments/[id]/vote/route.ts
Normal file
72
src/app/api/comments/[id]/vote/route.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* POST /api/comments/[id]/vote { vote: 1 | -1 | 0 }
|
||||
*
|
||||
* Up/down/clear vote on an article comment. Uses the existing
|
||||
* polymorphic forum_votes table (target_type='comment').
|
||||
*
|
||||
* 0 → remove your existing vote (toggle).
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createClient } from "@/lib/supabase/server";
|
||||
|
||||
export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await ctx.params;
|
||||
const sb = await createClient();
|
||||
const { data: { user } } = await sb.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: "Sign in to vote" }, { status: 401 });
|
||||
|
||||
let body: { vote?: number };
|
||||
try { body = await req.json(); }
|
||||
catch { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); }
|
||||
|
||||
const v = body.vote === 1 ? 1 : body.vote === -1 ? -1 : 0;
|
||||
|
||||
// Always look up the current vote first so we can compute the diff
|
||||
// and apply it atomically to the comment's denormalized counters.
|
||||
const { data: existing } = await sb
|
||||
.from("forum_votes")
|
||||
.select("id, vote_value")
|
||||
.eq("user_id", user.id)
|
||||
.eq("target_type", "comment")
|
||||
.eq("target_id", id)
|
||||
.maybeSingle();
|
||||
|
||||
let upDelta = 0, downDelta = 0;
|
||||
if (existing) {
|
||||
// Reverse the existing vote's counter contribution first
|
||||
if (existing.vote_value === 1) upDelta -= 1;
|
||||
else if (existing.vote_value === -1) downDelta -= 1;
|
||||
|
||||
if (v === 0) {
|
||||
await sb.from("forum_votes").delete().eq("id", existing.id);
|
||||
} else {
|
||||
await sb.from("forum_votes")
|
||||
.update({ vote_value: v, updated_at: new Date().toISOString() })
|
||||
.eq("id", existing.id);
|
||||
if (v === 1) upDelta += 1; else downDelta += 1;
|
||||
}
|
||||
} else if (v !== 0) {
|
||||
await sb.from("forum_votes").insert({
|
||||
user_id: user.id, target_type: "comment", target_id: id, vote_value: v,
|
||||
});
|
||||
if (v === 1) upDelta += 1; else downDelta += 1;
|
||||
}
|
||||
|
||||
// Apply counter delta to the comment row. Use rpc/raw fragment to add
|
||||
// integers atomically (supabase-js doesn't expose increment; we read &
|
||||
// write — racy under heavy concurrency but acceptable for forum scale).
|
||||
const { data: cur } = await sb
|
||||
.from("article_comments")
|
||||
.select("upvotes, downvotes")
|
||||
.eq("id", id)
|
||||
.maybeSingle();
|
||||
if (cur) {
|
||||
const upvotes = Math.max(0, (cur.upvotes || 0) + upDelta);
|
||||
const downvotes = Math.max(0, (cur.downvotes || 0) + downDelta);
|
||||
await sb.from("article_comments").update({
|
||||
upvotes, downvotes, vote_score: upvotes - downvotes,
|
||||
}).eq("id", id);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, vote: v });
|
||||
}
|
||||
Reference in New Issue
Block a user