multiple media upload and smartphone preview
This commit is contained in:
@@ -52,6 +52,24 @@
|
||||
.image-upload-progress-bar { height: 100%; background: #ffc700; border-radius: 2px; transition: width 0.3s; }
|
||||
.loading-spinner { border: 2px solid rgba(255, 199, 0, 0.2); border-top-color: #ffc700; border-radius: 50%; width: 20px; height: 20px; animation: spin 0.8s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
/* Preview Mode Toggle */
|
||||
.preview-mode-btn { padding: 6px 12px; font-size: 12px; font-weight: 500; border-radius: 6px; color: #9ca3af; background: transparent; border: none; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 6px; }
|
||||
.preview-mode-btn.active { background: rgba(255, 199, 0, 0.2); color: #ffc700; }
|
||||
.preview-mode-btn:hover:not(.active) { color: #d1d5db; }
|
||||
/* Desktop View */
|
||||
.linkedin-preview.desktop-view { max-width: 100%; margin: 0; }
|
||||
/* Mobile View - smartphone mockup */
|
||||
.linkedin-preview.mobile-view { max-width: 375px; margin: 0 auto; border: 12px solid #1f1f1f; border-radius: 36px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1); position: relative; }
|
||||
.linkedin-preview.mobile-view::before { content: ''; position: absolute; top: -8px; left: 50%; transform: translateX(-50%); width: 60px; height: 4px; background: #2a2a2a; border-radius: 2px; }
|
||||
.linkedin-preview.mobile-view .linkedin-header { padding: 16px; }
|
||||
.linkedin-preview.mobile-view .linkedin-avatar { width: 40px; height: 40px; font-size: 16px; }
|
||||
.linkedin-preview.mobile-view .linkedin-name { font-size: 13px; }
|
||||
.linkedin-preview.mobile-view .linkedin-headline { font-size: 11px; }
|
||||
.linkedin-preview.mobile-view .linkedin-timestamp { font-size: 11px; }
|
||||
.linkedin-preview.mobile-view .linkedin-content { padding: 0 16px 16px; font-size: 13px; line-height: 1.4; }
|
||||
.linkedin-preview.mobile-view .linkedin-engagement { padding: 12px 16px; font-size: 11px; }
|
||||
.linkedin-preview.mobile-view .linkedin-action-btn { font-size: 13px; padding: 14px 8px; gap: 4px; }
|
||||
.linkedin-preview.mobile-view .linkedin-action-btn svg { width: 18px; height: 18px; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -106,7 +124,23 @@
|
||||
</div>
|
||||
|
||||
<!-- LinkedIn Preview -->
|
||||
<div class="linkedin-preview shadow-lg">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
|
||||
LinkedIn Vorschau
|
||||
</div>
|
||||
<div class="flex items-center gap-1 bg-brand-bg rounded-lg p-1">
|
||||
<button onclick="setPreviewMode('desktop')" id="desktopPreviewBtn" class="preview-mode-btn active">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
||||
Desktop
|
||||
</button>
|
||||
<button onclick="setPreviewMode('mobile')" id="mobilePreviewBtn" class="preview-mode-btn">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"/></svg>
|
||||
Smartphone
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="linkedin-preview shadow-lg desktop-view">
|
||||
<div class="linkedin-header">
|
||||
<div class="linkedin-avatar">{{ employee_name[:2] | upper if employee_name else 'UN' }}</div>
|
||||
<div class="linkedin-user-info">
|
||||
@@ -122,11 +156,25 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="linkedin-content">{{ post.post_content }}</div>
|
||||
{% if post.image_url %}
|
||||
<img id="linkedinPostImage" src="{{ post.image_url }}" alt="Post image" class="linkedin-post-image">
|
||||
{% else %}
|
||||
<img id="linkedinPostImage" src="" alt="Post image" class="linkedin-post-image" style="display: none;">
|
||||
{% endif %}
|
||||
|
||||
<!-- LinkedIn Media Display -->
|
||||
<div id="linkedinMediaSection" class="{% if not post.media_items or post.media_items | length == 0 %}{% if not post.image_url %}hidden{% endif %}{% endif %}">
|
||||
{% if post.media_items and post.media_items | length > 0 %}
|
||||
<div class="grid gap-1" style="grid-template-columns: repeat({{ post.media_items | length if post.media_items | length <= 3 else 3 }}, 1fr);">
|
||||
{% for item in post.media_items %}
|
||||
{% if item.type == 'image' %}
|
||||
<img src="{{ item.url }}" alt="Post media" class="w-full h-48 object-cover">
|
||||
{% elif item.type == 'video' %}
|
||||
<video src="{{ item.url }}" class="w-full h-48 object-cover" controls></video>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif post.image_url %}
|
||||
<!-- Backward compatibility: single image -->
|
||||
<img src="{{ post.image_url }}" alt="Post image" class="linkedin-post-image">
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="linkedin-engagement">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="#0a66c2">
|
||||
<path d="M19.46 11l-3.91-3.91a7 7 0 01-1.69-2.74l-.49-1.47A2.76 2.76 0 0010.76 1 2.75 2.75 0 008 3.74v1.12a9.19 9.19 0 00.46 2.85L8.89 9H4.12A2.12 2.12 0 002 11.12a2.16 2.16 0 00.92 1.76A2.11 2.11 0 002 14.62a2.14 2.14 0 001.28 2 2 2 0 00-.28 1 2.12 2.12 0 002 2.12v.14A2.12 2.12 0 007.12 22h7.49a8.08 8.08 0 003.58-.84l.31-.16H21V11z"/>
|
||||
@@ -177,40 +225,56 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Upload -->
|
||||
<!-- Media Upload Section (Multi-Media Support) -->
|
||||
<div class="section-card rounded-xl p-6">
|
||||
<h3 class="font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||
Bild
|
||||
Medien (<span id="mediaCount">{{ post.media_items | length if post.media_items else 0 }}</span>/3)
|
||||
</h3>
|
||||
<div id="imageUploadZone" class="image-upload-zone {% if post.image_url %}hidden{% endif %}">
|
||||
<input type="file" id="imageFileInput" accept="image/jpeg,image/png,image/gif,image/webp">
|
||||
<svg class="w-8 h-8 mx-auto mb-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/></svg>
|
||||
<p class="text-sm text-gray-400">Bild hierher ziehen oder <span class="text-brand-highlight cursor-pointer">durchsuchen</span></p>
|
||||
<p class="text-xs text-gray-500 mt-1">JPEG, PNG, GIF, WebP - max. 5 MB</p>
|
||||
|
||||
<!-- Media Grid (shown when media exists) -->
|
||||
<div id="mediaGrid" class="grid gap-3 mb-3 {% if not post.media_items or post.media_items | length == 0 %}hidden{% endif %}" style="grid-template-columns: repeat({{ post.media_items | length if post.media_items and post.media_items | length <= 3 else 1 }}, 1fr);">
|
||||
{% if post.media_items %}
|
||||
{% for item in post.media_items %}
|
||||
<div class="media-item relative group rounded-lg overflow-hidden" data-index="{{ item.order if item.order is defined else loop.index0 }}" draggable="true" style="cursor: grab;">
|
||||
{% if item.type == 'image' %}
|
||||
<img src="{{ item.url }}" alt="Media {{ loop.index }}" class="w-full h-48 object-cover">
|
||||
{% elif item.type == 'video' %}
|
||||
<video src="{{ item.url }}" class="w-full h-48 object-cover" controls></video>
|
||||
{% endif %}
|
||||
|
||||
<!-- Delete button -->
|
||||
<button onclick="deleteMedia({{ item.order if item.order is defined else loop.index0 }})" class="absolute top-2 right-2 p-2 bg-red-600 rounded-full opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-700">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Order badge -->
|
||||
<div class="absolute bottom-2 left-2 px-2 py-1 bg-black/50 rounded text-xs text-white">
|
||||
{{ loop.index }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="imageUploadProgress" class="hidden">
|
||||
|
||||
<!-- Upload Zone (shown when < 3 media items) -->
|
||||
<div id="mediaUploadZone" class="{% if post.media_items and post.media_items | length >= 3 %}hidden{% endif %} border-2 border-dashed border-brand-bg-light rounded-xl p-6 text-center cursor-pointer hover:border-brand-highlight transition-colors">
|
||||
<input type="file" id="mediaFileInput" accept="image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm,video/quicktime" class="hidden">
|
||||
<svg class="w-8 h-8 mx-auto mb-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/></svg>
|
||||
<p class="text-sm text-gray-400">Bild oder Video hierher ziehen oder <span class="text-brand-highlight">durchsuchen</span></p>
|
||||
<p class="text-xs text-gray-500 mt-1">Bilder: max. 5 MB | Videos: max. 50 MB</p>
|
||||
<p id="mediaTypeWarning" class="text-xs text-yellow-400 mt-2 hidden">⚠️ Kann nicht Bilder und Videos mischen</p>
|
||||
</div>
|
||||
|
||||
<!-- Upload Progress -->
|
||||
<div id="mediaUploadProgress" class="hidden">
|
||||
<div class="image-upload-progress">
|
||||
<div id="imageProgressBar" class="image-upload-progress-bar" style="width: 0%"></div>
|
||||
<div id="mediaProgressBar" class="image-upload-progress-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1 text-center">Wird hochgeladen...</p>
|
||||
</div>
|
||||
<div id="imagePreviewSection" class="{% if not post.image_url %}hidden{% endif %}">
|
||||
<div class="image-preview-container mb-3">
|
||||
<img id="sidebarImagePreview" src="{{ post.image_url or '' }}" alt="Post-Bild">
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="document.getElementById('imageReplaceInput').click()" class="flex-1 px-3 py-2 bg-brand-bg hover:bg-brand-bg-light text-gray-300 rounded-lg transition-colors text-sm flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||||
Ersetzen
|
||||
</button>
|
||||
<button onclick="removeImage()" id="removeImageBtn" class="flex-1 px-3 py-2 bg-red-600/20 hover:bg-red-600/30 text-red-400 rounded-lg transition-colors text-sm flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||
Entfernen
|
||||
</button>
|
||||
</div>
|
||||
<input type="file" id="imageReplaceInput" accept="image/jpeg,image/png,image/gif,image/webp" class="hidden">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post Info -->
|
||||
@@ -256,6 +320,25 @@ function copyToClipboard() {
|
||||
});
|
||||
}
|
||||
|
||||
// Preview Mode Toggle (Desktop/Mobile)
|
||||
function setPreviewMode(mode) {
|
||||
const preview = document.querySelector('.linkedin-preview');
|
||||
const desktopBtn = document.getElementById('desktopPreviewBtn');
|
||||
const mobileBtn = document.getElementById('mobilePreviewBtn');
|
||||
|
||||
if (mode === 'desktop') {
|
||||
preview.classList.remove('mobile-view');
|
||||
preview.classList.add('desktop-view');
|
||||
desktopBtn.classList.add('active');
|
||||
mobileBtn.classList.remove('active');
|
||||
} else if (mode === 'mobile') {
|
||||
preview.classList.remove('desktop-view');
|
||||
preview.classList.add('mobile-view');
|
||||
mobileBtn.classList.add('active');
|
||||
desktopBtn.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateStatus(newStatus) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
@@ -294,13 +377,38 @@ function showToast(message, type = 'info') {
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
async function handleImageUpload(file) {
|
||||
// ==================== MULTI-MEDIA UPLOAD ====================
|
||||
|
||||
let currentMediaItems = {{ media_items_dict | tojson | safe }};
|
||||
|
||||
async function handleMediaUpload(file) {
|
||||
if (!file) return;
|
||||
|
||||
const uploadZone = document.getElementById('imageUploadZone');
|
||||
const progressEl = document.getElementById('imageUploadProgress');
|
||||
const progressBar = document.getElementById('imageProgressBar');
|
||||
const previewSection = document.getElementById('imagePreviewSection');
|
||||
const uploadZone = document.getElementById('mediaUploadZone');
|
||||
const progressEl = document.getElementById('mediaUploadProgress');
|
||||
const progressBar = document.getElementById('mediaProgressBar');
|
||||
const warningEl = document.getElementById('mediaTypeWarning');
|
||||
|
||||
if (currentMediaItems.length >= 3) {
|
||||
showToast('Maximal 3 Medien erlaubt', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const isVideo = file.type.startsWith('video/');
|
||||
const hasImages = currentMediaItems.some(item => item.type === 'image');
|
||||
const hasVideos = currentMediaItems.some(item => item.type === 'video');
|
||||
|
||||
if ((isVideo && hasImages) || (!isVideo && hasVideos)) {
|
||||
warningEl.classList.remove('hidden');
|
||||
showToast('Kann nicht Bilder und Videos mischen', 'error');
|
||||
setTimeout(() => warningEl.classList.add('hidden'), 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVideo && currentMediaItems.length > 0) {
|
||||
showToast('Nur 1 Video pro Post erlaubt', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
uploadZone.classList.add('hidden');
|
||||
progressEl.classList.remove('hidden');
|
||||
@@ -308,10 +416,10 @@ async function handleImageUpload(file) {
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
formData.append('file', file);
|
||||
progressBar.style.width = '60%';
|
||||
|
||||
const response = await fetch(`/api/posts/${POST_ID}/image`, {
|
||||
const response = await fetch(`/api/posts/${POST_ID}/media`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
@@ -325,69 +433,265 @@ async function handleImageUpload(file) {
|
||||
|
||||
const result = await response.json();
|
||||
progressBar.style.width = '100%';
|
||||
|
||||
document.getElementById('linkedinPostImage').src = result.image_url;
|
||||
document.getElementById('linkedinPostImage').style.display = 'block';
|
||||
document.getElementById('sidebarImagePreview').src = result.image_url;
|
||||
currentMediaItems = result.media_items || [];
|
||||
|
||||
setTimeout(() => {
|
||||
progressEl.classList.add('hidden');
|
||||
previewSection.classList.remove('hidden');
|
||||
refreshMediaGrid();
|
||||
refreshLinkedInPreview();
|
||||
}, 300);
|
||||
|
||||
showToast('Bild erfolgreich hochgeladen!', 'success');
|
||||
const mediaType = isVideo ? 'Video' : 'Bild';
|
||||
showToast(`${mediaType} erfolgreich hochgeladen!`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Image upload error:', error);
|
||||
console.error('Media upload error:', error);
|
||||
showToast('Fehler: ' + error.message, 'error');
|
||||
progressEl.classList.add('hidden');
|
||||
uploadZone.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeImage() {
|
||||
const btn = document.getElementById('removeImageBtn');
|
||||
const originalHTML = btn.innerHTML;
|
||||
btn.innerHTML = '<div class="loading-spinner" style="width:16px;height:16px;"></div>';
|
||||
btn.disabled = true;
|
||||
async function deleteMedia(index) {
|
||||
if (!confirm('Media wirklich löschen?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/posts/${POST_ID}/image`, { method: 'DELETE' });
|
||||
if (!response.ok) throw new Error('Löschen fehlgeschlagen');
|
||||
const response = await fetch(`/api/posts/${POST_ID}/media/${index}`, { method: 'DELETE' });
|
||||
|
||||
document.getElementById('linkedinPostImage').style.display = 'none';
|
||||
document.getElementById('imagePreviewSection').classList.add('hidden');
|
||||
document.getElementById('imageUploadZone').classList.remove('hidden');
|
||||
showToast('Bild entfernt.', 'success');
|
||||
if (!response.ok) {
|
||||
const err = await response.json();
|
||||
throw new Error(err.detail || 'Löschen fehlgeschlagen');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
currentMediaItems = result.media_items || [];
|
||||
|
||||
refreshMediaGrid();
|
||||
refreshLinkedInPreview();
|
||||
showToast('Media entfernt', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Image delete error:', error);
|
||||
console.error('Media delete error:', error);
|
||||
showToast('Fehler: ' + error.message, 'error');
|
||||
} finally {
|
||||
btn.innerHTML = originalHTML;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const uploadZone = document.getElementById('imageUploadZone');
|
||||
const fileInput = document.getElementById('imageFileInput');
|
||||
const replaceInput = document.getElementById('imageReplaceInput');
|
||||
function refreshMediaGrid() {
|
||||
const grid = document.getElementById('mediaGrid');
|
||||
const count = document.getElementById('mediaCount');
|
||||
const uploadZone = document.getElementById('mediaUploadZone');
|
||||
|
||||
if (uploadZone) {
|
||||
uploadZone.addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', (e) => { if (e.target.files[0]) handleImageUpload(e.target.files[0]); });
|
||||
if (replaceInput) {
|
||||
replaceInput.addEventListener('change', (e) => { if (e.target.files[0]) handleImageUpload(e.target.files[0]); });
|
||||
}
|
||||
uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
|
||||
uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); });
|
||||
uploadZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.remove('dragover');
|
||||
if (e.dataTransfer.files[0]) handleImageUpload(e.dataTransfer.files[0]);
|
||||
});
|
||||
count.textContent = currentMediaItems.length;
|
||||
|
||||
if (currentMediaItems.length === 0) {
|
||||
grid.classList.add('hidden');
|
||||
uploadZone.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
grid.classList.remove('hidden');
|
||||
grid.style.gridTemplateColumns = `repeat(${currentMediaItems.length}, 1fr)`;
|
||||
|
||||
grid.innerHTML = currentMediaItems.map((item, i) => {
|
||||
const isDragged = draggedItemData && item.url === draggedItemData.url;
|
||||
const ghostClass = isDragged ? 'is-ghost' : '';
|
||||
const ghostStyle = isDragged ? 'opacity: 0.3;' : '';
|
||||
|
||||
return `
|
||||
<div class="media-item relative group rounded-lg overflow-hidden border-2 border-transparent hover:border-brand-highlight ${ghostClass}"
|
||||
data-array-index="${i}"
|
||||
draggable="true"
|
||||
style="cursor: grab; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); ${ghostStyle}">
|
||||
<div class="media-content">
|
||||
${item.type === 'image'
|
||||
? `<img src="${item.url}" alt="Media ${i+1}" class="w-full h-48 object-cover pointer-events-none">`
|
||||
: `<video src="${item.url}" class="w-full h-48 object-cover pointer-events-none" controls></video>`
|
||||
}
|
||||
</div>
|
||||
<button onclick="deleteMedia(${item.order})"
|
||||
class="absolute top-2 right-2 p-2 bg-red-600 rounded-full opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-700 z-10">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="absolute bottom-2 left-2 px-2 py-1 bg-black/50 rounded text-xs text-white pointer-events-none">${i+1}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
initDragAndDrop();
|
||||
|
||||
if (currentMediaItems.length >= 3) {
|
||||
uploadZone.classList.add('hidden');
|
||||
} else {
|
||||
uploadZone.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function refreshLinkedInPreview() {
|
||||
const linkedinMediaSection = document.getElementById('linkedinMediaSection');
|
||||
if (!linkedinMediaSection) return;
|
||||
|
||||
if (currentMediaItems.length === 0) {
|
||||
linkedinMediaSection.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
linkedinMediaSection.classList.remove('hidden');
|
||||
const gridClass = currentMediaItems.length === 1 ? 'grid-cols-1' :
|
||||
currentMediaItems.length === 2 ? 'grid-cols-2' : 'grid-cols-3';
|
||||
|
||||
linkedinMediaSection.innerHTML = `
|
||||
<div class="grid ${gridClass} gap-1">
|
||||
${currentMediaItems.map(item =>
|
||||
item.type === 'image'
|
||||
? `<img src="${item.url}" class="w-full h-48 object-cover">`
|
||||
: `<video src="${item.url}" class="w-full h-48 object-cover" controls></video>`
|
||||
).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Drag-and-Drop Reorder (Ultra Smooth with Ghost)
|
||||
let draggedArrayIndex = null;
|
||||
let originalDraggedIndex = null; // Track the original starting position
|
||||
let draggedElement = null;
|
||||
let lastTargetIndex = null;
|
||||
let isSaving = false;
|
||||
let draggedItemData = null;
|
||||
|
||||
function initDragAndDrop() {
|
||||
const mediaGrid = document.getElementById('mediaGrid');
|
||||
if (!mediaGrid) return;
|
||||
|
||||
const mediaItems = mediaGrid.querySelectorAll('.media-item');
|
||||
|
||||
mediaItems.forEach((item, idx) => {
|
||||
item.addEventListener('dragstart', (e) => {
|
||||
draggedArrayIndex = parseInt(e.currentTarget.dataset.arrayIndex);
|
||||
originalDraggedIndex = draggedArrayIndex; // Save the original position
|
||||
draggedElement = e.currentTarget;
|
||||
lastTargetIndex = draggedArrayIndex;
|
||||
draggedItemData = currentMediaItems[draggedArrayIndex];
|
||||
|
||||
setTimeout(() => {
|
||||
e.currentTarget.style.opacity = '0.3';
|
||||
}, 0);
|
||||
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setDragImage(e.currentTarget, e.currentTarget.offsetWidth / 2, e.currentTarget.offsetHeight / 2);
|
||||
});
|
||||
|
||||
item.addEventListener('dragend', (e) => {
|
||||
const allItems = mediaGrid.querySelectorAll('.media-item');
|
||||
allItems.forEach(i => {
|
||||
i.style.opacity = '1';
|
||||
i.classList.remove('is-ghost');
|
||||
});
|
||||
|
||||
console.log('🎯 Dragend:', {originalDraggedIndex, lastTargetIndex});
|
||||
|
||||
if (lastTargetIndex !== null && lastTargetIndex !== originalDraggedIndex && !isSaving) {
|
||||
console.log('💾 Triggering save...');
|
||||
saveReorderInBackground();
|
||||
} else {
|
||||
console.log('⏭️ Skip save');
|
||||
}
|
||||
|
||||
draggedArrayIndex = null;
|
||||
originalDraggedIndex = null;
|
||||
draggedElement = null;
|
||||
lastTargetIndex = null;
|
||||
draggedItemData = null;
|
||||
});
|
||||
|
||||
item.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
|
||||
if (draggedArrayIndex === null) return;
|
||||
|
||||
const targetArrayIndex = parseInt(e.currentTarget.dataset.arrayIndex);
|
||||
|
||||
if (targetArrayIndex !== lastTargetIndex && draggedArrayIndex !== targetArrayIndex) {
|
||||
lastTargetIndex = targetArrayIndex;
|
||||
|
||||
const newMediaItems = [...currentMediaItems];
|
||||
const [movedItem] = newMediaItems.splice(draggedArrayIndex, 1);
|
||||
newMediaItems.splice(targetArrayIndex, 0, movedItem);
|
||||
|
||||
currentMediaItems = newMediaItems;
|
||||
draggedArrayIndex = targetArrayIndex;
|
||||
|
||||
refreshMediaGrid();
|
||||
refreshLinkedInPreview();
|
||||
}
|
||||
});
|
||||
|
||||
item.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function saveReorderInBackground() {
|
||||
if (isSaving) return;
|
||||
isSaving = true;
|
||||
|
||||
const orderArray = currentMediaItems.map((item, i) => item.order);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/posts/${POST_ID}/media/reorder`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({order: orderArray})
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Speichern fehlgeschlagen');
|
||||
|
||||
const result = await response.json();
|
||||
currentMediaItems = result.media_items || [];
|
||||
|
||||
console.log('✓ Reihenfolge im Hintergrund gespeichert');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Background save error:', error);
|
||||
showToast('Fehler beim Speichern - bitte Seite neu laden', 'error');
|
||||
} finally {
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function initMediaUpload() {
|
||||
const uploadZone = document.getElementById('mediaUploadZone');
|
||||
const fileInput = document.getElementById('mediaFileInput');
|
||||
|
||||
if (!uploadZone) return;
|
||||
|
||||
uploadZone.addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files[0]) handleMediaUpload(e.target.files[0]);
|
||||
e.target.value = '';
|
||||
});
|
||||
|
||||
uploadZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.add('dragover');
|
||||
});
|
||||
uploadZone.addEventListener('dragleave', () => {
|
||||
uploadZone.classList.remove('dragover');
|
||||
});
|
||||
uploadZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.remove('dragover');
|
||||
if (e.dataTransfer.files[0]) handleMediaUpload(e.dataTransfer.files[0]);
|
||||
});
|
||||
|
||||
initDragAndDrop();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initMediaUpload();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user