Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM, PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source files covering clients, berths, interests/pipeline, documents/EOI, expenses/invoices, email, notifications, dashboard, admin, and client portal. CI/CD via Gitea Actions with Docker builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
112
src/components/files/file-preview-dialog.tsx
Normal file
112
src/components/files/file-preview-dialog.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
|
||||
interface FilePreviewDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
fileId?: string;
|
||||
fileName?: string;
|
||||
mimeType?: string;
|
||||
}
|
||||
|
||||
export function FilePreviewDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
fileId,
|
||||
fileName,
|
||||
mimeType,
|
||||
}: FilePreviewDialogProps) {
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !fileId) {
|
||||
setPreviewUrl(null);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
apiFetch<{ data: { url: string } }>(`/api/v1/files/${fileId}/preview`)
|
||||
.then((res) => {
|
||||
setPreviewUrl(res.data.url);
|
||||
})
|
||||
.catch(() => {
|
||||
setError('Failed to load preview');
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [open, fileId]);
|
||||
|
||||
const isImage = mimeType?.startsWith('image/');
|
||||
const isPdf = mimeType === 'application/pdf';
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl w-full h-[80vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 truncate">
|
||||
<span className="truncate">{fileName ?? 'Preview'}</span>
|
||||
{previewUrl && (
|
||||
<a
|
||||
href={previewUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="shrink-0 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
)}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-hidden rounded-lg border bg-muted/20">
|
||||
{loading && (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
Loading preview...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="flex h-full items-center justify-center text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && previewUrl && isImage && (
|
||||
<div className="flex h-full items-center justify-center p-4">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt={fileName ?? 'Preview'}
|
||||
className="max-h-full max-w-full object-contain rounded"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && previewUrl && isPdf && (
|
||||
<iframe
|
||||
src={previewUrl}
|
||||
title={fileName ?? 'PDF Preview'}
|
||||
className="h-full w-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user