diff --git a/admin/builder.php b/admin/builder.php index ae09ef2..6964a6f 100644 --- a/admin/builder.php +++ b/admin/builder.php @@ -33,12 +33,12 @@ if (isset($_GET['id'])) { } // Load steps with prayer data $step_stmt = $pdo->prepare(" - SELECT bs.prayer_id, bs.attribution, + SELECT bs.step_type, bs.bead_type, bs.prayer_id, bs.attribution, cp.name, cp.leader_text, cp.all_text, cp.is_global, cp.created_by, IF(cp.is_global=1 AND u.role='superadmin','standard', IF(cp.is_global=1,'global','mine')) AS source_tag FROM builder_steps bs - JOIN custom_prayers cp ON cp.id = bs.prayer_id + LEFT JOIN custom_prayers cp ON cp.id = bs.prayer_id LEFT JOIN users u ON u.id = cp.created_by WHERE bs.session_id = ? ORDER BY bs.step_order ASC @@ -168,6 +168,37 @@ $page_title = $session ? 'Edit: ' . htmlspecialchars($session['name']) : 'Rosary + +
+
Bead Markers
+
+ + + +
+
+
Loading… @@ -255,8 +286,10 @@ var IS_ADMIN = ; var EDIT_SESSION_ID = ; var PRAYERS_DATA = ; var EXISTING_STEPS = [ - 'prayer_id' => (int)$s['prayer_id'], - 'attribution' => $s['attribution'], + 'step_type' => $s['step_type'] ?? 'prayer', + 'bead_type' => $s['bead_type'] ?? null, + 'prayer_id' => $s['prayer_id'] ? (int)$s['prayer_id'] : null, + 'attribution' => $s['attribution'] ?? 'leader_all', ], $edit_steps)) ?>; diff --git a/api/builder_session.php b/api/builder_session.php index c385955..12cc535 100644 --- a/api/builder_session.php +++ b/api/builder_session.php @@ -49,28 +49,40 @@ if ($name === '') json_err('Session name is required'); if (empty($steps)) json_err('Add at least one prayer to your sequence'); $valid_attributions = ['leader_all', 'leader_only', 'all_only', 'none']; +$valid_bead_types = ['small', 'large', 'crucifix']; -// Validate steps +// Validate steps and collect prayer IDs to verify +$prayer_ids = []; foreach ($steps as $i => $step) { - $pid = (int)($step['prayer_id'] ?? 0); - $att = $step['attribution'] ?? 'leader_all'; - if (!$pid) json_err("Step " . ($i + 1) . " is missing a prayer"); - if (!in_array($att, $valid_attributions)) json_err("Invalid attribution on step " . ($i + 1)); + $type = $step['step_type'] ?? 'prayer'; + if ($type === 'bead') { + if (!in_array($step['bead_type'] ?? '', $valid_bead_types)) { + json_err("Invalid bead type on step " . ($i + 1)); + } + } else { + $pid = (int)($step['prayer_id'] ?? 0); + $att = $step['attribution'] ?? 'leader_all'; + if (!$pid) json_err("Step " . ($i + 1) . " is missing a prayer"); + if (!in_array($att, $valid_attributions)) json_err("Invalid attribution on step " . ($i + 1)); + $prayer_ids[] = $pid; + } } // Verify all prayer_ids exist and are accessible -$prayer_ids = array_unique(array_column($steps, 'prayer_id')); -$in_placeholders = implode(',', array_fill(0, count($prayer_ids), '?')); -$valid_stmt = $pdo->prepare(" - SELECT id FROM custom_prayers - WHERE id IN ($in_placeholders) - AND (is_global = 1 OR created_by = ?) -"); -$valid_stmt->execute([...$prayer_ids, $uid]); -$valid_ids = array_column($valid_stmt->fetchAll(), 'id'); -foreach ($prayer_ids as $pid) { - if (!in_array((string)$pid, array_map('strval', $valid_ids))) { - json_err("Prayer ID {$pid} not found or not accessible"); +if (!empty($prayer_ids)) { + $prayer_ids = array_unique($prayer_ids); + $in_placeholders = implode(',', array_fill(0, count($prayer_ids), '?')); + $valid_stmt = $pdo->prepare(" + SELECT id FROM custom_prayers + WHERE id IN ($in_placeholders) + AND (is_global = 1 OR created_by = ?) + "); + $valid_stmt->execute([...$prayer_ids, $uid]); + $valid_ids = array_column($valid_stmt->fetchAll(), 'id'); + foreach ($prayer_ids as $pid) { + if (!in_array((string)$pid, array_map('strval', $valid_ids))) { + json_err("Prayer ID {$pid} not found or not accessible"); + } } } @@ -111,11 +123,16 @@ try { // Insert steps $step_stmt = $pdo->prepare( - "INSERT INTO builder_steps (session_id, step_order, prayer_id, attribution) - VALUES (?, ?, ?, ?)" + "INSERT INTO builder_steps (session_id, step_type, bead_type, step_order, prayer_id, attribution) + VALUES (?, ?, ?, ?, ?, ?)" ); foreach ($steps as $order => $step) { - $step_stmt->execute([$session_id, $order, (int)$step['prayer_id'], $step['attribution']]); + $type = $step['step_type'] ?? 'prayer'; + if ($type === 'bead') { + $step_stmt->execute([$session_id, 'bead', $step['bead_type'], $order, null, 'none']); + } else { + $step_stmt->execute([$session_id, 'prayer', null, $order, (int)$step['prayer_id'], $step['attribution']]); + } } $pdo->commit(); diff --git a/assets/css/builder.css b/assets/css/builder.css index 94163bd..0523fb0 100644 --- a/assets/css/builder.css +++ b/assets/css/builder.css @@ -439,6 +439,97 @@ margin-top: 2px; } +/* ── Bead markers section ────────────────────────────────────── */ + +.bead-markers-section { + padding: 10px 16px 8px; + border-bottom: 1px solid var(--border); + background: var(--bg-card); + flex-shrink: 0; +} + +.bead-markers-label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .06em; + color: var(--muted); + margin-bottom: 8px; +} + +.bead-marker-cards { + display: flex; + gap: 8px; +} + +.bead-marker-card { + flex: 1; + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg); + cursor: pointer; + font-family: var(--font); + font-size: 12px; + text-align: left; + transition: border-color .15s, background .15s; +} + +.bead-marker-card:hover { + border-color: var(--primary); + background: #eff6ff; +} + +.bead-icon-sm { font-size: 18px; color: #6b7280; flex-shrink: 0; line-height: 1; } +.bead-icon-lg { font-size: 20px; color: #374151; flex-shrink: 0; line-height: 1; } +.bead-icon-cx { font-size: 18px; color: var(--gold); flex-shrink: 0; line-height: 1; } + +.bead-card-text { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; +} + +.bead-card-text strong { font-size: 12px; font-weight: 600; color: var(--text); } +.bead-card-text small { font-size: 10px; color: var(--muted); } + +.bead-add-label { + font-size: 11px; + color: var(--primary); + font-weight: 600; + flex-shrink: 0; +} + +/* ── Bead step card (in sequence list) ───────────────────────── */ + +.step-card.bead-step { + background: #fefce8; + border-color: #fde68a; +} + +.step-card.bead-step:hover { + border-color: #f59e0b; +} + +.bead-step-name { + font-size: 13px; + font-weight: 600; + color: #92400e; + display: flex; + align-items: center; + gap: 6px; +} + +.bead-step-sub { + font-size: 11px; + color: #b45309; + margin-top: 2px; +} + /* ── Library footer ──────────────────────────────────────────── */ .library-footer { diff --git a/assets/js/builder.js b/assets/js/builder.js index 499394c..4274f79 100644 --- a/assets/js/builder.js +++ b/assets/js/builder.js @@ -23,8 +23,12 @@ // 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 }); + 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 }); + } }); } @@ -146,8 +150,30 @@ 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 p = step._prayer; + 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); @@ -164,11 +190,7 @@
-
- - - -
+
${moveUp}${moveDn}${remove}
`; }).join(''); } @@ -176,6 +198,18 @@ /* ───────────────────────────────────────────────────────── 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; @@ -186,7 +220,7 @@ 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 }); + STEPS.push({ step_type: 'prayer', prayer_id: prayerId, attribution: attr, _prayer: prayer }); renderSequence(); // Briefly highlight the new step const list = document.getElementById('step-list'); @@ -381,7 +415,9 @@ 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 })), + 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'); diff --git a/includes/build_slides.php b/includes/build_slides.php index cf6bbd2..ad0e723 100644 --- a/includes/build_slides.php +++ b/includes/build_slides.php @@ -15,9 +15,10 @@ require_once __DIR__ . '/../data/prayers.php'; */ function get_builder_steps(PDO $pdo, int $session_id): array { $st = $pdo->prepare(" - SELECT bs.attribution, cp.name, cp.leader_text, cp.all_text + SELECT bs.step_type, bs.bead_type, bs.attribution, + cp.name, cp.leader_text, cp.all_text FROM builder_steps bs - JOIN custom_prayers cp ON cp.id = bs.prayer_id + LEFT JOIN custom_prayers cp ON cp.id = bs.prayer_id WHERE bs.session_id = ? ORDER BY bs.step_order ASC "); @@ -104,8 +105,25 @@ function build_slides(array $session): array { 'photo_path' => $session['photo_path'] ?? null, ]; + $bead_idx = 0; // increments for each bead separator step foreach ($steps as $i => $step) { - $attr = $step['attribution']; + $type = $step['step_type'] ?? 'prayer'; + + if ($type === 'bead') { + $slides[] = [ + 'id' => 'bead_' . $i, + 'type' => 'prayer', + 'section' => 'bead', + 'title' => '', + 'leader' => '', + 'all' => '', + 'bead' => $step['bead_type'] ?? 'small', + 'bead_index' => $bead_idx++, + ]; + continue; + } + + $attr = $step['attribution'] ?? 'leader_all'; $leader = ''; $all = ''; @@ -116,7 +134,6 @@ function build_slides(array $session): array { $all = $step['all_text'] ?? ''; } if ($attr === 'none') { - // Show prayer text without attribution labels — put in leader field $leader = ($step['leader_text'] ?: $step['all_text']) ?? ''; } diff --git a/migrate_v5.php b/migrate_v5.php new file mode 100644 index 0000000..6179a22 --- /dev/null +++ b/migrate_v5.php @@ -0,0 +1,67 @@ +exec($sql); + $log[] = ['ok', $label]; + } catch (PDOException $e) { + if (in_array($e->errorInfo[1], [1060, 1061, 1054], true)) { + $log[] = ['skip', $label . ' (already exists)']; + } else { + $log[] = ['err', $label . ': ' . $e->getMessage()]; + } + } +} + +mig5_sql($pdo, 'Add step_type column', " + ALTER TABLE builder_steps + ADD COLUMN step_type ENUM('prayer','bead') NOT NULL DEFAULT 'prayer' AFTER session_id +", $log); + +mig5_sql($pdo, 'Add bead_type column', " + ALTER TABLE builder_steps + ADD COLUMN bead_type ENUM('small','large','crucifix') NULL AFTER step_type +", $log); + +mig5_sql($pdo, 'Make prayer_id nullable', " + ALTER TABLE builder_steps + MODIFY COLUMN prayer_id INT NULL +", $log); + +?> + + + + Migrate v5 + + + +

Migrate v5 — Bead Separator Support

+ + $l[0] === 'err')): ?> +
+ Migration complete. Delete this file: migrate_v5.php +
+ + +