/** * builder.js — Rosary Builder interactive logic * Depends on: window.PRAYERS_DATA (from PHP), window.EXISTING_STEPS (edit mode) */ (function () { 'use strict'; /* ───────────────────────────────────────────────────────── State ───────────────────────────────────────────────────────── */ let STEPS = []; // [{prayer_id, attribution, _prayer: {...}}] let PRAYERS = []; // full list from PHP let activeTab = 'all'; let searchQuery = ''; let editingPrayerId = null; // null = create, int = edit /* ───────────────────────────────────────────────────────── Boot ───────────────────────────────────────────────────────── */ function init() { PRAYERS = window.PRAYERS_DATA || []; // Populate existing steps when editing a session if (window.EXISTING_STEPS && window.EXISTING_STEPS.length) { window.EXISTING_STEPS.forEach(function (s) { const prayer = PRAYERS.find(p => String(p.id) === String(s.prayer_id)); if (prayer) STEPS.push({ prayer_id: s.prayer_id, attribution: s.attribution, _prayer: prayer }); }); } renderLibrary(); renderSequence(); bindEvents(); } // Scripts are at end of body — DOM is fully parsed by the time this runs, // so DOMContentLoaded may have already fired. Check readyState first. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } /* ───────────────────────────────────────────────────────── Event bindings ───────────────────────────────────────────────────────── */ function bindEvents() { // Search document.getElementById('prayer-search').addEventListener('input', function () { searchQuery = this.value.trim().toLowerCase(); renderLibrary(); }); // Tabs document.querySelectorAll('.tab-btn').forEach(function (btn) { btn.addEventListener('click', function () { document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); this.classList.add('active'); activeTab = this.dataset.tab; renderLibrary(); }); }); // Create prayer button document.getElementById('btn-create-prayer').addEventListener('click', function () { openModal(null); }); // Modal cancel document.getElementById('modal-cancel').addEventListener('click', closeModal); document.getElementById('prayer-modal').addEventListener('click', function (e) { if (e.target === this) closeModal(); }); // Modal attribution radios document.querySelectorAll('input[name="modal-attr"]').forEach(function (radio) { radio.addEventListener('change', updateModalFields); }); // Modal save document.getElementById('modal-save').addEventListener('click', saveModalPrayer); // Save session document.getElementById('btn-save').addEventListener('click', saveSession); } /* ───────────────────────────────────────────────────────── Library rendering ───────────────────────────────────────────────────────── */ function filteredPrayers() { return PRAYERS.filter(function (p) { if (activeTab === 'standard' && p.source_tag !== 'standard') return false; if (activeTab === 'mine' && p.source_tag !== 'mine') return false; if (searchQuery) { const hay = (p.name + ' ' + (p.leader_text || '') + ' ' + (p.all_text || '')).toLowerCase(); return hay.includes(searchQuery); } return true; }); } function renderLibrary() { const list = document.getElementById('prayer-list'); const visible = filteredPrayers(); if (!visible.length) { list.innerHTML = '
No prayers found. Try a different search or tab.
'; return; } list.innerHTML = visible.map(function (p) { const preview = (p.leader_text || p.all_text || '').replace(/\n/g, ' ').substring(0, 100); const badgeClass = p.source_tag === 'standard' ? 'standard' : (p.source_tag === 'global' ? 'global' : 'mine'); const badgeLabel = p.source_tag === 'standard' ? 'Standard' : (p.source_tag === 'global' ? 'Global' : 'Mine'); const canEdit = p.source_tag === 'mine' || window.IS_ADMIN; return `
${esc(p.name)} ${badgeLabel}
${esc(preview)}${preview.length >= 100 ? '…' : ''}
`; }).join(''); } /* ───────────────────────────────────────────────────────── Sequence rendering ───────────────────────────────────────────────────────── */ function renderSequence() { const list = document.getElementById('step-list'); const badge = document.getElementById('step-count-badge'); badge.textContent = STEPS.length + (STEPS.length === 1 ? ' prayer' : ' prayers'); badge.className = 'step-count-badge' + (STEPS.length ? ' has-steps' : ''); if (!STEPS.length) { list.innerHTML = `
Select a prayer from the library to begin building your sequence
`; return; } list.innerHTML = STEPS.map(function (step, i) { const p = step._prayer; const leaderPrev = (p.leader_text || '').replace(/\n/g, ' ').substring(0, 60); const allPrev = (p.all_text || '').replace(/\n/g, ' ').substring(0, 60); return `
${i + 1}
${esc(p.name)}
${leaderPrev ? `
Leader: ${esc(leaderPrev)}${leaderPrev.length >= 60 ? '…' : ''}
` : ''} ${allPrev ? `
All: ${esc(allPrev)}${allPrev.length >= 60 ? '…' : ''}
` : ''}
`; }).join(''); } /* ───────────────────────────────────────────────────────── Step manipulation (exposed globally) ───────────────────────────────────────────────────────── */ window.builderAddPrayer = function (prayerId) { const prayer = PRAYERS.find(p => String(p.id) === String(prayerId)); if (!prayer) return; // Default attribution: leader_all if prayer has both parts, else leader_only / all_only / none let attr = 'leader_all'; if (!prayer.leader_text && !prayer.all_text) attr = 'none'; else if (!prayer.all_text) attr = 'leader_only'; else if (!prayer.leader_text) attr = 'all_only'; STEPS.push({ prayer_id: prayerId, attribution: attr, _prayer: prayer }); renderSequence(); // Briefly highlight the new step const list = document.getElementById('step-list'); const cards = list.querySelectorAll('.step-card'); const last = cards[cards.length - 1]; if (last) { last.style.outline = '2px solid var(--primary)'; setTimeout(() => { last.style.outline = ''; }, 800); last.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } }; window.builderRemove = function (index) { STEPS.splice(index, 1); renderSequence(); }; window.builderMove = function (index, dir) { const target = index + dir; if (target < 0 || target >= STEPS.length) return; [STEPS[index], STEPS[target]] = [STEPS[target], STEPS[index]]; renderSequence(); }; window.builderSetAttribution = function (index, value) { if (STEPS[index]) STEPS[index].attribution = value; }; window.builderEditPrayer = function (prayerId) { openModal(prayerId); }; /* ───────────────────────────────────────────────────────── Modal — create / edit prayer ───────────────────────────────────────────────────────── */ function openModal(prayerId) { editingPrayerId = prayerId; const modal = document.getElementById('prayer-modal'); const title = document.getElementById('modal-title'); // Reset form document.getElementById('modal-name').value = ''; document.getElementById('modal-leader').value = ''; document.getElementById('modal-all').value = ''; document.getElementById('modal-global').checked = false; document.querySelector('input[name="modal-attr"][value="leader_all"]').checked = true; if (prayerId) { const p = PRAYERS.find(pr => String(pr.id) === String(prayerId)); if (!p) return; title.textContent = 'Edit Prayer'; document.getElementById('modal-name').value = p.name; document.getElementById('modal-leader').value = p.leader_text || ''; document.getElementById('modal-all').value = p.all_text || ''; document.getElementById('modal-global').checked = !!p.is_global; // Determine existing attribution for radio pre-selection const hasLeader = !!(p.leader_text || '').trim(); const hasAll = !!(p.all_text || '').trim(); const attrVal = hasLeader && hasAll ? 'leader_all' : hasLeader ? 'leader_only' : hasAll ? 'all_only' : 'none'; const radio = document.querySelector(`input[name="modal-attr"][value="${attrVal}"]`); if (radio) radio.checked = true; } else { title.textContent = 'New Custom Prayer'; } updateModalFields(); updateAttrRadioStyles(); modal.hidden = false; document.getElementById('modal-name').focus(); } function closeModal() { document.getElementById('prayer-modal').hidden = true; editingPrayerId = null; } function updateModalFields() { const attr = getSelectedAttr(); const leaderGroup = document.getElementById('modal-leader-group'); const allGroup = document.getElementById('modal-all-group'); leaderGroup.style.display = (attr === 'all_only') ? 'none' : ''; allGroup.style.display = (attr === 'leader_only' || attr === 'none') ? 'none' : ''; updateAttrRadioStyles(); } function updateAttrRadioStyles() { document.querySelectorAll('.attr-radio-label').forEach(function (label) { const radio = label.querySelector('input[type=radio]'); label.classList.toggle('checked', radio && radio.checked); }); } function getSelectedAttr() { const checked = document.querySelector('input[name="modal-attr"]:checked'); return checked ? checked.value : 'leader_all'; } function saveModalPrayer() { const name = document.getElementById('modal-name').value.trim(); const leader = document.getElementById('modal-leader').value.trim(); const all = document.getElementById('modal-all').value.trim(); const global = document.getElementById('modal-global').checked ? 1 : 0; const attr = getSelectedAttr(); if (!name) { document.getElementById('modal-name').focus(); showModalError('Prayer name is required.'); return; } // Set text fields based on attribution mode const sendLeader = (attr !== 'all_only') ? leader : ''; const sendAll = (attr !== 'leader_only' && attr !== 'none') ? all : ''; const method = editingPrayerId ? 'PUT' : 'POST'; const url = BASE_URL + '/api/prayers_api.php' + (editingPrayerId ? '?id=' + editingPrayerId : ''); const saveBtn = document.getElementById('modal-save'); saveBtn.disabled = true; saveBtn.textContent = 'Saving…'; fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, leader_text: sendLeader, all_text: sendAll, is_global: global }), }) .then(r => r.json()) .then(function (data) { if (data.error) { showModalError(data.error); return; } const prayer = data.prayer; if (editingPrayerId) { // Update in local list const idx = PRAYERS.findIndex(p => String(p.id) === String(prayer.id)); if (idx >= 0) PRAYERS[idx] = prayer; // Also update any steps referencing this prayer STEPS.forEach(function (step) { if (String(step.prayer_id) === String(prayer.id)) step._prayer = prayer; }); renderSequence(); } else { PRAYERS.unshift(prayer); } renderLibrary(); closeModal(); }) .catch(function () { showModalError('Network error. Please try again.'); }) .finally(function () { saveBtn.disabled = false; saveBtn.textContent = 'Save Prayer'; }); } function showModalError(msg) { let el = document.getElementById('modal-error'); if (!el) { el = document.createElement('div'); el.id = 'modal-error'; el.className = 'alert alert-error'; el.style.marginBottom = '12px'; document.querySelector('.modal-box').prepend(el); } el.textContent = msg; el.style.display = ''; setTimeout(() => { el.style.display = 'none'; }, 5000); } /* ───────────────────────────────────────────────────────── Save session ───────────────────────────────────────────────────────── */ function saveSession() { const name = document.getElementById('session-name').value.trim(); if (!name) { document.getElementById('session-name').focus(); showMsg('Please enter a session label.', 'error'); return; } if (!STEPS.length) { showMsg('Add at least one prayer to your sequence before saving.', 'error'); return; } const payload = { id: window.EDIT_SESSION_ID || 0, name: name, is_public: document.getElementById('is-public').checked, subject_name: document.getElementById('subject-name').value.trim(), subject_pronoun: document.getElementById('subject-pronoun').value, subject_dates: document.getElementById('subject-dates').value.trim(), steps: STEPS.map(s => ({ prayer_id: s.prayer_id, attribution: s.attribution })), }; const btn = document.getElementById('btn-save'); btn.disabled = true; btn.textContent = 'Saving…'; fetch(BASE_URL + '/api/builder_session.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) .then(r => r.json()) .then(function (data) { if (data.error) { showMsg(data.error, 'error'); return; } window.EDIT_SESSION_ID = data.session_id; // Update page title & edit button const presentLink = document.getElementById('present-link'); if (presentLink) { presentLink.href = data.present_url; presentLink.style.display = ''; } document.title = name + ' — Rosary Builder'; // Update URL without reload history.replaceState(null, '', BASE_URL + '/admin/builder.php?id=' + data.session_id); showMsg('Session saved! Open presentation →', 'success'); }) .catch(function () { showMsg('Network error. Please try again.', 'error'); }) .finally(function () { btn.disabled = false; btn.textContent = 'Save Session'; }); } function showMsg(html, type) { const wrap = document.getElementById('builder-msg'); wrap.innerHTML = `
${html}
`; wrap.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); if (type === 'success') setTimeout(() => { wrap.innerHTML = ''; }, 6000); } /* ───────────────────────────────────────────────────────── Utility ───────────────────────────────────────────────────────── */ function esc(str) { if (!str) return ''; return str.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } })();