3a1433b71e
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>
226 lines
9.5 KiB
JavaScript
226 lines
9.5 KiB
JavaScript
/**
|
|
* assets/js/setup.js
|
|
* Setup form interactivity: conditional field visibility + form submission.
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
const form = document.getElementById('session-form');
|
|
const occasionSel = document.getElementById('occasion');
|
|
const photoInput = document.getElementById('photo');
|
|
const uploadStatus = document.getElementById('upload-status');
|
|
const submitBtn = document.getElementById('submit-btn');
|
|
const msgBox = document.getElementById('form-message');
|
|
const nameHelp = document.getElementById('name-help');
|
|
|
|
// Are we editing an existing session?
|
|
const isEditMode = !!form.querySelector('[name="id"]');
|
|
|
|
// Fields to show/hide by occasion (field IDs without the "field-" prefix)
|
|
const OCCASION_FIELDS = {
|
|
novena_deceased: ['novena_day', 'novena_mystery_mode', 'subject_name', 'subject_pronoun', 'subject_dates'],
|
|
divine_mercy_novena:['novena_day'],
|
|
memorial: ['subject_name', 'subject_pronoun', 'subject_dates'],
|
|
general_rosary: [],
|
|
};
|
|
|
|
const mysteryStandardWrap = document.getElementById('field-mystery_set_standard');
|
|
|
|
function toggleFields() {
|
|
const occasion = occasionSel.value;
|
|
const show = OCCASION_FIELDS[occasion] || [];
|
|
const isNovenaDeceased = (occasion === 'novena_deceased');
|
|
const isDivineMercy = (occasion === 'divine_mercy_novena');
|
|
const isAnyNovena = isNovenaDeceased || isDivineMercy;
|
|
|
|
// Standard mystery dropdown: hide for both novena types
|
|
if (mysteryStandardWrap) {
|
|
mysteryStandardWrap.style.display = isAnyNovena ? 'none' : '';
|
|
const sel = document.getElementById('mystery_set');
|
|
if (sel) {
|
|
if (isAnyNovena) {
|
|
sel.removeAttribute('required');
|
|
if (!sel.value) sel.value = 'sorrowful';
|
|
} else {
|
|
sel.setAttribute('required', '');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show/hide conditional fields
|
|
document.querySelectorAll('.conditional-field').forEach(el => {
|
|
const fieldName = el.id.replace('field-', '');
|
|
// novena_mystery_mode only shows for novena_deceased, not divine_mercy_novena
|
|
if (fieldName === 'novena_mystery_mode' && !isNovenaDeceased) {
|
|
el.style.display = 'none';
|
|
el.querySelectorAll('input, select').forEach(input => input.removeAttribute('required'));
|
|
return;
|
|
}
|
|
const visible = show.includes(fieldName);
|
|
el.style.display = visible ? '' : 'none';
|
|
el.querySelectorAll('input, select').forEach(input => {
|
|
if (!visible) input.removeAttribute('required');
|
|
});
|
|
});
|
|
|
|
// Update submit button label
|
|
if (submitBtn) {
|
|
if (isNovenaDeceased) {
|
|
submitBtn.textContent = submitBtn.dataset.labelNovenaDeceased;
|
|
} else if (isDivineMercy) {
|
|
submitBtn.textContent = submitBtn.dataset.labelDivineMercy;
|
|
} else {
|
|
submitBtn.textContent = submitBtn.dataset.labelDefault;
|
|
}
|
|
}
|
|
|
|
// Update session name help text
|
|
if (nameHelp) {
|
|
nameHelp.style.display = isAnyNovena && !isEditMode ? '' : 'none';
|
|
}
|
|
}
|
|
|
|
occasionSel.addEventListener('change', toggleFields);
|
|
toggleFields(); // run on page load (handles edit mode pre-selection)
|
|
|
|
// ------------------------------------------------------------------
|
|
// Photo upload on file selection
|
|
// ------------------------------------------------------------------
|
|
var photoUploading = false;
|
|
|
|
function applyPhotoCropStyle() {
|
|
var previewImg = document.getElementById('photo-preview');
|
|
var fx = document.getElementById('photo_focal_x').value;
|
|
var fy = document.getElementById('photo_focal_y').value;
|
|
var zoom = document.getElementById('photo_zoom').value;
|
|
previewImg.style.cssText =
|
|
'object-position:' + fx + '% ' + fy + '%;transform:scale(' + zoom + ');transform-origin:' + fx + '% ' + fy + '%;';
|
|
}
|
|
|
|
var repositionBtn = document.getElementById('photo-reposition');
|
|
if (repositionBtn) {
|
|
repositionBtn.addEventListener('click', function () {
|
|
var previewImg = document.getElementById('photo-preview');
|
|
PhotoCrop.open({
|
|
imageUrl: previewImg.src,
|
|
focalX: parseFloat(document.getElementById('photo_focal_x').value),
|
|
focalY: parseFloat(document.getElementById('photo_focal_y').value),
|
|
zoom: parseFloat(document.getElementById('photo_zoom').value),
|
|
onApply: function (fx, fy, zoom) {
|
|
document.getElementById('photo_focal_x').value = fx;
|
|
document.getElementById('photo_focal_y').value = fy;
|
|
document.getElementById('photo_zoom').value = zoom;
|
|
applyPhotoCropStyle();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
if (photoInput) {
|
|
photoInput.addEventListener('change', async function () {
|
|
if (!this.files || !this.files[0]) return;
|
|
|
|
const file = this.files[0];
|
|
uploadStatus.style.display = '';
|
|
uploadStatus.className = 'upload-status uploading';
|
|
uploadStatus.textContent = 'Uploading\u2026';
|
|
|
|
// Disable submit while upload is in progress
|
|
photoUploading = true;
|
|
if (submitBtn) submitBtn.disabled = true;
|
|
|
|
const fd = new FormData();
|
|
fd.append('photo', file);
|
|
fd.append('csrf_token', document.querySelector('meta[name="csrf-token"]').content);
|
|
|
|
try {
|
|
const res = await fetch(BASE_URL + '/api/upload_photo.php', { method: 'POST', body: fd });
|
|
const data = await res.json();
|
|
|
|
if (data.error) {
|
|
uploadStatus.className = 'upload-status error';
|
|
uploadStatus.textContent = 'Upload failed: ' + data.error;
|
|
} else {
|
|
document.getElementById('photo_path').value = data.path;
|
|
// A newly uploaded photo starts centered/unzoomed.
|
|
document.getElementById('photo_focal_x').value = 50;
|
|
document.getElementById('photo_focal_y').value = 50;
|
|
document.getElementById('photo_zoom').value = 1;
|
|
var previewImg = document.getElementById('photo-preview');
|
|
var previewWrap = document.querySelector('#field-photo .photo-preview-wrap');
|
|
previewImg.src = '/' + data.path.replace(/^\//, '');
|
|
applyPhotoCropStyle();
|
|
previewWrap.style.display = '';
|
|
document.getElementById('photo-reposition').style.display = '';
|
|
// Clear the file input so the file is not re-sent with the main form
|
|
photoInput.value = '';
|
|
uploadStatus.className = 'upload-status success';
|
|
uploadStatus.textContent = 'Photo uploaded successfully.';
|
|
}
|
|
} catch (err) {
|
|
uploadStatus.className = 'upload-status error';
|
|
uploadStatus.textContent = 'Network error during upload.';
|
|
} finally {
|
|
photoUploading = false;
|
|
if (submitBtn) submitBtn.disabled = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Form submission via fetch
|
|
// ------------------------------------------------------------------
|
|
form.addEventListener('submit', async function (e) {
|
|
e.preventDefault();
|
|
|
|
if (!form.checkValidity()) {
|
|
form.reportValidity();
|
|
return;
|
|
}
|
|
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = 'Saving\u2026';
|
|
showMessage('', '');
|
|
|
|
const fd = new FormData(form);
|
|
|
|
try {
|
|
const res = await fetch(BASE_URL + '/api/save_session.php', { method: 'POST', body: fd });
|
|
const data = await res.json();
|
|
|
|
if (data.error) {
|
|
showMessage('error', data.error);
|
|
submitBtn.disabled = false;
|
|
const occ = occasionSel.value;
|
|
submitBtn.textContent = isEditMode
|
|
? submitBtn.dataset.labelDefault
|
|
: (occ === 'novena_deceased' ? submitBtn.dataset.labelNovenaDeceased
|
|
: occ === 'divine_mercy_novena' ? submitBtn.dataset.labelDivineMercy
|
|
: submitBtn.dataset.labelDefault);
|
|
} else if (data.novena) {
|
|
// All 9 days created — go to admin dashboard
|
|
window.location.href = BASE_URL + '/admin/?novena_created=' + data.ids.length;
|
|
} else {
|
|
// Single session — go to presentation
|
|
window.location.href = BASE_URL + '/present.php?id=' + data.id;
|
|
}
|
|
} catch (err) {
|
|
showMessage('error', 'Network error. Please try again.');
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = submitBtn.dataset.labelDefault;
|
|
}
|
|
});
|
|
|
|
function showMessage(type, text) {
|
|
if (!text) {
|
|
msgBox.style.display = 'none';
|
|
return;
|
|
}
|
|
msgBox.style.display = '';
|
|
msgBox.className = 'alert alert-' + type;
|
|
msgBox.textContent = text;
|
|
}
|
|
|
|
})();
|