[OCaml] High Intensity Training Online
1
open Dream_html
2
open Page_kit
3
open Page_shell
4
5
(* The account profile page. It names the signed-in trainee and offers two
6
independent forms: rename the account, and change the password. Each form
7
reports its own error inline. [notice] confirms a successful change. Both are
8
plain authenticated app-forms, so they work with or without script. *)
9
let profile request ?workout_started ~viewer ~import_open ?username_error
10
?password_error ?notice () =
11
let notice_block =
12
match notice with
13
| Some message -> [ tag "p" [ class_ "notice" ] [ txt "%s" message ] ]
14
| None -> []
15
in
16
let error_block = function Some message -> [ alert message ] | None -> [] in
17
html_page ~viewer ~request ~active:"profile" ?workout_started "Account"
18
([ tag "h1" [] [ txt "Account" ] ]
19
@ notice_block
20
@ [
21
(* Username: show the current value and a button that opens the edit
22
modal. The modal holds the rename form. A no-script client, or an
23
error re-render, opens the dialog through the [open] attribute so the
24
form stays reachable and its inline error shows. *)
25
tag "section"
26
[ class_ "profile-section" ]
27
[
28
tag "h2" [] [ txt "Username" ];
29
tag "div"
30
[ class_ "profile-value" ]
31
[
32
tag "span"
33
[ class_ "profile-value-text" ]
34
[ txt "%s" viewer.View_model.Viewer.username ];
35
tag "button"
36
[
37
type_ "button";
38
class_ "profile-edit";
39
Dream_html.string_attr "data-hito-dialog-open"
40
"username-dialog";
41
]
42
[ txt "Change username" ];
43
];
44
tag "dialog"
45
([
46
Dream_html.string_attr "id" "username-dialog";
47
class_ "profile-dialog";
48
]
49
@
50
if Option.is_some username_error then [ Dream_html.attr "open" ]
51
else [])
52
([
53
tag "h3" [] [ txt "Change username" ];
54
tag "form"
55
[
56
action Routes.profile_username;
57
post_form;
58
class_ "profile-form";
59
Dream_html.attr "data-hito-app-form";
60
]
61
[
62
Dream_html.csrf_tag request;
63
tag "div"
64
[ class_ "field" ]
65
[
66
tag "label"
67
[ Dream_html.string_attr "for" "username" ]
68
[ txt "Username" ];
69
void "input"
70
[
71
type_ "text";
72
name "username";
73
Dream_html.string_attr "id" "username";
74
Dream_html.string_attr "value" "%s"
75
viewer.View_model.Viewer.username;
76
required;
77
];
78
];
79
tag "div"
80
[ class_ "button-group" ]
81
[
82
void "input"
83
[ type_ "submit"; value "Change username" ];
84
tag "button"
85
[
86
type_ "button";
87
class_ "secondary";
88
Dream_html.attr "data-hito-dialog-close";
89
]
90
[ txt "Cancel" ];
91
];
92
];
93
]
94
@ error_block username_error);
95
];
96
(* Password: a button opens the change-password modal. *)
97
tag "section"
98
[ class_ "profile-section" ]
99
[
100
tag "h2" [] [ txt "Password" ];
101
tag "div"
102
[ class_ "profile-value" ]
103
[
104
tag "span" [ class_ "profile-value-text" ] [ txt "••••••••" ];
105
tag "button"
106
[
107
type_ "button";
108
class_ "profile-edit";
109
Dream_html.string_attr "data-hito-dialog-open"
110
"password-dialog";
111
]
112
[ txt "Change password" ];
113
];
114
tag "dialog"
115
([
116
Dream_html.string_attr "id" "password-dialog";
117
class_ "profile-dialog";
118
]
119
@
120
if Option.is_some password_error then [ Dream_html.attr "open" ]
121
else [])
122
([
123
tag "h3" [] [ txt "Change password" ];
124
tag "form"
125
[
126
action Routes.profile_password;
127
post_form;
128
class_ "profile-form";
129
Dream_html.attr "data-hito-app-form";
130
]
131
[
132
Dream_html.csrf_tag request;
133
tag "div"
134
[ class_ "field" ]
135
[
136
tag "label"
137
[ Dream_html.string_attr "for" "current" ]
138
[ txt "Current password" ];
139
void "input"
140
[
141
type_ "password";
142
name "current";
143
Dream_html.string_attr "id" "current";
144
required;
145
];
146
];
147
tag "div"
148
[ class_ "field" ]
149
[
150
tag "label"
151
[ Dream_html.string_attr "for" "next" ]
152
[ txt "New password" ];
153
void "input"
154
[
155
type_ "password";
156
name "next";
157
Dream_html.string_attr "id" "next";
158
required;
159
];
160
];
161
tag "div"
162
[ class_ "button-group" ]
163
[
164
void "input"
165
[ type_ "submit"; value "Change password" ];
166
tag "button"
167
[
168
type_ "button";
169
class_ "secondary";
170
Dream_html.attr "data-hito-dialog-close";
171
]
172
[ txt "Cancel" ];
173
];
174
];
175
]
176
@ error_block password_error);
177
];
178
]
179
@ (if import_open then
180
[
181
(* Import: a multipart form uploads a native Hevy CSV export. The file
182
field is [csv], the wall-clock offset is [utc_offset_seconds], and an
183
optional checkbox overrides duplicate-source rejection. The form is a
184
plain multipart POST with its CSRF token, so it needs no script. *)
185
tag "section"
186
[ class_ "profile-section" ]
187
[
188
tag "h2" [] [ txt "Import" ];
189
tag "form"
190
[
191
action Routes.import_upload;
192
post_form;
193
Dream_html.string_attr "enctype" "multipart/form-data";
194
class_ "profile-form";
195
]
196
[
197
Dream_html.csrf_tag request;
198
tag "div"
199
[ class_ "field" ]
200
[
201
tag "label"
202
[ Dream_html.string_attr "for" "csv" ]
203
[ txt "Hevy CSV export" ];
204
void "input"
205
[
206
type_ "file";
207
name "csv";
208
Dream_html.string_attr "id" "csv";
209
Dream_html.string_attr "accept" ".csv,text/csv";
210
required;
211
];
212
];
213
tag "div"
214
[ class_ "field" ]
215
[
216
tag "label"
217
[ Dream_html.string_attr "for" "utc_offset_seconds" ]
218
[ txt "UTC offset in seconds" ];
219
void "input"
220
[
221
type_ "number";
222
name "utc_offset_seconds";
223
Dream_html.string_attr "id" "utc_offset_seconds";
224
Dream_html.string_attr "value" "0";
225
required;
226
];
227
];
228
tag "div"
229
[ class_ "field" ]
230
[
231
void "input"
232
[
233
type_ "checkbox";
234
name "allow_duplicate";
235
Dream_html.string_attr "id" "allow_duplicate";
236
value "true";
237
];
238
tag "label"
239
[ Dream_html.string_attr "for" "allow_duplicate" ]
240
[
241
txt "Import even if this export was imported before";
242
];
243
];
244
void "input" [ type_ "submit"; value "Upload import" ];
245
];
246
];
247
]
248
else [])
249
@ [
250
tag "section"
251
[ class_ "profile-section profile-link-section" ]
252
[
253
tag "h2" [] [ txt "Settings" ];
254
tag "p" [] [ txt "Manage your theme and other account settings." ];
255
tag "a"
256
[
257
class_ "button";
258
href Routes.settings;
259
Dream_html.attr "data-hito-app-link";
260
]
261
[ txt "Open settings" ];
262
];
263
tag "section"
264
[ class_ "profile-section profile-link-section" ]
265
[
266
tag "h2" [] [ txt "App Feedback" ];
267
tag "p" [] [ txt "Share feedback about the application." ];
268
tag "a"
269
[
270
class_ "button";
271
href Routes.app_feedback;
272
Dream_html.attr "data-hito-app-link";
273
]
274
[ txt "Give feedback" ];
275
];
276
tag "section"
277
[ class_ "profile-section profile-signout-section" ]
278
[
279
tag "h2" [] [ txt "Sign out" ];
280
tag "form"
281
[
282
action Routes.logout;
283
post_form;
284
class_ "profile-signout-form";
285
Dream_html.attr "data-hito-app-form";
286
]
287
[
288
Dream_html.csrf_tag request;
289
void "input" [ type_ "submit"; value "Sign out" ];
290
];
291
];
292
])
293