diff --git a/app/admin/page.tsx b/app/admin/page.tsx index b49b473..23ddefc 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -4,6 +4,51 @@ import { useState, useEffect } from 'react'; 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); + +async function uploadBlobAsImage(blob: Blob, name: string): Promise { + const formData = new FormData(); + formData.append('file', new File([blob], name, { type: blob.type || 'image/png' })); + const res = await fetch('/api/upload', { method: 'POST', body: formData }); + const data = await res.json(); + return data.url || null; +} + +async function pdfToImageItems( + file: File, + onProgress: (page: number, total: number) => void +): Promise { + const pdfjs = await import('pdfjs-dist'); + // Worker file is copied to /public via the postinstall script + pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs'; + + const arrayBuffer = await file.arrayBuffer(); + const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise; + const baseName = file.name.replace(/\.pdf$/i, '').replace(/[^a-zA-Z0-9-_]/g, '_'); + const items: MediaItem[] = []; + + for (let i = 1; i <= pdf.numPages; i++) { + 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 }); + } + + return items; +} export default function AdminDashboard() { const [activeTab, setActiveTab] = useState<'cards' | 'settings'>('cards'); @@ -20,6 +65,7 @@ export default function AdminDashboard() { // NEW UI STATES: Toast and Confirm Dialog const [toast, setToast] = useState(null); const [confirmDialog, setConfirmDialog] = useState<{ message: string, onConfirm: () => void } | null>(null); + const [pdfProgress, setPdfProgress] = useState<{ name: string; page: number; total: number } | null>(null); // Helper to show auto-dismissing toast const showToast = (message: string) => { @@ -58,11 +104,25 @@ export default function AdminDashboard() { const uploaded: MediaItem[] = []; for (const file of Array.from(files)) { - 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 }); + try { + if (isPdfFile(file)) { + const items = await pdfToImageItems(file, (page, total) => + setPdfProgress({ name: file.name, page, total }) + ); + uploaded.push(...items); + setPdfProgress(null); + } 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 }); + } + } catch (err) { + console.error('Upload failed for', file.name, err); + showToast(`Failed to process "${file.name}".`); + setPdfProgress(null); + } } setIsEditing(prev => ({ @@ -376,20 +436,25 @@ export default function AdminDashboard() { )} - {/* Gallery Media (images + videos) */} + {/* Gallery Media (images + videos + PDFs) */}
- {uploading['extraMedia'] &&

Uploading...

} + {uploading['extraMedia'] && !pdfProgress &&

Uploading...

} + {pdfProgress && ( +

+ Processing “{pdfProgress.name}”: page {pdfProgress.page} of {pdfProgress.total} +

+ )}
{(isEditing.extraMedia || []).length > 0 && ( diff --git a/package.json b/package.json index 48d1e21..b7cdffa 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,12 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "postinstall": "node -e \"require('fs').copyFileSync('node_modules/pdfjs-dist/build/pdf.worker.min.mjs','public/pdf.worker.min.mjs')\"" }, "dependencies": { "next": "16.2.4", + "pdfjs-dist": "^4.7.76", "react": "19.2.4", "react-dom": "19.2.4" },