chore: deduplicate frontend codeUnverified
d74ecb3 parent: 5bb1327 modified
src/routes/entries.rs +2 -23 | @@ -16,7 +16,7 @@ use crate::error::AppError; | ||
| 16 | 16 | use crate::i18n::p1; |
| 17 | 17 | use crate::period::{Week, day_label, paris_today}; |
| 18 | 18 | use crate::render::{HtmlTemplate, Shell}; |
| 19 | -use crate::routes::shell; | |
| 19 | +use crate::routes::{Preset, duration_presets, shell}; | |
| 20 | 20 | use crate::state::AppState; |
| 21 | 21 | use crate::team::ActiveTeam; |
| 22 | 22 | |
| @@ -72,13 +72,6 @@ pub struct TimerPage { | ||
| 72 | 72 | |
| 73 | 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 | 75 | #[derive(Template)] |
| 83 | 76 | #[template(path = "add_step2.html")] |
| 84 | 77 | pub struct AddStep2Page { |
| @@ -184,21 +177,7 @@ pub async fn entry_new( | ||
| 184 | 177 | .or(pending.as_ref().map(|t| t.start_date())) |
| 185 | 178 | .unwrap_or_else(paris_today) |
| 186 | 179 | .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)); | |
| 202 | 181 | let sh = shell(&state, &ctx, tr.title_add, "add", toast).await?; |
| 203 | 182 | return Ok(HtmlTemplate(AddStep2Page { |
| 204 | 183 | 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 { | ||
| 98 | 98 | headers.insert("cache-control", HeaderValue::from_static("no-store")); |
| 99 | 99 | } |
| 100 | 100 | 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. | |
| 103 | 103 | headers.insert( |
| 104 | 104 | "content-security-policy", |
| 105 | 105 | HeaderValue::from_static("frame-ancestors 'none'"), |
| @@ -125,6 +125,34 @@ async fn fallback_404() -> AppError { | ||
| 125 | 125 | 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 | 156 | /// Construit le shell (titre + pill d'équipe + username + tab + toast + badge). |
| 129 | 157 | pub async fn shell( |
| 130 | 158 | 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::NotFound | 125 | 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; | ||
| 17 | 17 | use crate::i18n::{Tr, p1, p2}; |
| 18 | 18 | use crate::period::{day_label, day_month, paris_today}; |
| 19 | 19 | use crate::render::{HtmlTemplate, Shell}; |
| 20 | -use crate::routes::shell; | |
| 20 | +use crate::routes::{Preset, duration_presets, shell}; | |
| 21 | 21 | use crate::schedule; |
| 22 | 22 | use crate::state::AppState; |
| 23 | 23 | use crate::team::ActiveTeam; |
| @@ -442,13 +442,6 @@ pub async fn todo_release( | ||
| 442 | 442 | |
| 443 | 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 | 445 | #[derive(Template)] |
| 453 | 446 | #[template(path = "todo_done.html")] |
| 454 | 447 | pub struct TodoDonePage { |
| @@ -461,7 +454,7 @@ pub struct TodoDonePage { | ||
| 461 | 454 | pub minutes_pretty: String, |
| 462 | 455 | pub minus_href: String, |
| 463 | 456 | pub plus_href: String, |
| 464 | - pub presets: Vec<DonePreset>, | |
| 457 | + pub presets: Vec<Preset>, | |
| 465 | 458 | pub date: NaiveDate, |
| 466 | 459 | /// Borne haute de la date du temps (aujourd'hui) — attribut max de l'input. |
| 467 | 460 | pub today: NaiveDate, |
| @@ -471,7 +464,9 @@ pub struct TodoDonePage { | ||
| 471 | 464 | pub next: NaiveDate, |
| 472 | 465 | pub next_label: String, |
| 473 | 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 | 470 | /// « Rythme constaté : tous les ~7 jours » / « Pas encore de rythme… » |
| 476 | 471 | pub rhythm: String, |
| 477 | 472 | } |
| @@ -530,21 +525,7 @@ pub async fn todo_done_page( | ||
| 530 | 525 | minutes_pretty: fmt_minutes(minutes as i64), |
| 531 | 526 | minus_href: done_href(id, (minutes - 5).max(1), date, next), |
| 532 | 527 | 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)), | |
| 548 | 529 | date, |
| 549 | 530 | today, |
| 550 | 531 | day_prev: done_href(id, minutes, date - Duration::days(1), next), |
| @@ -552,7 +533,7 @@ pub async fn todo_done_page( | ||
| 552 | 533 | next, |
| 553 | 534 | next_label: day_label(ctx.lang, next), |
| 554 | 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 | 537 | rhythm, |
| 557 | 538 | shell, |
| 558 | 539 | })) |
| @@ -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 @@ | ||
| 1 | 1 | {% extends "app.html" %} |
| 2 | +{% import "components.html" as c %} | |
| 2 | 3 | {% block content %} |
| 3 | 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 | 5 | <div class="picked"> |
| @@ -6,91 +7,15 @@ | ||
| 6 | 7 | <span class="pname">{{ picked_name }}</span> |
| 7 | 8 | </div> |
| 8 | 9 | |
| 9 | -<form method="post" action="/temps"> | |
| 10 | +<form method="post" action="/temps" data-duration> | |
| 10 | 11 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 11 | 12 | <input type="hidden" name="categorie" value="{{ categorie }}"> |
| 12 | 13 | |
| 13 | 14 | <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 %} | |
| 77 | 16 | |
| 78 | 17 | <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 %} | |
| 94 | 19 | <p class="date-label">{{ date_label }}</p> |
| 95 | 20 | |
| 96 | 21 | <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 @@ | ||
| 171 | 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 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 | |
| 175 | 176 | // sheet injectée par l'écran Tâches) : steppers, presets et libellé sans |
| 176 | 177 | // recharger. Délégation : fonctionne aussi sur du contenu injecté. |
| 177 | 178 | (function () { |
| @@ -188,7 +189,7 @@ | ||
| 188 | 189 | } |
| 189 | 190 | document.addEventListener('click', function (e) { |
| 190 | 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 | 193 | if (!root) return; |
| 193 | 194 | var b = e.target.closest('.stepper-btn[data-step], .preset[data-minutes], a[data-date-step]'); |
| 194 | 195 | if (!b) return; |
| @@ -208,8 +209,8 @@ | ||
| 208 | 209 | refreshMinutes(root); |
| 209 | 210 | }); |
| 210 | 211 | 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]')); | |
| 213 | 214 | } |
| 214 | 215 | }); |
| 215 | 216 | })(); |
| @@ -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 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 | ||
| 175 | // sheet injectée par l'écran Tâches) : steppers, presets et libellé sans | 176 | // 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 @@ | ||
| 1 | 1 | {% extends "app.html" %} |
| 2 | +{% import "components.html" as c %} | |
| 2 | 3 | {% block content %} |
| 3 | 4 | {% if !invites.is_empty() %} |
| 4 | 5 | <div style="margin-bottom:var(--space-6)"> |
| @@ -32,7 +33,7 @@ | ||
| 32 | 33 | <span style="display:block;font-family:var(--font-heading);font-size:17px">{{ shell.tr.my_entries }}</span> |
| 33 | 34 | <span style="display:block;font-size:13px;opacity:.7">{{ shell.tr.my_entries_desc }}</span> |
| 34 | 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 | 37 | </a> |
| 37 | 38 | |
| 38 | 39 | <a class="card times-link" href="/parametres/categories" style="margin-bottom:var(--space-6)"> |
| @@ -40,7 +41,7 @@ | ||
| 40 | 41 | <span style="display:block;font-family:var(--font-heading);font-size:17px">{{ shell.tr.title_categories }}</span> |
| 41 | 42 | <span style="display:block;font-size:13px;opacity:.7">{{ shell.tr.manage_categories_desc }}</span> |
| 42 | 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 | 45 | </a> |
| 45 | 46 | |
| 46 | 47 | <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 @@ | ||
| 1 | 1 | {% extends "app.html" %} |
| 2 | +{% import "components.html" as c %} | |
| 2 | 3 | {% block content %} |
| 3 | 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 | 21 | <section class="chart-section day-chart"> |
| 21 | 22 | <div class="chart-head"> |
| 22 | 23 | <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 %} | |
| 37 | 25 | </div> |
| 38 | 26 | <p class="chart-range">{{ p.day_chart.range }}</p> |
| 39 | 27 | {% if !p.day_chart.legend.is_empty() %} |
| @@ -74,34 +62,21 @@ | ||
| 74 | 62 | </ul> |
| 75 | 63 | {% endif %} |
| 76 | 64 | |
| 77 | - {% for c in p.charts %} | |
| 65 | + {% for chart in p.charts %} | |
| 78 | 66 | <section class="chart-section"> |
| 79 | 67 | <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 %} | |
| 96 | 71 | {% endif %} |
| 97 | 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 | 76 | <p class="chart-empty">{{ shell.tr.no_time_period }}</p> |
| 102 | 77 | {% else %} |
| 103 | 78 | <div class="cols"> |
| 104 | - {% for row in c.rows %} | |
| 79 | + {% for row in chart.rows %} | |
| 105 | 80 | <div class="col"> |
| 106 | 81 | <span class="total">{{ row.total }}</span> |
| 107 | 82 | <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 @@ | ||
| 1 | 1 | {% extends "app.html" %} |
| 2 | +{% import "components.html" as c %} | |
| 2 | 3 | {% block content %} |
| 3 | 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 | 11 | <span class="done-name">{{ name }}</span> |
| 11 | 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 | 15 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 15 | 16 | |
| 16 | 17 | <section class="done-sec"> |
| @@ -18,48 +19,14 @@ | ||
| 18 | 19 | <h2>{{ shell.tr.duration_spent }}</h2> |
| 19 | 20 | <label class="swt-check"> |
| 20 | 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 | 23 | <span class="swt"><span class="knob"></span></span> |
| 23 | 24 | </label> |
| 24 | 25 | </div> |
| 25 | 26 | <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 %} | |
| 47 | 28 | <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 %} | |
| 63 | 30 | </div> |
| 64 | 31 | </section> |
| 65 | 32 | |
| @@ -74,15 +41,7 @@ | ||
| 74 | 41 | </div> |
| 75 | 42 | <div class="done-body"> |
| 76 | 43 | <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 %} | |
| 86 | 45 | <p style="font-size:13px;opacity:.7;margin:var(--space-2) 0 0">{{ next_label }}</p> |
| 87 | 46 | </div> |
| 88 | 47 | </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 @@ | ||
| 1 | 1 | {% extends "app.html" %} |
| 2 | +{% import "components.html" as c %} | |
| 2 | 3 | {% block content %} |
| 3 | 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 | 30 | <label for="todo-date">{{ shell.tr.activation_date }}</label> |
| 30 | 31 | <div style="display:flex;gap:var(--space-2);align-items:center"> |
| 31 | 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 | 34 | </button> |
| 34 | 35 | <input class="input" id="todo-date" name="date" type="date" value="{{ date }}" required |
| 35 | 36 | style="flex:1;min-height:46px;text-align:center"> |
| 36 | 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 | 39 | </button> |
| 39 | 40 | </div> |
| 40 | 41 | </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 }}" required | 35 | <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> |