434 lines
16 KiB
HTML
434 lines
16 KiB
HTML
{% 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>
|
||
<!-- Placeholder text; JS below will replace this with a random funny line -->
|
||
<div class="empty-subtitle">Looking for something witty…</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>
|
||
</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');
|
||
}
|
||
});
|
||
|
||
// ===============================
|
||
// No-results: show a random funny illustration
|
||
// ===============================
|
||
(function showRandomNoResults(){
|
||
const el = document.querySelector('.empty-state .empty-subtitle');
|
||
if (!el) return;
|
||
|
||
const messages = [
|
||
`Searching for an illustration is like opening the fridge at midnight. You’re hoping for cheesecake, but all you find is half a tomato and some wilted lettuce. You close the door, then open it again—because maybe cheesecake grew in there while you weren’t looking.`,
|
||
`It’s like hunting for the TV remote. You check the couch, the floor, the freezer (why?)—then realize it was in your hand the whole time.`,
|
||
`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.`,
|
||
`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.`,
|
||
`It’s like a jigsaw with one missing piece. You check under the table, the box, even the dog—then find the piece stuck to your elbow.`
|
||
];
|
||
|
||
const pick = messages[Math.floor(Math.random() * messages.length)];
|
||
el.textContent = pick;
|
||
})();
|
||
|
||
/* ===============================
|
||
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 it’s the same for everyone that day
|
||
const today = new Date();
|
||
const ymd = today.getFullYear()*10000 + (today.getMonth()+1)*100 + today.getDate();
|
||
|
||
// 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;
|
||
return (x >>> 0);
|
||
}
|
||
const seed = xorshift32(ymd ^ 0x9E3779B9);
|
||
|
||
const maxGuess = Math.max(100, Math.floor(total * 4));
|
||
const maxAttempts = Math.min(600, maxGuess);
|
||
|
||
function idAt(i){
|
||
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){
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
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 today’s 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;
|
||
}
|
||
}
|
||
iotdTextEl.textContent = 'Unable to load today’s 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>
|
||
|
||
<!-- Save q + selected fields exactly as submitted -->
|
||
<script>
|
||
(function(){
|
||
const form = document.querySelector('form.search-form') || document.querySelector('form[method="get"]');
|
||
if (!form) return;
|
||
const qInput = form.querySelector('input[name="q"]');
|
||
|
||
form.addEventListener('submit', () => {
|
||
const fd = new FormData(form);
|
||
const fields = [];
|
||
for (const [k, v] of fd.entries()) {
|
||
// sel[field]=on → capture "field"
|
||
if (k.startsWith('sel[') && k.endsWith(']') && (v === 'on' || v === 'true' || v === '1')) {
|
||
fields.push(k.slice(4, -1));
|
||
}
|
||
}
|
||
const q = (qInput && qInput.value || '').trim();
|
||
|
||
// Store exactly what the user selected — NO default to ["subject"]
|
||
try {
|
||
localStorage.setItem('lastSearchQ', q);
|
||
localStorage.setItem('lastSearchFields', JSON.stringify(fields));
|
||
} catch (_) {}
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
|
||
|
||
{% include "partials/announcement_modal.html" %}
|
||
{% endblock %} |