+1
-1
README.md
+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
+1
-1
dune-project
-2
frontend/README.md
-2
frontend/README.md
+3
-5
frontend/src/components/AccountSidebar.mlx
+3
-5
frontend/src/components/AccountSidebar.mlx
+102
-87
frontend/src/components/AccountSwitcher.mlx
+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
+1
-5
frontend/src/components/AdminSidebar.mlx
+27
-28
frontend/src/components/HandleInput.mlx
+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
+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
+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
+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
+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
+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
+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
+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
+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>