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
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";
<tbody>
${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 `
<tr>
<td>${file}</td>
<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" />
</button>
<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 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<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
const contentCache = new Map<string, { content: string | 'image' | 'video' | 'pdf', fileType: string, timestamp: number }>();
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<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
// 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
</table>
</div>
`;
}, [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 (
<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">
@ -395,9 +368,9 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
<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 items-center gap-2 flex-1 min-w-0">
<span className="truncate font-medium" title={filename}>{truncatedFilename}</span>
{fileType && (
<span className="badge badge-sm whitespace-nowrap">{fileType.split('/')[1]}</span>
<span className="truncate font-medium" title={state.filename}>{truncatedFilename}</span>
{state.fileType && (
<span className="badge badge-sm whitespace-nowrap">{state.fileType.split('/')[1]}</span>
)}
</div>
<button
@ -410,20 +383,20 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
</div>
<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">
<span className="loading loading-spinner loading-lg"></span>
</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="bg-warning/20 p-4 rounded-full">
<Icon icon="mdi:alert" className="h-12 w-12 text-warning" />
</div>
<div className="space-y-2">
<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>
<button
onClick={handleDownload}
@ -441,56 +414,97 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
</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">
<img
src={url}
alt={filename}
className="max-w-full h-auto rounded-lg"
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'
);
}}
<ImageWithFallback
url={state.url}
filename={state.filename}
onError={(message) => setState(prev => ({ ...prev, error: message }))}
/>
</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">
<video
controls
className="max-w-full h-auto rounded-lg"
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.
</video>
</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">
<iframe
src={url}
className="w-full h-full rounded-lg"
title={filename}
loading="lazy"
></iframe>
<div className="w-full h-full rounded-lg overflow-hidden">
{/* Use object tag instead of iframe for better PDF support */}
<object
data={state.url}
type="application/pdf"
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>
)}
{!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={`p-1 ${filename.toLowerCase().endsWith('.csv') ? 'p-4' : ''}`}>
{filename.toLowerCase().endsWith('.csv') ? (
<div dangerouslySetInnerHTML={{ __html: renderCSVTable(content) }} />
<div className={`p-1 ${state.filename.toLowerCase().endsWith('.csv') ? 'p-4' : ''}`}>
{state.filename.toLowerCase().endsWith('.csv') ? (
<div dangerouslySetInnerHTML={{ __html: renderCSVTable(state.content) }} />
) : (
<>
<div className="file-preview-code-container text-sm">
@ -521,24 +535,24 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
<div
dangerouslySetInnerHTML={{
__html: formatCodeWithLineNumbers(
content.split('\n').slice(0, visibleLines).join('\n'),
getLanguageFromFilename(filename)
state.content.split('\n').slice(0, state.visibleLines).join('\n'),
getLanguageFromFilename(state.filename)
)
}}
/>
</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">
{visibleLines < content.split('\n').length && (
{state.visibleLines < state.content.split('\n').length && (
<button
className="btn btn-sm btn-ghost gap-1"
onClick={handleShowMore}
>
<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>
)}
{visibleLines > INITIAL_LINES_TO_SHOW && (
{state.visibleLines > INITIAL_LINES_TO_SHOW && (
<button
className="btn btn-sm btn-ghost gap-1 ml-2"
onClick={handleShowLess}