Initial commit — Rosary Presenter App
Full source for loveandrosary.com: slide-based Rosary/novena/Divine Mercy Chaplet presentation tool with multi-user roles, SVG bead ring, audio uploads, donate strip, and public session profiles. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+231
@@ -0,0 +1,231 @@
|
||||
<?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);
|
||||
|
||||
// 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">✝</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">⌂</button>
|
||||
<?php if ($has_audio): ?>
|
||||
<button id="btn-audio" class="nav-btn nav-audio" aria-label="Toggle audio" title="Toggle audio">🔈</button>
|
||||
<?php endif; ?>
|
||||
<div class="nav-sep"></div>
|
||||
<button id="btn-prev" class="nav-btn" aria-label="Previous slide">◀</button>
|
||||
<span id="slide-counter"></span>
|
||||
<button id="btn-next" class="nav-btn" aria-label="Next slide">▶</button>
|
||||
</nav>
|
||||
|
||||
<!-- Keyboard hint -->
|
||||
<div id="key-hint">← → arrows to navigate | 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/') ?>;
|
||||
</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>
|
||||
Reference in New Issue
Block a user