objective categorical abstract machine language personal data server
1[@@@ocaml.warning "-26-27"]
2
3open Melange_json.Primitives
4open React
5
6type invite = {code: string; did: string; remaining: int} [@@deriving json]
7
8type props =
9 { invites: invite list
10 ; csrf_token: string
11 ; error: string option [@default None]
12 ; success: string option [@default None] }
13[@@deriving json]
14
15let[@react.component] make
16 ~props:({invites; csrf_token; error; success} : props) () =
17 (* create invite modal state *)
18 let createModalOpen, setCreateModalOpen = useState (fun () -> false) in
19 let newCode, setNewCode = useState (fun () -> "") in
20 let newDid, setNewDid = useState (fun () -> "") in
21 let newRemaining, setNewRemaining = useState (fun () -> "1") in
22 (* edit modal state *)
23 let editModalFor, setEditModalFor =
24 useState (fun () -> (None : invite option))
25 in
26 let editDid, setEditDid = useState (fun () -> "admin") in
27 let editRemaining, setEditRemaining = useState (fun () -> "") in
28 (* delete confirmation state *)
29 let deleteConfirmFor, setDeleteConfirmFor =
30 useState (fun () -> (None : invite option))
31 in
32 <div
33 className="w-full h-full max-w-4xl px-4 pt-16 mx-auto flex flex-col \
34 md:flex-row gap-12">
35 <AdminSidebar active_page="/admin/invites" />
36 <main className="flex-1 w-full">
37 <h1 className="text-2xl font-serif text-mana-200 mb-1">
38 (string "invite codes")
39 </h1>
40 <p className="text-mist-100 mb-4">
41 (string "Manage invite codes for new account registration.")
42 </p>
43 <div className="flex flex-col sm:flex-row gap-4 mb-6">
44 <ClientOnly
45 fallback=(<Button kind=`Primary className="w-full sm:max-w-64">
46 (string "create invite code")
47 </Button>)>
48 [%browser_only
49 fun () ->
50 let module Aria = ReactAria in
51 <Aria.DialogTrigger
52 isOpen=createModalOpen
53 onOpenChange=(fun o -> setCreateModalOpen (fun _ -> o))>
54 <Aria.Pressable>
55 <Button
56 kind=`Primary
57 className="w-full sm:max-w-64"
58 onClick=(fun _ -> setCreateModalOpen (fun _ -> true))>
59 (string "create invite code")
60 </Button>
61 </Aria.Pressable>
62 <Aria.ModalOverlay
63 className="fixed inset-0 z-50 bg-mist-80/80 flex \
64 items-center justify-center"
65 isDismissable=true>
66 <Aria.Modal
67 className="bg-feather-100 border border-mist-60 rounded-xl \
68 px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl">
69 <Aria.Dialog className="outline-none">
70 <Aria.Heading
71 slot="title"
72 className="text-lg font-serif text-mana-200 mb-2">
73 (string "create invite code")
74 </Aria.Heading>
75 <p className="text-mist-100 mb-4">
76 (string
77 "Create a new invite code for user registration." )
78 </p>
79 <form className="flex flex-col gap-y-3">
80 <input type_="hidden" name="dream.csrf" value=csrf_token
81 />
82 <input
83 type_="hidden" name="action" value="create_invite"
84 />
85 <Input
86 name="new_code"
87 label="Code (optional)"
88 placeholder="Leave empty to generate"
89 showIndicator=false
90 value=newCode
91 onChange=(fun e ->
92 setNewCode (fun _ ->
93 (Event.Form.target e)##value ) )
94 />
95 <Input
96 name="did"
97 label="For (DID)"
98 placeholder="admin"
99 showIndicator=false
100 value=newDid
101 onChange=(fun e ->
102 setNewDid (fun _ ->
103 (Event.Form.target e)##value ) )
104 />
105 <Input
106 name="remaining"
107 type_="number"
108 label="Available uses"
109 showIndicator=false
110 value=newRemaining
111 onChange=(fun e ->
112 setNewRemaining (fun _ ->
113 (Event.Form.target e)##value ) )
114 />
115 <Button
116 formMethod="post" type_="submit" className="mt-2">
117 (string "create")
118 </Button>
119 </form>
120 </Aria.Dialog>
121 </Aria.Modal>
122 </Aria.ModalOverlay>
123 </Aria.DialogTrigger>]
124 </ClientOnly>
125 </div>
126 ( match error with
127 | Some err ->
128 <div className="mb-4">
129 <span className="inline-flex items-center text-phoenix-100 text-sm">
130 <CircleAlertIcon className="w-4 h-4 mr-2" /> (string err)
131 </span>
132 </div>
133 | None ->
134 null )
135 ( match success with
136 | Some msg ->
137 <div className="mb-4">
138 <span className="inline-flex items-center text-mana-100 text-sm">
139 <CheckmarkIcon className="w-4 h-4 mr-2" /> (string msg)
140 </span>
141 </div>
142 | None ->
143 null )
144 <div
145 className=( if List.length invites > 0 then "overflow-x-auto"
146 else "overflow-x-hidden" )>
147 <table
148 className="w-full grid border-collapse text-sm"
149 style=(ReactDOM.Style.make
150 ~gridTemplateColumns:
151 "minmax(6rem, 1fr) 1fr minmax(2rem, 1fr) 4rem"
152 () )>
153 <thead className="contents">
154 <tr className="contents text-left text-mist-80">
155 <th className="border-b border-mist-60/50 pb-2 font-normal">
156 (string "Code")
157 </th>
158 <th className="border-b border-mist-60/50 pb-2 font-normal">
159 (string "For")
160 </th>
161 <th className="border-b border-mist-60/50 pb-2 font-normal">
162 (string "Remaining")
163 </th>
164 <th className="border-b border-mist-60/50 pb-2 font-normal w-20"
165 />
166 </tr>
167 </thead>
168 <tbody className="contents">
169 ( List.map
170 (fun (invite : invite) ->
171 <tr key=invite.code className="contents">
172 <td className="py-3 pr-4">
173 <span className="font-mono text-mana-100">
174 (string invite.code)
175 </span>
176 </td>
177 <td className="py-3 pr-4 text-mist-100">
178 (string invite.did)
179 </td>
180 <td className="py-3 pr-4 text-mist-100">
181 (string (string_of_int invite.remaining))
182 </td>
183 <td className="py-3">
184 <div className="flex gap-2">
185 <button
186 className="p-1 text-mist-80 hover:text-mana-100 \
187 cursor-pointer"
188 onClick=(fun _ ->
189 setEditDid (fun _ -> invite.did) ;
190 setEditRemaining (fun _ ->
191 string_of_int invite.remaining ) ;
192 setEditModalFor (fun _ -> Some invite) )>
193 <PencilLineIcon className="w-4 h-4" />
194 </button>
195 <button
196 className="p-1 text-mist-80 hover:text-phoenix-100 \
197 cursor-pointer"
198 onClick=(fun _ ->
199 setDeleteConfirmFor (fun _ -> Some invite) )>
200 <TrashIcon className="w-4 h-4" />
201 </button>
202 </div>
203 </td>
204 </tr> )
205 invites
206 |> Array.of_list |> array )
207 </tbody>
208 </table>
209 </div>
210 <ClientOnly fallback=null>
211 (* edit modal *)
212 [%browser_only
213 fun () ->
214 let module Aria = ReactAria in
215 <Aria.DialogTrigger
216 isOpen=(editModalFor <> None)
217 onOpenChange=(fun o ->
218 if not o then setEditModalFor (fun _ -> None) )>
219 <Aria.ModalOverlay
220 className="fixed inset-0 z-50 bg-mist-80/80 flex items-center \
221 justify-center"
222 isDismissable=true>
223 <Aria.Modal
224 className="bg-feather-100 border border-mist-60 rounded-xl \
225 px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl">
226 <Aria.Dialog className="outline-none">
227 ( match editModalFor with
228 | Some invite ->
229 <form className="flex flex-col gap-y-3">
230 <Aria.Heading
231 slot="title"
232 className="text-lg font-serif text-mana-200 mb-2">
233 (string "edit invite code")
234 </Aria.Heading>
235 <p className="text-mist-80 text-sm">
236 (string ("Code: " ^ invite.code))
237 </p>
238 <input
239 type_="hidden" name="dream.csrf" value=csrf_token
240 />
241 <input
242 type_="hidden" name="action" value="update_invite"
243 />
244 <input type_="hidden" name="code" value=invite.code />
245 <Input
246 name="did"
247 label="For (DID)"
248 showIndicator=false
249 value=editDid
250 onChange=(fun e ->
251 setEditDid (fun _ ->
252 (Event.Form.target e)##value ) )
253 />
254 <Input
255 name="remaining"
256 type_="number"
257 label="Remaining uses"
258 showIndicator=false
259 value=editRemaining
260 onChange=(fun e ->
261 setEditRemaining (fun _ ->
262 (Event.Form.target e)##value ) )
263 />
264 <div className="flex gap-3 mt-2">
265 <Button formMethod="post" type_="submit">
266 (string "save")
267 </Button>
268 <Button
269 kind=`Tertiary
270 onClick=(fun _ -> setEditModalFor (fun _ -> None))>
271 (string "cancel")
272 </Button>
273 </div>
274 </form>
275 | None ->
276 null )
277 </Aria.Dialog>
278 </Aria.Modal>
279 </Aria.ModalOverlay>
280 </Aria.DialogTrigger>]
281 </ClientOnly>
282 <ClientOnly fallback=null>
283 (* delete confirmation modal *)
284 [%browser_only
285 fun () ->
286 let module Aria = ReactAria in
287 <Aria.DialogTrigger
288 isOpen=(deleteConfirmFor <> None)
289 onOpenChange=(fun o ->
290 if not o then setDeleteConfirmFor (fun _ -> None) )>
291 <Aria.ModalOverlay
292 className="fixed inset-0 z-50 bg-mist-80/80 flex items-center \
293 justify-center"
294 isDismissable=true>
295 <Aria.Modal
296 className="bg-feather-100 border border-mist-60 rounded-xl \
297 px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl">
298 <Aria.Dialog className="outline-none">
299 ( match deleteConfirmFor with
300 | Some invite ->
301 <form className="flex flex-col gap-y-3">
302 <Aria.Heading
303 slot="title"
304 className="text-lg font-serif text-mana-200 mb-2">
305 (string "delete invite code")
306 </Aria.Heading>
307 <input
308 type_="hidden" name="dream.csrf" value=csrf_token
309 />
310 <input
311 type_="hidden" name="action" value="delete_invite"
312 />
313 <input type_="hidden" name="code" value=invite.code />
314 <p className="text-mist-100">
315 (string
316 ( "Are you sure you want to delete invite code "
317 ^ invite.code ^ "?" ) )
318 </p>
319 <div className="flex gap-3 mt-2">
320 <Button
321 kind=`Danger formMethod="post" type_="submit">
322 (string "delete")
323 </Button>
324 <Button
325 kind=`Tertiary
326 onClick=(fun _ ->
327 setDeleteConfirmFor (fun _ -> None) )>
328 (string "cancel")
329 </Button>
330 </div>
331 </form>
332 | None ->
333 null )
334 </Aria.Dialog>
335 </Aria.Modal>
336 </Aria.ModalOverlay>
337 </Aria.DialogTrigger>]
338 </ClientOnly>
339 </main>
340 </div>