From 7f0b1d1139e77550c351fe6b19741ecad064ca6c Mon Sep 17 00:00:00 2001 From: pollutri Date: Fri, 8 May 2026 12:44:37 +0200 Subject: [PATCH] Falback popolamento cover --- app/admin/page.tsx | 79 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 920fdf2..e90376c 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -6,6 +6,8 @@ import { Card, Portal, MediaItem } from '@/types'; const isVideoUrl = (url: string) => /\.(mp4|webm|mov|m4v|ogv)(\?|$)/i.test(url); const isPdfFile = (file: File) => file.type === 'application/pdf' || /\.pdf$/i.test(file.name); +const isVideoFile = (file: File) => + file.type.startsWith('video/') || /\.(mp4|webm|mov|m4v|ogv)$/i.test(file.name); async function uploadBlobAsImage(blob: Blob, name: string): Promise { const formData = new FormData(); @@ -15,6 +17,42 @@ async function uploadBlobAsImage(blob: Blob, name: string): Promise { + const url = URL.createObjectURL(file); + try { + const video = document.createElement('video'); + video.muted = true; + video.playsInline = true; + video.preload = 'metadata'; + video.src = url; + + await new Promise((resolve, reject) => { + video.addEventListener('loadedmetadata', () => resolve(), { once: true }); + video.addEventListener('error', () => reject(new Error('video load error')), { once: true }); + }); + + // Seek slightly past 0 — at exactly 0 some codecs return a black frame + video.currentTime = Math.min(0.1, Math.max(0, video.duration / 10)); + await new Promise((resolve, reject) => { + video.addEventListener('seeked', () => resolve(), { once: true }); + video.addEventListener('error', () => reject(new Error('video seek error')), { once: true }); + }); + + const canvas = document.createElement('canvas'); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const ctx = canvas.getContext('2d'); + if (!ctx) return null; + ctx.drawImage(video, 0, 0); + + return await new Promise((resolve) => + canvas.toBlob((b) => resolve(b), 'image/jpeg', 0.85) + ); + } finally { + URL.revokeObjectURL(url); + } +} + async function pdfToImageItems( file: File, onProgress: (page: number, total: number) => void @@ -102,6 +140,10 @@ export default function AdminDashboard() { if (!files || files.length === 0) return; setUploading(prev => ({ ...prev, extraMedia: true })); + const startedWithoutCover = !isEditing?.imageUrl; + let pendingCover: string | null = null; + const canPromote = () => startedWithoutCover && !pendingCover; + const uploaded: MediaItem[] = []; for (const file of Array.from(files)) { try { @@ -109,14 +151,46 @@ export default function AdminDashboard() { const items = await pdfToImageItems(file, (page, total) => setPdfProgress({ name: file.name, page, total }) ); - uploaded.push(...items); setPdfProgress(null); + if (items.length > 0 && canPromote()) { + // Promote the first PDF page to cover; skip it from the gallery to avoid duplication. + pendingCover = items[0].url; + uploaded.push(...items.slice(1)); + } else { + uploaded.push(...items); + } } else { const formData = new FormData(); formData.append('file', file); const res = await fetch('/api/upload', { method: 'POST', body: formData }); const data = await res.json(); - if (data.url) uploaded.push({ url: data.url }); + if (!data.url) continue; + + if (isVideoFile(file)) { + // Video always goes to the gallery so users can play it. + uploaded.push({ url: data.url }); + // If no cover yet, extract the first frame and use it as the cover. + if (canPromote()) { + try { + const blob = await extractVideoFrame(file); + if (blob) { + const baseName = file.name.replace(/\.[^.]+$/, '').replace(/[^a-zA-Z0-9-_]/g, '_'); + const posterUrl = await uploadBlobAsImage(blob, `${baseName}-poster.jpg`); + if (posterUrl) pendingCover = posterUrl; + } + } catch (err) { + console.warn('Could not extract video poster for', file.name, err); + } + } + } else { + // Plain image + if (canPromote()) { + // Promote to cover; skip the gallery to avoid duplication. + pendingCover = data.url; + } else { + uploaded.push({ url: data.url }); + } + } } } catch (err) { console.error('Upload failed for', file.name, err); @@ -127,6 +201,7 @@ export default function AdminDashboard() { setIsEditing(prev => ({ ...prev, + imageUrl: (startedWithoutCover && pendingCover) ? pendingCover : (prev?.imageUrl || ''), extraMedia: [...(prev?.extraMedia || []), ...uploaded], })); setUploading(prev => ({ ...prev, extraMedia: false }));