app-uau39m/frontend/src/components/features/FileUploadPanel.tsx
2026-09-28 23:38:56 +05:30

268 lines
9.0 KiB
TypeScript

import React, { useState, useRef } from 'react';
import { AlertCircle, CheckCircle2, Copy, File, FileText, Image as ImageIcon, Loader2, RefreshCw, Upload, X } from 'lucide-react';
import { api, resolveMediaUrl } from '../../api';
interface FileUploadPanelProps {
onUploadSuccess?: (url: string) => void;
label?: string;
accept?: string;
currentUrl?: string;
className?: string;
}
export default function FileUploadPanel({
onUploadSuccess,
label = 'Upload Media Asset',
accept = 'image/*',
currentUrl = '',
className = '',
}: FileUploadPanelProps) {
const [file, setFile] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(currentUrl ? resolveMediaUrl(currentUrl) : null);
const [uploadedUrl, setUploadedUrl] = useState<string>(currentUrl);
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const handleFileSelect = (selectedFile: File) => {
setError(null);
setFile(selectedFile);
if (selectedFile.type.startsWith('image/')) {
const objectUrl = URL.createObjectURL(selectedFile);
setPreview(objectUrl);
} else {
setPreview(null);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
handleFileSelect(e.target.files[0]);
}
};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleFileSelect(e.dataTransfer.files[0]);
}
};
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(false);
};
const uploadFile = async () => {
if (!file) return;
setIsUploading(true);
setError(null);
try {
const formData = new FormData();
formData.append('file', file);
const res = await api.post('/api/upload', formData, {
headers: { 'Content-Type': undefined },
});
if (res.data && res.data.success && res.data.url) {
const url = res.data.url;
setUploadedUrl(url);
if (onUploadSuccess) {
onUploadSuccess(url);
}
} else {
throw new Error(res.data?.msg || 'Failed to upload asset');
}
} catch (err: any) {
setError(err?.response?.data?.msg || err?.message || 'File upload failed. Please try again.');
} finally {
setIsUploading(false);
}
};
const handleCopy = () => {
if (!uploadedUrl) return;
const fullUrl = resolveMediaUrl(uploadedUrl);
navigator.clipboard.writeText(fullUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const handleReset = () => {
setFile(null);
setPreview(null);
setUploadedUrl('');
setError(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
return (
<div className={`p-4 bg-white rounded-xl border border-[#E2E1DA] shadow-sm ${className}`}>
<div className="flex items-center justify-between mb-3">
<label className="text-sm font-semibold text-slate-900 flex items-center gap-2">
<ImageIcon className="w-4 h-[#B93812]" />
{label}
</label>
{uploadedUrl && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-emerald-100 text-emerald-800">
<CheckCircle2 className="w-3 h-3 mr-1" /> Ready
</span>
)}
</div>
<input
ref={fileInputRef}
type="file"
accept={accept}
onChange={handleInputChange}
className="hidden"
/>
{!file && !uploadedUrl ? (
<div
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={() => fileInputRef.current?.click()}
className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
isDragging
? 'border-[#B93812] bg-orange-50/50'
: 'border-slate-300 hover:border-slate-400 bg-[#F8F7F4]/50'
}`}
>
<div className="mx-auto w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center mb-2 text-slate-600">
<Upload className="w-5 h-5" />
</div>
<p className="text-xs font-medium text-slate-800">
Click to select or drag and drop image file
</p>
<p className="text-[11px] text-slate-500 mt-1">
JPG, PNG, WebP up to 10MB
</p>
</div>
) : (
<div className="space-y-3">
{preview ? (
<div className="relative rounded-lg overflow-hidden border border-slate-200 bg-slate-950 max-h-48 flex items-center justify-center">
<img
src={preview}
alt="Selected asset preview"
className="max-h-48 w-full object-cover"
/>
<button
type="button"
onClick={handleReset}
className="absolute top-2 right-2 bg-slate-900/80 hover:bg-slate-900 text-white p-1 rounded-full text-xs transition-colors"
title="Remove asset"
>
<X className="w-4 h-4" />
</button>
</div>
) : (
<div className="p-3 bg-slate-50 rounded-lg border border-slate-200 flex items-center justify-between">
<div className="flex items-center space-x-3 overflow-hidden">
<FileText className="w-8 h-8 text-slate-400 flex-shrink-0" />
<div className="truncate text-xs">
<p className="font-medium text-slate-900 truncate">
{file ? file.name : 'Uploaded File'}
</p>
{file && (
<p className="text-slate-500">
{(file.size / 1024).toFixed(1)} KB
</p>
)}
</div>
</div>
<button
type="button"
onClick={handleReset}
className="p-1 text-slate-400 hover:text-slate-600"
>
<X className="w-4 h-4" />
</button>
</div>
)}
{file && !uploadedUrl && (
<button
type="button"
onClick={uploadFile}
disabled={isUploading}
className="w-full flex items-center justify-center gap-2 py-2 px-4 bg-[#0F172A] hover:bg-slate-800 text-white rounded-lg text-xs font-semibold transition-colors disabled:opacity-50"
>
{isUploading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Uploading Asset...
</>
) : (
<>
<Upload className="w-4 h-4" />
Confirm Upload
</>
)}
</button>
)}
{uploadedUrl && (
<div className="space-y-2 pt-1">
<div className="flex items-center justify-between text-xs bg-slate-50 p-2 rounded border border-slate-200">
<span className="font-mono text-slate-600 truncate mr-2 text-[11px]">
{uploadedUrl}
</span>
<button
type="button"
onClick={handleCopy}
className="flex items-center gap-1 text-[#B93812] font-medium hover:underline flex-shrink-0"
>
{copied ? (
<>
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600" />
<span className="text-emerald-600">Copied</span>
</>
) : (
<>
<Copy className="w-3.5 h-3.5" />
Copy URL
</>
)}
</button>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="flex-1 flex items-center justify-center gap-1.5 py-1.5 text-xs font-medium text-slate-700 bg-slate-100 hover:bg-slate-200 rounded transition-colors"
>
<RefreshCw className="w-3.5 h-3.5" /> Replace File
</button>
</div>
</div>
)}
</div>
)}
{error && (
<div className="mt-3 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700 flex items-center gap-2">
<AlertCircle className="w-4 h-4 flex-shrink-0 text-red-600" />
<span>{error}</span>
</div>
)}
</div>
);
}