[OCaml] High Intensity Training Online
1
open Dream_html
2
open Page_kit
3
4
(* The shell. [viewer] and [request] identify authenticated pages. Auth pages
5
omit the application navigation. *)
6
let theme_session_key = "hito.theme"
7
8
let deployment_version () =
9
match Sys.getenv_opt "HITO_VERSION" with
10
| Some version when version <> "" -> version
11
| _ -> "development"
12
13
let footer () =
14
tag "footer"
15
[ class_ "site-footer" ]
16
[
17
tag "div" [] [ txt "© 2026 Marius PETER. All rights reserved." ];
18
tag "div" []
19
[
20
txt "Version: ";
21
tag "code"
22
[ class_ "deployment-version" ]
23
[ txt "%s" (deployment_version ()) ];
24
];
25
]
26
27
let html_page ?viewer ?request ?(active = "") ?(show_nav = false)
28
?(show_footer = false) ?workout_started title content =
29
let spa_client = true in
30
(* The trainee is logging a workout exactly when its start epoch is present.
31
The middle nav slot then shows the elapsed timer on every page, so the
32
running workout is always visible; otherwise it is the routine link. *)
33
let theme =
34
match request with
35
| Some request -> (
36
match Dream.session_field request theme_session_key with
37
| Some "dark" -> "dark"
38
| _ -> "light")
39
| None -> "light"
40
in
41
let nav_link page path label =
42
let attrs = [ href path ] in
43
let attrs =
44
if spa_client then Dream_html.attr "data-hito-app-link" :: attrs
45
else attrs
46
in
47
let attrs =
48
if String.equal page active then
49
Dream_html.string_attr "aria-current" "page" :: attrs
50
else attrs
51
in
52
tag "a" attrs [ txt "%s" label ]
53
in
54
(* Three always-available actions. The middle slot points to the routine
55
normally. While logging it becomes the elapsed workout timer. The link
56
still carries the trainee to the current workout, so a running workout
57
stays one tap away without adding a fourth destination. The client ticks
58
the value from [data-started]. Without script the link shows a static
59
start. *)
60
let workout_timer_link started =
61
let attrs =
62
[ href Routes.workout; class_ "nav-timer" ]
63
@ (if spa_client then [ Dream_html.attr "data-hito-app-link" ] else [])
64
@ (if String.equal "workout" active then
65
[ Dream_html.string_attr "aria-current" "page" ]
66
else [])
67
@ [
68
Dream_html.attr "data-hito-workout-timer";
69
Dream_html.string_attr "data-started" "%d" started;
70
Dream_html.string_attr "role" "timer";
71
Dream_html.string_attr "aria-label" "Elapsed workout time";
72
]
73
in
74
tag "a" attrs
75
[
76
tag "span"
77
[
78
class_ "nav-timer-value";
79
Dream_html.string_attr "aria-live" "off";
80
Dream_html.attr "data-hito-workout-timer-value";
81
]
82
[ txt "0:00" ];
83
]
84
in
85
let navigation_links () =
86
[
87
nav_link "home" Routes.home "Home";
88
(match workout_started with
89
| Some started -> workout_timer_link started
90
| None -> nav_link "routine" Routes.routine "Routine");
91
nav_link "logbook" Routes.logbook "Logbook";
92
]
93
in
94
let application_navigation () =
95
let visible =
96
match (viewer, request) with
97
| Some (_ : View_model.Viewer.t), Some _ -> true
98
| _ -> show_nav
99
in
100
if visible then
101
[
102
tag "nav"
103
[
104
class_ "app-nav";
105
Dream_html.string_attr "aria-label" "Application navigation";
106
]
107
(navigation_links ());
108
]
109
else []
110
in
111
tag "html"
112
[
113
Dream_html.string_attr "lang" "en";
114
Dream_html.string_attr "data-theme" "%s" theme;
115
]
116
[
117
tag "head" []
118
[
119
tag "title" [] [ txt "Hito — %s" title ];
120
void "meta" [ Dream_html.string_attr "charset" "utf-8" ];
121
void "meta"
122
[
123
Dream_html.string_attr "name" "viewport";
124
Dream_html.string_attr "content"
125
"width=device-width,initial-scale=1";
126
];
127
void "link" [ rel "stylesheet"; href Routes.stylesheet ];
128
void "link"
129
[
130
rel "icon";
131
Dream_html.string_attr "type" "image/svg+xml";
132
href Routes.favicon;
133
];
134
];
135
tag "body"
136
[ Dream_html.string_attr "class" "hito-app" ]
137
([
138
tag "a"
139
[
140
class_ "skip-link"; Dream_html.string_attr "href" "#main-content";
141
]
142
[ txt "Skip to main content" ];
143
tag "div"
144
([
145
Dream_html.string_attr "class" "app-shell page-%s" active;
146
Dream_html.string_attr "data-hito-page-title" "Hito — %s" title;
147
]
148
@
149
if spa_client then [ Dream_html.attr "data-hito-app-shell" ]
150
else [])
151
([
152
tag "header"
153
[ class_ "masthead" ]
154
([
155
tag "a"
156
([ class_ "brand"; href Routes.home ]
157
@
158
if spa_client then
159
[ Dream_html.attr "data-hito-app-link" ]
160
else [])
161
[ txt "hito" ];
162
]
163
@ application_navigation ());
164
tag "main"
165
([ id "main-content"; tabindex "-1" ]
166
@
167
if spa_client then [ Dream_html.attr "data-hito-app-content" ]
168
else [])
169
content;
170
]
171
@ if show_footer then [ footer () ] else []);
172
]
173
@
174
if spa_client then
175
[
176
tag "script"
177
[ src Routes.workout_client; Dream_html.attr "defer" ]
178
[];
179
]
180
else []);
181
]
182