feat: implement photo carousel for remark images with navigation, captions, and delete support

This commit is contained in:
kdeterme 2026-08-10 20:22:41 +02:00
parent 4e4d84b3a4
commit f34e26f8f5
4 changed files with 371 additions and 54 deletions

View file

@ -19,6 +19,16 @@ body {
object-fit: cover; /* Garde le ratio et remplit */ object-fit: cover; /* Garde le ratio et remplit */
border-radius: 8px; /* Coins arrondis */ border-radius: 8px; /* Coins arrondis */
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1); /* Ombre légère */ box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1); /* Ombre légère */
transition: transform 0.15s ease-in-out, opacity 0.15s ease-in-out;
}
.remark-photo-thumb {
cursor: pointer;
}
.remark-photo-thumb:hover {
transform: scale(1.05);
opacity: 0.9;
} }
.remark-images a { .remark-images a {
@ -35,6 +45,16 @@ body {
z-index: 5; z-index: 5;
} }
#remarkPhotoCarouselModal .modal-body {
background-color: #000;
}
#remarkPhotoCarousel .carousel-control-prev-icon,
#remarkPhotoCarousel .carousel-control-next-icon {
width: 2.5rem;
height: 2.5rem;
}
#remark-form-container { #remark-form-container {
position: fixed; position: fixed;
top: 0; left: 0; width: 100%; height: 100%; top: 0; left: 0; width: 100%; height: 100%;

View file

