Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at main 208 lines 7.5 kB view raw
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}