judu/ex-egopublic Fork
⑂ d74ecb3
Commits
⬇ Clone ▾
git clone https://git.rickub.com/judu/ex-ego.git
git clone ssh://git@rickub.com/judu/ex-ego.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

chore: deduplicate frontend codeUnverified

Julien Durillon committed 2026-08-13T10:20:15+02:00 Browse files
d74ecb3 parent: 5bb1327
modified src/routes/entries.rs +2 -23
@@ -16,7 +16,7 @@ use crate::error::AppError;
1616 use crate::i18n::p1;
1717 use crate::period::{Week, day_label, paris_today};
1818 use crate::render::{HtmlTemplate, Shell};
19-use crate::routes::shell;
19+use crate::routes::{Preset, duration_presets, shell};
2020 use crate::state::AppState;
2121 use crate::team::ActiveTeam;
2222
@@ -72,13 +72,6 @@ pub struct TimerPage {
7272
7373 /// ─── Étape 2 : durée + date ───
7474
75-pub struct Preset {
76- pub label: &'static str,
77- pub href: String,
78- pub minutes: i32,
79- pub selected: bool,
80-}
81-
8275 #[derive(Template)]
8376 #[template(path = "add_step2.html")]
8477 pub struct AddStep2Page {
@@ -184,21 +177,7 @@ pub async fn entry_new(
184177 .or(pending.as_ref().map(|t| t.start_date()))
185178 .unwrap_or_else(paris_today)
186179 .min(paris_today());
187- let presets = [
188- (5, "5 min"),
189- (15, "15 min"),
190- (30, "30 min"),
191- (45, "45 min"),
192- (60, "1 h"),
193- ]
194- .into_iter()
195- .map(|(m, label)| Preset {
196- label,
197- href: step2_href(picked.id, m, date),
198- minutes: m,
199- selected: minutes == m,
200- })
201- .collect();
180+ let presets = duration_presets(minutes, |m| step2_href(picked.id, m, date));
202181 let sh = shell(&state, &ctx, tr.title_add, "add", toast).await?;
203182 return Ok(HtmlTemplate(AddStep2Page {
204183 csrf: ctx.user.csrf_token(&state),
@@ -16,7 +16,7 @@ use crate::error::AppError;
16 use crate::i18n::p1;16 use crate::i18n::p1;
17 use crate::period::{Week, day_label, paris_today};17 use crate::period::{Week, day_label, paris_today};
18 use crate::render::{HtmlTemplate, Shell};18 use crate::render::{HtmlTemplate, Shell};
19-use crate::routes::shell;19+use crate::routes::{Preset, duration_presets, shell};
20 use crate::state::AppState;20 use crate::state::AppState;
21 use crate::team::ActiveTeam;21 use crate::team::ActiveTeam;
22 22
@@ -72,13 +72,6 @@ pub struct TimerPage {
72 72
73 /// ─── Étape 2 : durée + date ───73 /// ─── Étape 2 : durée + date ───
74 74
75-pub struct Preset {
76- pub label: &'static str,
77- pub href: String,
78- pub minutes: i32,
79- pub selected: bool,
80-}
81-
82 #[derive(Template)]75 #[derive(Template)]
83 #[template(path = "add_step2.html")]76 #[template(path = "add_step2.html")]
84 pub struct AddStep2Page {77 pub struct AddStep2Page {
@@ -184,21 +177,7 @@ pub async fn entry_new(
184 .or(pending.as_ref().map(|t| t.start_date()))177 .or(pending.as_ref().map(|t| t.start_date()))
185 .unwrap_or_else(paris_today)178 .unwrap_or_else(paris_today)
186 .min(paris_today());179 .min(paris_today());
187- let presets = [180+ let presets = duration_presets(minutes, |m| step2_href(picked.id, m, date));
188- (5, "5 min"),
189- (15, "15 min"),
190- (30, "30 min"),
191- (45, "45 min"),
192- (60, "1 h"),
193- ]
194- .into_iter()
195- .map(|(m, label)| Preset {
196- label,
197- href: step2_href(picked.id, m, date),
198- minutes: m,
199- selected: minutes == m,
200- })
201- .collect();
202 let sh = shell(&state, &ctx, tr.title_add, "add", toast).await?;181 let sh = shell(&state, &ctx, tr.title_add, "add", toast).await?;
203 return Ok(HtmlTemplate(AddStep2Page {182 return Ok(HtmlTemplate(AddStep2Page {
204 csrf: ctx.user.csrf_token(&state),183 csrf: ctx.user.csrf_token(&state),
modified src/routes/mod.rs +30 -2
@@ -98,8 +98,8 @@ async fn security_headers(req: Request, next: Next) -> Response {
9898 headers.insert("cache-control", HeaderValue::from_static("no-store"));
9999 }
100100 headers.insert("x-frame-options", HeaderValue::from_static("DENY"));
101- // frame-ancestors seulement : une CSP complète casserait le <script>
102- // inline d'add_step2 et les styles inline.
101+ // frame-ancestors seulement : une CSP complète casserait les <script>
102+ // inline (app, stats, todos, todo_form) et les styles inline.
103103 headers.insert(
104104 "content-security-policy",
105105 HeaderValue::from_static("frame-ancestors 'none'"),
@@ -125,6 +125,34 @@ async fn fallback_404() -> AppError {
125125 AppError::NotFound
126126 }
127127
128+/// Preset de durée du sélecteur partagé (macro `duration_picker` de
129+/// templates/components.html) — ajout étape 2 et flux « effectuée ».
130+pub struct Preset {
131+ pub label: &'static str,
132+ pub href: String,
133+ pub minutes: i32,
134+ pub selected: bool,
135+}
136+
137+/// Les presets 5/15/30/45 min/1 h, avec l'href zéro-JS propre à chaque écran.
138+pub fn duration_presets(minutes: i32, href: impl Fn(i32) -> String) -> Vec<Preset> {
139+ [
140+ (5, "5 min"),
141+ (15, "15 min"),
142+ (30, "30 min"),
143+ (45, "45 min"),
144+ (60, "1 h"),
145+ ]
146+ .into_iter()
147+ .map(|(m, label)| Preset {
148+ label,
149+ href: href(m),
150+ minutes: m,
151+ selected: minutes == m,
152+ })
153+ .collect()
154+}
155+
128156 /// Construit le shell (titre + pill d'équipe + username + tab + toast + badge).
129157 pub async fn shell(
130158 state: &AppState,
@@ -98,8 +98,8 @@ async fn security_headers(req: Request, next: Next) -> Response {
98 headers.insert("cache-control", HeaderValue::from_static("no-store"));98 headers.insert("cache-control", HeaderValue::from_static("no-store"));
99 }99 }
100 headers.insert("x-frame-options", HeaderValue::from_static("DENY"));100 headers.insert("x-frame-options", HeaderValue::from_static("DENY"));
101- // frame-ancestors seulement : une CSP complète casserait le <script>101+ // frame-ancestors seulement : une CSP complète casserait les <script>
102- // inline d'add_step2 et les styles inline.102+ // inline (app, stats, todos, todo_form) et les styles inline.
103 headers.insert(103 headers.insert(
104 "content-security-policy",104 "content-security-policy",
105 HeaderValue::from_static("frame-ancestors 'none'"),105 HeaderValue::from_static("frame-ancestors 'none'"),
@@ -125,6 +125,34 @@ async fn fallback_404() -> AppError {
125 AppError::NotFound125 AppError::NotFound
126 }126 }
127 127
128+/// Preset de durée du sélecteur partagé (macro `duration_picker` de
129+/// templates/components.html) — ajout étape 2 et flux « effectuée ».
130+pub struct Preset {
131+ pub label: &'static str,
132+ pub href: String,
133+ pub minutes: i32,
134+ pub selected: bool,
135+}
136+
137+/// Les presets 5/15/30/45 min/1 h, avec l'href zéro-JS propre à chaque écran.
138+pub fn duration_presets(minutes: i32, href: impl Fn(i32) -> String) -> Vec<Preset> {
139+ [
140+ (5, "5 min"),
141+ (15, "15 min"),
142+ (30, "30 min"),
143+ (45, "45 min"),
144+ (60, "1 h"),
145+ ]
146+ .into_iter()
147+ .map(|(m, label)| Preset {
148+ label,
149+ href: href(m),
150+ minutes: m,
151+ selected: minutes == m,
152+ })
153+ .collect()
154+}
155+
128 /// Construit le shell (titre + pill d'équipe + username + tab + toast + badge).156 /// Construit le shell (titre + pill d'équipe + username + tab + toast + badge).
129 pub async fn shell(157 pub async fn shell(
130 state: &AppState,158 state: &AppState,
modified src/routes/todos.rs +7 -26
@@ -17,7 +17,7 @@ use crate::error::AppError;
1717 use crate::i18n::{Tr, p1, p2};
1818 use crate::period::{day_label, day_month, paris_today};
1919 use crate::render::{HtmlTemplate, Shell};
20-use crate::routes::shell;
20+use crate::routes::{Preset, duration_presets, shell};
2121 use crate::schedule;
2222 use crate::state::AppState;
2323 use crate::team::ActiveTeam;
@@ -442,13 +442,6 @@ pub async fn todo_release(
442442
443443 /// ─── Flux « effectuée » (page unique, un seul POST) ───
444444
445-pub struct DonePreset {
446- pub label: &'static str,
447- pub href: String,
448- pub minutes: i32,
449- pub selected: bool,
450-}
451-
452445 #[derive(Template)]
453446 #[template(path = "todo_done.html")]
454447 pub struct TodoDonePage {
@@ -461,7 +454,7 @@ pub struct TodoDonePage {
461454 pub minutes_pretty: String,
462455 pub minus_href: String,
463456 pub plus_href: String,
464- pub presets: Vec<DonePreset>,
457+ pub presets: Vec<Preset>,
465458 pub date: NaiveDate,
466459 /// Borne haute de la date du temps (aujourd'hui) — attribut max de l'input.
467460 pub today: NaiveDate,
@@ -471,7 +464,9 @@ pub struct TodoDonePage {
471464 pub next: NaiveDate,
472465 pub next_label: String,
473466 pub next_prev: String,
474- pub next_next: String,
467+ /// Toujours Some — Option pour partager la macro date_row (flèche → jamais
468+ /// désactivée : la reprogrammation peut viser n'importe quel jour futur).
469+ pub next_next: Option<String>,
475470 /// « Rythme constaté : tous les ~7 jours » / « Pas encore de rythme… »
476471 pub rhythm: String,
477472 }
@@ -530,21 +525,7 @@ pub async fn todo_done_page(
530525 minutes_pretty: fmt_minutes(minutes as i64),
531526 minus_href: done_href(id, (minutes - 5).max(1), date, next),
532527 plus_href: done_href(id, minutes + 5, date, next),
533- presets: [5, 15, 30, 45, 60]
534- .into_iter()
535- .map(|m| DonePreset {
536- label: match m {
537- 5 => "5 min",
538- 15 => "15 min",
539- 30 => "30 min",
540- 45 => "45 min",
541- _ => "1 h",
542- },
543- href: done_href(id, m, date, next),
544- minutes: m,
545- selected: m == minutes,
546- })
547- .collect(),
528+ presets: duration_presets(minutes, |m| done_href(id, m, date, next)),
548529 date,
549530 today,
550531 day_prev: done_href(id, minutes, date - Duration::days(1), next),
@@ -552,7 +533,7 @@ pub async fn todo_done_page(
552533 next,
553534 next_label: day_label(ctx.lang, next),
554535 next_prev: done_href(id, minutes, date, next - Duration::days(1)),
555- next_next: done_href(id, minutes, date, next + Duration::days(1)),
536+ next_next: Some(done_href(id, minutes, date, next + Duration::days(1))),
556537 rhythm,
557538 shell,
558539 }))
@@ -17,7 +17,7 @@ use crate::error::AppError;
17 use crate::i18n::{Tr, p1, p2};17 use crate::i18n::{Tr, p1, p2};
18 use crate::period::{day_label, day_month, paris_today};18 use crate::period::{day_label, day_month, paris_today};
19 use crate::render::{HtmlTemplate, Shell};19 use crate::render::{HtmlTemplate, Shell};
20-use crate::routes::shell;20+use crate::routes::{Preset, duration_presets, shell};
21 use crate::schedule;21 use crate::schedule;
22 use crate::state::AppState;22 use crate::state::AppState;
23 use crate::team::ActiveTeam;23 use crate::team::ActiveTeam;
@@ -442,13 +442,6 @@ pub async fn todo_release(
442 442
443 /// ─── Flux « effectuée » (page unique, un seul POST) ───443 /// ─── Flux « effectuée » (page unique, un seul POST) ───
444 444
445-pub struct DonePreset {
446- pub label: &'static str,
447- pub href: String,
448- pub minutes: i32,
449- pub selected: bool,
450-}
451-
452 #[derive(Template)]445 #[derive(Template)]
453 #[template(path = "todo_done.html")]446 #[template(path = "todo_done.html")]
454 pub struct TodoDonePage {447 pub struct TodoDonePage {
@@ -461,7 +454,7 @@ pub struct TodoDonePage {
461 pub minutes_pretty: String,454 pub minutes_pretty: String,
462 pub minus_href: String,455 pub minus_href: String,
463 pub plus_href: String,456 pub plus_href: String,
464- pub presets: Vec<DonePreset>,457+ pub presets: Vec<Preset>,
465 pub date: NaiveDate,458 pub date: NaiveDate,
466 /// Borne haute de la date du temps (aujourd'hui) — attribut max de l'input.459 /// Borne haute de la date du temps (aujourd'hui) — attribut max de l'input.
467 pub today: NaiveDate,460 pub today: NaiveDate,
@@ -471,7 +464,9 @@ pub struct TodoDonePage {
471 pub next: NaiveDate,464 pub next: NaiveDate,
472 pub next_label: String,465 pub next_label: String,
473 pub next_prev: String,466 pub next_prev: String,
474- pub next_next: String,467+ /// Toujours Some — Option pour partager la macro date_row (flèche → jamais
468+ /// désactivée : la reprogrammation peut viser n'importe quel jour futur).
469+ pub next_next: Option<String>,
475 /// « Rythme constaté : tous les ~7 jours » / « Pas encore de rythme… »470 /// « Rythme constaté : tous les ~7 jours » / « Pas encore de rythme… »
476 pub rhythm: String,471 pub rhythm: String,
477 }472 }
@@ -530,21 +525,7 @@ pub async fn todo_done_page(
530 minutes_pretty: fmt_minutes(minutes as i64),525 minutes_pretty: fmt_minutes(minutes as i64),
531 minus_href: done_href(id, (minutes - 5).max(1), date, next),526 minus_href: done_href(id, (minutes - 5).max(1), date, next),
532 plus_href: done_href(id, minutes + 5, date, next),527 plus_href: done_href(id, minutes + 5, date, next),
533- presets: [5, 15, 30, 45, 60]528+ presets: duration_presets(minutes, |m| done_href(id, m, date, next)),
534- .into_iter()
535- .map(|m| DonePreset {
536- label: match m {
537- 5 => "5 min",
538- 15 => "15 min",
539- 30 => "30 min",
540- 45 => "45 min",
541- _ => "1 h",
542- },
543- href: done_href(id, m, date, next),
544- minutes: m,
545- selected: m == minutes,
546- })
547- .collect(),
548 date,529 date,
549 today,530 today,
550 day_prev: done_href(id, minutes, date - Duration::days(1), next),531 day_prev: done_href(id, minutes, date - Duration::days(1), next),
@@ -552,7 +533,7 @@ pub async fn todo_done_page(
552 next,533 next,
553 next_label: day_label(ctx.lang, next),534 next_label: day_label(ctx.lang, next),
554 next_prev: done_href(id, minutes, date, next - Duration::days(1)),535 next_prev: done_href(id, minutes, date, next - Duration::days(1)),
555- next_next: done_href(id, minutes, date, next + Duration::days(1)),536+ next_next: Some(done_href(id, minutes, date, next + Duration::days(1))),
556 rhythm,537 rhythm,
557 shell,538 shell,
558 }))539 }))
modified templates/add_step2.html +4 -79
@@ -1,4 +1,5 @@
11 {% extends "app.html" %}
2+{% import "components.html" as c %}
23 {% block content %}
34 <a class="btn btn-ghost" style="margin:0 0 var(--space-3);padding-left:0" href="/temps/nouveau?mode=manuel">{{ shell.tr.change_category }}</a>
45 <div class="picked">
@@ -6,91 +7,15 @@
67 <span class="pname">{{ picked_name }}</span>
78 </div>
89
9-<form method="post" action="/temps">
10+<form method="post" action="/temps" data-duration>
1011 <input type="hidden" name="csrf" value="{{ csrf }}">
1112 <input type="hidden" name="categorie" value="{{ categorie }}">
1213
1314 <label class="mini-label" for="minutes">{{ shell.tr.duration_label }}</label>
14- <div class="duration-row">
15- <div class="stepper">
16- <a class="stepper-btn" href="{{ minus_href }}" data-step="-5" aria-label="{{ shell.tr.five_less }}">
17- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round"><path d="M5 12h14"></path></svg>
18- </a>
19- <input class="input minutes-input" id="minutes" name="minutes" type="number" inputmode="numeric"
20- min="1" required value="{{ minutes }}">
21- <a class="stepper-btn" href="{{ plus_href }}" data-step="5" aria-label="{{ shell.tr.five_more }}">
22- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round"><path d="M12 5v14M5 12h14"></path></svg>
23- </a>
24- </div>
25- <div>
26- <div class="unit">{{ shell.tr.minutes_unit }}</div>
27- <div class="pretty" id="minutes-pretty">{{ minutes_pretty }}</div>
28- </div>
29- </div>
30- <script>
31- // Amélioration progressive : ±5 min sans recharger la page.
32- // Sans JS, les liens href rechargent le formulaire pré-rempli.
33- (function () {
34- var input = document.getElementById('minutes');
35- var pretty = document.getElementById('minutes-pretty');
36- function dur(m) {
37- if (!m || m < 1) return '0 min';
38- var h = Math.floor(m / 60), r = m % 60;
39- if (!h) return r + ' min';
40- return r ? h + ' h ' + (r < 10 ? '0' + r : r) : h + ' h';
41- }
42- function refresh() {
43- var v = Math.round(+input.value) || 0;
44- pretty.textContent = dur(v);
45- // État sélectionné des presets suit la valeur du champ.
46- document.querySelectorAll('.preset[data-minutes]').forEach(function (p) {
47- if (+p.dataset.minutes === v) {
48- p.setAttribute('aria-current', 'true');
49- } else {
50- p.removeAttribute('aria-current');
51- }
52- });
53- }
54- document.querySelectorAll('.stepper-btn[data-step]').forEach(function (btn) {
55- btn.addEventListener('click', function (e) {
56- e.preventDefault();
57- var v = Math.round(+input.value) || 0;
58- input.value = Math.max(5, v + (+btn.dataset.step));
59- refresh();
60- });
61- });
62- document.querySelectorAll('.preset[data-minutes]').forEach(function (p) {
63- p.addEventListener('click', function (e) {
64- e.preventDefault();
65- input.value = +p.dataset.minutes;
66- refresh();
67- });
68- });
69- input.addEventListener('input', refresh);
70- })();
71- </script>
72- <div class="presets">
73- {% for p in presets %}
74- <a class="preset" href="{{ p.href }}" data-minutes="{{ p.minutes }}" {% if p.selected %}aria-current="true"{% endif %}>{{ p.label }}</a>
75- {% endfor %}
76- </div>
15+ {% call c::duration_picker(shell, minus_href, plus_href, minutes, minutes_pretty, "", presets) %}{% endcall %}
7716
7817 <label class="mini-label" for="date">{{ shell.tr.date_label }}</label>
79- <div class="date-row">
80- <a class="btn btn-secondary btn-icon" href="{{ day_prev }}" aria-label="{{ shell.tr.day_prev }}">
81- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>
82- </a>
83- <input class="input" id="date" name="date" type="date" value="{{ date }}" required>
84- {% if let Some(next) = day_next %}
85- <a class="btn btn-secondary btn-icon" href="{{ next }}" aria-label="{{ shell.tr.day_next }}">
86- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
87- </a>
88- {% else %}
89- <span class="btn btn-secondary btn-icon" aria-disabled="true">
90- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
91- </span>
92- {% endif %}
93- </div>
18+ {% call c::date_row(shell, "date", "date", date, "", "", day_prev, day_next, false) %}{% endcall %}
9419 <p class="date-label">{{ date_label }}</p>
9520
9621 <button type="submit" class="btn btn-primary btn-block" style="min-height:52px;font-size:16px">{{ shell.tr.title_add }}</button>
@@ -1,4 +1,5 @@
1 {% extends "app.html" %}1 {% extends "app.html" %}
2+{% import "components.html" as c %}
2 {% block content %}3 {% block content %}
3 <a class="btn btn-ghost" style="margin:0 0 var(--space-3);padding-left:0" href="/temps/nouveau?mode=manuel">{{ shell.tr.change_category }}</a>4 <a class="btn btn-ghost" style="margin:0 0 var(--space-3);padding-left:0" href="/temps/nouveau?mode=manuel">{{ shell.tr.change_category }}</a>
4 <div class="picked">5 <div class="picked">
@@ -6,91 +7,15 @@
6 <span class="pname">{{ picked_name }}</span>7 <span class="pname">{{ picked_name }}</span>
7 </div>8 </div>
8 9
9-<form method="post" action="/temps">10+<form method="post" action="/temps" data-duration>
10 <input type="hidden" name="csrf" value="{{ csrf }}">11 <input type="hidden" name="csrf" value="{{ csrf }}">
11 <input type="hidden" name="categorie" value="{{ categorie }}">12 <input type="hidden" name="categorie" value="{{ categorie }}">
12 13
13 <label class="mini-label" for="minutes">{{ shell.tr.duration_label }}</label>14 <label class="mini-label" for="minutes">{{ shell.tr.duration_label }}</label>
14- <div class="duration-row">15+ {% call c::duration_picker(shell, minus_href, plus_href, minutes, minutes_pretty, "", presets) %}{% endcall %}
15- <div class="stepper">
16- <a class="stepper-btn" href="{{ minus_href }}" data-step="-5" aria-label="{{ shell.tr.five_less }}">
17- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round"><path d="M5 12h14"></path></svg>
18- </a>
19- <input class="input minutes-input" id="minutes" name="minutes" type="number" inputmode="numeric"
20- min="1" required value="{{ minutes }}">
21- <a class="stepper-btn" href="{{ plus_href }}" data-step="5" aria-label="{{ shell.tr.five_more }}">
22- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round"><path d="M12 5v14M5 12h14"></path></svg>
23- </a>
24- </div>
25- <div>
26- <div class="unit">{{ shell.tr.minutes_unit }}</div>
27- <div class="pretty" id="minutes-pretty">{{ minutes_pretty }}</div>
28- </div>
29- </div>
30- <script>
31- // Amélioration progressive : ±5 min sans recharger la page.
32- // Sans JS, les liens href rechargent le formulaire pré-rempli.
33- (function () {
34- var input = document.getElementById('minutes');
35- var pretty = document.getElementById('minutes-pretty');
36- function dur(m) {
37- if (!m || m < 1) return '0 min';
38- var h = Math.floor(m / 60), r = m % 60;
39- if (!h) return r + ' min';
40- return r ? h + ' h ' + (r < 10 ? '0' + r : r) : h + ' h';
41- }
42- function refresh() {
43- var v = Math.round(+input.value) || 0;
44- pretty.textContent = dur(v);
45- // État sélectionné des presets suit la valeur du champ.
46- document.querySelectorAll('.preset[data-minutes]').forEach(function (p) {
47- if (+p.dataset.minutes === v) {
48- p.setAttribute('aria-current', 'true');
49- } else {
50- p.removeAttribute('aria-current');
51- }
52- });
53- }
54- document.querySelectorAll('.stepper-btn[data-step]').forEach(function (btn) {
55- btn.addEventListener('click', function (e) {
56- e.preventDefault();
57- var v = Math.round(+input.value) || 0;
58- input.value = Math.max(5, v + (+btn.dataset.step));
59- refresh();
60- });
61- });
62- document.querySelectorAll('.preset[data-minutes]').forEach(function (p) {
63- p.addEventListener('click', function (e) {
64- e.preventDefault();
65- input.value = +p.dataset.minutes;
66- refresh();
67- });
68- });
69- input.addEventListener('input', refresh);
70- })();
71- </script>
72- <div class="presets">
73- {% for p in presets %}
74- <a class="preset" href="{{ p.href }}" data-minutes="{{ p.minutes }}" {% if p.selected %}aria-current="true"{% endif %}>{{ p.label }}</a>
75- {% endfor %}
76- </div>
77 16
78 <label class="mini-label" for="date">{{ shell.tr.date_label }}</label>17 <label class="mini-label" for="date">{{ shell.tr.date_label }}</label>
79- <div class="date-row">18+ {% call c::date_row(shell, "date", "date", date, "", "", day_prev, day_next, false) %}{% endcall %}
80- <a class="btn btn-secondary btn-icon" href="{{ day_prev }}" aria-label="{{ shell.tr.day_prev }}">
81- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>
82- </a>
83- <input class="input" id="date" name="date" type="date" value="{{ date }}" required>
84- {% if let Some(next) = day_next %}
85- <a class="btn btn-secondary btn-icon" href="{{ next }}" aria-label="{{ shell.tr.day_next }}">
86- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
87- </a>
88- {% else %}
89- <span class="btn btn-secondary btn-icon" aria-disabled="true">
90- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
91- </span>
92- {% endif %}
93- </div>
94 <p class="date-label">{{ date_label }}</p>19 <p class="date-label">{{ date_label }}</p>
95 20
96 <button type="submit" class="btn btn-primary btn-block" style="min-height:52px;font-size:16px">{{ shell.tr.title_add }}</button>21 <button type="submit" class="btn btn-primary btn-block" style="min-height:52px;font-size:16px">{{ shell.tr.title_add }}</button>
modified templates/app.html +5 -4
@@ -171,7 +171,8 @@
171171 if (e.key === 'Escape' && openSheet()) location.hash = 'fermer';
172172 });
173173 })();
174-// Amélioration progressive du formulaire « tâche effectuée » (page dédiée ou
174+// Amélioration progressive des formulaires à sélecteur de durée/date
175+// (form[data-duration] : ajout étape 2, tâche effectuée — page dédiée ou
175176 // sheet injectée par l'écran Tâches) : steppers, presets et libellé sans
176177 // recharger. Délégation : fonctionne aussi sur du contenu injecté.
177178 (function () {
@@ -188,7 +189,7 @@
188189 }
189190 document.addEventListener('click', function (e) {
190191 if (!e.target.closest) return;
191- var root = e.target.closest('form[data-done]');
192+ var root = e.target.closest('form[data-duration]');
192193 if (!root) return;
193194 var b = e.target.closest('.stepper-btn[data-step], .preset[data-minutes], a[data-date-step]');
194195 if (!b) return;
@@ -208,8 +209,8 @@
208209 refreshMinutes(root);
209210 });
210211 document.addEventListener('input', function (e) {
211- if (e.target.id === 'minutes' && e.target.closest && e.target.closest('form[data-done]')) {
212- refreshMinutes(e.target.closest('form[data-done]'));
212+ if (e.target.id === 'minutes' && e.target.closest && e.target.closest('form[data-duration]')) {
213+ refreshMinutes(e.target.closest('form[data-duration]'));
213214 }
214215 });
215216 })();
@@ -171,7 +171,8 @@
171 if (e.key === 'Escape' && openSheet()) location.hash = 'fermer';171 if (e.key === 'Escape' && openSheet()) location.hash = 'fermer';
172 });172 });
173 })();173 })();
174-// Amélioration progressive du formulaire « tâche effectuée » (page dédiée ou174+// Amélioration progressive des formulaires à sélecteur de durée/date
175+// (form[data-duration] : ajout étape 2, tâche effectuée — page dédiée ou
175 // sheet injectée par l'écran Tâches) : steppers, presets et libellé sans176 // sheet injectée par l'écran Tâches) : steppers, presets et libellé sans
176 // recharger. Délégation : fonctionne aussi sur du contenu injecté.177 // recharger. Délégation : fonctionne aussi sur du contenu injecté.
177 (function () {178 (function () {
@@ -188,7 +189,7 @@
188 }189 }
189 document.addEventListener('click', function (e) {190 document.addEventListener('click', function (e) {
190 if (!e.target.closest) return;191 if (!e.target.closest) return;
191- var root = e.target.closest('form[data-done]');192+ var root = e.target.closest('form[data-duration]');
192 if (!root) return;193 if (!root) return;
193 var b = e.target.closest('.stepper-btn[data-step], .preset[data-minutes], a[data-date-step]');194 var b = e.target.closest('.stepper-btn[data-step], .preset[data-minutes], a[data-date-step]');
194 if (!b) return;195 if (!b) return;
@@ -208,8 +209,8 @@
208 refreshMinutes(root);209 refreshMinutes(root);
209 });210 });
210 document.addEventListener('input', function (e) {211 document.addEventListener('input', function (e) {
211- if (e.target.id === 'minutes' && e.target.closest && e.target.closest('form[data-done]')) {212+ if (e.target.id === 'minutes' && e.target.closest && e.target.closest('form[data-duration]')) {
212- refreshMinutes(e.target.closest('form[data-done]'));213+ refreshMinutes(e.target.closest('form[data-duration]'));
213 }214 }
214 });215 });
215 })();216 })();
added templates/components.html +75 -0
new file mode 100644
@@ -0,0 +1,75 @@
1+{#- Composants partagés, importés via {% import "components.html" as c %}.
2+ Les macros askama ne voient que leurs arguments : passer `shell`
3+ explicitement. Convention pour les paramètres optionnels de type texte
4+ (max, input_aria) : chaîne vide = attribut absent. -#}
5+
6+{% macro chevron_left(size) %}<svg width="{{ size }}" height="{{ size }}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>{% endmacro %}
7+
8+{% macro chevron_right(size) %}<svg width="{{ size }}" height="{{ size }}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>{% endmacro %}
9+
10+{#- Sélecteur de durée : stepper ± 5 min (liens, zéro JS) + libellé « pretty »
11+ + presets. L'amélioration progressive vit dans app.html, déléguée sur
12+ form[data-duration] : le form appelant doit porter cet attribut. -#}
13+{% macro duration_picker(shell, minus_href, plus_href, minutes, minutes_pretty, input_aria, presets) %}
14+<div class="duration-row">
15+ <div class="stepper">
16+ <a class="stepper-btn" href="{{ minus_href }}" data-step="-5" aria-label="{{ shell.tr.five_less }}">
17+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round"><path d="M5 12h14"></path></svg>
18+ </a>
19+ <input class="input minutes-input" id="minutes" name="minutes" type="number" inputmode="numeric"
20+ min="1" required value="{{ minutes }}"{% if !input_aria.is_empty() %} aria-label="{{ input_aria }}"{% endif %}>
21+ <a class="stepper-btn" href="{{ plus_href }}" data-step="5" aria-label="{{ shell.tr.five_more }}">
22+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round"><path d="M12 5v14M5 12h14"></path></svg>
23+ </a>
24+ </div>
25+ <div>
26+ <div class="unit">{{ shell.tr.minutes_unit }}</div>
27+ <div class="pretty" id="minutes-pretty">{{ minutes_pretty }}</div>
28+ </div>
29+</div>
30+<div class="presets">
31+ {% for p in presets %}
32+ <a class="preset" href="{{ p.href }}" data-minutes="{{ p.minutes }}" {% if p.selected %}aria-current="true"{% endif %}>{{ p.label }}</a>
33+ {% endfor %}
34+</div>
35+{% endmacro %}
36+
37+{#- Rangée date : ← input →. next_href est une Option (None = aujourd'hui,
38+ flèche désactivée). js=true pose les data-attrs du stepping délégué
39+ d'app.html (form[data-duration]) ; js=false = navigation par rechargement. -#}
40+{% macro date_row(shell, id, name, value, max, input_aria, prev_href, next_href, js) %}
41+<div class="date-row">
42+ <a class="btn btn-secondary btn-icon" href="{{ prev_href }}"{% if js %} data-date-step="-1" data-date-for="{{ id }}"{% endif %} aria-label="{{ shell.tr.day_prev }}">
43+ {% call chevron_left(16) %}{% endcall %}
44+ </a>
45+ <input class="input" id="{{ id }}" name="{{ name }}" type="date" value="{{ value }}"{% if !max.is_empty() %} max="{{ max }}"{% endif %} required{% if !input_aria.is_empty() %} aria-label="{{ input_aria }}"{% endif %}>
46+ {% if let Some(next) = next_href %}
47+ <a class="btn btn-secondary btn-icon" href="{{ next }}"{% if js %} data-date-step="1" data-date-for="{{ id }}"{% endif %} aria-label="{{ shell.tr.day_next }}">
48+ {% call chevron_right(16) %}{% endcall %}
49+ </a>
50+ {% else %}
51+ <span class="btn btn-secondary btn-icon" aria-disabled="true">
52+ {% call chevron_right(16) %}{% endcall %}
53+ </span>
54+ {% endif %}
55+</div>
56+{% endmacro %}
57+
58+{#- Navigation ← → d'un graphique de stats (next : Option, None = période
59+ courante, flèche désactivée). -#}
60+{% macro chart_nav(shell, prev, next) %}
61+<div class="chart-nav">
62+ <a class="btn btn-secondary btn-icon-sm" href="{{ prev }}" aria-label="{{ shell.tr.prev_period }}">
63+ {% call chevron_left(15) %}{% endcall %}
64+ </a>
65+ {% if let Some(n) = next %}
66+ <a class="btn btn-secondary btn-icon-sm" href="{{ n }}" aria-label="{{ shell.tr.next_period }}">
67+ {% call chevron_right(15) %}{% endcall %}
68+ </a>
69+ {% else %}
70+ <span class="btn btn-secondary btn-icon-sm" aria-disabled="true">
71+ {% call chevron_right(15) %}{% endcall %}
72+ </span>
73+ {% endif %}
74+</div>
75+{% endmacro %}
new file mode 100644
@@ -0,0 +1,75 @@
1+{#- Composants partagés, importés via {% import "components.html" as c %}.
2+ Les macros askama ne voient que leurs arguments : passer `shell`
3+ explicitement. Convention pour les paramètres optionnels de type texte
4+ (max, input_aria) : chaîne vide = attribut absent. -#}
5+
6+{% macro chevron_left(size) %}<svg width="{{ size }}" height="{{ size }}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>{% endmacro %}
7+
8+{% macro chevron_right(size) %}<svg width="{{ size }}" height="{{ size }}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>{% endmacro %}
9+
10+{#- Sélecteur de durée : stepper ± 5 min (liens, zéro JS) + libellé « pretty »
11+ + presets. L'amélioration progressive vit dans app.html, déléguée sur
12+ form[data-duration] : le form appelant doit porter cet attribut. -#}
13+{% macro duration_picker(shell, minus_href, plus_href, minutes, minutes_pretty, input_aria, presets) %}
14+<div class="duration-row">
15+ <div class="stepper">
16+ <a class="stepper-btn" href="{{ minus_href }}" data-step="-5" aria-label="{{ shell.tr.five_less }}">
17+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round"><path d="M5 12h14"></path></svg>
18+ </a>
19+ <input class="input minutes-input" id="minutes" name="minutes" type="number" inputmode="numeric"
20+ min="1" required value="{{ minutes }}"{% if !input_aria.is_empty() %} aria-label="{{ input_aria }}"{% endif %}>
21+ <a class="stepper-btn" href="{{ plus_href }}" data-step="5" aria-label="{{ shell.tr.five_more }}">
22+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round"><path d="M12 5v14M5 12h14"></path></svg>
23+ </a>
24+ </div>
25+ <div>
26+ <div class="unit">{{ shell.tr.minutes_unit }}</div>
27+ <div class="pretty" id="minutes-pretty">{{ minutes_pretty }}</div>
28+ </div>
29+</div>
30+<div class="presets">
31+ {% for p in presets %}
32+ <a class="preset" href="{{ p.href }}" data-minutes="{{ p.minutes }}" {% if p.selected %}aria-current="true"{% endif %}>{{ p.label }}</a>
33+ {% endfor %}
34+</div>
35+{% endmacro %}
36+
37+{#- Rangée date : ← input →. next_href est une Option (None = aujourd'hui,
38+ flèche désactivée). js=true pose les data-attrs du stepping délégué
39+ d'app.html (form[data-duration]) ; js=false = navigation par rechargement. -#}
40+{% macro date_row(shell, id, name, value, max, input_aria, prev_href, next_href, js) %}
41+<div class="date-row">
42+ <a class="btn btn-secondary btn-icon" href="{{ prev_href }}"{% if js %} data-date-step="-1" data-date-for="{{ id }}"{% endif %} aria-label="{{ shell.tr.day_prev }}">
43+ {% call chevron_left(16) %}{% endcall %}
44+ </a>
45+ <input class="input" id="{{ id }}" name="{{ name }}" type="date" value="{{ value }}"{% if !max.is_empty() %} max="{{ max }}"{% endif %} required{% if !input_aria.is_empty() %} aria-label="{{ input_aria }}"{% endif %}>
46+ {% if let Some(next) = next_href %}
47+ <a class="btn btn-secondary btn-icon" href="{{ next }}"{% if js %} data-date-step="1" data-date-for="{{ id }}"{% endif %} aria-label="{{ shell.tr.day_next }}">
48+ {% call chevron_right(16) %}{% endcall %}
49+ </a>
50+ {% else %}
51+ <span class="btn btn-secondary btn-icon" aria-disabled="true">
52+ {% call chevron_right(16) %}{% endcall %}
53+ </span>
54+ {% endif %}
55+</div>
56+{% endmacro %}
57+
58+{#- Navigation ← → d'un graphique de stats (next : Option, None = période
59+ courante, flèche désactivée). -#}
60+{% macro chart_nav(shell, prev, next) %}
61+<div class="chart-nav">
62+ <a class="btn btn-secondary btn-icon-sm" href="{{ prev }}" aria-label="{{ shell.tr.prev_period }}">
63+ {% call chevron_left(15) %}{% endcall %}
64+ </a>
65+ {% if let Some(n) = next %}
66+ <a class="btn btn-secondary btn-icon-sm" href="{{ n }}" aria-label="{{ shell.tr.next_period }}">
67+ {% call chevron_right(15) %}{% endcall %}
68+ </a>
69+ {% else %}
70+ <span class="btn btn-secondary btn-icon-sm" aria-disabled="true">
71+ {% call chevron_right(15) %}{% endcall %}
72+ </span>
73+ {% endif %}
74+</div>
75+{% endmacro %}
modified templates/settings.html +3 -2
@@ -1,4 +1,5 @@
11 {% extends "app.html" %}
2+{% import "components.html" as c %}
23 {% block content %}
34 {% if !invites.is_empty() %}
45 <div style="margin-bottom:var(--space-6)">
@@ -32,7 +33,7 @@
3233 <span style="display:block;font-family:var(--font-heading);font-size:17px">{{ shell.tr.my_entries }}</span>
3334 <span style="display:block;font-size:13px;opacity:.7">{{ shell.tr.my_entries_desc }}</span>
3435 </span>
35- <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
36+ {% call c::chevron_right(17) %}{% endcall %}
3637 </a>
3738
3839 <a class="card times-link" href="/parametres/categories" style="margin-bottom:var(--space-6)">
@@ -40,7 +41,7 @@
4041 <span style="display:block;font-family:var(--font-heading);font-size:17px">{{ shell.tr.title_categories }}</span>
4142 <span style="display:block;font-size:13px;opacity:.7">{{ shell.tr.manage_categories_desc }}</span>
4243 </span>
43- <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
44+ {% call c::chevron_right(17) %}{% endcall %}
4445 </a>
4546
4647 <form method="post" action="/equipes/renommer">
@@ -1,4 +1,5 @@
1 {% extends "app.html" %}1 {% extends "app.html" %}
2+{% import "components.html" as c %}
2 {% block content %}3 {% block content %}
3 {% if !invites.is_empty() %}4 {% if !invites.is_empty() %}
4 <div style="margin-bottom:var(--space-6)">5 <div style="margin-bottom:var(--space-6)">
@@ -32,7 +33,7 @@
32 <span style="display:block;font-family:var(--font-heading);font-size:17px">{{ shell.tr.my_entries }}</span>33 <span style="display:block;font-family:var(--font-heading);font-size:17px">{{ shell.tr.my_entries }}</span>
33 <span style="display:block;font-size:13px;opacity:.7">{{ shell.tr.my_entries_desc }}</span>34 <span style="display:block;font-size:13px;opacity:.7">{{ shell.tr.my_entries_desc }}</span>
34 </span>35 </span>
35- <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>36+ {% call c::chevron_right(17) %}{% endcall %}
36 </a>37 </a>
37 38
38 <a class="card times-link" href="/parametres/categories" style="margin-bottom:var(--space-6)">39 <a class="card times-link" href="/parametres/categories" style="margin-bottom:var(--space-6)">
@@ -40,7 +41,7 @@
40 <span style="display:block;font-family:var(--font-heading);font-size:17px">{{ shell.tr.title_categories }}</span>41 <span style="display:block;font-family:var(--font-heading);font-size:17px">{{ shell.tr.title_categories }}</span>
41 <span style="display:block;font-size:13px;opacity:.7">{{ shell.tr.manage_categories_desc }}</span>42 <span style="display:block;font-size:13px;opacity:.7">{{ shell.tr.manage_categories_desc }}</span>
42 </span>43 </span>
43- <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>44+ {% call c::chevron_right(17) %}{% endcall %}
44 </a>45 </a>
45 46
46 <form method="post" action="/equipes/renommer">47 <form method="post" action="/equipes/renommer">
modified templates/stats.html +9 -34
@@ -1,4 +1,5 @@
11 {% extends "app.html" %}
2+{% import "components.html" as c %}
23 {% block content %}
34 <a class="btn btn-ghost" style="margin:0 0 var(--space-4);padding-left:0;font-size:16px;min-height:44px" href="/temps">{{ shell.tr.my_entries_arrow }}</a>
45
@@ -20,20 +21,7 @@
2021 <section class="chart-section day-chart">
2122 <div class="chart-head">
2223 <h2>{{ p.day_chart.title }}</h2>
23- <div class="chart-nav">
24- <a class="btn btn-secondary btn-icon-sm" href="{{ p.day_chart.prev }}" aria-label="{{ shell.tr.prev_period }}">
25- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>
26- </a>
27- {% if let Some(next) = p.day_chart.next %}
28- <a class="btn btn-secondary btn-icon-sm" href="{{ next }}" aria-label="{{ shell.tr.next_period }}">
29- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
30- </a>
31- {% else %}
32- <span class="btn btn-secondary btn-icon-sm" aria-disabled="true">
33- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
34- </span>
35- {% endif %}
36- </div>
24+ {% call c::chart_nav(shell, p.day_chart.prev, p.day_chart.next) %}{% endcall %}
3725 </div>
3826 <p class="chart-range">{{ p.day_chart.range }}</p>
3927 {% if !p.day_chart.legend.is_empty() %}
@@ -74,34 +62,21 @@
7462 </ul>
7563 {% endif %}
7664
77- {% for c in p.charts %}
65+ {% for chart in p.charts %}
7866 <section class="chart-section">
7967 <div class="chart-head">
80- <h2>{{ c.title }}</h2>
81- {% if c.nav %}
82- <div class="chart-nav">
83- <a class="btn btn-secondary btn-icon-sm" href="{{ c.prev }}" aria-label="{{ shell.tr.prev_period }}">
84- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>
85- </a>
86- {% if let Some(next) = c.next %}
87- <a class="btn btn-secondary btn-icon-sm" href="{{ next }}" aria-label="{{ shell.tr.next_period }}">
88- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
89- </a>
90- {% else %}
91- <span class="btn btn-secondary btn-icon-sm" aria-disabled="true">
92- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
93- </span>
94- {% endif %}
95- </div>
68+ <h2>{{ chart.title }}</h2>
69+ {% if chart.nav %}
70+ {% call c::chart_nav(shell, chart.prev, chart.next) %}{% endcall %}
9671 {% endif %}
9772 </div>
98- <p class="chart-range">{{ c.range }}</p>
73+ <p class="chart-range">{{ chart.range }}</p>
9974
100- {% if c.empty %}
75+ {% if chart.empty %}
10176 <p class="chart-empty">{{ shell.tr.no_time_period }}</p>
10277 {% else %}
10378 <div class="cols">
104- {% for row in c.rows %}
79+ {% for row in chart.rows %}
10580 <div class="col">
10681 <span class="total">{{ row.total }}</span>
10782 <div class="col-bar" style="--h:{{ row.height_pct }}">
@@ -1,4 +1,5 @@
1 {% extends "app.html" %}1 {% extends "app.html" %}
2+{% import "components.html" as c %}
2 {% block content %}3 {% block content %}
3 <a class="btn btn-ghost" style="margin:0 0 var(--space-4);padding-left:0;font-size:16px;min-height:44px" href="/temps">{{ shell.tr.my_entries_arrow }}</a>4 <a class="btn btn-ghost" style="margin:0 0 var(--space-4);padding-left:0;font-size:16px;min-height:44px" href="/temps">{{ shell.tr.my_entries_arrow }}</a>
4 5
@@ -20,20 +21,7 @@
20 <section class="chart-section day-chart">21 <section class="chart-section day-chart">
21 <div class="chart-head">22 <div class="chart-head">
22 <h2>{{ p.day_chart.title }}</h2>23 <h2>{{ p.day_chart.title }}</h2>
23- <div class="chart-nav">24+ {% call c::chart_nav(shell, p.day_chart.prev, p.day_chart.next) %}{% endcall %}
24- <a class="btn btn-secondary btn-icon-sm" href="{{ p.day_chart.prev }}" aria-label="{{ shell.tr.prev_period }}">
25- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>
26- </a>
27- {% if let Some(next) = p.day_chart.next %}
28- <a class="btn btn-secondary btn-icon-sm" href="{{ next }}" aria-label="{{ shell.tr.next_period }}">
29- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
30- </a>
31- {% else %}
32- <span class="btn btn-secondary btn-icon-sm" aria-disabled="true">
33- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
34- </span>
35- {% endif %}
36- </div>
37 </div>25 </div>
38 <p class="chart-range">{{ p.day_chart.range }}</p>26 <p class="chart-range">{{ p.day_chart.range }}</p>
39 {% if !p.day_chart.legend.is_empty() %}27 {% if !p.day_chart.legend.is_empty() %}
@@ -74,34 +62,21 @@
74 </ul>62 </ul>
75 {% endif %}63 {% endif %}
76 64
77- {% for c in p.charts %}65+ {% for chart in p.charts %}
78 <section class="chart-section">66 <section class="chart-section">
79 <div class="chart-head">67 <div class="chart-head">
80- <h2>{{ c.title }}</h2>68+ <h2>{{ chart.title }}</h2>
81- {% if c.nav %}69+ {% if chart.nav %}
82- <div class="chart-nav">70+ {% call c::chart_nav(shell, chart.prev, chart.next) %}{% endcall %}
83- <a class="btn btn-secondary btn-icon-sm" href="{{ c.prev }}" aria-label="{{ shell.tr.prev_period }}">
84- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>
85- </a>
86- {% if let Some(next) = c.next %}
87- <a class="btn btn-secondary btn-icon-sm" href="{{ next }}" aria-label="{{ shell.tr.next_period }}">
88- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
89- </a>
90- {% else %}
91- <span class="btn btn-secondary btn-icon-sm" aria-disabled="true">
92- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
93- </span>
94- {% endif %}
95- </div>
96 {% endif %}71 {% endif %}
97 </div>72 </div>
98- <p class="chart-range">{{ c.range }}</p>73+ <p class="chart-range">{{ chart.range }}</p>
99 74
100- {% if c.empty %}75+ {% if chart.empty %}
101 <p class="chart-empty">{{ shell.tr.no_time_period }}</p>76 <p class="chart-empty">{{ shell.tr.no_time_period }}</p>
102 {% else %}77 {% else %}
103 <div class="cols">78 <div class="cols">
104- {% for row in c.rows %}79+ {% for row in chart.rows %}
105 <div class="col">80 <div class="col">
106 <span class="total">{{ row.total }}</span>81 <span class="total">{{ row.total }}</span>
107 <div class="col-bar" style="--h:{{ row.height_pct }}">82 <div class="col-bar" style="--h:{{ row.height_pct }}">
modified templates/todo_done.html +6 -47
@@ -1,4 +1,5 @@
11 {% extends "app.html" %}
2+{% import "components.html" as c %}
23 {% block content %}
34 <a class="btn btn-ghost" style="margin:0 0 var(--space-4);padding-left:0" href="/taches">{{ shell.tr.back_todos }}</a>
45
@@ -10,7 +11,7 @@
1011 <span class="done-name">{{ name }}</span>
1112 </div>
1213
13-<form method="post" action="/taches/{{ id }}/terminer" data-done>
14+<form method="post" action="/taches/{{ id }}/terminer" data-done data-duration>
1415 <input type="hidden" name="csrf" value="{{ csrf }}">
1516
1617 <section class="done-sec">
@@ -18,48 +19,14 @@
1819 <h2>{{ shell.tr.duration_spent }}</h2>
1920 <label class="swt-check">
2021 {{ shell.tr.record_time }}
21- <input type="checkbox" name="avec_temps" value="1" checked>
22+ <input type="checkbox" name="avec_temps" value="1">
2223 <span class="swt"><span class="knob"></span></span>
2324 </label>
2425 </div>
2526 <div class="done-body">
26- <div class="duration-row">
27- <div class="stepper">
28- <a class="stepper-btn" href="{{ minus_href }}" data-step="-5" aria-label="{{ shell.tr.five_less }}">
29- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round"><path d="M5 12h14"></path></svg>
30- </a>
31- <input class="input minutes-input" id="minutes" name="minutes" type="number" inputmode="numeric"
32- min="1" required value="{{ minutes }}" aria-label="{{ shell.tr.minutes_spent_aria }}">
33- <a class="stepper-btn" href="{{ plus_href }}" data-step="5" aria-label="{{ shell.tr.five_more }}">
34- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round"><path d="M12 5v14M5 12h14"></path></svg>
35- </a>
36- </div>
37- <div>
38- <div class="unit">{{ shell.tr.minutes_unit }}</div>
39- <div class="pretty" id="minutes-pretty">{{ minutes_pretty }}</div>
40- </div>
41- </div>
42- <div class="presets">
43- {% for p in presets %}
44- <a class="preset" href="{{ p.href }}" data-minutes="{{ p.minutes }}" {% if p.selected %}aria-current="true"{% endif %}>{{ p.label }}</a>
45- {% endfor %}
46- </div>
27+ {% call c::duration_picker(shell, minus_href, plus_href, minutes, minutes_pretty, shell.tr.minutes_spent_aria, presets) %}{% endcall %}
4728 <label class="mini-label" for="done-date" style="margin-top:var(--space-3)">{{ shell.tr.time_date }}</label>
48- <div class="date-row">
49- <a class="btn btn-secondary btn-icon" href="{{ day_prev }}" data-date-step="-1" data-date-for="done-date" aria-label="{{ shell.tr.day_prev }}">
50- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>
51- </a>
52- <input class="input" id="done-date" name="date" type="date" value="{{ date }}" max="{{ today }}" required>
53- {% if let Some(next_href) = day_next %}
54- <a class="btn btn-secondary btn-icon" href="{{ next_href }}" data-date-step="1" data-date-for="done-date" aria-label="{{ shell.tr.day_next }}">
55- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
56- </a>
57- {% else %}
58- <span class="btn btn-secondary btn-icon" aria-disabled="true">
59- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
60- </span>
61- {% endif %}
62- </div>
29+ {% call c::date_row(shell, "done-date", "date", date, today.to_string(), "", day_prev, day_next, true) %}{% endcall %}
6330 </div>
6431 </section>
6532
@@ -74,15 +41,7 @@
7441 </div>
7542 <div class="done-body">
7643 <p style="font-size:13px;opacity:.7;margin:0 0 var(--space-3)">{{ rhythm }}</p>
77- <div class="date-row">
78- <a class="btn btn-secondary btn-icon" href="{{ next_prev }}" data-date-step="-1" data-date-for="next-date" aria-label="{{ shell.tr.day_prev }}">
79- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>
80- </a>
81- <input class="input" id="next-date" name="prochaine" type="date" value="{{ next }}" required aria-label="{{ shell.tr.next_activation_aria }}">
82- <a class="btn btn-secondary btn-icon" href="{{ next_next }}" data-date-step="1" data-date-for="next-date" aria-label="{{ shell.tr.day_next }}">
83- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
84- </a>
85- </div>
44+ {% call c::date_row(shell, "next-date", "prochaine", next, "", shell.tr.next_activation_aria, next_prev, next_next, true) %}{% endcall %}
8645 <p style="font-size:13px;opacity:.7;margin:var(--space-2) 0 0">{{ next_label }}</p>
8746 </div>
8847 </section>
@@ -1,4 +1,5 @@
1 {% extends "app.html" %}1 {% extends "app.html" %}
2+{% import "components.html" as c %}
2 {% block content %}3 {% block content %}
3 <a class="btn btn-ghost" style="margin:0 0 var(--space-4);padding-left:0" href="/taches">{{ shell.tr.back_todos }}</a>4 <a class="btn btn-ghost" style="margin:0 0 var(--space-4);padding-left:0" href="/taches">{{ shell.tr.back_todos }}</a>
4 5
@@ -10,7 +11,7 @@
10 <span class="done-name">{{ name }}</span>11 <span class="done-name">{{ name }}</span>
11 </div>12 </div>
12 13
13-<form method="post" action="/taches/{{ id }}/terminer" data-done>14+<form method="post" action="/taches/{{ id }}/terminer" data-done data-duration>
14 <input type="hidden" name="csrf" value="{{ csrf }}">15 <input type="hidden" name="csrf" value="{{ csrf }}">
15 16
16 <section class="done-sec">17 <section class="done-sec">
@@ -18,48 +19,14 @@
18 <h2>{{ shell.tr.duration_spent }}</h2>19 <h2>{{ shell.tr.duration_spent }}</h2>
19 <label class="swt-check">20 <label class="swt-check">
20 {{ shell.tr.record_time }}21 {{ shell.tr.record_time }}
21- <input type="checkbox" name="avec_temps" value="1" checked>22+ <input type="checkbox" name="avec_temps" value="1">
22 <span class="swt"><span class="knob"></span></span>23 <span class="swt"><span class="knob"></span></span>
23 </label>24 </label>
24 </div>25 </div>
25 <div class="done-body">26 <div class="done-body">
26- <div class="duration-row">27+ {% call c::duration_picker(shell, minus_href, plus_href, minutes, minutes_pretty, shell.tr.minutes_spent_aria, presets) %}{% endcall %}
27- <div class="stepper">
28- <a class="stepper-btn" href="{{ minus_href }}" data-step="-5" aria-label="{{ shell.tr.five_less }}">
29- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round"><path d="M5 12h14"></path></svg>
30- </a>
31- <input class="input minutes-input" id="minutes" name="minutes" type="number" inputmode="numeric"
32- min="1" required value="{{ minutes }}" aria-label="{{ shell.tr.minutes_spent_aria }}">
33- <a class="stepper-btn" href="{{ plus_href }}" data-step="5" aria-label="{{ shell.tr.five_more }}">
34- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round"><path d="M12 5v14M5 12h14"></path></svg>
35- </a>
36- </div>
37- <div>
38- <div class="unit">{{ shell.tr.minutes_unit }}</div>
39- <div class="pretty" id="minutes-pretty">{{ minutes_pretty }}</div>
40- </div>
41- </div>
42- <div class="presets">
43- {% for p in presets %}
44- <a class="preset" href="{{ p.href }}" data-minutes="{{ p.minutes }}" {% if p.selected %}aria-current="true"{% endif %}>{{ p.label }}</a>
45- {% endfor %}
46- </div>
47 <label class="mini-label" for="done-date" style="margin-top:var(--space-3)">{{ shell.tr.time_date }}</label>28 <label class="mini-label" for="done-date" style="margin-top:var(--space-3)">{{ shell.tr.time_date }}</label>
48- <div class="date-row">29+ {% call c::date_row(shell, "done-date", "date", date, today.to_string(), "", day_prev, day_next, true) %}{% endcall %}
49- <a class="btn btn-secondary btn-icon" href="{{ day_prev }}" data-date-step="-1" data-date-for="done-date" aria-label="{{ shell.tr.day_prev }}">
50- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>
51- </a>
52- <input class="input" id="done-date" name="date" type="date" value="{{ date }}" max="{{ today }}" required>
53- {% if let Some(next_href) = day_next %}
54- <a class="btn btn-secondary btn-icon" href="{{ next_href }}" data-date-step="1" data-date-for="done-date" aria-label="{{ shell.tr.day_next }}">
55- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
56- </a>
57- {% else %}
58- <span class="btn btn-secondary btn-icon" aria-disabled="true">
59- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
60- </span>
61- {% endif %}
62- </div>
63 </div>30 </div>
64 </section>31 </section>
65 32
@@ -74,15 +41,7 @@
74 </div>41 </div>
75 <div class="done-body">42 <div class="done-body">
76 <p style="font-size:13px;opacity:.7;margin:0 0 var(--space-3)">{{ rhythm }}</p>43 <p style="font-size:13px;opacity:.7;margin:0 0 var(--space-3)">{{ rhythm }}</p>
77- <div class="date-row">44+ {% call c::date_row(shell, "next-date", "prochaine", next, "", shell.tr.next_activation_aria, next_prev, next_next, true) %}{% endcall %}
78- <a class="btn btn-secondary btn-icon" href="{{ next_prev }}" data-date-step="-1" data-date-for="next-date" aria-label="{{ shell.tr.day_prev }}">
79- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>
80- </a>
81- <input class="input" id="next-date" name="prochaine" type="date" value="{{ next }}" required aria-label="{{ shell.tr.next_activation_aria }}">
82- <a class="btn btn-secondary btn-icon" href="{{ next_next }}" data-date-step="1" data-date-for="next-date" aria-label="{{ shell.tr.day_next }}">
83- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
84- </a>
85- </div>
86 <p style="font-size:13px;opacity:.7;margin:var(--space-2) 0 0">{{ next_label }}</p>45 <p style="font-size:13px;opacity:.7;margin:var(--space-2) 0 0">{{ next_label }}</p>
87 </div>46 </div>
88 </section>47 </section>
modified templates/todo_form.html +3 -2
@@ -1,4 +1,5 @@
11 {% extends "app.html" %}
2+{% import "components.html" as c %}
23 {% block content %}
34 <a class="btn btn-ghost" style="margin:0 0 var(--space-4);padding-left:0" href="/taches">{{ shell.tr.back_todos }}</a>
45
@@ -29,12 +30,12 @@
2930 <label for="todo-date">{{ shell.tr.activation_date }}</label>
3031 <div style="display:flex;gap:var(--space-2);align-items:center">
3132 <button type="button" class="btn btn-secondary btn-icon" data-day="-1" aria-label="{{ shell.tr.day_prev }}" hidden>
32- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>
33+ {% call c::chevron_left(16) %}{% endcall %}
3334 </button>
3435 <input class="input" id="todo-date" name="date" type="date" value="{{ date }}" required
3536 style="flex:1;min-height:46px;text-align:center">
3637 <button type="button" class="btn btn-secondary btn-icon" data-day="1" aria-label="{{ shell.tr.day_next }}" hidden>
37- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>
38+ {% call c::chevron_right(16) %}{% endcall %}
3839 </button>
3940 </div>
4041 </div>
@@ -1,4 +1,5 @@
1 {% extends "app.html" %}1 {% extends "app.html" %}
2+{% import "components.html" as c %}
2 {% block content %}3 {% block content %}
3 <a class="btn btn-ghost" style="margin:0 0 var(--space-4);padding-left:0" href="/taches">{{ shell.tr.back_todos }}</a>4 <a class="btn btn-ghost" style="margin:0 0 var(--space-4);padding-left:0" href="/taches">{{ shell.tr.back_todos }}</a>
4 5
@@ -29,12 +30,12 @@
29 <label for="todo-date">{{ shell.tr.activation_date }}</label>30 <label for="todo-date">{{ shell.tr.activation_date }}</label>
30 <div style="display:flex;gap:var(--space-2);align-items:center">31 <div style="display:flex;gap:var(--space-2);align-items:center">
31 <button type="button" class="btn btn-secondary btn-icon" data-day="-1" aria-label="{{ shell.tr.day_prev }}" hidden>32 <button type="button" class="btn btn-secondary btn-icon" data-day="-1" aria-label="{{ shell.tr.day_prev }}" hidden>
32- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>33+ {% call c::chevron_left(16) %}{% endcall %}
33 </button>34 </button>
34 <input class="input" id="todo-date" name="date" type="date" value="{{ date }}" required35 <input class="input" id="todo-date" name="date" type="date" value="{{ date }}" required
35 style="flex:1;min-height:46px;text-align:center">36 style="flex:1;min-height:46px;text-align:center">
36 <button type="button" class="btn btn-secondary btn-icon" data-day="1" aria-label="{{ shell.tr.day_next }}" hidden>37 <button type="button" class="btn btn-secondary btn-icon" data-day="1" aria-label="{{ shell.tr.day_next }}" hidden>
37- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg>38+ {% call c::chevron_right(16) %}{% endcall %}
38 </button>39 </button>
39 </div>40 </div>
40 </div>41 </div>