judu/ex-egopublic Fork
⑂ 642b973
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

fix(ajout): la flèche de date ne remet plus la durée à 30 minUnverified

Sur l'étape 2, les flèches ← → de date étaient des liens serveur figés
au rendu, donc porteurs de la durée initiale, alors que le stepper de
durée modifie l'input en place. Un pas de date rechargeait la page et
écrasait la durée saisie.

La rangée de date passe au stepping en place déjà utilisé par « Tâche
effectuée » : le JS refait le libellé (motif et noms fournis par le
serveur via DateVocab) et pilote la flèche → selon le plafond. Au
plafond, cette flèche est un <a> inerte role=button que le JS réveille
en lien ; sans JS, les liens serveur restent le parcours.

Sur « Tâche effectuée », la synchro de section repasse par la synchro
de date : la flèche → d'une date au plafond reste éteinte.

Claude-Session: https://claude.ai/code/session_01T6va3mdY9tqrDQYJBVYgT6
Julien Durillon committed 2026-09-04T10:22:22+02:00 Browse files
642b973 parent: 8d9c362
modified src/routes/entries.rs +7 -1
@@ -14,7 +14,7 @@ use crate::db::categories::{self, CategoryRow};
1414 use crate::db::timers;
1515 use crate::error::AppError;
1616 use crate::i18n::p1;
17-use crate::period::{Week, day_label, paris_today};
17+use crate::period::{DateVocab, Week, day_label, paris_today};
1818 use crate::render::{HtmlTemplate, Shell};
1919 use crate::routes::{Preset, duration_presets, shell};
2020 use crate::state::AppState;
@@ -92,6 +92,10 @@ pub struct AddStep2Page {
9292 pub presets: Vec<Preset>,
9393 pub date: NaiveDate,
9494 pub date_label: String,
95+ /// Plafond de l'input date (le JS y borne le stepping en place).
96+ pub today: NaiveDate,
97+ /// Motif + noms du libellé de date, pour que le JS le refasse sans recharger.
98+ pub vocab: DateVocab,
9599 pub day_prev: String,
96100 /// None = aujourd'hui, flèche désactivée.
97101 pub day_next: Option<String>,
@@ -210,6 +214,8 @@ pub async fn entry_new(
210214 presets,
211215 date,
212216 date_label: day_label(ctx.lang, date),
217+ today: paris_today(),
218+ vocab: DateVocab::of(ctx.lang),
213219 day_prev: step2_href(picked.id, minutes, date - Duration::days(1)),
214220 day_next: (date < paris_today())
215221 .then(|| step2_href(picked.id, minutes, date + Duration::days(1))),
@@ -14,7 +14,7 @@ use crate::db::categories::{self, CategoryRow};
14 use crate::db::timers;14 use crate::db::timers;
15 use crate::error::AppError;15 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::{DateVocab, Week, day_label, paris_today};
18 use crate::render::{HtmlTemplate, Shell};18 use crate::render::{HtmlTemplate, Shell};
19 use crate::routes::{Preset, duration_presets, shell};19 use crate::routes::{Preset, duration_presets, shell};
20 use crate::state::AppState;20 use crate::state::AppState;
@@ -92,6 +92,10 @@ pub struct AddStep2Page {
92 pub presets: Vec<Preset>,92 pub presets: Vec<Preset>,
93 pub date: NaiveDate,93 pub date: NaiveDate,
94 pub date_label: String,94 pub date_label: String,
95+ /// Plafond de l'input date (le JS y borne le stepping en place).
96+ pub today: NaiveDate,
97+ /// Motif + noms du libellé de date, pour que le JS le refasse sans recharger.
98+ pub vocab: DateVocab,
95 pub day_prev: String,99 pub day_prev: String,
96 /// None = aujourd'hui, flèche désactivée.100 /// None = aujourd'hui, flèche désactivée.
97 pub day_next: Option<String>,101 pub day_next: Option<String>,
@@ -210,6 +214,8 @@ pub async fn entry_new(
210 presets,214 presets,
211 date,215 date,
212 date_label: day_label(ctx.lang, date),216 date_label: day_label(ctx.lang, date),
217+ today: paris_today(),
218+ vocab: DateVocab::of(ctx.lang),
213 day_prev: step2_href(picked.id, minutes, date - Duration::days(1)),219 day_prev: step2_href(picked.id, minutes, date - Duration::days(1)),
214 day_next: (date < paris_today())220 day_next: (date < paris_today())
215 .then(|| step2_href(picked.id, minutes, date + Duration::days(1))),221 .then(|| step2_href(picked.id, minutes, date + Duration::days(1))),
modified templates/add_step2.html +5 -2
@@ -25,8 +25,11 @@
2525 {% call c::duration_picker(shell, minus_href, plus_href, minutes, minutes_pretty, "", presets) %}{% endcall %}
2626
2727 <label class="mini-label" for="date">{{ shell.tr.date_label }}</label>
28- {% call c::date_row(shell, "date", "date", date, "", "", day_prev, day_next, false) %}{% endcall %}
29- <p class="date-label">{{ date_label }}</p>
28+ {% call c::date_row(shell, "date", "date", date, today.to_string(), "", day_prev, day_next, true) %}{% endcall %}
29+ <p class="date-label" data-date-label-for="date"
30+ data-pattern="{{ vocab.pattern }}" data-weekdays="{{ vocab.weekdays }}"
31+ data-months="{{ vocab.months }}" data-today="{{ vocab.today }}"
32+ data-today-suffix="{{ vocab.today_suffix }}">{{ date_label }}</p>
3033
3134 <button type="submit" class="btn btn-primary btn-block btn-lg">{{ shell.tr.title_add }}</button>
3235 </form>
@@ -25,8 +25,11 @@
25 {% call c::duration_picker(shell, minus_href, plus_href, minutes, minutes_pretty, "", presets) %}{% endcall %}25 {% call c::duration_picker(shell, minus_href, plus_href, minutes, minutes_pretty, "", presets) %}{% endcall %}
26 26
27 <label class="mini-label" for="date">{{ shell.tr.date_label }}</label>27 <label class="mini-label" for="date">{{ shell.tr.date_label }}</label>
28- {% call c::date_row(shell, "date", "date", date, "", "", day_prev, day_next, false) %}{% endcall %}28+ {% call c::date_row(shell, "date", "date", date, today.to_string(), "", day_prev, day_next, true) %}{% endcall %}
29- <p class="date-label">{{ date_label }}</p>29+ <p class="date-label" data-date-label-for="date"
30+ data-pattern="{{ vocab.pattern }}" data-weekdays="{{ vocab.weekdays }}"
31+ data-months="{{ vocab.months }}" data-today="{{ vocab.today }}"
32+ data-today-suffix="{{ vocab.today_suffix }}">{{ date_label }}</p>
30 33
31 <button type="submit" class="btn btn-primary btn-block btn-lg">{{ shell.tr.title_add }}</button>34 <button type="submit" class="btn btn-primary btn-block btn-lg">{{ shell.tr.title_add }}</button>
32 </form>35 </form>
modified templates/app.html +41 -6
@@ -185,6 +185,32 @@
185185 else { p.removeAttribute('aria-current'); }
186186 });
187187 }
188+ // Tout en UTC : les steppers restent justes en travers d'un changement d'heure.
189+ function parseDate(v) {
190+ var p = String(v || '').split('-');
191+ return p.length === 3 ? new Date(Date.UTC(+p[0], +p[1] - 1, +p[2])) : null;
192+ }
193+ // Après un changement de date : la flèche → suit le plafond (max) et le
194+ // libellé est refait avec le motif et les noms rendus par le serveur
195+ // (data-* d'un DateVocab), jamais avec du texte écrit ici.
196+ function refreshDate(root, inp) {
197+ if (inp.disabled) return;
198+ var atMax = !!inp.max && inp.value >= inp.max;
199+ root.querySelectorAll('a[data-date-step="1"][data-date-for="' + inp.id + '"]').forEach(function (a) {
200+ a.setAttribute('aria-disabled', atMax ? 'true' : 'false');
201+ if (!atMax && !a.getAttribute('href')) { a.setAttribute('href', '#'); a.removeAttribute('role'); }
202+ });
203+ var label = root.querySelector('[data-date-label-for="' + inp.id + '"]');
204+ if (!label) return;
205+ var d = parseDate(inp.value);
206+ if (!d) { label.textContent = ''; return; }
207+ var s = label.dataset.pattern
208+ .replace('{w}', label.dataset.weekdays.split('|')[(d.getUTCDay() + 6) % 7])
209+ .replace('{d}', d.getUTCDate())
210+ .replace('{m}', label.dataset.months.split('|')[d.getUTCMonth()]);
211+ label.textContent = s.charAt(0).toUpperCase() + s.slice(1)
212+ + (inp.value === label.dataset.today ? label.dataset.todaySuffix : '');
213+ }
188214 document.addEventListener('click', function (e) {
189215 if (!e.target.closest) return;
190216 var root = e.target.closest('form[data-duration]');
@@ -195,11 +221,12 @@
195221 if (b.getAttribute('aria-disabled') === 'true') return;
196222 if (b.dataset.dateStep) {
197223 var inp = root.querySelector('#' + b.dataset.dateFor);
198- var d = inp.valueAsDate || new Date();
199- d.setDate(d.getDate() + (+b.dataset.dateStep));
224+ var d = parseDate(inp.value) || parseDate(inp.max) || new Date();
225+ d.setUTCDate(d.getUTCDate() + (+b.dataset.dateStep));
200226 var v = d.toISOString().slice(0, 10);
201227 if (inp.max && v > inp.max) v = inp.max;
202228 inp.value = v;
229+ refreshDate(root, inp);
203230 return;
204231 }
205232 var min = root.querySelector('#minutes');
@@ -207,10 +234,13 @@
207234 else { min.value = +b.dataset.minutes; }
208235 refreshMinutes(root);
209236 });
210- document.addEventListener('input', function (e) {
211- if (e.target.id === 'minutes' && e.target.closest && e.target.closest('form[data-duration]')) {
212- refreshMinutes(e.target.closest('form[data-duration]'));
213- }
237+ ['input', 'change'].forEach(function (ev) {
238+ document.addEventListener(ev, function (e) {
239+ var root = e.target.closest && e.target.closest('form[data-duration]');
240+ if (!root) return;
241+ if (e.target.id === 'minutes') refreshMinutes(root);
242+ else if (e.target.type === 'date') refreshDate(root, e.target);
243+ });
214244 });
215245 })();
216246 // Amélioration progressive : sur « Tâche effectuée », l'interrupteur d'une
@@ -228,6 +258,11 @@
228258 el.setAttribute('aria-disabled', on ? 'false' : 'true');
229259 el.tabIndex = on ? 0 : -1;
230260 });
261+ // La flèche → d'une date au plafond doit rester éteinte : on repasse par
262+ // la synchro de date (délégation ci-dessus).
263+ sec.querySelectorAll('.done-body input[type="date"]').forEach(function (el) {
264+ el.dispatchEvent(new Event('change', { bubbles: true }));
265+ });
231266 }
232267 document.addEventListener('change', function (e) {
233268 if (!e.target.closest || !e.target.matches('.swt-check input[type="checkbox"]')) return;
@@ -185,6 +185,32 @@
185 else { p.removeAttribute('aria-current'); }185 else { p.removeAttribute('aria-current'); }
186 });186 });
187 }187 }
188+ // Tout en UTC : les steppers restent justes en travers d'un changement d'heure.
189+ function parseDate(v) {
190+ var p = String(v || '').split('-');
191+ return p.length === 3 ? new Date(Date.UTC(+p[0], +p[1] - 1, +p[2])) : null;
192+ }
193+ // Après un changement de date : la flèche → suit le plafond (max) et le
194+ // libellé est refait avec le motif et les noms rendus par le serveur
195+ // (data-* d'un DateVocab), jamais avec du texte écrit ici.
196+ function refreshDate(root, inp) {
197+ if (inp.disabled) return;
198+ var atMax = !!inp.max && inp.value >= inp.max;
199+ root.querySelectorAll('a[data-date-step="1"][data-date-for="' + inp.id + '"]').forEach(function (a) {
200+ a.setAttribute('aria-disabled', atMax ? 'true' : 'false');
201+ if (!atMax && !a.getAttribute('href')) { a.setAttribute('href', '#'); a.removeAttribute('role'); }
202+ });
203+ var label = root.querySelector('[data-date-label-for="' + inp.id + '"]');
204+ if (!label) return;
205+ var d = parseDate(inp.value);
206+ if (!d) { label.textContent = ''; return; }
207+ var s = label.dataset.pattern
208+ .replace('{w}', label.dataset.weekdays.split('|')[(d.getUTCDay() + 6) % 7])
209+ .replace('{d}', d.getUTCDate())
210+ .replace('{m}', label.dataset.months.split('|')[d.getUTCMonth()]);
211+ label.textContent = s.charAt(0).toUpperCase() + s.slice(1)
212+ + (inp.value === label.dataset.today ? label.dataset.todaySuffix : '');
213+ }
188 document.addEventListener('click', function (e) {214 document.addEventListener('click', function (e) {
189 if (!e.target.closest) return;215 if (!e.target.closest) return;
190 var root = e.target.closest('form[data-duration]');216 var root = e.target.closest('form[data-duration]');
@@ -195,11 +221,12 @@
195 if (b.getAttribute('aria-disabled') === 'true') return;221 if (b.getAttribute('aria-disabled') === 'true') return;
196 if (b.dataset.dateStep) {222 if (b.dataset.dateStep) {
197 var inp = root.querySelector('#' + b.dataset.dateFor);223 var inp = root.querySelector('#' + b.dataset.dateFor);
198- var d = inp.valueAsDate || new Date();224+ var d = parseDate(inp.value) || parseDate(inp.max) || new Date();
199- d.setDate(d.getDate() + (+b.dataset.dateStep));225+ d.setUTCDate(d.getUTCDate() + (+b.dataset.dateStep));
200 var v = d.toISOString().slice(0, 10);226 var v = d.toISOString().slice(0, 10);
201 if (inp.max && v > inp.max) v = inp.max;227 if (inp.max && v > inp.max) v = inp.max;
202 inp.value = v;228 inp.value = v;
229+ refreshDate(root, inp);
203 return;230 return;
204 }231 }
205 var min = root.querySelector('#minutes');232 var min = root.querySelector('#minutes');
@@ -207,10 +234,13 @@
207 else { min.value = +b.dataset.minutes; }234 else { min.value = +b.dataset.minutes; }
208 refreshMinutes(root);235 refreshMinutes(root);
209 });236 });
210- document.addEventListener('input', function (e) {237+ ['input', 'change'].forEach(function (ev) {
211- if (e.target.id === 'minutes' && e.target.closest && e.target.closest('form[data-duration]')) {238+ document.addEventListener(ev, function (e) {
212- refreshMinutes(e.target.closest('form[data-duration]'));239+ var root = e.target.closest && e.target.closest('form[data-duration]');
213- }240+ if (!root) return;
241+ if (e.target.id === 'minutes') refreshMinutes(root);
242+ else if (e.target.type === 'date') refreshDate(root, e.target);
243+ });
214 });244 });
215 })();245 })();
216 // Amélioration progressive : sur « Tâche effectuée », l'interrupteur d'une246 // Amélioration progressive : sur « Tâche effectuée », l'interrupteur d'une
@@ -228,6 +258,11 @@
228 el.setAttribute('aria-disabled', on ? 'false' : 'true');258 el.setAttribute('aria-disabled', on ? 'false' : 'true');
229 el.tabIndex = on ? 0 : -1;259 el.tabIndex = on ? 0 : -1;
230 });260 });
261+ // La flèche → d'une date au plafond doit rester éteinte : on repasse par
262+ // la synchro de date (délégation ci-dessus).
263+ sec.querySelectorAll('.done-body input[type="date"]').forEach(function (el) {
264+ el.dispatchEvent(new Event('change', { bubbles: true }));
265+ });
231 }266 }
232 document.addEventListener('change', function (e) {267 document.addEventListener('change', function (e) {
233 if (!e.target.closest || !e.target.matches('.swt-check input[type="checkbox"]')) return;268 if (!e.target.closest || !e.target.matches('.swt-check input[type="checkbox"]')) return;
modified templates/components.html +11 -1
@@ -36,7 +36,10 @@
3636
3737 {#- Rangée date : ← input →. next_href est une Option (None = aujourd'hui,
3838 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. -#}
39+ d'app.html (form[data-duration]) — max doit alors être renseigné, le JS
40+ y borne le stepping et y rallume la flèche → ; un libellé
41+ [data-date-label-for=id] (data-* d'un DateVocab) est refait à chaque
42+ changement. js=false = navigation par rechargement. -#}
4043 {% macro date_row(shell, id, name, value, max, input_aria, prev_href, next_href, js) %}
4144 <div class="date-row">
4245 <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 }}">
@@ -47,6 +50,13 @@
4750 <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 }}">
4851 {% call chevron_right(16) %}{% endcall %}
4952 </a>
53+ {% else if js %}
54+ {#- Sans href, un <a> est inerte (ni lien ni focus) et sans rôle — d'où le
55+ role="button", qui autorise aria-label/aria-disabled ; le JS le
56+ réveille en lien quand la date repasse sous le plafond. -#}
57+ <a class="btn btn-secondary btn-icon" role="button" aria-disabled="true" data-date-step="1" data-date-for="{{ id }}" aria-label="{{ shell.tr.day_next }}">
58+ {% call chevron_right(16) %}{% endcall %}
59+ </a>
5060 {% else %}
5161 <span class="btn btn-secondary btn-icon" aria-disabled="true">
5262 {% call chevron_right(16) %}{% endcall %}
@@ -36,7 +36,10 @@
36 36
37 {#- Rangée date : ← input →. next_href est une Option (None = aujourd'hui,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é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. -#}39+ d'app.html (form[data-duration]) — max doit alors être renseigné, le JS
40+ y borne le stepping et y rallume la flèche → ; un libellé
41+ [data-date-label-for=id] (data-* d'un DateVocab) est refait à chaque
42+ changement. js=false = navigation par rechargement. -#}
40 {% macro date_row(shell, id, name, value, max, input_aria, prev_href, next_href, js) %}43 {% macro date_row(shell, id, name, value, max, input_aria, prev_href, next_href, js) %}
41 <div class="date-row">44 <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 }}">45 <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 }}">
@@ -47,6 +50,13 @@
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 }}">50 <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 %}51 {% call chevron_right(16) %}{% endcall %}
49 </a>52 </a>
53+ {% else if js %}
54+ {#- Sans href, un <a> est inerte (ni lien ni focus) et sans rôle — d'où le
55+ role="button", qui autorise aria-label/aria-disabled ; le JS le
56+ réveille en lien quand la date repasse sous le plafond. -#}
57+ <a class="btn btn-secondary btn-icon" role="button" aria-disabled="true" data-date-step="1" data-date-for="{{ id }}" aria-label="{{ shell.tr.day_next }}">
58+ {% call chevron_right(16) %}{% endcall %}
59+ </a>
50 {% else %}60 {% else %}
51 <span class="btn btn-secondary btn-icon" aria-disabled="true">61 <span class="btn btn-secondary btn-icon" aria-disabled="true">
52 {% call chevron_right(16) %}{% endcall %}62 {% call chevron_right(16) %}{% endcall %}