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.

Commit
d2a7563101faf07e909b10a67c1588b7e48471ab
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
ENHANCEMENTS.org
index f484dce3..7628c5fa 100644..100644
@@ -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
index 407d1519..499b9d7f 100644..100644
@@ -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
index 3f2c4736..ae1af346 100644..100644
@@ -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
index 5edf382b..856892f4 100644..100644
@@ -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
index f03c5f09..f4836cfc 100644..100644
@@ -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
index d0bdfa0d..9e8d1929 100644..100644
@@ -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 () ->