@ -1795,44 +1795,198 @@ function bindAutocompleteForRow(card) {
} }
})(); })();
// Fonction pour afficher une image dans une modal // Global variables for remark photo carousel
function showImageModal(imageUrl) { let currentRemarkCarouselPhotos = [];
// Créer une modal pour afficher l'image en plein écran let currentRemarkCarouselIndex = 0;
let modal = document.getElementById('image-preview-modal');
if (!modal) { function openRemarkPhotoCarousel(thumbElement) {
// Créer la modal si elle n'existe pas if (!thumbElement) return;
modal = document.createElement('div'); const remarkId = thumbElement.dataset.remarkId;
modal.id = 'image-preview-modal';
modal.className = 'modal fade';
modal.tabIndex = -1;
modal.setAttribute('aria-hidden', 'true');
modal.innerHTML = ` // Find all photo thumbnails belonging to this remark
<div class="modal-dialog modal-fullscreen"> let thumbs = [];
<div class="modal-content bg-dark"> const remarkCard = document.getElementById(`remark-${remarkId}`);
<div class="modal-header border-0"> if (remarkCard) {
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Fermer"></button> thumbs = Array.from(remarkCard.querySelectorAll('.remark-photo-thumb'));
</div> } else if (remarkId) {
<div class="modal-body d-flex align-items-center justify-content-center"> thumbs = Array.from(document.querySelectorAll(`.remark-photo-thumb[data-remark-id="${remarkId}"]`));
<img id="modal-preview-image" src="" class="img-fluid" style="max-width: 100%; max-height: 90vh; object-fit: contain;">
</div>
</div>
</div>
`;
document.body.appendChild(modal);
} }
// Mettre à jour l'image if (thumbs.length === 0) {
const img = modal.querySelector('#modal-preview-image'); thumbs = [thumbElement];
if (img) {
img.src = imageUrl;
} }
// Afficher la modal currentRemarkCarouselPhotos = thumbs.map(t => ({
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { fullUrl: t.dataset.fullUrl || t.src,
const modalInstance = new bootstrap.Modal(modal); thumbUrl: t.dataset.thumbUrl || t.src,
modalInstance.show(); imageId: t.dataset.imageId || null,
caption: t.dataset.caption || '',
remarkId: t.dataset.remarkId || remarkId,
element: t
}));
let startIndex = thumbs.indexOf(thumbElement);
if (startIndex < 0) startIndex = 0;
showRemarkCarouselModal(startIndex);
}
function openRemarkPhotoCarouselByRemarkId(remarkId) {
const remarkCard = document.getElementById(`remark-${remarkId}`);
let firstThumb = null;
if (remarkCard) {
firstThumb = remarkCard.querySelector('.remark-photo-thumb');
} else {
firstThumb = document.querySelector(`.remark-photo-thumb[data-remark-id="${remarkId}"]`);
}
if (firstThumb) {
openRemarkPhotoCarousel(firstThumb);
} }
} }
function showRemarkCarouselModal(startIndex) {
const modalEl = document.getElementById('remarkPhotoCarouselModal');
if (!modalEl) {
if (currentRemarkCarouselPhotos[startIndex]) {
window.open(currentRemarkCarouselPhotos[startIndex].fullUrl, '_blank');
}
return;
}
const carouselInner = document.getElementById('remark-carousel-inner');
const carouselIndicators = document.getElementById('remark-carousel-indicators');
const total = currentRemarkCarouselPhotos.length;
currentRemarkCarouselIndex = startIndex;
if (!carouselInner) return;
carouselInner.innerHTML = '';
if (carouselIndicators) carouselIndicators.innerHTML = '';
currentRemarkCarouselPhotos.forEach((photo, idx) => {
const item = document.createElement('div');
item.className = `carousel-item ${idx === startIndex ? 'active' : ''} h-100`;
item.innerHTML = `
<div class="d-flex align-items-center justify-content-center h-100 p-2">
<img src="${photo.fullUrl}" class="d-block img-fluid mh-100 mw-100" style="object-fit: contain; max-height: 75vh;" alt="${photo.caption}">
</div>
`;
carouselInner.appendChild(item);
if (carouselIndicators && total > 1) {
const ind = document.createElement('button');
ind.type = 'button';
ind.setAttribute('data-bs-target', '#remarkPhotoCarousel');
ind.setAttribute('data-bs-slide-to', idx.toString());
if (idx === startIndex) {
ind.className = 'active';
ind.setAttribute('aria-current', 'true');
}
ind.setAttribute('aria-label', `Photo ${idx + 1}`);
carouselIndicators.appendChild(ind);
}
});
const prevBtn = modalEl.querySelector('.carousel-control-prev');
const nextBtn = modalEl.querySelector('.carousel-control-next');
if (prevBtn) prevBtn.style.display = total > 1 ? 'flex' : 'none';
if (nextBtn) nextBtn.style.display = total > 1 ? 'flex' : 'none';
if (carouselIndicators) carouselIndicators.style.display = total > 1 ? 'flex' : 'none';
updateRemarkCarouselActiveSlide(startIndex);
const carouselEl = document.getElementById('remarkPhotoCarousel');
if (carouselEl && typeof bootstrap !== 'undefined' && bootstrap.Carousel) {
let bsCarousel = bootstrap.Carousel.getInstance(carouselEl);
if (bsCarousel) {
bsCarousel.dispose();
}
bsCarousel = new bootstrap.Carousel(carouselEl, { ride: false, interval: false });
bsCarousel.to(startIndex);
carouselEl.addEventListener('slid.bs.carousel', function (e) {
updateRemarkCarouselActiveSlide(e.to);
}, { once: false });
}
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
const bsModal = bootstrap.Modal.getOrCreateInstance(modalEl);
bsModal.show();
}
}
function updateRemarkCarouselActiveSlide(idx) {
currentRemarkCarouselIndex = idx;
const photo = currentRemarkCarouselPhotos[idx];
const total = currentRemarkCarouselPhotos.length;
const counterBadge = document.getElementById('remark-carousel-counter');
const captionEl = document.getElementById('remark-carousel-caption');
const hdBtn = document.getElementById('remark-carousel-hd-btn');
const deleteBtn = document.getElementById('remark-carousel-delete-btn');
if (counterBadge) {
counterBadge.textContent = `${idx + 1} / ${total}`;
}
if (captionEl && photo) {
captionEl.textContent = photo.caption || '';
}
if (hdBtn && photo) {
hdBtn.href = photo.fullUrl;
}
if (deleteBtn) {
if (photo && photo.imageId) {
deleteBtn.style.display = 'inline-flex';
deleteBtn.dataset.imageId = photo.imageId;
deleteBtn.dataset.remarkId = photo.remarkId;
deleteBtn.onclick = function () {
if (photo.element) {
const trashBtn = photo.element.parentElement ? photo.element.parentElement.querySelector('.remark-image-delete') : null;
if (trashBtn) {
trashBtn.click();
} else {
deleteRemarkImage(photo.imageId, photo.element);
}
} else {
deleteRemarkImage(photo.imageId, deleteBtn);
}
};
} else {
deleteBtn.style.display = 'none';
}
}
}
// Support navigation au clavier (flèche gauche / droite) pour le carrousel
document.addEventListener('DOMContentLoaded', function() {
const modalEl = document.getElementById('remarkPhotoCarouselModal');
if (modalEl) {
modalEl.addEventListener('keydown', function(e) {
const carouselEl = document.getElementById('remarkPhotoCarousel');
if (!carouselEl || typeof bootstrap === 'undefined' || !bootstrap.Carousel) return;
const bsCarousel = bootstrap.Carousel.getInstance(carouselEl);
if (!bsCarousel) return;
if (e.key === 'ArrowLeft') {
e.preventDefault();
bsCarousel.prev();
} else if (e.key === 'ArrowRight') {
e.preventDefault();
bsCarousel.next();
}
});
}
});
// Backward compatibility for single image preview
function showImageModal(imageUrl) {
currentRemarkCarouselPhotos = [{
fullUrl: imageUrl,
thumbUrl: imageUrl,
imageId: null,
caption: '',
remarkId: null,
element: null
}];
showRemarkCarouselModal(0);
}

