objective categorical abstract machine language personal data server
65
fork

Configure Feed

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

at main 340 lines 15 kB view raw
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>