268 lines
9.0 KiB
TypeScript
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>
|
|
);
|
|
}
|