Files
Epicure/apps/web/components/social/report-button.tsx
T
Arnaud 57c29f62b4 feat: user blocking and content reporting/flagging
- user_blocks table (composite PK), Block/Unblock button on profiles.
  Blocking severs any existing follow relationship both ways and
  prevents the blocked party from following, commenting on the
  blocker's recipes, or the blocker from seeing their comments.
- reports table (recipe/comment/user targets, pending/reviewed/
  dismissed status). ReportButton on comments, admin review queue at
  /admin/reports with dismiss/mark-reviewed actions, audit-logged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 22:09:14 +02:00

92 lines
3.0 KiB
TypeScript

"use client";
import { useState } from "react";
import { toast } from "sonner";
import { Flag } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
export function ReportButton({
targetType,
targetId,
variant = "icon",
}: {
targetType: "recipe" | "comment" | "user";
targetId: string;
variant?: "icon" | "text";
}) {
const [open, setOpen] = useState(false);
const [reason, setReason] = useState("");
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
async function handleSubmit() {
if (!reason.trim()) return;
setSubmitting(true);
try {
const res = await fetch("/api/v1/reports", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ targetType, targetId, reason: reason.trim() }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({})) as { error?: string };
toast.error(err.error ?? "Failed to submit report");
return;
}
setSubmitted(true);
toast.success("Report submitted — thanks for flagging this");
setTimeout(() => setOpen(false), 800);
} finally {
setSubmitting(false);
}
}
return (
<>
{variant === "icon" ? (
<button
onClick={() => setOpen(true)}
className="text-xs text-muted-foreground hover:text-destructive flex items-center gap-1"
>
<Flag className="h-3 w-3" /> Report
</button>
) : (
<Button variant="ghost" size="icon" onClick={() => setOpen(true)}>
<Flag className="h-4 w-4" />
</Button>
)}
<Dialog open={open} onOpenChange={(next) => { setOpen(next); if (!next) { setReason(""); setSubmitted(false); } }}>
<DialogContent className="max-w-md">
<DialogHeader><DialogTitle>Report {targetType}</DialogTitle></DialogHeader>
{submitted ? (
<p className="text-sm text-muted-foreground py-4">Thanks an admin will review this.</p>
) : (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="report-reason">What's wrong with this?</Label>
<Textarea
id="report-reason"
value={reason}
onChange={(e) => setReason(e.target.value)}
placeholder="Spam, harassment, off-topic…"
rows={3}
/>
</div>
<Button
className="w-full"
onClick={() => { void handleSubmit(); }}
disabled={!reason.trim() || submitting}
>
{submitting ? "Submitting…" : "Submit report"}
</Button>
</div>
)}
</DialogContent>
</Dialog>
</>
);
}