"use server"; import { revalidatePath } from "next/cache"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; // The reaction set the UI offers. The action rejects anything outside this list // so the website_article_reactions.reaction VARCHAR(50) only ever holds known // values. Keep this in sync with REACTIONS in src/app/news/[slug]/page.tsx. const ALLOWED_REACTIONS = new Set(["like", "love", "wow"]); /** * Toggle the SIGNED-IN user's reaction on a news article. * * The voter id is read from the session (re-fetched via auth()), never from the * submitted FormData, so a crafted form cannot vote as another account. A user * has at most one ACTIVE reaction per article: * - clicking the reaction they already have active -> deactivates it (un-vote) * - clicking a different reaction -> that reaction becomes active and any other * reaction rows for this user/article are deactivated * - first-ever reaction of a type -> a new active row is created * * Rows are toggled (active flag) rather than deleted so a user's history of * reaction types is preserved. website_article_reactions has no composite * unique key, so we resolve the existing row with findFirst rather than upsert. */ export async function toggleReaction(formData: FormData): Promise { const session = await auth(); if (!session?.user?.id) return; const userId = Number(session.user.id); if (!Number.isFinite(userId)) return; const reaction = String(formData.get("reaction") ?? "") .trim() .toLowerCase(); if (!ALLOWED_REACTIONS.has(reaction)) return; const articleIdRaw = String(formData.get("articleId") ?? "").trim(); if (!/^\d+$/.test(articleIdRaw)) return; let articleId: bigint; try { articleId = BigInt(articleIdRaw); } catch { return; } let slug: string | null; try { // Confirm the article exists (and grab its slug for revalidation). const article = await prisma.websiteArticles.findUnique({ where: { id: articleId }, select: { slug: true }, }); if (!article) return; slug = article.slug; // The user's current row for THIS reaction on THIS article, if any. const existing = await prisma.websiteArticleReactions.findFirst({ where: { userId, articleId, reaction }, select: { id: true, active: true }, }); if (existing?.active) { // Already reacting with this exact reaction -> un-vote (deactivate it). await prisma.websiteArticleReactions.update({ where: { id: existing.id }, data: { active: false }, }); } else { // Switching to (or first-time picking) this reaction: clear any other // active reaction by this user on this article, then activate this one. await prisma.websiteArticleReactions.updateMany({ where: { userId, articleId, active: true }, data: { active: false }, }); if (existing) { await prisma.websiteArticleReactions.update({ where: { id: existing.id }, data: { active: true }, }); } else { await prisma.websiteArticleReactions.create({ data: { userId, articleId, reaction, active: true }, }); } } } catch { // DB unavailable — fail soft; nothing to persist. return; } if (slug) revalidatePath(`/news/${slug}`); }