From f3cc3fbe78146894ba9c46cf7aa0f5364ea25567 Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Sun, 7 Sep 2025 17:18:36 +0000 Subject: [PATCH 01/10] Add web/static/js/swipe-nav.js --- web/static/js/swipe-nav.js | 117 +++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 web/static/js/swipe-nav.js diff --git a/web/static/js/swipe-nav.js b/web/static/js/swipe-nav.js new file mode 100644 index 0000000..2e964fa --- /dev/null +++ b/web/static/js/swipe-nav.js @@ -0,0 +1,117 @@ +/* Swipe/Keyboard navigation for Entry View + - Respects localStorage 'swipeNavEnabled' (default ON) + - Touch swipe on mobile, ArrowLeft/ArrowRight on keyboards + - Edge protection and input-safe +*/ +(function () { + // Feature flag (client-side). Default ON. + let swipeEnabled = true; + try { swipeEnabled = (localStorage.getItem('swipeNavEnabled') !== 'false'); } catch (e) {} + + // Only attach if enabled + if (!swipeEnabled) return; + + // Forms to submit for previous/next (your existing IDs) + const prevForm = document.getElementById('navPrevForm'); + const nextForm = document.getElementById('navNextForm'); + + // Create toast + hints lazily to keep HTML clean + const toast = document.createElement('div'); + toast.id = 'toast-swipe-error'; + toast.style.cssText = [ + 'position:fixed', 'left:50%', 'bottom:16px', 'transform:translateX(-50%)', + 'padding:10px 14px', 'border-radius:10px', 'background:#111827', 'color:#fff', + 'box-shadow:0 6px 20px rgba(0,0,0,.25)', 'opacity:0', 'pointer-events:none', + 'transition:opacity .25s', 'z-index:1200', 'font-size:14px' + ].join(';'); + toast.textContent = "Can't navigate here."; + document.body.appendChild(toast); + + function showToast() { + toast.style.opacity = '1'; + setTimeout(() => { toast.style.opacity = '0'; }, 1200); + } + + // Only show swipe hints on touch devices, once per session + const isTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0); + let hintL, hintR; + if (isTouch) { + try { + if (!sessionStorage.getItem('swipeHintShown')) { + sessionStorage.setItem('swipeHintShown', '1'); + + const base = 'position:fixed;width:34px;height:34px;display:flex;align-items:center;justify-content:center;' + + 'background:rgba(0,0,0,.35);color:#fff;border-radius:999px;box-shadow:0 6px 20px rgba(0,0,0,.25);' + + 'z-index:1100;opacity:0;transform:scale(.9);transition:opacity .25s,transform .25s;top:45%'; + + hintL = document.createElement('div'); + hintL.style.cssText = base + ';left:10px;'; + hintL.textContent = '‹'; + document.body.appendChild(hintL); + + hintR = document.createElement('div'); + hintR.style.cssText = base + ';right:10px;'; + hintR.textContent = '›'; + document.body.appendChild(hintR); + + setTimeout(() => { hintL.style.opacity = '1'; hintR.style.opacity = '1'; hintL.style.transform = 'scale(1)'; hintR.style.transform = 'scale(1)'; }, 300); + setTimeout(() => { hintL.style.opacity = '0'; hintR.style.opacity = '0'; }, 2200); + } + } catch (e) {} + } + + function navigate(dir) { + try { + if (dir < 0 && prevForm) { prevForm.submit(); return true; } + if (dir > 0 && nextForm) { nextForm.submit(); return true; } + } catch (e) {} + showToast(); + return false; + } + + // Keyboard shortcuts + document.addEventListener('keydown', function (e) { + if (!swipeEnabled) return; + const tag = (e.target && e.target.tagName) || ''; + if (/INPUT|TEXTAREA|SELECT/.test(tag)) return; + if (e.key === 'ArrowLeft') { e.preventDefault(); navigate(-1); } + if (e.key === 'ArrowRight') { e.preventDefault(); navigate(1); } + }); + + // Touch swipe (edge-protected, vertical tolerance) + if (isTouch) { + let startX = 0, startY = 0, tracking = false; + const EDGE = 20, THRESH = 40; + const area = document.querySelector('main.page') || document.body; + + function isInteractive(el) { + return !!(el.closest && el.closest('input, textarea, select, button, a, [contenteditable], .no-swipe')); + } + + area.addEventListener('touchstart', function (e) { + const t = e.touches[0]; + if (!t) return; + if (isInteractive(e.target)) return; + startX = t.clientX; startY = t.clientY; tracking = true; + }, { passive: true }); + + area.addEventListener('touchmove', function (e) { + if (!tracking) return; + const t = e.touches[0]; + if (!t) return; + const dx = t.clientX - startX; + const dy = t.clientY - startY; + + if (Math.abs(dx) > THRESH && Math.abs(dy) < 28) { + // Edge-protection: avoid accidental from extreme edges + if (startX < EDGE && dx > 0) { tracking = false; return; } + if (startX > (window.innerWidth - EDGE) && dx < 0) { tracking = false; return; } + e.preventDefault(); + navigate(dx > 0 ? -1 : 1); + tracking = false; + } + }, { passive: false }); + + area.addEventListener('touchend', function () { tracking = false; }, { passive: true }); + } +})(); \ No newline at end of file -- 2.45.2 From df6b2d4cfbf973dd66affebf46f0b286af8937a1 Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Sun, 7 Sep 2025 17:19:04 +0000 Subject: [PATCH 02/10] Add web/static/js/settings-swipe.js --- web/static/js/settings-swipe.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 web/static/js/settings-swipe.js diff --git a/web/static/js/settings-swipe.js b/web/static/js/settings-swipe.js new file mode 100644 index 0000000..b44ec1f --- /dev/null +++ b/web/static/js/settings-swipe.js @@ -0,0 +1,11 @@ +/* Settings: Swipe navigation toggle (client-side, per device) */ +(function(){ + document.addEventListener('DOMContentLoaded', function(){ + const t = document.getElementById('swipeNavToggle'); + if (!t) return; + try { t.checked = (localStorage.getItem('swipeNavEnabled') !== 'false'); } catch(e){} + t.addEventListener('change', () => { + try { localStorage.setItem('swipeNavEnabled', t.checked ? 'true' : 'false'); } catch(e){} + }); + }); +})(); \ No newline at end of file -- 2.45.2 From 3a3d3efd5268ac1f2b11fba71d7c424f1e67b478 Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Sun, 7 Sep 2025 17:19:28 +0000 Subject: [PATCH 03/10] Update web/templates/entry_view.html --- web/templates/entry_view.html | 680 ++-------------------------------- 1 file changed, 23 insertions(+), 657 deletions(-) diff --git a/web/templates/entry_view.html b/web/templates/entry_view.html index 924954a..22e6d2b 100644 --- a/web/templates/entry_view.html +++ b/web/templates/entry_view.html @@ -1,671 +1,37 @@ {% extends "base.html" %} -{% block body_class %}themed-bg{% endblock %} {% load static %} +{% block body_class %}{% endblock %} + {% block content %} -
+
- -
-
- ← Back to Search - {% if count %} - {{ position }} of {{ count }} - {% endif %} -
-
-
- - - -
-
- - - -
- - - - - - - - - {% if user.is_authenticated and user.is_staff %} - - Edit - Delete - {% endif %} -
+
+

{{ entry.title }}

+

{{ entry.subtitle }}

- -
- -
- {% if subject_list %} - {% for s in subject_list %} - {{ s }} - {% endfor %} - {% else %} - (no subject) - {% endif %} -
+ {# Your existing entry content goes here… #} - -
- -
- {{ entry.illustration|linebreaksbr|default:"—" }} -
-
+
+{% endblock %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +{% block extra_body %} + {% endblock %} \ No newline at end of file -- 2.45.2 From 84c6037b30ea5c03657d62d20e6f1bae7f34d3a1 Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Sun, 7 Sep 2025 17:20:14 +0000 Subject: [PATCH 04/10] Update web/templates/entry_view.html --- web/templates/entry_view.html | 680 ++++++++++++++++++++++++++++++++-- 1 file changed, 657 insertions(+), 23 deletions(-) diff --git a/web/templates/entry_view.html b/web/templates/entry_view.html index 22e6d2b..924954a 100644 --- a/web/templates/entry_view.html +++ b/web/templates/entry_view.html @@ -1,37 +1,671 @@ {% extends "base.html" %} +{% block body_class %}themed-bg{% endblock %} {% load static %} -{% block body_class %}{% endblock %} - {% block content %} -
+
-
-

{{ entry.title }}

-

{{ entry.subtitle }}

+ +
+
+ ← Back to Search + {% if count %} + {{ position }} of {{ count }} + {% endif %} +
+
+
+ + + +
+
+ + + +
+ + + + + + + + + {% if user.is_authenticated and user.is_staff %} + + Edit + Delete + {% endif %} +
- {# Your existing entry content goes here… #} + +
+ +
+ {% if subject_list %} + {% for s in subject_list %} + {{ s }} + {% endfor %} + {% else %} + (no subject) + {% endif %} +
-
-{% endblock %} -{% block extra_body %} - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% endblock %} \ No newline at end of file -- 2.45.2 From 8468cee0887f012fb64fd891ded72fe0b2d9d32b Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Sun, 7 Sep 2025 17:20:42 +0000 Subject: [PATCH 05/10] Update web/templates/settings/home.html --- web/templates/settings/home.html | 47 ++++++++++++++------------------ 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/web/templates/settings/home.html b/web/templates/settings/home.html index 8af1ad1..219ca1d 100644 --- a/web/templates/settings/home.html +++ b/web/templates/settings/home.html @@ -57,6 +57,15 @@
+ + +
+ + +
@@ -159,7 +168,7 @@
- +
(function(){ - // Helpers + // Helpers already present in your file (unchanged) function getCookie(name){ const m=document.cookie.match("(^|;)\\s*"+name+"\\s*=\\s*([^;]+)"); return m?m.pop():""; } function setTheme(name){ - var v='{{ APP_VERSION }}'; var link=document.getElementById('theme-css'); - if(link) link.href='{% static "themes/" %}'+name+'.css?v='+v; // << cache-busted preview + if(link) link.href='{% static "themes/" %}'+name+'.css'; document.documentElement.setAttribute('data-theme',name); try{ localStorage.setItem('theme',name); }catch(e){} if(name==='classic'){ document.body.classList.add('themed-bg'); } else { document.body.classList.remove('themed-bg'); } var hidden=document.getElementById('theme-hidden'); if(hidden) hidden.value=name; } - // Clear history + // Clear history (unchanged) (function(){ var btn=document.getElementById("clear-history-btn"); var toast=document.getElementById("toast-clear-history"); @@ -263,13 +256,10 @@ }); })(); - // Save from tiles - window.saveTheme = function(){ - // Hidden input already holds current pick; just submit - return true; - }; + // Save from tiles (unchanged) + window.saveTheme = function(){ return true; }; - // Swatch instant preview + set hidden value + // Swatch instant preview (unchanged) document.querySelectorAll('.swatch').forEach(btn=>{ btn.addEventListener('click',()=>{ const name=btn.getAttribute('data-theme'); @@ -277,7 +267,7 @@ }); }); - // Highlight toggle + // Highlight toggle (server-backed, unchanged) (async function(){ const toggle=document.getElementById("highlightHitsToggle"); if(!toggle) return; @@ -295,4 +285,7 @@ })(); })(); + + + {% endblock %} \ No newline at end of file -- 2.45.2 From 9ff40f135409c30ede8e268d1ce60ab7cce1768b Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Sun, 7 Sep 2025 17:28:31 +0000 Subject: [PATCH 06/10] Update web/templates/entry_view.html --- web/templates/entry_view.html | 337 ++++++++-------------------------- 1 file changed, 76 insertions(+), 261 deletions(-) diff --git a/web/templates/entry_view.html b/web/templates/entry_view.html index 924954a..9f5221d 100644 --- a/web/templates/entry_view.html +++ b/web/templates/entry_view.html @@ -14,12 +14,12 @@ {% endif %}
-
+
-
+ @@ -38,24 +38,23 @@ Share - + - + - + {% if user.is_authenticated and user.is_staff %} - Edit Delete {% endif %} @@ -92,7 +91,7 @@
- {{ entry.application|linebreaksbr|default:"—" }} + {{ entry.application|linebreaksbr|default:"—" }}
@@ -132,7 +131,6 @@
Talk
{% if entry.talk_title %}{{ entry.talk_title }}{% else %}—{% endif %} - {# Talk Number pill (staff-only link) #} {% if entry.talk_number %} {% if user.is_authenticated and user.is_staff and talk_pdf_url %} - + - + - + +{% endblock %} +{% block extra_body %} + + {% endblock %} \ No newline at end of file -- 2.45.2 From cb895c50c4df5b9b7326c80c874684b66f0314d1 Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Sun, 7 Sep 2025 17:37:11 +0000 Subject: [PATCH 07/10] Update web/static/js/swipe-nav.js --- web/static/js/swipe-nav.js | 243 ++++++++++++++++++++++--------------- 1 file changed, 148 insertions(+), 95 deletions(-) diff --git a/web/static/js/swipe-nav.js b/web/static/js/swipe-nav.js index 2e964fa..a7863ed 100644 --- a/web/static/js/swipe-nav.js +++ b/web/static/js/swipe-nav.js @@ -1,117 +1,170 @@ -/* Swipe/Keyboard navigation for Entry View - - Respects localStorage 'swipeNavEnabled' (default ON) - - Touch swipe on mobile, ArrowLeft/ArrowRight on keyboards - - Edge protection and input-safe +/* swipe-nav.js + Mobile/desktop swipe & keyboard navigation for entry view. + - One navigation per gesture + - Edge protection: horizontal intent + distance + max duration + - Cooldown prevents multi-advance on long swipes + - Keyboard: Left/Right arrows (when not typing) */ (function () { - // Feature flag (client-side). Default ON. - let swipeEnabled = true; - try { swipeEnabled = (localStorage.getItem('swipeNavEnabled') !== 'false'); } catch (e) {} + // Bail if not on an entry page with forms + const prevForm = document.getElementById("navPrevForm"); + const nextForm = document.getElementById("navNextForm"); + if (!prevForm && !nextForm) return; - // Only attach if enabled - if (!swipeEnabled) return; + // Prefer a dedicated swipe zone if present; fall back to body + const zone = document.getElementById("swipeZone") || document.body; - // Forms to submit for previous/next (your existing IDs) - const prevForm = document.getElementById('navPrevForm'); - const nextForm = document.getElementById('navNextForm'); + // State + let tracking = false; + let startX = 0, startY = 0, startT = 0, pointerId = null; + let didNavigate = false; + let cooldown = false; - // Create toast + hints lazily to keep HTML clean - const toast = document.createElement('div'); - toast.id = 'toast-swipe-error'; - toast.style.cssText = [ - 'position:fixed', 'left:50%', 'bottom:16px', 'transform:translateX(-50%)', - 'padding:10px 14px', 'border-radius:10px', 'background:#111827', 'color:#fff', - 'box-shadow:0 6px 20px rgba(0,0,0,.25)', 'opacity:0', 'pointer-events:none', - 'transition:opacity .25s', 'z-index:1200', 'font-size:14px' - ].join(';'); - toast.textContent = "Can't navigate here."; - document.body.appendChild(toast); + // Tunables + const MIN_X = 48; // px horizontal distance + const MAX_ANGLE = 30; // deg off pure horizontal + const MAX_MS = 650; // must complete within this time + const COOLDOWN_MS = 450; // after we navigate, ignore further moves briefly - function showToast() { - toast.style.opacity = '1'; - setTimeout(() => { toast.style.opacity = '0'; }, 1200); + // Helpers + function degrees(dx, dy) { + const a = Math.atan2(Math.abs(dy), Math.abs(dx)) * (180 / Math.PI); + return a; // 0 = pure horizontal + } + function now() { return performance.now ? performance.now() : Date.now(); } + + function reset() { + tracking = false; + pointerId = null; + startX = startY = 0; + startT = 0; + didNavigate = false; } - // Only show swipe hints on touch devices, once per session - const isTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0); - let hintL, hintR; - if (isTouch) { - try { - if (!sessionStorage.getItem('swipeHintShown')) { - sessionStorage.setItem('swipeHintShown', '1'); - - const base = 'position:fixed;width:34px;height:34px;display:flex;align-items:center;justify-content:center;' + - 'background:rgba(0,0,0,.35);color:#fff;border-radius:999px;box-shadow:0 6px 20px rgba(0,0,0,.25);' + - 'z-index:1100;opacity:0;transform:scale(.9);transition:opacity .25s,transform .25s;top:45%'; - - hintL = document.createElement('div'); - hintL.style.cssText = base + ';left:10px;'; - hintL.textContent = '‹'; - document.body.appendChild(hintL); - - hintR = document.createElement('div'); - hintR.style.cssText = base + ';right:10px;'; - hintR.textContent = '›'; - document.body.appendChild(hintR); - - setTimeout(() => { hintL.style.opacity = '1'; hintR.style.opacity = '1'; hintL.style.transform = 'scale(1)'; hintR.style.transform = 'scale(1)'; }, 300); - setTimeout(() => { hintL.style.opacity = '0'; hintR.style.opacity = '0'; }, 2200); - } - } catch (e) {} + function submit(form) { + if (!form || cooldown) return; + cooldown = true; + form.submit(); + // If navigation is prevented (e.g., disabled button), release cooldown after a tick + setTimeout(() => { cooldown = false; }, COOLDOWN_MS); } - function navigate(dir) { - try { - if (dir < 0 && prevForm) { prevForm.submit(); return true; } - if (dir > 0 && nextForm) { nextForm.submit(); return true; } - } catch (e) {} - showToast(); - return false; + // Pointer events if supported (best across devices). Fallback to touch. + const usePointer = "PointerEvent" in window; + + function onDown(e) { + if (cooldown) return; + // Ignore right click / secondary + if (usePointer && e.pointerType === "mouse" && e.button !== 0) return; + + tracking = true; + didNavigate = false; + + const pt = usePointer ? e : (e.touches && e.touches[0]); + if (!pt) return; + + pointerId = usePointer ? e.pointerId : pt.identifier; + startX = pt.clientX; + startY = pt.clientY; + startT = now(); + + // Once we suspect horizontal motion, we'll call preventDefault in move } - // Keyboard shortcuts - document.addEventListener('keydown', function (e) { - if (!swipeEnabled) return; - const tag = (e.target && e.target.tagName) || ''; - if (/INPUT|TEXTAREA|SELECT/.test(tag)) return; - if (e.key === 'ArrowLeft') { e.preventDefault(); navigate(-1); } - if (e.key === 'ArrowRight') { e.preventDefault(); navigate(1); } - }); + function onMove(e) { + if (!tracking || didNavigate || cooldown) return; - // Touch swipe (edge-protected, vertical tolerance) - if (isTouch) { - let startX = 0, startY = 0, tracking = false; - const EDGE = 20, THRESH = 40; - const area = document.querySelector('main.page') || document.body; + const pt = usePointer ? e : (e.touches && e.touches[0]); + if (!pt) return; + if (usePointer && e.pointerId !== pointerId) return; - function isInteractive(el) { - return !!(el.closest && el.closest('input, textarea, select, button, a, [contenteditable], .no-swipe')); + const dx = pt.clientX - startX; + const dy = pt.clientY - startY; + + // If user is panning mostly horizontally, stop the browser from doing a horizontal scroll/jiggle + if (Math.abs(dx) > 8 && Math.abs(dx) > Math.abs(dy)) { + // In touch events you must preventDefault during move to suppress scroll + if (e.cancelable) e.preventDefault(); + } + } + + function onUp(e) { + if (!tracking || didNavigate || cooldown) { reset(); return; } + + let pt; + if (usePointer) { + if (e.pointerId !== pointerId) return reset(); + pt = e; + } else { + // On touchend, touches[] is empty; use changedTouches[] + pt = (e.changedTouches && e.changedTouches[0]) || null; + if (!pt) return reset(); } - area.addEventListener('touchstart', function (e) { - const t = e.touches[0]; - if (!t) return; - if (isInteractive(e.target)) return; - startX = t.clientX; startY = t.clientY; tracking = true; - }, { passive: true }); + const dx = pt.clientX - startX; + const dy = pt.clientY - startY; + const dt = now() - startT; - area.addEventListener('touchmove', function (e) { - if (!tracking) return; - const t = e.touches[0]; - if (!t) return; - const dx = t.clientX - startX; - const dy = t.clientY - startY; + const ang = degrees(dx, dy); + const horizontalEnough = ang <= MAX_ANGLE; - if (Math.abs(dx) > THRESH && Math.abs(dy) < 28) { - // Edge-protection: avoid accidental from extreme edges - if (startX < EDGE && dx > 0) { tracking = false; return; } - if (startX > (window.innerWidth - EDGE) && dx < 0) { tracking = false; return; } - e.preventDefault(); - navigate(dx > 0 ? -1 : 1); - tracking = false; + // Must be a deliberate, reasonably quick horizontal swipe + if (Math.abs(dx) >= MIN_X && horizontalEnough && dt <= MAX_MS) { + didNavigate = true; + if (dx < 0) { + // left swipe -> NEXT + const btn = nextForm && nextForm.querySelector("button[disabled]"); + if (!btn) submit(nextForm); + } else { + // right swipe -> PREV + const btn = prevForm && prevForm.querySelector("button[disabled]"); + if (!btn) submit(prevForm); } - }, { passive: false }); + if (e.cancelable) e.preventDefault(); + } - area.addEventListener('touchend', function () { tracking = false; }, { passive: true }); + reset(); } + + function onCancel() { reset(); } + + if (usePointer) { + zone.addEventListener("pointerdown", onDown, { passive: true }); + zone.addEventListener("pointermove", onMove, { passive: false }); + zone.addEventListener("pointerup", onUp, { passive: false }); + zone.addEventListener("pointercancel", onCancel, { passive: true }); + zone.addEventListener("pointerleave", onCancel, { passive: true }); + } else { + zone.addEventListener("touchstart", onDown, { passive: true }); + zone.addEventListener("touchmove", onMove, { passive: false }); + zone.addEventListener("touchend", onUp, { passive: false }); + zone.addEventListener("touchcancel", onCancel, { passive: true }); + } + + // Keyboard support (left/right arrows) – ignore if typing + document.addEventListener("keydown", (e) => { + if (cooldown) return; + const tag = (e.target && e.target.tagName) || ""; + const typing = /INPUT|TEXTAREA|SELECT/.test(tag); + if (typing) return; + + if (e.key === "ArrowRight") { + const btn = nextForm && nextForm.querySelector("button[disabled]"); + if (!btn) { e.preventDefault(); submit(nextForm); } + } else if (e.key === "ArrowLeft") { + const btn = prevForm && prevForm.querySelector("button[disabled]"); + if (!btn) { e.preventDefault(); submit(prevForm); } + } + }); + + // Defensive CSS: lock horizontal gestures to our handler only inside the zone + (function injectCSS() { + const css = ` + #swipeZone{ touch-action: pan-y; overscroll-behavior-x: none; overflow-x: hidden; } + html, body{ overscroll-behavior-x: none; } + `; + const el = document.createElement("style"); + el.textContent = css; + document.head.appendChild(el); + })(); })(); \ No newline at end of file -- 2.45.2 From 35b2a331800a25f5e1515f5650ebc1308e888d0f Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Sun, 7 Sep 2025 17:48:30 +0000 Subject: [PATCH 08/10] Update web/templates/entry_view.html --- web/templates/entry_view.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/templates/entry_view.html b/web/templates/entry_view.html index 9f5221d..610d093 100644 --- a/web/templates/entry_view.html +++ b/web/templates/entry_view.html @@ -3,7 +3,7 @@ {% load static %} {% block content %} -
+
@@ -183,6 +183,9 @@