Files
Wedding-Website/backend/templates/gallery.html
T

366 lines
15 KiB
HTML
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.
{% extends 'base.html' %}
{% block content %}
<section class="card">
<h1>{{ t('gallery') }}</h1>
{% if images %}
<div class="gallery-filter-controls">
<div class="gallery-uploader-filter" id="gallery-uploader-filter">
<button
class="gallery-uploader-trigger"
id="gallery-uploader-trigger"
type="button"
aria-expanded="false"
aria-controls="gallery-uploader-menu"
aria-label="{{ t('gallery_filter_label') }}"
>
<svg class="gallery-uploader-search" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="m21 20-4.35-4.35a8 8 0 1 0-1.41 1.41L19.59 21 21 20ZM5 11a6 6 0 1 1 12 0 6 6 0 0 1-12 0Z" />
</svg>
<span>{{ t('gallery_filter_trigger') }}</span>
<strong id="gallery-uploader-summary">{{ t('gallery_uploader_all') }}</strong>
<svg class="gallery-uploader-chevron" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="m7 9.5 5 5 5-5 1.4 1.4-6.4 6.4-6.4-6.4L7 9.5Z" />
</svg>
</button>
<div class="gallery-uploader-menu" id="gallery-uploader-menu" aria-label="{{ t('gallery_uploader_filter_label') }}" hidden>
<label class="gallery-uploader-option gallery-uploader-option-all">
<input id="gallery-uploader-all" type="checkbox" checked />
<span>{{ t('gallery_uploader_all') }}</span>
</label>
<div class="gallery-uploader-options">
{% for uploader in uploaders %}
<label class="gallery-uploader-option">
<input type="checkbox" value="{{ uploader }}" data-uploader-option checked />
<span>{{ uploader }}</span>
</label>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="gallery-grid">
{% for image in images %}
<figure class="gallery-item gallery-card" data-uploader="{{ image['uploaded_by_name'] }}">
<div class="gallery-media">
<a
href="{{ url_for('serve_upload', filename=image['filename']) }}"
class="gallery-open"
data-image-index="{{ loop.index0 }}"
>
<img src="{{ url_for('serve_thumbnail', filename=image['filename']) }}" alt="{{ t('gallery_image_alt').format(name=image['uploaded_by_name']) }}" loading="lazy" />
</a>
{% if is_host or guest_id == image['uploaded_by'] %}
<form
class="gallery-delete-form"
method="post"
action="{{ url_for('delete_image', image_id=image['id']) }}"
>
<button class="gallery-delete-btn" type="submit" aria-label="{{ t('delete') }}" title="{{ t('delete') }}">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M9 3h6l1 2h4v2H4V5h4l1-2zm1 6h2v8h-2V9zm4 0h2v8h-2V9zM7 9h2v8H7V9z" />
</svg>
</button>
</form>
{% endif %}
</div>
<figcaption>
<span>{{ t('gallery_uploaded_by_prefix') }}</span>
<strong>{{ image['uploaded_by_name'] }}</strong>
</figcaption>
</figure>
{% endfor %}
</div>
<p class="gallery-filter-empty" id="gallery-filter-empty" hidden>{{ t('gallery_filter_no_results') }}</p>
<div class="lightbox" id="gallery-lightbox" aria-hidden="true">
<button class="lightbox-close" type="button" aria-label="Close">×</button>
<div class="lightbox-counter" id="lightbox-counter">1 / 1</div>
<a class="lightbox-download" id="lightbox-download" href="#" aria-label="{{ t('download') }}" title="{{ t('download') }}">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M12 3a1 1 0 0 1 1 1v8.59l2.3-2.29a1 1 0 1 1 1.4 1.41l-4 4a1 1 0 0 1-1.4 0l-4-4a1 1 0 1 1 1.4-1.41L11 12.59V4a1 1 0 0 1 1-1zm-7 14a1 1 0 0 1 1 1v1h12v-1a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/>
</svg>
</a>
<button class="lightbox-nav lightbox-prev" type="button" aria-label="Previous image">‹</button>
<img class="lightbox-image" id="lightbox-image" alt="" />
<button class="lightbox-nav lightbox-next" type="button" aria-label="Next image">›</button>
<div class="lightbox-save-hint" id="lightbox-save-hint" role="status">{{ t('gallery_save_hint') }}</div>
</div>
<dialog class="gallery-delete-dialog" id="gallery-delete-dialog" aria-labelledby="gallery-delete-title" aria-describedby="gallery-delete-message">
<h2 id="gallery-delete-title">{{ t('gallery_delete_title') }}</h2>
<p id="gallery-delete-message">{{ t('gallery_delete_confirm') }}</p>
<div class="gallery-delete-dialog-actions">
<button class="btn btn-ghost" id="gallery-delete-cancel" type="button">{{ t('gallery_delete_cancel') }}</button>
<button class="btn btn-danger" id="gallery-delete-confirm" type="button">{{ t('delete') }}</button>
</div>
</dialog>
<script>
(() => {
const deleteDialog = document.getElementById("gallery-delete-dialog");
const deleteCancel = document.getElementById("gallery-delete-cancel");
const deleteConfirm = document.getElementById("gallery-delete-confirm");
let pendingDeleteForm = null;
document.querySelectorAll(".gallery-delete-form").forEach((form) => {
form.addEventListener("submit", (event) => {
event.preventDefault();
pendingDeleteForm = form;
deleteDialog.showModal();
});
});
deleteCancel.addEventListener("click", () => deleteDialog.close());
deleteConfirm.addEventListener("click", () => {
if (pendingDeleteForm) {
HTMLFormElement.prototype.submit.call(pendingDeleteForm);
}
});
deleteDialog.addEventListener("close", () => {
pendingDeleteForm = null;
});
deleteDialog.addEventListener("click", (event) => {
if (event.target === deleteDialog) {
deleteDialog.close();
}
});
const lightbox = document.getElementById("gallery-lightbox");
const lightboxImage = document.getElementById("lightbox-image");
const lightboxDownload = document.getElementById("lightbox-download");
const lightboxCounter = document.getElementById("lightbox-counter");
const saveHint = document.getElementById("lightbox-save-hint");
const closeBtn = lightbox.querySelector(".lightbox-close");
const prevBtn = lightbox.querySelector(".lightbox-prev");
const nextBtn = lightbox.querySelector(".lightbox-next");
const openButtons = Array.from(document.querySelectorAll(".gallery-open"));
const galleryItems = Array.from(document.querySelectorAll(".gallery-item"));
const filterEmpty = document.getElementById("gallery-filter-empty");
const uploaderFilter = document.getElementById("gallery-uploader-filter");
const uploaderTrigger = document.getElementById("gallery-uploader-trigger");
const uploaderMenu = document.getElementById("gallery-uploader-menu");
const uploaderSummary = document.getElementById("gallery-uploader-summary");
const uploaderAll = document.getElementById("gallery-uploader-all");
const uploaderOptions = Array.from(document.querySelectorAll("[data-uploader-option]"));
const uploaderLabelAll = {{ t('gallery_uploader_all')|tojson }};
const uploaderLabelNone = {{ t('gallery_uploader_none')|tojson }};
const uploaderLabelSelected = {{ t('gallery_uploader_selected')|tojson }};
const images = [
{% for image in images %}
{
index: {{ loop.index0 }},
uploader: {{ image['uploaded_by_name']|tojson }},
src: {{ url_for('serve_upload', filename=image['filename'])|tojson }},
download: {{ url_for('serve_upload', filename=image['filename'], download=1)|tojson }},
alt: {{ t('gallery_image_alt').format(name=image['uploaded_by_name'])|tojson }}
}{% if not loop.last %},{% endif %}
{% endfor %}
];
let visibleImages = [...images];
let currentIndex = 0;
let touchStartX = 0;
let touchStartY = 0;
let didSwipe = false;
let controlsTimer = null;
let hintTimer = null;
const scheduleHideControls = () => {
clearTimeout(controlsTimer);
controlsTimer = window.setTimeout(() => {
lightbox.classList.add("lightbox-controls-hidden");
}, 3000);
};
const showControlsTemporarily = () => {
lightbox.classList.remove("lightbox-controls-hidden");
scheduleHideControls();
};
const setImage = (index, options = {}) => {
const { revealControls = true } = options;
if (!visibleImages.length) {
return;
}
currentIndex = (index + visibleImages.length) % visibleImages.length;
const item = visibleImages[currentIndex];
lightboxImage.classList.remove("is-fading");
void lightboxImage.offsetWidth;
lightboxImage.classList.add("is-fading");
lightboxImage.src = item.src;
lightboxImage.alt = item.alt;
lightboxDownload.href = item.download;
lightboxCounter.textContent = `${currentIndex + 1} / ${visibleImages.length}`;
if (revealControls) {
showControlsTemporarily();
}
};
const openLightbox = (index) => {
setImage(index);
lightbox.classList.add("is-open");
lightbox.setAttribute("aria-hidden", "false");
document.body.classList.add("no-scroll");
showControlsTemporarily();
saveHint.classList.add("is-visible");
clearTimeout(hintTimer);
hintTimer = window.setTimeout(() => saveHint.classList.remove("is-visible"), 5000);
};
const closeLightbox = () => {
lightbox.classList.remove("is-open");
lightbox.setAttribute("aria-hidden", "true");
document.body.classList.remove("no-scroll");
lightbox.classList.remove("lightbox-controls-hidden");
clearTimeout(controlsTimer);
clearTimeout(hintTimer);
saveHint.classList.remove("is-visible");
};
openButtons.forEach((link) => {
link.addEventListener("click", (event) => {
event.preventDefault();
const imageIndex = Number(link.dataset.imageIndex || 0);
const visibleIndex = visibleImages.findIndex((item) => item.index === imageIndex);
if (visibleIndex >= 0) {
openLightbox(visibleIndex);
}
});
});
const applyFilters = () => {
const selectedUploaders = new Set(
uploaderOptions.filter((option) => option.checked).map((option) => option.value)
);
galleryItems.forEach((item) => {
const matchesUploader = selectedUploaders.has(item.dataset.uploader || "");
item.classList.toggle("is-filtered-out", !matchesUploader);
});
visibleImages = images.filter((item) => selectedUploaders.has(item.uploader));
filterEmpty.hidden = visibleImages.length > 0;
};
const syncUploaderSummary = () => {
const selectedCount = uploaderOptions.filter((option) => option.checked).length;
uploaderAll.checked = selectedCount === uploaderOptions.length;
uploaderAll.indeterminate = selectedCount > 0 && selectedCount < uploaderOptions.length;
if (selectedCount === uploaderOptions.length) {
uploaderSummary.textContent = uploaderLabelAll;
} else if (selectedCount === 0) {
uploaderSummary.textContent = uploaderLabelNone;
} else {
uploaderSummary.textContent = uploaderLabelSelected.replace("{count}", String(selectedCount));
}
};
const closeUploaderMenu = () => {
uploaderMenu.hidden = true;
uploaderTrigger.setAttribute("aria-expanded", "false");
};
uploaderTrigger.addEventListener("click", () => {
const shouldOpen = uploaderMenu.hidden;
uploaderMenu.hidden = !shouldOpen;
uploaderTrigger.setAttribute("aria-expanded", String(shouldOpen));
});
uploaderAll.addEventListener("change", () => {
uploaderOptions.forEach((option) => {
option.checked = uploaderAll.checked;
});
syncUploaderSummary();
applyFilters();
});
uploaderOptions.forEach((option) => {
option.addEventListener("change", () => {
syncUploaderSummary();
applyFilters();
});
});
document.addEventListener("click", (event) => {
if (!uploaderFilter.contains(event.target)) {
closeUploaderMenu();
}
});
prevBtn.addEventListener("click", () => setImage(currentIndex - 1, { revealControls: true }));
nextBtn.addEventListener("click", () => setImage(currentIndex + 1, { revealControls: true }));
closeBtn.addEventListener("click", closeLightbox);
lightbox.addEventListener("mousemove", () => {
if (lightbox.classList.contains("is-open")) {
showControlsTemporarily();
}
});
lightbox.addEventListener("click", (event) => {
if (event.target === lightbox) {
closeLightbox();
} else if (
lightbox.classList.contains("is-open") &&
event.target.closest(".lightbox-close, .lightbox-download, .lightbox-nav")
) {
showControlsTemporarily();
}
});
lightboxImage.addEventListener("touchstart", (event) => {
const point = event.changedTouches[0];
touchStartX = point.clientX;
touchStartY = point.clientY;
didSwipe = false;
}, { passive: true });
lightboxImage.addEventListener("touchend", (event) => {
const point = event.changedTouches[0];
const deltaX = point.clientX - touchStartX;
const deltaY = point.clientY - touchStartY;
const absX = Math.abs(deltaX);
const absY = Math.abs(deltaY);
if (absX < 40 || absX <= absY) {
return;
}
didSwipe = true;
if (deltaX < 0) {
setImage(currentIndex + 1, { revealControls: false });
} else {
setImage(currentIndex - 1, { revealControls: false });
}
}, { passive: true });
lightboxImage.addEventListener("click", (event) => {
if (didSwipe) {
event.preventDefault();
event.stopPropagation();
didSwipe = false;
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !uploaderMenu.hidden) {
closeUploaderMenu();
uploaderTrigger.focus();
return;
}
if (!lightbox.classList.contains("is-open")) {
return;
}
showControlsTemporarily();
if (event.key === "Escape") {
closeLightbox();
} else if (event.key === "ArrowLeft") {
setImage(currentIndex - 1, { revealControls: true });
} else if (event.key === "ArrowRight") {
setImage(currentIndex + 1, { revealControls: true });
}
});
})();
</script>
{% else %}
<p>{{ t('gallery_empty') }}</p>
{% endif %}
</section>
{% endblock %}