| @@ -172,6 +172,16 @@ async function uploadBlobAsImage(blob: Blob, name: string): Promise<string | nul | |||||
| const formData = new FormData(); | const formData = new FormData(); | ||||
| formData.append('file', new File([blob], name, { type: blob.type || 'image/png' })); | formData.append('file', new File([blob], name, { type: blob.type || 'image/png' })); | ||||
| const res = await fetch(withBasePath('/api/upload'), { method: 'POST', body: formData }); | const res = await fetch(withBasePath('/api/upload'), { method: 'POST', body: formData }); | ||||
| // Se il server rifiuta (4xx/5xx) propaga il messaggio specifico, così il chiamante | |||||
| // (pdfToImageItems / handleUploadExtraMedia) vede la causa invece di un null muto. | |||||
| if (!res.ok) { | |||||
| let serverMsg = `HTTP ${res.status}`; | |||||
| try { | |||||
| const errBody = await res.json(); | |||||
| if (errBody?.error) serverMsg = errBody.error; | |||||
| } catch { /* response non era JSON */ } | |||||
| throw new Error(`/api/upload rejected "${name}": ${serverMsg}`); | |||||
| } | |||||
| const data = await res.json(); | const data = await res.json(); | ||||
| return data.url || null; | return data.url || null; | ||||
| } | } | ||||
| @@ -216,35 +226,80 @@ async function pdfToImageItems( | |||||
| file: File, | file: File, | ||||
| onProgress: (page: number, total: number) => void | onProgress: (page: number, total: number) => void | ||||
| ): Promise<MediaItem[]> { | ): Promise<MediaItem[]> { | ||||
| const pdfjs = await import('pdfjs-dist'); | |||||
| // Log step-by-step nella console del browser per poter diagnosticare un fallimento. | |||||
| // Apri DevTools → Console e filtra per "[pdf]" per vedere ogni passo. | |||||
| const log = (msg: string, extra?: unknown) => { | |||||
| if (extra !== undefined) console.info(`[pdf] ${msg}`, extra); | |||||
| else console.info(`[pdf] ${msg}`); | |||||
| }; | |||||
| log(`start: file="${file.name}", size=${(file.size / 1024).toFixed(1)} KB, type="${file.type}"`); | |||||
| let pdfjs; | |||||
| try { | |||||
| pdfjs = await import('pdfjs-dist'); | |||||
| log(`pdfjs-dist imported`); | |||||
| } catch (err) { | |||||
| throw new Error(`Could not load the PDF library (pdfjs-dist). ${(err as Error).message}`); | |||||
| } | |||||
| // Worker file is copied to /public via the postinstall script. Must include the | // Worker file is copied to /public via the postinstall script. Must include the | ||||
| // basePath so the browser can find it when the app is mounted under /cards. | // basePath so the browser can find it when the app is mounted under /cards. | ||||
| pdfjs.GlobalWorkerOptions.workerSrc = withBasePath('/pdf.worker.min.mjs'); | |||||
| const workerUrl = withBasePath('/pdf.worker.min.mjs'); | |||||
| pdfjs.GlobalWorkerOptions.workerSrc = workerUrl; | |||||
| log(`worker set to ${workerUrl}`); | |||||
| const arrayBuffer = await file.arrayBuffer(); | const arrayBuffer = await file.arrayBuffer(); | ||||
| const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise; | |||||
| log(`file loaded into memory (${arrayBuffer.byteLength} bytes)`); | |||||
| let pdf; | |||||
| try { | |||||
| pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise; | |||||
| } catch (err) { | |||||
| // getDocument fallisce per: worker non raggiungibile (404), PDF cifrato, PDF malformato. | |||||
| throw new Error(`Could not open the PDF (pdfjs getDocument failed): ${(err as Error).message}. The PDF may be encrypted, corrupted, or the pdf.worker file may not be reachable at ${workerUrl}.`); | |||||
| } | |||||
| log(`PDF opened: ${pdf.numPages} pages`); | |||||
| const baseName = file.name.replace(/\.pdf$/i, '').replace(/[^a-zA-Z0-9-_]/g, '_'); | const baseName = file.name.replace(/\.pdf$/i, '').replace(/[^a-zA-Z0-9-_]/g, '_'); | ||||
| const items: MediaItem[] = []; | const items: MediaItem[] = []; | ||||
| for (let i = 1; i <= pdf.numPages; i++) { | for (let i = 1; i <= pdf.numPages; i++) { | ||||
| log(`page ${i}/${pdf.numPages}: rendering...`); | |||||
| onProgress(i, pdf.numPages); | onProgress(i, pdf.numPages); | ||||
| const page = await pdf.getPage(i); | |||||
| const viewport = page.getViewport({ scale: 1.5 }); | |||||
| const canvas = document.createElement('canvas'); | |||||
| canvas.width = viewport.width; | |||||
| canvas.height = viewport.height; | |||||
| const ctx = canvas.getContext('2d'); | |||||
| if (!ctx) continue; | |||||
| await page.render({ canvasContext: ctx, viewport }).promise; | |||||
| const blob: Blob = await new Promise((resolve, reject) => { | |||||
| canvas.toBlob(b => b ? resolve(b) : reject(new Error('toBlob failed')), 'image/png'); | |||||
| }); | |||||
| const url = await uploadBlobAsImage(blob, `${baseName}-page${i}.png`); | |||||
| if (url) items.push({ url }); | |||||
| try { | |||||
| const page = await pdf.getPage(i); | |||||
| const viewport = page.getViewport({ scale: 1.5 }); | |||||
| const canvas = document.createElement('canvas'); | |||||
| canvas.width = viewport.width; | |||||
| canvas.height = viewport.height; | |||||
| const ctx = canvas.getContext('2d'); | |||||
| if (!ctx) { | |||||
| log(`page ${i}: skipped — could not get 2D canvas context`); | |||||
| continue; | |||||
| } | |||||
| await page.render({ canvasContext: ctx, viewport }).promise; | |||||
| log(`page ${i}: rendered (${viewport.width}x${viewport.height})`); | |||||
| const blob: Blob = await new Promise((resolve, reject) => { | |||||
| canvas.toBlob(b => b ? resolve(b) : reject(new Error('canvas.toBlob returned null (PNG encoding failed)')), 'image/png'); | |||||
| }); | |||||
| log(`page ${i}: encoded to PNG (${(blob.size / 1024).toFixed(1)} KB)`); | |||||
| const url = await uploadBlobAsImage(blob, `${baseName}-page${i}.png`); | |||||
| if (url) { | |||||
| log(`page ${i}: uploaded → ${url}`); | |||||
| items.push({ url }); | |||||
| } else { | |||||
| log(`page ${i}: upload returned no URL (server response had no .url field — check the /api/upload network response)`); | |||||
| } | |||||
| } catch (err) { | |||||
| // Propaga ma con contesto sulla pagina specifica così l'utente vede DOVE. | |||||
| throw new Error(`Failed on page ${i}: ${(err as Error).message}`); | |||||
| } | |||||
| } | } | ||||
| log(`finished: ${items.length} pages uploaded successfully`); | |||||
| return items; | return items; | ||||
| } | } | ||||
| @@ -505,9 +560,18 @@ export default function AdminDashboard() { | |||||
| } | } | ||||
| } | } | ||||
| } catch (err) { | } catch (err) { | ||||
| console.error('Upload failed for', file.name, err); | |||||
| const reason = (err as Error)?.message || 'unknown error'; | |||||
| showToast(`Failed to process "${file.name}": ${reason}. Check the browser console for details.`, 'error'); | |||||
| // Dump completo nella console (stack incluso) per il debug step-by-step | |||||
| // visibile in DevTools → Console. L'utente vede invece la causa nel toast. | |||||
| const e = err as Error; | |||||
| console.error(`[upload] Failed to process "${file.name}"`); | |||||
| console.error(`[upload] Message: ${e?.message ?? '(no message)'}`); | |||||
| console.error(`[upload] Stack:`, e?.stack ?? '(no stack)'); | |||||
| console.error(`[upload] Raw error object:`, err); | |||||
| const reason = e?.message || 'unknown error'; | |||||
| showToast( | |||||
| `Failed to process "${file.name}": ${reason}. Open DevTools (F12) → Console and filter by "[pdf]" or "[upload]" to see step-by-step diagnostics.`, | |||||
| 'error', | |||||
| ); | |||||
| setPdfProgress(null); | setPdfProgress(null); | ||||
| } | } | ||||
| } | } | ||||