forked from
joebasser.com/atmosphere-account
this repo has no description
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}