this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 212 lines 6.3 kB view raw
1import { useSignal } from "@preact/signals"; 2 3interface Props { 4 id: number; 5 reviewId: number; 6 targetHandle: string; 7 reviewerDid: string | null; 8 reporterDid: string | null; 9 rating: number | null; 10 body: string | null; 11 reviewStatus: string | null; 12 reasonLabel: string; 13 details: string | null; 14 createdAt: number; 15 copy: { 16 action: string; 17 dismiss: string; 18 hide: string; 19 remove: string; 20 restore: string; 21 actionedLabel: string; 22 dismissedLabel: string; 23 hiddenLabel: string; 24 removedLabel: string; 25 restoredLabel: string; 26 notePlaceholder: string; 27 reasonLabel: string; 28 reporterLabel: string; 29 reviewerLabel: string; 30 detailsLabel: string; 31 reviewLabel: string; 32 submittedAt: string; 33 error: string; 34 }; 35} 36 37type DoneKind = "actioned" | "dismissed" | "hidden" | "removed" | "restored"; 38 39export default function AdminReviewReportRow(p: Props) { 40 const notes = useSignal(""); 41 const status = useSignal< 42 | { kind: "open" } 43 | { kind: "submitting" } 44 | { kind: "done"; action: DoneKind } 45 | { kind: "error"; text: string } 46 >({ kind: "open" }); 47 48 const resolve = async (action: "actioned" | "dismissed") => { 49 status.value = { kind: "submitting" }; 50 try { 51 const r = await fetch(`/api/admin/review-reports/${p.id}/resolve`, { 52 method: "POST", 53 headers: { "content-type": "application/json" }, 54 body: JSON.stringify({ 55 action, 56 notes: notes.value.trim() || undefined, 57 }), 58 }); 59 if (!r.ok) throw new Error(await r.text()); 60 status.value = { kind: "done", action }; 61 } catch (err) { 62 status.value = { 63 kind: "error", 64 text: err instanceof Error ? err.message : String(err), 65 }; 66 } 67 }; 68 69 const moderate = async (action: "hide" | "remove" | "restore") => { 70 status.value = { kind: "submitting" }; 71 try { 72 const r = await fetch(`/api/admin/reviews/${p.reviewId}/${action}`, { 73 method: "POST", 74 headers: { "content-type": "application/json" }, 75 body: JSON.stringify({ notes: notes.value.trim() || undefined }), 76 }); 77 if (!r.ok) throw new Error(await r.text()); 78 if (action !== "restore") { 79 await resolve("actioned"); 80 status.value = { 81 kind: "done", 82 action: action === "hide" ? "hidden" : "removed", 83 }; 84 return; 85 } 86 status.value = { kind: "done", action: "restored" }; 87 } catch (err) { 88 status.value = { 89 kind: "error", 90 text: err instanceof Error ? err.message : String(err), 91 }; 92 } 93 }; 94 95 if (status.value.kind === "done") { 96 const labels: Record<DoneKind, string> = { 97 actioned: p.copy.actionedLabel, 98 dismissed: p.copy.dismissedLabel, 99 hidden: p.copy.hiddenLabel, 100 removed: p.copy.removedLabel, 101 restored: p.copy.restoredLabel, 102 }; 103 return ( 104 <div class="admin-report-row admin-report-row--done"> 105 <div class="admin-report-meta"> 106 <span> 107 <strong>@{p.targetHandle}</strong> 108 </span> 109 <span>{p.reasonLabel}</span> 110 <span> 111 <span class="admin-status-badge admin-status-badge--approved"> 112 {labels[status.value.action]} 113 </span> 114 </span> 115 </div> 116 </div> 117 ); 118 } 119 120 const submitted = new Date(p.createdAt).toISOString().slice(0, 10); 121 const reviewMissing = p.rating == null || p.body == null; 122 return ( 123 <div class="admin-report-row"> 124 <div class="admin-report-meta"> 125 <span> 126 <strong> 127 <a href={`/explore/${p.targetHandle}`}>@{p.targetHandle}</a> 128 </strong> 129 </span> 130 <span> 131 {p.copy.reasonLabel}: <strong>{p.reasonLabel}</strong> 132 </span> 133 <span> 134 {p.copy.reporterLabel}: <strong>{p.reporterDid ?? "Unknown"}</strong> 135 </span> 136 <span> 137 {p.copy.reviewerLabel}: <strong>{p.reviewerDid ?? "Unknown"}</strong> 138 </span> 139 <span> 140 {p.copy.submittedAt}: <strong>{submitted}</strong> 141 </span> 142 </div> 143 {p.details && ( 144 <p class="admin-report-details"> 145 <strong>{p.copy.detailsLabel}:</strong> {p.details} 146 </p> 147 )} 148 <p class="admin-report-details"> 149 <strong>{p.copy.reviewLabel}:</strong> {reviewMissing 150 ? "Review no longer exists." 151 : `${"★".repeat(p.rating!)} ${p.body || "(no text)"}`} 152 </p> 153 <div class="admin-report-actions"> 154 <input 155 type="text" 156 class="admin-report-notes-input" 157 placeholder={p.copy.notePlaceholder} 158 value={notes.value} 159 onInput={(e) => 160 notes.value = (e.currentTarget as HTMLInputElement).value} 161 /> 162 <button 163 type="button" 164 class="profile-form-button-primary" 165 onClick={() => resolve("actioned")} 166 disabled={status.value.kind === "submitting"} 167 > 168 {p.copy.action} 169 </button> 170 <button 171 type="button" 172 class="profile-form-button-secondary" 173 onClick={() => resolve("dismissed")} 174 disabled={status.value.kind === "submitting"} 175 > 176 {p.copy.dismiss} 177 </button> 178 <button 179 type="button" 180 class="admin-report-takedown-button" 181 onClick={() => moderate("hide")} 182 disabled={status.value.kind === "submitting" || reviewMissing} 183 > 184 {p.copy.hide} 185 </button> 186 <button 187 type="button" 188 class="admin-report-takedown-button" 189 onClick={() => moderate("remove")} 190 disabled={status.value.kind === "submitting" || reviewMissing} 191 > 192 {p.copy.remove} 193 </button> 194 {p.reviewStatus && p.reviewStatus !== "visible" && ( 195 <button 196 type="button" 197 class="profile-form-button-secondary" 198 onClick={() => moderate("restore")} 199 disabled={status.value.kind === "submitting" || reviewMissing} 200 > 201 {p.copy.restore} 202 </button> 203 )} 204 </div> 205 {status.value.kind === "error" && ( 206 <p class="admin-icon-row-error"> 207 {p.copy.error}: {status.value.text} 208 </p> 209 )} 210 </div> 211 ); 212}