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
642b973 parent: 8d9c362 modified
src/routes/entries.rs +7 -1 | @@ -14,7 +14,7 @@ use crate::db::categories::{self, CategoryRow}; | ||
| 14 | 14 | use crate::db::timers; |
| 15 | 15 | use crate::error::AppError; |
| 16 | 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 | 18 | use crate::render::{HtmlTemplate, Shell}; |
| 19 | 19 | use crate::routes::{Preset, duration_presets, shell}; |
| 20 | 20 | use crate::state::AppState; |
| @@ -92,6 +92,10 @@ pub struct AddStep2Page { | ||
| 92 | 92 | pub presets: Vec<Preset>, |
| 93 | 93 | pub date: NaiveDate, |
| 94 | 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 | 99 | pub day_prev: String, |
| 96 | 100 | /// None = aujourd'hui, flèche désactivée. |
| 97 | 101 | pub day_next: Option<String>, |
| @@ -210,6 +214,8 @@ pub async fn entry_new( | ||
| 210 | 214 | presets, |
| 211 | 215 | date, |
| 212 | 216 | date_label: day_label(ctx.lang, date), |
| 217 | + today: paris_today(), | |
| 218 | + vocab: DateVocab::of(ctx.lang), | |
| 213 | 219 | day_prev: step2_href(picked.id, minutes, date - Duration::days(1)), |
| 214 | 220 | day_next: (date < paris_today()) |
| 215 | 221 | .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 @@ | ||
| 25 | 25 | {% call c::duration_picker(shell, minus_href, plus_href, minutes, minutes_pretty, "", presets) %}{% endcall %} |
| 26 | 26 | |
| 27 | 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 %} | |
| 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> | |
| 30 | 33 | |
| 31 | 34 | <button type="submit" class="btn btn-primary btn-block btn-lg">{{ shell.tr.title_add }}</button> |
| 32 | 35 | </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 @@ | ||
| 185 | 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 | 214 | document.addEventListener('click', function (e) { |
| 189 | 215 | if (!e.target.closest) return; |
| 190 | 216 | var root = e.target.closest('form[data-duration]'); |
| @@ -195,11 +221,12 @@ | ||
| 195 | 221 | if (b.getAttribute('aria-disabled') === 'true') return; |
| 196 | 222 | if (b.dataset.dateStep) { |
| 197 | 223 | 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)); | |
| 200 | 226 | var v = d.toISOString().slice(0, 10); |
| 201 | 227 | if (inp.max && v > inp.max) v = inp.max; |
| 202 | 228 | inp.value = v; |
| 229 | + refreshDate(root, inp); | |
| 203 | 230 | return; |
| 204 | 231 | } |
| 205 | 232 | var min = root.querySelector('#minutes'); |
| @@ -207,10 +234,13 @@ | ||
| 207 | 234 | else { min.value = +b.dataset.minutes; } |
| 208 | 235 | refreshMinutes(root); |
| 209 | 236 | }); |
| 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 | + }); | |
| 214 | 244 | }); |
| 215 | 245 | })(); |
| 216 | 246 | // Amélioration progressive : sur « Tâche effectuée », l'interrupteur d'une |
| @@ -228,6 +258,11 @@ | ||
| 228 | 258 | el.setAttribute('aria-disabled', on ? 'false' : 'true'); |
| 229 | 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 | 267 | document.addEventListener('change', function (e) { |
| 233 | 268 | 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'une | 246 | // 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 @@ | ||
| 36 | 36 | |
| 37 | 37 | {#- Rangée date : ← input →. next_href est une Option (None = aujourd'hui, |
| 38 | 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 | 43 | {% macro date_row(shell, id, name, value, max, input_aria, prev_href, next_href, js) %} |
| 41 | 44 | <div class="date-row"> |
| 42 | 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 | 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 | 51 | {% call chevron_right(16) %}{% endcall %} |
| 49 | 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 | 60 | {% else %} |
| 51 | 61 | <span class="btn btn-secondary btn-icon" aria-disabled="true"> |
| 52 | 62 | {% 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 %} |