/**
* 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) {
if (s.step_type === 'bead') {
STEPS.push({ step_type: 'bead', bead_type: s.bead_type });
} else {
const prayer = PRAYERS.find(p => String(p.id) === String(s.prayer_id));
if (prayer) STEPS.push({ step_type: 'prayer', 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;
}
const beadMeta = {
small: { icon: '○', label: 'Small Bead', sub: 'Hail Mary bead' },
large: { icon: '●', label: 'Large Bead', sub: 'Our Father bead' },
crucifix: { icon: '✝', label: 'Crucifix', sub: 'Cross bead' },
};
list.innerHTML = STEPS.map(function (step, i) {
const moveUp = ``;
const moveDn = ``;
const remove = ``;
if (step.step_type === 'bead') {
const m = beadMeta[step.bead_type] || beadMeta.small;
return `
${i + 1}
${m.icon} ${m.label}
${m.sub}
${moveUp}${moveDn}${remove}
`;
}
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 ? '…' : ''}
` : ''}
${moveUp}${moveDn}${remove}
`;
}).join('');
}
/* ─────────────────────────────────────────────────────────
Step manipulation (exposed globally)
───────────────────────────────────────────────────────── */
window.builderAddBead = function (beadType) {
STEPS.push({ step_type: 'bead', bead_type: beadType });
renderSequence();
const cards = document.querySelectorAll('#step-list .step-card');
const last = cards[cards.length - 1];
if (last) {
last.style.outline = '2px solid #f59e0b';
setTimeout(() => { last.style.outline = ''; }, 800);
last.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
};
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({ step_type: 'prayer', 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.classList.add('modal-open');
document.getElementById('modal-name').focus();
}
function closeModal() {
document.getElementById('prayer-modal').classList.remove('modal-open');
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 => s.step_type === 'bead'
? { step_type: 'bead', bead_type: s.bead_type }
: { step_type: 'prayer', 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,'"');
}
})();