109 lines
3.6 KiB
TypeScript
109 lines
3.6 KiB
TypeScript
/**
|
|
* File preview modal for DMS.
|
|
* Renders PDF files in an iframe via the preview endpoint.
|
|
* Shows file metadata and download button for non-PDF files.
|
|
*/
|
|
|
|
import React, { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Modal } from '@/components/ui/Modal';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { formatDateShort } from '@/utils/date';
|
|
import { Badge } from '@/components/ui/Badge';
|
|
import { getFilePreviewUrl, type DmsFile } from '@/api/dms';
|
|
import { FileText } from 'lucide-react';
|
|
|
|
export interface FilePreviewModalProps {
|
|
open: boolean;
|
|
file: DmsFile | null;
|
|
onClose: () => void;
|
|
}
|
|
|
|
function formatFileSize(bytes: number): string {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
}
|
|
|
|
export function FilePreviewModal({ open, file, onClose }: FilePreviewModalProps) {
|
|
const { t } = useTranslation();
|
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (open && file) {
|
|
setPreviewUrl(getFilePreviewUrl(file.id));
|
|
} else {
|
|
setPreviewUrl(null);
|
|
}
|
|
}, [open, file]);
|
|
|
|
if (!file) return null;
|
|
|
|
const isPdf = file.mime_type === 'application/pdf';
|
|
const isImage = file.mime_type.startsWith('image/');
|
|
|
|
return (
|
|
<Modal
|
|
open={open}
|
|
onClose={onClose}
|
|
title={file.name}
|
|
size="xl"
|
|
data-testid="file-preview-modal"
|
|
>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-4 flex-wrap">
|
|
<Badge variant="info">{file.mime_type.split('/')[1]?.toUpperCase() || t('dms.icon.other')}</Badge>
|
|
<span className="text-sm text-secondary-500">{formatFileSize(file.size_bytes ?? file.size ?? 0)}</span>
|
|
{file.created_at && (
|
|
<span className="text-sm text-secondary-500">
|
|
{t('dms.fileModified')}: {formatDateShort(file.created_at)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{isPdf && previewUrl && (
|
|
<div className="border border-secondary-200 rounded-lg overflow-hidden" data-testid="pdf-preview">
|
|
<iframe
|
|
src={previewUrl} className="w-full h-[60vh]"
|
|
title={file.name} aria-label={t('dms.preview')}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{isImage && previewUrl && (
|
|
<div className="flex justify-center border border-secondary-200 rounded-lg p-4" data-testid="image-preview">
|
|
<img
|
|
src={previewUrl}
|
|
alt={file.name}
|
|
className="max-h-[60vh] object-contain"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{!isPdf && !isImage && (
|
|
<div className="text-center py-12" data-testid="no-preview-available">
|
|
<FileText className="mx-auto h-12 w-12 text-secondary-300" aria-hidden="true" strokeWidth={1.5} />
|
|
<p className="mt-2 text-sm text-secondary-500">{t('dms.preview')}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end gap-3">
|
|
{previewUrl && (
|
|
<a
|
|
href={previewUrl}
|
|
download={file.name}
|
|
className="inline-flex items-center px-4 py-2 rounded-md bg-primary-600 text-white text-sm font-medium hover:bg-primary-700 min-h-touch"
|
|
>
|
|
{t('dms.download')}
|
|
</a>
|
|
)}
|
|
<Button variant="secondary" onClick={onClose}>
|
|
{t('dms.cancel')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|