This commit is contained in:
chark1es 2025-03-08 02:01:49 -08:00
parent f641ee722a
commit 1b936a9410
2 changed files with 372 additions and 253 deletions

View file

@ -192,26 +192,113 @@ import EventLoad from "./EventsSection/EventLoad";
// Universal file preview function for events section // Universal file preview function for events section
window.previewFileEvents = function (url: string, filename: string) { window.previewFileEvents = function (url: string, filename: string) {
console.log("previewFileEvents called with:", { url, filename }); 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( const modal = document.getElementById(
"filePreviewModal" "filePreviewModal"
) as HTMLDialogElement; ) as HTMLDialogElement;
const previewFileName = document.getElementById("previewFileName"); const previewFileName = document.getElementById("previewFileName");
const previewContent = document.getElementById("previewContent"); const previewContent = document.getElementById("previewContent");
const loadingSpinner = document.getElementById("previewLoadingSpinner");
if (modal && previewFileName && previewContent) { if (modal && previewFileName && previewContent) {
console.log("Found all required elements"); console.log("Found all required elements");
// Show loading spinner
if (loadingSpinner) {
loadingSpinner.classList.remove("hidden");
}
// Update the filename display // Update the filename display
previewFileName.textContent = filename; previewFileName.textContent = filename;
// Show the modal // Show the modal
modal.showModal(); modal.showModal();
// Dispatch state change event // Test the URL with a fetch before dispatching the event
window.dispatchEvent( fetch(url, { method: "HEAD" })
new CustomEvent("filePreviewStateChange", { .then((response) => {
detail: { url, filename }, 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; ) as HTMLDialogElement;
const previewFileName = document.getElementById("previewFileName"); const previewFileName = document.getElementById("previewFileName");
const previewContent = document.getElementById("previewContent"); const previewContent = document.getElementById("previewContent");
const loadingSpinner = document.getElementById("previewLoadingSpinner");
if (loadingSpinner) {
loadingSpinner.classList.add("hidden");
}
if (modal && previewFileName && previewContent) { if (modal && previewFileName && previewContent) {
console.log("Resetting preview and closing modal"); 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( window.dispatchEvent(
new CustomEvent("filePreviewStateChange", { new CustomEvent("filePreviewStateChange", {
detail: { url: "", filename: "" }, detail: { url: "", filename: "" },
@ -238,6 +332,10 @@ import EventLoad from "./EventsSection/EventLoad";
// Close the modal // Close the modal
modal.close(); 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; name: string;
}) { }) {
console.log("showFilePreviewEvents called with:", file); 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); window.previewFileEvents(file.url, file.name);
}; };
@ -301,17 +404,19 @@ import EventLoad from "./EventsSection/EventLoad";
<tbody> <tbody>
${event.files ${event.files
.map((file: string) => { .map((file: string) => {
// Ensure the file URL is properly formatted
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${file}`; const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${file}`;
const fileType = getFileType(file); const fileType = getFileType(file);
const previewData = JSON.stringify({ // Properly escape the data for the onclick handler
const fileData = {
url: fileUrl, url: fileUrl,
name: file, name: file,
}).replace(/'/g, "\\'"); };
return ` return `
<tr> <tr>
<td>${file}</td> <td>${file}</td>
<td class="text-right"> <td class="text-right">
<button class="btn btn-ghost btn-sm" onclick='window.showFilePreviewEvents(${previewData})'> <button class="btn btn-ghost btn-sm" onclick="window.showFilePreviewEvents({'url': '${fileUrl}', 'name': '${file}'})">
<iconify-icon icon="heroicons:document" className="h-4 w-4" /> <iconify-icon icon="heroicons:document" className="h-4 w-4" />
</button> </button>
<a href="${fileUrl}" download="${file}" class="btn btn-ghost btn-sm"> <a href="${fileUrl}" download="${file}" class="btn btn-ghost btn-sm">

View file

@ -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 { Icon } from "@iconify/react";
import hljs from 'highlight.js'; import hljs from 'highlight.js';
import 'highlight.js/styles/github-dark.css'; import 'highlight.js/styles/github-dark.css';
import { Authentication } from '../../../scripts/pocketbase/Authentication'; 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<string>(url);
const [isObjectUrl, setIsObjectUrl] = useState<boolean>(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 (
<img
src={imgSrc}
alt={filename}
className="max-w-full h-auto rounded-lg"
loading="lazy"
onError={handleError}
/>
);
};
// Cache for file content // Cache for file content
const contentCache = new Map<string, { content: string | 'image' | 'video' | 'pdf', fileType: string, timestamp: number }>(); const contentCache = new Map<string, { content: string | 'image' | 'video' | 'pdf', fileType: string, timestamp: number }>();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes 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) { export default function FilePreview({ url: initialUrl = '', filename: initialFilename = '', isModal = false }: FilePreviewProps) {
const [url, setUrl] = useState(initialUrl); // Constants
const [filename, setFilename] = useState(initialFilename); const CHUNK_SIZE = 50;
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);
const [isVisible, setIsVisible] = useState(false);
const [visibleLines, setVisibleLines] = useState(20);
const CHUNK_SIZE = 50; // Number of additional lines to show when expanding
const INITIAL_LINES_TO_SHOW = 20; 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 // Memoize the truncated filename
const truncatedFilename = useMemo(() => { const truncatedFilename = useMemo(() => {
if (!filename) return ''; if (!state.filename) return '';
const maxLength = 40; const maxLength = 40;
if (filename.length <= maxLength) return filename; if (state.filename.length <= maxLength) return state.filename;
const extension = filename.split('.').pop(); const extension = state.filename.split('.').pop();
const name = filename.substring(0, filename.lastIndexOf('.')); const name = state.filename.substring(0, state.filename.lastIndexOf('.'));
const truncatedName = name.substring(0, maxLength - 3 - (extension?.length || 0)); const truncatedName = name.substring(0, maxLength - 3 - (extension?.length || 0));
return `${truncatedName}...${extension ? `.${extension}` : ''}`; return `${truncatedName}...${extension ? `.${extension}` : ''}`;
}, [filename]); }, [state.filename]);
// Update URL and filename when props change // Update ref when props change
useEffect(() => { useEffect(() => {
console.log('FilePreview props changed:', { initialUrl, initialFilename }); latestPropsRef.current = { url: initialUrl, filename: initialFilename };
if (initialUrl !== url) { // Clear state when URL changes
console.log('URL changed from props:', initialUrl); setState(prev => ({
setUrl(initialUrl); ...prev,
} url: initialUrl,
if (initialFilename !== filename) { filename: initialFilename,
console.log('Filename changed from props:', initialFilename); content: null,
setFilename(initialFilename); error: null,
} fileType: null,
}, [initialUrl, initialFilename, url, filename]); 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(() => { useEffect(() => {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
([entry]) => { ([entry]) => {
setIsVisible(entry.isIntersecting); setState(prev => ({ ...prev, isVisible: entry.isIntersecting }));
}, },
{ threshold: 0.1 } { threshold: 0.1 }
); );
// Target the entire component instead of just preview-content
const previewElement = document.querySelector('.file-preview-container'); const previewElement = document.querySelector('.file-preview-container');
if (previewElement) { if (previewElement) {
observer.observe(previewElement); observer.observe(previewElement);
@ -68,180 +204,14 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
return () => observer.disconnect(); 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 () => { const handleDownload = async () => {
try { try {
const response = await fetch(url); const response = await fetch(state.url);
const blob = await response.blob(); const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob); const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
link.href = downloadUrl; link.href = downloadUrl;
link.download = filename; link.download = state.filename;
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
@ -305,7 +275,7 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
const renderCSVTable = useCallback((csvContent: string) => { const renderCSVTable = useCallback((csvContent: string) => {
const { headers, rows } = parseCSV(csvContent); const { headers, rows } = parseCSV(csvContent);
const totalRows = rows.length; const totalRows = rows.length;
const rowsToShow = Math.min(visibleLines, totalRows); const rowsToShow = Math.min(state.visibleLines, totalRows);
const displayedRows = rows.slice(0, rowsToShow); const displayedRows = rows.slice(0, rowsToShow);
return ` return `
@ -333,7 +303,7 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
</table> </table>
</div> </div>
`; `;
}, [visibleLines]); }, [state.visibleLines]);
const formatCodeWithLineNumbers = useCallback((code: string, language: string) => { const formatCodeWithLineNumbers = useCallback((code: string, language: string) => {
try { try {
@ -371,15 +341,18 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
}, []); }, []);
const handleShowMore = useCallback(() => { const handleShowMore = useCallback(() => {
setVisibleLines(prev => Math.min(prev + CHUNK_SIZE, content?.split('\n').length || 0)); setState(prev => ({
}, [content]); ...prev,
visibleLines: Math.min(prev.visibleLines + CHUNK_SIZE, (prev.content as string).split('\n').length)
}));
}, []);
const handleShowLess = useCallback(() => { 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 is empty, show a message
if (!url) { if (!state.url) {
return ( return (
<div className="file-preview-container bg-base-100 rounded-lg shadow-md overflow-hidden"> <div className="file-preview-container bg-base-100 rounded-lg shadow-md overflow-hidden">
<div className="p-6 flex flex-col items-center justify-center text-center"> <div className="p-6 flex flex-col items-center justify-center text-center">
@ -395,9 +368,9 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
<div className="file-preview-container space-y-4"> <div className="file-preview-container space-y-4">
<div className="flex justify-between items-center bg-base-200 p-3 rounded-t-lg"> <div className="flex justify-between items-center bg-base-200 p-3 rounded-t-lg">
<div className="flex items-center gap-2 flex-1 min-w-0"> <div className="flex items-center gap-2 flex-1 min-w-0">
<span className="truncate font-medium" title={filename}>{truncatedFilename}</span> <span className="truncate font-medium" title={state.filename}>{truncatedFilename}</span>
{fileType && ( {state.fileType && (
<span className="badge badge-sm whitespace-nowrap">{fileType.split('/')[1]}</span> <span className="badge badge-sm whitespace-nowrap">{state.fileType.split('/')[1]}</span>
)} )}
</div> </div>
<button <button
@ -410,20 +383,20 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
</div> </div>
<div className="preview-content overflow-auto border border-base-300 rounded-lg bg-base-200/50 relative"> <div className="preview-content overflow-auto border border-base-300 rounded-lg bg-base-200/50 relative">
{loading && ( {state.loading && (
<div className="flex justify-center items-center p-8"> <div className="flex justify-center items-center p-8">
<span className="loading loading-spinner loading-lg"></span> <span className="loading loading-spinner loading-lg"></span>
</div> </div>
)} )}
{error && ( {state.error && (
<div className="flex flex-col items-center justify-center p-8 bg-base-200 rounded-lg text-center space-y-4"> <div className="flex flex-col items-center justify-center p-8 bg-base-200 rounded-lg text-center space-y-4">
<div className="bg-warning/20 p-4 rounded-full"> <div className="bg-warning/20 p-4 rounded-full">
<Icon icon="mdi:alert" className="h-12 w-12 text-warning" /> <Icon icon="mdi:alert" className="h-12 w-12 text-warning" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-lg font-semibold">Preview Unavailable</h3> <h3 className="text-lg font-semibold">Preview Unavailable</h3>
<p className="text-base-content/70 max-w-md">{error}</p> <p className="text-base-content/70 max-w-md">{state.error}</p>
</div> </div>
<button <button
onClick={handleDownload} onClick={handleDownload}
@ -441,56 +414,97 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
</div> </div>
)} )}
{!loading && !error && content === 'image' && ( {!state.loading && !state.error && state.content === 'image' && (
<div className="flex justify-center bg-base-200 p-4 rounded-b-lg"> <div className="flex justify-center bg-base-200 p-4 rounded-b-lg">
<img <ImageWithFallback
src={url} url={state.url}
alt={filename} filename={state.filename}
className="max-w-full h-auto rounded-lg" onError={(message) => setState(prev => ({ ...prev, error: message }))}
loading="lazy"
onError={(e) => {
console.error('Image failed to load:', e);
setError('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'
);
}}
/> />
</div> </div>
)} )}
{!loading && !error && content === 'video' && ( {!state.loading && !state.error && state.content === 'video' && (
<div className="flex justify-center bg-base-200 p-4 rounded-b-lg"> <div className="flex justify-center bg-base-200 p-4 rounded-b-lg">
<video <video
controls controls
className="max-w-full h-auto rounded-lg" className="max-w-full h-auto rounded-lg"
preload="metadata" preload="metadata"
onError={(e) => {
console.error('Video failed to load:', e);
setState(prev => ({
...prev,
error: 'Failed to load video. This might be due to permission issues or the file may not exist.'
}));
}}
> >
<source src={url} type={fileType || 'video/mp4'} /> <source src={state.url} type={state.fileType || 'video/mp4'} />
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
</div> </div>
)} )}
{!loading && !error && content === 'pdf' && ( {!state.loading && !state.error && state.content === 'pdf' && (
<div className="w-full h-[600px] bg-base-200 p-4 rounded-b-lg"> <div className="w-full h-[600px] bg-base-200 p-4 rounded-b-lg">
<iframe <div className="w-full h-full rounded-lg overflow-hidden">
src={url} {/* Use object tag instead of iframe for better PDF support */}
className="w-full h-full rounded-lg" <object
title={filename} data={state.url}
loading="lazy" type="application/pdf"
></iframe> className="w-full h-full rounded-lg"
onError={(e) => {
console.error('PDF object failed to load:', e);
// Create a fallback div with a download link
const obj = e.target as HTMLObjectElement;
const container = obj.parentElement;
if (container) {
container.innerHTML = `
<div class="flex flex-col items-center justify-center h-full bg-base-200 p-6 text-center">
<div class="bg-warning/20 p-4 rounded-full mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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>
</div>
<h3 class="text-lg font-semibold mb-2">PDF Preview Unavailable</h3>
<p class="text-base-content/70 mb-4">The PDF cannot be displayed in the browser due to security restrictions.</p>
<a href="${state.url}" download="${state.filename}" class="btn btn-primary gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Download PDF Instead
</a>
</div>
`;
}
}}
>
<div className="flex flex-col items-center justify-center h-full bg-base-200 p-6 text-center">
<div className="bg-warning/20 p-4 rounded-full mb-4">
<svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 text-warning" 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>
</div>
<h3 className="text-lg font-semibold mb-2">PDF Preview Unavailable</h3>
<p className="text-base-content/70 mb-4">Your browser cannot display this PDF or it failed to load.</p>
<a href={state.url} download={state.filename} className="btn btn-primary gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Download PDF Instead
</a>
</div>
</object>
</div>
</div> </div>
)} )}
{!loading && !error && content && !['image', 'video', 'pdf'].includes(content) && ( {!state.loading && !state.error && state.content && !['image', 'video', 'pdf'].includes(state.content) && (
<div className="overflow-x-auto max-h-[600px] bg-base-200"> <div className="overflow-x-auto max-h-[600px] bg-base-200">
<div className={`p-1 ${filename.toLowerCase().endsWith('.csv') ? 'p-4' : ''}`}> <div className={`p-1 ${state.filename.toLowerCase().endsWith('.csv') ? 'p-4' : ''}`}>
{filename.toLowerCase().endsWith('.csv') ? ( {state.filename.toLowerCase().endsWith('.csv') ? (
<div dangerouslySetInnerHTML={{ __html: renderCSVTable(content) }} /> <div dangerouslySetInnerHTML={{ __html: renderCSVTable(state.content) }} />
) : ( ) : (
<> <>
<div className="file-preview-code-container text-sm"> <div className="file-preview-code-container text-sm">
@ -521,24 +535,24 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
<div <div
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: formatCodeWithLineNumbers( __html: formatCodeWithLineNumbers(
content.split('\n').slice(0, visibleLines).join('\n'), state.content.split('\n').slice(0, state.visibleLines).join('\n'),
getLanguageFromFilename(filename) getLanguageFromFilename(state.filename)
) )
}} }}
/> />
</div> </div>
{content.split('\n').length > visibleLines && ( {state.content.split('\n').length > state.visibleLines && (
<div className="flex justify-center p-2 border-t border-base-300 bg-base-200/50"> <div className="flex justify-center p-2 border-t border-base-300 bg-base-200/50">
{visibleLines < content.split('\n').length && ( {state.visibleLines < state.content.split('\n').length && (
<button <button
className="btn btn-sm btn-ghost gap-1" className="btn btn-sm btn-ghost gap-1"
onClick={handleShowMore} onClick={handleShowMore}
> >
<Icon icon="mdi:chevron-down" className="h-4 w-4" /> <Icon icon="mdi:chevron-down" className="h-4 w-4" />
Show {Math.min(CHUNK_SIZE, content.split('\n').length - visibleLines)} more lines Show {Math.min(CHUNK_SIZE, state.content.split('\n').length - state.visibleLines)} more lines
</button> </button>
)} )}
{visibleLines > INITIAL_LINES_TO_SHOW && ( {state.visibleLines > INITIAL_LINES_TO_SHOW && (
<button <button
className="btn btn-sm btn-ghost gap-1 ml-2" className="btn btn-sm btn-ghost gap-1 ml-2"
onClick={handleShowLess} onClick={handleShowLess}