|
|
|
@@ -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<string | null> {
|
|
|
|
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<MediaItem[]> {
|
|
|
|
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<string | null>(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() { |
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* Gallery Media (images + videos) */}
|
|
|
|
{/* Gallery Media (images + videos + PDFs) */}
|
|
|
|
<div>
|
|
|
|
<label className="block text-sm font-semibold text-gray-800 mb-1">
|
|
|
|
Gallery Media <span className="text-gray-400 font-normal text-xs">(images or videos, shown in detail modal)</span>
|
|
|
|
Gallery Media <span className="text-gray-400 font-normal text-xs">(images, videos or PDFs — PDF pages become slides)</span>
|
|
|
|
</label>
|
|
|
|
<div className="border-2 border-dashed border-gray-300 rounded-lg p-3 hover:bg-gray-50 transition-colors">
|
|
|
|
<input
|
|
|
|
type="file"
|
|
|
|
accept="image/*,video/*"
|
|
|
|
accept="image/*,video/*,application/pdf,.pdf"
|
|
|
|
multiple
|
|
|
|
onChange={handleUploadExtraMedia}
|
|
|
|
className="block w-full text-sm text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-purple-50 file:text-purple-700 hover:file:bg-purple-100 cursor-pointer"
|
|
|
|
/>
|
|
|
|
{uploading['extraMedia'] && <p className="mt-2 text-sm text-purple-600 font-medium">Uploading...</p>}
|
|
|
|
{uploading['extraMedia'] && !pdfProgress && <p className="mt-2 text-sm text-purple-600 font-medium">Uploading...</p>}
|
|
|
|
{pdfProgress && (
|
|
|
|
<p className="mt-2 text-sm text-purple-600 font-medium">
|
|
|
|
Processing “{pdfProgress.name}”: page {pdfProgress.page} of {pdfProgress.total}
|
|
|
|
</p>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{(isEditing.extraMedia || []).length > 0 && (
|
|
|
|
|