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
|
// 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">
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Reference in a new issue