monacousa-portal/src/lib/components/documents/DocumentPreviewModal.svelte

248 lines
7.1 KiB
Svelte

<script lang="ts">
import { X, Download, ZoomIn, ZoomOut, RotateCw, Maximize2, FileText, Image, File } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button';
import { LoadingSpinner } from '$lib/components/ui';
interface Document {
id: string;
title: string;
file_name: string;
file_path: string;
mime_type: string;
file_size: number;
}
let { document, previewUrl, onClose }: {
document: Document;
previewUrl: string;
onClose: () => void;
} = $props();
let zoom = $state(100);
let rotation = $state(0);
let isLoading = $state(true);
let loadError = $state(false);
let textContent = $state<string | null>(null);
const isPdf = $derived(document.mime_type === 'application/pdf');
const isImage = $derived(document.mime_type.startsWith('image/'));
const isText = $derived(
document.mime_type.startsWith('text/') ||
['application/json', 'application/javascript', 'text/csv'].includes(document.mime_type)
);
const isOffice = $derived(
document.mime_type.includes('word') ||
document.mime_type.includes('excel') ||
document.mime_type.includes('spreadsheet') ||
document.mime_type.includes('powerpoint') ||
document.mime_type.includes('presentation')
);
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onClose();
}
}
function handleZoomIn() {
zoom = Math.min(zoom + 25, 300);
}
function handleZoomOut() {
zoom = Math.max(zoom - 25, 25);
}
function handleRotate() {
rotation = (rotation + 90) % 360;
}
function handleDownload() {
const link = document.createElement('a');
link.href = previewUrl;
link.download = document.file_name;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Load text content for text files
$effect(() => {
if (isText && previewUrl) {
fetch(previewUrl)
.then(res => res.text())
.then(text => {
textContent = text;
isLoading = false;
})
.catch(() => {
loadError = true;
isLoading = false;
});
}
});
</script>
<svelte:window onkeydown={handleKeydown} />
<!-- Modal Backdrop -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4"
role="dialog"
aria-modal="true"
aria-labelledby="preview-title"
>
<!-- Modal Container -->
<div class="relative flex h-full w-full max-w-6xl flex-col rounded-2xl bg-slate-900/95 shadow-2xl">
<!-- Header -->
<div class="flex items-center justify-between border-b border-slate-700/50 px-6 py-4">
<div class="flex items-center gap-3">
{#if isPdf}
<FileText class="h-6 w-6 text-red-400" />
{:else if isImage}
<Image class="h-6 w-6 text-blue-400" />
{:else}
<File class="h-6 w-6 text-slate-400" />
{/if}
<div>
<h2 id="preview-title" class="text-lg font-semibold text-white">{document.title}</h2>
<p class="text-sm text-slate-400">{document.file_name} · {formatFileSize(document.file_size)}</p>
</div>
</div>
<div class="flex items-center gap-2">
<!-- Zoom Controls (for images) -->
{#if isImage}
<div class="flex items-center gap-1 rounded-lg bg-slate-800 px-2 py-1">
<button
onclick={handleZoomOut}
class="rounded p-1 text-slate-400 hover:bg-slate-700 hover:text-white"
aria-label="Zoom out"
>
<ZoomOut class="h-4 w-4" />
</button>
<span class="min-w-[3rem] text-center text-sm text-slate-300">{zoom}%</span>
<button
onclick={handleZoomIn}
class="rounded p-1 text-slate-400 hover:bg-slate-700 hover:text-white"
aria-label="Zoom in"
>
<ZoomIn class="h-4 w-4" />
</button>
<button
onclick={handleRotate}
class="rounded p-1 text-slate-400 hover:bg-slate-700 hover:text-white"
aria-label="Rotate"
>
<RotateCw class="h-4 w-4" />
</button>
</div>
{/if}
<!-- Download Button -->
<Button
variant="outline"
size="sm"
onclick={handleDownload}
class="border-slate-600 bg-slate-800 text-white hover:bg-slate-700"
>
<Download class="mr-2 h-4 w-4" />
Download
</Button>
<!-- Close Button -->
<button
onclick={onClose}
class="rounded-lg p-2 text-slate-400 hover:bg-slate-800 hover:text-white"
aria-label="Close preview"
>
<X class="h-6 w-6" />
</button>
</div>
</div>
<!-- Content Area -->
<div class="relative flex-1 overflow-auto p-4">
{#if isLoading && !isImage && !isPdf}
<div class="flex h-full items-center justify-center">
<LoadingSpinner size="lg" class="text-monaco-600" />
</div>
{:else if loadError}
<div class="flex h-full flex-col items-center justify-center gap-4 text-slate-400">
<File class="h-16 w-16" />
<p>Unable to load preview</p>
<Button
variant="outline"
onclick={handleDownload}
class="border-slate-600 bg-slate-800 text-white hover:bg-slate-700"
>
<Download class="mr-2 h-4 w-4" />
Download File
</Button>
</div>
{:else if isPdf}
<!-- PDF Preview -->
<iframe
src={previewUrl}
class="h-full w-full rounded-lg bg-white"
title={document.title}
onload={() => isLoading = false}
onerror={() => { loadError = true; isLoading = false; }}
></iframe>
{:else if isImage}
<!-- Image Preview -->
<div class="flex h-full items-center justify-center overflow-auto">
<img
src={previewUrl}
alt={document.title}
class="max-h-full max-w-full object-contain transition-transform duration-200"
style="transform: scale({zoom / 100}) rotate({rotation}deg);"
onload={() => isLoading = false}
onerror={() => { loadError = true; isLoading = false; }}
/>
</div>
{:else if isText && textContent !== null}
<!-- Text Preview -->
<div class="h-full overflow-auto rounded-lg bg-slate-950 p-4">
<pre class="whitespace-pre-wrap font-mono text-sm text-slate-300">{textContent}</pre>
</div>
{:else if isOffice}
<!-- Office Documents - Offer Download -->
<div class="flex h-full flex-col items-center justify-center gap-4 text-slate-400">
<File class="h-16 w-16" />
<p class="text-lg">Office documents cannot be previewed directly</p>
<p class="text-sm">Download the file to view it in Microsoft Office or compatible application</p>
<Button
variant="monaco"
onclick={handleDownload}
>
<Download class="mr-2 h-4 w-4" />
Download {document.file_name}
</Button>
</div>
{:else}
<!-- Unsupported file type -->
<div class="flex h-full flex-col items-center justify-center gap-4 text-slate-400">
<File class="h-16 w-16" />
<p class="text-lg">Preview not available for this file type</p>
<Button
variant="monaco"
onclick={handleDownload}
>
<Download class="mr-2 h-4 w-4" />
Download File
</Button>
</div>
{/if}
</div>
</div>
</div>