Files
Rosary/admin/setup.php
T
pguzman 3a1433b71e Add drag-to-pan + zoom photo repositioning for card/avatar crops
Every photo upload (session setup, novena group, Rosary Builder title
photo) gets shown two ways: full-size on the presentation cover slide
(unaffected, stays untouched), and cropped to a fixed box everywhere else
— home page cards, profile cards, the novena day-picker's circular hero
photo, and each admin form's own preview thumbnail. All of those crops
used to just take the image's dead center, with no way to control what
part of the photo that was — cropping out people's heads on portrait
photos.

- New sessions/novena_groups columns: photo_focal_x, photo_focal_y (0-100%),
  photo_zoom (1-3x), defaulting to 50/50/1 — today's exact centered/
  unzoomed behavior, so this is fully backward compatible until someone
  actively repositions a photo.

- New assets/js/photo-crop.js: a reusable drag-to-pan + zoom modal editor.
  The crop frame renders with the *exact* CSS recipe used at final render
  time (object-position + transform:scale/transform-origin), so the editor
  is a truthful live preview, not an approximation. A reference thumbnail
  shows the full photo dimmed outside a rectangle marking the current crop.
  All math reads actual rendered box dimensions rather than assuming fixed
  pixel sizes, so it holds up responsively at any viewport width — caught
  and fixed a real mismatch bug here by testing the widget standalone in a
  browser before wiring it into any PHP form.

- New includes/photo.php: photo_crop_style() builds the inline style="..."
  from a session/group row, used everywhere a crop is displayed.

- Wired into all three upload locations (admin/setup.php,
  admin/novena_group.php, admin/builder.php) with a "Reposition" button,
  and persisted through api/save_session.php, admin/novena_group.php's
  save handler, and api/builder_session.php.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 15:10:43 -07:00

