[OCaml] High Intensity Training Online
1
open Dream_html
2
open Page_kit
3
open Page_shell
4
5
let choose_routine request ~workout_started ~viewer
6
~(routines : View_model.Routine_choice.t list) =
7
html_page ~viewer ~request ~active:"home" ?workout_started "Choose a routine"
8
([ tag "h1" [] [ txt "Routines" ] ]
9
@ List.map
10
(fun (choice : View_model.Routine_choice.t) ->
11
tag "form"
12
[
13
action Routes.select_routine choice.id;
14
post_form;
15
Dream_html.attr "data-hito-app-form";
16
]
17
[
18
Dream_html.csrf_tag request;
19
tag "fieldset" []
20
[
21
tag "legend" [] [ txt "%s" choice.name ];
22
tag "p" []
23
[ txt "%d workouts in the cycle." choice.workout_count ];
24
void "input" [ type_ "submit"; value "Use this routine" ];
25
];
26
])
27
routines)
28
29
(* Both begin forms post the same fields. Only the override flag differs. *)
30
let begin_fields request ~routine_id ~override =
31
[
32
Dream_html.csrf_tag request;
33
void "input"
34
[
35
type_ "hidden";
36
name "routine";
37
Dream_html.string_attr "value" "%s" routine_id;
38
];
39
void "input"
40
[
41
type_ "hidden";
42
name "override";
43
Dream_html.string_attr "value" "%s" (string_of_bool override);
44
];
45
]
46
47
let begin_form request ~routine_id ~override label =
48
tag "form"
49
[ action Routes.workout; post_form; Dream_html.attr "data-hito-app-form" ]
50
(begin_fields request ~routine_id ~override
51
@ [ void "input" [ type_ "submit"; value label ] ])
52
53
(* Beginning under override needs an explicit acknowledgement, so its submit is
54
gated behind a confirmation modal. Without script the form submits at once —
55
the doctrine's deviation stays possible. The modal only makes it deliberate.
56
The client reuses the shared confirm machinery: [data-hito-confirm-cancel]
57
opens the dialog, [data-hito-confirm-form] is the form its accept submits. *)
58
let override_begin request ~routine_id =
59
[
60
tag "form"
61
[
62
action Routes.workout;
63
post_form;
64
Dream_html.attr "data-hito-app-form";
65
Dream_html.attr "data-hito-confirm-form";
66
]
67
(begin_fields request ~routine_id ~override:true
68
@ [
69
void "input"
70
[
71
type_ "submit";
72
class_ "outline";
73
value "Begin under override";
74
Dream_html.attr "data-hito-confirm-cancel";
75
];
76
]);
77
tag "dialog"
78
[ class_ "confirm-dialog"; Dream_html.attr "data-hito-confirm-modal" ]
79
[
80
tag "h2" [] [ txt "Begin before recovery is complete?" ];
81
tag "p" []
82
[
83
txt
84
"Heavy Duty grows muscle during recovery, not in the gym. \
85
Training early can reduce it. The app records this as an \
86
override.";
87
];
88
tag "div"
89
[ class_ "button-group" ]
90
[
91
tag "button"
92
[ type_ "button"; Dream_html.attr "data-hito-confirm-accept" ]
93
[ txt "Begin under override" ];
94
tag "button"
95
[
96
type_ "button";
97
class_ "secondary";
98
Dream_html.attr "data-hito-confirm-dismiss";
99
]
100
[ txt "Wait to recover" ];
101
];
102
];
103
]
104
105
let home request ~viewer ~home:(vm : View_model.Home.t) =
106
let status, gate =
107
match vm.gate with
108
| View_model.Home.Ready ->
109
( None,
110
[
111
begin_form request ~routine_id:vm.routine_id ~override:false
112
"Begin next workout";
113
] )
114
| View_model.Home.Recovering { status } ->
115
( Some status,
116
[
117
tag "div"
118
[ class_ "warn" ]
119
([
120
tag "p" []
121
[
122
txt "Heavy Duty requires recovery before the next workout.";
123
];
124
]
125
@ override_begin request ~routine_id:vm.routine_id);
126
] )
127
in
128
html_page ~viewer ~request ~active:"home" "Home"
129
([
130
tag "h1" [] [ txt "Next Workout" ]; upcoming_workout_card vm.next_workout;
131
]
132
@ Option.to_list
133
(Option.map (fun text -> tag "p" [] [ txt "%s" text ]) status)
134
@ gate)
135
136
(* Home while a workout is in progress. Rather than the recovery gate or the
137
routine chooser, Home shows a single card: the workout is under way and one
138
tap returns to it. The middle navigation action already reads "Current
139
workout" here, and this card is the matching Home affordance. *)
140
let workout_in_progress request ~viewer ~workout_started ~workout_name =
141
html_page ~viewer ~request ~active:"home" ~workout_started "Home"
142
[
143
tag "a"
144
[
145
class_ "in-progress";
146
href Routes.workout;
147
Dream_html.attr "data-hito-app-link";
148
]
149
[
150
tag "p" [ class_ "eyebrow" ] [ txt "Workout in progress" ];
151
tag "h1" [] [ txt "%s" workout_name ];
152
tag "p" [] [ txt "Return to the current workout to keep logging." ];
153
];
154
]
155
156
(* One workout stop on the timeline. The workout is a native [details], so it
157
works without script and needs no [name]: stops open independently and the
158
trainee can compare workouts. Only the next workout starts open. The rest
159
stays outside the [details], so a closed timeline still shows the gating. *)
160
let workout_stop (workout : View_model.Routine.workout) =
161
let next, recovery =
162
match workout.position with
163
| View_model.Routine.Next { recovery } -> (true, recovery)
164
| View_model.Routine.Elsewhere -> (false, None)
165
in
166
tag "li"
167
([ class_ "routine-stop" ]
168
@ if next then [ Dream_html.string_attr "aria-current" "step" ] else [])
169
[
170
tag "details"
171
([ class_ "routine-workout" ]
172
@ if next then [ Dream_html.attr "open" ] else [])
173
[
174
tag "summary" []
175
([ tag "h2" [ class_ "routine-day" ] [ txt "%s" workout.name ] ]
176
@ (if next then
177
[ tag "span" [ class_ "routine-next" ] [ txt "Next" ] ]
178
else [])
179
@
180
match recovery with
181
| Some status ->
182
[ tag "span" [ class_ "routine-recovery" ] [ txt "%s" status ] ]
183
| None -> []);
184
(* The panel clips the content while the unfold animates. *)
185
tag "div"
186
[ class_ "routine-panel" ]
187
[
188
tag "ol"
189
[ class_ "routine-exercises" ]
190
(List.map
191
(fun (exercise : View_model.Routine.exercise) ->
192
tag "li" []
193
[
194
tag "span"
195
[ class_ "routine-exercise" ]
196
[ txt "%s" exercise.name ];
197
])
198
workout.exercises);
199
];
200
];
201
tag "p" [ class_ "routine-rest" ] [ txt "%s" workout.rest_after ];
202
]
203
204
let routine request ?workout_started ~viewer (vm : View_model.Routine.t) =
205
html_page ~viewer ~request ~active:"routine" ?workout_started "Routine"
206
[
207
tag "h1" [] [ txt "%s" vm.name ];
208
tag "p" []
209
[
210
tag "a"
211
[ href Routes.routines; Dream_html.attr "data-hito-app-link" ]
212
[ txt "Browse routines" ];
213
];
214
tag "ol" [ class_ "routine-timeline" ] (List.map workout_stop vm.workouts);
215
]
216