View raw

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