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:
@@ -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>✝ <?= htmlspecialchars($site_name) ?></h1>
|
||||
<div class="header-actions">
|
||||
<a href="<?= BASE_URL ?>/" class="btn btn-ghost" style="font-size:13px">← 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">← 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">✓ 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>
|
||||
Reference in New Issue
Block a user