SuperSeller3000/templates/admin/field/photos.html.twig
Simon Kuehn 1df6b7f0c6 fix: use entity.instance instead of ea.entity.instance in field template
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:16:30 +00:00

213 lines
7.8 KiB
Twig
Raw Permalink 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.

{# Article photo gallery — shown on detail page #}
{% set article = entity.instance %}
{% set articleId = article.id.toRfc4122() %}
{% set photos = article.photos|sort((a, b) => a.sortOrder <=> b.sortOrder) %}
<style>
.photo-gallery { margin-top: .5rem; }
.photo-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
min-height: 80px;
}
.photo-card {
position: relative;
width: 160px;
border: 2px solid #dee2e6;
border-radius: 8px;
overflow: hidden;
background: #f8f9fa;
cursor: grab;
transition: box-shadow .15s;
}
.photo-card:active { cursor: grabbing; }
.photo-card.is-main { border-color: #0d6efd; }
.photo-card.sortable-ghost { opacity: .4; }
.photo-card img {
width: 100%;
height: 120px;
object-fit: cover;
display: block;
}
.photo-card-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 6px;
background: rgba(0,0,0,.04);
}
.photo-main-badge {
font-size: .65rem;
font-weight: 700;
color: #0d6efd;
text-transform: uppercase;
letter-spacing: .04em;
}
.photo-card-actions .btn { padding: 2px 6px; font-size: .75rem; }
.upload-zone {
border: 2px dashed #adb5bd;
border-radius: 8px;
padding: 20px;
text-align: center;
color: #6c757d;
cursor: pointer;
margin-bottom: 16px;
transition: border-color .2s, background .2s;
}
.upload-zone.drag-over {
border-color: #0d6efd;
background: #e8f0fe;
}
.upload-zone input[type=file] { display: none; }
</style>
<div class="photo-gallery" id="photo-gallery-{{ articleId }}">
{# Upload zone #}
<div class="upload-zone" id="upload-zone-{{ articleId }}">
<input type="file" id="photo-file-input-{{ articleId }}"
accept="image/jpeg,image/png,image/webp" multiple>
<i class="fa fa-cloud-upload-alt fa-lg mb-2 d-block"></i>
<span>Fotos hier ablegen oder <strong>klicken zum Hochladen</strong></span>
<div id="upload-status-{{ articleId }}" class="mt-2 small"></div>
</div>
{# Photo grid #}
<div class="photo-grid" id="photo-grid-{{ articleId }}">
{% for photo in photos %}
<div class="photo-card {% if photo.isMain %}is-main{% endif %}"
data-photo-id="{{ photo.id.toRfc4122() }}">
<img src="{{ path('admin_photo_serve', {filename: photo.filename}) }}"
alt="Photo">
<div class="photo-card-actions">
<span class="photo-main-badge">{% if photo.isMain %}⭐ Main{% endif %}</span>
<div class="d-flex gap-1">
{% if not photo.isMain %}
<button class="btn btn-sm btn-outline-primary photo-btn-main"
title="Als Hauptbild setzen">★</button>
{% endif %}
<button class="btn btn-sm btn-outline-danger photo-btn-delete"
title="Löschen">×</button>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script>
(function () {
const articleId = {{ articleId|json_encode|raw }};
const grid = document.getElementById('photo-grid-' + articleId);
const zone = document.getElementById('upload-zone-' + articleId);
const fileInput = document.getElementById('photo-file-input-' + articleId);
const status = document.getElementById('upload-status-' + articleId);
/* ---------- drag-and-drop reorder ---------- */
Sortable.create(grid, {
animation: 150,
ghostClass: 'sortable-ghost',
onEnd() {
const order = [...grid.querySelectorAll('.photo-card')]
.map(el => el.dataset.photoId);
fetch('/api/articles/' + articleId + '/photos/sort', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order }),
});
},
});
/* ---------- upload ---------- */
zone.addEventListener('click', () => fileInput.click());
zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag-over'); });
zone.addEventListener('dragleave', () => zone.classList.remove('drag-over'));
zone.addEventListener('drop', e => {
e.preventDefault();
zone.classList.remove('drag-over');
uploadFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', () => uploadFiles(fileInput.files));
async function uploadFiles(files) {
for (const file of files) {
status.textContent = 'Lade hoch: ' + file.name + '…';
const fd = new FormData();
fd.append('photo', file);
const res = await fetch('/api/articles/' + articleId + '/photos', {
method: 'POST',
body: fd,
});
if (res.ok) {
const data = await res.json();
appendPhotoCard(data.id, '/admin/photos/' + encodeURIComponent(file.name.replace(/^.*[\\/]/, '')), data.isMain, file);
status.textContent = '';
} else {
status.textContent = 'Fehler beim Hochladen von ' + file.name;
}
}
fileInput.value = '';
}
function appendPhotoCard(photoId, imgSrc, isMain, file) {
const card = document.createElement('div');
card.className = 'photo-card' + (isMain ? ' is-main' : '');
card.dataset.photoId = photoId;
const img = document.createElement('img');
img.src = URL.createObjectURL(file);
img.alt = 'Photo';
card.appendChild(img);
const actions = document.createElement('div');
actions.className = 'photo-card-actions';
actions.innerHTML = '<span class="photo-main-badge">' + (isMain ? '⭐ Main' : '') + '</span>'
+ '<div class="d-flex gap-1">'
+ (isMain ? '' : '<button class="btn btn-sm btn-outline-primary photo-btn-main" title="Als Hauptbild setzen">★</button>')
+ '<button class="btn btn-sm btn-outline-danger photo-btn-delete" title="Löschen">×</button>'
+ '</div>';
card.appendChild(actions);
grid.appendChild(card);
wireCard(card);
}
/* ---------- set main / delete ---------- */
function wireCard(card) {
const btnMain = card.querySelector('.photo-btn-main');
const btnDel = card.querySelector('.photo-btn-delete');
btnMain?.addEventListener('click', async () => {
await fetch('/api/articles/' + articleId + '/photos/' + card.dataset.photoId + '/main', {
method: 'PATCH',
});
// update badges
grid.querySelectorAll('.photo-card').forEach(c => {
c.classList.remove('is-main');
c.querySelector('.photo-main-badge').textContent = '';
const bm = c.querySelector('.photo-btn-main');
if (!bm) {
const acts = c.querySelector('.photo-card-actions .d-flex');
acts.insertAdjacentHTML('afterbegin',
'<button class="btn btn-sm btn-outline-primary photo-btn-main" title="Als Hauptbild setzen">★</button>');
wireCard(c);
}
});
card.classList.add('is-main');
card.querySelector('.photo-main-badge').textContent = '⭐ Main';
btnMain.remove();
});
btnDel?.addEventListener('click', async () => {
if (!confirm('Foto löschen?')) return;
const res = await fetch('/api/articles/' + articleId + '/photos/' + card.dataset.photoId, {
method: 'DELETE',
});
if (res.ok) card.remove();
});
}
grid.querySelectorAll('.photo-card').forEach(wireCard);
})();
</script>