Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
1import React, { useState } from "react";
2import { Flag, X } from "lucide-react";
3import { reportUser } from "../../api/client";
4import type { ReportReasonType } from "../../types";
5
6interface ReportModalProps {
7 isOpen: boolean;
8 onClose: () => void;
9 subjectDid: string;
10 subjectUri?: string;
11 subjectHandle?: string;
12}
13
14const REASONS: {
15 value: ReportReasonType;
16 label: string;
17 description: string;
18}[] = [
19 { value: "spam", label: "Spam", description: "Unwanted repetitive content" },
20 {
21 value: "violation",
22 label: "Rule violation",
23 description: "Violates community guidelines",
24 },
25 {
26 value: "misleading",
27 label: "Misleading",
28 description: "False or misleading information",
29 },
30 {
31 value: "rude",
32 label: "Rude or harassing",
33 description: "Targeting or harassing a user",
34 },
35 {
36 value: "sexual",
37 label: "Inappropriate content",
38 description: "Sexual or explicit material",
39 },
40 {
41 value: "other",
42 label: "Other",
43 description: "Something else not listed above",
44 },
45];
46
47export default function ReportModal({
48 isOpen,
49 onClose,
50 subjectDid,
51 subjectUri,
52 subjectHandle,
53}: ReportModalProps) {
54 const [selectedReason, setSelectedReason] = useState<ReportReasonType | null>(
55 null,
56 );
57 const [additionalText, setAdditionalText] = useState("");
58 const [submitting, setSubmitting] = useState(false);
59 const [submitted, setSubmitted] = useState(false);
60
61 if (!isOpen) return null;
62
63 const handleSubmit = async () => {
64 if (!selectedReason) return;
65
66 setSubmitting(true);
67 const success = await reportUser({
68 subjectDid: subjectDid,
69 subjectUri: subjectUri,
70 reasonType: selectedReason,
71 reasonText: additionalText || undefined,
72 });
73
74 setSubmitting(false);
75 if (success) {
76 setSubmitted(true);
77 setTimeout(() => {
78 onClose();
79 setSubmitted(false);
80 setSelectedReason(null);
81 setAdditionalText("");
82 }, 1500);
83 }
84 };
85
86 const handleClose = () => {
87 onClose();
88 setSelectedReason(null);
89 setAdditionalText("");
90 setSubmitted(false);
91 };
92
93 return (
94 <div
95 className="fixed inset-0 z-[200] flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in"
96 onClick={handleClose}
97 >
98 <div
99 className="bg-white dark:bg-surface-900 rounded-2xl shadow-2xl border border-surface-200 dark:border-surface-700 w-full max-w-md mx-4 overflow-hidden"
100 onClick={(e) => e.stopPropagation()}
101 >
102 {submitted ? (
103 <div className="p-8 text-center">
104 <div className="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-3">
105 <Flag size={20} className="text-green-600 dark:text-green-400" />
106 </div>
107 <h3 className="text-lg font-semibold text-surface-900 dark:text-white">
108 Report submitted
109 </h3>
110 <p className="text-surface-500 dark:text-surface-400 text-sm mt-1">
111 Thank you. We'll review this shortly.
112 </p>
113 </div>
114 ) : (
115 <>
116 <div className="flex items-center justify-between p-4 border-b border-surface-200 dark:border-surface-700">
117 <div className="flex items-center gap-2.5">
118 <div className="w-8 h-8 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center">
119 <Flag size={16} className="text-red-600 dark:text-red-400" />
120 </div>
121 <div>
122 <h3 className="text-base font-semibold text-surface-900 dark:text-white">
123 Report {subjectHandle ? `@${subjectHandle}` : "user"}
124 </h3>
125 {subjectUri && (
126 <p className="text-xs text-surface-400 dark:text-surface-500">
127 Reporting specific content
128 </p>
129 )}
130 </div>
131 </div>
132 <button
133 onClick={handleClose}
134 className="p-1.5 text-surface-400 hover:text-surface-600 dark:hover:text-surface-300 rounded-lg hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors"
135 >
136 <X size={18} />
137 </button>
138 </div>
139
140 <div className="p-4 space-y-2">
141 <p className="text-sm font-medium text-surface-700 dark:text-surface-300 mb-3">
142 What's the issue?
143 </p>
144 {REASONS.map((reason) => (
145 <button
146 key={reason.value}
147 onClick={() => setSelectedReason(reason.value)}
148 className={`w-full text-left px-3.5 py-2.5 rounded-xl border transition-all ${
149 selectedReason === reason.value
150 ? "border-primary-500 bg-primary-50 dark:bg-primary-900/20"
151 : "border-surface-200 dark:border-surface-700 hover:border-surface-300 dark:hover:border-surface-600"
152 }`}
153 >
154 <span
155 className={`text-sm font-medium ${
156 selectedReason === reason.value
157 ? "text-primary-700 dark:text-primary-300"
158 : "text-surface-800 dark:text-surface-200"
159 }`}
160 >
161 {reason.label}
162 </span>
163 <span
164 className={`block text-xs mt-0.5 ${
165 selectedReason === reason.value
166 ? "text-primary-600/70 dark:text-primary-400/70"
167 : "text-surface-400 dark:text-surface-500"
168 }`}
169 >
170 {reason.description}
171 </span>
172 </button>
173 ))}
174 </div>
175
176 {selectedReason && (
177 <div className="px-4 pb-2">
178 <textarea
179 value={additionalText}
180 onChange={(e) => setAdditionalText(e.target.value)}
181 placeholder="Additional details (optional)"
182 rows={2}
183 className="w-full px-3.5 py-2.5 text-sm rounded-xl border border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-800 text-surface-800 dark:text-surface-200 placeholder:text-surface-400 focus:outline-none focus:ring-2 focus:ring-primary-500/30 focus:border-primary-500 resize-none"
184 />
185 </div>
186 )}
187
188 <div className="flex items-center justify-end gap-2 p-4 border-t border-surface-200 dark:border-surface-700">
189 <button
190 onClick={handleClose}
191 className="px-4 py-2 text-sm font-medium text-surface-600 dark:text-surface-400 hover:text-surface-800 dark:hover:text-surface-200 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors"
192 >
193 Cancel
194 </button>
195 <button
196 onClick={handleSubmit}
197 disabled={!selectedReason || submitting}
198 className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
199 >
200 {submitting ? "Submitting…" : "Submit Report"}
201 </button>
202 </div>
203 </>
204 )}
205 </div>
206 </div>
207 );
208}