diff --git a/src/components/dashboard/Officer_EventManagement.astro b/src/components/dashboard/Officer_EventManagement.astro index 99b2445..c9d23c0 100644 --- a/src/components/dashboard/Officer_EventManagement.astro +++ b/src/components/dashboard/Officer_EventManagement.astro @@ -672,7 +672,7 @@ const currentPage = eventResponse.page; @@ -699,17 +699,12 @@ const currentPage = eventResponse.page;
- +
@@ -1834,48 +1829,62 @@ const currentPage = eventResponse.page; // Universal file preview function window.previewFile = function (url: string, filename: string) { - const modal = document.getElementById( - "filePreviewModal" - ) as HTMLDialogElement; + const modal = document.getElementById("filePreviewModal") as HTMLDialogElement; + const filePreview = document.getElementById("universalFilePreview") as any; const previewFileName = document.getElementById("previewFileName"); - const filePreview = document.getElementById("universalFilePreview"); - const loadingSpinner = document.getElementById("previewLoadingSpinner"); - - if (!modal || !previewFileName || !filePreview || !loadingSpinner) - return; - - // Show modal and update filename - modal.showModal(); - previewFileName.textContent = filename; - - // Show loading spinner - loadingSpinner.classList.remove("hidden"); - - try { - // Update the FilePreview component + + if (filePreview && modal && previewFileName) { + // Update the preview component const event = new CustomEvent("updateFilePreview", { detail: { url, filename }, }); filePreview.dispatchEvent(event); - } finally { - // Hide loading spinner - loadingSpinner.classList.add("hidden"); + + // Update the filename display + previewFileName.textContent = filename; + + // Show the modal + modal.showModal(); } }; // Close file preview window.closeFilePreview = function () { - const modal = document.getElementById( - "filePreviewModal" - ) as HTMLDialogElement; + const modal = document.getElementById("filePreviewModal") as HTMLDialogElement; const filePreview = document.getElementById("universalFilePreview"); + const previewFileName = document.getElementById("previewFileName"); + const filesContent = document.getElementById("filesContent"); - if (modal && filePreview) { + if (modal && filePreview && previewFileName) { // Reset the preview 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'); + } + } + }; + + // Close event details modal + window.closeEventDetailsModal = function () { + const modal = document.getElementById("eventDetailsModal") as HTMLDialogElement; + const filesContent = document.getElementById("filesContent"); + const attendeesContent = document.getElementById("attendeesContent"); + + if (modal) { + // Reset tab states + if (filesContent && attendeesContent) { + filesContent.classList.remove('hidden'); + attendeesContent.classList.add('hidden'); + } + + // Close the modal modal.close(); } }; @@ -1884,30 +1893,46 @@ const currentPage = eventResponse.page; function updateFilePreviewButtons(files: string[], eventId: string) { return files .map( - (filename) => ` -
- ${filename} -
- -
- ${ - filesToDelete.has(filename) - ? `` - : `` - } -
-
-
- ` + (filename) => { + const fileUrl = fileManager.getFileUrl("events", eventId, filename); + return ` +
+
+ + ${filename} +
+
+ + + + + +
+ ${ + filesToDelete.has(filename) + ? `` + : `` + } +
+
+
+ `; + } ) .join(""); } @@ -1919,12 +1944,30 @@ const currentPage = eventResponse.page; ) as HTMLDialogElement; const filesContent = document.getElementById("filesContent"); const attendeesContent = document.getElementById("attendeesContent"); + const tabs = document.querySelectorAll('.tabs .tab'); - if (!modal || !filesContent || !attendeesContent) return; + if (!modal || !filesContent || !attendeesContent || !tabs) return; // Show modal modal.showModal(); + // Add tab functionality + tabs.forEach(tab => { + tab.addEventListener('click', () => { + const tabName = (tab as HTMLElement).dataset.tab; + tabs.forEach(t => t.classList.remove('tab-active')); + tab.classList.add('tab-active'); + + if (tabName === 'files') { + filesContent.classList.remove('hidden'); + attendeesContent.classList.add('hidden'); + } else { + filesContent.classList.add('hidden'); + attendeesContent.classList.remove('hidden'); + } + }); + }); + // Update files list if (event.files && event.files.length > 0) { filesContent.innerHTML = ` @@ -1939,6 +1982,14 @@ const currentPage = eventResponse.page; `; } + + // Show files tab by default + const filesTab = Array.from(tabs).find(tab => (tab as HTMLElement).dataset.tab === 'files'); + if (filesTab) { + filesTab.classList.add('tab-active'); + filesContent.classList.remove('hidden'); + attendeesContent.classList.add('hidden'); + } }; // Add file input change handler to show selected files diff --git a/src/components/dashboard/Officer_EventManagement/EventEditor.tsx b/src/components/dashboard/Officer_EventManagement/EventEditor.tsx index 7834373..956ba58 100644 --- a/src/components/dashboard/Officer_EventManagement/EventEditor.tsx +++ b/src/components/dashboard/Officer_EventManagement/EventEditor.tsx @@ -1,4 +1,10 @@ import { useState, useEffect } from "react"; +import { Get } from "../../pocketbase/Get"; +import { Authentication } from "../../pocketbase/Authentication"; +import { Update } from "../../pocketbase/Update"; +import { FileManager } from "../../pocketbase/FileManager"; +import { SendLog } from "../../pocketbase/SendLog"; +import FilePreview from "../../modals/FilePreview"; // Extend Window interface declare global { @@ -7,12 +13,6 @@ declare global { hideLoading?: () => void; } } -import FilePreview from "../../modals/FilePreview"; -import { Get } from "../../pocketbase/Get"; -import { Authentication } from "../../pocketbase/Authentication"; -import { Update } from "../../pocketbase/Update"; -import { FileManager } from "../../pocketbase/FileManager"; -import { SendLog } from "../../pocketbase/SendLog"; interface Event { id: string; @@ -257,35 +257,18 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) { }; // Add preview section - const previewSection = ( -
-
- -

- {previewFilename} -

-
-
-
- -
-
- -
-
-
+ const previewSection = showPreview && event?.id && ( + { + setShowPreview(false); + const modal = document.getElementById('filePreviewModal') as HTMLDialogElement; + if (modal) { + modal.close(); + } + }} + /> ); return ( diff --git a/src/components/modals/FilePreview.tsx b/src/components/modals/FilePreview.tsx index da1402b..31badb4 100644 --- a/src/components/modals/FilePreview.tsx +++ b/src/components/modals/FilePreview.tsx @@ -1,415 +1,318 @@ -import React from 'react'; -import hljs from 'highlight.js'; -import 'highlight.js/styles/github-dark.css'; +import { useState, useEffect } from "react"; +import { FileManager } from "../pocketbase/FileManager"; interface FilePreviewProps { url: string; filename: string; - id?: string; + onClose?: () => void; } -type JSXElement = React.ReactElement; +export default function FilePreview({ url, filename, onClose }: FilePreviewProps) { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [fileType, setFileType] = useState(null); + const [modalElement, setModalElement] = useState(null); -const FilePreview: React.FC = ({ url: initialUrl, filename: initialFilename, id }) => { - const [url, setUrl] = React.useState(initialUrl); - const [filename, setFilename] = React.useState(initialFilename); - const [visibleLines, setVisibleLines] = React.useState(20); - const elementRef = React.useRef(null); - - // Constants for text preview - const INITIAL_LINES = 20; - const INCREMENT_LINES = 50; - const MAX_CHARS_PER_LINE = 120; - const TRUNCATION_MESSAGE = '...'; - - // Determine file type from extension - const fileExtension = filename.split('.').pop()?.toLowerCase() || ''; - - const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(fileExtension); - const isPDF = fileExtension === 'pdf'; - const isCode = [ - 'py', 'js', 'jsx', 'ts', 'tsx', 'html', 'htm', 'css', 'scss', - 'java', 'c', 'cpp', 'cs', 'go', 'rs', 'sql', 'php', 'rb', - 'swift', 'kt', 'sh', 'bash', 'yaml', 'yml', 'json', 'md', - 'astro', 'vue', 'svelte', 'xml', 'toml', 'ini', 'env', - 'graphql', 'prisma', 'dockerfile', 'nginx' - ].includes(fileExtension); - const isText = isCode || ['txt', 'log', 'csv'].includes(fileExtension); - const isVideo = ['mp4', 'webm', 'mov', 'avi', 'mkv'].includes(fileExtension); - const isAudio = ['mp3', 'wav', 'm4a', 'ogg'].includes(fileExtension); - - // Function to highlight code using highlight.js - const highlightCode = (code: string, language?: string): string => { - if (!language) return code; - try { - return hljs.highlight(code, { language }).value; - } catch (error) { - console.warn(`Failed to highlight code for language ${language}:`, error); - return code; - } - }; - - // Function to get the appropriate language for highlight.js - const getHighlightLanguage = (ext: string): string | undefined => { - // Map file extensions to highlight.js languages - const languageMap: { [key: string]: string } = { - 'py': 'python', - 'js': 'javascript', - 'jsx': 'javascript', - 'ts': 'typescript', - 'tsx': 'typescript', - 'html': 'html', - 'htm': 'html', - 'css': 'css', - 'scss': 'scss', - 'java': 'java', - 'c': 'c', - 'cpp': 'cpp', - 'cs': 'csharp', - 'go': 'go', - 'rs': 'rust', - 'sql': 'sql', - 'php': 'php', - 'rb': 'ruby', - 'swift': 'swift', - 'kt': 'kotlin', - 'sh': 'bash', - 'bash': 'bash', - 'yaml': 'yaml', - 'yml': 'yaml', - 'json': 'json', - 'md': 'markdown', - 'xml': 'xml', - 'toml': 'ini', - 'ini': 'ini', - 'dockerfile': 'dockerfile', - 'prisma': 'prisma', - 'graphql': 'graphql' - }; - return languageMap[ext]; - }; - - // Function to truncate text content - const truncateContent = (text: string, maxLines: number): string => { - const lines = text.split('\n'); - if (lines.length <= maxLines) return text; - - const truncatedLines = lines.slice(0, maxLines).map(line => - line.length > MAX_CHARS_PER_LINE - ? line.slice(0, MAX_CHARS_PER_LINE) + '...' - : line - ); - return truncatedLines.join('\n') + '\n' + TRUNCATION_MESSAGE; - }; - - // Reset visible lines when file changes - React.useEffect(() => { - setVisibleLines(INITIAL_LINES); - }, [url]); - - // Function to show more lines - const showMoreLines = () => { - setVisibleLines(prev => prev + INCREMENT_LINES); - }; - - // Function to reset to initial view - const resetView = () => { - setVisibleLines(INITIAL_LINES); - }; - - // Listen for custom events to update the preview - React.useEffect(() => { - const element = elementRef.current; - if (!element) return; - - const handleUpdatePreview = (e: CustomEvent<{ url: string; filename: string }>) => { - setUrl(e.detail.url); - setFilename(e.detail.filename); - }; - - element.addEventListener('updateFilePreview', handleUpdatePreview as EventListener); - return () => { - element.removeEventListener('updateFilePreview', handleUpdatePreview as EventListener); - }; - }, []); - - // Update state when props change - React.useEffect(() => { - setUrl(initialUrl); - setFilename(initialFilename); - }, [initialUrl, initialFilename]); - - // For text files, we need to fetch and display the content - const [textContent, setTextContent] = React.useState(''); - const [isLoading, setIsLoading] = React.useState(false); - const [error, setError] = React.useState(null); - - React.useEffect(() => { - async function fetchTextContent() { - if (!isText) return; - - setIsLoading(true); - setError(null); + useEffect(() => { + const modal = document.getElementById('filePreviewModal') as HTMLDialogElement; + setModalElement(modal); + const initializeViewer = async () => { try { - const response = await fetch(url); - const text = await response.text(); - setTextContent(text); + 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 text content'); - console.error('Error fetching text content:', err); - } finally { - setIsLoading(false); + setError("Failed to load file preview"); + setLoading(false); } - } - - if (isText) { - fetchTextContent(); - } - }, [url, isText]); - - // Function to parse CSV text into array - const parseCSV = (text: string): string[][] => { - const rows = text.split(/\r?\n/).filter(row => row.trim()); - return rows.map(row => { - // Handle both quoted and unquoted CSV - const matches = row.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || []; - return matches.map(cell => cell.replace(/^"|"$/g, '').trim()); - }); - }; - - // Function to format JSON with syntax highlighting - const formatJSON = (text: string): string => { - try { - const parsed = JSON.parse(text); - return highlightCode(JSON.stringify(parsed, null, 2), 'json'); - } catch { - return text; // Return original text if not valid JSON - } - }; - - // Function to render CSV as table - const renderCSVTable = (csvData: string[][]): JSXElement => { - if (csvData.length === 0) return

No data

; - - const headers = csvData[0]; - const allRows = csvData.slice(1); - const rows = allRows.slice(0, visibleLines); - const remainingRows = allRows.length - visibleLines; - const hasMore = remainingRows > 0; - - return ( -
-
- - - - {headers.map((header, i) => ( - - ))} - - - - {rows.map((row, i) => ( - - {row.map((cell, j) => ( - - ))} - - ))} - -
{header}
{cell}
-
-
- {hasMore && ( - - )} - {visibleLines > INITIAL_LINES && ( - - )} -
-
- ); - }; - - // Function to render text content based on file type - const renderTextContent = (): JSXElement => { - if (!textContent) return

No content

; - - if (fileExtension === 'csv') { - const csvData = parseCSV(textContent); - return renderCSVTable(csvData); - } - - const lines = textContent.split('\n'); - const content = truncateContent(textContent, visibleLines); - const remainingLines = lines.length - visibleLines; - const hasMore = remainingLines > 0; - - const renderContent = () => { - if (isCode) { - const language = getHighlightLanguage(fileExtension); - const highlightedCode = highlightCode(content, language); - return ( - - ); - } - - return {content}; }; - return ( -
-
-
-                        {renderContent()}
-                    
-
-
- {hasMore && ( - - )} - {visibleLines > INITIAL_LINES && ( - - )} -
-
- ); + // 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?.(); }; - if (isLoading) { - return ( -
- -
- ); - } + const [fileContent, setFileContent] = useState(null); - if (error) { - return ( -
- - - - {error} -
- ); - } + 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 (isImage) { - return ( -
- {filename} setError('Failed to load image')} - /> -
- ); - } + 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'; + }; - if (isPDF) { - return ( -
-