213 lines
7.8 KiB
Twig
213 lines
7.8 KiB
Twig
{# 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>
|