146 lines
5.6 KiB
TypeScript
146 lines
5.6 KiB
TypeScript
|
|
'use client'
|
||
|
|
|
||
|
|
import { trpc } from '@/lib/trpc/client'
|
||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||
|
|
import { Badge } from '@/components/ui/badge'
|
||
|
|
import { Button } from '@/components/ui/button'
|
||
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
||
|
|
import { FileText, Download, ExternalLink } from 'lucide-react'
|
||
|
|
import { toast } from 'sonner'
|
||
|
|
|
||
|
|
interface MultiWindowDocViewerProps {
|
||
|
|
roundId: string
|
||
|
|
projectId: string
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatFileSize(bytes: number): string {
|
||
|
|
if (bytes === 0) return '0 B'
|
||
|
|
const k = 1024
|
||
|
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
||
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||
|
|
}
|
||
|
|
|
||
|
|
function getFileIcon(mimeType: string) {
|
||
|
|
if (mimeType.startsWith('image/')) return '🖼️'
|
||
|
|
if (mimeType.startsWith('video/')) return '🎥'
|
||
|
|
if (mimeType.includes('pdf')) return '📄'
|
||
|
|
if (mimeType.includes('word') || mimeType.includes('document')) return '📝'
|
||
|
|
if (mimeType.includes('sheet') || mimeType.includes('excel')) return '📊'
|
||
|
|
if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) return '📊'
|
||
|
|
return '📎'
|
||
|
|
}
|
||
|
|
|
||
|
|
export function MultiWindowDocViewer({ roundId, projectId }: MultiWindowDocViewerProps) {
|
||
|
|
const { data: windows, isLoading } = trpc.round.getVisibleWindows.useQuery(
|
||
|
|
{ roundId },
|
||
|
|
{ enabled: !!roundId }
|
||
|
|
)
|
||
|
|
|
||
|
|
if (isLoading) {
|
||
|
|
return (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<Skeleton className="h-6 w-48" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<Skeleton className="h-64" />
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!windows || windows.length === 0) {
|
||
|
|
return (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>Documents</CardTitle>
|
||
|
|
<CardDescription>Submission windows and uploaded files</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="text-center py-8">
|
||
|
|
<FileText className="h-12 w-12 text-muted-foreground/50 mx-auto mb-3" />
|
||
|
|
<p className="text-sm text-muted-foreground">No submission windows available</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>Documents</CardTitle>
|
||
|
|
<CardDescription>Files submitted across all windows</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<Tabs defaultValue={windows[0]?.id || ''} className="w-full">
|
||
|
|
<TabsList className="w-full flex-wrap justify-start h-auto gap-1 bg-transparent p-0 mb-4">
|
||
|
|
{windows.map((window: any) => (
|
||
|
|
<TabsTrigger
|
||
|
|
key={window.id}
|
||
|
|
value={window.id}
|
||
|
|
className="data-[state=active]:bg-brand-blue data-[state=active]:text-white px-4 py-2 rounded-md text-sm"
|
||
|
|
>
|
||
|
|
{window.name}
|
||
|
|
{window.files && window.files.length > 0 && (
|
||
|
|
<Badge variant="secondary" className="ml-2 text-xs">
|
||
|
|
{window.files.length}
|
||
|
|
</Badge>
|
||
|
|
)}
|
||
|
|
</TabsTrigger>
|
||
|
|
))}
|
||
|
|
</TabsList>
|
||
|
|
|
||
|
|
{windows.map((window: any) => (
|
||
|
|
<TabsContent key={window.id} value={window.id} className="mt-0">
|
||
|
|
{!window.files || window.files.length === 0 ? (
|
||
|
|
<div className="text-center py-8 border border-dashed rounded-lg">
|
||
|
|
<FileText className="h-10 w-10 text-muted-foreground/50 mx-auto mb-2" />
|
||
|
|
<p className="text-sm text-muted-foreground">No files uploaded</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
||
|
|
{window.files.map((file: any) => (
|
||
|
|
<Card key={file.id} className="overflow-hidden">
|
||
|
|
<CardContent className="p-4">
|
||
|
|
<div className="flex items-start gap-3">
|
||
|
|
<div className="text-2xl">{getFileIcon(file.mimeType || '')}</div>
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<p className="font-medium text-sm truncate" title={file.filename}>
|
||
|
|
{file.filename}
|
||
|
|
</p>
|
||
|
|
<div className="flex items-center gap-2 mt-1">
|
||
|
|
<Badge variant="outline" className="text-xs">
|
||
|
|
{file.mimeType?.split('/')[1]?.toUpperCase() || 'FILE'}
|
||
|
|
</Badge>
|
||
|
|
{file.size && (
|
||
|
|
<span className="text-xs text-muted-foreground">
|
||
|
|
{formatFileSize(file.size)}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div className="flex gap-2 mt-3">
|
||
|
|
<Button size="sm" variant="outline" className="h-7 text-xs">
|
||
|
|
<Download className="mr-1 h-3 w-3" />
|
||
|
|
Download
|
||
|
|
</Button>
|
||
|
|
<Button size="sm" variant="ghost" className="h-7 text-xs">
|
||
|
|
<ExternalLink className="mr-1 h-3 w-3" />
|
||
|
|
Preview
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</TabsContent>
|
||
|
|
))}
|
||
|
|
</Tabs>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
)
|
||
|
|
}
|