fix file
This commit is contained in:
parent
f641ee722a
commit
1b936a9410
2 changed files with 372 additions and 253 deletions
|
@ -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
|
||||
// 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">
|
||||
|
|
|
@ -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}
|
||||
<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"
|
||||
title={filename}
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
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}
|
||||
|
|
Loading…
Reference in a new issue