Add Rosary Builder — custom prayer-sequence sessions

Superuser+ can now build a custom prayer sequence from scratch:
- Two-panel builder UI: step sequence (left) + searchable prayer library (right)
- 16 standard prayers seeded globally; users can create private custom prayers
- Admin can promote private prayers to global and manage the library
- Four attribution modes per step: Leader/All, Leader only, All together, None
- Optional subject name/pronoun for variable substitution in prayers
- Custom sessions fully presented via the existing presenter (auto-split works)
- migrate_v4.php creates custom_prayers + builder_steps tables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-13 19:07:11 -07:00
parent 663fde3909
commit f3fa77da17
9 changed files with 2086 additions and 2 deletions
+100
View File
@@ -10,6 +10,21 @@
require_once __DIR__ . '/../data/prayers.php';
/**
* Fetch ordered builder steps (with prayer text) for a custom session.
*/
function get_builder_steps(PDO $pdo, int $session_id): array {
$st = $pdo->prepare("
SELECT bs.attribution, cp.name, cp.leader_text, cp.all_text
FROM builder_steps bs
JOIN custom_prayers cp ON cp.id = bs.prayer_id
WHERE bs.session_id = ?
ORDER BY bs.step_order ASC
");
$st->execute([$session_id]);
return $st->fetchAll();
}
/**
* Returns the slide array for one decade of the Chaplet of Divine Mercy.
*
@@ -65,6 +80,91 @@ function build_slides(array $session): array {
$slides = [];
// -----------------------------------------------------------------------
// Custom builder session — build directly from builder_steps
// -----------------------------------------------------------------------
if ($session['occasion'] === 'custom') {
$pdo = get_pdo();
$steps = get_builder_steps($pdo, (int)$session['id']);
// Cover slide
$cover_all = '';
if (!empty($session['subject_name'])) {
$cover_all = "For {$session['subject_name']}";
}
$slides[] = [
'id' => 'cover',
'type' => 'cover',
'section' => 'cover',
'title' => $session['name'],
'leader' => '',
'all' => $cover_all,
'bead' => null,
'bead_index' => null,
'photo_path' => $session['photo_path'] ?? null,
];
foreach ($steps as $i => $step) {
$attr = $step['attribution'];
$leader = '';
$all = '';
if ($attr === 'leader_all' || $attr === 'leader_only') {
$leader = $step['leader_text'] ?? '';
}
if ($attr === 'leader_all' || $attr === 'all_only') {
$all = $step['all_text'] ?? '';
}
if ($attr === 'none') {
// Show prayer text without attribution labels — put in leader field
$leader = ($step['leader_text'] ?: $step['all_text']) ?? '';
}
$slides[] = [
'id' => 'custom_' . $i,
'type' => 'prayer',
'section' => $step['name'],
'title' => $step['name'],
'leader' => $leader,
'all' => $all,
'bead' => null,
'bead_index' => null,
];
}
// Closing slide
$slides[] = [
'id' => 'closing_custom',
'type' => 'closing',
'section' => 'closing',
'title' => '',
'leader' => '',
'all' => "May the souls of the faithful departed,\nthrough the mercy of God, rest in peace.",
'bead' => null,
'bead_index' => null,
];
// Apply variable substitution and return early
$name = $session['subject_name'] ?? '';
$pronoun = $session['subject_pronoun'] ?? 'he';
$dates = $session['subject_dates'] ?? '';
$pronoun_obj = ($pronoun === 'she') ? 'her' : 'him';
$pronoun_poss = ($pronoun === 'she') ? 'her' : 'his';
$find = ['{name}', '{pronoun}', '{pronoun_obj}', '{pronoun_poss}', '{subject_dates}'];
$replace = [$name, $pronoun, $pronoun_obj, $pronoun_poss, $dates];
foreach ($slides as &$slide) {
$slide['title'] = str_replace($find, $replace, $slide['title'] ?? '');
$slide['leader'] = str_replace($find, $replace, $slide['leader'] ?? '');
$slide['all'] = str_replace($find, $replace, $slide['all'] ?? '');
}
unset($slide);
return $slides;
}
// -----------------------------------------------------------------------
// 1. Cover slide
// -----------------------------------------------------------------------