maintenance improvements
This commit is contained in:
@@ -49,6 +49,25 @@ export default function SystemSettings({}: SystemSettingsProps) {
|
||||
execute: { loading: false, message: '' }
|
||||
});
|
||||
|
||||
const [hoveredImage, setHoveredImage] = useState<{ src: string; alt: string } | null>(null);
|
||||
const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
|
||||
const handleImageHover = (filePath: string, fileName: string, event: React.MouseEvent) => {
|
||||
// Convert backend file path to frontend image URL
|
||||
const imageUrl = filePath.replace(/^.*\/images\//, '/images/');
|
||||
setHoveredImage({ src: imageUrl, alt: fileName });
|
||||
setMousePosition({ x: event.clientX, y: event.clientY });
|
||||
};
|
||||
|
||||
const handleImageLeave = () => {
|
||||
setHoveredImage(null);
|
||||
};
|
||||
|
||||
const isImageFile = (fileName: string): boolean => {
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'];
|
||||
return imageExtensions.some(ext => fileName.toLowerCase().endsWith(ext));
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleCompleteBackup = async () => {
|
||||
@@ -231,13 +250,7 @@ export default function SystemSettings({}: SystemSettingsProps) {
|
||||
}));
|
||||
}
|
||||
|
||||
// Clear message after 10 seconds
|
||||
setTimeout(() => {
|
||||
setCleanupStatus(prev => ({
|
||||
...prev,
|
||||
preview: { loading: false, message: '', success: undefined }
|
||||
}));
|
||||
}, 10000);
|
||||
// Note: Preview message no longer auto-clears to allow users to review file details
|
||||
};
|
||||
|
||||
const handleImageCleanupExecute = async () => {
|
||||
@@ -614,6 +627,18 @@ export default function SystemSettings({}: SystemSettingsProps) {
|
||||
>
|
||||
{cleanupStatus.execute.loading ? 'Cleaning...' : 'Execute Cleanup'}
|
||||
</Button>
|
||||
{cleanupStatus.preview.message && (
|
||||
<Button
|
||||
onClick={() => setCleanupStatus(prev => ({
|
||||
...prev,
|
||||
preview: { loading: false, message: '', success: undefined, data: undefined }
|
||||
}))}
|
||||
variant="ghost"
|
||||
className="px-4 py-2 text-sm"
|
||||
>
|
||||
Clear Preview
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Preview Results */}
|
||||
@@ -667,6 +692,76 @@ export default function SystemSettings({}: SystemSettingsProps) {
|
||||
<span className="font-medium">Referenced Images:</span> {cleanupStatus.preview.data.referencedImagesCount}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detailed File List */}
|
||||
{cleanupStatus.preview.data.orphanedFiles && cleanupStatus.preview.data.orphanedFiles.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<details className="cursor-pointer">
|
||||
<summary className="font-medium text-sm theme-header mb-2">
|
||||
📁 View Files to be Deleted ({cleanupStatus.preview.data.orphanedFiles.length})
|
||||
</summary>
|
||||
<div className="mt-3 max-h-96 overflow-y-auto border theme-border rounded">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-gray-100 dark:bg-gray-800 sticky top-0">
|
||||
<tr>
|
||||
<th className="text-left p-2 font-medium">File Name</th>
|
||||
<th className="text-left p-2 font-medium">Size</th>
|
||||
<th className="text-left p-2 font-medium">Story</th>
|
||||
<th className="text-left p-2 font-medium">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cleanupStatus.preview.data.orphanedFiles.map((file: any, index: number) => (
|
||||
<tr key={index} className="border-t theme-border hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td className="p-2">
|
||||
<div
|
||||
className={`truncate max-w-xs ${isImageFile(file.fileName) ? 'cursor-pointer text-blue-600 dark:text-blue-400' : ''}`}
|
||||
title={file.fileName}
|
||||
onMouseEnter={isImageFile(file.fileName) ? (e) => handleImageHover(file.filePath, file.fileName, e) : undefined}
|
||||
onMouseMove={isImageFile(file.fileName) ? (e) => setMousePosition({ x: e.clientX, y: e.clientY }) : undefined}
|
||||
onMouseLeave={isImageFile(file.fileName) ? handleImageLeave : undefined}
|
||||
>
|
||||
{isImageFile(file.fileName) && '🖼️ '}{file.fileName}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 truncate max-w-xs" title={file.filePath}>
|
||||
{file.filePath}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-2">{file.formattedSize}</td>
|
||||
<td className="p-2">
|
||||
{file.storyExists && file.storyTitle ? (
|
||||
<a
|
||||
href={`/stories/${file.storyId}`}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline truncate max-w-xs block"
|
||||
title={file.storyTitle}
|
||||
>
|
||||
{file.storyTitle}
|
||||
</a>
|
||||
) : file.storyId !== 'unknown' && file.storyId !== 'error' ? (
|
||||
<span className="text-gray-500" title={`Story ID: ${file.storyId}`}>
|
||||
Deleted Story
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-400">Unknown</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{file.storyExists ? (
|
||||
<span className="text-orange-600 dark:text-orange-400 text-xs">Orphaned</span>
|
||||
) : file.storyId !== 'unknown' && file.storyId !== 'error' ? (
|
||||
<span className="text-red-600 dark:text-red-400 text-xs">Story Deleted</span>
|
||||
) : (
|
||||
<span className="text-gray-500 text-xs">Unknown Folder</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -787,6 +882,31 @@ export default function SystemSettings({}: SystemSettingsProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Preview Overlay */}
|
||||
{hoveredImage && (
|
||||
<div
|
||||
className="fixed pointer-events-none z-50 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-lg shadow-xl p-2 max-w-sm"
|
||||
style={{
|
||||
left: mousePosition.x + 10,
|
||||
top: mousePosition.y - 100,
|
||||
transform: mousePosition.x > window.innerWidth - 300 ? 'translateX(-100%)' : 'none'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={hoveredImage.src}
|
||||
alt={hoveredImage.alt}
|
||||
className="max-w-full max-h-64 object-contain rounded"
|
||||
onError={() => {
|
||||
// Hide preview if image fails to load
|
||||
setHoveredImage(null);
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs theme-text mt-1 truncate">
|
||||
{hoveredImage.alt}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user