1
+
Added:
open Eliom_content.Html.F
2
+
Added:
open Hito_app
3
+
Added:
4
+
Added:
type page = Html_types.html Eliom_content.Html.elt
5
+
Added:
6
+
Added:
(* Named so that inference cannot mistake a routine_id for a plain string at the
7
+
Added:
use sites. *)
8
+
Added:
let id_string (id : Repository.routine_id) = (id :> string)
9
+
Added:
10
+
Added:
(* Kept inline so the server needs no static file extension. *)
11
+
Added:
let stylesheet =
12
+
Added:
"*{box-sizing:border-box} body{font:16px/1.5 system-ui,sans-serif;margin:0 \
13
+
Added:
auto;max-width:34rem;padding:1rem;color:#191919;background:#fafafa} \
14
+
Added:
h1{font-size:1.4rem;margin:0 0 .25rem} h2{font-size:1.1rem;margin:1.5rem 0 \
15
+
Added:
.5rem} p{margin:.4rem 0} a{color:#0b5} nav{margin:.5rem 0 \
16
+
Added:
1.5rem;font-size:.9rem} nav a{margin-right:1rem} fieldset{border:1px solid \
17
+
Added:
#ddd;border-radius:6px;margin:0 0 1rem;padding:.75rem} \
18
+
Added:
legend{font-weight:600;padding:0 .35rem} label{display:block;margin:.4rem 0 \
19
+
Added:
.1rem;font-size:.85rem;color:#555} \
20
+
Added:
input,select{font:inherit;padding:.4rem;width:100%;border:1px solid \
21
+
Added:
#ccc;border-radius:4px;background:#fff} \
22
+
Added:
input[type=submit]{background:#191919;color:#fff;border:0;cursor:pointer;margin-top:.6rem;width:auto;padding:.5rem \
23
+
Added:
1rem} .row{display:flex;gap:.5rem} .row>div{flex:1} \
24
+
Added:
.warn{background:#fff6e5;border-left:3px solid #e59b00;padding:.6rem \
25
+
Added:
.8rem;margin:1rem 0} .done{color:#666;font-size:.9rem} \
26
+
Added:
ul{padding-left:1.2rem} li{margin:.2rem 0}"
27
+
Added:
28
+
Added:
let shell ~title:t content =
29
+
Added:
html
30
+
Added:
(head
31
+
Added:
(title (txt (t ^ " — hito")))
32
+
Added:
[
33
+
Added:
meta
34
+
Added:
~a:
35
+
Added:
[
36
+
Added:
a_name "viewport"; a_content "width=device-width,initial-scale=1";
37
+
Added:
]
38
+
Added:
();
39
+
Added:
style [ txt stylesheet ];
40
+
Added:
])
41
+
Added:
(body
42
+
Added:
([
43
+
Added:
h1 [ txt "hito" ];
44
+
Added:
nav
45
+
Added:
[
46
+
Added:
a ~service:Routes.home [ txt "Routines" ] ();
47
+
Added:
a ~service:Routes.log [ txt "Current workout" ] ();
48
+
Added:
a ~service:Routes.history [ txt "History" ] ();
49
+
Added:
];
50
+
Added:
]
51
+
Added:
@ content))
52
+
Added:
53
+
Added:
let problem ~title:t ~detail =
54
+
Added:
shell ~title:t [ h2 [ txt t ]; p ~a:[ a_class [ "warn" ] ] [ txt detail ] ]
55
+
Added:
56
+
Added:
(* A prescription described in words: the movements, and the rep window that
57
+
Added:
calibrates the load. *)
58
+
Added:
let describe_prescription p =
59
+
Added:
let window =
60
+
Added:
Format.asprintf "%a reps" Units.Rep_range.pp (Prescription.rep_range p)
61
+
Added:
in
62
+
Added:
match Prescription.delivery p with
63
+
Added:
| Prescription.Single e -> Printf.sprintf "%s — %s" (Exercise.name e) window
64
+
Added:
| Prescription.Pre_exhaust { isolation; compound } ->
65
+
Added:
Printf.sprintf "%s into %s, no pause — %s" (Exercise.name isolation)
66
+
Added:
(Exercise.name compound) window
67
+
Added:
68
+
Added:
let extension_select name =
69
+
Added:
match
70
+
Added:
List.map
71
+
Added:
(fun (value, label) -> Form.Option ([], value, Some (txt label), false))
72
+
Added:
Routes.extension_choices
73
+
Added:
with
74
+
Added:
| first :: rest -> Form.select ~name Form.string first rest
75
+
Added:
| [] -> assert false
76
+
Added:
77
+
Added:
let choose_routine ~(routines : (Repository.routine_id * Routine.t) list) =
78
+
Added:
shell ~title:"Choose a routine"
79
+
Added:
[
80
+
Added:
h2 [ txt "Routines" ];
81
+
Added:
div
82
+
Added:
(List.map
83
+
Added:
(fun (id, r) ->
84
+
Added:
Form.post_form ~service:Routes.begin_workout
85
+
Added:
(fun (routine, reason) ->
86
+
Added:
[
87
+
Added:
fieldset
88
+
Added:
~legend:(legend [ txt (Routine.name r) ])
89
+
Added:
[
90
+
Added:
p
91
+
Added:
[
92
+
Added:
txt
93
+
Added:
(Printf.sprintf "%d workouts in the cycle."
94
+
Added:
(List.length (Routine.workouts r)));
95
+
Added:
];
96
+
Added:
Form.input ~input_type:`Hidden ~name:routine
97
+
Added:
~value:(id_string id) Form.string;
98
+
Added:
Form.input ~input_type:`Hidden ~name:reason ~value:""
99
+
Added:
Form.string;
100
+
Added:
Form.input ~input_type:`Submit
101
+
Added:
~value:"Begin next workout" Form.string;
102
+
Added:
];
103
+
Added:
])
104
+
Added:
())
105
+
Added:
routines);
106
+
Added:
]
107
+
Added:
108
+
Added:
let recovery_gate ~(routine : Repository.routine_id) ~workout ~readiness =
109
+
Added:
let remaining =
110
+
Added:
match readiness with
111
+
Added:
| Recovery.Ready -> "Recovery is complete."
112
+
Added:
| Recovery.Recovering { rested; recommended } ->
113
+
Added:
Format.asprintf "You have rested %a of the %a this workout asks for."
114
+
Added:
Recovery.pp_duration rested Recovery.pp_duration recommended
115
+
Added:
in
116
+
Added:
shell ~title:"Not recovered"
117
+
Added:
[
118
+
Added:
h2 [ txt ("Next: " ^ Workout_prescription.name workout) ];
119
+
Added:
div
120
+
Added:
~a:[ a_class [ "warn" ] ]
121
+
Added:
[
122
+
Added:
p [ txt remaining ];
123
+
Added:
p
124
+
Added:
[
125
+
Added:
txt
126
+
Added:
"Heavy Duty treats training before recovery finishes as the \
127
+
Added:
main way progress is lost — growth happens while you rest, \
128
+
Added:
not while you lift. Train anyway only with a reason, which is \
129
+
Added:
kept with the workout so a later stall can be explained.";
130
+
Added:
];
131
+
Added:
];
132
+
Added:
Form.post_form ~service:Routes.begin_workout
133
+
Added:
(fun (routine_name, reason) ->
134
+
Added:
[
135
+
Added:
fieldset
136
+
Added:
~legend:(legend [ txt "Train anyway" ])
137
+
Added:
[
138
+
Added:
Form.input ~input_type:`Hidden ~name:routine_name
139
+
Added:
~value:(id_string routine) Form.string;
140
+
Added:
label
141
+
Added:
~a:[ a_label_for "reason" ]
142
+
Added:
[ txt "Why are you training early?" ];
143
+
Added:
Form.input
144
+
Added:
~a:
145
+
Added:
[
146
+
Added:
a_id "reason";
147
+
Added:
a_required ();
148
+
Added:
a_placeholder "travelling tomorrow";
149
+
Added:
]
150
+
Added:
~input_type:`Text ~name:reason Form.string;
151
+
Added:
Form.input ~input_type:`Submit ~value:"Begin under override"
152
+
Added:
Form.string;
153
+
Added:
];
154
+
Added:
])
155
+
Added:
();
156
+
Added:
]
157
+
Added:
158
+
Added:
let single_form ~slot ~prescription =
159
+
Added:
Form.post_form ~service:Routes.log_single
160
+
Added:
(fun (slot_name, (load_name, (reps_name, ext_name))) ->
161
+
Added:
[
162
+
Added:
fieldset
163
+
Added:
~legend:(legend [ txt (describe_prescription prescription) ])
164
+
Added:
[
165
+
Added:
Form.input ~input_type:`Hidden ~name:slot_name ~value:slot Form.int;
166
+
Added:
div
167
+
Added:
~a:[ a_class [ "row" ] ]
168
+
Added:
[
169
+
Added:
div
170
+
Added:
[
171
+
Added:
label ~a:[ a_label_for "l" ] [ txt "Load (kg)" ];
172
+
Added:
Form.input
173
+
Added:
~a:[ a_id "l"; a_step (Some 0.5); a_required () ]
174
+
Added:
~input_type:`Number ~name:load_name Form.float;
175
+
Added:
];
176
+
Added:
div
177
+
Added:
[
178
+
Added:
label ~a:[ a_label_for "r" ] [ txt "Reps to failure" ];
179
+
Added:
Form.input
180
+
Added:
~a:[ a_id "r"; a_required () ]
181
+
Added:
~input_type:`Number ~name:reps_name Form.int;
182
+
Added:
];
183
+
Added:
];
184
+
Added:
label [ txt "Ending" ];
185
+
Added:
extension_select ext_name;
186
+
Added:
Form.input ~input_type:`Submit ~value:"Record" Form.string;
187
+
Added:
];
188
+
Added:
])
189
+
Added:
()
190
+
Added:
191
+
Added:
let pair_form ~slot ~prescription ~isolation ~compound =
192
+
Added:
Form.post_form ~service:Routes.log_pair
193
+
Added:
(fun (slot_name, (iso_load, (iso_reps, (comp_load, (comp_reps, ext_name)))))
194
+
Added:
->
195
+
Added:
[
196
+
Added:
fieldset
197
+
Added:
~legend:(legend [ txt (describe_prescription prescription) ])
198
+
Added:
[
199
+
Added:
Form.input ~input_type:`Hidden ~name:slot_name ~value:slot Form.int;
200
+
Added:
p ~a:[ a_class [ "done" ] ] [ txt (Exercise.name isolation) ];
201
+
Added:
div
202
+
Added:
~a:[ a_class [ "row" ] ]
203
+
Added:
[
204
+
Added:
div
205
+
Added:
[
206
+
Added:
label [ txt "Load (kg)" ];
207
+
Added:
Form.input
208
+
Added:
~a:[ a_step (Some 0.5); a_required () ]
209
+
Added:
~input_type:`Number ~name:iso_load Form.float;
210
+
Added:
];
211
+
Added:
div
212
+
Added:
[
213
+
Added:
label [ txt "Reps" ];
214
+
Added:
Form.input
215
+
Added:
~a:[ a_required () ]
216
+
Added:
~input_type:`Number ~name:iso_reps Form.int;
217
+
Added:
];
218
+
Added:
];
219
+
Added:
p ~a:[ a_class [ "done" ] ] [ txt (Exercise.name compound) ];
220
+
Added:
div
221
+
Added:
~a:[ a_class [ "row" ] ]
222
+
Added:
[
223
+
Added:
div
224
+
Added:
[
225
+
Added:
label [ txt "Load (kg)" ];
226
+
Added:
Form.input
227
+
Added:
~a:[ a_step (Some 0.5); a_required () ]
228
+
Added:
~input_type:`Number ~name:comp_load Form.float;
229
+
Added:
];
230
+
Added:
div
231
+
Added:
[
232
+
Added:
label [ txt "Reps" ];
233
+
Added:
Form.input
234
+
Added:
~a:[ a_required () ]
235
+
Added:
~input_type:`Number ~name:comp_reps Form.int;
236
+
Added:
];
237
+
Added:
];
238
+
Added:
label [ txt "Ending" ];
239
+
Added:
extension_select ext_name;
240
+
Added:
Form.input ~input_type:`Submit ~value:"Record" Form.string;
241
+
Added:
];
242
+
Added:
])
243
+
Added:
()
244
+
Added:
245
+
Added:
let describe_stimulus s =
246
+
Added:
let movement m =
247
+
Added:
Format.asprintf "%s %a x %a"
248
+
Added:
(Exercise.name (Stimulus.Movement.exercise m))
249
+
Added:
Units.Weight.pp (Stimulus.Movement.load m) Units.Reps.pp
250
+
Added:
(Stimulus.Movement.reps m)
251
+
Added:
in
252
+
Added:
let body =
253
+
Added:
String.concat " into " (List.map movement (Stimulus.movements s))
254
+
Added:
in
255
+
Added:
match Stimulus.extensions s with
256
+
Added:
| [] -> body
257
+
Added:
| es ->
258
+
Added:
body ^ ", "
259
+
Added:
^ String.concat " then "
260
+
Added:
(List.map (Format.asprintf "%a" Stimulus.pp_extension) es)
261
+
Added:
262
+
Added:
let log_workout ~entry =
263
+
Added:
let prescription = Entry.prescription entry in
264
+
Added:
let performed = Entry.stimuli entry in
265
+
Added:
let outstanding = Entry.outstanding entry in
266
+
Added:
let override_note =
267
+
Added:
match Recovery.basis (Entry.clearance entry) with
268
+
Added:
| Recovery.Recovered -> []
269
+
Added:
| Recovery.Overridden { reason; _ } ->
270
+
Added:
[
271
+
Added:
div
272
+
Added:
~a:[ a_class [ "warn" ] ]
273
+
Added:
[ txt ("Begun before recovery finished: " ^ reason) ];
274
+
Added:
]
275
+
Added:
in
276
+
Added:
shell
277
+
Added:
~title:(Workout_prescription.name prescription)
278
+
Added:
(override_note
279
+
Added:
@ [
280
+
Added:
h2 [ txt (Workout_prescription.name prescription) ];
281
+
Added:
p
282
+
Added:
[
283
+
Added:
txt
284
+
Added:
(Printf.sprintf "%d of %d recorded." (List.length performed)
285
+
Added:
(List.length (Workout_prescription.prescriptions prescription)));
286
+
Added:
];
287
+
Added:
]
288
+
Added:
@ (if performed = [] then []
289
+
Added:
else
290
+
Added:
[
291
+
Added:
h2 [ txt "Recorded" ];
292
+
Added:
ul (List.map (fun s -> li [ txt (describe_stimulus s) ]) performed);
293
+
Added:
])
294
+
Added:
@ (if outstanding = [] then
295
+
Added:
[ p [ txt "Everything prescribed has been recorded." ] ]
296
+
Added:
else
297
+
Added:
h2 [ txt "Still to do" ]
298
+
Added:
:: List.map
299
+
Added:
(fun (slot, p) ->
300
+
Added:
match Prescription.delivery p with
301
+
Added:
| Prescription.Single _ -> single_form ~slot ~prescription:p
302
+
Added:
| Prescription.Pre_exhaust { isolation; compound } ->
303
+
Added:
pair_form ~slot ~prescription:p ~isolation ~compound)
304
+
Added:
outstanding)
305
+
Added:
@ [
306
+
Added:
Form.post_form ~service:Routes.finish
307
+
Added:
(fun () ->
308
+
Added:
[
309
+
Added:
Form.input ~input_type:`Submit ~value:"Finish workout" Form.string;
310
+
Added:
])
311
+
Added:
();
312
+
Added:
])
313
+
Added:
314
+
Added:
let history ~records =
315
+
Added:
shell ~title:"History"
316
+
Added:
[
317
+
Added:
h2 [ txt "History" ];
318
+
Added:
(if records = [] then p [ txt "Nothing logged yet." ]
319
+
Added:
else
320
+
Added:
ul
321
+
Added:
(List.map
322
+
Added:
(fun r ->
323
+
Added:
let e = r.Repository.entry in
324
+
Added:
li
325
+
Added:
[
326
+
Added:
txt
327
+
Added:
(Printf.sprintf "%s — %d stimuli"
328
+
Added:
(Workout_prescription.name (Entry.prescription e))
329
+
Added:
(List.length (Entry.stimuli e)));
330
+
Added:
ul
331
+
Added:
(List.map
332
+
Added:
(fun s -> li [ txt (describe_stimulus s) ])
333
+
Added:
(Entry.stimuli e));
334
+
Added:
])
335
+
Added:
records));
336
+
Added:
]