fix file preview
This commit is contained in:
parent
05a92208ce
commit
1977e9ffb6
5 changed files with 318 additions and 562 deletions
|
@ -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">
|
||||
<!-- 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="backToFileList()">
|
||||
← Back
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
onclick="window.closeFilePreviewEvents()"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<h3 class="font-bold text-lg truncate" id="previewFileName"></h3>
|
||||
<h3 class="font-bold text-lg truncate" id="previewFileName">
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative" id="previewContainer">
|
||||
<div
|
||||
id="loadingSpinner"
|
||||
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"></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() {
|
||||
|
@ -964,14 +889,57 @@ import JSZip from "jszip";
|
|||
}
|
||||
}
|
||||
|
||||
// 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");
|
||||
|
||||
// Add openDetailsModal function
|
||||
if (modal && previewFileName && previewContent) {
|
||||
console.log('Found all required elements');
|
||||
// Update the filename display
|
||||
previewFileName.textContent = filename;
|
||||
|
||||
// Show the modal
|
||||
modal.showModal();
|
||||
|
||||
// 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
|
||||
|
@ -1008,25 +972,20 @@ import JSZip from "jszip";
|
|||
.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.showFilePreview(${JSON.stringify(
|
||||
{
|
||||
url: fileUrl,
|
||||
type: fileType,
|
||||
name: file,
|
||||
},
|
||||
)})'>
|
||||
<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" />
|
||||
<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" />
|
||||
<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>
|
||||
|
@ -1042,7 +1001,7 @@ import JSZip from "jszip";
|
|||
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" />
|
||||
<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>
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 */}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue