![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.ca/public_html/src/components/ |
import React, { useRef, useState } from 'react';
const FILE_TYPE_ICONS: Record<string, JSX.Element> = {
pdf: <svg className="w-7 h-7 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h7l5 5v11a2 2 0 01-2 2z" /></svg>,
image: <svg className="w-7 h-7 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="5" rx="2" /><circle cx="8.5" cy="10.5" r="1.5" /><path d="M21 19l-5.5-5.5a2.121 2.121 0 00-3 0L3 19" /></svg>,
video: <svg className="w-7 h-7 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="5" rx="2" /><polygon points="10,9 16,12 10,15" fill="currentColor" /></svg>,
audio: <svg className="w-7 h-7 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M9 19V6l12-2v13" /><circle cx="6" cy="18" r="3" /></svg>,
doc: <svg className="w-7 h-7 text-blue-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="5" rx="2" /><path d="M2 7h20" /></svg>,
other: <svg className="w-7 h-7 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="5" rx="2" /><path d="M2 7h20" /></svg>,
};
function getFileTypeIcon(name: string, type: string) {
if (type.includes('pdf') || name.match(/\.pdf$/i)) return FILE_TYPE_ICONS.pdf;
if (type.startsWith('image') || name.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/i)) return FILE_TYPE_ICONS.image;
if (type.startsWith('video') || name.match(/\.(mp4|webm|mov|avi)$/i)) return FILE_TYPE_ICONS.video;
if (type.startsWith('audio') || name.match(/\.(mp3|wav|ogg)$/i)) return FILE_TYPE_ICONS.audio;
if (type.includes('word') || name.match(/\.(doc|docx)$/i)) return FILE_TYPE_ICONS.doc;
if (type.includes('excel') || name.match(/\.(xls|xlsx)$/i)) return FILE_TYPE_ICONS.doc;
return FILE_TYPE_ICONS.other;
}
function getFileTypeLabel(type: string, name: string) {
if (type.includes('pdf') || name.match(/\.pdf$/i)) return 'PDF Document';
if (type.startsWith('image') || name.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/i)) return 'Image';
if (type.startsWith('video') || name.match(/\.(mp4|webm|mov|avi)$/i)) return 'Video';
if (type.startsWith('audio') || name.match(/\.(mp3|wav|ogg)$/i)) return 'Audio';
if (type.includes('word') || name.match(/\.(doc|docx)$/i)) return 'Word Document';
if (type.includes('excel') || name.match(/\.(xls|xlsx)$/i)) return 'Excel Spreadsheet';
return 'File';
}
interface FileItem {
id: string;
name: string;
url: string;
type: string;
size?: number;
status?: string;
}
const DocumentManager: React.FC<{
files: FileItem[];
onUpload: (files: File[]) => void;
onDelete: (id: string) => void;
onPreview?: (file: FileItem) => void;
}> = ({ files, onUpload, onDelete, onPreview }) => {
const [dragActive, setDragActive] = useState(false);
const [previewFile, setPreviewFile] = useState<FileItem | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(e.type === 'dragover');
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
onUpload(Array.from(e.dataTransfer.files));
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
onUpload(Array.from(e.target.files));
}
};
// Helper for status badge
const getStatusBadge = (status?: string) => {
if (!status) return null;
let color = 'bg-gray-200 text-gray-700';
if (status.toLowerCase() === 'pending') color = 'bg-yellow-100 text-yellow-800';
if (status.toLowerCase() === 'approved') color = 'bg-green-100 text-green-800';
if (status.toLowerCase() === 'rejected') color = 'bg-red-100 text-red-800';
return (
<span className={`ml-2 px-2 py-0.5 rounded-full text-xs font-semibold ${color}`}>{status.charAt(0).toUpperCase() + status.slice(1)}</span>
);
};
return (
<div className="w-full max-w-2xl mx-auto">
{/* Upload Area */}
<div
className={`relative flex flex-col items-center justify-center p-8 mb-8 rounded-2xl border-2 border-dashed transition-all duration-200 ${dragActive ? 'border-blue-500 bg-blue-50/60' : 'border-gray-300 bg-white/60'} backdrop-blur-md shadow-lg`}
onDragOver={handleDrag}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDrop={handleDrop}
style={{ minHeight: 140 }}
>
<input
ref={inputRef}
type="file"
multiple
className="absolute inset-0 opacity-0 cursor-pointer"
onChange={handleFileChange}
/>
<div className="flex flex-col items-center pointer-events-none">
<svg className="w-12 h-12 text-blue-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4a1 1 0 011-1h8a1 1 0 011 1v12m-5 4v-4m0 0l-2 2m2-2l2 2" /></svg>
<span className="font-semibold text-gray-700">Drag & drop files here or <span className="text-blue-600 underline">browse</span></span>
<span className="text-xs text-gray-500 mt-1">PDF, images, video, audio, Word, Excel, and more. Max 100MB each.</span>
</div>
</div>
{/* File List */}
<div className="grid gap-4">
{files.map((file) => (
<div
key={file.id}
className="flex items-center bg-white/80 rounded-xl shadow-md p-4 hover:shadow-xl transition-shadow backdrop-blur-md cursor-pointer group"
onClick={() => onPreview ? onPreview(file) : setPreviewFile(file)}
tabIndex={0}
role="button"
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { onPreview ? onPreview(file) : setPreviewFile(file); } }}
>
<div className="mr-4 flex-shrink-0">{getFileTypeIcon(file.name, file.type)}</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-gray-900 truncate flex items-center">
{file.name}
{getStatusBadge(file.status)}
</div>
<div className="text-xs text-gray-500 truncate">{getFileTypeLabel(file.type, file.name)}{file.size ? ` • ${(file.size / 1024).toFixed(1)} KB` : ''}</div>
</div>
<div className="flex items-center gap-2 ml-4">
{/* Always allow preview if file has a URL */}
{file.url && (
<button
onClick={e => { e.stopPropagation(); onPreview ? onPreview(file) : setPreviewFile(file); }}
className="px-2 py-1 text-blue-600 hover:text-blue-800 rounded border border-blue-100 hover:border-blue-300 text-xs group-hover:bg-blue-50"
>
Preview
</button>
)}
<a
href={file.url}
target="_blank"
rel="noopener noreferrer"
className="px-2 py-1 text-green-600 hover:text-green-800 rounded border border-green-100 hover:border-green-300 text-xs group-hover:bg-green-50"
download
onClick={e => e.stopPropagation()}
>
Download
</a>
<button
onClick={e => { e.stopPropagation(); onDelete(file.id); }}
className="ml-2 text-red-500 hover:text-red-700 p-1 rounded-full group-hover:bg-red-50"
title="Delete"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
</div>
))}
</div>
{/* Preview Modal */}
{previewFile && !onPreview && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="bg-white rounded-2xl shadow-2xl max-w-2xl w-full p-6 relative">
<button
onClick={() => setPreviewFile(null)}
className="absolute top-3 right-3 text-gray-400 hover:text-gray-700"
>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
<div className="mb-4 font-semibold text-lg text-gray-900 truncate">{previewFile.name}</div>
{/* Preview content by type */}
{previewFile.type.startsWith('image') ? (
<img src={previewFile.url} alt={previewFile.name} className="max-h-[60vh] mx-auto rounded-lg" />
) : previewFile.type.startsWith('video') ? (
<video src={previewFile.url} controls className="max-h-[60vh] mx-auto rounded-lg" />
) : previewFile.type.startsWith('audio') ? (
<audio src={previewFile.url} controls className="w-full" />
) : previewFile.type.includes('pdf') ? (
<iframe src={previewFile.url} className="w-full h-[60vh] rounded-lg" title={previewFile.name} />
) : (
<div className="text-gray-500">Preview not available for this file type.</div>
)}
</div>
</div>
)}
</div>
);
};
export default DocumentManager;