266 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* admin/setup.php — Create or edit a rosary session.
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/csrf.php';
require_once __DIR__ . '/../includes/photo.php';
require_auth();
$pdo = get_pdo();
$user = current_user();
$uid = (int)$user['id'];
$site_name = get_setting('site_name', APP_NAME);
// Load existing session for editing
$session = null;
if (isset($_GET['id'])) {
$stmt = $pdo->prepare('SELECT * FROM sessions WHERE id = ?');
$stmt->execute([(int)$_GET['id']]);
$session = $stmt->fetch();
if (!$session) {
header('Location: ' . BASE_URL . '/admin/');
exit;
}
// Ownership check: must own or be admin
if (!has_role('admin') && (int)$session['user_id'] !== $uid) {
header('Location: ' . BASE_URL . '/admin/');
exit;
}
}
// Creating new session? Check rosary limit
$limit_error = '';
if (!$session && !can_create_rosary($uid, $user['rosary_limit'])) {
$limit = $user['rosary_limit'];
$limit_error = "You have reached your rosary limit ({$limit}). Please contact an administrator to increase your limit.";
}
$page_title = $session ? 'Edit Session' : 'New Session';
?>
<!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><?= $page_title ?> — <?= htmlspecialchars($site_name) ?></title>
<link rel="stylesheet" href="<?= BASE_URL ?>/assets/css/setup.css">
<link rel="stylesheet" href="<?= BASE_URL ?>/assets/css/photo-crop.css">
<meta name="csrf-token" content="<?= htmlspecialchars(csrf_token()) ?>">
<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('superuser')): ?>
<a href="<?= BASE_URL ?>/admin/builder.php" class="btn btn-ghost">&#x2712; Builder</a>
<?php endif; ?>
<?php if (has_role('admin')): ?>
<a href="<?= BASE_URL ?>/admin/prayers.php" class="btn btn-ghost">Prayers</a>
<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; Back</a>
<a href="<?= BASE_URL ?>/logout" class="btn btn-ghost">Logout</a>
</div>
</header>
<main>
<h2><?= $page_title ?></h2>
<?php if ($limit_error): ?>
<div class="alert alert-error"><?= htmlspecialchars($limit_error) ?></div>
<?php else: ?>
<div id="form-message" class="alert" style="display:none"></div>
<form id="session-form" novalidate>
<?= csrf_field() ?>
<?php if ($session): ?>
<input type="hidden" name="id" value="<?= (int)$session['id'] ?>">
<?php endif; ?>
<!-- Session Name -->
<div class="form-group">
<label for="name">Session Label <span class="required">*</span></label>
<input type="text" id="name" name="name"
placeholder="e.g. Medy"
value="<?= htmlspecialchars($session['name'] ?? '') ?>"
required>
<p class="help-text" id="name-help">
For novena: enter just the name (e.g. "Medy") — sessions will be created as
"Medy — Day 1" through "Medy — Day 9".
</p>
</div>
<!-- Occasion -->
<div class="form-group">
<label for="occasion">Occasion <span class="required">*</span></label>
<select id="occasion" name="occasion" required>
<option value="">— Select —</option>
<option value="novena_deceased"
<?= ($session['occasion'] ?? '') === 'novena_deceased' ? 'selected' : '' ?>>
Novena for Deceased
</option>
<option value="divine_mercy_novena"
<?= ($session['occasion'] ?? '') === 'divine_mercy_novena' ? 'selected' : '' ?>>
Divine Mercy Novena
</option>
<option value="general_rosary"
<?= ($session['occasion'] ?? '') === 'general_rosary' ? 'selected' : '' ?>>
General Rosary
</option>
<option value="memorial"
<?= ($session['occasion'] ?? '') === 'memorial' ? 'selected' : '' ?>>
Memorial / Month's Mind
</option>
</select>
</div>
<!-- Mystery Set (non-novena) -->
<div class="form-group" id="field-mystery_set_standard">
<label for="mystery_set">Mystery Set <span class="required">*</span></label>
<select id="mystery_set" name="mystery_set" required>
<option value="">— Select —</option>
<option value="sorrowful"
<?= ($session['mystery_set'] ?? '') === 'sorrowful' ? 'selected' : '' ?>>
Sorrowful Mysteries
</option>
<option value="joyful"
<?= ($session['mystery_set'] ?? '') === 'joyful' ? 'selected' : '' ?>>
Joyful Mysteries
</option>
<option value="glorious"
<?= ($session['mystery_set'] ?? '') === 'glorious' ? 'selected' : '' ?>>
Glorious Mysteries
</option>
<option value="luminous"
<?= ($session['mystery_set'] ?? '') === 'luminous' ? 'selected' : '' ?>>
Luminous Mysteries
</option>
</select>
</div>
<!-- Novena Mystery Mode (only shown for novena_deceased) -->
<div class="form-group conditional-field" id="field-novena_mystery_mode" style="display:none">
<label for="novena_mystery_mode">Mysteries for this Novena <span class="required">*</span></label>
<select id="novena_mystery_mode" name="novena_mystery_mode">
<option value="all_sorrowful"
<?= ($session['mystery_set'] ?? '') === 'sorrowful' || ($session['mystery_set'] ?? '') === 'all_sorrowful' ? 'selected' : '' ?>>
All Sorrowful (traditional for deceased)
</option>
<option value="by_day_of_week"
<?= ($session['mystery_set'] ?? '') === 'by_day_of_week' ? 'selected' : '' ?>>
By Day of Week — auto-selected at presentation time
</option>
</select>
<p class="help-text">
Day-of-week schedule: Sun &amp; Wed = Glorious &middot; Mon &amp; Sat = Joyful &middot;
Tue &amp; Fri = Sorrowful &middot; Thu = Luminous
</p>
</div>
<!-- Novena Day -->
<div class="form-group conditional-field" id="field-novena_day" style="display:none">
<?php if ($session): ?>
<label>Novena Day</label>
<div class="form-static">Day <?= (int)($session['novena_day'] ?? 1) ?> of 9</div>
<?php else: ?>
<div class="novena-auto-notice">
&#x2713;&nbsp; Will automatically create <strong>Day 1 through Day 9</strong>.
</div>
<?php endif; ?>
</div>
<!-- Subject Name (conditional) -->
<div class="form-group conditional-field" id="field-subject_name" style="display:none">
<label for="subject_name">Name of Deceased / Honoree</label>
<input type="text" id="subject_name" name="subject_name"
placeholder="e.g. Maria Santos"
value="<?= htmlspecialchars($session['subject_name'] ?? '') ?>">
</div>
<!-- Pronoun (conditional) -->
<div class="form-group conditional-field" id="field-subject_pronoun" style="display:none">
<label for="subject_pronoun">Pronoun</label>
<select id="subject_pronoun" name="subject_pronoun">
<option value="he"
<?= ($session['subject_pronoun'] ?? 'he') === 'he' ? 'selected' : '' ?>>
He / Him / His
</option>
<option value="she"
<?= ($session['subject_pronoun'] ?? '') === 'she' ? 'selected' : '' ?>>
She / Her / Her
</option>
</select>
</div>
<!-- Dates (conditional) -->
<div class="form-group conditional-field" id="field-subject_dates" style="display:none">
<label for="subject_dates">Life Dates</label>
<input type="text" id="subject_dates" name="subject_dates"
placeholder="e.g. Aug 12, 1949 July 25, 2025"
value="<?= htmlspecialchars($session['subject_dates'] ?? '') ?>">
</div>
<!-- Photo -->
<div class="form-group" id="field-photo">
<label for="photo">Photo (optional)</label>
<div class="photo-preview-wrap" style="<?= empty($session['photo_path']) ? 'display:none' : '' ?>">
<img id="photo-preview"
src="<?= !empty($session['photo_path']) ? htmlspecialchars('/' . ltrim($session['photo_path'], '/')) : '' ?>"
alt="Current photo" class="photo-preview"
style="<?= !empty($session['photo_path']) ? htmlspecialchars(photo_crop_style($session)) : '' ?>">
</div>
<input type="file" id="photo" name="photo" accept="image/*">
<p class="help-text">JPEG, PNG, WebP — max 5 MB. Recommended: square or landscape, at least 800 × 600 px. Shown on the home page card and cover slide.</p>
<input type="hidden" id="photo_path" name="photo_path"
value="<?= htmlspecialchars($session['photo_path'] ?? '') ?>">
<input type="hidden" id="photo_focal_x" name="photo_focal_x" value="<?= htmlspecialchars($session['photo_focal_x'] ?? 50) ?>">
<input type="hidden" id="photo_focal_y" name="photo_focal_y" value="<?= htmlspecialchars($session['photo_focal_y'] ?? 50) ?>">
<input type="hidden" id="photo_zoom" name="photo_zoom" value="<?= htmlspecialchars($session['photo_zoom'] ?? 1) ?>">
<button type="button" id="photo-reposition" class="btn btn-secondary" style="margin-top:8px;<?= empty($session['photo_path']) ? 'display:none' : '' ?>">
Reposition
</button>
<div id="upload-status" style="display:none" class="upload-status"></div>
</div>
<!-- Public toggle -->
<div class="form-group">
<label style="display:flex;align-items:center;gap:10px;cursor:pointer">
<input type="checkbox" name="is_public" value="1"
<?= (!$session || $session['is_public']) ? 'checked' : '' ?>
style="width:18px;height:18px">
<span>Make this session public (visible on your profile and the home page)</span>
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary btn-large" id="submit-btn"
data-label-default="<?= $session ? 'Save Changes' : 'Create Session' ?>"
data-label-novena-deceased="<?= $session ? 'Save Changes' : 'Create 9-Day Novena' ?>"
data-label-divine-mercy="<?= $session ? 'Save Changes' : 'Create Divine Mercy Novena' ?>">
<?= $session ? 'Save Changes' : 'Create Session' ?>
</button>
<a href="<?= BASE_URL ?>/admin/" class="btn btn-ghost">Cancel</a>
</div>
</form>
<?php endif; ?>
</main>
</div>
<script src="<?= BASE_URL ?>/assets/js/photo-crop.js"></script>
<script src="<?= BASE_URL ?>/assets/js/setup.js?v=5"></script>
</body>
</html>