Illustrations/web/templates/search.html

434 lines
16 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>
<!-- 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. Youre 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 werent looking.`,
`Its 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.`,
`Its like looking for a matching sock. You find five that are “close enough,” but never the one you actually need.`,
`Its 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 youre late. You check the counter, pockets, and finally… the fridge. Next to the milk.`,
`Its like walking around the house holding your phone in the air, trying to catch a WiFi signal that appears and vanishes at random.`,
`Imagine a giant library where youre sure the book is “right here.” You scan up, down, left, right—then realize youre in the wrong aisle.`,
`Its like following GPS: “Turn left now!” You miss it, circle the block, and the voice keeps politely “recalculating.”`,
`Its like trying to find an umbrella on a sunny day—no luck. The moment it pours, suddenly you own five.`,
`Its 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 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);
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 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;
}
}
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>
<!-- 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 %}