"use client"; import { useState, useEffect } from "react"; import { toast } from "sonner"; import { useTranslations } from "next-intl"; const REACTIONS: Record = { like: "👍", love: "❤️", laugh: "😂", wow: "😮", sad: "😢", fire: "🔥", }; type Props = { recipeId: string; commentId: string; initialCounts?: Record; initialUserReactions?: string[]; }; export function CommentReactions({ recipeId, commentId, initialCounts = {}, initialUserReactions = [] }: Props) { const t = useTranslations("social"); const [counts, setCounts] = useState>(initialCounts); const [userReactions, setUserReactions] = useState(initialUserReactions); const [pending, setPending] = useState(null); useEffect(() => { let cancelled = false; fetch(`/api/v1/recipes/${recipeId}/comments/${commentId}/reactions`) .then((res) => (res.ok ? res.json() : null)) .then((data: { counts: Record; userReactions: string[] } | null) => { if (!data || cancelled) return; setCounts(data.counts); setUserReactions(data.userReactions); }) .catch(() => {}); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [recipeId, commentId]); async function toggle(type: string) { if (pending) return; setPending(type); // Optimistic update const hasReacted = userReactions.includes(type); setUserReactions((prev) => hasReacted ? prev.filter((r) => r !== type) : [...prev, type], ); setCounts((prev) => ({ ...prev, [type]: Math.max(0, (prev[type] ?? 0) + (hasReacted ? -1 : 1)), })); try { const res = await fetch(`/api/v1/recipes/${recipeId}/comments/${commentId}/reactions`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type }), }); if (!res.ok) { // Revert optimistic update setUserReactions((prev) => hasReacted ? [...prev, type] : prev.filter((r) => r !== type), ); setCounts((prev) => ({ ...prev, [type]: Math.max(0, (prev[type] ?? 0) + (hasReacted ? 1 : -1)), })); if (res.status === 401) { toast.error(t("signInToReact")); } else { toast.error(t("reactionFailed")); } return; } const data = await res.json() as { counts: Record; added: boolean }; setCounts(data.counts); setUserReactions((prev) => { if (data.added && !prev.includes(type)) return [...prev, type]; if (!data.added && prev.includes(type)) return prev.filter((r) => r !== type); return prev; }); } catch { // Revert on network error setUserReactions((prev) => hasReacted ? [...prev, type] : prev.filter((r) => r !== type), ); setCounts((prev) => ({ ...prev, [type]: Math.max(0, (prev[type] ?? 0) + (hasReacted ? 1 : -1)), })); toast.error(t("reactionFailed")); } finally { setPending(null); } } return (
{Object.entries(REACTIONS).map(([type, emoji]) => { const reacted = userReactions.includes(type); const cnt = counts[type] ?? 0; return ( ); })}
); }