docs / src /components /file-item.tsx
Zerotracex-Stuff
First model version
a5871f0
'use client';
import * as React from 'react';
import ReactDOM from 'react-dom';
import { FileText, Download, X, Eye, Loader2, File as FileIcon, Presentation, Share2 } from 'lucide-react';
import { TableRow, TableCell } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import type { File as FileType } from '@/app/api/files/route';
import { useToast } from "@/hooks/use-toast"
import { Progress } from './ui/progress';
import { cn } from '@/lib/utils';
import dynamic from 'next/dynamic';
import { Badge } from './ui/badge';
import { Checkbox } from './ui/checkbox';
import { ShareDialog } from './share-dialog';
const PdfThumbnail = dynamic(() => import('./pdf-thumbnail').then(mod => mod.PdfThumbnail), {
ssr: false,
loading: () => <div className="h-[280px] w-[200px] flex items-center justify-center bg-muted"><Loader2 className="h-8 w-8 animate-spin text-primary" /></div>
});
function PdfLoader() {
const [isMounted, setIsMounted] = React.useState(false);
React.useEffect(() => {
setIsMounted(true);
return () => setIsMounted(false);
}, []);
if (!isMounted) {
return null;
}
return ReactDOM.createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>,
document.body
);
}
const PdfViewer = dynamic(() => import('./pdf-viewer').then(mod => mod.PdfViewer), {
ssr: false,
loading: () => <PdfLoader />,
});
interface FileItemProps extends React.HTMLAttributes<HTMLTableRowElement> {
file: FileType;
isSelected: boolean;
onSelectItem: (id: string, isSelected: boolean) => void;
isSelectionActive: boolean;
isPublicShare?: boolean;
}
const fileTypeConfig = {
pdf: {
icon: FileText,
color: 'destructive' as const,
label: 'PDF',
},
docx: {
icon: FileText,
color: 'default' as const,
label: 'DOCX',
},
pptx: {
icon: Presentation,
color: 'secondary' as const,
label: 'PPTX',
},
other: {
icon: FileIcon,
color: 'outline' as const,
label: 'File'
}
}
export function FileItem({ file, className, isSelected, onSelectItem, isSelectionActive, isPublicShare, ...props }: FileItemProps) {
const [downloadProgress, setDownloadProgress] = React.useState<number | null>(null);
const [isDownloading, setIsDownloading] = React.useState(false);
const downloadAbortController = React.useRef<AbortController | null>(null);
const [isPreviewOpen, setIsPreviewOpen] = React.useState(false);
const [isShareOpen, setIsShareOpen] = React.useState(false);
const { toast } = useToast();
const handleRowClick = (e: React.MouseEvent<HTMLTableRowElement>) => {
if ((e.target as HTMLElement).closest('[role="checkbox"]') || (e.target as HTMLElement).closest('button')) {
return;
}
if (isSelectionActive) {
onSelectItem(file.id, !isSelected);
} else if (file.fileType === 'pdf') {
setIsPreviewOpen(true);
}
}
const handleDownload = async () => {
if (isDownloading) {
if (downloadAbortController.current) {
downloadAbortController.current.abort();
}
return;
}
setIsDownloading(true);
setDownloadProgress(0);
const controller = new AbortController();
downloadAbortController.current = controller;
try {
const response = await fetch(file.path, {
signal: controller.signal,
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
if (!response.body) {
throw new Error('Response body is null');
}
const contentLength = response.headers.get('content-length');
const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
let loaded = 0;
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
chunks.push(value);
loaded += value.length;
if (totalSize > 0) {
const progress = Math.round((loaded / totalSize) * 100);
setDownloadProgress(progress);
}
}
const blob = new Blob(chunks);
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
setDownloadProgress(100);
} catch (error: any) {
if (error.name === 'AbortError') {
console.log('Download was aborted.');
} else {
toast({
variant: "destructive",
title: "Download Error",
description: "There was a problem downloading the file.",
});
console.error('Download error:', error);
}
} finally {
setIsDownloading(false);
setDownloadProgress(null);
downloadAbortController.current = null;
}
};
const config = fileTypeConfig[file.fileType] ?? fileTypeConfig.other;
const Icon = config.icon;
const FileNameDisplay = (
<span className="cursor-pointer hover:underline" onClick={(e) => {e.stopPropagation(); if (file.fileType === 'pdf' && !isSelectionActive) setIsPreviewOpen(true);}}>{file.name}</span>
)
const handleShareClick = (e: React.MouseEvent) => {
e.stopPropagation();
setIsShareOpen(true);
}
return (
<>
<TableRow
className={cn("group cursor-pointer", className, isSelected && "bg-accent/50")}
data-selected={isSelected}
onClick={handleRowClick}
{...props}
>
<TableCell className={cn("w-[40px]", !isSelectionActive && "hidden")} onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={isSelected}
onCheckedChange={(checked) => onSelectItem(file.id, Boolean(checked))}
aria-label={`Select file ${file.name}`}
/>
</TableCell>
<TableCell className="font-medium">
<div className="flex items-center gap-3">
<Icon className="h-5 w-5 text-muted-foreground" />
<div className="flex-1 flex flex-col gap-1">
<div className="flex items-center gap-2">
{file.fileType === 'pdf' ? (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
{FileNameDisplay}
</TooltipTrigger>
<TooltipContent className="p-0 border-2 border-primary/20 shadow-2xl bg-muted" side="bottom" align="start">
<PdfThumbnail fileUrl={file.path} />
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : FileNameDisplay }
<Badge variant={config.color}>{config.label}</Badge>
</div>
{isDownloading && downloadProgress !== null && (
<div className="flex items-center gap-2 mt-1">
<Progress value={downloadProgress} className="w-full h-1.5" />
<span className="text-xs text-muted-foreground w-10 text-right">{downloadProgress}%</span>
</div>
)}
</div>
</div>
</TableCell>
<TableCell className="text-muted-foreground max-w-sm truncate">
<p>{file.contentSnippet}</p>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<TooltipProvider>
{!isPublicShare && (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={handleShareClick}>
<Share2 className="h-4 w-4" />
<span className="sr-only">Share</span>
</Button>
</TooltipTrigger>
<TooltipContent><p>Share</p></TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={(e) => {e.stopPropagation(); if (file.fileType === 'pdf') setIsPreviewOpen(true);}} disabled={file.fileType !== 'pdf'}>
<Eye className="h-4 w-4" />
<span className="sr-only">Preview</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Preview (PDFs only)</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={(e) => {e.stopPropagation(); handleDownload();}}>
{isDownloading ? <X className="h-4 w-4" /> : <Download className="h-4 w-4" />}
<span className="sr-only">{isDownloading ? 'Cancel' : 'Download'}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{isDownloading ? 'Cancel' : 'Download'}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</TableCell>
</TableRow>
{isPreviewOpen && file.fileType === 'pdf' && (
<PdfViewer
isOpen={isPreviewOpen}
onOpenChange={setIsPreviewOpen}
fileUrl={file.path}
fileName={file.name}
/>
)}
{!isPublicShare && (
<ShareDialog
isOpen={isShareOpen}
onOpenChange={setIsShareOpen}
itemName={file.name}
itemPath={file.path}
itemType="file"
/>
)}
</>
);
}