objective categorical abstract machine language personal data server
67
fork

Configure Feed

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

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

futur.blue 6a511b27 5d757018

verified
+1872 -1520
+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>