Illustrations/web/templates/search.html

394 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">Try broadening your terms or enabling more fields above.</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="container">
<form method="get" class="search-form">
<h1 class="page-title">Illustration of the Day</h1>
<!-- insert illustration of the day here -->
<div id="iotd-card" class="card" style="padding:16px; margin:12px 0;">
<div id="iotd-text" class="lead-text" style="white-space:pre-wrap; margin-bottom:12px;">
Loading…
</div>
<div id="iotd-actions">
<a id="iotd-open" href="#" class="btn btn-primary">Open Illustration</a>
</div>
</div>
</form>
</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();
// Simple string hash -> 32-bit -> xorshift for decent spread
function xorshift32(seed){
let x = seed | 0;
x ^= x << 13; x ^= x >>> 17; x ^= x << 5;
return (x >>> 0);
}
function pickIndex(max, seed){
// (0..max-1)
return xorshift32(seed) % max;
}
// We dont know if IDs are contiguous, so:
// 1) choose a *candidate* ID in [1..maxIdGuess]
// 2) try up to N nearby IDs until one 200s
// Best guess for max id is "total * 1.5" (cheap heuristic).
const maxIdGuess = Math.max(total, Math.floor(total * 1.5));
let candidate = 1 + pickIndex(maxIdGuess, ymd ^ 0x9E3779B9);
async function tryFetch(id){
const url = entryUrlFor(id);
try{
const r = await fetch(url, { credentials:'same-origin' });
if (r.ok) {
const html = await r.text();
return { ok:true, id, html };
}
}catch(_){}
return { ok:false };
}
function ensurePunct(str){
const s = (str || '').trim();
if (!s) return '';
// If ends with punctuation, keep; else add period.
return /[.!?…]$/.test(s) ? s : (s + '.');
}
function extractSectionText(doc, label){
// Look for a section with a label like "Illustration" / "Application"
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){
// usual structure: label inside a .section; body nearby
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: first .lead-text / .section-body on page if label search failed
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);
}
(async function findAndRender(){
// Try candidate, then probe up to +/- 10 around it
const offsets = [0,1,-1,2,-2,3,-3,4,-4,5,-5,6,-6,7,-7,8,-8,9,-9,10,-10];
for (const off of offsets){
const id = Math.max(1, candidate + off);
const res = await tryFetch(id);
if (res.ok) { renderIotd(id, res.html); return; }
}
// Give up—show a graceful fallback
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 %}