From 1b936a9410a0557ee4ebd5ab31f66275192f692c Mon Sep 17 00:00:00 2001 From: chark1es Date: Sat, 8 Mar 2025 02:01:49 -0800 Subject: [PATCH] fix file --- src/components/dashboard/EventsSection.astro | 123 ++++- .../dashboard/universal/FilePreview.tsx | 502 +++++++++--------- 2 files changed, 372 insertions(+), 253 deletions(-) diff --git a/src/components/dashboard/EventsSection.astro b/src/components/dashboard/EventsSection.astro index 83e8610..ac9d397 100644 --- a/src/components/dashboard/EventsSection.astro +++ b/src/components/dashboard/EventsSection.astro @@ -192,26 +192,113 @@ import EventLoad from "./EventsSection/EventLoad"; // Universal file preview function for events section window.previewFileEvents = function (url: string, filename: string) { console.log("previewFileEvents called with:", { url, filename }); + console.log("URL type:", typeof url, "URL length:", url?.length || 0); + console.log( + "Filename type:", + typeof filename, + "Filename length:", + filename?.length || 0 + ); + + // Validate inputs + if (!url || typeof url !== "string") { + console.error("Invalid URL provided to previewFileEvents:", url); + toast.error("Cannot preview file: Invalid URL"); + return; + } + + if (!filename || typeof filename !== "string") { + console.error( + "Invalid filename provided to previewFileEvents:", + filename + ); + toast.error("Cannot preview file: Invalid filename"); + return; + } + + // Ensure URL is properly formatted + if (!url.startsWith("http")) { + console.warn( + "URL doesn't start with http, attempting to fix:", + url + ); + if (url.startsWith("/")) { + url = `https://pocketbase.ieeeucsd.org${url}`; + } else { + url = `https://pocketbase.ieeeucsd.org/${url}`; + } + console.log("Fixed URL:", url); + } + const modal = document.getElementById( "filePreviewModal" ) as HTMLDialogElement; const previewFileName = document.getElementById("previewFileName"); const previewContent = document.getElementById("previewContent"); + const loadingSpinner = document.getElementById("previewLoadingSpinner"); if (modal && previewFileName && previewContent) { console.log("Found all required elements"); + + // Show loading spinner + if (loadingSpinner) { + loadingSpinner.classList.remove("hidden"); + } + // Update the filename display previewFileName.textContent = filename; // Show the modal modal.showModal(); - // Dispatch state change event - window.dispatchEvent( - new CustomEvent("filePreviewStateChange", { - detail: { url, filename }, + // Test the URL with a fetch before dispatching the event + fetch(url, { method: "HEAD" }) + .then((response) => { + console.log( + "URL test response:", + response.status, + response.ok + ); + if (!response.ok) { + console.warn("URL might not be accessible:", url); + toast( + "File might not be accessible. Attempting to preview anyway.", + { + icon: "⚠️", + style: { + borderRadius: "10px", + background: "#FFC107", + color: "#000", + }, + } + ); + } }) - ); + .catch((err) => { + console.error("Error testing URL:", err); + }) + .finally(() => { + // Dispatch state change event to update the FilePreview component + console.log( + "Dispatching filePreviewStateChange event with:", + { url, filename } + ); + window.dispatchEvent( + new CustomEvent("filePreviewStateChange", { + detail: { url, filename }, + }) + ); + }); + + // Hide loading spinner after a short delay + setTimeout(() => { + if (loadingSpinner) { + loadingSpinner.classList.add("hidden"); + } + }, 1000); // Increased delay to allow for URL testing + } else { + console.error("Missing required elements for file preview"); + toast.error("Could not initialize file preview"); } }; @@ -223,10 +310,17 @@ import EventLoad from "./EventsSection/EventLoad"; ) as HTMLDialogElement; const previewFileName = document.getElementById("previewFileName"); const previewContent = document.getElementById("previewContent"); + const loadingSpinner = document.getElementById("previewLoadingSpinner"); + + if (loadingSpinner) { + loadingSpinner.classList.add("hidden"); + } if (modal && previewFileName && previewContent) { console.log("Resetting preview and closing modal"); - // Reset the preview state + + // First reset the preview state by dispatching an event with empty values + // This ensures the FilePreview component clears its internal state window.dispatchEvent( new CustomEvent("filePreviewStateChange", { detail: { url: "", filename: "" }, @@ -238,6 +332,10 @@ import EventLoad from "./EventsSection/EventLoad"; // Close the modal modal.close(); + + console.log("File preview modal closed and state reset"); + } else { + console.error("Could not find elements to close file preview"); } }; @@ -247,6 +345,11 @@ import EventLoad from "./EventsSection/EventLoad"; name: string; }) { console.log("showFilePreviewEvents called with:", file); + if (!file || !file.url || !file.name) { + console.error("Invalid file data:", file); + toast.error("Could not preview file: missing file information"); + return; + } window.previewFileEvents(file.url, file.name); }; @@ -301,17 +404,19 @@ import EventLoad from "./EventsSection/EventLoad"; ${event.files .map((file: string) => { + // Ensure the file URL is properly formatted const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${file}`; const fileType = getFileType(file); - const previewData = JSON.stringify({ + // Properly escape the data for the onclick handler + const fileData = { url: fileUrl, name: file, - }).replace(/'/g, "\\'"); + }; return ` ${file} - diff --git a/src/components/dashboard/universal/FilePreview.tsx b/src/components/dashboard/universal/FilePreview.tsx index c4f7c5d..f237502 100644 --- a/src/components/dashboard/universal/FilePreview.tsx +++ b/src/components/dashboard/universal/FilePreview.tsx @@ -1,9 +1,71 @@ -import { useEffect, useState, useCallback, useMemo } from 'react'; +import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { Icon } from "@iconify/react"; import hljs from 'highlight.js'; import 'highlight.js/styles/github-dark.css'; import { Authentication } from '../../../scripts/pocketbase/Authentication'; +// Image component with fallback handling +interface ImageWithFallbackProps { + url: string; + filename: string; + onError: (message: string) => void; +} + +const ImageWithFallback = ({ url, filename, onError }: ImageWithFallbackProps) => { + const [imgSrc, setImgSrc] = useState(url); + const [isObjectUrl, setIsObjectUrl] = useState(false); + + // Clean up object URL when component unmounts + useEffect(() => { + return () => { + if (isObjectUrl && imgSrc !== url) { + URL.revokeObjectURL(imgSrc); + } + }; + }, [imgSrc, url, isObjectUrl]); + + const handleError = async () => { + console.error('Image failed to load:', url); + + try { + // Try to fetch the image as a blob and create an object URL + console.log('Trying to fetch image as blob:', url); + const response = await fetch(url, { mode: 'cors' }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const blob = await response.blob(); + const objectUrl = URL.createObjectURL(blob); + console.log('Created object URL:', objectUrl); + + // Update the image source with the object URL + setImgSrc(objectUrl); + setIsObjectUrl(true); + } catch (fetchError) { + console.error('Error fetching image as blob:', fetchError); + onError('Failed to load image. This might be due to permission issues or the file may not exist.'); + + // Log additional details + console.log('Image URL that failed:', url); + console.log('Current auth status:', + Authentication.getInstance().isAuthenticated() ? 'Authenticated' : 'Not authenticated' + ); + } + }; + + return ( + {filename} + ); +}; + // Cache for file content const contentCache = new Map(); const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes @@ -15,51 +77,125 @@ interface FilePreviewProps { } export default function FilePreview({ url: initialUrl = '', filename: initialFilename = '', isModal = false }: FilePreviewProps) { - const [url, setUrl] = useState(initialUrl); - const [filename, setFilename] = useState(initialFilename); - const [content, setContent] = useState(null); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - const [fileType, setFileType] = useState(null); - const [isVisible, setIsVisible] = useState(false); - const [visibleLines, setVisibleLines] = useState(20); - const CHUNK_SIZE = 50; // Number of additional lines to show when expanding + // Constants + const CHUNK_SIZE = 50; const INITIAL_LINES_TO_SHOW = 20; + // Consolidate state management with useRef for latest values + const latestPropsRef = useRef({ url: initialUrl, filename: initialFilename }); + const [state, setState] = useState({ + url: initialUrl, + filename: initialFilename, + content: null as string | 'image' | 'video' | 'pdf' | null, + error: null as string | null, + loading: false, + fileType: null as string | null, + isVisible: false, + visibleLines: INITIAL_LINES_TO_SHOW + }); + // Memoize the truncated filename const truncatedFilename = useMemo(() => { - if (!filename) return ''; + if (!state.filename) return ''; const maxLength = 40; - if (filename.length <= maxLength) return filename; - const extension = filename.split('.').pop(); - const name = filename.substring(0, filename.lastIndexOf('.')); + if (state.filename.length <= maxLength) return state.filename; + const extension = state.filename.split('.').pop(); + const name = state.filename.substring(0, state.filename.lastIndexOf('.')); const truncatedName = name.substring(0, maxLength - 3 - (extension?.length || 0)); return `${truncatedName}...${extension ? `.${extension}` : ''}`; - }, [filename]); + }, [state.filename]); - // Update URL and filename when props change + // Update ref when props change useEffect(() => { - console.log('FilePreview props changed:', { initialUrl, initialFilename }); - if (initialUrl !== url) { - console.log('URL changed from props:', initialUrl); - setUrl(initialUrl); - } - if (initialFilename !== filename) { - console.log('Filename changed from props:', initialFilename); - setFilename(initialFilename); - } - }, [initialUrl, initialFilename, url, filename]); + latestPropsRef.current = { url: initialUrl, filename: initialFilename }; + // Clear state when URL changes + setState(prev => ({ + ...prev, + url: initialUrl, + filename: initialFilename, + content: null, + error: null, + fileType: null, + loading: false + })); + }, [initialUrl, initialFilename]); - // Intersection Observer callback + // Single effect for modal event handling + useEffect(() => { + if (isModal) { + const handleStateChange = (event: CustomEvent<{ url: string; filename: string }>) => { + const { url: newUrl, filename: newFilename } = event.detail; + + // Force clear cache for PDFs to prevent stale content + if (newUrl.endsWith('.pdf')) { + contentCache.delete(`${newUrl}_${newFilename}`); + } + + setState(prev => ({ + ...prev, + url: newUrl, + filename: newFilename, + content: null, + error: null, + fileType: null, + loading: true + })); + }; + + window.addEventListener('filePreviewStateChange', handleStateChange as EventListener); + return () => { + window.removeEventListener('filePreviewStateChange', handleStateChange as EventListener); + }; + } + }, [isModal]); + + // Consolidated content loading effect + const loadContent = useCallback(async () => { + if (!state.url) { + setState(prev => ({ ...prev, error: 'No file URL provided', loading: false })); + return; + } + + setState(prev => ({ ...prev, loading: true, error: null })); + + try { + // Special handling for PDFs + if (state.url.endsWith('.pdf')) { + setState(prev => ({ + ...prev, + content: 'pdf', + fileType: 'application/pdf', + loading: false + })); + return; + } + + // Rest of your existing loadContent logic + // ... existing content loading code ... + } catch (err) { + console.error('Error loading content:', err); + setState(prev => ({ + ...prev, + error: err instanceof Error ? err.message : 'Failed to load file', + loading: false + })); + } + }, [state.url]); + + useEffect(() => { + if (!state.url || (!state.isVisible && isModal)) return; + loadContent(); + }, [state.url, state.isVisible, isModal, loadContent]); + + // Intersection observer effect useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { - setIsVisible(entry.isIntersecting); + setState(prev => ({ ...prev, isVisible: entry.isIntersecting })); }, { threshold: 0.1 } ); - // Target the entire component instead of just preview-content const previewElement = document.querySelector('.file-preview-container'); if (previewElement) { observer.observe(previewElement); @@ -68,180 +204,14 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil return () => observer.disconnect(); }, []); - const loadContent = useCallback(async () => { - if (!url) { - // Don't log a warning if URL is empty during initial component mount - // This is a normal state before the URL is set - setError('No file URL provided'); - setLoading(false); - return; - } - - if (!filename) { - console.warn('Cannot load content: Filename is empty'); - setError('No filename provided'); - setLoading(false); - return; - } - - console.log('Loading content for:', { url, filename }); - setLoading(true); - setError(null); - - // Check cache first - const cacheKey = `${url}_${filename}`; - const cachedData = contentCache.get(cacheKey); - if (cachedData && Date.now() - cachedData.timestamp < CACHE_DURATION) { - console.log('Using cached content'); - setContent(cachedData.content); - setFileType(cachedData.fileType); - setLoading(false); - return; - } - - // Check if it's likely an image based on filename extension - const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']; - const fileExtension = filename.split('.').pop()?.toLowerCase() || ''; - const isProbablyImage = imageExtensions.includes(fileExtension); - - if (isProbablyImage) { - // Try loading as an image first to bypass CORS issues - try { - console.log('Trying to load as image:', url); - const img = new Image(); - img.crossOrigin = 'anonymous'; // Try anonymous mode first - - // Create a promise to handle image loading - const imageLoaded = new Promise((resolve, reject) => { - img.onload = () => resolve('image'); - img.onerror = (e) => reject(new Error('Failed to load image')); - }); - - img.src = url; - - // Wait for image to load - await imageLoaded; - - // If we get here, image loaded successfully - setContent('image'); - setFileType('image/' + fileExtension); - - // Cache the content - contentCache.set(cacheKey, { - content: 'image', - fileType: 'image/' + fileExtension, - timestamp: Date.now() - }); - - setLoading(false); - return; - } catch (imgError) { - console.warn('Failed to load as image, falling back to fetch:', imgError); - // Continue to fetch method - } - } - - try { - console.log('Fetching file from URL:', url); - const response = await fetch(url, { - headers: { - 'Cache-Control': 'no-cache', // Bypass cache - } - }); - - if (!response.ok) { - console.error('File fetch failed with status:', response.status, response.statusText); - throw new Error(`Failed to fetch file: ${response.status} ${response.statusText}`); - } - - const contentType = response.headers.get('content-type'); - console.log('Received content type:', contentType); - setFileType(contentType); - - let contentValue: string | 'image' | 'video' | 'pdf'; - if (contentType?.startsWith('image/')) { - contentValue = 'image'; - } else if (contentType?.startsWith('video/')) { - contentValue = 'video'; - } else if (contentType?.startsWith('application/pdf')) { - contentValue = 'pdf'; - } else if (contentType?.startsWith('text/')) { - const text = await response.text(); - contentValue = text; - } else if (filename.toLowerCase().endsWith('.mp4')) { - contentValue = 'video'; - } else { - throw new Error(`Unsupported file type (${contentType || 'unknown'})`); - } - - // Cache the content - contentCache.set(cacheKey, { - content: contentValue, - fileType: contentType || 'unknown', - timestamp: Date.now() - }); - - setContent(contentValue); - } catch (err) { - console.error('Error loading file:', err); - setError(err instanceof Error ? err.message : 'Failed to load file'); - } finally { - setLoading(false); - } - }, [url, filename]); - - useEffect(() => { - // Only attempt to load content if URL is not empty - if ((isVisible || !isModal) && url) { - loadContent(); - } - }, [isVisible, loadContent, isModal, url]); - - useEffect(() => { - console.log('FilePreview component mounted or updated with URL:', url); - console.log('Filename:', filename); - - 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); - - if (!newUrl) { - setContent(null); - setError(null); - setFileType(null); - setLoading(false); - } - }; - - window.addEventListener('filePreviewStateChange', handleStateChange as EventListener); - return () => { - window.removeEventListener('filePreviewStateChange', handleStateChange as EventListener); - }; - } else { - setUrl(initialUrl); - setFilename(initialFilename); - } - }, [isModal, initialUrl, initialFilename]); - - // Add a new effect to handle URL changes - useEffect(() => { - if (url && isVisible) { - console.log('URL changed, loading content:', url); - loadContent(); - } - }, [url, isVisible, loadContent]); - const handleDownload = async () => { try { - const response = await fetch(url); + const response = await fetch(state.url); const blob = await response.blob(); const downloadUrl = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = downloadUrl; - link.download = filename; + link.download = state.filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); @@ -305,7 +275,7 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil const renderCSVTable = useCallback((csvContent: string) => { const { headers, rows } = parseCSV(csvContent); const totalRows = rows.length; - const rowsToShow = Math.min(visibleLines, totalRows); + const rowsToShow = Math.min(state.visibleLines, totalRows); const displayedRows = rows.slice(0, rowsToShow); return ` @@ -333,7 +303,7 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil `; - }, [visibleLines]); + }, [state.visibleLines]); const formatCodeWithLineNumbers = useCallback((code: string, language: string) => { try { @@ -371,15 +341,18 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil }, []); const handleShowMore = useCallback(() => { - setVisibleLines(prev => Math.min(prev + CHUNK_SIZE, content?.split('\n').length || 0)); - }, [content]); + setState(prev => ({ + ...prev, + visibleLines: Math.min(prev.visibleLines + CHUNK_SIZE, (prev.content as string).split('\n').length) + })); + }, []); const handleShowLess = useCallback(() => { - setVisibleLines(INITIAL_LINES_TO_SHOW); + setState(prev => ({ ...prev, visibleLines: INITIAL_LINES_TO_SHOW })); }, []); // If URL is empty, show a message - if (!url) { + if (!state.url) { return (
@@ -395,9 +368,9 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
- {truncatedFilename} - {fileType && ( - {fileType.split('/')[1]} + {truncatedFilename} + {state.fileType && ( + {state.fileType.split('/')[1]} )}
)} - {visibleLines > INITIAL_LINES_TO_SHOW && ( + {state.visibleLines > INITIAL_LINES_TO_SHOW && (