objective categorical abstract machine language personal data server

Update mlx to allow formatting mlx files with [%browser_only]

futur.blue 6a511b27 5d757018

verified
+1 -1
README.md
··· 154 154 155 155 to download the formatter and LSP services. You can run `dune fmt` to format the project. 156 156 157 - The [frontend](frontend/) is written in [MLX](https://github.com/ocaml-mlx/mlx), a JSX-ish OCaml dialect. To format it, you'll need to `opam install ocamlformat-mlx`, then `ocamlformat-mlx -i frontend/**/*.mlx`. You'll see a few errors on formatting files containing `[%browser_only]`; I'm waiting on the next release of `mlx` to fix those. 157 + The [frontend](frontend/) and [email templates](pegasus/lib/emails/) are written in [MLX](https://github.com/ocaml-mlx/mlx), a JSX-ish OCaml dialect. To format them, you'll need to `opam install ocamlformat-mlx`, then `ocamlformat-mlx -i ./{frontend,pegasus}/**/*.mlx`.
+1 -1
dune-project
··· 92 92 melange 93 93 melange-json 94 94 melange-json-native 95 - mlx 95 + (mlx (>= 0.11)) 96 96 (reason-react (>= 0.16.0)) 97 97 (reason-react-ppx (>= 0.16.0)) 98 98 server-reason-react))
-2
frontend/README.md
··· 47 47 # Format MLX files 48 48 ocamlformat-mlx -i frontend/src/**/*.mlx 49 49 ``` 50 - 51 - Note: You may see errors formatting files containing `[%browser_only]`. This is a known issue pending the next MLX release.
+3 -5
frontend/src/components/AccountSidebar.mlx
··· 11 11 <Sidebar 12 12 pages 13 13 active_page 14 - header=( 15 - <AccountSwitcher 16 - current_user logged_in_users add_account_url="/account/login" 17 - /> 18 - ) 14 + header=(<AccountSwitcher 15 + current_user logged_in_users add_account_url="/account/login" 16 + />) 19 17 />
+102 -87
frontend/src/components/AccountSwitcher.mlx
··· 7 7 let button_class_inline = 8 8 "group inline-flex flex-row items-center px-1.5 py-1 -mx-0.75 -my-1 \ 9 9 rounded-lg focus-visible:outline-none hover:bg-mist-20/40 \ 10 - active:bg-mist-20/40" 10 + active:bg-mist-20/40" 11 11 12 12 let button_class_default = 13 - "group w-64 flex flex-row items-center gap-x-1 px-2 py-1.5 -mx-2 \ 14 - rounded-lg focus-visible:outline-none hover:bg-mist-20/40 \ 15 - active:bg-mist-20/40" 13 + "group w-64 flex flex-row items-center gap-x-1 px-2 py-1.5 -mx-2 rounded-lg \ 14 + focus-visible:outline-none hover:bg-mist-20/40 active:bg-mist-20/40" 16 15 17 - let value_class_inline = "text-mana-100 font-serif inline-flex items-center gap-x-1" 16 + let value_class_inline = 17 + "text-mana-100 font-serif inline-flex items-center gap-x-1" 18 18 19 19 let value_class_default = "text-mana-100 font-serif flex items-center gap-x-1" 20 20 ··· 23 23 [@@deriving json] 24 24 25 25 let fallback handle avatar inline = 26 - let button_class = if inline then button_class_inline else button_class_default in 27 - let value_class = if inline then value_class_inline else value_class_default in 26 + let button_class = 27 + if inline then button_class_inline else button_class_default 28 + in 29 + let value_class = 30 + if inline then value_class_inline else value_class_default 31 + in 28 32 <button className=button_class> 29 33 <span className=value_class> 30 - (match avatar with 31 - | Some uri -> <img className="w-5 h-5 mr-1 rounded-md" src=uri /> 32 - | None -> null) 33 - <span className="truncate self-baseline select-none">(string handle)</span> 34 - <ChevronDownIcon 35 - className="w-3 h-3 mt-0.5 text-mana-100" 36 - strokeWidth="3" 34 + ( match avatar with 35 + | Some uri -> 36 + <img className="w-5 h-5 mr-1 rounded-md" src=uri /> 37 + | None -> 38 + null ) 39 + <span className="truncate self-baseline select-none"> 40 + (string handle) 41 + </span> 42 + <ChevronDownIcon className="w-3 h-3 mt-0.5 text-mana-100" strokeWidth="3" 37 43 /> 38 44 </span> 39 45 </button> ··· 43 49 let handleAccountSwitch = 44 50 [%browser_only 45 51 fun newDid -> 46 - if newDid <> current_user.did then 52 + if newDid <> current_user.did then ( 47 53 let body = 48 54 Fetch.BodyInit.make 49 - (Webapi.Url.URLSearchParams.makeWithArray [|("did", newDid)|] 50 - |> Webapi.Url.URLSearchParams.toString ) 51 - in 55 + ( Webapi.Url.URLSearchParams.makeWithArray [|("did", newDid)|] 56 + |> Webapi.Url.URLSearchParams.toString ) 57 + in 52 58 let _ = 53 59 Fetch.fetchWithInit "/account/switch" 54 60 (Fetch.RequestInit.make ~method_:Fetch.Post 55 - ~headers:(Fetch.HeadersInit.makeWithArray [|("Content-Type", "application/x-www-form-urlencoded")|]) 56 - ~body 57 - () ) 61 + ~headers: 62 + (Fetch.HeadersInit.makeWithArray 63 + [|("Content-Type", "application/x-www-form-urlencoded")|] ) 64 + ~body () ) 58 65 |> Js.Promise.then_ (fun response -> 59 - if Fetch.Response.ok response then ( 60 - ignore ([%mel.raw {| window.location.reload() |}] : unit) ; 61 - Js.Promise.resolve () ) 62 - else Js.Promise.resolve () ) 66 + if Fetch.Response.ok response then ( 67 + ignore ([%mel.raw {| window.location.reload() |}] : unit) ; 68 + Js.Promise.resolve () ) 69 + else Js.Promise.resolve () ) 63 70 |> Js.Promise.catch (fun _ -> Js.Promise.resolve ()) 64 71 in 65 72 () ; 66 - ( match onChange with 67 - | Some f -> 68 - f newDid 69 - | None -> 70 - () ) 73 + match onChange with Some f -> f newDid | None -> () ) 71 74 else ()] 72 75 in 73 - let button_class = if inline then button_class_inline else button_class_default in 74 - let value_class = if inline then value_class_inline else value_class_default in 75 - <ClientOnly fallback=(fallback current_user.handle current_user.avatar_data_uri inline)> 76 - [%browser_only 77 - (fun () -> 78 - <Aria.Select name className="inline" defaultValue=current_user.did placeholder="select account" onChange=handleAccountSwitch> 79 - <Aria.Button className=button_class> 80 - <Aria.SelectValue className=value_class /> 81 - </Aria.Button> 82 - <Aria.Popover 83 - style= 84 - (ReactDOM.Style.make 85 - ~minWidth:"max(var(--spacing) * 32, calc(var(--trigger-width) + var(--spacing) * 3))" () ) 86 - className="focus-visible:outline-none"> 87 - <Aria.ListBox 88 - className="w-full flex flex-col gap-y-1 p-1.5 -ml-1.5 rounded-lg \ 89 - bg-mist-20 font-light"> 90 - ( List.map 91 - (fun (user : actor) -> 92 - <Aria.ListBoxItem 93 - className="flex flex-row items-center py-1.5 px-2 gap-x-1 \ 94 - font-serif text-mist-100 rounded-md \ 95 - focus-visible:outline-none \ 96 - data-hovered:text-mist-20 \ 97 - data-focused:text-mist-20 \ 98 - data-hovered:bg-mana-100 \ 99 - data-focused:bg-mana-100" 100 - key=user.did 101 - id=user.did> 102 - ( match user.avatar_data_uri with 103 - | Some src -> 104 - <img src className="w-5 h-5 mr-1 rounded-md" /> 105 - | None -> 106 - null ) 107 - <span className="truncate self-baseline select-none"> 108 - (string user.handle) 109 - </span> 110 - <ChevronDownIcon 111 - className="w-3 h-3 mt-0.5 text-mana-100 hidden \ 112 - group-aria-[haspopup]:inline" 113 - strokeWidth="3" 114 - /> 115 - </Aria.ListBoxItem> ) 116 - logged_in_users 117 - |> Array.of_list |> array ) 118 - <Aria.ListBoxItem 119 - className="flex flex-row items-center p-1 pl-2 text-mana-100 \ 120 - font-normal underline rounded-md \ 121 - focus-visible:outline-none \ 122 - data-hovered:text-mist-20 data-focused:text-mist-20 \ 123 - data-hovered:bg-mana-100 data-focused:bg-mana-100" 124 - href=add_account_url> 125 - (string "add account") 126 - </Aria.ListBoxItem> 127 - </Aria.ListBox> 128 - </Aria.Popover> 129 - </Aria.Select> )] 76 + let button_class = 77 + if inline then button_class_inline else button_class_default 78 + in 79 + let value_class = 80 + if inline then value_class_inline else value_class_default 81 + in 82 + <ClientOnly 83 + fallback=(fallback current_user.handle current_user.avatar_data_uri inline)> 84 + [%browser_only 85 + fun () -> 86 + <Aria.Select 87 + name 88 + className="inline" 89 + defaultValue=current_user.did 90 + placeholder="select account" 91 + onChange=handleAccountSwitch> 92 + <Aria.Button className=button_class> 93 + <Aria.SelectValue className=value_class /> 94 + </Aria.Button> 95 + <Aria.Popover 96 + style=(ReactDOM.Style.make 97 + ~minWidth: 98 + "max(var(--spacing) * 32, calc(var(--trigger-width) + \ 99 + var(--spacing) * 3))" 100 + () ) 101 + className="focus-visible:outline-none"> 102 + <Aria.ListBox 103 + className="w-full flex flex-col gap-y-1 p-1.5 -ml-1.5 rounded-lg \ 104 + bg-mist-20 font-light"> 105 + ( List.map 106 + (fun (user : actor) -> 107 + <Aria.ListBoxItem 108 + className="flex flex-row items-center py-1.5 px-2 \ 109 + gap-x-1 font-serif text-mist-100 rounded-md \ 110 + focus-visible:outline-none \ 111 + data-hovered:text-mist-20 \ 112 + data-focused:text-mist-20 \ 113 + data-hovered:bg-mana-100 \ 114 + data-focused:bg-mana-100" 115 + key=user.did 116 + id=user.did> 117 + ( match user.avatar_data_uri with 118 + | Some src -> 119 + <img src className="w-5 h-5 mr-1 rounded-md" /> 120 + | None -> 121 + null ) 122 + <span className="truncate self-baseline select-none"> 123 + (string user.handle) 124 + </span> 125 + <ChevronDownIcon 126 + className="w-3 h-3 mt-0.5 text-mana-100 hidden \ 127 + group-aria-[haspopup]:inline" 128 + strokeWidth="3" 129 + /> 130 + </Aria.ListBoxItem> ) 131 + logged_in_users 132 + |> Array.of_list |> array ) 133 + <Aria.ListBoxItem 134 + className="flex flex-row items-center p-1 pl-2 text-mana-100 \ 135 + font-normal underline rounded-md \ 136 + focus-visible:outline-none \ 137 + data-hovered:text-mist-20 data-focused:text-mist-20 \ 138 + data-hovered:bg-mana-100 data-focused:bg-mana-100" 139 + href=add_account_url> 140 + (string "add account") 141 + </Aria.ListBoxItem> 142 + </Aria.ListBox> 143 + </Aria.Popover> 144 + </Aria.Select>] 130 145 </ClientOnly>
+1 -5
frontend/src/components/AdminSidebar.mlx
··· 5 5 ; ("Invite codes", "/admin/invites") 6 6 ; ("Blobs", "/admin/blobs") ] 7 7 8 - let[@react.component] make ~active_page () = 9 - <Sidebar 10 - pages 11 - active_page 12 - /> 8 + let[@react.component] make ~active_page () = <Sidebar pages active_page />
+27 -28
frontend/src/components/HandleInput.mlx
··· 27 27 ( "/account/signup/check-handle?handle=" 28 28 ^ Js.Global.encodeURIComponent handle ) 29 29 |> Js.Promise.then_ (fun response -> 30 - if Fetch.Response.ok response then Fetch.Response.json response 31 - else Js.Promise.reject (Js.Exn.raiseError "Request failed") ) 30 + if Fetch.Response.ok response then Fetch.Response.json response 31 + else Js.Promise.reject (Js.Exn.raiseError "Request failed") ) 32 32 |> Js.Promise.then_ (fun json -> 33 - let valid = 34 - Js.Dict.get (Obj.magic json) "valid" 35 - |> Option.map Obj.magic 36 - |> Option.value ~default:false 37 - in 38 - let available = 39 - Js.Dict.get (Obj.magic json) "available" 40 - |> Option.map Obj.magic 41 - |> Option.value ~default:false 42 - in 43 - let error = 44 - Option.bind 45 - ( Js.Dict.get (Obj.magic json) "error" 46 - |> Option.map Obj.magic ) 47 - (fun x -> 48 - if Js.Nullable.isNullable (Obj.magic x) then None 49 - else Some x ) 50 - in 51 - if valid && available then setHandleStatus (fun _ -> Valid) 52 - else 53 - setHandleStatus (fun _ -> 54 - Invalid (Option.value error ~default:"Invalid handle") ) ; 55 - Js.Promise.resolve () ) 33 + let valid = 34 + Js.Dict.get (Obj.magic json) "valid" 35 + |> Option.map Obj.magic 36 + |> Option.value ~default:false 37 + in 38 + let available = 39 + Js.Dict.get (Obj.magic json) "available" 40 + |> Option.map Obj.magic 41 + |> Option.value ~default:false 42 + in 43 + let error = 44 + Option.bind 45 + (Js.Dict.get (Obj.magic json) "error" |> Option.map Obj.magic) 46 + (fun x -> 47 + if Js.Nullable.isNullable (Obj.magic x) then None 48 + else Some x ) 49 + in 50 + if valid && available then setHandleStatus (fun _ -> Valid) 51 + else 52 + setHandleStatus (fun _ -> 53 + Invalid (Option.value error ~default:"Invalid handle") ) ; 54 + Js.Promise.resolve () ) 56 55 |> Js.Promise.catch (fun _ -> 57 - setHandleStatus (fun _ -> 58 - Invalid "Couldn't check handle availability" ) ; 59 - Js.Promise.resolve () ) 56 + setHandleStatus (fun _ -> 57 + Invalid "Couldn't check handle availability" ) ; 58 + Js.Promise.resolve () ) 60 59 in 61 60 ()] 62 61 in
+105 -102
frontend/src/templates/AccountIdentityPage.mlx
··· 37 37 <div 38 38 className="w-full h-full max-w-[816px] px-8 pt-16 mx-auto flex flex-col \ 39 39 md:flex-row gap-8"> 40 - <AccountSidebar 41 - current_user logged_in_users active_page="/account/identity" 40 + <AccountSidebar current_user logged_in_users active_page="/account/identity" 42 41 /> 43 42 <main className="flex-1 w-full md:max-w-lg"> 44 43 <h1 className="text-2xl font-serif text-mana-200 mb-1"> ··· 46 45 </h1> 47 46 ( if not is_plc then 48 47 <p className="text-mist-100"> 49 - (string "Identity management is only available for did:plc accounts.") 48 + (string 49 + "Identity management is only available for did:plc accounts." ) 50 50 </p> 51 51 else 52 52 <div> ··· 56 56 permanently unusable. Make sure you know what you're doing!" ) 57 57 </p> 58 58 <ClientOnly 59 - fallback=( 60 - <div className="flex flex-col gap-y-4"> 61 - <textarea 62 - className="w-full h-96 p-3 font-mono text-sm bg-feather-100 \ 63 - border border-mist-60 rounded-xl text-mana-200 \ 64 - resize-none focus:outline-none focus:border-mana-100" 65 - value=credentialsPlaceholder 66 - disabled=true 67 - /> 68 - <Button type_="submit" disabled=true> 69 - (string "submit operation") 70 - </Button> 71 - </div> 72 - )> 59 + fallback=(<div className="flex flex-col gap-y-4"> 60 + <textarea 61 + className="w-full h-96 p-3 font-mono text-sm \ 62 + bg-feather-100 border border-mist-60 \ 63 + rounded-xl text-mana-200 resize-none \ 64 + focus:outline-none \ 65 + focus:border-mana-100" 66 + value=credentialsPlaceholder 67 + disabled=true 68 + /> 69 + <Button type_="submit" disabled=true> 70 + (string "submit operation") 71 + </Button> 72 + </div>)> 73 73 [%browser_only 74 - (fun () -> 75 - let submitOperation () = 76 - setLoading (fun _ -> true) ; 77 - setErrorState (fun _ -> None) ; 78 - setSuccessState (fun _ -> None) ; 79 - let body = 80 - Fetch.BodyInit.make 81 - (Webapi.Url.URLSearchParams.makeWithArray 82 - [| ("dream.csrf", csrf_token) 83 - ; ("action", "submit") 84 - ; ("credentials", credentialsInput) |] 85 - |> Webapi.Url.URLSearchParams.toString ) 86 - in 87 - let _ = 88 - Fetch.fetchWithInit "/account/identity" 89 - (Fetch.RequestInit.make ~method_:Post ~body 90 - ~headers: 91 - (Fetch.HeadersInit.makeWithArray 92 - [| ( "Content-Type" 93 - , "application/x-www-form-urlencoded" ) |] ) 94 - () ) 95 - |> Js.Promise.then_ (fun response -> 96 - setLoading (fun _ -> false) ; 97 - Fetch.Response.json response ) 98 - |> Js.Promise.then_ (fun json -> 99 - let open Js.Json in 100 - let dict = 101 - decodeObject json |> Option.value ~default:(Js.Dict.empty ()) 102 - in 103 - ( match Js.Dict.get dict "error" with 104 - | Some err_json -> 105 - let err = 106 - decodeString err_json 107 - |> Option.value ~default:"An error occurred" 108 - in 109 - setErrorState (fun _ -> Some err) 110 - | None -> 111 - setSuccessState (fun _ -> 112 - Some "Operation submitted successfully." ) ) ; 113 - Js.Promise.resolve () ) 114 - |> Js.Promise.catch (fun _ -> 115 - setLoading (fun _ -> false) ; 116 - setErrorState (fun _ -> 117 - Some "An error occurred. Please try again." ) ; 118 - Js.Promise.resolve () ) 119 - in 120 - () 74 + fun () -> 75 + let submitOperation () = 76 + setLoading (fun _ -> true) ; 77 + setErrorState (fun _ -> None) ; 78 + setSuccessState (fun _ -> None) ; 79 + let body = 80 + Fetch.BodyInit.make 81 + ( Webapi.Url.URLSearchParams.makeWithArray 82 + [| ("dream.csrf", csrf_token) 83 + ; ("action", "submit") 84 + ; ("credentials", credentialsInput) |] 85 + |> Webapi.Url.URLSearchParams.toString ) 86 + in 87 + let _ = 88 + Fetch.fetchWithInit "/account/identity" 89 + (Fetch.RequestInit.make ~method_:Post ~body 90 + ~headers: 91 + (Fetch.HeadersInit.makeWithArray 92 + [| ( "Content-Type" 93 + , "application/x-www-form-urlencoded" ) |] ) 94 + () ) 95 + |> Js.Promise.then_ (fun response -> 96 + setLoading (fun _ -> false) ; 97 + Fetch.Response.json response ) 98 + |> Js.Promise.then_ (fun json -> 99 + let open Js.Json in 100 + let dict = 101 + decodeObject json 102 + |> Option.value ~default:(Js.Dict.empty ()) 103 + in 104 + ( match Js.Dict.get dict "error" with 105 + | Some err_json -> 106 + let err = 107 + decodeString err_json 108 + |> Option.value ~default:"An error occurred" 109 + in 110 + setErrorState (fun _ -> Some err) 111 + | None -> 112 + setSuccessState (fun _ -> 113 + Some "Operation submitted successfully." ) ) ; 114 + Js.Promise.resolve () ) 115 + |> Js.Promise.catch (fun _ -> 116 + setLoading (fun _ -> false) ; 117 + setErrorState (fun _ -> 118 + Some "An error occurred. Please try again." ) ; 119 + Js.Promise.resolve () ) 121 120 in 122 - <div className="flex flex-col gap-y-4"> 123 - <textarea 124 - className="w-full h-96 p-3 font-mono text-sm bg-feather-100 \ 125 - border border-mist-60 rounded-xl text-mana-200 \ 126 - resize-none focus:outline-none focus:border-mana-100" 127 - value=credentialsInput 128 - onChange=(fun e -> 129 - setCredentialsInput (fun _ -> 130 - (Event.Form.target e)##value ) ) 131 - disabled=loading 132 - /> 133 - ( match errorState with 134 - | Some err -> 135 - <span 136 - className="inline-flex items-center text-phoenix-100 \ 137 - text-sm"> 138 - <CircleAlertIcon className="w-4 h-4 mr-2" /> 139 - (string err) 140 - </span> 141 - | None -> 142 - null ) 143 - ( match successState with 144 - | Some msg -> 145 - <span 146 - className="inline-flex items-center text-mana-100 \ 147 - text-sm"> 148 - (string msg) 149 - </span> 150 - | None -> 151 - null ) 152 - <Button 153 - type_="button" 154 - disabled=loading 155 - onClick=(fun _ -> submitOperation ())> 156 - (string 157 - (if loading then "submitting..." else "submit operation") ) 158 - </Button> 159 - </div> )] 121 + () 122 + in 123 + <div className="flex flex-col gap-y-4"> 124 + <textarea 125 + className="w-full h-96 p-3 font-mono text-sm \ 126 + bg-feather-100 border border-mist-60 \ 127 + rounded-xl text-mana-200 resize-none \ 128 + focus:outline-none focus:border-mana-100" 129 + value=credentialsInput 130 + onChange=(fun e -> 131 + setCredentialsInput (fun _ -> 132 + (Event.Form.target e)##value ) ) 133 + disabled=loading 134 + /> 135 + ( match errorState with 136 + | Some err -> 137 + <span 138 + className="inline-flex items-center text-phoenix-100 \ 139 + text-sm"> 140 + <CircleAlertIcon className="w-4 h-4 mr-2" /> 141 + (string err) 142 + </span> 143 + | None -> 144 + null ) 145 + ( match successState with 146 + | Some msg -> 147 + <span 148 + className="inline-flex items-center text-mana-100 \ 149 + text-sm"> 150 + (string msg) 151 + </span> 152 + | None -> 153 + null ) 154 + <Button 155 + type_="button" 156 + disabled=loading 157 + onClick=(fun _ -> submitOperation ())> 158 + (string 159 + ( if loading then "submitting..." 160 + else "submit operation" ) ) 161 + </Button> 162 + </div>] 160 163 </ClientOnly> 161 164 </div> ) 162 165 </main>
+566 -481
frontend/src/templates/AccountPage.mlx
··· 75 75 let confirmEmailError, setConfirmEmailError = 76 76 useState (fun () -> email_confirmation_error) 77 77 in 78 - let confirmEmailLoading, setConfirmEmailLoading = useState (fun () -> false) in 79 - let confirmEmailTokenInput, setConfirmEmailTokenInput = useState (fun () -> "") in 78 + let confirmEmailLoading, setConfirmEmailLoading = 79 + useState (fun () -> false) 80 + in 81 + let confirmEmailTokenInput, setConfirmEmailTokenInput = 82 + useState (fun () -> "") 83 + in 80 84 let successMessage, setSuccessMessage = useState (fun () -> success) in 81 85 let importLoading, setImportLoading = useState (fun () -> false) in 82 86 let importError, setImportError = useState (fun () -> None) in 83 87 let importSuccess, setImportSuccess = useState (fun () -> false) in 84 - let fileInputRef : Dom.element Js.nullable React.ref = useRef Js.Nullable.null in 85 - <div className="w-full h-full max-w-[816px] px-8 pt-16 mx-auto flex flex-col md:flex-row gap-8"> 86 - <AccountSidebar 87 - current_user logged_in_users active_page="/account" 88 - /> 88 + let fileInputRef : Dom.element Js.nullable React.ref = 89 + useRef Js.Nullable.null 90 + in 91 + <div 92 + className="w-full h-full max-w-[816px] px-8 pt-16 mx-auto flex flex-col \ 93 + md:flex-row gap-8"> 94 + <AccountSidebar current_user logged_in_users active_page="/account" /> 89 95 <main className="flex-1 w-full md:max-w-lg"> 90 96 <h1 className="text-2xl font-serif text-mana-200 mb-1"> 91 97 (string "my account") ··· 124 130 </p> 125 131 <form className="flex flex-col gap-y-3"> 126 132 <input type_="hidden" name="dream.csrf" value=csrf_token /> 127 - <ClientOnly fallback=( 128 - <Input 129 - name="email" 130 - type_="email" 131 - label="Email" 132 - placeholder=email 133 - required=true 134 - showIndicator=false 135 - disabled=true 136 - trailing=( 137 - <button 138 - type_="button" 139 - className="p-1 hover:text-mana-100 cursor-pointer" 140 - ariaLabel="Change email"> 141 - <PencilLineIcon className="w-4 h-4" /> 142 - </button> 143 - ) /> 144 - )> 145 - [%browser_only 146 - (fun () -> 147 - let module Aria = ReactAria in 148 - let submitEmailForm action fields = 149 - setEmailLoading (fun _ -> true) ; 150 - setEmailErrorState (fun _ -> None) ; 151 - let body = 152 - Fetch.BodyInit.make 153 - (Webapi.Url.URLSearchParams.makeWithArray 154 - (Array.append 155 - [|("dream.csrf", csrf_token); ("action", action)|] 156 - fields ) 157 - |> Webapi.Url.URLSearchParams.toString ) 158 - in 159 - let _ = 160 - Fetch.fetchWithInit "/account" 161 - (Fetch.RequestInit.make ~method_:Post ~body 162 - ~headers: 163 - (Fetch.HeadersInit.makeWithArray 164 - [|("Content-Type", "application/x-www-form-urlencoded")|] ) 165 - () ) 166 - |> Js.Promise.then_ (fun response -> 167 - setEmailLoading (fun _ -> false) ; 168 - if Fetch.Response.ok response then 169 - match action with 170 - | "request_email_change" -> 171 - let new_email = 172 - Array.find_opt 173 - (fun (k, _) -> k = "new_email") 174 - fields 175 - |> Option.map snd 176 - in 177 - setEmailPending (fun _ -> true) ; 178 - setEmailPendingAddress (fun _ -> new_email) ; 179 - Js.Promise.resolve () 180 - | "confirm_email_change" -> 181 - setEmailModalOpen (fun _ -> false) ; 182 - setEmailPending (fun _ -> false) ; 183 - setEmailPendingAddress (fun _ -> None) ; 184 - Webapi.Dom.( 185 - location |> Location.reload) ; 186 - Js.Promise.resolve () 187 - | "cancel_email_change" -> 188 - setEmailPending (fun _ -> false) ; 189 - setEmailPendingAddress (fun _ -> None) ; 190 - setEmailModalOpen (fun _ -> false) ; 191 - Js.Promise.resolve () 192 - | _ -> 193 - Js.Promise.resolve () 194 - else ( 195 - setEmailErrorState (fun _ -> 196 - Some "An error occurred. Please try again." ) ; 197 - Js.Promise.resolve () ) ) 198 - |> Js.Promise.catch (fun _ -> 199 - setEmailLoading (fun _ -> false) ; 200 - setEmailErrorState (fun _ -> 201 - Some "An error occurred. Please try again." ) ; 202 - Js.Promise.resolve () ) 203 - in 204 - () 205 - in 206 - <Aria.DialogTrigger defaultOpen=email_change_pending> 207 - <Input 208 - name="email" 209 - type_="email" 210 - label="Email" 211 - placeholder=email 212 - showIndicator=false 213 - disabled=true 214 - trailing=( 215 - <Aria.Pressable> 216 - <button 217 - type_="button" 218 - className="p-1 hover:text-mana-100 cursor-pointer" 219 - ariaLabel="Change email" 220 - onClick=(fun _ -> setEmailModalOpen (fun _ -> true))> 221 - <PencilLineIcon className="w-4 h-4" /> 222 - </button> 223 - </Aria.Pressable> 224 - ) 225 - /> 226 - <Aria.ModalOverlay 227 - className="fixed inset-0 z-50 bg-mist-80/80 \ 228 - flex items-center justify-center" 229 - isDismissable=(not emailPending) 230 - isOpen=emailModalOpen 231 - onOpenChange=(fun o -> setEmailModalOpen (fun _ -> o)) 232 - > 233 - <Aria.Modal 234 - className="bg-feather-100 border border-mist-60 rounded-xl \ 235 - px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 236 - <Aria.Dialog className="outline-none"> 237 - <Aria.Heading 238 - slot="title" 239 - className="text-lg font-serif text-mana-200 mb-2"> 240 - (string "change email") 241 - </Aria.Heading> 242 - ( if emailPending then 243 - <form 244 - className="flex flex-col gap-y-3" 245 - onSubmit=(fun e -> 246 - Event.Form.preventDefault e ; 247 - submitEmailForm "confirm_email_change" 248 - [|("token", emailTokenInput)|] )> 249 - <p className="text-mist-100"> 250 - (string 251 - ( "Enter the verification code sent to " 252 - ^ Option.value emailPendingAddress 253 - ~default:"your new email" 254 - ^ "." ) ) 255 - </p> 256 - <Input 257 - name="token" 258 - label="Verification code" 259 - placeholder="A1B2C-3D4E5" 260 - required=true 261 - showIndicator=false 262 - value=emailTokenInput 263 - onChange=(fun e -> 264 - setEmailTokenInput (fun _ -> 265 - (Event.Form.target e)##value ) ) 266 - /> 267 - ( match emailErrorState with 268 - | Some err -> 269 - <span 270 - className="inline-flex items-center \ 271 - text-phoenix-100 text-sm"> 272 - <CircleAlertIcon className="w-4 h-4 mr-2" /> 273 - (string err) 274 - </span> 275 - | None -> 276 - null ) 277 - <div className="flex flex-row gap-x-3 mt-2"> 278 - <Button 279 - type_="submit" 280 - disabled=emailLoading> 281 - (string (if emailLoading then "verifying..." else "verify")) 282 - </Button> 283 - <Button 284 - kind=`Secondary 285 - type_="button" 286 - disabled=emailLoading 287 - onClick=(fun _ -> 288 - submitEmailForm "cancel_email_change" [||])> 289 - (string "cancel") 290 - </Button> 291 - </div> 292 - </form> 293 - else 294 - <form 295 - className="flex flex-col gap-y-3" 296 - onSubmit=(fun e -> 297 - Event.Form.preventDefault e ; 298 - submitEmailForm "request_email_change" 299 - [|("new_email", newEmailInput)|] )> 300 - <Input 301 - name="new_email" 302 - label="New email address" 303 - type_="email" 304 - required=true 305 - showIndicator=false 306 - value=newEmailInput 307 - onChange=(fun e -> 308 - setNewEmailInput (fun _ -> 309 - (Event.Form.target e)##value ) ) 310 - /> 311 - ( match emailErrorState with 312 - | Some err -> 313 - <span 314 - className="inline-flex items-center \ 315 - text-phoenix-100 text-sm"> 316 - <CircleAlertIcon className="w-4 h-4 mr-2" /> 317 - (string err) 318 - </span> 319 - | None -> 320 - null ) 321 - <div className="flex flex-row gap-x-3 mt-2"> 322 - <Button 323 - type_="submit" 324 - disabled=emailLoading> 325 - (string (if emailLoading then "sending..." else "send code")) 326 - </Button> 327 - <Button kind=`Tertiary 328 - className="text-mist-100 hover:text-mana-100" 329 - onClick=(fun _ -> setEmailModalOpen (fun _ -> false))> 330 - (string "cancel") 331 - </Button> 332 - </div> 333 - </form> ) 334 - </Aria.Dialog> 335 - </Aria.Modal> 336 - </Aria.ModalOverlay> 337 - </Aria.DialogTrigger> )] 133 + <ClientOnly 134 + fallback=(<Input 135 + name="email" 136 + type_="email" 137 + label="Email" 138 + placeholder=email 139 + required=true 140 + showIndicator=false 141 + disabled=true 142 + trailing=(<button 143 + type_="button" 144 + className="p-1 hover:text-mana-100 \ 145 + cursor-pointer" 146 + ariaLabel="Change email"> 147 + <PencilLineIcon className="w-4 h-4" /> 148 + </button>) 149 + />)> 150 + [%browser_only 151 + fun () -> 152 + let module Aria = ReactAria in 153 + let submitEmailForm action fields = 154 + setEmailLoading (fun _ -> true) ; 155 + setEmailErrorState (fun _ -> None) ; 156 + let body = 157 + Fetch.BodyInit.make 158 + ( Webapi.Url.URLSearchParams.makeWithArray 159 + (Array.append 160 + [| ("dream.csrf", csrf_token) 161 + ; ("action", action) |] 162 + fields ) 163 + |> Webapi.Url.URLSearchParams.toString ) 164 + in 165 + let _ = 166 + Fetch.fetchWithInit "/account" 167 + (Fetch.RequestInit.make ~method_:Post ~body 168 + ~headers: 169 + (Fetch.HeadersInit.makeWithArray 170 + [| ( "Content-Type" 171 + , "application/x-www-form-urlencoded" ) 172 + |] ) 173 + () ) 174 + |> Js.Promise.then_ (fun response -> 175 + setEmailLoading (fun _ -> false) ; 176 + if Fetch.Response.ok response then 177 + match action with 178 + | "request_email_change" -> 179 + let new_email = 180 + Array.find_opt 181 + (fun (k, _) -> k = "new_email") 182 + fields 183 + |> Option.map snd 184 + in 185 + setEmailPending (fun _ -> true) ; 186 + setEmailPendingAddress (fun _ -> new_email) ; 187 + Js.Promise.resolve () 188 + | "confirm_email_change" -> 189 + setEmailModalOpen (fun _ -> false) ; 190 + setEmailPending (fun _ -> false) ; 191 + setEmailPendingAddress (fun _ -> None) ; 192 + Webapi.Dom.(location |> Location.reload) ; 193 + Js.Promise.resolve () 194 + | "cancel_email_change" -> 195 + setEmailPending (fun _ -> false) ; 196 + setEmailPendingAddress (fun _ -> None) ; 197 + setEmailModalOpen (fun _ -> false) ; 198 + Js.Promise.resolve () 199 + | _ -> 200 + Js.Promise.resolve () 201 + else ( 202 + setEmailErrorState (fun _ -> 203 + Some "An error occurred. Please try again." ) ; 204 + Js.Promise.resolve () ) ) 205 + |> Js.Promise.catch (fun _ -> 206 + setEmailLoading (fun _ -> false) ; 207 + setEmailErrorState (fun _ -> 208 + Some "An error occurred. Please try again." ) ; 209 + Js.Promise.resolve () ) 210 + in 211 + () 212 + in 213 + <Aria.DialogTrigger defaultOpen=email_change_pending> 214 + <Input 215 + name="email" 216 + type_="email" 217 + label="Email" 218 + placeholder=email 219 + showIndicator=false 220 + disabled=true 221 + trailing=(<Aria.Pressable> 222 + <button 223 + type_="button" 224 + className="p-1 hover:text-mana-100 \ 225 + cursor-pointer" 226 + ariaLabel="Change email" 227 + onClick=(fun _ -> 228 + setEmailModalOpen (fun _ -> 229 + true ) )> 230 + <PencilLineIcon className="w-4 h-4" /> 231 + </button> 232 + </Aria.Pressable>) 233 + /> 234 + <Aria.ModalOverlay 235 + className="fixed inset-0 z-50 bg-mist-80/80 flex \ 236 + items-center justify-center" 237 + isDismissable=(not emailPending) 238 + isOpen=emailModalOpen 239 + onOpenChange=(fun o -> setEmailModalOpen (fun _ -> o))> 240 + <Aria.Modal 241 + className="bg-feather-100 border border-mist-60 \ 242 + rounded-xl px-6 pb-6 pt-5 w-full max-w-sm \ 243 + mx-4 shadow-xl"> 244 + <Aria.Dialog className="outline-none"> 245 + <Aria.Heading 246 + slot="title" 247 + className="text-lg font-serif text-mana-200 mb-2"> 248 + (string "change email") 249 + </Aria.Heading> 250 + ( if emailPending then 251 + <form 252 + className="flex flex-col gap-y-3" 253 + onSubmit=(fun e -> 254 + Event.Form.preventDefault e ; 255 + submitEmailForm 256 + "confirm_email_change" 257 + [|("token", emailTokenInput)|] )> 258 + <p className="text-mist-100"> 259 + (string 260 + ( "Enter the verification code sent to " 261 + ^ Option.value emailPendingAddress 262 + ~default:"your new email" 263 + ^ "." ) ) 264 + </p> 265 + <Input 266 + name="token" 267 + label="Verification code" 268 + placeholder="A1B2C-3D4E5" 269 + required=true 270 + showIndicator=false 271 + value=emailTokenInput 272 + onChange=(fun e -> 273 + setEmailTokenInput (fun _ -> 274 + (Event.Form.target e)##value ) ) 275 + /> 276 + ( match emailErrorState with 277 + | Some err -> 278 + <span 279 + className="inline-flex items-center \ 280 + text-phoenix-100 text-sm"> 281 + <CircleAlertIcon 282 + className="w-4 h-4 mr-2" 283 + /> 284 + (string err) 285 + </span> 286 + | None -> 287 + null ) 288 + <div className="flex flex-row gap-x-3 mt-2"> 289 + <Button 290 + type_="submit" disabled=emailLoading> 291 + (string 292 + ( if emailLoading then "verifying..." 293 + else "verify" ) ) 294 + </Button> 295 + <Button 296 + kind=`Secondary 297 + type_="button" 298 + disabled=emailLoading 299 + onClick=(fun _ -> 300 + submitEmailForm 301 + "cancel_email_change" [||] )> 302 + (string "cancel") 303 + </Button> 304 + </div> 305 + </form> 306 + else 307 + <form 308 + className="flex flex-col gap-y-3" 309 + onSubmit=(fun e -> 310 + Event.Form.preventDefault e ; 311 + submitEmailForm 312 + "request_email_change" 313 + [|("new_email", newEmailInput)|] )> 314 + <Input 315 + name="new_email" 316 + label="New email address" 317 + type_="email" 318 + required=true 319 + showIndicator=false 320 + value=newEmailInput 321 + onChange=(fun e -> 322 + setNewEmailInput (fun _ -> 323 + (Event.Form.target e)##value ) ) 324 + /> 325 + ( match emailErrorState with 326 + | Some err -> 327 + <span 328 + className="inline-flex items-center \ 329 + text-phoenix-100 text-sm"> 330 + <CircleAlertIcon 331 + className="w-4 h-4 mr-2" 332 + /> 333 + (string err) 334 + </span> 335 + | None -> 336 + null ) 337 + <div className="flex flex-row gap-x-3 mt-2"> 338 + <Button 339 + type_="submit" disabled=emailLoading> 340 + (string 341 + ( if emailLoading then "sending..." 342 + else "send code" ) ) 343 + </Button> 344 + <Button 345 + kind=`Tertiary 346 + className="text-mist-100 \ 347 + hover:text-mana-100" 348 + onClick=(fun _ -> 349 + setEmailModalOpen (fun _ -> 350 + false ) )> 351 + (string "cancel") 352 + </Button> 353 + </div> 354 + </form> ) 355 + </Aria.Dialog> 356 + </Aria.Modal> 357 + </Aria.ModalOverlay> 358 + </Aria.DialogTrigger>] 338 359 </ClientOnly> 339 360 ( if not email_confirmed then 340 - <ClientOnly fallback=( 341 - <span className="-mt-1.5 inline-flex items-center text-mist-80 text-sm"> 342 - <CircleAlertIcon className="w-4 h-4 mr-2" /> 343 - (string ("Your email address isn't confirmed " ^ {js|·|js})) 344 - <button 345 - type_="button" 346 - disabled=true 347 - className="ml-1 underline text-mana-100 hover:text-mana-200 cursor-pointer \ 348 - disabled:cursor-not-allowed"> 349 - (string "send confirmation code") 350 - </button> 351 - </span> 352 - )> 353 - [%browser_only 354 - (fun () -> 361 + <ClientOnly 362 + fallback=(<span 363 + className="-mt-1.5 inline-flex items-center \ 364 + text-mist-80 text-sm"> 365 + <CircleAlertIcon className="w-4 h-4 mr-2" /> 366 + (string 367 + ( "Your email address isn't confirmed " 368 + ^ {js|·|js} ) ) 369 + <button 370 + type_="button" 371 + disabled=true 372 + className="ml-1 underline text-mana-100 \ 373 + hover:text-mana-200 \ 374 + cursor-pointer \ 375 + disabled:cursor-not-allowed"> 376 + (string "send confirmation code") 377 + </button> 378 + </span>)> 379 + [%browser_only 380 + fun () -> 355 381 let submitConfirmEmailForm action fields = 356 382 setConfirmEmailLoading (fun _ -> true) ; 357 383 setConfirmEmailError (fun _ -> None) ; 358 384 let body = 359 385 Fetch.BodyInit.make 360 - (Webapi.Url.URLSearchParams.makeWithArray 361 - (Array.append 362 - [|("dream.csrf", csrf_token); ("action", action)|] 363 - fields ) 386 + ( Webapi.Url.URLSearchParams.makeWithArray 387 + (Array.append 388 + [| ("dream.csrf", csrf_token) 389 + ; ("action", action) |] 390 + fields ) 364 391 |> Webapi.Url.URLSearchParams.toString ) 365 392 in 366 393 let _ = ··· 368 395 (Fetch.RequestInit.make ~method_:Post ~body 369 396 ~headers: 370 397 (Fetch.HeadersInit.makeWithArray 371 - [|("Content-Type", "application/x-www-form-urlencoded")|] ) 398 + [| ( "Content-Type" 399 + , "application/x-www-form-urlencoded" 400 + ) |] ) 372 401 () ) 373 402 |> Js.Promise.then_ (fun response -> 374 - setConfirmEmailLoading (fun _ -> false) ; 375 - if Fetch.Response.ok response then 376 - match action with 377 - | "request_email_confirmation" -> 378 - setConfirmEmailPending (fun _ -> true) ; 379 - Js.Promise.resolve () 380 - | "confirm_email_confirmation" -> 381 - Webapi.Dom.( 382 - location |> Location.reload) ; 383 - setSuccessMessage (fun _ -> Some "Email confirmed!") ; 384 - Js.Promise.resolve () 385 - | "cancel_email_confirmation" -> 386 - setConfirmEmailPending (fun _ -> false) ; 387 - Js.Promise.resolve () 388 - | _ -> 389 - Js.Promise.resolve () 390 - else ( 391 - setConfirmEmailError (fun _ -> 392 - Some "An error occurred. Please try again." ) ; 393 - Js.Promise.resolve () ) ) 403 + setConfirmEmailLoading (fun _ -> false) ; 404 + if Fetch.Response.ok response then 405 + match action with 406 + | "request_email_confirmation" -> 407 + setConfirmEmailPending (fun _ -> true) ; 408 + Js.Promise.resolve () 409 + | "confirm_email_confirmation" -> 410 + Webapi.Dom.(location |> Location.reload) ; 411 + setSuccessMessage (fun _ -> 412 + Some "Email confirmed!" ) ; 413 + Js.Promise.resolve () 414 + | "cancel_email_confirmation" -> 415 + setConfirmEmailPending (fun _ -> false) ; 416 + Js.Promise.resolve () 417 + | _ -> 418 + Js.Promise.resolve () 419 + else ( 420 + setConfirmEmailError (fun _ -> 421 + Some 422 + "An error occurred. Please try again." ) ; 423 + Js.Promise.resolve () ) ) 394 424 |> Js.Promise.catch (fun _ -> 395 - setConfirmEmailLoading (fun _ -> false) ; 396 - setConfirmEmailError (fun _ -> 397 - Some "An error occurred. Please try again." ) ; 398 - Js.Promise.resolve () ) 425 + setConfirmEmailLoading (fun _ -> false) ; 426 + setConfirmEmailError (fun _ -> 427 + Some "An error occurred. Please try again." ) ; 428 + Js.Promise.resolve () ) 399 429 in 400 430 () 401 431 in 402 432 <div className="flex flex-col gap-y-3"> 403 - <span className="-mt-1.5 inline-flex items-center text-mist-80 text-sm"> 433 + <span 434 + className="-mt-1.5 inline-flex items-center \ 435 + text-mist-80 text-sm"> 404 436 <CircleAlertIcon className="w-4 h-4 mr-2" /> 405 - (string ("Your email address isn't confirmed " ^ {js|·|js})) 437 + (string 438 + ( "Your email address isn't confirmed " 439 + ^ {js|·|js} ) ) 406 440 <button 407 441 type_="button" 408 442 disabled=confirmEmailLoading 409 - className="ml-1 underline text-mana-100 hover:text-mana-200 cursor-pointer \ 443 + className="ml-1 underline text-mana-100 \ 444 + hover:text-mana-200 cursor-pointer \ 410 445 disabled:cursor-not-allowed" 411 446 onClick=(fun _ -> 412 - submitConfirmEmailForm "request_email_confirmation" [||])> 413 - (string (if confirmEmailLoading then "sending..." 414 - else if confirmEmailPending then "resend confirmation code" 415 - else "send confirmation code")) 447 + submitConfirmEmailForm 448 + "request_email_confirmation" [||] )> 449 + (string 450 + ( if confirmEmailLoading then "sending..." 451 + else if confirmEmailPending then 452 + "resend confirmation code" 453 + else "send confirmation code" ) ) 416 454 </button> 417 455 </span> 418 456 ( if confirmEmailPending then 419 457 <div className="flex flex-col gap-y-3"> 420 - <Input 458 + <Input 421 459 name="email_token" 422 460 label="Confirmation code" 423 461 placeholder="A1B2C-3D4E5" 424 462 showIndicator=false 425 463 value=confirmEmailTokenInput 426 464 onChange=(fun e -> 427 - setConfirmEmailTokenInput (fun _ -> 428 - (Event.Form.target e)##value ) ) 429 - /> 430 - ( match confirmEmailError with 431 - | Some err -> 432 - <span 465 + setConfirmEmailTokenInput 466 + (fun _ -> 467 + (Event.Form.target e)##value ) ) 468 + /> 469 + ( match confirmEmailError with 470 + | Some err -> 471 + <span 433 472 className="inline-flex items-center \ 434 - text-phoenix-100 text-sm"> 435 - <CircleAlertIcon className="w-4 h-4 mr-2" /> 473 + text-phoenix-100 text-sm"> 474 + <CircleAlertIcon className="w-4 h-4 mr-2" 475 + /> 436 476 (string err) 437 - </span> 438 - | None -> 439 - null ) 440 - <div className="flex flex-row gap-x-3"> 477 + </span> 478 + | None -> 479 + null ) 480 + <div className="flex flex-row gap-x-3"> 441 481 <Button 442 482 type_="button" 443 483 disabled=confirmEmailLoading 444 484 onClick=(fun _ -> 445 - submitConfirmEmailForm "confirm_email_confirmation" 446 - [|("token", confirmEmailTokenInput)|])> 447 - (string (if confirmEmailLoading then "confirming..." else "confirm")) 485 + submitConfirmEmailForm 486 + "confirm_email_confirmation" 487 + [| ( "token" 488 + , confirmEmailTokenInput ) 489 + |] )> 490 + (string 491 + ( if confirmEmailLoading then 492 + "confirming..." 493 + else "confirm" ) ) 448 494 </Button> 449 495 <Button 450 496 kind=`Secondary 451 497 type_="button" 452 498 disabled=confirmEmailLoading 453 499 onClick=(fun _ -> 454 - submitConfirmEmailForm "cancel_email_confirmation" [||])> 500 + submitConfirmEmailForm 501 + "cancel_email_confirmation" [||] )> 455 502 (string "cancel") 456 503 </Button> 457 - </div> 504 + </div> 458 505 </div> 459 - else 460 - null ) 461 - </div> )] 506 + else null ) 507 + </div>] 462 508 </ClientOnly> 463 509 else null ) 464 510 <HandleInput ··· 507 553 </h2> 508 554 <p className="text-mist-100 mb-4"> 509 555 (string 510 - "Export your data to back up or transfer to another PDS, or import from a CAR backup.") 556 + "Export your data to back up or transfer to another PDS, or \ 557 + import from a CAR backup." ) 511 558 </p> 512 559 <div className="flex flex-row gap-x-3"> 513 560 <a 514 561 href=("/xrpc/com.atproto.sync.getRepo?did=" ^ current_user.did) 515 562 download=("repo-" ^ current_user.handle ^ ".car") 516 563 className="basis-1/2 grow"> 517 - <Button kind=`Primary> 518 - (string "download") 519 - </Button> 564 + <Button kind=`Primary>(string "download")</Button> 520 565 </a> 521 - <ClientOnly fallback=( 522 - <Button kind=`Secondary disabled=true className="basis-1/2 grow"> 523 - (string "import") 524 - </Button> 525 - )> 526 - [%browser_only 527 - (fun () -> 566 + <ClientOnly 567 + fallback=(<Button 568 + kind=`Secondary 569 + disabled=true 570 + className="basis-1/2 grow"> 571 + (string "import") 572 + </Button>)> 573 + [%browser_only 574 + fun () -> 528 575 let handleFileChange e = 529 576 let files = (Event.Form.target e)##files in 530 577 if Js.Array.length files > 0 then begin ··· 533 580 setImportError (fun _ -> None) ; 534 581 setImportSuccess (fun _ -> false) ; 535 582 let _ = 536 - Fetch.fetchWithInit "/xrpc/com.atproto.repo.importRepo" 583 + Fetch.fetchWithInit 584 + "/xrpc/com.atproto.repo.importRepo" 537 585 (Fetch.RequestInit.make ~method_:Post 538 586 ~body:(Fetch.BodyInit.makeWithBlob file) 539 587 ~headers: 540 588 (Fetch.HeadersInit.makeWithArray 541 - [|("Content-Type", "application/vnd.ipld.car")|] ) 589 + [| ( "Content-Type" 590 + , "application/vnd.ipld.car" ) |] ) 542 591 () ) 543 592 |> Js.Promise.then_ (fun response -> 544 - setImportLoading (fun _ -> false) ; 545 - if Fetch.Response.ok response then begin 546 - setImportSuccess (fun _ -> true) ; 547 - Js.Promise.resolve () 548 - end else begin 549 - setImportError (fun _ -> 550 - Some "Import failed. Please try again." ) ; 551 - Js.Promise.resolve () 552 - end ) 593 + setImportLoading (fun _ -> false) ; 594 + if Fetch.Response.ok response then begin 595 + setImportSuccess (fun _ -> true) ; 596 + Js.Promise.resolve () 597 + end 598 + else begin 599 + setImportError (fun _ -> 600 + Some "Import failed. Please try again." ) ; 601 + Js.Promise.resolve () 602 + end ) 553 603 |> Js.Promise.catch (fun _ -> 554 - setImportLoading (fun _ -> false) ; 555 - setImportError (fun _ -> 556 - Some "An error occurred. Please try again." ) ; 557 - Js.Promise.resolve () ) 604 + setImportLoading (fun _ -> false) ; 605 + setImportError (fun _ -> 606 + Some "An error occurred. Please try again." ) ; 607 + Js.Promise.resolve () ) 558 608 in 559 609 () 560 610 end ··· 571 621 kind=`Secondary 572 622 disabled=importLoading 573 623 onClick=(fun _ -> 574 - match Js.Nullable.toOption fileInputRef.current with 575 - | Some el -> 576 - let input = Webapi.Dom.Element.unsafeAsHtmlElement el in 577 - Webapi.Dom.HtmlElement.click input 578 - | None -> () )> 579 - (string (if importLoading then "importing..." else "import")) 624 + match 625 + Js.Nullable.toOption fileInputRef.current 626 + with 627 + | Some el -> 628 + let input = 629 + Webapi.Dom.Element.unsafeAsHtmlElement 630 + el 631 + in 632 + Webapi.Dom.HtmlElement.click input 633 + | None -> 634 + () )> 635 + (string 636 + (if importLoading then "importing..." else "import") ) 580 637 </Button> 581 - </div> )] 638 + </div>] 582 639 </ClientOnly> 583 640 </div> 584 641 ( match importError with 585 642 | Some err -> 586 - <span className="inline-flex items-center text-phoenix-100 text-sm mt-4"> 587 - <CircleAlertIcon className="w-4 h-4 mr-2" /> 588 - (string err) 643 + <span 644 + className="inline-flex items-center text-phoenix-100 \ 645 + text-sm mt-4"> 646 + <CircleAlertIcon className="w-4 h-4 mr-2" /> (string err) 589 647 </span> 590 648 | None when importSuccess -> 591 - <span className="inline-flex items-center text-mana-100 text-sm mt-4"> 649 + <span 650 + className="inline-flex items-center text-mana-100 text-sm \ 651 + mt-4"> 592 652 (string "Repository imported successfully.") 593 653 </span> 594 654 | None -> 595 655 <p className="text-mist-80 text-sm mt-4"> 596 - (string "It is recommended to export a copy of your \ 597 - repository before importing a backup. \ 598 - The import process will overwrite any existing \ 599 - data with the contents of the backup.") 656 + (string 657 + "It is recommended to export a copy of your repository \ 658 + before importing a backup. The import process will \ 659 + overwrite any existing data with the contents of the \ 660 + backup." ) 600 661 </p> ) 601 662 </section> 602 663 <section className="mt-8"> ··· 625 686 (string "deactivate account") 626 687 </Button> 627 688 </form> 628 - <ClientOnly fallback=( 629 - <Button kind=`Danger className="flex-1">(string "delete account")</Button> 630 - )> 631 - [%browser_only 632 - (fun () -> 633 - let module Aria = ReactAria in 634 - let submitDeleteForm action fields = 635 - setDeleteLoading (fun _ -> true) ; 636 - setDeleteErrorState (fun _ -> None) ; 637 - let body = 638 - Fetch.BodyInit.make 639 - (Webapi.Url.URLSearchParams.makeWithArray 640 - (Array.append 641 - [|("dream.csrf", csrf_token); ("action", action)|] 642 - fields ) 643 - |> Webapi.Url.URLSearchParams.toString ) 644 - in 645 - let _ = 646 - Fetch.fetchWithInit "/account" 647 - (Fetch.RequestInit.make ~method_:Post ~body 648 - ~headers: 649 - (Fetch.HeadersInit.makeWithArray 650 - [|("Content-Type", "application/x-www-form-urlencoded")|] ) 651 - () ) 652 - |> Js.Promise.then_ (fun response -> 653 - setDeleteLoading (fun _ -> false) ; 654 - if Fetch.Response.ok response then 655 - match action with 656 - | "request_delete" -> 657 - setDeletePendingState (fun _ -> true) ; 658 - Js.Promise.resolve () 659 - | "confirm_delete" -> 660 - Webapi.Dom.( 661 - Window.setLocation window "/account/login") ; 662 - Js.Promise.resolve () 663 - | "cancel_delete" -> 664 - setDeletePendingState (fun _ -> false) ; 665 - setDeleteModalOpen (fun _ -> false) ; 666 - Js.Promise.resolve () 667 - | _ -> 668 - Js.Promise.resolve () 669 - else ( 670 - setDeleteErrorState (fun _ -> 671 - Some "An error occurred. Please try again." ) ; 672 - Js.Promise.resolve () ) ) 673 - |> Js.Promise.catch (fun _ -> 674 - setDeleteLoading (fun _ -> false) ; 675 - setDeleteErrorState (fun _ -> 676 - Some "An error occurred. Please try again." ) ; 677 - Js.Promise.resolve () ) 678 - in 679 - () 689 + <ClientOnly 690 + fallback=(<Button kind=`Danger className="flex-1"> 691 + (string "delete account") 692 + </Button>)> 693 + [%browser_only 694 + fun () -> 695 + let module Aria = ReactAria in 696 + let submitDeleteForm action fields = 697 + setDeleteLoading (fun _ -> true) ; 698 + setDeleteErrorState (fun _ -> None) ; 699 + let body = 700 + Fetch.BodyInit.make 701 + ( Webapi.Url.URLSearchParams.makeWithArray 702 + (Array.append 703 + [| ("dream.csrf", csrf_token) 704 + ; ("action", action) |] 705 + fields ) 706 + |> Webapi.Url.URLSearchParams.toString ) 707 + in 708 + let _ = 709 + Fetch.fetchWithInit "/account" 710 + (Fetch.RequestInit.make ~method_:Post ~body 711 + ~headers: 712 + (Fetch.HeadersInit.makeWithArray 713 + [| ( "Content-Type" 714 + , "application/x-www-form-urlencoded" ) 715 + |] ) 716 + () ) 717 + |> Js.Promise.then_ (fun response -> 718 + setDeleteLoading (fun _ -> false) ; 719 + if Fetch.Response.ok response then 720 + match action with 721 + | "request_delete" -> 722 + setDeletePendingState (fun _ -> true) ; 723 + Js.Promise.resolve () 724 + | "confirm_delete" -> 725 + Webapi.Dom.( 726 + Window.setLocation window "/account/login" ) ; 727 + Js.Promise.resolve () 728 + | "cancel_delete" -> 729 + setDeletePendingState (fun _ -> false) ; 730 + setDeleteModalOpen (fun _ -> false) ; 731 + Js.Promise.resolve () 732 + | _ -> 733 + Js.Promise.resolve () 734 + else ( 735 + setDeleteErrorState (fun _ -> 736 + Some "An error occurred. Please try again." ) ; 737 + Js.Promise.resolve () ) ) 738 + |> Js.Promise.catch (fun _ -> 739 + setDeleteLoading (fun _ -> false) ; 740 + setDeleteErrorState (fun _ -> 741 + Some "An error occurred. Please try again." ) ; 742 + Js.Promise.resolve () ) 680 743 in 681 - <Aria.DialogTrigger defaultOpen=delete_pending> 682 - <Aria.Pressable> 683 - <Button kind=`Danger className="flex-1" onClick=(fun _ -> setDeleteModalOpen (fun _ -> true))> 684 - (string "delete account") 685 - </Button> 686 - </Aria.Pressable> 687 - <Aria.ModalOverlay 688 - className="fixed inset-0 z-50 bg-mist-80/80 \ 689 - flex items-center justify-center" 690 - isDismissable=(not deletePendingState) 691 - isOpen=deleteModalOpen onOpenChange=(fun o -> setDeleteModalOpen (fun _ -> o))> 692 - <Aria.Modal 693 - className="bg-feather-100 border border-mist-60 rounded-xl \ 694 - px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 695 - <Aria.Dialog className="outline-none"> 696 - <Aria.Heading 697 - slot="title" 698 - className="text-lg font-serif text-mana-200 mb-2"> 699 - (string "delete account") 700 - </Aria.Heading> 701 - ( if deletePendingState then 702 - <form 703 - className="flex flex-col gap-y-3" 704 - onSubmit=(fun e -> 705 - Event.Form.preventDefault e ; 706 - submitDeleteForm "confirm_delete" 707 - [|("token", deleteTokenInput)|] )> 708 - <p className="text-mist-100 text-sm"> 709 - (string 710 - "Enter the confirmation code sent to your email." ) 711 - </p> 712 - <Input 713 - name="token" 714 - label="Confirmation code" 715 - placeholder="del-..." 716 - required=true 717 - showIndicator=false 718 - value=deleteTokenInput 719 - onChange=(fun e -> 720 - setDeleteTokenInput (fun _ -> 721 - (Event.Form.target e)##value ) ) 722 - /> 723 - ( match deleteErrorState with 724 - | Some err -> 725 - <span 726 - className="inline-flex items-center \ 727 - text-phoenix-100 text-sm"> 728 - <CircleAlertIcon className="w-4 h-4 mr-2" /> 729 - (string err) 730 - </span> 731 - | None -> 732 - null ) 733 - <div className="flex flex-row gap-x-3 mt-2"> 734 - <Button 735 - kind=`Danger 736 - type_="submit" 737 - disabled=deleteLoading> 738 - (string (if deleteLoading then "deleting..." else "confirm")) 739 - </Button> 740 - <Button 741 - kind=`Secondary 742 - type_="button" 743 - disabled=deleteLoading 744 - onClick=(fun _ -> 745 - submitDeleteForm "cancel_delete" [||])> 746 - (string "cancel") 747 - </Button> 748 - </div> 749 - </form> 750 - else 751 - <form 752 - className="flex flex-col gap-y-3" 753 - onSubmit=(fun e -> 754 - Event.Form.preventDefault e ; 755 - submitDeleteForm "request_delete" [||] )> 756 - <p className="text-mist-100"> 757 - (string 758 - "This action is irreversible. A confirmation \ 759 - code will be sent to your email." ) 760 - </p> 761 - <div className="flex flex-row gap-x-3 mt-2"> 762 - <Button 763 - kind=`Danger 764 - type_="submit" 765 - disabled=deleteLoading> 766 - (string (if deleteLoading then "sending..." else "send code")) 767 - </Button> 768 - <Button kind=`Tertiary 769 - onClick=(fun _ -> setDeleteModalOpen (fun _ -> false)) 770 - className="text-mist-100 hover:text-mana-100"> 771 - (string "cancel") 772 - </Button> 773 - </div> 774 - </form> ) 775 - </Aria.Dialog> 776 - </Aria.Modal> 777 - </Aria.ModalOverlay> 778 - </Aria.DialogTrigger> )] 744 + () 745 + in 746 + <Aria.DialogTrigger defaultOpen=delete_pending> 747 + <Aria.Pressable> 748 + <Button 749 + kind=`Danger 750 + className="flex-1" 751 + onClick=(fun _ -> setDeleteModalOpen (fun _ -> true))> 752 + (string "delete account") 753 + </Button> 754 + </Aria.Pressable> 755 + <Aria.ModalOverlay 756 + className="fixed inset-0 z-50 bg-mist-80/80 flex \ 757 + items-center justify-center" 758 + isDismissable=(not deletePendingState) 759 + isOpen=deleteModalOpen 760 + onOpenChange=(fun o -> setDeleteModalOpen (fun _ -> o))> 761 + <Aria.Modal 762 + className="bg-feather-100 border border-mist-60 \ 763 + rounded-xl px-6 pb-6 pt-5 w-full \ 764 + max-w-sm mx-4 shadow-xl"> 765 + <Aria.Dialog className="outline-none"> 766 + <Aria.Heading 767 + slot="title" 768 + className="text-lg font-serif text-mana-200 \ 769 + mb-2"> 770 + (string "delete account") 771 + </Aria.Heading> 772 + ( if deletePendingState then 773 + <form 774 + className="flex flex-col gap-y-3" 775 + onSubmit=(fun e -> 776 + Event.Form.preventDefault e ; 777 + submitDeleteForm "confirm_delete" 778 + [|("token", deleteTokenInput)|] )> 779 + <p className="text-mist-100 text-sm"> 780 + (string 781 + "Enter the confirmation code sent to \ 782 + your email." ) 783 + </p> 784 + <Input 785 + name="token" 786 + label="Confirmation code" 787 + placeholder="del-..." 788 + required=true 789 + showIndicator=false 790 + value=deleteTokenInput 791 + onChange=(fun e -> 792 + setDeleteTokenInput (fun _ -> 793 + (Event.Form.target e)##value ) ) 794 + /> 795 + ( match deleteErrorState with 796 + | Some err -> 797 + <span 798 + className="inline-flex items-center \ 799 + text-phoenix-100 text-sm"> 800 + <CircleAlertIcon 801 + className="w-4 h-4 mr-2" 802 + /> 803 + (string err) 804 + </span> 805 + | None -> 806 + null ) 807 + <div className="flex flex-row gap-x-3 mt-2"> 808 + <Button 809 + kind=`Danger 810 + type_="submit" 811 + disabled=deleteLoading> 812 + (string 813 + ( if deleteLoading then "deleting..." 814 + else "confirm" ) ) 815 + </Button> 816 + <Button 817 + kind=`Secondary 818 + type_="button" 819 + disabled=deleteLoading 820 + onClick=(fun _ -> 821 + submitDeleteForm 822 + "cancel_delete" [||] )> 823 + (string "cancel") 824 + </Button> 825 + </div> 826 + </form> 827 + else 828 + <form 829 + className="flex flex-col gap-y-3" 830 + onSubmit=(fun e -> 831 + Event.Form.preventDefault e ; 832 + submitDeleteForm "request_delete" 833 + [||] )> 834 + <p className="text-mist-100"> 835 + (string 836 + "This action is irreversible. A \ 837 + confirmation code will be sent to \ 838 + your email." ) 839 + </p> 840 + <div className="flex flex-row gap-x-3 mt-2"> 841 + <Button 842 + kind=`Danger 843 + type_="submit" 844 + disabled=deleteLoading> 845 + (string 846 + ( if deleteLoading then "sending..." 847 + else "send code" ) ) 848 + </Button> 849 + <Button 850 + kind=`Tertiary 851 + onClick=(fun _ -> 852 + setDeleteModalOpen (fun _ -> 853 + false ) ) 854 + className="text-mist-100 \ 855 + hover:text-mana-100"> 856 + (string "cancel") 857 + </Button> 858 + </div> 859 + </form> ) 860 + </Aria.Dialog> 861 + </Aria.Modal> 862 + </Aria.ModalOverlay> 863 + </Aria.DialogTrigger>] 779 864 </ClientOnly> 780 865 </div> 781 866 </section>
+2 -2
frontend/src/templates/AccountPermissionsPage.mlx
··· 68 68 ({current_user; logged_in_users; csrf_token; authorized_apps; devices} : 69 69 props ) () = 70 70 <div 71 - className="w-full h-full max-w-[816px] px-8 pt-16 mx-auto flex \ 72 - flex-col md:flex-row gap-8"> 71 + className="w-full h-full max-w-[816px] px-8 pt-16 mx-auto flex flex-col \ 72 + md:flex-row gap-8"> 73 73 <AccountSidebar 74 74 current_user logged_in_users active_page="/account/permissions" 75 75 />
+158 -98
frontend/src/templates/AdminBlobsPage.mlx
··· 22 22 ; success: string option [@default None] } 23 23 [@@deriving json] 24 24 25 - let is_image mimetype = 26 - String.starts_with ~prefix:"image/" mimetype 25 + let is_image mimetype = String.starts_with ~prefix:"image/" mimetype 27 26 28 - let is_video mimetype = 29 - String.starts_with ~prefix:"video/" mimetype 27 + let is_video mimetype = String.starts_with ~prefix:"video/" mimetype 30 28 31 29 let[@react.component] make 32 - ~props: 33 - ({ blobs 34 - ; csrf_token 35 - ; cursor 36 - ; next_cursor 37 - ; error 38 - ; success } : 39 - props ) () = 40 - let deleteConfirmFor, setDeleteConfirmFor = useState (fun () -> (None : blob option)) in 41 - <div className="w-full h-full max-w-4xl px-4 pt-16 mx-auto flex flex-col md:flex-row gap-12"> 30 + ~props:({blobs; csrf_token; cursor; next_cursor; error; success} : props) () 31 + = 32 + let deleteConfirmFor, setDeleteConfirmFor = 33 + useState (fun () -> (None : blob option)) 34 + in 35 + <div 36 + className="w-full h-full max-w-4xl px-4 pt-16 mx-auto flex flex-col \ 37 + md:flex-row gap-12"> 42 38 <AdminSidebar active_page="/admin/blobs" /> 43 39 <main className="flex-1 w-full"> 44 40 <h1 className="text-2xl font-serif text-mana-200 mb-1"> ··· 54 50 <CircleAlertIcon className="w-4 h-4 mr-2" /> (string err) 55 51 </span> 56 52 </div> 57 - | None -> null ) 53 + | None -> 54 + null ) 58 55 ( match success with 59 56 | Some msg -> 60 57 <div className="mb-4"> ··· 62 59 <CheckmarkIcon className="w-4 h-4 mr-2" /> (string msg) 63 60 </span> 64 61 </div> 65 - | None -> null ) 62 + | None -> 63 + null ) 66 64 ( if List.length blobs = 0 then 67 65 <div className="mt-12 text-center text-mist-80"> 68 66 <p>(string "No blobs found.")</p> ··· 71 69 <div className="grid grid-cols-3 sm:grid-cols-4 gap-3 mb-6"> 72 70 ( List.map 73 71 (fun (blob : blob) -> 74 - let view_url = "/admin/blobs/view?did=" ^ blob.did ^ "&cid=" ^ blob.cid in 72 + let view_url = 73 + "/admin/blobs/view?did=" ^ blob.did ^ "&cid=" ^ blob.cid 74 + in 75 75 let is_img = is_image blob.mimetype in 76 76 let is_vid = is_video blob.mimetype in 77 77 <a ··· 79 79 target="_blank" 80 80 rel="noopener noreferrer" 81 81 key=(blob.did ^ "-" ^ blob.cid) 82 - className="group border border-mist-60/50 rounded-lg overflow-hidden bg-feather-100 hover:border-mana-100 hover:shadow-md transition-all"> 83 - <div className="block aspect-[4/3] bg-mist-60/20 relative overflow-hidden"> 82 + className="group border border-mist-60/50 rounded-lg \ 83 + overflow-hidden bg-feather-100 \ 84 + hover:border-mana-100 hover:shadow-md \ 85 + transition-all"> 86 + <div 87 + className="block aspect-[4/3] bg-mist-60/20 relative \ 88 + overflow-hidden"> 84 89 ( if is_img then 85 90 <img 86 91 src=view_url 87 92 alt="blob preview" 88 - className="w-full h-full object-cover" 89 - /> 93 + className="w-full h-full object-cover" /> 90 94 else if is_vid then 91 - <div className="w-full h-full flex flex-col items-center justify-center text-mist-80"> 95 + <div 96 + className="w-full h-full flex flex-col \ 97 + items-center justify-center \ 98 + text-mist-80"> 92 99 <UploadIcon className="w-8 h-8 mb-1" /> 93 100 <span className="text-xs">(string "Video")</span> 94 101 </div> 95 102 else 96 - <div className="flex flex-col items-center justify-center text-mist-80 h-full px-2"> 103 + <div 104 + className="flex flex-col items-center \ 105 + justify-center text-mist-80 h-full px-2"> 97 106 <UploadIcon className="w-8 h-8 mb-1" /> 98 - <span className="text-xs text-center break-all line-clamp-2"> 99 - (string (if blob.mimetype = "*/*" then "Unknown" else blob.mimetype)) 107 + <span 108 + className="text-xs text-center break-all \ 109 + line-clamp-2"> 110 + (string 111 + ( if blob.mimetype = "*/*" then "Unknown" 112 + else blob.mimetype ) ) 100 113 </span> 101 114 </div> ) 102 - <div className="absolute inset-0 bg-mist-80/0 group-hover:bg-mist-80/60 transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100"> 115 + <div 116 + className="absolute inset-0 bg-mist-80/0 \ 117 + group-hover:bg-mist-80/60 transition-colors \ 118 + flex items-center justify-center opacity-0 \ 119 + group-hover:opacity-100"> 103 120 <button 104 121 onClick=(fun e -> 105 - Event.Mouse.preventDefault e ; 106 - Event.Mouse.stopPropagation e ; 107 - setDeleteConfirmFor (fun _ -> Some blob)) 108 - className="px-3 py-2 bg-phoenix-100 hover:bg-phoenix-80 text-feather-100 rounded-lg shadow-lg transition-colors flex items-center gap-2"> 122 + Event.Mouse.preventDefault e ; 123 + Event.Mouse.stopPropagation e ; 124 + setDeleteConfirmFor (fun _ -> Some blob) ) 125 + className="px-3 py-2 bg-phoenix-100 \ 126 + hover:bg-phoenix-80 text-feather-100 \ 127 + rounded-lg shadow-lg transition-colors \ 128 + flex items-center gap-2"> 109 129 <TrashIcon className="w-4 h-4" /> 110 130 <span className="text-sm">(string "Delete")</span> 111 131 </button> 112 132 </div> 113 133 </div> 114 134 <div className="p-2 bg-feather-100"> 115 - <p className="text-xs text-mana-100 font-medium truncate mb-1" title=blob.handle> 135 + <p 136 + className="text-xs text-mana-100 font-medium truncate \ 137 + mb-1" 138 + title=blob.handle> 116 139 (string blob.handle) 117 140 </p> 118 - <div className="flex items-center justify-between text-xs text-mist-80"> 141 + <div 142 + className="flex items-center justify-between text-xs \ 143 + text-mist-80"> 119 144 <span>(string blob.size)</span> 120 - <span className="px-1.5 py-0.5 rounded bg-mist-60/30 text-mist-100"> 145 + <span 146 + className="px-1.5 py-0.5 rounded bg-mist-60/30 \ 147 + text-mist-100"> 121 148 (string blob.storage) 122 149 </span> 123 150 </div> ··· 133 160 <Button kind=`Secondary>(string "Load more")</Button> 134 161 </a> 135 162 </div> 136 - | None -> null ) 163 + | None -> 164 + null ) 137 165 <ClientOnly fallback=null> 138 - [%browser_only 139 - (fun () -> 140 - let module Aria = ReactAria in 141 - <Aria.DialogTrigger 142 - isOpen=(deleteConfirmFor <> None) 143 - onOpenChange=(fun o -> if not o then setDeleteConfirmFor (fun _ -> None))> 144 - <Aria.ModalOverlay 145 - className="fixed inset-0 z-50 bg-mist-80/80 flex items-center justify-center" 146 - isDismissable=true> 147 - <Aria.Modal 148 - className="bg-feather-100 border border-mist-60 rounded-xl px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 149 - <Aria.Dialog className="outline-none"> 150 - ( match deleteConfirmFor with 151 - | Some blob -> 152 - let view_url = "/admin/blobs/view?did=" ^ blob.did ^ "&cid=" ^ blob.cid in 153 - let is_img = is_image blob.mimetype in 154 - <form className="flex flex-col gap-y-2"> 155 - <Aria.Heading 156 - slot="title" 157 - className="text-lg font-serif text-mana-200 mb-2"> 158 - (string "delete blob") 159 - </Aria.Heading> 160 - <input type_="hidden" name="dream.csrf" value=csrf_token /> 161 - <input type_="hidden" name="action" value="delete_blob" /> 162 - <input type_="hidden" name="did" value=blob.did /> 163 - <input type_="hidden" name="cid" value=blob.cid /> 164 - ( if is_img then 165 - <div className="mb-2 rounded overflow-hidden border border-mist-60/50"> 166 - <img 167 - src=view_url 168 - alt="blob preview" 169 - className="w-full h-auto" 170 - /> 171 - </div> 172 - else null ) 173 - <p className="text-mist-100 mb-2"> 174 - (string ("Delete this blob from " ^ blob.handle ^ "?")) 175 - </p> 176 - <div className="bg-mist-60/20 rounded p-3 mb-2"> 177 - <p className="text-xs text-mist-80 mb-1">(string "Type")</p> 178 - <p className="text-xs text-mist-100 mb-2">(string blob.mimetype)</p> 179 - <p className="text-xs text-mist-80 mb-1">(string "Size")</p> 180 - <p className="text-xs text-mist-100 mb-2">(string blob.size)</p> 181 - <p className="text-xs text-mist-80 mb-1">(string "CID")</p> 182 - <p className="font-mono text-xs text-mist-100 break-all"> 183 - (string blob.cid) 166 + [%browser_only 167 + fun () -> 168 + let module Aria = ReactAria in 169 + <Aria.DialogTrigger 170 + isOpen=(deleteConfirmFor <> None) 171 + onOpenChange=(fun o -> 172 + if not o then setDeleteConfirmFor (fun _ -> None) )> 173 + <Aria.ModalOverlay 174 + className="fixed inset-0 z-50 bg-mist-80/80 flex items-center \ 175 + justify-center" 176 + isDismissable=true> 177 + <Aria.Modal 178 + className="bg-feather-100 border border-mist-60 rounded-xl \ 179 + px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 180 + <Aria.Dialog className="outline-none"> 181 + ( match deleteConfirmFor with 182 + | Some blob -> 183 + let view_url = 184 + "/admin/blobs/view?did=" ^ blob.did ^ "&cid=" 185 + ^ blob.cid 186 + in 187 + let is_img = is_image blob.mimetype in 188 + <form className="flex flex-col gap-y-2"> 189 + <Aria.Heading 190 + slot="title" 191 + className="text-lg font-serif text-mana-200 mb-2"> 192 + (string "delete blob") 193 + </Aria.Heading> 194 + <input 195 + type_="hidden" name="dream.csrf" value=csrf_token 196 + /> 197 + <input 198 + type_="hidden" name="action" value="delete_blob" 199 + /> 200 + <input type_="hidden" name="did" value=blob.did /> 201 + <input type_="hidden" name="cid" value=blob.cid /> 202 + ( if is_img then 203 + <div 204 + className="mb-2 rounded overflow-hidden border \ 205 + border-mist-60/50"> 206 + <img 207 + src=view_url 208 + alt="blob preview" 209 + className="w-full h-auto" 210 + /> 211 + </div> 212 + else null ) 213 + <p className="text-mist-100 mb-2"> 214 + (string 215 + ("Delete this blob from " ^ blob.handle ^ "?") ) 184 216 </p> 185 - </div> 186 - <p className="text-xs text-phoenix-100"> 187 - (string "This action cannot be undone and may break references to this blob.") 188 - </p> 189 - <div className="flex gap-3 mt-2"> 190 - <Button kind=`Danger formMethod="post" type_="submit">(string "delete")</Button> 191 - <Button 192 - kind=`Tertiary 193 - onClick=(fun _ -> setDeleteConfirmFor (fun _ -> None))> 194 - (string "cancel") 195 - </Button> 196 - </div> 197 - </form> 198 - | None -> null ) 199 - </Aria.Dialog> 200 - </Aria.Modal> 201 - </Aria.ModalOverlay> 202 - </Aria.DialogTrigger> )] 217 + <div className="bg-mist-60/20 rounded p-3 mb-2"> 218 + <p className="text-xs text-mist-80 mb-1"> 219 + (string "Type") 220 + </p> 221 + <p className="text-xs text-mist-100 mb-2"> 222 + (string blob.mimetype) 223 + </p> 224 + <p className="text-xs text-mist-80 mb-1"> 225 + (string "Size") 226 + </p> 227 + <p className="text-xs text-mist-100 mb-2"> 228 + (string blob.size) 229 + </p> 230 + <p className="text-xs text-mist-80 mb-1"> 231 + (string "CID") 232 + </p> 233 + <p 234 + className="font-mono text-xs text-mist-100 \ 235 + break-all"> 236 + (string blob.cid) 237 + </p> 238 + </div> 239 + <p className="text-xs text-phoenix-100"> 240 + (string 241 + "This action cannot be undone and may break \ 242 + references to this blob." ) 243 + </p> 244 + <div className="flex gap-3 mt-2"> 245 + <Button 246 + kind=`Danger formMethod="post" type_="submit"> 247 + (string "delete") 248 + </Button> 249 + <Button 250 + kind=`Tertiary 251 + onClick=(fun _ -> 252 + setDeleteConfirmFor (fun _ -> None) )> 253 + (string "cancel") 254 + </Button> 255 + </div> 256 + </form> 257 + | None -> 258 + null ) 259 + </Aria.Dialog> 260 + </Aria.Modal> 261 + </Aria.ModalOverlay> 262 + </Aria.DialogTrigger>] 203 263 </ClientOnly> 204 264 </main> 205 265 </div>
+244 -196
frontend/src/templates/AdminInvitesPage.mlx
··· 3 3 open Melange_json.Primitives 4 4 open React 5 5 6 - type invite = 7 - { code: string 8 - ; did: string 9 - ; remaining: int } 10 - [@@deriving json] 6 + type invite = {code: string; did: string; remaining: int} [@@deriving json] 11 7 12 8 type props = 13 9 { invites: invite list ··· 17 13 [@@deriving json] 18 14 19 15 let[@react.component] make 20 - ~props: 21 - ({ invites 22 - ; csrf_token 23 - ; error 24 - ; success } : 25 - props ) () = 16 + ~props:({invites; csrf_token; error; success} : props) () = 26 17 (* create invite modal state *) 27 18 let createModalOpen, setCreateModalOpen = useState (fun () -> false) in 28 19 let newCode, setNewCode = useState (fun () -> "") in 29 20 let newDid, setNewDid = useState (fun () -> "") in 30 21 let newRemaining, setNewRemaining = useState (fun () -> "1") in 31 22 (* edit modal state *) 32 - let editModalFor, setEditModalFor = useState (fun () -> (None : invite option)) in 23 + let editModalFor, setEditModalFor = 24 + useState (fun () -> (None : invite option)) 25 + in 33 26 let editDid, setEditDid = useState (fun () -> "admin") in 34 27 let editRemaining, setEditRemaining = useState (fun () -> "") in 35 28 (* delete confirmation state *) 36 - let deleteConfirmFor, setDeleteConfirmFor = useState (fun () -> (None : invite option)) in 37 - <div className="w-full h-full max-w-4xl px-4 pt-16 mx-auto flex flex-col md:flex-row gap-12"> 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"> 38 35 <AdminSidebar active_page="/admin/invites" /> 39 36 <main className="flex-1 w-full"> 40 37 <h1 className="text-2xl font-serif text-mana-200 mb-1"> ··· 44 41 (string "Manage invite codes for new account registration.") 45 42 </p> 46 43 <div className="flex flex-col sm:flex-row gap-4 mb-6"> 47 - <ClientOnly fallback=( 48 - <Button kind=`Primary className="w-full sm:max-w-64"> 49 - (string "create invite code") 50 - </Button> 51 - )> 52 - [%browser_only 53 - (fun () -> 54 - let module Aria = ReactAria in 55 - <Aria.DialogTrigger 56 - isOpen=createModalOpen 57 - onOpenChange=(fun o -> setCreateModalOpen (fun _ -> o))> 58 - <Aria.Pressable> 59 - <Button 60 - kind=`Primary 61 - className="w-full sm:max-w-64" 62 - onClick=(fun _ -> setCreateModalOpen (fun _ -> true))> 63 - (string "create invite code") 64 - </Button> 65 - </Aria.Pressable> 66 - <Aria.ModalOverlay 67 - className="fixed inset-0 z-50 bg-mist-80/80 flex items-center justify-center" 68 - isDismissable=true> 69 - <Aria.Modal 70 - className="bg-feather-100 border border-mist-60 rounded-xl px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 71 - <Aria.Dialog className="outline-none"> 72 - <Aria.Heading 73 - slot="title" 74 - className="text-lg font-serif text-mana-200 mb-2"> 44 + <ClientOnly 45 + fallback=(<Button kind=`Primary className="w-full sm:max-w-64"> 75 46 (string "create invite code") 76 - </Aria.Heading> 77 - <p className="text-mist-100 mb-4"> 78 - (string "Create a new invite code for user registration.") 79 - </p> 80 - <form className="flex flex-col gap-y-3"> 81 - <input type_="hidden" name="dream.csrf" value=csrf_token /> 82 - <input type_="hidden" name="action" value="create_invite" /> 83 - <Input 84 - name="new_code" 85 - label="Code (optional)" 86 - placeholder="Leave empty to generate" 87 - showIndicator=false 88 - value=newCode 89 - onChange=(fun e -> 90 - setNewCode (fun _ -> (Event.Form.target e)##value)) 91 - /> 92 - <Input 93 - name="did" 94 - label="For (DID)" 95 - placeholder="admin" 96 - showIndicator=false 97 - value=newDid 98 - onChange=(fun e -> 99 - setNewDid (fun _ -> (Event.Form.target e)##value)) 100 - /> 101 - <Input 102 - name="remaining" 103 - type_="number" 104 - label="Available uses" 105 - showIndicator=false 106 - value=newRemaining 107 - onChange=(fun e -> 108 - setNewRemaining (fun _ -> (Event.Form.target e)##value)) 109 - /> 110 - <Button formMethod="post" type_="submit" className="mt-2"> 111 - (string "create") 112 - </Button> 113 - </form> 114 - </Aria.Dialog> 115 - </Aria.Modal> 116 - </Aria.ModalOverlay> 117 - </Aria.DialogTrigger> )] 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>] 118 124 </ClientOnly> 119 125 </div> 120 126 ( match error with ··· 124 130 <CircleAlertIcon className="w-4 h-4 mr-2" /> (string err) 125 131 </span> 126 132 </div> 127 - | None -> null ) 133 + | None -> 134 + null ) 128 135 ( match success with 129 136 | Some msg -> 130 137 <div className="mb-4"> ··· 132 139 <CheckmarkIcon className="w-4 h-4 mr-2" /> (string msg) 133 140 </span> 134 141 </div> 135 - | None -> null ) 142 + | None -> 143 + null ) 136 144 <div className="overflow-x-auto"> 137 145 <table 138 146 className="w-full grid border-collapse text-sm" 139 - style=(ReactDOM.Style.make ~gridTemplateColumns:"minmax(6rem, 1fr) 1fr minmax(2rem, 1fr) 4rem" ())> 147 + style=(ReactDOM.Style.make 148 + ~gridTemplateColumns: 149 + "minmax(6rem, 1fr) 1fr minmax(2rem, 1fr) 4rem" 150 + () )> 140 151 <thead className="contents"> 141 152 <tr className="contents text-left text-mist-80"> 142 - <th className="border-b border-mist-60/50 pb-2 font-normal">(string "Code")</th> 143 - <th className="border-b border-mist-60/50 pb-2 font-normal">(string "For")</th> 144 - <th className="border-b border-mist-60/50 pb-2 font-normal">(string "Remaining")</th> 145 - <th className="border-b border-mist-60/50 pb-2 font-normal w-20"></th> 153 + <th className="border-b border-mist-60/50 pb-2 font-normal"> 154 + (string "Code") 155 + </th> 156 + <th className="border-b border-mist-60/50 pb-2 font-normal"> 157 + (string "For") 158 + </th> 159 + <th className="border-b border-mist-60/50 pb-2 font-normal"> 160 + (string "Remaining") 161 + </th> 162 + <th className="border-b border-mist-60/50 pb-2 font-normal w-20" 163 + /> 146 164 </tr> 147 165 </thead> 148 166 <tbody className="contents"> ··· 163 181 <td className="py-3"> 164 182 <div className="flex gap-2"> 165 183 <button 166 - className="p-1 text-mist-80 hover:text-mana-100 cursor-pointer" 184 + className="p-1 text-mist-80 hover:text-mana-100 \ 185 + cursor-pointer" 167 186 onClick=(fun _ -> 168 - setEditDid (fun _ -> invite.did) ; 169 - setEditRemaining (fun _ -> string_of_int invite.remaining) ; 170 - setEditModalFor (fun _ -> Some invite))> 187 + setEditDid (fun _ -> invite.did) ; 188 + setEditRemaining (fun _ -> 189 + string_of_int invite.remaining ) ; 190 + setEditModalFor (fun _ -> Some invite) )> 171 191 <PencilLineIcon className="w-4 h-4" /> 172 192 </button> 173 193 <button 174 - className="p-1 text-mist-80 hover:text-phoenix-100 cursor-pointer" 175 - onClick=(fun _ -> setDeleteConfirmFor (fun _ -> Some invite))> 194 + className="p-1 text-mist-80 hover:text-phoenix-100 \ 195 + cursor-pointer" 196 + onClick=(fun _ -> 197 + setDeleteConfirmFor (fun _ -> Some invite) )> 176 198 <TrashIcon className="w-4 h-4" /> 177 199 </button> 178 200 </div> ··· 183 205 </tbody> 184 206 </table> 185 207 </div> 186 - (* edit modal *) 187 208 <ClientOnly fallback=null> 188 - [%browser_only 189 - (fun () -> 190 - let module Aria = ReactAria in 191 - <Aria.DialogTrigger 192 - isOpen=(editModalFor <> None) 193 - onOpenChange=(fun o -> if not o then setEditModalFor (fun _ -> None))> 194 - <Aria.ModalOverlay 195 - className="fixed inset-0 z-50 bg-mist-80/80 flex items-center justify-center" 196 - isDismissable=true> 197 - <Aria.Modal 198 - className="bg-feather-100 border border-mist-60 rounded-xl px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 199 - <Aria.Dialog className="outline-none"> 200 - ( match editModalFor with 201 - | Some invite -> 202 - <form className="flex flex-col gap-y-3"> 203 - <Aria.Heading 204 - slot="title" 205 - className="text-lg font-serif text-mana-200 mb-2"> 206 - (string "edit invite code") 207 - </Aria.Heading> 208 - <p className="text-mist-80 text-sm"> 209 - (string ("Code: " ^ invite.code)) 210 - </p> 211 - <input type_="hidden" name="dream.csrf" value=csrf_token /> 212 - <input type_="hidden" name="action" value="update_invite" /> 213 - <input type_="hidden" name="code" value=invite.code /> 214 - <Input 215 - name="did" 216 - label="For (DID)" 217 - showIndicator=false 218 - value=editDid 219 - onChange=(fun e -> 220 - setEditDid (fun _ -> (Event.Form.target e)##value)) 221 - /> 222 - <Input 223 - name="remaining" 224 - type_="number" 225 - label="Remaining uses" 226 - showIndicator=false 227 - value=editRemaining 228 - onChange=(fun e -> 229 - setEditRemaining (fun _ -> (Event.Form.target e)##value)) 230 - /> 231 - <div className="flex gap-3 mt-2"> 232 - <Button formMethod="post" type_="submit">(string "save")</Button> 233 - <Button 234 - kind=`Tertiary 235 - onClick=(fun _ -> setEditModalFor (fun _ -> None))> 236 - (string "cancel") 237 - </Button> 238 - </div> 239 - </form> 240 - | None -> null ) 241 - </Aria.Dialog> 242 - </Aria.Modal> 243 - </Aria.ModalOverlay> 244 - </Aria.DialogTrigger> )] 209 + (* edit modal *) 210 + [%browser_only 211 + fun () -> 212 + let module Aria = ReactAria in 213 + <Aria.DialogTrigger 214 + isOpen=(editModalFor <> None) 215 + onOpenChange=(fun o -> 216 + if not o then setEditModalFor (fun _ -> None) )> 217 + <Aria.ModalOverlay 218 + className="fixed inset-0 z-50 bg-mist-80/80 flex items-center \ 219 + justify-center" 220 + isDismissable=true> 221 + <Aria.Modal 222 + className="bg-feather-100 border border-mist-60 rounded-xl \ 223 + px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 224 + <Aria.Dialog className="outline-none"> 225 + ( match editModalFor with 226 + | Some invite -> 227 + <form className="flex flex-col gap-y-3"> 228 + <Aria.Heading 229 + slot="title" 230 + className="text-lg font-serif text-mana-200 mb-2"> 231 + (string "edit invite code") 232 + </Aria.Heading> 233 + <p className="text-mist-80 text-sm"> 234 + (string ("Code: " ^ invite.code)) 235 + </p> 236 + <input 237 + type_="hidden" name="dream.csrf" value=csrf_token 238 + /> 239 + <input 240 + type_="hidden" name="action" value="update_invite" 241 + /> 242 + <input type_="hidden" name="code" value=invite.code /> 243 + <Input 244 + name="did" 245 + label="For (DID)" 246 + showIndicator=false 247 + value=editDid 248 + onChange=(fun e -> 249 + setEditDid (fun _ -> 250 + (Event.Form.target e)##value ) ) 251 + /> 252 + <Input 253 + name="remaining" 254 + type_="number" 255 + label="Remaining uses" 256 + showIndicator=false 257 + value=editRemaining 258 + onChange=(fun e -> 259 + setEditRemaining (fun _ -> 260 + (Event.Form.target e)##value ) ) 261 + /> 262 + <div className="flex gap-3 mt-2"> 263 + <Button formMethod="post" type_="submit"> 264 + (string "save") 265 + </Button> 266 + <Button 267 + kind=`Tertiary 268 + onClick=(fun _ -> setEditModalFor (fun _ -> None))> 269 + (string "cancel") 270 + </Button> 271 + </div> 272 + </form> 273 + | None -> 274 + null ) 275 + </Aria.Dialog> 276 + </Aria.Modal> 277 + </Aria.ModalOverlay> 278 + </Aria.DialogTrigger>] 245 279 </ClientOnly> 246 - (* delete confirmation modal *) 247 280 <ClientOnly fallback=null> 248 - [%browser_only 249 - (fun () -> 250 - let module Aria = ReactAria in 251 - <Aria.DialogTrigger 252 - isOpen=(deleteConfirmFor <> None) 253 - onOpenChange=(fun o -> if not o then setDeleteConfirmFor (fun _ -> None))> 254 - <Aria.ModalOverlay 255 - className="fixed inset-0 z-50 bg-mist-80/80 flex items-center justify-center" 256 - isDismissable=true> 257 - <Aria.Modal 258 - className="bg-feather-100 border border-mist-60 rounded-xl px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 259 - <Aria.Dialog className="outline-none"> 260 - ( match deleteConfirmFor with 261 - | Some invite -> 262 - <form className="flex flex-col gap-y-3"> 263 - <Aria.Heading 264 - slot="title" 265 - className="text-lg font-serif text-mana-200 mb-2"> 266 - (string "delete invite code") 267 - </Aria.Heading> 268 - <input type_="hidden" name="dream.csrf" value=csrf_token /> 269 - <input type_="hidden" name="action" value="delete_invite" /> 270 - <input type_="hidden" name="code" value=invite.code /> 271 - <p className="text-mist-100"> 272 - (string ("Are you sure you want to delete invite code " ^ invite.code ^ "?")) 273 - </p> 274 - <div className="flex gap-3 mt-2"> 275 - <Button kind=`Danger formMethod="post" type_="submit">(string "delete")</Button> 276 - <Button 277 - kind=`Tertiary 278 - onClick=(fun _ -> setDeleteConfirmFor (fun _ -> None))> 279 - (string "cancel") 280 - </Button> 281 - </div> 282 - </form> 283 - | None -> null ) 284 - </Aria.Dialog> 285 - </Aria.Modal> 286 - </Aria.ModalOverlay> 287 - </Aria.DialogTrigger> )] 281 + (* delete confirmation modal *) 282 + [%browser_only 283 + fun () -> 284 + let module Aria = ReactAria in 285 + <Aria.DialogTrigger 286 + isOpen=(deleteConfirmFor <> None) 287 + onOpenChange=(fun o -> 288 + if not o then setDeleteConfirmFor (fun _ -> None) )> 289 + <Aria.ModalOverlay 290 + className="fixed inset-0 z-50 bg-mist-80/80 flex items-center \ 291 + justify-center" 292 + isDismissable=true> 293 + <Aria.Modal 294 + className="bg-feather-100 border border-mist-60 rounded-xl \ 295 + px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 296 + <Aria.Dialog className="outline-none"> 297 + ( match deleteConfirmFor with 298 + | Some invite -> 299 + <form className="flex flex-col gap-y-3"> 300 + <Aria.Heading 301 + slot="title" 302 + className="text-lg font-serif text-mana-200 mb-2"> 303 + (string "delete invite code") 304 + </Aria.Heading> 305 + <input 306 + type_="hidden" name="dream.csrf" value=csrf_token 307 + /> 308 + <input 309 + type_="hidden" name="action" value="delete_invite" 310 + /> 311 + <input type_="hidden" name="code" value=invite.code /> 312 + <p className="text-mist-100"> 313 + (string 314 + ( "Are you sure you want to delete invite code " 315 + ^ invite.code ^ "?" ) ) 316 + </p> 317 + <div className="flex gap-3 mt-2"> 318 + <Button 319 + kind=`Danger formMethod="post" type_="submit"> 320 + (string "delete") 321 + </Button> 322 + <Button 323 + kind=`Tertiary 324 + onClick=(fun _ -> 325 + setDeleteConfirmFor (fun _ -> None) )> 326 + (string "cancel") 327 + </Button> 328 + </div> 329 + </form> 330 + | None -> 331 + null ) 332 + </Aria.Dialog> 333 + </Aria.Modal> 334 + </Aria.ModalOverlay> 335 + </Aria.DialogTrigger>] 288 336 </ClientOnly> 289 337 </main> 290 338 </div>
+564 -414
frontend/src/templates/AdminUsersPage.mlx
··· 31 31 try 32 32 for i = 0 to len1 - len2 do 33 33 if String.sub s1 i len2 = s2 then raise Exit 34 - done; 34 + done ; 35 35 false 36 36 with Exit -> true 37 37 ··· 39 39 ~props: 40 40 ({ actors 41 41 ; csrf_token 42 - ; filter=initial_filter 42 + ; filter= initial_filter 43 43 ; cursor 44 44 ; next_cursor 45 45 ; hostname ··· 52 52 let newEmail, setNewEmail = useState (fun () -> "") in 53 53 let newPassword, setNewPassword = useState (fun () -> "") in 54 54 (* action menu state *) 55 - let menuOpenFor, setMenuOpenFor = useState (fun () -> (None : string option)) in 55 + let menuOpenFor, setMenuOpenFor = 56 + useState (fun () -> (None : string option)) 57 + in 56 58 (* edit modal state, tracks relevant actor and action *) 57 - let editModal, setEditModal = useState (fun () -> (None : (actor * string) option)) in 59 + let editModal, setEditModal = 60 + useState (fun () -> (None : (actor * string) option)) 61 + in 58 62 let editValue, setEditValue = useState (fun () -> "") in 59 63 (* delete confirmation state *) 60 - let deleteConfirmFor, setDeleteConfirmFor = useState (fun () -> (None : actor option)) in 61 - <div className="w-full h-full max-w-4xl px-4 pt-16 mx-auto flex flex-col md:flex-row gap-12"> 64 + let deleteConfirmFor, setDeleteConfirmFor = 65 + useState (fun () -> (None : actor option)) 66 + in 67 + <div 68 + className="w-full h-full max-w-4xl px-4 pt-16 mx-auto flex flex-col \ 69 + md:flex-row gap-12"> 62 70 <AdminSidebar active_page="/admin/users" /> 63 71 <main className="flex-1 w-full"> 64 72 <h1 className="text-2xl font-serif text-mana-200 mb-1"> ··· 73 81 placeholder="filter users" 74 82 showIndicator=false 75 83 value=filter 76 - onChange=(fun e -> setFilter (fun _ -> (Obj.magic (Event.Form.target e))##value)) 84 + onChange=(fun e -> 85 + setFilter (fun _ -> 86 + (Obj.magic (Event.Form.target e))##value ) ) 77 87 /> 78 - <ClientOnly fallback=( 79 - <Button kind=`Primary className="w-full sm:max-w-64"> 80 - (string "create account") 81 - </Button> 82 - )> 83 - [%browser_only 84 - (fun () -> 85 - let module Aria = ReactAria in 86 - <Aria.DialogTrigger 87 - isOpen=createModalOpen 88 - onOpenChange=(fun o -> setCreateModalOpen (fun _ -> o))> 89 - <Aria.Pressable> 90 - <Button 91 - kind=`Primary 92 - className="w-full sm:max-w-64" 93 - onClick=(fun _ -> setCreateModalOpen (fun _ -> true))> 94 - (string "create account") 95 - </Button> 96 - </Aria.Pressable> 97 - <Aria.ModalOverlay 98 - className="fixed inset-0 z-50 bg-mist-80/80 flex items-center justify-center" 99 - isDismissable=true> 100 - <Aria.Modal 101 - className="bg-feather-100 border border-mist-60 rounded-xl px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 102 - <Aria.Dialog className="outline-none"> 103 - <Aria.Heading 104 - slot="title" 105 - className="text-lg font-serif text-mana-200 mb-2"> 88 + <ClientOnly 89 + fallback=(<Button kind=`Primary className="w-full sm:max-w-64"> 106 90 (string "create account") 107 - </Aria.Heading> 108 - <p className="text-mist-100 mb-4"> 109 - (string "Quickly create a new account on this PDS.") 110 - </p> 111 - <form className="flex flex-col gap-y-3"> 112 - <input type_="hidden" name="dream.csrf" value=csrf_token /> 113 - <input type_="hidden" name="action" value="create_account" /> 114 - <Input 115 - name="email" 116 - type_="email" 117 - label="Email" 118 - required=true 119 - showIndicator=false 120 - value=newEmail 121 - onChange=(fun e -> 122 - setNewEmail (fun _ -> (Event.Form.target e)##value)) 123 - /> 124 - <HandleInput 125 - name="handle" 126 - label="Handle" 127 - required=true 128 - showIndicator=false 129 - hostname 130 - /> 131 - <Input 132 - name="password" 133 - type_="password" 134 - label="Password" 135 - required=true 136 - showIndicator=false 137 - value=newPassword 138 - onChange=(fun e -> 139 - setNewPassword (fun _ -> (Event.Form.target e)##value)) 140 - /> 141 - <Button formMethod="post" type_="submit" className="mt-2"> 142 - (string "create") 143 - </Button> 144 - </form> 145 - </Aria.Dialog> 146 - </Aria.Modal> 147 - </Aria.ModalOverlay> 148 - </Aria.DialogTrigger> )] 91 + </Button>)> 92 + [%browser_only 93 + fun () -> 94 + let module Aria = ReactAria in 95 + <Aria.DialogTrigger 96 + isOpen=createModalOpen 97 + onOpenChange=(fun o -> setCreateModalOpen (fun _ -> o))> 98 + <Aria.Pressable> 99 + <Button 100 + kind=`Primary 101 + className="w-full sm:max-w-64" 102 + onClick=(fun _ -> setCreateModalOpen (fun _ -> true))> 103 + (string "create account") 104 + </Button> 105 + </Aria.Pressable> 106 + <Aria.ModalOverlay 107 + className="fixed inset-0 z-50 bg-mist-80/80 flex \ 108 + items-center justify-center" 109 + isDismissable=true> 110 + <Aria.Modal 111 + className="bg-feather-100 border border-mist-60 rounded-xl \ 112 + px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 113 + <Aria.Dialog className="outline-none"> 114 + <Aria.Heading 115 + slot="title" 116 + className="text-lg font-serif text-mana-200 mb-2"> 117 + (string "create account") 118 + </Aria.Heading> 119 + <p className="text-mist-100 mb-4"> 120 + (string "Quickly create a new account on this PDS.") 121 + </p> 122 + <form className="flex flex-col gap-y-3"> 123 + <input type_="hidden" name="dream.csrf" value=csrf_token 124 + /> 125 + <input 126 + type_="hidden" name="action" value="create_account" 127 + /> 128 + <Input 129 + name="email" 130 + type_="email" 131 + label="Email" 132 + required=true 133 + showIndicator=false 134 + value=newEmail 135 + onChange=(fun e -> 136 + setNewEmail (fun _ -> 137 + (Event.Form.target e)##value ) ) 138 + /> 139 + <HandleInput 140 + name="handle" 141 + label="Handle" 142 + required=true 143 + showIndicator=false 144 + hostname 145 + /> 146 + <Input 147 + name="password" 148 + type_="password" 149 + label="Password" 150 + required=true 151 + showIndicator=false 152 + value=newPassword 153 + onChange=(fun e -> 154 + setNewPassword (fun _ -> 155 + (Event.Form.target e)##value ) ) 156 + /> 157 + <Button 158 + formMethod="post" type_="submit" className="mt-2"> 159 + (string "create") 160 + </Button> 161 + </form> 162 + </Aria.Dialog> 163 + </Aria.Modal> 164 + </Aria.ModalOverlay> 165 + </Aria.DialogTrigger>] 149 166 </ClientOnly> 150 167 </div> 151 168 ( match error with ··· 155 172 <CircleAlertIcon className="w-4 h-4 mr-2" /> (string err) 156 173 </span> 157 174 </div> 158 - | None -> null ) 175 + | None -> 176 + null ) 159 177 ( match success with 160 178 | Some msg -> 161 179 <div className="mb-4"> ··· 163 181 <CheckmarkIcon className="w-4 h-4 mr-2" /> (string msg) 164 182 </span> 165 183 </div> 166 - | None -> null ) 184 + | None -> 185 + null ) 167 186 <div className="overflow-x-auto"> 168 187 <table 169 188 className="w-full min-w-xl grid border-collapse text-sm" 170 - style=(ReactDOM.Style.make ~gridTemplateColumns:"minmax(200px, 2fr) minmax(160px, 2fr) minmax(160px, 1.5fr) 1.25rem" ())> 189 + style=(ReactDOM.Style.make 190 + ~gridTemplateColumns: 191 + "minmax(200px, 2fr) minmax(160px, 2fr) minmax(160px, \ 192 + 1.5fr) 1.25rem" 193 + () )> 171 194 <thead className="contents"> 172 195 <tr className="contents text-left text-mist-80"> 173 - <th className="border-b border-mist-60/50 pb-2 font-normal">(string "Handle")</th> 174 - <th className="border-b border-mist-60/50 pb-2 font-normal">(string "Email")</th> 175 - <th className="border-b border-mist-60/50 pb-2 font-normal">(string "Created at")</th> 176 - <th className="border-b border-mist-60/50 pb-2 font-normal w-8"></th> 196 + <th className="border-b border-mist-60/50 pb-2 font-normal"> 197 + (string "Handle") 198 + </th> 199 + <th className="border-b border-mist-60/50 pb-2 font-normal"> 200 + (string "Email") 201 + </th> 202 + <th className="border-b border-mist-60/50 pb-2 font-normal"> 203 + (string "Created at") 204 + </th> 205 + <th className="border-b border-mist-60/50 pb-2 font-normal w-8" /> 177 206 </tr> 178 207 </thead> 179 208 <tbody className="contents"> 180 209 ( List.map 181 210 (fun (actor : actor) -> 182 - if filter <> "" 183 - && not (contains_str actor.handle filter) 184 - && not (contains_str actor.did filter) 185 - && not (contains_str actor.email filter) then null else 186 - let handleClasses = "font-medium truncate " ^ 187 - (if actor.deactivated then "text-phoenix-100" else "text-mana-100") in 188 - <tr key=actor.did className="contents"> 189 - <td className="py-3 pr-4"> 190 - <div className="flex items-center gap-3"> 191 - <div className="max-w-54 truncate"> 192 - <p className=handleClasses ?title=(if actor.deactivated then Some "Deactivated" else None)> 193 - (string actor.handle) 194 - </p> 195 - <p className="text-xs text-mist-80 truncate"> 196 - (string actor.did) 197 - </p> 211 + if 212 + filter <> "" 213 + && (not (contains_str actor.handle filter)) 214 + && (not (contains_str actor.did filter)) 215 + && not (contains_str actor.email filter) 216 + then null 217 + else 218 + let handleClasses = 219 + "font-medium truncate " 220 + ^ 221 + if actor.deactivated then "text-phoenix-100" 222 + else "text-mana-100" 223 + in 224 + <tr key=actor.did className="contents"> 225 + <td className="py-3 pr-4"> 226 + <div className="flex items-center gap-3"> 227 + <div className="max-w-54 truncate"> 228 + <p 229 + className=handleClasses 230 + ?title=( if actor.deactivated then 231 + Some "Deactivated" 232 + else None )> 233 + (string actor.handle) 234 + </p> 235 + <p className="text-xs text-mist-80 truncate"> 236 + (string actor.did) 237 + </p> 238 + </div> 239 + </div> 240 + </td> 241 + <td className="py-3 pr-4"> 242 + <div className="flex items-center gap-1"> 243 + <span className="text-mist-100 truncate"> 244 + (string actor.email) 245 + </span> 246 + ( if actor.email_confirmed then 247 + <CheckmarkIcon 248 + className="flex-shrink-0 w-4 h-4 text-mana-100" 249 + /> 250 + else 251 + <XIcon 252 + className="flex-shrink-0 w-4 h-4 \ 253 + text-phoenix-100" /> ) 198 254 </div> 199 - </div> 200 - </td> 201 - <td className="py-3 pr-4"> 202 - <div className="flex items-center gap-1"> 203 - <span className="text-mist-100 truncate">(string actor.email)</span> 204 - ( if actor.email_confirmed then 205 - <CheckmarkIcon className="flex-shrink-0 w-4 h-4 text-mana-100" /> 206 - else 207 - <XIcon className="flex-shrink-0 w-4 h-4 text-phoenix-100" /> ) 208 - </div> 209 - </td> 210 - <td className="py-3 pr-4 text-mist-100 truncate"> 211 - (string actor.created_at) 212 - </td> 213 - <td className="py-3"> 214 - <ClientOnly fallback=( 215 - <button className="text-mist-80 hover:text-mana-100"> 216 - <EllipsisIcon className="w-5 h-5" /> 217 - </button> 218 - )> 219 - [%browser_only 220 - (fun () -> 221 - let module Aria = ReactAria in 222 - let isOpen = menuOpenFor = Some actor.did in 223 - <Aria.MenuTrigger 224 - isOpen=isOpen 225 - onOpenChange=(fun o -> 226 - setMenuOpenFor (fun _ -> if o then Some actor.did else None))> 227 - <Aria.Pressable> 228 - <button 229 - className="text-mist-80 hover:text-mana-100 cursor-pointer" 230 - onClick=(fun _ -> setMenuOpenFor (fun _ -> Some actor.did))> 231 - <EllipsisIcon className="w-5 h-5" /> 232 - </button> 233 - </Aria.Pressable> 234 - <Aria.Popover 235 - className="bg-feather-100 border border-mist-60/50 rounded-lg shadow-xl py-1 min-w-48"> 236 - <Aria.Menu 237 - className="outline-none" 238 - onAction=(fun action -> 239 - setMenuOpenFor (fun _ -> None) ; 240 - match action with 241 - | "change_handle" -> 242 - setEditValue (fun _ -> actor.handle) ; 243 - setEditModal (fun _ -> Some (actor, "handle")) 244 - | "change_email" -> 245 - setEditValue (fun _ -> actor.email) ; 246 - setEditModal (fun _ -> Some (actor, "email")) 247 - | "change_password" -> 248 - setEditValue (fun _ -> "") ; 249 - setEditModal (fun _ -> Some (actor, "password")) 250 - | "send_password_reset" -> 251 - setEditModal (fun _ -> Some (actor, "send_reset")) 252 - | "deactivate" -> 253 - setEditModal (fun _ -> Some (actor, "deactivate")) 254 - | "reactivate" -> 255 - setEditModal (fun _ -> Some (actor, "reactivate")) 256 - | "delete" -> 257 - setDeleteConfirmFor (fun _ -> Some actor) 258 - | _ -> () )> 259 - <Aria.MenuItem 260 - id="change_handle" 261 - className="px-3 py-2 outline-none cursor-pointer hover:bg-mist-60/30 text-mist-100"> 262 - (string "Change handle") 263 - </Aria.MenuItem> 264 - <Aria.MenuItem 265 - id="change_email" 266 - className="px-3 py-2 outline-none cursor-pointer hover:bg-mist-60/30 text-mist-100"> 267 - (string "Change email") 268 - </Aria.MenuItem> 269 - <Aria.MenuItem 270 - id="change_password" 271 - className="px-3 py-2 outline-none cursor-pointer hover:bg-mist-60/30 text-mist-100"> 272 - (string "Change password") 273 - </Aria.MenuItem> 274 - <Aria.MenuItem 275 - id="send_password_reset" 276 - className="px-3 py-2 outline-none cursor-pointer hover:bg-mist-60/30 text-mist-100"> 277 - (string "Send password reset code") 278 - </Aria.MenuItem> 279 - <Aria.Separator className="my-1 border-t border-mist-60/50" /> 280 - ( if actor.deactivated then 255 + </td> 256 + <td className="py-3 pr-4 text-mist-100 truncate"> 257 + (string actor.created_at) 258 + </td> 259 + <td className="py-3"> 260 + <ClientOnly 261 + fallback=(<button 262 + className="text-mist-80 \ 263 + hover:text-mana-100"> 264 + <EllipsisIcon className="w-5 h-5" /> 265 + </button>)> 266 + [%browser_only 267 + fun () -> 268 + let module Aria = ReactAria in 269 + let isOpen = menuOpenFor = Some actor.did in 270 + <Aria.MenuTrigger 271 + isOpen 272 + onOpenChange=(fun o -> 273 + setMenuOpenFor (fun _ -> 274 + if o then Some actor.did 275 + else None ) )> 276 + <Aria.Pressable> 277 + <button 278 + className="text-mist-80 \ 279 + hover:text-mana-100 \ 280 + cursor-pointer" 281 + onClick=(fun _ -> 282 + setMenuOpenFor (fun _ -> 283 + Some actor.did ) )> 284 + <EllipsisIcon className="w-5 h-5" /> 285 + </button> 286 + </Aria.Pressable> 287 + <Aria.Popover 288 + className="bg-feather-100 border \ 289 + border-mist-60/50 rounded-lg \ 290 + shadow-xl py-1 min-w-48"> 291 + <Aria.Menu 292 + className="outline-none" 293 + onAction=(fun action -> 294 + setMenuOpenFor (fun _ -> None) ; 295 + match action with 296 + | "change_handle" -> 297 + setEditValue (fun _ -> 298 + actor.handle ) ; 299 + setEditModal (fun _ -> 300 + Some (actor, "handle") ) 301 + | "change_email" -> 302 + setEditValue (fun _ -> 303 + actor.email ) ; 304 + setEditModal (fun _ -> 305 + Some (actor, "email") ) 306 + | "change_password" -> 307 + setEditValue (fun _ -> "") ; 308 + setEditModal (fun _ -> 309 + Some (actor, "password") ) 310 + | "send_password_reset" -> 311 + setEditModal (fun _ -> 312 + Some (actor, "send_reset") ) 313 + | "deactivate" -> 314 + setEditModal (fun _ -> 315 + Some (actor, "deactivate") ) 316 + | "reactivate" -> 317 + setEditModal (fun _ -> 318 + Some (actor, "reactivate") ) 319 + | "delete" -> 320 + setDeleteConfirmFor (fun _ -> 321 + Some actor ) 322 + | _ -> 323 + () )> 281 324 <Aria.MenuItem 282 - id="reactivate" 283 - className="px-3 py-2 outline-none cursor-pointer hover:bg-mist-60/30 text-mana-100"> 284 - (string "Reactivate account") 325 + id="change_handle" 326 + className="px-3 py-2 outline-none \ 327 + cursor-pointer \ 328 + hover:bg-mist-60/30 \ 329 + text-mist-100"> 330 + (string "Change handle") 285 331 </Aria.MenuItem> 286 - else 287 332 <Aria.MenuItem 288 - id="deactivate" 289 - className="px-3 py-2 outline-none cursor-pointer hover:bg-mist-60/30 text-phoenix-100"> 290 - (string "Deactivate account") 291 - </Aria.MenuItem> ) 292 - <Aria.MenuItem 293 - id="delete" 294 - className="px-3 py-2 outline-none cursor-pointer hover:bg-mist-60/30 text-phoenix-100"> 295 - (string "Delete account") 296 - </Aria.MenuItem> 297 - </Aria.Menu> 298 - </Aria.Popover> 299 - </Aria.MenuTrigger> )] 300 - </ClientOnly> 301 - </td> 302 - </tr> ) 333 + id="change_email" 334 + className="px-3 py-2 outline-none \ 335 + cursor-pointer \ 336 + hover:bg-mist-60/30 \ 337 + text-mist-100"> 338 + (string "Change email") 339 + </Aria.MenuItem> 340 + <Aria.MenuItem 341 + id="change_password" 342 + className="px-3 py-2 outline-none \ 343 + cursor-pointer \ 344 + hover:bg-mist-60/30 \ 345 + text-mist-100"> 346 + (string "Change password") 347 + </Aria.MenuItem> 348 + <Aria.MenuItem 349 + id="send_password_reset" 350 + className="px-3 py-2 outline-none \ 351 + cursor-pointer \ 352 + hover:bg-mist-60/30 \ 353 + text-mist-100"> 354 + (string "Send password reset code") 355 + </Aria.MenuItem> 356 + <Aria.Separator 357 + className="my-1 border-t \ 358 + border-mist-60/50" 359 + /> 360 + ( if actor.deactivated then 361 + <Aria.MenuItem 362 + id="reactivate" 363 + className="px-3 py-2 outline-none \ 364 + cursor-pointer \ 365 + hover:bg-mist-60/30 \ 366 + text-mana-100"> 367 + (string "Reactivate account") 368 + </Aria.MenuItem> 369 + else 370 + <Aria.MenuItem 371 + id="deactivate" 372 + className="px-3 py-2 outline-none \ 373 + cursor-pointer \ 374 + hover:bg-mist-60/30 \ 375 + text-phoenix-100"> 376 + (string "Deactivate account") 377 + </Aria.MenuItem> ) 378 + <Aria.MenuItem 379 + id="delete" 380 + className="px-3 py-2 outline-none \ 381 + cursor-pointer \ 382 + hover:bg-mist-60/30 \ 383 + text-phoenix-100"> 384 + (string "Delete account") 385 + </Aria.MenuItem> 386 + </Aria.Menu> 387 + </Aria.Popover> 388 + </Aria.MenuTrigger>] 389 + </ClientOnly> 390 + </td> 391 + </tr> ) 303 392 actors 304 393 |> Array.of_list |> array ) 305 394 </tbody> ··· 312 401 <Button kind=`Secondary>(string "Load more")</Button> 313 402 </a> 314 403 </div> 315 - | None -> null ) 316 - (* edit modal *) 404 + | None -> 405 + null ) 317 406 <ClientOnly fallback=null> 318 - [%browser_only 319 - (fun () -> 320 - let module Aria = ReactAria in 321 - <Aria.DialogTrigger 322 - isOpen=(editModal <> None) 323 - onOpenChange=(fun o -> if not o then setEditModal (fun _ -> None))> 324 - <Aria.ModalOverlay 325 - className="fixed inset-0 z-50 bg-mist-80/80 flex items-center justify-center" 326 - isDismissable=true> 327 - <Aria.Modal 328 - className="bg-feather-100 border border-mist-60 rounded-xl px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 329 - <Aria.Dialog className="outline-none"> 330 - ( match editModal with 331 - | Some (actor, "handle") -> 332 - <form className="flex flex-col gap-y-3"> 333 - <Aria.Heading 334 - slot="title" 335 - className="text-lg font-serif text-mana-200 mb-2"> 336 - (string "change handle") 337 - </Aria.Heading> 338 - <input type_="hidden" name="dream.csrf" value=csrf_token /> 339 - <input type_="hidden" name="action" value="change_handle" /> 340 - <input type_="hidden" name="did" value=actor.did /> 341 - <HandleInput 342 - name="handle" 343 - label="New handle" 344 - required=true 345 - showIndicator=false 346 - value=editValue 347 - onChange=(fun e -> 348 - setEditValue (fun _ -> (Event.Form.target e)##value)) 349 - /> 350 - <div className="flex gap-3 mt-2"> 351 - <Button formMethod="post" type_="submit">(string "save")</Button> 352 - <Button 353 - kind=`Tertiary 354 - onClick=(fun _ -> setEditModal (fun _ -> None))> 355 - (string "cancel") 356 - </Button> 357 - </div> 358 - </form> 359 - | Some (actor, "email") -> 360 - <form className="flex flex-col gap-y-3"> 361 - <Aria.Heading 362 - slot="title" 363 - className="text-lg font-serif text-mana-200 mb-2"> 364 - (string "change email") 365 - </Aria.Heading> 366 - <input type_="hidden" name="dream.csrf" value=csrf_token /> 367 - <input type_="hidden" name="action" value="change_email" /> 368 - <input type_="hidden" name="did" value=actor.did /> 369 - <Input 370 - name="email" 371 - type_="email" 372 - label="New email" 373 - required=true 374 - showIndicator=false 375 - value=editValue 376 - onChange=(fun e -> 377 - setEditValue (fun _ -> (Event.Form.target e)##value)) 378 - /> 379 - <div className="flex gap-3 mt-2"> 380 - <Button formMethod="post" type_="submit">(string "save")</Button> 381 - <Button 382 - kind=`Tertiary 383 - onClick=(fun _ -> setEditModal (fun _ -> None))> 384 - (string "cancel") 385 - </Button> 386 - </div> 387 - </form> 388 - | Some (actor, "password") -> 389 - <form className="flex flex-col gap-y-3"> 390 - <Aria.Heading 391 - slot="title" 392 - className="text-lg font-serif text-mana-200 mb-2"> 393 - (string "change password") 394 - </Aria.Heading> 395 - <input type_="hidden" name="dream.csrf" value=csrf_token /> 396 - <input type_="hidden" name="action" value="change_password" /> 397 - <input type_="hidden" name="did" value=actor.did /> 398 - <Input 399 - name="password" 400 - type_="password" 401 - label="New password" 402 - required=true 403 - showIndicator=false 404 - value=editValue 405 - onChange=(fun e -> 406 - setEditValue (fun _ -> (Event.Form.target e)##value)) 407 - /> 408 - <div className="flex gap-3 mt-2"> 409 - <Button formMethod="post" type_="submit">(string "save")</Button> 410 - <Button 411 - kind=`Tertiary 412 - onClick=(fun _ -> setEditModal (fun _ -> None))> 413 - (string "cancel") 414 - </Button> 415 - </div> 416 - </form> 417 - | Some (actor, "send_reset") -> 418 - <form className="flex flex-col gap-y-3"> 419 - <Aria.Heading 420 - slot="title" 421 - className="text-lg font-serif text-mana-200 mb-2"> 422 - (string "send password reset") 423 - </Aria.Heading> 424 - <input type_="hidden" name="dream.csrf" value=csrf_token /> 425 - <input type_="hidden" name="action" value="send_password_reset" /> 426 - <input type_="hidden" name="did" value=actor.did /> 427 - <p className="text-mist-100"> 428 - (string ("Send a password reset email to " ^ actor.email ^ "?")) 429 - </p> 430 - <div className="flex gap-3 mt-2"> 431 - <Button formMethod="post" type_="submit">(string "send")</Button> 432 - <Button 433 - kind=`Tertiary 434 - onClick=(fun _ -> setEditModal (fun _ -> None))> 435 - (string "cancel") 436 - </Button> 437 - </div> 438 - </form> 439 - | Some (actor, "deactivate") -> 440 - <form className="flex flex-col gap-y-3"> 441 - <Aria.Heading 442 - slot="title" 443 - className="text-lg font-serif text-mana-200 mb-2"> 444 - (string "deactivate account") 445 - </Aria.Heading> 446 - <input type_="hidden" name="dream.csrf" value=csrf_token /> 447 - <input type_="hidden" name="action" value="deactivate" /> 448 - <input type_="hidden" name="did" value=actor.did /> 449 - <p className="text-mist-100"> 450 - (string ("Deactivate " ^ actor.handle ^ "? The account can be reactivated later.")) 451 - </p> 452 - <div className="flex gap-3 mt-2"> 453 - <Button kind=`Danger formMethod="post" type_="submit">(string "deactivate")</Button> 454 - <Button 455 - kind=`Tertiary 456 - onClick=(fun _ -> setEditModal (fun _ -> None))> 457 - (string "cancel") 458 - </Button> 459 - </div> 460 - </form> 461 - | Some (actor, "reactivate") -> 462 - <form className="flex flex-col gap-y-3"> 463 - <Aria.Heading 464 - slot="title" 465 - className="text-lg font-serif text-mana-200 mb-2"> 466 - (string "reactivate account") 467 - </Aria.Heading> 468 - <input type_="hidden" name="dream.csrf" value=csrf_token /> 469 - <input type_="hidden" name="action" value="reactivate" /> 470 - <input type_="hidden" name="did" value=actor.did /> 471 - <p className="text-mist-100"> 472 - (string ("Reactivate " ^ actor.handle ^ "?")) 473 - </p> 474 - <div className="flex gap-3 mt-2"> 475 - <Button formMethod="post" type_="submit">(string "reactivate")</Button> 476 - <Button 477 - kind=`Tertiary 478 - onClick=(fun _ -> setEditModal (fun _ -> None))> 479 - (string "cancel") 480 - </Button> 481 - </div> 482 - </form> 483 - | _ -> null ) 484 - </Aria.Dialog> 485 - </Aria.Modal> 486 - </Aria.ModalOverlay> 487 - </Aria.DialogTrigger> )] 407 + (* edit modal *) 408 + [%browser_only 409 + fun () -> 410 + let module Aria = ReactAria in 411 + <Aria.DialogTrigger 412 + isOpen=(editModal <> None) 413 + onOpenChange=(fun o -> if not o then setEditModal (fun _ -> None))> 414 + <Aria.ModalOverlay 415 + className="fixed inset-0 z-50 bg-mist-80/80 flex items-center \ 416 + justify-center" 417 + isDismissable=true> 418 + <Aria.Modal 419 + className="bg-feather-100 border border-mist-60 rounded-xl \ 420 + px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 421 + <Aria.Dialog className="outline-none"> 422 + ( match editModal with 423 + | Some (actor, "handle") -> 424 + <form className="flex flex-col gap-y-3"> 425 + <Aria.Heading 426 + slot="title" 427 + className="text-lg font-serif text-mana-200 mb-2"> 428 + (string "change handle") 429 + </Aria.Heading> 430 + <input 431 + type_="hidden" name="dream.csrf" value=csrf_token 432 + /> 433 + <input 434 + type_="hidden" name="action" value="change_handle" 435 + /> 436 + <input type_="hidden" name="did" value=actor.did /> 437 + <HandleInput 438 + name="handle" 439 + label="New handle" 440 + required=true 441 + showIndicator=false 442 + value=editValue 443 + onChange=(fun e -> 444 + setEditValue (fun _ -> 445 + (Event.Form.target e)##value ) ) 446 + /> 447 + <div className="flex gap-3 mt-2"> 448 + <Button formMethod="post" type_="submit"> 449 + (string "save") 450 + </Button> 451 + <Button 452 + kind=`Tertiary 453 + onClick=(fun _ -> setEditModal (fun _ -> None))> 454 + (string "cancel") 455 + </Button> 456 + </div> 457 + </form> 458 + | Some (actor, "email") -> 459 + <form className="flex flex-col gap-y-3"> 460 + <Aria.Heading 461 + slot="title" 462 + className="text-lg font-serif text-mana-200 mb-2"> 463 + (string "change email") 464 + </Aria.Heading> 465 + <input 466 + type_="hidden" name="dream.csrf" value=csrf_token 467 + /> 468 + <input 469 + type_="hidden" name="action" value="change_email" 470 + /> 471 + <input type_="hidden" name="did" value=actor.did /> 472 + <Input 473 + name="email" 474 + type_="email" 475 + label="New email" 476 + required=true 477 + showIndicator=false 478 + value=editValue 479 + onChange=(fun e -> 480 + setEditValue (fun _ -> 481 + (Event.Form.target e)##value ) ) 482 + /> 483 + <div className="flex gap-3 mt-2"> 484 + <Button formMethod="post" type_="submit"> 485 + (string "save") 486 + </Button> 487 + <Button 488 + kind=`Tertiary 489 + onClick=(fun _ -> setEditModal (fun _ -> None))> 490 + (string "cancel") 491 + </Button> 492 + </div> 493 + </form> 494 + | Some (actor, "password") -> 495 + <form className="flex flex-col gap-y-3"> 496 + <Aria.Heading 497 + slot="title" 498 + className="text-lg font-serif text-mana-200 mb-2"> 499 + (string "change password") 500 + </Aria.Heading> 501 + <input 502 + type_="hidden" name="dream.csrf" value=csrf_token 503 + /> 504 + <input 505 + type_="hidden" name="action" value="change_password" 506 + /> 507 + <input type_="hidden" name="did" value=actor.did /> 508 + <Input 509 + name="password" 510 + type_="password" 511 + label="New password" 512 + required=true 513 + showIndicator=false 514 + value=editValue 515 + onChange=(fun e -> 516 + setEditValue (fun _ -> 517 + (Event.Form.target e)##value ) ) 518 + /> 519 + <div className="flex gap-3 mt-2"> 520 + <Button formMethod="post" type_="submit"> 521 + (string "save") 522 + </Button> 523 + <Button 524 + kind=`Tertiary 525 + onClick=(fun _ -> setEditModal (fun _ -> None))> 526 + (string "cancel") 527 + </Button> 528 + </div> 529 + </form> 530 + | Some (actor, "send_reset") -> 531 + <form className="flex flex-col gap-y-3"> 532 + <Aria.Heading 533 + slot="title" 534 + className="text-lg font-serif text-mana-200 mb-2"> 535 + (string "send password reset") 536 + </Aria.Heading> 537 + <input 538 + type_="hidden" name="dream.csrf" value=csrf_token 539 + /> 540 + <input 541 + type_="hidden" 542 + name="action" 543 + value="send_password_reset" 544 + /> 545 + <input type_="hidden" name="did" value=actor.did /> 546 + <p className="text-mist-100"> 547 + (string 548 + ( "Send a password reset email to " ^ actor.email 549 + ^ "?" ) ) 550 + </p> 551 + <div className="flex gap-3 mt-2"> 552 + <Button formMethod="post" type_="submit"> 553 + (string "send") 554 + </Button> 555 + <Button 556 + kind=`Tertiary 557 + onClick=(fun _ -> setEditModal (fun _ -> None))> 558 + (string "cancel") 559 + </Button> 560 + </div> 561 + </form> 562 + | Some (actor, "deactivate") -> 563 + <form className="flex flex-col gap-y-3"> 564 + <Aria.Heading 565 + slot="title" 566 + className="text-lg font-serif text-mana-200 mb-2"> 567 + (string "deactivate account") 568 + </Aria.Heading> 569 + <input 570 + type_="hidden" name="dream.csrf" value=csrf_token 571 + /> 572 + <input type_="hidden" name="action" value="deactivate" 573 + /> 574 + <input type_="hidden" name="did" value=actor.did /> 575 + <p className="text-mist-100"> 576 + (string 577 + ( "Deactivate " ^ actor.handle 578 + ^ "? The account can be reactivated later." ) ) 579 + </p> 580 + <div className="flex gap-3 mt-2"> 581 + <Button 582 + kind=`Danger formMethod="post" type_="submit"> 583 + (string "deactivate") 584 + </Button> 585 + <Button 586 + kind=`Tertiary 587 + onClick=(fun _ -> setEditModal (fun _ -> None))> 588 + (string "cancel") 589 + </Button> 590 + </div> 591 + </form> 592 + | Some (actor, "reactivate") -> 593 + <form className="flex flex-col gap-y-3"> 594 + <Aria.Heading 595 + slot="title" 596 + className="text-lg font-serif text-mana-200 mb-2"> 597 + (string "reactivate account") 598 + </Aria.Heading> 599 + <input 600 + type_="hidden" name="dream.csrf" value=csrf_token 601 + /> 602 + <input type_="hidden" name="action" value="reactivate" 603 + /> 604 + <input type_="hidden" name="did" value=actor.did /> 605 + <p className="text-mist-100"> 606 + (string ("Reactivate " ^ actor.handle ^ "?")) 607 + </p> 608 + <div className="flex gap-3 mt-2"> 609 + <Button formMethod="post" type_="submit"> 610 + (string "reactivate") 611 + </Button> 612 + <Button 613 + kind=`Tertiary 614 + onClick=(fun _ -> setEditModal (fun _ -> None))> 615 + (string "cancel") 616 + </Button> 617 + </div> 618 + </form> 619 + | _ -> 620 + null ) 621 + </Aria.Dialog> 622 + </Aria.Modal> 623 + </Aria.ModalOverlay> 624 + </Aria.DialogTrigger>] 488 625 </ClientOnly> 489 - (* delete confirmation modal *) 490 626 <ClientOnly fallback=null> 491 - [%browser_only 492 - (fun () -> 493 - let module Aria = ReactAria in 494 - <Aria.DialogTrigger 495 - isOpen=(deleteConfirmFor <> None) 496 - onOpenChange=(fun o -> if not o then setDeleteConfirmFor (fun _ -> None))> 497 - <Aria.ModalOverlay 498 - className="fixed inset-0 z-50 bg-mist-80/80 flex items-center justify-center" 499 - isDismissable=true> 500 - <Aria.Modal 501 - className="bg-feather-100 border border-mist-60 rounded-xl px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 502 - <Aria.Dialog className="outline-none"> 503 - ( match deleteConfirmFor with 504 - | Some actor -> 505 - <form className="flex flex-col gap-y-3"> 506 - <Aria.Heading 507 - slot="title" 508 - className="text-lg font-serif text-mana-200 mb-2"> 509 - (string "delete account") 510 - </Aria.Heading> 511 - <input type_="hidden" name="dream.csrf" value=csrf_token /> 512 - <input type_="hidden" name="action" value="delete" /> 513 - <input type_="hidden" name="did" value=actor.did /> 514 - <p className="text-mist-100"> 515 - (string ("Are you sure you want to delete " ^ actor.handle ^ "? This action cannot be undone.")) 516 - </p> 517 - <div className="flex gap-3 mt-2"> 518 - <Button kind=`Danger formMethod="post" type_="submit">(string "delete")</Button> 519 - <Button 520 - kind=`Tertiary 521 - onClick=(fun _ -> setDeleteConfirmFor (fun _ -> None))> 522 - (string "cancel") 523 - </Button> 524 - </div> 525 - </form> 526 - | None -> null ) 527 - </Aria.Dialog> 528 - </Aria.Modal> 529 - </Aria.ModalOverlay> 530 - </Aria.DialogTrigger> )] 627 + (* delete confirmation modal *) 628 + [%browser_only 629 + fun () -> 630 + let module Aria = ReactAria in 631 + <Aria.DialogTrigger 632 + isOpen=(deleteConfirmFor <> None) 633 + onOpenChange=(fun o -> 634 + if not o then setDeleteConfirmFor (fun _ -> None) )> 635 + <Aria.ModalOverlay 636 + className="fixed inset-0 z-50 bg-mist-80/80 flex items-center \ 637 + justify-center" 638 + isDismissable=true> 639 + <Aria.Modal 640 + className="bg-feather-100 border border-mist-60 rounded-xl \ 641 + px-6 pb-6 pt-5 w-full max-w-sm mx-4 shadow-xl"> 642 + <Aria.Dialog className="outline-none"> 643 + ( match deleteConfirmFor with 644 + | Some actor -> 645 + <form className="flex flex-col gap-y-3"> 646 + <Aria.Heading 647 + slot="title" 648 + className="text-lg font-serif text-mana-200 mb-2"> 649 + (string "delete account") 650 + </Aria.Heading> 651 + <input 652 + type_="hidden" name="dream.csrf" value=csrf_token 653 + /> 654 + <input type_="hidden" name="action" value="delete" /> 655 + <input type_="hidden" name="did" value=actor.did /> 656 + <p className="text-mist-100"> 657 + (string 658 + ( "Are you sure you want to delete " 659 + ^ actor.handle 660 + ^ "? This action cannot be undone." ) ) 661 + </p> 662 + <div className="flex gap-3 mt-2"> 663 + <Button 664 + kind=`Danger formMethod="post" type_="submit"> 665 + (string "delete") 666 + </Button> 667 + <Button 668 + kind=`Tertiary 669 + onClick=(fun _ -> 670 + setDeleteConfirmFor (fun _ -> None) )> 671 + (string "cancel") 672 + </Button> 673 + </div> 674 + </form> 675 + | None -> 676 + null ) 677 + </Aria.Dialog> 678 + </Aria.Modal> 679 + </Aria.ModalOverlay> 680 + </Aria.DialogTrigger>] 531 681 </ClientOnly> 532 682 </main> 533 683 </div>
+4 -2
frontend/src/templates/MigratePage.mlx
··· 340 340 341 341 module BlobProgress = struct 342 342 let[@react.component] make ~csrf_token ~did ~blobs_imported ~blobs_failed () = 343 - let form_ref : Dom.htmlFormElement Js.nullable React.ref = useRef Js.Nullable.null in 343 + let form_ref : Dom.htmlFormElement Js.nullable React.ref = 344 + useRef Js.Nullable.null 345 + in 344 346 (* auto-submit the form after a brief delay to continue importing *) 345 347 React.useEffect0 (fun () -> 346 348 let timer_id = ··· 349 351 match Js.Nullable.toOption form_ref.current with 350 352 | Some form -> 351 353 Webapi.Dom.HtmlFormElement.setAttribute "method" "post" form ; 352 - Webapi.Dom.HtmlFormElement.submit form ; 354 + Webapi.Dom.HtmlFormElement.submit form 353 355 | None -> 354 356 () ) 355 357 100
+94 -96
frontend/src/templates/OauthAuthorizePage.mlx
··· 329 329 <tbody> 330 330 ( coll_actions_list 331 331 |> List.map (fun (coll, actions) -> 332 - let star_create = 333 - Option.map (fun a -> a.create) star_actions 334 - |> Option.value ~default:false 335 - in 336 - let star_update = 337 - Option.map (fun a -> a.update) star_actions 338 - |> Option.value ~default:false 339 - in 340 - let star_delete = 341 - Option.map (fun a -> a.delete) star_actions 342 - |> Option.value ~default:false 343 - in 344 - <tr key=coll className="text-mist-100"> 345 - <td className="py-0.5"> 346 - <span className="font-medium"> 347 - (string 348 - ( if coll = "*" then "Any collection" 349 - else coll ) ) 350 - </span> 351 - </td> 352 - <td className="text-center"> 353 - ( if star_create || actions.create then 354 - <span className="text-mana-100"> 355 - (string {js|✓|js}) 356 - </span> 357 - else null ) 358 - </td> 359 - <td className="text-center"> 360 - ( if star_update || actions.update then 361 - <span className="text-mana-100"> 362 - (string {js|✓|js}) 363 - </span> 364 - else null ) 365 - </td> 366 - <td className="text-center"> 367 - ( if star_delete || actions.delete then 368 - <span className="text-mana-100"> 369 - (string {js|✓|js}) 370 - </span> 371 - else null ) 372 - </td> 373 - </tr> ) 332 + let star_create = 333 + Option.map (fun a -> a.create) star_actions 334 + |> Option.value ~default:false 335 + in 336 + let star_update = 337 + Option.map (fun a -> a.update) star_actions 338 + |> Option.value ~default:false 339 + in 340 + let star_delete = 341 + Option.map (fun a -> a.delete) star_actions 342 + |> Option.value ~default:false 343 + in 344 + <tr key=coll className="text-mist-100"> 345 + <td className="py-0.5"> 346 + <span className="font-medium"> 347 + (string 348 + ( if coll = "*" then "Any collection" 349 + else coll ) ) 350 + </span> 351 + </td> 352 + <td className="text-center"> 353 + ( if star_create || actions.create then 354 + <span className="text-mana-100"> 355 + (string {js|✓|js}) 356 + </span> 357 + else null ) 358 + </td> 359 + <td className="text-center"> 360 + ( if star_update || actions.update then 361 + <span className="text-mana-100"> 362 + (string {js|✓|js}) 363 + </span> 364 + else null ) 365 + </td> 366 + <td className="text-center"> 367 + ( if star_delete || actions.delete then 368 + <span className="text-mana-100"> 369 + (string {js|✓|js}) 370 + </span> 371 + else null ) 372 + </td> 373 + </tr> ) 374 374 |> Array.of_list |> array ) 375 375 </tbody> 376 376 </table> ··· 383 383 let aud_lxms_list = 384 384 StringMap.bindings aud_lxms_map 385 385 |> List.map (fun (aud, lxms) -> 386 - let sorted_lxms = 387 - if List.mem "*" lxms then ["*"] 388 - else List.sort String.compare lxms 389 - in 390 - (aud, sorted_lxms) ) 386 + let sorted_lxms = 387 + if List.mem "*" lxms then ["*"] 388 + else List.sort String.compare lxms 389 + in 390 + (aud, sorted_lxms) ) 391 391 |> List.sort (fun (a, _) (b, _) -> String.compare a b) 392 392 in 393 393 let has_full_access = ··· 427 427 <tbody> 428 428 ( aud_lxms_list 429 429 |> List.concat_map (fun (aud, lxms) -> 430 - let render_aud () = 431 - if aud = "*" then 432 - <span className="text-mist-100 font-medium"> 433 - (string "Any service") 434 - </span> 435 - else if 436 - String.starts_with 437 - ~prefix:"did:web:api.bsky.app#" aud 438 - then 439 - <span className="text-mist-100" title=aud> 440 - (string "Bluesky services") 441 - </span> 442 - else if 443 - String.starts_with 444 - ~prefix:"did:web:api.bsky.chat#" aud 445 - then 446 - <span className="text-mist-100" title=aud> 447 - (string "Bluesky chat services") 448 - </span> 449 - else if 450 - String.starts_with ~prefix:"did:web:" aud 451 - && String.contains aud '#' 452 - then 453 - let domain = 454 - String.sub aud 8 (String.index aud '#' - 8) 455 - in 456 - <span className="text-mist-100" title=aud> 457 - (string ("Service by " ^ domain)) 458 - </span> 459 - else 460 - <span className="text-mist-100"> 461 - (string aud) 462 - </span> 463 - in 464 - List.map 465 - (fun lxm -> 466 - <tr key=(aud ^ lxm) className="text-mist-100"> 467 - <td className="py-0.5"> 468 - <span 469 - className="text-mist-100 font-medium"> 470 - (string 471 - ( if lxm = "*" then "Any method" 472 - else lxm ) ) 473 - </span> 474 - </td> 475 - <td className="py-0.5">(render_aud ())</td> 476 - </tr> ) 477 - lxms ) 430 + let render_aud () = 431 + if aud = "*" then 432 + <span className="text-mist-100 font-medium"> 433 + (string "Any service") 434 + </span> 435 + else if 436 + String.starts_with ~prefix:"did:web:api.bsky.app#" 437 + aud 438 + then 439 + <span className="text-mist-100" title=aud> 440 + (string "Bluesky services") 441 + </span> 442 + else if 443 + String.starts_with 444 + ~prefix:"did:web:api.bsky.chat#" aud 445 + then 446 + <span className="text-mist-100" title=aud> 447 + (string "Bluesky chat services") 448 + </span> 449 + else if 450 + String.starts_with ~prefix:"did:web:" aud 451 + && String.contains aud '#' 452 + then 453 + let domain = 454 + String.sub aud 8 (String.index aud '#' - 8) 455 + in 456 + <span className="text-mist-100" title=aud> 457 + (string ("Service by " ^ domain)) 458 + </span> 459 + else 460 + <span className="text-mist-100"> 461 + (string aud) 462 + </span> 463 + in 464 + List.map 465 + (fun lxm -> 466 + <tr key=(aud ^ lxm) className="text-mist-100"> 467 + <td className="py-0.5"> 468 + <span className="text-mist-100 font-medium"> 469 + (string 470 + (if lxm = "*" then "Any method" else lxm) ) 471 + </span> 472 + </td> 473 + <td className="py-0.5">(render_aud ())</td> 474 + </tr> ) 475 + lxms ) 478 476 |> Array.of_list |> array ) 479 477 </tbody> 480 478 </table> ··· 517 515 <div className="text-sm text-mist-100"> 518 516 ( unknowns 519 517 |> List.map (fun s -> 520 - <span key=s className="block">(string s)</span> ) 518 + <span key=s className="block">(string s)</span> ) 521 519 |> Array.of_list |> array ) 522 520 </div> 523 521 </div>