fix file preview

This commit is contained in:
chark1es 2025-02-16 03:47:59 -08:00
parent 05a92208ce
commit 1977e9ffb6
5 changed files with 318 additions and 562 deletions

View file

@ -1,6 +1,7 @@
---
import { Icon } from "astro-icon/components";
import JSZip from "jszip";
import FilePreview from "./Officer_EventManagement/FilePreview";
---
<div id="eventsSection" class="dashboard-section hidden">
@ -149,7 +150,7 @@ import JSZip from "jszip";
</div>
<button
class="btn btn-circle btn-ghost"
onclick="eventDetailsModal.close()"
onclick="window.closeEventDetailsModal()"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -170,30 +171,41 @@ import JSZip from "jszip";
<div id="filesContent" class="space-y-4">
<!-- Files list will be populated here -->
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick="window.closeEventDetailsModal()">close</button>
</form>
</dialog>
<!-- File Preview Section -->
<div id="filePreviewSection" class="hidden">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-3">
<button class="btn btn-ghost btn-sm" onclick="backToFileList()">
← Back
</button>
<h3 class="font-bold text-lg truncate" id="previewFileName"></h3>
</div>
</div>
<div class="relative" id="previewContainer">
<div
id="loadingSpinner"
class="absolute inset-0 flex items-center justify-center bg-base-200 bg-opacity-50 hidden"
<!-- Universal File Preview Modal -->
<dialog id="filePreviewModal" class="modal">
<div class="modal-box max-w-4xl">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-3">
<button
class="btn btn-ghost btn-sm"
onclick="window.closeFilePreviewEvents()"
>
<span class="loading loading-spinner loading-lg"></span>
</div>
<div id="previewContent" class="w-full"></div>
Close
</button>
<h3 class="font-bold text-lg truncate" id="previewFileName">
</h3>
</div>
</div>
<div class="relative" id="previewContainer">
<div
id="previewLoadingSpinner"
class="absolute inset-0 flex items-center justify-center bg-base-200 bg-opacity-50 hidden"
>
<span class="loading loading-spinner loading-lg"></span>
</div>
<div id="previewContent" class="w-full">
<FilePreview client:load isModal={true} />
</div>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
<button onclick="window.closeFilePreviewEvents()">close</button>
</form>
</dialog>
@ -858,95 +870,8 @@ import JSZip from "jszip";
}
function showFilePreview(file: { url: string; type: string; name: string }) {
const filePreviewSection = document.getElementById("filePreviewSection");
const filesContent = document.getElementById("filesContent");
const previewContent = document.getElementById("previewContent");
const previewFileName = document.getElementById("previewFileName");
const modalTitle = document.getElementById("modalTitle");
if (
!filePreviewSection ||
!filesContent ||
!previewContent ||
!previewFileName
)
return;
filePreviewSection.classList.remove("hidden");
filesContent.classList.add("hidden");
previewFileName.textContent = file.name;
if (modalTitle) modalTitle.textContent = "File Preview";
const fileType = file.type.toLowerCase();
showLoading();
// Create the PocketBase URL
const baseUrl = "https://pocketbase.ieeeucsd.org";
const fileUrl = `${baseUrl}/api/files/events/${currentEventId}/${file.name}`;
if (!isPreviewableType(fileType)) {
previewContent.innerHTML = `
<div class="flex flex-col items-center justify-center p-8">
<div class="text-4xl mb-4">📄</div>
<p class="text-center">
This file type (${file.type}) cannot be previewed.
<br />
<a href="${fileUrl}" download="${file.name}" class="btn btn-primary mt-4" target="_blank" rel="noopener noreferrer">
Open in New Tab
</a>
</p>
</div>
`;
hideLoading();
return;
}
if (fileType.startsWith("image/")) {
previewContent.innerHTML = `
<img
src="${fileUrl}"
alt="${file.name}"
class="max-w-full max-h-[70vh] object-contain"
onload="hideLoading()"
onerror="handlePreviewError()"
/>
`;
} else if (fileType.startsWith("video/")) {
previewContent.innerHTML = `
<video controls class="max-w-full max-h-[70vh]" onloadeddata="hideLoading()" onerror="handlePreviewError()">
<source src="${fileUrl}" type="${file.type}" />
Your browser does not support the video tag.
</video>
`;
} else if (fileType === "application/pdf") {
previewContent.innerHTML = `
<iframe
src="${fileUrl}"
class="w-full h-[70vh]"
onload="hideLoading()"
onerror="handlePreviewError()"
></iframe>
`;
} else if (
fileType.startsWith("text/") ||
fileType === "application/json"
) {
previewContent.innerHTML = `
<iframe
src="${fileUrl}"
class="w-full h-[70vh] font-mono"
onload="hideLoading()"
onerror="handlePreviewError()"
></iframe>
`;
} else if (fileType.startsWith("audio/")) {
previewContent.innerHTML = `
<audio controls class="w-full" onloadeddata="hideLoading()" onerror="handlePreviewError()">
<source src="${fileUrl}" type="${file.type}" />
Your browser does not support the audio element.
</audio>
`;
}
console.log('showFilePreview called with:', file);
window.previewFileEvents(file.url, file.name);
}
function handlePreviewError() {
@ -954,24 +879,67 @@ import JSZip from "jszip";
const previewContent = document.getElementById("previewContent");
if (previewContent) {
previewContent.innerHTML = `
<div class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Failed to load file preview</span>
</div>
`;
<div class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Failed to load file preview</span>
</div>
`;
}
}
// Make helper functions available globally
window.showFilePreview = showFilePreview;
window.backToFileList = backToFileList;
window.handlePreviewError = handlePreviewError;
window.showLoading = showLoading;
window.hideLoading = hideLoading;
// Universal file preview function for events section
window.previewFileEvents = function (url: string, filename: string) {
console.log('previewFileEvents called with:', { url, filename });
const modal = document.getElementById("filePreviewModal") as HTMLDialogElement;
const previewFileName = document.getElementById("previewFileName");
const previewContent = document.getElementById("previewContent");
if (modal && previewFileName && previewContent) {
console.log('Found all required elements');
// Update the filename display
previewFileName.textContent = filename;
// Show the modal
modal.showModal();
// Add openDetailsModal function
// Dispatch state change event
window.dispatchEvent(new CustomEvent('filePreviewStateChange', {
detail: { url, filename }
}));
}
};
// Close file preview for events section
window.closeFilePreviewEvents = function () {
console.log('closeFilePreviewEvents called');
const modal = document.getElementById("filePreviewModal") as HTMLDialogElement;
const previewFileName = document.getElementById("previewFileName");
const previewContent = document.getElementById("previewContent");
if (modal && previewFileName && previewContent) {
console.log('Resetting preview and closing modal');
// Reset the preview state
window.dispatchEvent(new CustomEvent('filePreviewStateChange', {
detail: { url: "", filename: "" }
}));
// Reset the UI
previewFileName.textContent = "";
// Close the modal
modal.close();
}
};
// Update the showFilePreview function for events section
window.showFilePreviewEvents = function (file: { url: string; name: string }) {
console.log('showFilePreviewEvents called with:', file);
window.previewFileEvents(file.url, file.name);
};
// Update the openDetailsModal function to use the events-specific preview
window.openDetailsModal = function (event: any) {
const modal = document.getElementById(
"eventDetailsModal",
@ -979,13 +947,9 @@ import JSZip from "jszip";
const filesContent = document.getElementById(
"filesContent",
) as HTMLDivElement;
const filePreviewSection = document.getElementById(
"filePreviewSection",
) as HTMLDivElement;
// Reset state
currentEventId = event.id;
if (filePreviewSection) filePreviewSection.classList.add("hidden");
if (filesContent) filesContent.classList.remove("hidden");
// Populate files content
@ -995,58 +959,53 @@ import JSZip from "jszip";
const recordId = event.id;
filesContent.innerHTML = `
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>File Name</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
${event.files
.map((file: string) => {
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${file}`;
const fileType = getFileType(file);
return `
<tr>
<td>${file}</td>
<td class="text-right">
<button class="btn btn-ghost btn-sm" onclick='window.showFilePreview(${JSON.stringify(
{
url: fileUrl,
type: fileType,
name: file,
},
)})'>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" />
</svg>
</button>
<a href="${fileUrl}" download="${file}" class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</a>
</td>
</tr>
`;
})
.join("")}
</tbody>
</table>
</div>
`;
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>File Name</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
${event.files
.map((file: string) => {
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${file}`;
const fileType = getFileType(file);
const previewData = JSON.stringify({ url: fileUrl, name: file }).replace(/'/g, "\\'");
return `
<tr>
<td>${file}</td>
<td class="text-right">
<button class="btn btn-ghost btn-sm" onclick='window.showFilePreviewEvents(${previewData})'>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/>
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/>
</svg>
</button>
<a href="${fileUrl}" download="${file}" class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</a>
</td>
</tr>
`;
})
.join("")}
</tbody>
</table>
</div>
`;
} else {
filesContent.innerHTML = `
<div class="text-center py-8 text-base-content/70">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 opacity-50" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd" />
</svg>
<p>No files attached to this event</p>
</div>
`;
<div class="text-center py-8 text-base-content/70">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 opacity-50" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/>
</svg>
<p>No files attached to this event</p>
</div>
`;
}
modal.showModal();
@ -1120,6 +1079,36 @@ import JSZip from "jszip";
}
};
// Close event details modal
window.closeEventDetailsModal = function () {
const modal = document.getElementById("eventDetailsModal") as HTMLDialogElement;
const filesContent = document.getElementById("filesContent");
if (modal) {
// Reset the files content
if (filesContent) {
filesContent.innerHTML = '';
}
// Reset any other state if needed
currentEventId = '';
// Close the modal
modal.close();
}
};
// Make helper functions available globally
window.showFilePreview = showFilePreview;
window.handlePreviewError = handlePreviewError;
window.showLoading = showLoading;
window.hideLoading = hideLoading;
window.previewFileEvents = previewFileEvents;
window.closeFilePreviewEvents = closeFilePreviewEvents;
window.showFilePreviewEvents = showFilePreviewEvents;
window.openDetailsModal = openDetailsModal;
window.closeEventDetailsModal = closeEventDetailsModal;
// Add TypeScript interface for Window
declare global {
interface Window {

View file

@ -648,27 +648,7 @@ const currentPage = eventResponse.page;
</div>
<div id="attendeesContent" class="space-y-4 hidden">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Event Attendees</h3>
<button
id="downloadAttendeesCSV"
class="btn btn-primary btn-sm gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
Download CSV
</button>
</div>
<!-- Attendees list will be populated here -->
<!-- Attendees content -->
</div>
</div>
<form method="dialog" class="modal-backdrop">
@ -683,9 +663,9 @@ const currentPage = eventResponse.page;
<div class="flex items-center gap-3">
<button
class="btn btn-ghost btn-sm"
onclick="window.closeFilePreview()"
onclick="window.closeFilePreviewOfficer()"
>
← Back
Close
</button>
<h3 class="font-bold text-lg truncate" id="previewFileName">
</h3>
@ -699,12 +679,12 @@ const currentPage = eventResponse.page;
<span class="loading loading-spinner loading-lg"></span>
</div>
<div id="previewContent" class="w-full">
<FilePreview client:only url="" filename="" id="universalFilePreview" />
<FilePreview client:only="react" />
</div>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick="window.closeFilePreview()">close</button>
<button onclick="window.closeFilePreviewOfficer()">close</button>
</form>
</dialog>
@ -1776,29 +1756,8 @@ const currentPage = eventResponse.page;
// Update the showFilePreview function
window.showFilePreview = function (file: { url: string; name: string }) {
const fileListSection = document.getElementById("filesContent");
const previewSection = document.getElementById("filePreviewSection");
const mainFilePreview = document.getElementById("mainFilePreview");
const previewFileName = document.getElementById("previewFileName");
if (
!fileListSection ||
!previewSection ||
!mainFilePreview ||
!previewFileName
)
return;
// Hide file list and show preview section
fileListSection.classList.add("hidden");
previewSection.classList.remove("hidden");
previewFileName.textContent = file.name;
// Dispatch a custom event to update the FilePreview
const event = new CustomEvent("updateFilePreview", {
detail: { url: file.url, filename: file.name },
});
mainFilePreview.dispatchEvent(event);
console.log('showFilePreview called with:', file);
window.previewFile(file.url, file.name);
};
// Add backToEditForm function
@ -1829,11 +1788,13 @@ const currentPage = eventResponse.page;
// Universal file preview function
window.previewFile = function (url: string, filename: string) {
console.log('previewFile called with:', { url, filename });
const modal = document.getElementById("filePreviewModal") as HTMLDialogElement;
const filePreview = document.getElementById("universalFilePreview") as any;
const filePreview = document.getElementById("officerFilePreview") as any;
const previewFileName = document.getElementById("previewFileName");
if (filePreview && modal && previewFileName) {
console.log('Found all required elements');
// Update the filename display
previewFileName.textContent = filename;
@ -1841,29 +1802,36 @@ const currentPage = eventResponse.page;
modal.showModal();
// Update the preview component
filePreview.setAttribute("url", url);
filePreview.setAttribute("filename", filename);
console.log('Dispatching updateFilePreview event');
const event = new CustomEvent("updateFilePreview", {
detail: { url, filename }
});
filePreview.dispatchEvent(event);
} else {
console.error('Missing required elements:', {
modal: !!modal,
filePreview: !!filePreview,
previewFileName: !!previewFileName
});
}
};
// Close file preview
window.closeFilePreview = function () {
console.log('closeFilePreview called');
const modal = document.getElementById("filePreviewModal") as HTMLDialogElement;
const filePreview = document.getElementById("universalFilePreview") as any;
const filePreview = document.getElementById("officerFilePreview") as any;
const previewFileName = document.getElementById("previewFileName");
const filesContent = document.getElementById("filesContent");
if (modal && filePreview && previewFileName) {
console.log('Resetting preview and closing modal');
// Reset the preview
filePreview.setAttribute("url", "");
filePreview.setAttribute("filename", "");
const event = new CustomEvent("updateFilePreview", {
detail: { url: "", filename: "" }
});
filePreview.dispatchEvent(event);
previewFileName.textContent = "";
modal.close();
// Show the files list if we're in the event details modal
if (filesContent) {
filesContent.classList.remove('hidden');
}
}
};
@ -1891,13 +1859,14 @@ const currentPage = eventResponse.page;
.map(
(filename) => {
const fileUrl = fileManager.getFileUrl("events", eventId, filename);
const previewData = JSON.stringify({ url: fileUrl, name: filename }).replace(/'/g, "\\'");
return `
<div class="flex items-center justify-between p-2 bg-base-200 rounded-lg${filesToDelete.has(filename) ? " opacity-50" : ""}" data-filename="${filename}">
<div class="flex items-center gap-2 flex-1 min-w-0">
<span class="truncate">${filename}</span>
</div>
<div class="flex gap-2 flex-shrink-0">
<button type="button" class="btn btn-ghost btn-xs" onclick="window.previewFile('${fileUrl}', '${filename}')">
<button type="button" class="btn btn-ghost btn-xs" onclick='window.showFilePreviewOfficer(${previewData})'>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/>
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/>
@ -2318,4 +2287,69 @@ const currentPage = eventResponse.page;
}
}
};
// Create a custom event for file preview state management
const FILE_PREVIEW_STATE_CHANGE = 'filePreviewStateChange';
// Universal file preview function for officer section
window.previewFileOfficer = function (url: string, filename: string) {
console.log('previewFileOfficer called with:', { url, filename });
const modal = document.getElementById("filePreviewModal") as HTMLDialogElement;
const previewFileName = document.getElementById("previewFileName");
if (modal && previewFileName) {
console.log('Found all required elements');
// Update the filename display
previewFileName.textContent = filename;
// Show the modal
modal.showModal();
// Dispatch state change event
console.log('Dispatching state change event');
window.dispatchEvent(new CustomEvent(FILE_PREVIEW_STATE_CHANGE, {
detail: { url, filename }
}));
} else {
console.error('Missing required elements:', {
modal: !!modal,
previewFileName: !!previewFileName
});
}
};
// Close file preview for officer section
window.closeFilePreviewOfficer = function () {
console.log('closeFilePreviewOfficer called');
const modal = document.getElementById("filePreviewModal") as HTMLDialogElement;
const previewContent = document.getElementById("previewContent");
const previewFileName = document.getElementById("previewFileName");
if (modal && previewContent && previewFileName) {
console.log('Resetting preview and closing modal');
// Reset the preview
const filePreview = previewContent.querySelector('astro-island') as any;
if (filePreview) {
const component = filePreview.querySelector('[data-astro-cid]') as any;
if (component) {
component.setAttribute('url', '');
component.setAttribute('filename', '');
}
}
previewFileName.textContent = "";
modal.close();
}
};
// Update the showFilePreview function for officer section
window.showFilePreviewOfficer = function (file: { url: string; name: string }) {
console.log('showFilePreviewOfficer called with:', file);
window.previewFileOfficer(file.url, file.name);
};
// Make helper functions available globally
window.updateFilePreviewButtons = updateFilePreviewButtons;
window.previewFileOfficer = previewFileOfficer;
window.closeFilePreviewOfficer = closeFilePreviewOfficer;
window.showFilePreviewOfficer = showFilePreviewOfficer;
</script>

View file

@ -267,7 +267,7 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
className="btn btn-ghost btn-sm"
onClick={() => setShowPreview(false)}
>
Back
Close
</button>
<h3 className="font-bold text-lg truncate">
{previewFilename}
@ -278,6 +278,7 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
<FilePreview
url={previewUrl}
filename={previewFilename}
isModal={false}
/>
</div>
</div>

View file

@ -1,18 +1,57 @@
import React, { useEffect, useState } from 'react';
interface FilePreviewProps {
url: string;
filename: string;
url?: string;
filename?: string;
isModal?: boolean;
}
const FilePreview: React.FC<FilePreviewProps> = ({ url, filename }) => {
const FilePreview: React.FC<FilePreviewProps> = ({ url: initialUrl = '', filename: initialFilename = '', isModal = false }) => {
const [url, setUrl] = useState(initialUrl);
const [filename, setFilename] = useState(initialFilename);
const [content, setContent] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [fileType, setFileType] = useState<string | null>(null);
useEffect(() => {
console.log('FilePreview component mounted');
if (isModal) {
const handleStateChange = (event: CustomEvent<{ url: string; filename: string }>) => {
console.log('Received state change event:', event.detail);
const { url: newUrl, filename: newFilename } = event.detail;
setUrl(newUrl);
setFilename(newFilename);
// Reset state when url is empty (modal closing)
if (!newUrl) {
setContent(null);
setError(null);
setFileType(null);
setLoading(false);
}
};
// Add event listener only for modal mode
window.addEventListener('filePreviewStateChange', handleStateChange as EventListener);
// Cleanup
return () => {
window.removeEventListener('filePreviewStateChange', handleStateChange as EventListener);
};
} else {
// For integrated preview, use props directly
setUrl(initialUrl);
setFilename(initialFilename);
}
}, [isModal, initialUrl, initialFilename]);
useEffect(() => {
console.log('FilePreview state updated:', { url, filename });
if (!url || !filename) {
console.log('No URL or filename, resetting state');
setContent(null);
setError(null);
setFileType(null);
@ -20,38 +59,47 @@ const FilePreview: React.FC<FilePreviewProps> = ({ url, filename }) => {
}
const loadContent = async () => {
console.log('Loading content for:', { url, filename });
setLoading(true);
setError(null);
try {
console.log('Fetching file...');
const response = await fetch(url);
const contentType = response.headers.get('content-type');
console.log('Received content type:', contentType);
setFileType(contentType);
if (contentType?.startsWith('image/')) {
console.log('Setting content type as image');
setContent('image');
} else if (contentType?.startsWith('video/')) {
console.log('Setting content type as video');
setContent('video');
} else if (contentType?.startsWith('application/pdf')) {
console.log('Setting content type as pdf');
setContent('pdf');
} else if (contentType?.startsWith('text/')) {
console.log('Loading text content');
const text = await response.text();
// Truncate text if it's too long (e.g., more than 100KB)
if (text.length > 100000) {
console.log('Text content truncated due to length');
setContent(text.substring(0, 100000) + '\n\n... Content truncated. Please download the file to view the complete content.');
} else {
setContent(text);
}
} else if (filename.toLowerCase().endsWith('.mp4')) {
// Fallback for video files when content-type might not be correct
console.log('Fallback to video for .mp4 file');
setContent('video');
} else {
console.log('Unsupported file type');
setError(`This file type (${contentType || 'unknown'}) is not supported for preview. Please download the file to view it.`);
}
} catch (err) {
setError('Failed to load file');
console.error('Error loading file:', err);
setError('Failed to load file');
} finally {
console.log('Finished loading content');
setLoading(false);
}
};
@ -77,6 +125,8 @@ const FilePreview: React.FC<FilePreviewProps> = ({ url, filename }) => {
}
};
console.log('Rendering FilePreview with:', { content, error, loading, fileType });
return (
<div className="space-y-4">
{/* Header with filename and download button */}

View file

@ -1,318 +0,0 @@
import { useState, useEffect } from "react";
import { FileManager } from "../pocketbase/FileManager";
interface FilePreviewProps {
url: string;
filename: string;
onClose?: () => void;
}
export default function FilePreview({ url, filename, onClose }: FilePreviewProps) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [fileType, setFileType] = useState<string | null>(null);
const [modalElement, setModalElement] = useState<HTMLDialogElement | null>(null);
useEffect(() => {
const modal = document.getElementById('filePreviewModal') as HTMLDialogElement;
setModalElement(modal);
const initializeViewer = async () => {
try {
setLoading(true);
setError(null);
setFileContent(null);
// Determine file type from extension
const extension = filename.split('.').pop()?.toLowerCase() || '';
const type = getFileType(extension);
setFileType(type);
// If it's a code file, fetch its content
if (type === 'code') {
await fetchCodeContent(url);
}
setLoading(false);
} catch (err) {
setError("Failed to load file preview");
setLoading(false);
}
};
// Only initialize if we have both url and filename
if (url && filename) {
initializeViewer();
if (modal && !modal.open) {
modal.showModal();
}
}
// Cleanup function
return () => {
if (modal?.open) {
modal.close();
}
setFileContent(null);
};
}, [url, filename]);
const handleClose = () => {
if (modalElement?.open) {
modalElement.close();
}
onClose?.();
};
const [fileContent, setFileContent] = useState<string | null>(null);
const getFileType = (extension: string): string => {
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
const videoTypes = ['mp4', 'webm', 'ogg', 'mov'];
const documentTypes = ['pdf', 'doc', 'docx', 'txt', 'md'];
const spreadsheetTypes = ['xls', 'xlsx', 'csv'];
const presentationTypes = ['ppt', 'pptx'];
const codeTypes = ['js', 'ts', 'jsx', 'tsx', 'html', 'css', 'json', 'py', 'java', 'cpp', 'h', 'c', 'cs', 'php', 'rb', 'swift', 'go', 'rs'];
if (imageTypes.includes(extension)) return 'image';
if (videoTypes.includes(extension)) return 'video';
if (documentTypes.includes(extension)) return 'document';
if (spreadsheetTypes.includes(extension)) return 'spreadsheet';
if (presentationTypes.includes(extension)) return 'presentation';
if (codeTypes.includes(extension)) return 'code';
return 'other';
};
// Function to fetch and set code content
const fetchCodeContent = async (url: string) => {
try {
const response = await fetch(url);
const text = await response.text();
setFileContent(text);
} catch (err) {
console.error('Failed to fetch code content:', err);
setError('Failed to load code content');
}
};
const renderFileIcon = () => {
switch (fileType) {
case 'image':
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
);
case 'video':
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
);
case 'document':
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
);
case 'code':
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
);
case 'spreadsheet':
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
);
default:
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
);
}
};
const renderPreview = () => {
if (loading) {
return (
<div className="flex items-center justify-center h-96">
<span className="loading loading-spinner loading-lg"></span>
</div>
);
}
if (error) {
return (
<div className="flex flex-col items-center justify-center h-96 text-error">
<svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<p>{error}</p>
</div>
);
}
switch (fileType) {
case 'image':
return (
<div className="flex flex-col items-center justify-center gap-4">
<div className="relative group">
<img
src={url}
alt={filename}
className="max-w-full max-h-[calc(100vh-16rem)] object-contain rounded-lg"
onError={() => setError("Failed to load image")}
/>
<a
href={url}
download
className="btn btn-sm btn-primary absolute bottom-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity"
target="_blank"
rel="noopener noreferrer"
>
Download
</a>
</div>
</div>
);
case 'video':
return (
<div className="flex flex-col items-center justify-center gap-4">
<video
controls
className="max-w-full max-h-[calc(100vh-16rem)] rounded-lg"
onError={() => setError("Failed to load video")}
>
<source src={url} type={`video/${filename.split('.').pop()}`} />
Your browser does not support the video tag.
</video>
<a
href={url}
download
className="btn btn-primary btn-sm"
target="_blank"
rel="noopener noreferrer"
>
Download Video
</a>
</div>
);
case 'code':
if (fileContent !== null) {
return (
<div className="bg-base-200 rounded-lg p-4 overflow-x-auto">
<pre className="text-sm">
<code>{fileContent}</code>
</pre>
<div className="mt-4 flex justify-end">
<a
href={url}
download
className="btn btn-primary btn-sm"
target="_blank"
rel="noopener noreferrer"
>
Download Source
</a>
</div>
</div>
);
}
return (
<div className="flex flex-col items-center justify-center h-96 gap-4">
{renderFileIcon()}
<p className="text-lg font-semibold text-center max-w-full truncate px-4">{filename}</p>
<div className="loading loading-spinner loading-md"></div>
</div>
);
case 'document':
if (filename.toLowerCase().endsWith('.pdf')) {
return (
<div className="flex flex-col gap-4">
<iframe
src={url}
className="w-full h-[calc(100vh-16rem)] rounded-lg"
title={filename}
/>
<div className="flex justify-end">
<a
href={url}
download
className="btn btn-primary btn-sm"
target="_blank"
rel="noopener noreferrer"
>
Download PDF
</a>
</div>
</div>
);
}
// For other document types, show download button
return (
<div className="flex flex-col items-center justify-center h-96 gap-4">
{renderFileIcon()}
<p className="text-lg font-semibold text-center max-w-full truncate px-4">{filename}</p>
<a
href={url}
download
className="btn btn-primary"
target="_blank"
rel="noopener noreferrer"
>
Download File
</a>
</div>
);
default:
return (
<div className="flex flex-col items-center justify-center h-96 gap-4">
{renderFileIcon()}
<p className="text-lg font-semibold text-center max-w-full truncate px-4">{filename}</p>
<a
href={url}
download
className="btn btn-primary"
target="_blank"
rel="noopener noreferrer"
>
Download File
</a>
</div>
);
}
};
// Only render if we have both url and filename
if (!url || !filename) return null;
return (
<dialog id="filePreviewModal" className="modal">
<div className="modal-box max-w-4xl w-full">
<div className="flex justify-between items-center mb-4">
<div className="flex items-center gap-3 flex-1 min-w-0">
<button
className="btn btn-ghost btn-sm flex-shrink-0"
onClick={handleClose}
>
Back
</button>
<h3 className="font-bold text-lg truncate">
{filename}
</h3>
</div>
</div>
<div className="overflow-auto">
{renderPreview()}
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button onClick={handleClose}>close</button>
</form>
</dialog>
);
}