[OCaml] High Intensity Training Online
feat reduce navigation to three toggling actions
Replace the four fixed links with three: Home, a middle action, and the username history link. The middle action points to the routine when idle and to the workout in progress while logging, so a running workout stays reachable without a fourth destination. Handlers pass a logging flag drawn from the in-progress workout; the bottom nav becomes a three-column grid.
Changed files
ENHANCEMENTS.org
@@ -5,7 +5,7 @@
5
5
** DONE Remove all doctrine-note elements
6
6
** DONE Use one desktop-to-mobile breakpoint
7
7
Desktop has top navigation. Mobile has an identical bottom navigation.
8
Removed:
** TODO Use three always-available navigation actions
8
Added:
** DONE Use three always-available navigation actions
9
9
Use `Home`, `Routine` when the user is not logging (changing to `Current Workout` while logging), and `<username>` for the workout log. Keep all three actions available in both modes.
10
10
** TODO Simplify the top bar brand
11
11
Replace the top bar brand with `hito` in Work Sans, without a subtitle.
@@ -13,3 +13,9 @@
13
13
Do not show a hamburger menu on mobile.
14
14
** TODO Add placeholders to load and reps fields
15
15
Add placeholder values to the load and reps fields on the workout logging page.
16
Added:
** TODO Remove the redundant fieldset legend
17
Added:
Remove the redundant fieldset legend from workout logging.
18
Added:
** TODO Add animations to routine accordions
19
Added:
Add animations to the routine accordions.
20
Added:
** TODO Style workout accordions with CSS
21
Added:
Style the workout accordions with CSS.
lib/web/assets/hito.css
@@ -529,7 +529,7 @@
529
529
bottom: 0;
530
530
left: 0;
531
531
display: grid;
532
Removed:
grid-template-columns: repeat(4, minmax(0, 1fr));
532
Added:
grid-template-columns: repeat(3, minmax(0, 1fr));
533
533
gap: 1px;
534
534
border-top: 2px solid var(--ink);
535
535
background: var(--ink);
lib/web/handlers.ml
@@ -231,7 +231,7 @@
231
231
Service.active_routine t.service trainee.Trainee.id >>= function
232
232
| None ->
233
233
Lwt.return
234
Removed:
(Pages.choose_routine request ~trainee
234
Added:
(Pages.choose_routine request ~logging:false ~trainee
235
235
~routines:(Service.list_routines t.service))
236
236
| Some (routine, selected) -> (
237
237
Service.next_workout t.service trainee.Trainee.id ~routine
@@ -251,8 +251,11 @@
251
251
let home t trainee request = page t trainee request >>= html
252
252
253
253
let routines t trainee request =
254
Added:
Service.in_progress t.service trainee.Trainee.id >>= fun in_progress ->
254
255
html
255
Removed:
(Pages.choose_routine request ~trainee
256
Added:
(Pages.choose_routine request
257
Added:
~logging:(Option.is_some in_progress)
258
Added:
~trainee
256
259
~routines:(Service.list_routines t.service))
257
260
258
261
let select_routine t trainee request id =
@@ -266,8 +269,13 @@
266
269
| Error _ -> not_found Present.unknown_routine)
267
270
268
271
let routine t trainee request =
272
Added:
Service.in_progress t.service trainee.Trainee.id >>= fun in_progress ->
269
273
Service.active_routine t.service trainee.Trainee.id >>= function
270
Removed:
| Some (_, routine) -> html (Pages.routine request ~trainee routine)
274
Added:
| Some (_, routine) ->
275
Added:
html
276
Added:
(Pages.routine request
277
Added:
~logging:(Option.is_some in_progress)
278
Added:
~trainee routine)
271
279
| None -> redirect_to request Routes.routines
272
280
273
281
let routes t =
@@ -391,15 +399,22 @@
391
399
392
400
module History = struct
393
401
let index t trainee request =
402
Added:
Service.in_progress t.service trainee.Trainee.id >>= fun in_progress ->
394
403
Service.history t.service trainee.Trainee.id >>= fun records ->
395
Removed:
html (Pages.history request ~trainee records)
404
Added:
html
405
Added:
(Pages.history request
406
Added:
~logging:(Option.is_some in_progress)
407
Added:
~trainee records)
396
408
397
409
let show t trainee request id =
410
Added:
Service.in_progress t.service trainee.Trainee.id >>= fun in_progress ->
398
411
record_target t trainee.Trainee.id id >>= function
399
412
| None -> not_found Present.unknown_record
400
413
| Some (_, workout) ->
401
414
html
402
Removed:
(Pages.workout request ~trainee ~record_id:(Some id)
415
Added:
(Pages.workout request ~trainee
416
Added:
~logging:(Option.is_some in_progress)
417
Added:
~record_id:(Some id)
403
418
~active_slot:(active_slot request workout)
404
419
workout)
405
420
lib/web/pages.ml
@@ -22,7 +22,8 @@
22
22
(* The shell. [trainee] and [request] are present on authenticated pages, which
23
23
then show a logout control and the username on the history link. Auth pages
24
24
omit both. *)
25
Removed:
let html_page ?trainee ?request ?(active = "") title content =
25
Added:
let html_page ?trainee ?request ?(active = "") ?(logging = false) title content
26
Added:
=
26
27
let spa_client = true in
27
28
let nav_link page path label =
28
29
let attrs = [ href path ] in
@@ -37,11 +38,14 @@
37
38
in
38
39
tag "a" attrs [ txt "%s" label ]
39
40
in
41
Added:
(* Three always-available actions. The middle slot points to the routine
42
Added:
normally and to the workout in progress while logging, so a running
43
Added:
workout stays one tap away without adding a fourth destination. *)
40
44
let navigation_links username =
41
45
[
42
Removed:
nav_link "home" Routes.home "Overview";
43
Removed:
nav_link "routine" Routes.routine "Routine";
44
Removed:
nav_link "workout" Routes.workout "Current workout";
46
Added:
nav_link "home" Routes.home "Home";
47
Added:
(if logging then nav_link "workout" Routes.workout "Current workout"
48
Added:
else nav_link "routine" Routes.routine "Routine");
45
49
nav_link "history" Routes.history username;
46
50
]
47
51
in
@@ -355,8 +359,8 @@
355
359
option "static" "then a static hold";
356
360
]
357
361
358
Removed:
let choose_routine request ~trainee ~routines =
359
Removed:
html_page ~trainee ~request ~active:"home" "Choose a routine"
362
Added:
let choose_routine request ~logging ~trainee ~routines =
363
Added:
html_page ~trainee ~request ~active:"home" ~logging "Choose a routine"
360
364
[
361
365
tag "h1" [] [ txt "Routines" ];
362
366
tag "div" []
@@ -438,8 +442,8 @@
438
442
]
439
443
@ gate)
440
444
441
Removed:
let routine request ~trainee routine =
442
Removed:
html_page ~trainee ~request ~active:"routine" "Routine"
445
Added:
let routine request ?(logging = false) ~trainee routine =
446
Added:
html_page ~trainee ~request ~active:"routine" ~logging "Routine"
443
447
[
444
448
tag "h1" [] [ txt "%s" (Prescription.Routine.name routine) ];
445
449
tag "div"
@@ -668,9 +672,13 @@
668
672
in
669
673
Printf.sprintf "%s?slot=%d" base slot
670
674
671
Removed:
let workout request ~trainee ?(errors = []) ?editing ~record_id ~active_slot
672
Removed:
workout =
675
Added:
let workout request ~trainee ?(errors = []) ?editing ?logging ~record_id
676
Added:
~active_slot workout =
673
677
let enhanced = Option.is_none record_id in
678
Added:
(* A live workout view is itself the workout in progress, so the middle nav
679
Added:
action reads "Current workout". A saved-record view defers to the handler,
680
Added:
which knows whether a separate workout is in progress. *)
681
Added:
let logging = Option.value logging ~default:enhanced in
674
682
let prescription = Evidence.Workout.prescription workout in
675
683
let performed = Evidence.Workout.performed workout in
676
684
let prescribed_slots = Prescription.Workout.stimuli prescription in
@@ -833,7 +841,7 @@
833
841
];
834
842
]
835
843
in
836
Removed:
html_page ~trainee ~request ~active:"workout"
844
Added:
html_page ~trainee ~request ~active:"workout" ~logging
837
845
(Prescription.Workout.name prescription)
838
846
[
839
847
tag "div"
@@ -877,8 +885,8 @@
877
885
| (slot, _) :: _ -> slot
878
886
| [] -> 0
879
887
880
Removed:
let history request ~trainee records =
881
Removed:
html_page ~trainee ~request ~active:"history" "History"
888
Added:
let history request ?(logging = false) ~trainee records =
889
Added:
html_page ~trainee ~request ~active:"history" ~logging "History"
882
890
[
883
891
tag "h1" [] [ txt "History" ];
884
892
(if records = [] then tag "p" [] [ txt "Nothing logged yet." ]
lib/web/pages.mli
@@ -10,6 +10,7 @@
10
10
11
11
val choose_routine :
12
12
Dream.request ->
13
Added:
logging:bool ->
13
14
trainee:Trainee.t ->
14
15
routines:(Repository.routine_id * Prescription.Routine.t) list ->
15
16
page
@@ -24,13 +25,18 @@
24
25
page
25
26
26
27
val routine :
27
Removed:
Dream.request -> trainee:Trainee.t -> Prescription.Routine.t -> page
28
Added:
Dream.request ->
29
Added:
?logging:bool ->
30
Added:
trainee:Trainee.t ->
31
Added:
Prescription.Routine.t ->
32
Added:
page
28
33
29
34
val workout :
30
35
Dream.request ->
31
36
trainee:Trainee.t ->
32
37
?errors:(string * string) list ->
33
38
?editing:int ->
39
Added:
?logging:bool ->
34
40
record_id:string option ->
35
41
active_slot:int ->
36
42
Evidence.Workout.t ->
@@ -41,13 +47,18 @@
41
47
saved-record edits use the generic authenticated form path. [active_slot]
42
48
button; a handler clamps a requested slot and falls back to {!default_slot}.
43
49
[record_id] is [None] for the workout in progress and [Some id] for a saved
44
Removed:
history record. *)
50
Added:
history record. [logging] sets the middle navigation action; it defaults to
51
Added:
a live workout view (true when [record_id] is [None]). *)
45
52
46
53
val default_slot : Evidence.Workout.t -> int
47
54
(** The slot a workout view opens on: the first slot still awaiting a record, or
48
55
the first slot when every slot is filled. *)
49
56
50
57
val history :
51
Removed:
Dream.request -> trainee:Trainee.t -> Repository.record list -> page
58
Added:
Dream.request ->
59
Added:
?logging:bool ->
60
Added:
trainee:Trainee.t ->
61
Added:
Repository.record list ->
62
Added:
page
52
63
53
64
val problem : title:string -> detail:string -> page
test/test_web.ml
@@ -237,13 +237,13 @@
237
237
String.sub page start (stop - start)
238
238
in
239
239
Alcotest.(check bool)
240
Removed:
"menu links to the overview" true
240
Added:
"menu links to the home overview" true
241
241
(contains ~substring:"href=\"/\"" panel);
242
242
Alcotest.(check bool)
243
Removed:
"menu links to the routine" true
243
Added:
"menu links to the routine when not logging" true
244
244
(contains ~substring:"href=\"/routine\"" panel);
245
245
Alcotest.(check bool)
246
Removed:
"menu links to the current workout" true
246
Added:
"menu omits the workout link when not logging" false
247
247
(contains ~substring:"href=\"/workout\"" panel);
248
248
Alcotest.(check bool)
249
249
"menu links to the history" true
@@ -257,6 +257,40 @@
257
257
Alcotest.(check bool)
258
258
"the menu sign-out is labelled" true
259
259
(contains ~substring:"Sign out" panel) );
260
Added:
( "navigation offers three actions that toggle with logging",
261
Added:
`Quick,
262
Added:
fun () ->
263
Added:
let c = client () in
264
Added:
let _ = sign_in_new c in
265
Added:
(* Not logging: the middle action is Routine, labelled "Routine",
266
Added:
and no workout link is present. The three actions are Home,
267
Added:
Routine, and the username history link. *)
268
Added:
let idle = body (get c "/") in
269
Added:
Alcotest.(check bool)
270
Added:
"labels the home action" true
271
Added:
(contains ~substring:">Home</a>" idle);
272
Added:
Alcotest.(check bool)
273
Added:
"offers the routine action when idle" true
274
Added:
(contains ~substring:">Routine</a>" idle);
275
Added:
Alcotest.(check bool)
276
Added:
"hides the current workout action when idle" false
277
Added:
(contains ~substring:">Current workout</a>" idle);
278
Added:
(* Start a workout, then the middle action becomes Current workout,
279
Added:
pointing at /workout, and the standalone Routine action is gone. *)
280
Added:
let token = Option.get (csrf_token idle) in
281
Added:
let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in
282
Added:
let home_page = body (get c "/") in
283
Added:
let token = Option.get (csrf_token home_page) in
284
Added:
let _ =
285
Added:
post c "/workout" [ ("dream.csrf", token); ("override", "false") ]
286
Added:
in
287
Added:
let logging = body (get c "/history") in
288
Added:
Alcotest.(check bool)
289
Added:
"swaps in the current workout action while logging" true
290
Added:
(contains ~substring:">Current workout</a>" logging);
291
Added:
Alcotest.(check bool)
292
Added:
"drops the routine action while logging" false
293
Added:
(contains ~substring:">Routine</a>" logging) );
260
294
( "auth pages omit the hamburger menu",
261
295
`Quick,
262
296
fun () ->