Skip to content
Permalink
Newer
Older
100644 455 lines (399 sloc) 11.8 KB
October 19, 2023 16:49
1
import {
2
Box,
3
Button,
October 19, 2023 16:49
4
Container,
5
Flex,
October 20, 2023 13:11
6
Heading,
October 19, 2023 16:49
7
Popover,
8
PopoverArrow,
9
PopoverBody,
10
PopoverCloseButton,
11
PopoverContent,
12
PopoverHeader,
13
PopoverTrigger,
October 19, 2023 16:49
14
Select,
October 20, 2023 13:11
15
Spacer,
October 19, 2023 16:49
16
useBreakpointValue,
17
useDisclosure,
18
useToast,
October 19, 2023 16:50
19
VStack,
October 19, 2023 16:49
20
} from "@chakra-ui/react";
October 29, 2023 03:38
21
import {
22
type MutableRefObject,
23
type ReactNode,
24
useEffect,
25
useRef,
26
useState,
27
} from "react";
28
import { useLoaderData } from "@remix-run/react";
29
import AppealBans from "../../components/AppealBans.js";
October 19, 2023 16:49
30
import AppealCard from "../../components/AppealCard.js";
October 19, 2023 16:49
31
import GameAppealCard from "../../components/GameAppealCard.js";
32
import NewGameBan from "../../components/NewGameBan.js";
33
import NewInfractionModal from "../../components/NewInfractionModal.js";
October 19, 2023 16:49
34
import ReportCard from "../../components/ReportCard.js";
35
import NewInactivityNotice from "../../components/NewInactivityNotice.js";
36
import InactivityNoticeCard from "../../components/InactivityNoticeCard.js";
October 19, 2023 16:49
37
October 19, 2023 16:49
38
export async function loader({ context }: { context: RequestContext }) {
39
const { current_user: currentUser } = context.data;
October 19, 2023 16:49
41
if (!currentUser)
42
throw new Response(null, {
October 19, 2023 16:50
43
status: 401,
October 19, 2023 16:49
45
46
const departments = {
47
DM: 1 << 2,
48
ET: 1 << 3,
49
FM: 1 << 10,
October 19, 2023 16:50
50
WM: 1 << 9,
51
};
52
October 19, 2023 16:49
53
const newItemPermissions = {
54
appeal_bans: [1 << 0, 1 << 11],
October 19, 2023 16:49
55
game_ban: [1 << 5],
56
inactivity: [1 << 2, 1 << 9, 1 << 10],
October 19, 2023 16:50
57
infraction: [1 << 0, 1 << 2, 1 << 6, 1 << 7],
58
user_lookup: [1 << 5, 1 << 8],
October 19, 2023 16:49
59
};
60
61
const newItemNames: { [k: string]: string } = {
62
appeal_bans: "Appeal Bans",
October 19, 2023 16:50
63
game_ban: "New Game Ban",
64
inactivity: "New Inactivity Notice",
65
infraction: "New Infraction",
66
user_lookup: "User Lookup",
October 19, 2023 16:49
67
};
68
69
const typePermissions = {
70
appeal: [1 << 0, 1 << 1],
71
gma: [1 << 5],
72
inactivity: [1 << 4, 1 << 6, 1 << 7, 1 << 11, 1 << 12],
October 19, 2023 16:50
73
report: [1 << 5],
October 19, 2023 16:49
74
};
75
76
const typeNames: { [k: string]: string } = {
77
appeal: "Discord Appeals",
78
gma: "Game Appeals",
79
inactivity: "Inactivity Notices",
October 19, 2023 16:50
80
report: "Game Reports",
October 19, 2023 16:49
81
};
82
83
const allowedNewItems = [];
84
const allowedTypes = [];
85
86
for (const [item, ints] of Object.entries(newItemPermissions)) {
87
if (ints.find((i) => currentUser.permissions & i))
88
allowedNewItems.push({ name: newItemNames[item], value: item });
89
}
90
91
for (const [type, ints] of Object.entries(typePermissions)) {
92
if (ints.find((i) => currentUser.permissions & i))
93
allowedTypes.push({ name: typeNames[type], value: type });
94
}
95
96
if (!allowedTypes.length && !allowedNewItems.length)
97
throw new Response(null, {
October 19, 2023 16:50
98
status: 403,
October 19, 2023 16:49
99
});
100
101
return {
102
can_edit_ban_users: [
103
"165594923586945025",
104
"289372404541554689",
October 19, 2023 16:50
105
"396347223736057866",
106
].includes(currentUser.id),
107
departments: Object.entries(departments)
108
.filter((d) => d[1] & currentUser.permissions)
109
.map((arr) => arr[0]),
October 19, 2023 16:49
110
entry_types: allowedTypes,
October 19, 2023 16:50
111
item_types: allowedNewItems,
October 19, 2023 16:49
112
};
113
}
114
October 19, 2023 16:49
115
export function meta() {
October 19, 2023 16:49
116
return [
117
{
October 19, 2023 16:50
118
title: "Moderation Queue - Car Crushers",
119
},
October 19, 2023 16:49
120
];
October 19, 2023 16:49
121
}
122
October 19, 2023 16:50
123
export default function () {
October 19, 2023 16:49
124
const pageProps = useLoaderData<typeof loader>();
October 19, 2023 16:49
125
const isDesktop = useBreakpointValue({ base: false, lg: true });
126
const entryTypes = [];
127
const [entries, setEntries] = useState(
128
[] as { element: ReactNode; id: string }[],
129
);
130
const [before, setBefore] = useState(Date.now());
October 30, 2023 18:47
131
const [queue, setQueue] = useState("");
October 29, 2023 03:38
132
const messageChannel: MutableRefObject<MessageChannel | null> = useRef(null);
133
const toast = useToast();
October 19, 2023 16:49
134
135
for (const type of pageProps.entry_types)
October 19, 2023 16:49
136
entryTypes.push(
137
<option key={type.value} value={type.value}>
138
{type.name}
October 19, 2023 16:50
139
</option>,
October 19, 2023 16:49
140
);
October 19, 2023 16:49
141
142
useEffect(() => {
October 29, 2023 03:38
143
if (messageChannel.current) {
144
messageChannel.current.port1.onmessage = function (ev) {
145
const { data }: { data: string } = ev;
146
147
setEntries([...entries].filter((entry) => entry.id !== data));
148
};
149
}
October 29, 2023 03:38
150
}, [entries, messageChannel.current]);
October 19, 2023 16:49
152
async function updateQueue(
153
queue_type: string,
154
before: number,
155
show_closed = false,
October 19, 2023 16:50
156
jump_item_to_top = false,
October 19, 2023 16:51
157
clear_all_others = false,
October 19, 2023 16:49
158
): Promise<void> {
October 30, 2023 18:47
159
const searchParams = new URLSearchParams(location.search);
160
const itemId = searchParams.get("id");
161
const queueType = searchParams.get("type") ?? queue_type;
162
163
if (!pageProps.entry_types.find((type) => type.value === queueType)) {
164
toast({
165
description: "You cannot access that queue",
166
isClosable: true,
167
status: "error",
168
title: "Forbidden",
169
});
170
171
return;
172
}
173
174
if (!searchParams.get("type") && itemId) {
175
toast({
176
description: "Cannot load item by id without type",
177
isClosable: true,
178
status: "error",
179
title: "Bad link",
180
});
181
182
return;
183
}
184
185
if (queueType !== queue_type) setQueue(queueType);
186
October 19, 2023 16:49
187
const queueReq = await fetch(
October 30, 2023 18:47
188
`/api/mod-queue/list?before=${before}&showClosed=${show_closed}&type=${queueType}`,
October 19, 2023 16:49
189
);
190
191
if (!queueReq.ok) {
192
const errorData: { error: string } = await queueReq.json();
193
195
description: errorData.error,
196
duration: 10000,
197
isClosable: true,
198
status: "error",
October 19, 2023 16:50
199
title: "Failed to load queue",
200
});
201
202
return;
203
}
October 19, 2023 16:49
204
October 19, 2023 16:50
205
let entryData: { [k: string]: any }[] = await queueReq.json();
October 19, 2023 16:51
206
const newEntries = clear_all_others ? [] : [...entries];
October 19, 2023 16:49
207
October 30, 2023 18:47
208
if (itemId && jump_item_to_top) {
209
history.replaceState(null, "", location.origin + location.pathname);
210
October 19, 2023 16:50
211
const specifiedItem = entryData.find((e) => e.id === itemId);
October 19, 2023 16:50
213
if (specifiedItem) {
214
entryData = entryData.filter((entry) => entry.id !== specifiedItem.id);
215
entryData.unshift(specifiedItem);
216
} else {
October 30, 2023 18:47
217
const itemReq = await fetch(`/api/mod-queue/${queueType}/${itemId}`);
October 19, 2023 16:50
218
219
if (!itemReq.ok) {
220
toast({
221
description: "Failed to load item with id " + itemId,
October 19, 2023 16:50
222
duration: 10000,
223
isClosable: true,
224
status: "error",
October 19, 2023 16:50
225
title: ((await itemReq.json()) as { error: string }).error,
October 19, 2023 16:50
226
});
227
} else {
228
const itemData: { [k: string]: any } = await itemReq.json();
229
230
entryData.unshift(itemData);
231
}
235
if (!entryData.length) {
236
setEntries([]);
237
return;
238
}
October 19, 2023 16:49
240
for (const entry of entryData) {
October 30, 2023 19:23
241
let cardType = queueType;
242
243
if (
244
entryData.indexOf(entry) > 0 &&
245
entryData.filter((d) => d.id === entry.id).length > 1
246
)
247
continue;
248
249
switch (cardType) {
October 19, 2023 16:49
250
case "appeal":
251
newEntries.push({
252
element: (
253
<AppealCard
254
{...(entry as AppealCardProps & { port?: MessagePort })}
October 29, 2023 03:38
255
port={messageChannel.current?.port2}
256
/>
257
),
258
id: `appeal_${entry.id}`,
259
});
October 19, 2023 16:49
260
261
break;
262
October 19, 2023 16:49
263
case "gma":
264
newEntries.push({
265
element: (
266
<GameAppealCard
267
{...(entry as GameAppealProps & { port?: MessagePort })}
October 29, 2023 03:38
268
port={messageChannel.current?.port2}
269
/>
270
),
271
id: `gma_${entry.id}`,
272
});
October 19, 2023 16:49
273
274
break;
275
276
case "inactivity":
277
newEntries.push({
278
element: (
279
<InactivityNoticeCard
280
{...(entry as InactivityNoticeProps & { port?: MessagePort })}
October 29, 2023 03:38
281
port={messageChannel.current?.port2}
282
/>
283
),
284
id: `inactivity_${entry.id}`,
285
});
286
287
break;
288
October 19, 2023 16:49
289
case "report":
290
newEntries.push({
291
element: (
292
<ReportCard
293
{...(entry as ReportCardProps & { port?: MessagePort })}
October 29, 2023 03:38
294
port={messageChannel.current?.port2}
295
/>
296
),
297
id: `report_${entry.id}`,
298
});
October 19, 2023 16:49
299
300
break;
October 19, 2023 16:49
301
}
302
}
303
304
setEntries(newEntries);
October 19, 2023 16:49
305
setBefore(entryData[entryData.length - 1].created_at);
306
}
October 19, 2023 16:49
307
308
const itemModals: {
309
[k: string]: {
310
isOpen: boolean;
311
onOpen: () => void;
312
onClose: () => void;
313
[k: string]: any;
314
};
315
} = {
316
appeal_bans: useDisclosure(),
317
game_ban: useDisclosure(),
318
inactivity: useDisclosure(),
October 19, 2023 16:50
319
infraction: useDisclosure(),
320
user_lookup: {
321
isOpen: false,
322
onClose: () => {},
323
onOpen: () => location.assign("/hammer"),
324
},
325
};
326
327
useEffect(() => {
October 29, 2023 03:38
328
messageChannel.current = new MessageChannel();
329
October 19, 2023 16:50
330
(async function () {
331
await updateQueue(pageProps.entry_types[0].value, before, false, true);
334
const searchParams = new URLSearchParams(location.search);
335
const modal = searchParams.get("modal");
336
337
if (!modal || !pageProps.item_types.find((m) => m.value === modal)) return;
338
339
itemModals[modal].onOpen();
340
}, []);
342
const ItemDisplay = (
343
<Select
344
onChange={async (v) => {
345
setBefore(Date.now());
346
347
const { target } = v;
348
October 30, 2023 18:47
349
setQueue(target.options[target.selectedIndex].value);
350
351
await updateQueue(
352
target.options[target.selectedIndex].value,
353
Date.now(),
October 19, 2023 16:51
354
false,
355
false,
October 19, 2023 16:51
356
true,
357
);
358
}}
October 30, 2023 18:47
359
value={queue}
361
{entryTypes}
362
</Select>
363
);
364
October 19, 2023 16:49
365
return (
366
<Container maxW="container.lg">
367
<AppealBans
368
isOpen={itemModals.appeal_bans.isOpen}
369
onClose={itemModals.appeal_bans.onClose}
370
/>
371
<NewGameBan
372
isOpen={itemModals.game_ban.isOpen}
373
onClose={itemModals.game_ban.onClose}
374
/>
375
<NewInactivityNotice
376
departments={pageProps.departments}
377
isOpen={itemModals.inactivity.isOpen}
378
onClose={itemModals.inactivity.onClose}
379
/>
380
<NewInfractionModal
381
isOpen={itemModals.infraction.isOpen}
382
onClose={itemModals.infraction.onClose}
383
/>
October 19, 2023 16:49
384
<Flex>
385
<VStack w={isDesktop ? "container.md" : "container.lg"}>
386
<Box display={isDesktop ? "none" : undefined} mb="16px" w="90%">
387
{ItemDisplay}
388
</Box>
389
{entries.length ? (
390
entries.map((entry) => entry.element)
391
) : (
392
<Container
393
left="50%"
394
maxW="container.md"
395
pos="absolute"
396
mt="64px"
397
transform="translate(-50%)"
398
>
399
<Flex>
400
<Spacer />
401
<img alt="Thonkery" src="/files/Thonkery.png" />
402
<Spacer />
403
</Flex>
404
<br />
405
<Heading textAlign="center">Nothing here</Heading>
406
</Container>
407
)}
October 19, 2023 16:49
408
</VStack>
409
<Box display={isDesktop ? undefined : "none"} ml="16px" w="248px">
410
{ItemDisplay}
October 19, 2023 16:49
411
</Box>
412
</Flex>
October 19, 2023 16:50
413
<Popover placement="top-end">
October 19, 2023 16:49
414
<PopoverTrigger>
415
<Button
416
borderRadius="50%"
417
bottom="10vh"
418
h="16"
419
position="absolute"
420
right="10vh"
421
w="16"
422
>
October 19, 2023 16:49
423
<svg
424
xmlns="http://www.w3.org/2000/svg"
425
width="32"
426
height="32"
October 19, 2023 16:49
427
fill="currentColor"
428
viewBox="0 0 16 16"
429
>
October 19, 2023 16:50
430
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
October 19, 2023 16:49
431
</svg>
432
</Button>
433
</PopoverTrigger>
434
<PopoverContent>
435
<PopoverArrow />
436
<PopoverCloseButton />
October 19, 2023 16:50
437
<PopoverHeader>Tools</PopoverHeader>
October 19, 2023 16:49
438
<PopoverBody>
439
<VStack>
440
{pageProps.item_types.map((item) => (
441
<Button
442
key={item.value}
443
onClick={() => itemModals[item.value].onOpen()}
444
w="100%"
445
>
446
{item.name}
447
</Button>
448
))}
October 19, 2023 16:49
449
</VStack>
450
</PopoverBody>
451
</PopoverContent>
452
</Popover>
October 19, 2023 16:49
453
</Container>
454
);
455
}