Files
Rosary/present.php
T
pguzman 76a5061fba Custom sessions now draw their own bead ring matching the builder sequence
present.php extracts the ordered bead types (small/large/crucifix) from
custom session slides and passes them as CUSTOM_BEADS to the JS layer.

rosary.js reads CUSTOM_BEADS on init: if present, it draws exactly those
N beads (with their correct types) instead of the standard 60-bead ring.
The three hardcoded 60s and the fixed type Sets are now dynamic so any
sequence length works. Standard sessions are unchanged (CUSTOM_BEADS=null).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 21:34:24 -07:00

246 lines
8.9 KiB
PHP

<?php
/**
* present.php — Public presentation view.
* No authentication required — URL is shared with participants.
* Supports pretty-URL access: /username/slug → ?username=X&slug=Y via .htaccess
*/
require_once __DIR__ . '/config/db.php';
require_once __DIR__ . '/includes/build_slides.php';
// Support pretty-URL access: /username/slug
if (!isset($_GET['id']) && isset($_GET['username'], $_GET['slug'])) {
$pdo = get_pdo();
$user_stmt = $pdo->prepare('SELECT id FROM users WHERE username = ?');
$user_stmt->execute([$_GET['username']]);
$user_row = $user_stmt->fetch();
if (!$user_row) {
http_response_code(404);
die('User not found.');
}
// Try sessions first
$ses_stmt = $pdo->prepare('SELECT id FROM sessions WHERE user_id = ? AND slug = ? AND is_public = 1');
$ses_stmt->execute([$user_row['id'], $_GET['slug']]);
$ses_row = $ses_stmt->fetch();
if ($ses_row) {
$_GET['id'] = $ses_row['id'];
} else {
// Try novena group — find first day session
$grp_stmt = $pdo->prepare('SELECT id FROM novena_groups WHERE user_id = ? AND slug = ? AND is_public = 1');
$grp_stmt->execute([$user_row['id'], $_GET['slug']]);
$grp_row = $grp_stmt->fetch();
if ($grp_row) {
// Show day-picker instead of jumping straight to Day 1
header('Location: ' . BASE_URL . '/novena_public.php?group_id=' . (int)$grp_row['id']);
exit;
} else {
http_response_code(404);
die('Rosary not found.');
}
}
}
// Load session
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id < 1) {
die('Missing session ID. Please use a valid presentation link.');
}
$stmt = get_pdo()->prepare('SELECT * FROM sessions WHERE id = ?');
$stmt->execute([$id]);
$session = $stmt->fetch();
if (!$session) {
die('Session not found.');
}
// Resolve "by_day_of_week" mystery set to the actual set for today
if ($session['mystery_set'] === 'by_day_of_week') {
$dow_map = [
0 => 'glorious', // Sunday
1 => 'joyful', // Monday
2 => 'sorrowful', // Tuesday
3 => 'glorious', // Wednesday
4 => 'luminous', // Thursday
5 => 'sorrowful', // Friday
6 => 'joyful', // Saturday
];
$session['mystery_set'] = $dow_map[(int)date('w')];
}
// "all_sorrowful" is stored as-is but maps to sorrowful
if ($session['mystery_set'] === 'all_sorrowful') {
$session['mystery_set'] = 'sorrowful';
}
// Build slide array
$slides = build_slides($session);
// For custom sessions, extract the ordered bead sequence so rosary.js
// can draw exactly those beads instead of the standard 60-bead ring.
$custom_beads = null;
if ($session['occasion'] === 'custom') {
$custom_beads = [];
foreach ($slides as $slide) {
if (!empty($slide['bead'])) {
$custom_beads[] = $slide['bead'];
}
}
if (empty($custom_beads)) $custom_beads = null;
}
// Prepare JSON for JavaScript (HTML-safe)
$slides_json = json_encode($slides, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
// Session display info
$mystery_labels = [
'sorrowful' => 'Sorrowful Mysteries',
'joyful' => 'Joyful Mysteries',
'glorious' => 'Glorious Mysteries',
'luminous' => 'Luminous Mysteries',
];
$mystery_label = $mystery_labels[$session['mystery_set']] ?? ucfirst($session['mystery_set']);
$site_name = get_setting('site_name', APP_NAME);
// Back URL — novena sessions return to day-picker; others return home
$back_url = BASE_URL . '/';
if (!empty($session['novena_group_id'])) {
$back_url = BASE_URL . '/novena_public.php?group_id=' . (int)$session['novena_group_id'];
}
// Build audio manifest: scan uploads/audio/ for available files
$audio_manifest = [];
$audio_dir = UPLOADS_DIR . 'audio/';
if (is_dir($audio_dir)) {
foreach (glob($audio_dir . '*.*') ?: [] as $f) {
$base = basename($f);
$dot = strrpos($base, '.');
if ($dot !== false) {
$k = substr($base, 0, $dot);
$e = strtolower(substr($base, $dot + 1));
if (preg_match('/^[a-z0-9_]+$/', $k)) {
$audio_manifest[$k] = $e;
}
}
}
}
$has_audio = !empty($audio_manifest);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="<?= BASE_URL ?>/favicon.svg">
<title><?= htmlspecialchars($session['name']) ?> — <?= htmlspecialchars($site_name) ?></title>
<link rel="stylesheet" href="<?= BASE_URL ?>/assets/css/present.css?v=8">
</head>
<body>
<!-- Rosary bead overlay (SVG inserted by rosary.js) -->
<div id="rosary-overlay"></div>
<!-- Presentation area -->
<div id="presenter">
<!-- Slide content -->
<div id="slide-content">
<!-- Cover -->
<div id="cover-slide" class="cover-slide" style="display:none">
<div id="cover-photo-wrap">
<img id="cover-photo" src="" alt="">
</div>
<h1 id="cover-title"></h1>
<div id="cover-body"></div>
</div>
<!-- Prayer slide -->
<div id="prayer-slide" style="display:none">
<p class="repeat-badge" style="display:none"></p>
<h2 id="slide-title"></h2>
<div id="slide-text">
<div id="leader-wrap">
<span class="label leader-label">Leader:</span>
<p id="leader-text"></p>
</div>
<div id="all-wrap">
<span class="label all-label">All:</span>
<p id="all-text"></p>
</div>
</div>
</div>
<!-- Mystery slide -->
<div id="mystery-slide" style="display:none">
<div class="mystery-number" id="mystery-number"></div>
<h2 id="mystery-title"></h2>
<p id="mystery-fruit"></p>
</div>
<!-- Litany slide -->
<div id="litany-slide" style="display:none">
<p class="repeat-badge" style="display:none"></p>
<h2 id="litany-title"></h2>
<div id="litany-text">
<div id="litany-leader-wrap">
<span class="label leader-label">Leader:</span>
<p id="litany-leader"></p>
</div>
<div id="litany-all-wrap">
<span class="label all-label">All:</span>
<p id="litany-all"></p>
</div>
</div>
</div>
<!-- Closing slide -->
<div id="closing-slide" style="display:none">
<div id="closing-photo-wrap" style="display:none">
<img id="closing-photo" src="" alt="">
</div>
<div class="closing-cross">&#x271D;</div>
<h2 id="closing-title"></h2>
<p id="closing-subtitle"></p>
<p id="closing-body"></p>
</div>
</div>
<!-- Navigation -->
<nav id="presenter-nav">
<button id="btn-exit" class="nav-btn nav-exit" aria-label="Exit presentation" title="Exit — your place is saved">&#8962;</button>
<?php if ($has_audio): ?>
<button id="btn-audio" class="nav-btn nav-audio" aria-label="Toggle audio" title="Toggle audio">&#128264;</button>
<?php endif; ?>
<div class="nav-sep"></div>
<button id="btn-prev" class="nav-btn" aria-label="Previous slide">&#9664;</button>
<span id="slide-counter"></span>
<button id="btn-next" class="nav-btn" aria-label="Next slide">&#9654;</button>
</nav>
<!-- Keyboard hint -->
<div id="key-hint">&#8592; &#8594; arrows to navigate &nbsp;|&nbsp; F = fullscreen</div>
<!-- Resume toast (auto-fades in when a saved position is restored) -->
<div id="resume-toast"></div>
</div>
<!-- Session data for JS -->
<script>
var SLIDES = <?= $slides_json ?>;
var SESSION_NAME = <?= json_encode($session['name']) ?>;
var MYSTERY_LABEL = <?= json_encode($mystery_label) ?>;
var UPLOADS_BASE = <?= json_encode(UPLOADS_URL) ?>;
var SESSION_ID = <?= json_encode((int)$session['id']) ?>;
var BACK_URL = <?= json_encode($back_url) ?>;
var AUDIO_MANIFEST = <?= json_encode($audio_manifest, JSON_HEX_TAG | JSON_HEX_AMP) ?>;
var AUDIO_BASE_URL = <?= json_encode(UPLOADS_URL . 'audio/') ?>;
var CUSTOM_BEADS = <?= json_encode($custom_beads) ?>;
</script>
<script src="<?= BASE_URL ?>/assets/js/rosary.js?v=9"></script>
<script src="<?= BASE_URL ?>/assets/js/presenter.js?v=11"></script>
</body>
</html>