open Dream_html
open Page_kit
open Page_shell
(* The account profile page. It names the signed-in trainee and offers two
independent forms: rename the account, and change the password. Each form
reports its own error inline. [notice] confirms a successful change. Both are
plain authenticated app-forms, so they work with or without script. *)
let profile request ?workout_started ~viewer ~import_open ?username_error
?password_error ?notice () =
let notice_block =
match notice with
| Some message -> [ tag "p" [ class_ "notice" ] [ txt "%s" message ] ]
| None -> []
in
let error_block = function Some message -> [ alert message ] | None -> [] in
html_page ~viewer ~request ~active:"profile" ?workout_started "Account"
([ tag "h1" [] [ txt "Account" ] ]
@ notice_block
@ [
(* Username: show the current value and a button that opens the edit
modal. The modal holds the rename form. A no-script client, or an
error re-render, opens the dialog through the [open] attribute so the
form stays reachable and its inline error shows. *)
tag "section"
[ class_ "profile-section" ]
[
tag "h2" [] [ txt "Username" ];
tag "div"
[ class_ "profile-value" ]
[
tag "span"
[ class_ "profile-value-text" ]
[ txt "%s" viewer.View_model.Viewer.username ];
tag "button"
[
type_ "button";
class_ "profile-edit";
Dream_html.string_attr "data-hito-dialog-open"
"username-dialog";
]
[ txt "Change username" ];
];
tag "dialog"
([
Dream_html.string_attr "id" "username-dialog";
class_ "profile-dialog";
]
@
if Option.is_some username_error then [ Dream_html.attr "open" ]
else [])
([
tag "h3" [] [ txt "Change username" ];
tag "form"
[
action Routes.profile_username;
post_form;
class_ "profile-form";
Dream_html.attr "data-hito-app-form";
]
[
Dream_html.csrf_tag request;
tag "div"
[ class_ "field" ]
[
tag "label"
[ Dream_html.string_attr "for" "username" ]
[ txt "Username" ];
void "input"
[
type_ "text";
name "username";
Dream_html.string_attr "id" "username";
Dream_html.string_attr "value" "%s"
viewer.View_model.Viewer.username;
required;
];
];
tag "div"
[ class_ "button-group" ]
[
void "input"
[ type_ "submit"; value "Change username" ];
tag "button"
[
type_ "button";
class_ "secondary";
Dream_html.attr "data-hito-dialog-close";
]
[ txt "Cancel" ];
];
];
]
@ error_block username_error);
];
(* Password: a button opens the change-password modal. *)
tag "section"
[ class_ "profile-section" ]
[
tag "h2" [] [ txt "Password" ];
tag "div"
[ class_ "profile-value" ]
[
tag "span" [ class_ "profile-value-text" ] [ txt "••••••••" ];
tag "button"
[
type_ "button";
class_ "profile-edit";
Dream_html.string_attr "data-hito-dialog-open"
"password-dialog";
]
[ txt "Change password" ];
];
tag "dialog"
([
Dream_html.string_attr "id" "password-dialog";
class_ "profile-dialog";
]
@
if Option.is_some password_error then [ Dream_html.attr "open" ]
else [])
([
tag "h3" [] [ txt "Change password" ];
tag "form"
[
action Routes.profile_password;
post_form;
class_ "profile-form";
Dream_html.attr "data-hito-app-form";
]
[
Dream_html.csrf_tag request;
tag "div"
[ class_ "field" ]
[
tag "label"
[ Dream_html.string_attr "for" "current" ]
[ txt "Current password" ];
void "input"
[
type_ "password";
name "current";
Dream_html.string_attr "id" "current";
required;
];
];
tag "div"
[ class_ "field" ]
[
tag "label"
[ Dream_html.string_attr "for" "next" ]
[ txt "New password" ];
void "input"
[
type_ "password";
name "next";
Dream_html.string_attr "id" "next";
required;
];
];
tag "div"
[ class_ "button-group" ]
[
void "input"
[ type_ "submit"; value "Change password" ];
tag "button"
[
type_ "button";
class_ "secondary";
Dream_html.attr "data-hito-dialog-close";
]
[ txt "Cancel" ];
];
];
]
@ error_block password_error);
];
]
@ (if import_open then
[
(* Import: a multipart form uploads a native Hevy CSV export. The file
field is [csv], the wall-clock offset is [utc_offset_seconds], and an
optional checkbox overrides duplicate-source rejection. The form is a
plain multipart POST with its CSRF token, so it needs no script. *)
tag "section"
[ class_ "profile-section" ]
[
tag "h2" [] [ txt "Import" ];
tag "form"
[
action Routes.import_upload;
post_form;
Dream_html.string_attr "enctype" "multipart/form-data";
class_ "profile-form";
]
[
Dream_html.csrf_tag request;
tag "div"
[ class_ "field" ]
[
tag "label"
[ Dream_html.string_attr "for" "csv" ]
[ txt "Hevy CSV export" ];
void "input"
[
type_ "file";
name "csv";
Dream_html.string_attr "id" "csv";
Dream_html.string_attr "accept" ".csv,text/csv";
required;
];
];
tag "div"
[ class_ "field" ]
[
tag "label"
[ Dream_html.string_attr "for" "utc_offset_seconds" ]
[ txt "UTC offset in seconds" ];
void "input"
[
type_ "number";
name "utc_offset_seconds";
Dream_html.string_attr "id" "utc_offset_seconds";
Dream_html.string_attr "value" "0";
required;
];
];
tag "div"
[ class_ "field" ]
[
void "input"
[
type_ "checkbox";
name "allow_duplicate";
Dream_html.string_attr "id" "allow_duplicate";
value "true";
];
tag "label"
[ Dream_html.string_attr "for" "allow_duplicate" ]
[
txt "Import even if this export was imported before";
];
];
void "input" [ type_ "submit"; value "Upload import" ];
];
];
]
else [])
@ [
tag "section"
[ class_ "profile-section profile-link-section" ]
[
tag "h2" [] [ txt "Settings" ];
tag "p" [] [ txt "Manage your theme and other account settings." ];
tag "a"
[
class_ "button";
href Routes.settings;
Dream_html.attr "data-hito-app-link";
]
[ txt "Open settings" ];
];
tag "section"
[ class_ "profile-section profile-link-section" ]
[
tag "h2" [] [ txt "App Feedback" ];
tag "p" [] [ txt "Share feedback about the application." ];
tag "a"
[
class_ "button";
href Routes.app_feedback;
Dream_html.attr "data-hito-app-link";
]
[ txt "Give feedback" ];
];
tag "section"
[ class_ "profile-section profile-signout-section" ]
[
tag "h2" [] [ txt "Sign out" ];
tag "form"
[
action Routes.logout;
post_form;
class_ "profile-signout-form";
Dream_html.attr "data-hito-app-form";
]
[
Dream_html.csrf_tag request;
void "input" [ type_ "submit"; value "Sign out" ];
];
];
])