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 diff --git a/web/static/js/swipe-nav.js b/web/static/js/swipe-nav.js new file mode 100644 index 0000000..a7863ed --- /dev/null +++ b/web/static/js/swipe-nav.js @@ -0,0 +1,170 @@ +/* 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 () { + // Bail if not on an entry page with forms + const prevForm = document.getElementById("navPrevForm"); + const nextForm = document.getElementById("navNextForm"); + if (!prevForm && !nextForm) return; + + // Prefer a dedicated swipe zone if present; fall back to body + const zone = document.getElementById("swipeZone") || document.body; + + // State + let tracking = false; + let startX = 0, startY = 0, startT = 0, pointerId = null; + let didNavigate = false; + let cooldown = false; + + // 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 + + // 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; + } + + 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); + } + + // 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 + } + + function onMove(e) { + if (!tracking || didNavigate || cooldown) return; + + const pt = usePointer ? e : (e.touches && e.touches[0]); + if (!pt) return; + if (usePointer && e.pointerId !== pointerId) return; + + 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(); + } + + const dx = pt.clientX - startX; + const dy = pt.clientY - startY; + const dt = now() - startT; + + const ang = degrees(dx, dy); + const horizontalEnough = ang <= MAX_ANGLE; + + // 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); + } + if (e.cancelable) e.preventDefault(); + } + + 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 diff --git a/web/templates/entry_view.html b/web/templates/entry_view.html index 924954a..20102bb 100644 --- a/web/templates/entry_view.html +++ b/web/templates/entry_view.html @@ -3,7 +3,7 @@ {% load static %} {% block content %} -
+
@@ -14,12 +14,12 @@ {% endif %}
-
+
-
+ @@ -35,27 +35,25 @@ - Share - + - + - + {% if user.is_authenticated and user.is_staff %} - Edit Delete {% endif %} @@ -92,7 +90,7 @@
- {{ entry.application|linebreaksbr|default:"—" }} + {{ entry.application|linebreaksbr|default:"—" }}
@@ -132,7 +130,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 diff --git a/web/templates/search.html b/web/templates/search.html index 5b6267a..1d01a9b 100644 --- a/web/templates/search.html +++ b/web/templates/search.html @@ -9,34 +9,6 @@
- - - -
- - -
-

How to Use Search Operators

-
    -
  • Simple keyword — type any word to find entries that contain it.
    - Example: faith
  • -
  • Phrase search — put quotes around a phrase to match it exactly.
    - Example: "Jehovah is my shepherd"
  • -
  • OR search — use OR (uppercase).
    - Example: love OR kindness
  • -
  • Exclude terms — use - to remove.
    - Example: hope -future
  • -
  • Wildcard search — use *.
    - Example: lov*
  • -
  • Scripture search — type a Bible book.
    - Example: John 3:16
  • -
@@ -225,22 +197,6 @@ }); }); - // Help panel toggle (unchanged) - document.addEventListener('click', function(e){ - const btn = e.target.closest('.help-toggle'); - if (btn) { - const panel = document.querySelector(btn.dataset.target || '#search-help-panel'); - if (panel) panel.classList.toggle('open'); - return; - } - const panel = document.querySelector('#search-help-panel'); - if (!panel) return; - if (panel.classList.contains('open')) { - const clickedInside = panel.contains(e.target) || e.target.closest('.help-toggle'); - if (!clickedInside) panel.classList.remove('open'); - } - }); - // =============================== // No-results: show a random funny illustration // =============================== @@ -254,7 +210,7 @@ `It’s like looking for a matching sock. You find five that are “close enough,” but never the one you actually need.`, `It’s like running into the store for peanut butter. You leave with bread, bananas, cereal, and gum—somehow no peanut butter.`, `It feels like searching for your car keys when you’re late. You check the counter, pockets, and finally… the fridge. Next to the milk.`, - `It’s like walking around the house holding your phone in the air, trying to catch a Wi‑Fi signal that appears and vanishes at random.`, + `It’s like walking around the house holding your phone in the air, trying to catch a Wi-Fi signal that appears and vanishes at random.`, `Imagine a giant library where you’re sure the book is “right here.” You scan up, down, left, right—then realize you’re in the wrong aisle.`, `It’s like following GPS: “Turn left now!” You miss it, circle the block, and the voice keeps politely “recalculating.”`, `It’s like trying to find an umbrella on a sunny day—no luck. The moment it pours, suddenly you own five.`, @@ -284,7 +240,7 @@ const today = new Date(); const ymd = today.getFullYear()*10000 + (today.getMonth()+1)*100 + today.getDate(); - // 32‑bit xorshift PRNG for a good daily seed + // 32-bit xorshift PRNG for a good daily seed function xorshift32(seed){ let x = seed | 0; x ^= x << 13; x ^= x >>> 17; x ^= x << 5; @@ -362,19 +318,6 @@