Spaces:
Build error
Build error
| '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" | |
| /> | |
| )} | |
| </> | |
| ); | |
| } | |