type page = Dream_html.node
let tag = Dream_html.std_tag
let void = Dream_html.void_tag
let class_ value = Dream_html.string_attr "class" value
let name value = Dream_html.string_attr "name" value
let value value = Dream_html.string_attr "value" value
let rel value = Dream_html.string_attr "rel" value
let type_ value = Dream_html.string_attr "type" value
let step value = Dream_html.string_attr "step" value
let id value = Dream_html.string_attr "id" value
let tabindex value = Dream_html.string_attr "tabindex" value
let required = Dream_html.attr "required"
let href path = Dream_html.path_attr (Dream_html.uri_attr "href") path
let src path = Dream_html.path_attr (Dream_html.uri_attr "src") path
let action path = Dream_html.path_attr (Dream_html.uri_attr "action") path
let post_form = Dream_html.string_attr "method" "post"
let alert message =
tag "p"
[ class_ "warn"; Dream_html.string_attr "role" "alert" ]
[ Dream_html.txt "%s" message ]
(* Render server-side link tabs. The active tab uses [aria-current] because
each tab navigates to a complete page rather than switching a client panel. *)
let link_tabs ~aria_label ~active tabs =
let tab_link (key, label, path) =
let attrs =
[
class_ "page-tab";
Dream_html.string_attr "href" "%s" path;
Dream_html.attr "data-hito-app-link";
]
in
let attrs =
if String.equal key active then
Dream_html.string_attr "aria-current" "page" :: attrs
else attrs
in
tag "a" attrs [ Dream_html.txt "%s" label ]
in
tag "nav"
[ class_ "page-tabs"; Dream_html.string_attr "aria-label" "%s" aria_label ]
(List.map tab_link tabs)
(* Render the common comparison table structure. The caller supplies grouped
bodies, so recorded and planned workouts keep their different values. *)
let workout_evidence_table ?(upcoming = false) ?(caption_class = "sr-only")
~caption bodies =
let classes =
if upcoming then "workout-evidence-table upcoming-workout-table"
else "workout-evidence-table"
in
let column label =
tag "th"
[ Dream_html.string_attr "scope" "col" ]
[ Dream_html.txt "%s" label ]
in
let headers =
[ column "Exercise"; column "Load (kg)" ]
@
if upcoming then [ column "Reps" ]
else [ column "Δ"; column "Reps"; column "Δ" ]
in
tag "table"
[ class_ "%s" classes ]
([
tag "caption" [ class_ "%s" caption_class ] caption;
tag "thead" [] [ tag "tr" [] headers ];
]
@ bodies)
(* Render a prescribed workout before it has a log. Both Home and Logbook use
this component, so the same upcoming workout shows the same evidence. *)
let upcoming_workout_card (planned : View_model.Upcoming_workout.t) =
let format_load load = Printf.sprintf "%g" load in
let format_reps reps = string_of_int reps in
let rows_of_group (group : View_model.Upcoming_workout.group) =
List.map
(fun (exercise : View_model.Upcoming_workout.exercise) ->
tag "tr" []
[
tag "th"
[
Dream_html.string_attr "scope" "row"; class_ "evidence-exercise";
]
[ Dream_html.txt "%s" exercise.name ];
tag "td"
[ class_ "observation-latest" ]
[
Dream_html.txt "%s"
(Option.fold ~none:"—" ~some:format_load exercise.latest_load);
];
tag "td"
[ class_ "observation-latest" ]
[
Dream_html.txt "%s"
(Option.fold ~none:"—" ~some:format_reps exercise.latest_reps);
];
])
group.exercises
in
let caption =
[
tag "strong"
[ class_ "workout-evidence-caption-name" ]
[ Dream_html.txt "%s, %s" planned.routine_name planned.name ];
tag "span"
[ class_ "workout-evidence-caption-status upcoming" ]
[ Dream_html.txt "Planned on %s" planned.scheduled_on ];
]
in
tag "article"
[ class_ "selected-upcoming-workout" ]
[
workout_evidence_table ~upcoming:true
~caption_class:"upcoming-workout-caption" ~caption
(List.map
(fun group -> tag "tbody" [] (rows_of_group group))
planned.groups);
]