Illustrations/web/templates/search.html

386 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block body_class %}themed-bg{% endblock %}
{% load static %}
{% block content %}
<div class="container">
<form method="get" class="search-form">
<h1 class="page-title">Illustration Search</h1>
<div class="search-row">
<input type="text" name="q" value="{{ q }}" placeholder="Type to search…" class="search-input" autofocus>
<button class="btn btn-primary">Search</button>
<!-- Help button -->
<button
class="btn btn-secondary help-toggle"
type="button"
data-target="#search-help-panel"
>
Help
</button>
</div>
<!-- Help panel -->
<div id="search-help-panel" class="help-panel">
<h3>How to Use Search Operators</h3>
<ul>
<li><strong>Simple keyword</strong> — type any word to find entries that contain it.<br>
<em>Example:</em> <code>faith</code></li>
<li><strong>Phrase search</strong> — put quotes around a phrase to match it exactly.<br>
<em>Example:</em> <code>"Jehovah is my shepherd"</code></li>
<li><strong>OR search</strong> — use <code>OR</code> (uppercase).<br>
<em>Example:</em> <code>love OR kindness</code></li>
<li><strong>Exclude terms</strong> — use <code>-</code> to remove.<br>
<em>Example:</em> <code>hope -future</code></li>
<li><strong>Wildcard search</strong> — use <code>*</code>.<br>
<em>Example:</em> <code>lov*</code></li>
<li><strong>Scripture search</strong> — type a Bible book.<br>
<em>Example:</em> <code>John 3:16</code></li>
</ul>
</div>
<div class="filter-row">
{% for f in field_options %}
<label class="check-pill">
<input type="checkbox" name="{{ f.name }}" {% if f.checked %}checked{% endif %}>
<span>{{ f.label }}</span>
</label>
{% endfor %}
</div>
</form>
{% if ran_search and result_count == 0 %}
<div class="empty-state">
<div class="empty-title">No results</div>
<div class="empty-subtitle">Looking for the right illustration can feel a lot like standing in front of your fridge at midnight. You open the door, expecting to see something amazing—leftover pizza, a slice of cake, maybe even some ice cream. Instead, you just stare at a jar of pickles, an old bag of shredded cheese, and half a bottle of ketchup. You close the door, sigh, then open it again five seconds later, hoping something new has magically appeared.
Thats exactly how it feels scrolling through a list of illustrations—you know something tasty has to be in there somewhere, but all you can find are pickles.</div>
</div>
{% endif %}
<!-- ===== History Dropdown ===== -->
<div class="card" style="padding:0; margin:16px 0 12px;">
<h1 class="page-title dropdown-toggle history-title"
data-target="#history-panel"
role="button"
aria-expanded="false">
Search History <span class="chevron"></span>
</h1>
<div id="history-panel" class="dropdown-panel">
<hr style="border:none; border-top:1px solid var(--border); margin:12px 0;">
<!-- Recent Searches -->
<div style="margin-top:6px;">
<div class="small muted" style="margin-bottom:6px;">Your Recent Searches</div>
<ul id="searchHistoryList" class="small" style="margin:0; padding-left:18px;"></ul>
<div id="searchHistoryEmpty" class="muted small" style="display:none;">No history yet.</div>
</div>
<hr style="border:none; border-top:1px solid var(--border); margin:12px 0;">
<!-- Recently Viewed -->
<div>
<div class="small muted" style="margin-bottom:6px;">Recently Viewed Illustrations</div>
<ul id="recentViewsList" class="small" style="margin:0; padding-left:18px;"></ul>
<div id="recentViewsEmpty" class="muted small" style="display:none;">Nothing yet—open an illustration and linger 10s.</div>
</div>
</div>
</div>
<!-- ===== Illustration of the Day ===== -->
<div class="card" style="padding:0; margin:16px 0 12px;">
<h1 class="page-title iotd-title">Illustration of the Day</h1>
<div id="iotd-box" class="iotd-panel">
<p id="iotd-text" style="margin:0 0 12px;">Loading…</p>
<a id="iotd-open" href="#" class="btn btn-primary" style="display:none;">Open Illustration</a>
</div>
</div>
<script>
(function(){
// --- CSRF helper ---
function getCookie(name){
const m = document.cookie.match('(^|;)\\s*'+name+'\\s*=\\s*([^;]+)');
return m ? m.pop() : '';
}
const csrftoken = getCookie('csrftoken');
// Build entry view URL
const ENTRY_URL_TEMPLATE = "{% url 'entry_view' 1 %}";
function entryUrlFor(id){
return ENTRY_URL_TEMPLATE.replace(/\/\d+\/?$/, '/' + id + '/');
}
// First N words
function firstWords(text, n){
const clean = (text || '').replace(/\s+/g, ' ').trim();
if (!clean) return '';
const words = clean.split(' ');
if (words.length <= n) return clean;
return words.slice(0, n).join(' ') + '…';
}
// Font prefs
function applyFont(size){
const root = document.documentElement;
root.classList.remove('fs-small','fs-default','fs-large','fs-xlarge');
root.classList.add('fs-'+size);
}
fetch("{% url 'api_get_prefs' %}", { cache: 'no-store', credentials: 'same-origin' })
.then(r=>r.json()).then(j=>{
if (j.ok) applyFont(j.font_size || 'default');
}).catch(()=>{});
// Small helper to add a cache-busting param
function bust(url){
const sep = url.includes('?') ? '&' : '?';
return url + sep + 't=' + Date.now();
}
// Search history (keeps selected-fields subtitle)
function renderHistory(items){
const list = document.getElementById('searchHistoryList');
const empty = document.getElementById('searchHistoryEmpty');
list.innerHTML = '';
if (!items || !items.length){ empty.style.display='block'; return; }
empty.style.display='none';
items.forEach(it=>{
const params = new URLSearchParams();
if (it.q) params.set('q', it.q);
const sel = it.selected || {};
Object.keys(sel).forEach(k=>{ if (sel[k]) params.set(k, 'on'); });
const li = document.createElement('li');
const selectedList = Object.keys(sel).filter(k=>sel[k]).join(', ');
li.innerHTML = `<a href="{% url 'search' %}?${params.toString()}"><strong>${it.q || '(blank)'}</strong></a>
<span class="muted">— ${selectedList}</span>`;
list.appendChild(li);
});
}
function refetchHistory(){
fetch(bust("{% url 'api_get_search_history' %}"), {
cache: 'no-store',
credentials: 'same-origin'
}).then(r=>r.json()).then(j=>{
if (j.ok) renderHistory(j.items);
}).catch(()=>{});
}
refetchHistory();
// Recently viewed
function renderRecent(items){
const list = document.getElementById('recentViewsList');
const empty = document.getElementById('recentViewsEmpty');
list.innerHTML = '';
if (!items || !items.length){ empty.style.display='block'; return; }
empty.style.display='none';
items.forEach(it=>{
const url = entryUrlFor(it.entry_id);
const snippet = (it.snippet && it.snippet.trim())
|| firstWords(it.illustration || '', 20)
|| `Entry #${it.entry_id}`;
const when = new Date(it.viewed_at);
const li = document.createElement('li');
li.innerHTML = `<a href="${url}">${snippet}</a>
<span class="muted"> — ${when.toLocaleString()}</span>`;
list.appendChild(li);
});
}
function refetchRecent(){
fetch(bust("{% url 'api_get_recent_views' %}"), {
cache: 'no-store',
credentials: 'same-origin'
}).then(r=>r.json()).then(j=>{
if (j.ok) renderRecent(j.items);
}).catch(()=>{});
}
refetchRecent();
// ✅ Log searches with Beacon on submit
const searchForm = document.querySelector('form.search-form');
if (searchForm){
searchForm.addEventListener('submit', ()=>{
try{
const fd = new FormData(searchForm);
const data = new URLSearchParams();
data.append('q', (fd.get('q') || '').trim());
['subject','illustration','application','scripture_raw','source','talk_title','talk_number','entry_code']
.forEach(k=>{
if (fd.get(k)) data.append(`sel[${k}]`, 'on');
});
const blob = new Blob([data.toString()], { type:'application/x-www-form-urlencoded' });
navigator.sendBeacon("{% url 'api_log_search' %}", blob);
}catch(_){}
});
}
// Dropdown toggle for History
document.querySelectorAll('.dropdown-toggle').forEach(btn=>{
btn.addEventListener('click', ()=>{
const target = document.querySelector(btn.dataset.target);
if (target){
target.classList.toggle('open');
btn.classList.toggle('open');
}
});
});
// 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');
}
});
/* ===============================
Illustration of the Day (client-only, deterministic)
=============================== */
(function illustrationOfTheDay(){
const total = {{ total|default:0 }};
const iotdTextEl = document.getElementById('iotd-text');
const iotdOpenEl = document.getElementById('iotd-open');
if (!iotdTextEl || !iotdOpenEl) return;
if (!total || total < 1){
iotdTextEl.textContent = 'No illustrations available yet.';
iotdOpenEl.style.display = 'none';
return;
}
// Seed from local date (YYYYMMDD) so its the same for everyone that day
const today = new Date();
const ymd = today.getFullYear()*10000 + (today.getMonth()+1)*100 + today.getDate();
// 32bit xorshift PRNG for a good daily seed
function xorshift32(seed){
let x = seed | 0;
x ^= x << 13; x ^= x >>> 17; x ^= x << 5;
return (x >>> 0);
}
const seed = xorshift32(ymd ^ 0x9E3779B9);
// We dont know ID gaps, so try a deterministic pseudorandom
// permutation over a *generous* range and stop at the first 200.
const maxGuess = Math.max(100, Math.floor(total * 4)); // cast a wide net
const maxAttempts = Math.min(600, maxGuess); // cap network work
// Linear congruential generator style “index -> id” mapping (deterministic)
function idAt(i){
// multiplier is odd; modulus is implicit 2^32, then map to [1..maxGuess]
const v = (Math.imul(i + 1, 1103515245) + 12345 + seed) >>> 0;
return 1 + (v % maxGuess);
}
async function fetchEntryHtml(id){
const url = entryUrlFor(id);
try{
const r = await fetch(url, { credentials: 'same-origin' });
if (r.ok) return await r.text();
}catch(_){}
return null;
}
function ensurePunct(str){
const s = (str || '').trim();
if (!s) return '';
return /[.!?…]$/.test(s) ? s : (s + '.');
}
function extractSectionText(doc, label){
// Look for label “Illustration” / “Application” commonly used in your templates
const labels = doc.querySelectorAll('.section-label, .meta-label, h3, h4, strong');
for (const el of labels){
const t = (el.textContent || '').trim().toLowerCase();
if (t === label){
const section = el.closest('.section') || el.parentElement;
if (section){
const body = section.querySelector('.lead-text') ||
section.querySelector('.section-body') ||
section.querySelector('p');
if (body) return (body.textContent || '').trim();
}
}
}
// fallback
const alt = doc.querySelector('.lead-text, .section-body');
return alt ? (alt.textContent || '').trim() : '';
}
function renderIotd(id, html){
const dom = new DOMParser().parseFromString(html, 'text/html');
const illustration = extractSectionText(dom, 'illustration');
const application = extractSectionText(dom, 'application');
const merged = (ensurePunct(illustration) + (application ? ' ' + application : '')).trim();
iotdTextEl.textContent = merged || 'Open to view todays illustration.';
iotdOpenEl.href = entryUrlFor(id);
iotdOpenEl.style.display = 'inline-block';
}
(async function findAndRender(){
for (let i = 0; i < maxAttempts; i++){
const id = idAt(i);
const html = await fetchEntryHtml(id);
if (html){
renderIotd(id, html);
return;
}
}
// If nothing responded 200 within our attempts, fail gracefully
iotdTextEl.textContent = 'Unable to load todays illustration.';
iotdOpenEl.style.display = 'none';
})();
})();
})();
</script>
<style>
/* Help panel */
.help-panel{
display:none;
margin-top:10px;
background:#fff;
border:1px solid var(--border);
border-radius:12px;
padding:14px;
box-shadow:0 4px 16px rgba(0,0,0,.06);
font-size:14px;
}
.help-panel.open{ display:block; }
/* Dropdown panel */
.dropdown-panel { display:none; padding:12px; }
.dropdown-panel.open { display:block; }
/* Chevron style sized for heading */
.page-title .chevron {
font-size: 0.9em;
margin-left: 8px;
vertical-align: middle;
transition: transform 0.2s ease;
display:inline-block;
}
.page-title.open .chevron { transform: rotate(180deg); }
.dropdown-toggle { cursor:pointer; }
/* Keep the chevron next to the text */
.history-title{
display:inline-flex !important;
align-items:center;
justify-content:flex-start !important;
gap:8px;
padding:10px 16px;
margin:0;
}
</style>
{% endblock %}