Files
MOPC-Portal/src/components/applicant/file-upload-slot.tsx

182 lines
5.5 KiB
TypeScript
Raw Normal View History

'use client'
import { useState, useRef } from 'react'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Upload, X, FileText, AlertCircle } from 'lucide-react'
interface FileRequirement {
id: string
label: string
description?: string
mimeTypes: string[]
maxSizeMb?: number
required: boolean
}
interface FileUploadSlotProps {
requirement: FileRequirement
isLocked: boolean
onUpload: (file: File) => void
}
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 formatMimeTypes(mimeTypes: string[]): string {
const extensions = mimeTypes.map(mime => {
const parts = mime.split('/')
return parts[1] || mime
})
return extensions.join(', ').toUpperCase()
}
export function FileUploadSlot({ requirement, isLocked, onUpload }: FileUploadSlotProps) {
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [error, setError] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
setError(null)
// Validate file type
if (requirement.mimeTypes.length > 0 && !requirement.mimeTypes.includes(file.type)) {
setError(`File type not allowed. Accepted types: ${formatMimeTypes(requirement.mimeTypes)}`)
return
}
// Validate file size
if (requirement.maxSizeMb) {
const maxBytes = requirement.maxSizeMb * 1024 * 1024
if (file.size > maxBytes) {
setError(`File size exceeds ${requirement.maxSizeMb} MB limit`)
return
}
}
setSelectedFile(file)
onUpload(file)
}
const handleRemove = () => {
setSelectedFile(null)
setError(null)
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}
const handleClick = () => {
if (!isLocked) {
fileInputRef.current?.click()
}
}
return (
<Card className={isLocked ? 'opacity-60' : ''}>
<CardContent className="p-4">
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="font-medium">{requirement.label}</h3>
{requirement.required && (
<Badge variant="destructive" className="text-xs">
Required
</Badge>
)}
{isLocked && (
<Badge variant="secondary" className="text-xs">
Locked
</Badge>
)}
</div>
{requirement.description && (
<p className="text-sm text-muted-foreground mt-1">
{requirement.description}
</p>
)}
<div className="flex flex-wrap gap-2 mt-2 text-xs text-muted-foreground">
{requirement.mimeTypes.length > 0 && (
<span>Accepted: {formatMimeTypes(requirement.mimeTypes)}</span>
)}
{requirement.maxSizeMb && (
<span> Max size: {requirement.maxSizeMb} MB</span>
)}
</div>
</div>
</div>
{/* File preview or upload button */}
{selectedFile ? (
<div className="flex items-center gap-3 p-3 rounded-lg border bg-muted/50">
<FileText className="h-8 w-8 text-brand-blue shrink-0" />
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate" title={selectedFile.name}>
{selectedFile.name}
</p>
<p className="text-xs text-muted-foreground">
{formatFileSize(selectedFile.size)}
</p>
</div>
<div className="flex gap-2 shrink-0">
<Button
size="sm"
variant="outline"
onClick={handleClick}
disabled={isLocked}
>
Replace
</Button>
<Button
size="sm"
variant="ghost"
onClick={handleRemove}
disabled={isLocked}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
) : (
<div>
<Button
variant="outline"
className="w-full border-dashed"
onClick={handleClick}
disabled={isLocked}
>
<Upload className="mr-2 h-4 w-4" />
{isLocked ? 'Upload Disabled' : 'Choose File'}
</Button>
<input
ref={fileInputRef}
type="file"
className="hidden"
accept={requirement.mimeTypes.join(',')}
onChange={handleFileSelect}
disabled={isLocked}
/>
</div>
)}
{/* Error message */}
{error && (
<div className="flex items-start gap-2 mt-3 p-2 rounded-md bg-red-50 border border-red-200">
<AlertCircle className="h-4 w-4 text-red-600 shrink-0 mt-0.5" />
<p className="text-sm text-red-700">{error}</p>
</div>
)}
</CardContent>
</Card>
)
}