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:
2026-05-13 18:44:08 -07:00
commit 663fde3909
46 changed files with 10902 additions and 0 deletions
+338
View File
@@ -0,0 +1,338 @@
<?php
/**
* admin/novena_group.php — View and manage one novena group.
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../includes/auth.php';
require_auth();
$pdo = get_pdo();
$user = current_user();
$uid = (int)$user['id'];
$gid = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$site_name = get_setting('site_name', APP_NAME);
if ($gid < 1) {
header('Location: ' . BASE_URL . '/admin/');
exit;
}
// Load group and verify ownership
$group = $pdo->prepare('SELECT * FROM novena_groups WHERE id = ?');
$group->execute([$gid]);
$group = $group->fetch();
if (!$group) {
header('Location: ' . BASE_URL . '/admin/');
exit;
}
// Ownership check
if (!has_role('admin') && (int)$group['user_id'] !== $uid) {
header('Location: ' . BASE_URL . '/admin/');
exit;
}
$is_dm = ($group['mystery_set'] === 'chaplet');
// Handle delete of a single day session
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_day_id'])) {
$did = (int)$_POST['delete_day_id'];
$pdo->prepare('DELETE FROM sessions WHERE id = ? AND novena_group_id = ?')->execute([$did, $gid]);
$remaining = (int)$pdo->query("SELECT COUNT(*) FROM sessions WHERE novena_group_id = {$gid}")->fetchColumn();
if ($remaining < 1) {
$pdo->prepare('DELETE FROM novena_groups WHERE id = ?')->execute([$gid]);
header('Location: ' . BASE_URL . '/admin/');
exit;
}
header('Location: ' . BASE_URL . '/admin/novena_group.php?id=' . $gid . '&deleted=1');
exit;
}
// Handle group-level metadata edit
$save_error = '';
$save_success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_group'])) {
$g_name = trim($_POST['g_name'] ?? '');
$g_photo = trim($_POST['g_photo'] ?? '') ?: null;
$g_public = isset($_POST['is_public']) ? 1 : 0;
if ($is_dm) {
// Divine Mercy: keep mystery_set as 'chaplet', no subject fields
$g_mystery = 'chaplet';
$g_subject = null;
$g_pronoun = null;
$g_dates = null;
} else {
$g_mystery = trim($_POST['g_mystery'] ?? '');
$g_subject = trim($_POST['g_subject'] ?? '') ?: null;
$g_pronoun = trim($_POST['g_pronoun'] ?? '') ?: null;
$g_dates = trim($_POST['g_dates'] ?? '') ?: null;
}
$valid_mysteries = ['sorrowful', 'joyful', 'glorious', 'luminous', 'by_day_of_week', 'chaplet'];
if ($g_name === '') {
$save_error = 'Name is required.';
} elseif (!in_array($g_mystery, $valid_mysteries, true)) {
$save_error = 'Invalid mystery set.';
} else {
$pdo->prepare('
UPDATE novena_groups
SET name = ?, mystery_set = ?, subject_name = ?,
subject_pronoun = ?, subject_dates = ?,
photo_path = COALESCE(?, photo_path),
is_public = ?
WHERE id = ?
')->execute([$g_name, $g_mystery, $g_subject, $g_pronoun, $g_dates, $g_photo, $g_public, $gid]);
$pdo->prepare('
UPDATE sessions
SET mystery_set = ?, subject_name = ?,
subject_pronoun = ?, subject_dates = ?,
photo_path = COALESCE(?, photo_path),
name = CONCAT(?, CONCAT(\' — Day \', novena_day)),
is_public = ?
WHERE novena_group_id = ?
')->execute([$g_mystery, $g_subject, $g_pronoun, $g_dates, $g_photo, $g_name, $g_public, $gid]);
$save_success = true;
// Reload group
$grp_stmt = $pdo->prepare('SELECT * FROM novena_groups WHERE id = ?');
$grp_stmt->execute([$gid]);
$group = $grp_stmt->fetch();
}
}
$days = $pdo->prepare('SELECT * FROM sessions WHERE novena_group_id = ? ORDER BY novena_day');
$days->execute([$gid]);
$days = $days->fetchAll();
$mystery_labels = [
'sorrowful' => 'Sorrowful Mysteries',
'joyful' => 'Joyful Mysteries',
'glorious' => 'Glorious Mysteries',
'luminous' => 'Luminous Mysteries',
'by_day_of_week' => 'By Day of Week',
'chaplet' => 'Chaplet of Divine Mercy',
];
?>
<!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($group['name']) ?> — <?= htmlspecialchars($site_name) ?></title>
<link rel="stylesheet" href="<?= BASE_URL ?>/assets/css/setup.css">
<script>var BASE_URL = '<?= BASE_URL ?>';</script>
</head>
<body>
<div class="admin-container">
<header class="admin-header">
<h1>&#x271D; <?= htmlspecialchars($site_name) ?></h1>
<div class="header-actions">
<a href="<?= BASE_URL ?>/" class="btn btn-ghost" style="font-size:13px">&#x2190; View Site</a>
<?php if (has_role('admin')): ?>
<a href="<?= BASE_URL ?>/admin/users.php" class="btn btn-ghost">Users</a>
<?php endif; ?>
<?php if (has_role('superadmin')): ?>
<a href="<?= BASE_URL ?>/admin/settings.php" class="btn btn-ghost">Settings</a>
<?php endif; ?>
<a href="<?= BASE_URL ?>/admin/profile.php" class="btn btn-ghost"><?= htmlspecialchars($user['display_name'] ?: $user['username']) ?></a>
<a href="<?= BASE_URL ?>/admin/" class="btn btn-ghost">&#x2190; All Sessions</a>
<a href="<?= BASE_URL ?>/logout" class="btn btn-ghost">Logout</a>
</div>
</header>
<main>
<nav class="breadcrumb">
<a href="<?= BASE_URL ?>/admin/">Sessions</a>
<span class="bc-sep">/</span>
<span><?= htmlspecialchars($group['name']) ?></span>
</nav>
<?php if (isset($_GET['deleted'])): ?>
<div class="alert alert-success">Day deleted successfully.</div>
<?php endif; ?>
<?php if ($save_success): ?>
<div class="alert alert-success">&#x2713; Novena updated. Changes applied to all 9 day sessions.</div>
<?php elseif ($save_error): ?>
<div class="alert alert-error"><?= htmlspecialchars($save_error) ?></div>
<?php endif; ?>
<section class="card" style="margin-bottom:32px">
<h2 class="card-title">Novena Details</h2>
<form method="post">
<input type="hidden" name="save_group" value="1">
<div class="form-grid">
<div class="form-group">
<label class="form-label" for="g_name">Novena Name</label>
<input type="text" id="g_name" name="g_name" class="form-input"
value="<?= htmlspecialchars($group['name']) ?>" required>
<p class="form-help">Used as the base name for all 9 days.</p>
</div>
<?php if (!$is_dm): ?>
<div class="form-group">
<label class="form-label" for="g_mystery">Mysteries</label>
<select id="g_mystery" name="g_mystery" class="form-input">
<?php foreach ([
'sorrowful' => 'All Sorrowful Mysteries',
'by_day_of_week' => 'By Day of Week',
'joyful' => 'All Joyful Mysteries',
'glorious' => 'All Glorious Mysteries',
'luminous' => 'All Luminous Mysteries',
] as $val => $label): ?>
<option value="<?= $val ?>" <?= $group['mystery_set'] === $val ? 'selected' : '' ?>>
<?= $label ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label" for="g_subject">Name of Deceased</label>
<input type="text" id="g_subject" name="g_subject" class="form-input"
value="<?= htmlspecialchars($group['subject_name'] ?? '') ?>">
</div>
<div class="form-group">
<label class="form-label">Pronoun</label>
<div class="radio-row">
<label><input type="radio" name="g_pronoun" value="he"
<?= ($group['subject_pronoun'] ?? 'he') === 'he' ? 'checked' : '' ?>> He / Him</label>
<label><input type="radio" name="g_pronoun" value="she"
<?= ($group['subject_pronoun'] ?? '') === 'she' ? 'checked' : '' ?>> She / Her</label>
</div>
</div>
<div class="form-group">
<label class="form-label" for="g_dates">Life Dates</label>
<input type="text" id="g_dates" name="g_dates" class="form-input"
placeholder="e.g., March 15, 1942 June 3, 2025"
value="<?= htmlspecialchars($group['subject_dates'] ?? '') ?>">
</div>
<?php endif; ?>
<div class="form-group" id="photo-group">
<label class="form-label">Photo</label>
<?php if ($group['photo_path']): ?>
<div class="photo-preview-wrap">
<img id="photo-preview" src="<?= htmlspecialchars('/' . ltrim($group['photo_path'], '/')) ?>"
alt="Current photo" class="photo-preview">
</div>
<?php else: ?>
<div class="photo-preview-wrap" style="display:none">
<img id="photo-preview" src="" alt="" class="photo-preview">
</div>
<?php endif; ?>
<input type="hidden" id="g_photo" name="g_photo"
value="<?= htmlspecialchars($group['photo_path'] ?? '') ?>">
<label class="btn btn-secondary btn-upload" style="margin-top:8px">
<?= $group['photo_path'] ? 'Replace Photo' : 'Upload Photo' ?>
<input type="file" id="photo-file" accept="image/*" style="display:none">
</label>
<span id="photo-status" class="form-help"></span>
</div>
<div class="form-group">
<label style="display:flex;align-items:center;gap:10px;cursor:pointer">
<input type="checkbox" name="is_public" value="1"
<?= $group['is_public'] ? 'checked' : '' ?>
style="width:18px;height:18px">
<span>Public (visible on your profile)</span>
</label>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Changes to All Days</button>
</div>
</form>
</section>
<section>
<h2 style="margin-bottom:16px">Days (<?= count($days) ?> of 9)</h2>
<div class="sessions-table-wrap">
<table class="sessions-table">
<thead>
<tr>
<th>Day</th>
<th>Mysteries</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php for ($d = 1; $d <= 9; $d++):
$ses = null;
foreach ($days as $day_row) {
if ((int)$day_row['novena_day'] === $d) { $ses = $day_row; break; }
}
?>
<tr <?= !$ses ? 'class="day-missing"' : '' ?>>
<td><strong>Day <?= $d ?></strong></td>
<td><?= $ses ? htmlspecialchars($mystery_labels[$ses['mystery_set']] ?? $ses['mystery_set']) : '<em>missing</em>' ?></td>
<td class="actions">
<?php if ($ses): ?>
<a href="<?= BASE_URL ?>/present.php?id=<?= $ses['id'] ?>"
target="_blank"
class="btn btn-sm btn-primary">Present</a>
<form method="post" style="display:inline"
onsubmit="return confirm('Delete Day <?= $d ?>?')">
<input type="hidden" name="delete_day_id" value="<?= $ses['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
<?php else: ?>
<span class="text-muted">—</span>
<?php endif; ?>
</td>
</tr>
<?php endfor; ?>
</tbody>
</table>
</div>
</section>
</main>
</div>
<script>
(function () {
var fileInput = document.getElementById('photo-file');
var photoHidden = document.getElementById('g_photo');
var photoStatus = document.getElementById('photo-status');
var previewWrap = document.querySelector('.photo-preview-wrap');
var previewImg = document.getElementById('photo-preview');
if (!fileInput) return;
fileInput.addEventListener('change', function () {
var file = fileInput.files[0];
if (!file) return;
var fd = new FormData();
fd.append('photo', file);
photoStatus.textContent = 'Uploading\u2026';
fetch(BASE_URL + '/api/upload_photo.php', { method: 'POST', body: fd })
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.path) {
photoHidden.value = data.path;
previewImg.src = '/' + data.path.replace(/^\//, '');
previewWrap.style.display = '';
photoStatus.textContent = 'Photo ready.';
} else {
photoStatus.textContent = 'Upload failed: ' + (data.error || 'unknown error');
}
})
.catch(function () { photoStatus.textContent = 'Upload failed.'; });
});
}());
</script>
</body>
</html>