View file

@ -365,9 +365,16 @@
<!-- ✅ Première Ligne : Numéro, Statut --> <!-- ✅ Première Ligne : Numéro, Statut -->
<div class="d-flex justify-content-between align-items-center pe-5"> <div class="d-flex justify-content-between align-items-center pe-5">
<!-- Numéro --> <!-- Numéro -->
<div class="d-flex align-items-center gap-2">
<div class="remark-number"> <div class="remark-number">
{{ remark.remark_number }} {{ remark.remark_number }}
</div> </div>
{% if remark.images.exists %}
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-2 py-0" onclick="openRemarkPhotoCarouselByRemarkId({{ remark.id }})" title="{% translate 'Visualiser les photos dans le carrousel' %}">
<i class="bi bi-images me-1"></i>{{ remark.images.count }}
</button>
{% endif %}
</div>
<!-- ✅ Statut --> <!-- ✅ Statut -->
<span id="status-{{ remark.id }}" class="badge {{ remark.status|status_badge }}"> <span id="status-{{ remark.id }}" class="badge {{ remark.status|status_badge }}">
@ -408,9 +415,16 @@
<div class="remark-image-wrapper"> <div class="remark-image-wrapper">
{% private_file_url image 'file' as image_file_url %} {% private_file_url image 'file' as image_file_url %}
{% private_file_url image 'thumbnail' as image_thumbnail_url %} {% private_file_url image 'thumbnail' as image_thumbnail_url %}
<a href="{{ image_file_url }}" target="_blank"> <img src="{{ image_thumbnail_url }}"
<img src="{{ image_thumbnail_url }}" alt="{% translate 'Image de la remarque' %}"> alt="{% translate 'Image de la remarque' %}"
</a> class="remark-photo-thumb shadow-sm"
style="cursor: pointer;"
data-remark-id="{{ remark.id }}"
data-full-url="{{ image_file_url }}"
data-thumb-url="{{ image_thumbnail_url }}"
data-image-id="{{ image.id }}"
data-caption="{% translate 'Remarque' %} #{{ remark.remark_number }}{% if entry.created_at %} — {{ entry.created_at|timezone:'Europe/Paris'|date:'d/m/Y H:i' }}{% endif %}"
onclick="openRemarkPhotoCarousel(this)">
{% if can_change_control %} {% if can_change_control %}
<button type="button" <button type="button"
class="btn btn-sm btn-danger remark-image-delete" class="btn btn-sm btn-danger remark-image-delete"
@ -445,9 +459,16 @@
<div class="remark-image-wrapper"> <div class="remark-image-wrapper">
{% private_file_url image 'file' as image_file_url %} {% private_file_url image 'file' as image_file_url %}
{% private_file_url image 'thumbnail' as image_thumbnail_url %} {% private_file_url image 'thumbnail' as image_thumbnail_url %}
<a href="{{ image_file_url }}" target="_blank"> <img src="{{ image_thumbnail_url }}"
<img src="{{ image_thumbnail_url }}" alt="{% translate 'Image de la remarque' %}"> alt="{% translate 'Image de la remarque' %}"
</a> class="remark-photo-thumb shadow-sm"
style="cursor: pointer;"
data-remark-id="{{ remark.id }}"
data-full-url="{{ image_file_url }}"
data-thumb-url="{{ image_thumbnail_url }}"
data-image-id="{{ image.id }}"
data-caption="{% translate 'Remarque' %} #{{ remark.remark_number }}"
onclick="openRemarkPhotoCarousel(this)">
{% if can_change_control %} {% if can_change_control %}
<button type="button" <button type="button"
class="btn btn-sm btn-danger remark-image-delete" class="btn btn-sm btn-danger remark-image-delete"
@ -646,9 +667,16 @@
<div class="remark-image-wrapper"> <div class="remark-image-wrapper">
{% private_file_url image 'file' as image_file_url %} {% private_file_url image 'file' as image_file_url %}
{% private_file_url image 'thumbnail' as image_thumbnail_url %} {% private_file_url image 'thumbnail' as image_thumbnail_url %}
<a href="{{ image_file_url }}" target="_blank"> <img src="{{ image_thumbnail_url }}"
<img src="{{ image_thumbnail_url }}" alt="{% translate 'Image de la remarque' %}"> alt="{% translate 'Image de la remarque' %}"
</a> class="remark-photo-thumb shadow-sm"
style="cursor: pointer;"
data-remark-id="{{ remark.id }}"
data-full-url="{{ image_file_url }}"
data-thumb-url="{{ image_thumbnail_url }}"
data-image-id="{{ image.id }}"
data-caption="{% translate 'Remarque' %} #{{ remark.remark_number }} ({% translate 'Archivée' %})"
onclick="openRemarkPhotoCarousel(this)">
{% if can_change_control %} {% if can_change_control %}
<button type="button" <button type="button"
class="btn btn-sm btn-danger remark-image-delete" class="btn btn-sm btn-danger remark-image-delete"
@ -748,6 +776,50 @@
</div> </div>
<!-- Modal Carrousel pour les photos d'une remarque -->
<div class="modal fade" id="remarkPhotoCarouselModal" tabindex="-1" aria-labelledby="remarkPhotoCarouselModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content bg-dark text-white shadow-lg border-0">
<div class="modal-header border-bottom border-secondary py-2 px-3">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-images text-primary fs-5"></i>
<h5 class="modal-title fs-6 mb-0 text-white" id="remarkPhotoCarouselModalLabel">{% translate "Photos de la remarque" %}</h5>
<span class="badge bg-primary rounded-pill px-2 py-1 fs-7" id="remark-carousel-counter">1 / 1</span>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
</div>
<div class="modal-body p-0 position-relative bg-black d-flex align-items-center justify-content-center overflow-hidden" style="min-height: 350px; max-height: 80vh;">
<div id="remarkPhotoCarousel" class="carousel slide w-100 h-100 d-flex flex-column justify-content-center" data-bs-ride="false">
<div class="carousel-indicators mb-2" id="remark-carousel-indicators"></div>
<div class="carousel-inner h-100" id="remark-carousel-inner"></div>
<button class="carousel-control-prev" type="button" data-bs-target="#remarkPhotoCarousel" data-bs-slide="prev" style="z-index: 10;">
<span class="carousel-control-prev-icon p-3 rounded-circle bg-dark bg-opacity-75" aria-hidden="true"></span>
<span class="visually-hidden">{% translate "Précédent" %}</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#remarkPhotoCarousel" data-bs-slide="next" style="z-index: 10;">
<span class="carousel-control-next-icon p-3 rounded-circle bg-dark bg-opacity-75" aria-hidden="true"></span>
<span class="visually-hidden">{% translate "Suivant" %}</span>
</button>
</div>
</div>
<div class="modal-footer border-top border-secondary py-2 px-3 d-flex justify-content-between align-items-center bg-dark">
<div class="small text-muted text-truncate me-2" id="remark-carousel-caption"></div>
<div class="d-flex align-items-center gap-2">
<a id="remark-carousel-hd-btn" href="#" target="_blank" class="btn btn-sm btn-outline-light d-inline-flex align-items-center gap-1" title="{% translate 'Ouvrir en plein écran HD' %}">
<i class="bi bi-box-arrow-up-right"></i> <span class="d-none d-sm-inline">{% translate "Plein écran" %}</span>
</a>
{% if can_change_control %}
<button type="button" id="remark-carousel-delete-btn" class="btn btn-sm btn-outline-danger d-inline-flex align-items-center gap-1" title="{% translate 'Supprimer cette photo' %}">
<i class="bi bi-trash"></i> <span class="d-none d-sm-inline">{% translate "Supprimer" %}</span>
</button>
{% endif %}
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
</div>
</div>
</div>
</div>
</div>
<script> <script>
// Mettre à jour le badge du nombre de remarques en attente // Mettre à jour le badge du nombre de remarques en attente
async function updatePendingBadge() { async function updatePendingBadge() {

View file

@ -411,9 +411,16 @@
<!-- ✅ Première Ligne : Numéro, Statut --> <!-- ✅ Première Ligne : Numéro, Statut -->
<div class="d-flex justify-content-between align-items-center pe-5"> <div class="d-flex justify-content-between align-items-center pe-5">
<div class="d-flex align-items-center gap-2">
<div class="remark-number"> <div class="remark-number">
{{ remark.remark_number }} {{ remark.remark_number }}
</div> </div>
{% if remark.images.exists %}
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-2 py-0" onclick="openRemarkPhotoCarouselByRemarkId({{ remark.id }})" title="{% translate 'Visualiser les photos dans le carrousel' %}">
<i class="bi bi-images me-1"></i>{{ remark.images.count }}
</button>
{% endif %}
</div>
<span id="status-{{ remark.id }}" class="badge {{ remark.status|status_badge }}"> <span id="status-{{ remark.id }}" class="badge {{ remark.status|status_badge }}">
<i class="bi {{ remark.status|status_icon }}"></i> <i class="bi {{ remark.status|status_icon }}"></i>
@ -454,9 +461,16 @@
<div class="remark-image-wrapper"> <div class="remark-image-wrapper">
{% private_file_url image 'file' as image_file_url %} {% private_file_url image 'file' as image_file_url %}
{% private_file_url image 'thumbnail' as image_thumb_url %} {% private_file_url image 'thumbnail' as image_thumb_url %}
<a href="{{ image_file_url }}" target="_blank"> <img src="{{ image_thumb_url }}"
<img src="{{ image_thumb_url }}" alt="{% translate 'Image de la remarque' %}"> alt="{% translate 'Image de la remarque' %}"
</a> class="remark-photo-thumb shadow-sm"
style="cursor: pointer;"
data-remark-id="{{ remark.id }}"
data-full-url="{{ image_file_url }}"
data-thumb-url="{{ image_thumb_url }}"
data-image-id="{{ image.id }}"
data-caption="{% translate 'Remarque' %} #{{ remark.remark_number }}{% if entry.created_at %} — {{ entry.created_at|timezone:'Europe/Paris'|date:'d/m/Y H:i' }}{% endif %}"
onclick="openRemarkPhotoCarousel(this)">
{% if can_change_control %} {% if can_change_control %}
<button type="button" <button type="button"
class="btn btn-sm btn-danger remark-image-delete" class="btn btn-sm btn-danger remark-image-delete"
@ -491,9 +505,16 @@
<div class="remark-image-wrapper"> <div class="remark-image-wrapper">
{% private_file_url image 'file' as image_file_url %} {% private_file_url image 'file' as image_file_url %}
{% private_file_url image 'thumbnail' as image_thumb_url %} {% private_file_url image 'thumbnail' as image_thumb_url %}
<a href="{{ image_file_url }}" target="_blank"> <img src="{{ image_thumb_url }}"
<img src="{{ image_thumb_url }}" alt="{% translate 'Image de la remarque' %}"> alt="{% translate 'Image de la remarque' %}"
</a> class="remark-photo-thumb shadow-sm"
style="cursor: pointer;"
data-remark-id="{{ remark.id }}"
data-full-url="{{ image_file_url }}"
data-thumb-url="{{ image_thumb_url }}"
data-image-id="{{ image.id }}"
data-caption="{% translate 'Remarque' %} #{{ remark.remark_number }}"
onclick="openRemarkPhotoCarousel(this)">
{% if can_change_control %} {% if can_change_control %}
<button type="button" <button type="button"
class="btn btn-sm btn-danger remark-image-delete" class="btn btn-sm btn-danger remark-image-delete"
@ -686,9 +707,16 @@
{% private_file_url image 'file' as image_file_url %} {% private_file_url image 'file' as image_file_url %}
{% private_file_url image 'thumbnail' as image_thumb_url %} {% private_file_url image 'thumbnail' as image_thumb_url %}
<div class="remark-image-wrapper"> <div class="remark-image-wrapper">
<a href="{{ image_file_url }}" target="_blank"> <img src="{{ image_thumb_url }}"
<img src="{{ image_thumb_url }}" alt="{% translate 'Image de la remarque' %}"> alt="{% translate 'Image de la remarque' %}"
</a> class="remark-photo-thumb shadow-sm"
style="cursor: pointer;"
data-remark-id="{{ remark.id }}"
data-full-url="{{ image_file_url }}"
data-thumb-url="{{ image_thumb_url }}"
data-image-id="{{ image.id }}"
data-caption="{% translate 'Remarque' %} #{{ remark.remark_number }} ({% translate 'Archivée' %})"
onclick="openRemarkPhotoCarousel(this)">
{% if can_change_control %} {% if can_change_control %}
<button type="button" <button type="button"
class="btn btn-sm btn-danger remark-image-delete" class="btn btn-sm btn-danger remark-image-delete"
@ -1123,6 +1151,49 @@ document.getElementById('force-sync-btn')?.addEventListener('click', async () =>
await displayPendingRemarks(); await displayPendingRemarks();
} }
}); });
<!-- Modal Carrousel pour les photos d'une remarque -->
<div class="modal fade" id="remarkPhotoCarouselModal" tabindex="-1" aria-labelledby="remarkPhotoCarouselModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content bg-dark text-white shadow-lg border-0">
<div class="modal-header border-bottom border-secondary py-2 px-3">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-images text-primary fs-5"></i>
<h5 class="modal-title fs-6 mb-0 text-white" id="remarkPhotoCarouselModalLabel">{% translate "Photos de la remarque" %}</h5>
<span class="badge bg-primary rounded-pill px-2 py-1 fs-7" id="remark-carousel-counter">1 / 1</span>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
</div>
<div class="modal-body p-0 position-relative bg-black d-flex align-items-center justify-content-center overflow-hidden" style="min-height: 300px; max-height: 80vh;">
<div id="remarkPhotoCarousel" class="carousel slide w-100 h-100 d-flex flex-column justify-content-center" data-bs-ride="false">
<div class="carousel-indicators mb-2" id="remark-carousel-indicators"></div>
<div class="carousel-inner h-100" id="remark-carousel-inner"></div>
<button class="carousel-control-prev" type="button" data-bs-target="#remarkPhotoCarousel" data-bs-slide="prev" style="z-index: 10;">
<span class="carousel-control-prev-icon p-3 rounded-circle bg-dark bg-opacity-75" aria-hidden="true"></span>
<span class="visually-hidden">{% translate "Précédent" %}</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#remarkPhotoCarousel" data-bs-slide="next" style="z-index: 10;">
<span class="carousel-control-next-icon p-3 rounded-circle bg-dark bg-opacity-75" aria-hidden="true"></span>
<span class="visually-hidden">{% translate "Suivant" %}</span>
</button>
</div>
</div>
<div class="modal-footer border-top border-secondary py-2 px-3 d-flex justify-content-between align-items-center bg-dark">
<div class="small text-muted text-truncate me-2" id="remark-carousel-caption"></div>
<div class="d-flex align-items-center gap-2">
<a id="remark-carousel-hd-btn" href="#" target="_blank" class="btn btn-sm btn-outline-light d-inline-flex align-items-center gap-1" title="{% translate 'Ouvrir en plein écran HD' %}">
<i class="bi bi-box-arrow-up-right"></i> <span class="d-none d-sm-inline">{% translate "Plein écran" %}</span>
</a>
{% if can_change_control %}
<button type="button" id="remark-carousel-delete-btn" class="btn btn-sm btn-outline-danger d-inline-flex align-items-center gap-1" title="{% translate 'Supprimer cette photo' %}">
<i class="bi bi-trash"></i> <span class="d-none d-sm-inline">{% translate "Supprimer" %}</span>
</button>
{% endif %}
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
</div>
</div>
</div>
</div>
</div>
</script> </script>
{% endblock content %} {% endblock content %}