BlueSky & more on desktop
lazurite.stormlightlabs.org/
tauri
rust
typescript
bluesky
appview
atproto
solid
1import { MODERATION_REASON_OPTIONS } from "$/lib/api/moderation";
2import type { ModerationReasonType } from "$/lib/types";
3import { createEffect, createSignal, For, type ParentProps, Show } from "solid-js";
4import { Motion, Presence } from "solid-motionone";
5
6type ReportDialogProps = {
7 open: boolean;
8 subjectLabel: string;
9 onClose: () => void;
10 onSubmit: (input: { reasonType: ModerationReasonType; reason: string }) => Promise<void> | void;
11};
12
13export function ReportDialog(props: ReportDialogProps) {
14 const [reasonType, setReasonType] = createSignal<ModerationReasonType>(MODERATION_REASON_OPTIONS[0].value);
15 const [reason, setReason] = createSignal("");
16 const [submitting, setSubmitting] = createSignal(false);
17
18 createEffect(() => {
19 if (!props.open) {
20 return;
21 }
22
23 setReasonType(MODERATION_REASON_OPTIONS[0].value);
24 setReason("");
25 setSubmitting(false);
26 });
27
28 async function submit() {
29 if (submitting()) {
30 return;
31 }
32
33 setSubmitting(true);
34 try {
35 await props.onSubmit({ reason: reason().trim(), reasonType: reasonType() });
36 props.onClose();
37 } finally {
38 setSubmitting(false);
39 }
40 }
41
42 return (
43 <Presence>
44 <Show when={props.open}>
45 <DialogBackdrop onClose={props.onClose}>
46 <DialogSurface>
47 <DialogHeader subjectLabel={props.subjectLabel} />
48 <ReasonTypeField value={reasonType()} onChange={setReasonType} />
49 <ReasonDetailsField value={reason()} onChange={setReason} />
50 <DialogActions submitting={submitting()} onCancel={props.onClose} onSubmit={() => void submit()} />
51 </DialogSurface>
52 </DialogBackdrop>
53 </Show>
54 </Presence>
55 );
56}
57
58function DialogBackdrop(props: ParentProps<{ onClose: () => void }>) {
59 return (
60 <Motion.div
61 class="fixed inset-0 z-60 flex items-center justify-center bg-surface-container-highest/70 p-4 backdrop-blur-xl"
62 initial={{ opacity: 0 }}
63 animate={{ opacity: 1 }}
64 exit={{ opacity: 0 }}
65 transition={{ duration: 0.2 }}>
66 <button
67 type="button"
68 aria-label="Close report dialog"
69 class="absolute inset-0 border-0 bg-transparent"
70 onClick={() => props.onClose()} />
71 {props.children}
72 </Motion.div>
73 );
74}
75
76function DialogSurface(props: ParentProps) {
77 return (
78 <Motion.div
79 class="relative z-1 grid w-full max-w-lg gap-4 rounded-2xl bg-surface-container p-5 shadow-2xl"
80 initial={{ scale: 0.96, opacity: 0 }}
81 animate={{ scale: 1, opacity: 1 }}
82 exit={{ scale: 0.96, opacity: 0 }}
83 transition={{ duration: 0.2 }}>
84 {props.children}
85 </Motion.div>
86 );
87}
88
89function DialogHeader(props: { subjectLabel: string }) {
90 return (
91 <div class="grid gap-1">
92 <h3 class="m-0 text-lg font-semibold text-on-surface">Report content</h3>
93 <p class="m-0 text-sm text-on-surface-variant">{props.subjectLabel}</p>
94 </div>
95 );
96}
97
98function ReasonTypeField(props: { value: ModerationReasonType; onChange: (value: ModerationReasonType) => void }) {
99 return (
100 <label class="grid gap-1">
101 <span class="text-sm font-medium text-on-surface">Reason type</span>
102 <select
103 value={props.value}
104 class="rounded-xl border border-white/10 bg-black/40 px-3 py-2 text-sm text-on-surface outline-none transition focus:border-primary/50"
105 onInput={(event) => props.onChange(event.currentTarget.value as ModerationReasonType)}>
106 <For each={MODERATION_REASON_OPTIONS}>{(option) => <option value={option.value}>{option.label}</option>}</For>
107 </select>
108 </label>
109 );
110}
111
112function ReasonDetailsField(props: { value: string; onChange: (value: string) => void }) {
113 return (
114 <label class="grid gap-1">
115 <span class="text-sm font-medium text-on-surface">Details (optional)</span>
116 <textarea
117 rows={4}
118 value={props.value}
119 placeholder="Add context for moderators"
120 class="rounded-xl border border-white/10 bg-black/40 px-3 py-2 text-sm text-on-surface outline-none transition focus:border-primary/50"
121 onInput={(event) => props.onChange(event.currentTarget.value)} />
122 </label>
123 );
124}
125
126function DialogActions(props: { submitting: boolean; onCancel: () => void; onSubmit: () => void }) {
127 return (
128 <div class="flex justify-end gap-2">
129 <button
130 type="button"
131 class="rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-on-surface transition hover:bg-white/5"
132 onClick={() => props.onCancel()}>
133 Cancel
134 </button>
135 <button
136 type="button"
137 disabled={props.submitting}
138 class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-on-primary-fixed transition hover:bg-primary-dim disabled:cursor-wait disabled:opacity-70"
139 onClick={() => props.onSubmit()}>
140 {props.submitting ? "Submitting..." : "Submit report"}
141 </button>
142 </div>
143 );
144}