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" ]; ]; ]; ])