| @@ -172,6 +172,16 @@ async function uploadBlobAsImage(blob: Blob, name: string): Promise<string | nul | |||
| const formData = new FormData(); | |||
| formData.append('file', new File([blob], name, { type: blob.type || 'image/png' })); | |||
| 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(); | |||
| return data.url || null; | |||
| } | |||
| @@ -216,35 +226,80 @@ async function pdfToImageItems( | |||
| file: File, | |||
| onProgress: (page: number, total: number) => void | |||
| ): 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 | |||
| // 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 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 items: MediaItem[] = []; | |||
| for (let i = 1; i <= pdf.numPages; i++) { | |||
| log(`page ${i}/${pdf.numPages}: rendering...`); | |||
| 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; | |||
| } | |||
| @@ -505,9 +560,18 @@ export default function AdminDashboard() { | |||
| } | |||
| } | |||
| } 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); | |||
| } | |||
| } | |||