165 lines
5.4 KiB
TypeScript
165 lines
5.4 KiB
TypeScript
|
|
import { useState } from 'react';
|
|
import { Upload, UploadCloud, X, FileText, Image as ImageIcon, FileVideo } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card } from "@/components/ui/card";
|
|
|
|
interface Asset {
|
|
id: string;
|
|
file: File;
|
|
previewUrl?: string;
|
|
type: string;
|
|
}
|
|
|
|
interface AssetUploaderProps {
|
|
onAssetsChange?: (assets: Asset[]) => void;
|
|
maxAssets?: number;
|
|
allowedTypes?: string[];
|
|
label?: string;
|
|
description?: string;
|
|
}
|
|
|
|
export default function AssetUploader({
|
|
onAssetsChange,
|
|
maxAssets = 10,
|
|
allowedTypes = ['image/*', 'application/pdf', 'video/*'],
|
|
label = 'Upload Assets',
|
|
description = 'Upload creative assets for testing'
|
|
}: AssetUploaderProps) {
|
|
const [assets, setAssets] = useState<Asset[]>([]);
|
|
|
|
const handleFileUpload = (files: FileList | null) => {
|
|
if (!files || files.length === 0) return;
|
|
|
|
// Check if adding these files would exceed the limit
|
|
if (assets.length + files.length > maxAssets) {
|
|
toast.error(`You can only upload up to ${maxAssets} assets`);
|
|
return;
|
|
}
|
|
|
|
// Convert FileList to array and create asset objects
|
|
const newAssets: Asset[] = Array.from(files).map(file => {
|
|
// Generate a preview URL for images
|
|
const previewUrl = file.type.startsWith('image/')
|
|
? URL.createObjectURL(file)
|
|
: undefined;
|
|
|
|
return {
|
|
id: `asset-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
file,
|
|
previewUrl,
|
|
type: file.type,
|
|
};
|
|
});
|
|
|
|
const updatedAssets = [...assets, ...newAssets];
|
|
setAssets(updatedAssets);
|
|
|
|
// Notify parent component about the change
|
|
if (onAssetsChange) {
|
|
onAssetsChange(updatedAssets);
|
|
}
|
|
|
|
toast.success(`${newAssets.length} asset(s) uploaded`, {
|
|
description: "Assets added to your project",
|
|
});
|
|
};
|
|
|
|
const handleRemoveAsset = (assetId: string) => {
|
|
const assetToRemove = assets.find(asset => asset.id === assetId);
|
|
if (assetToRemove?.previewUrl) {
|
|
URL.revokeObjectURL(assetToRemove.previewUrl);
|
|
}
|
|
|
|
const updatedAssets = assets.filter(asset => asset.id !== assetId);
|
|
setAssets(updatedAssets);
|
|
|
|
// Notify parent component about the change
|
|
if (onAssetsChange) {
|
|
onAssetsChange(updatedAssets);
|
|
}
|
|
|
|
toast.info('Asset removed');
|
|
};
|
|
|
|
// Determine the icon to use based on file type
|
|
const getAssetIcon = (type: string) => {
|
|
if (type.startsWith('image/')) {
|
|
return <ImageIcon className="h-10 w-10 text-slate-400" />;
|
|
} else if (type.startsWith('video/')) {
|
|
return <FileVideo className="h-10 w-10 text-slate-400" />;
|
|
} else if (type === 'application/pdf') {
|
|
return <FileText className="h-10 w-10 text-slate-400" />;
|
|
} else {
|
|
return <FileText className="h-10 w-10 text-slate-400" />;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Upload area */}
|
|
<div className="border-2 border-dashed border-slate-200 rounded-lg p-6 flex flex-col items-center justify-center bg-slate-50 hover:bg-slate-100 transition cursor-pointer">
|
|
<UploadCloud className="h-10 w-10 text-slate-400 mb-2" />
|
|
<p className="text-sm text-slate-600 mb-1">{label}</p>
|
|
<p className="text-xs text-slate-500 mb-3">{description}</p>
|
|
<input
|
|
type="file"
|
|
accept={allowedTypes.join(',')}
|
|
multiple
|
|
onChange={(e) => handleFileUpload(e.target.files)}
|
|
className="hidden"
|
|
id="asset-uploader-input"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => document.getElementById('asset-uploader-input')?.click()}
|
|
>
|
|
<Upload className="mr-2 h-4 w-4" />
|
|
Select Files
|
|
</Button>
|
|
<p className="text-xs text-slate-500 mt-2">
|
|
{maxAssets - assets.length} of {maxAssets} uploads remaining
|
|
</p>
|
|
</div>
|
|
|
|
{/* Assets preview */}
|
|
{assets.length > 0 && (
|
|
<Card className="p-4">
|
|
<h4 className="text-sm font-medium mb-3">Uploaded Assets ({assets.length})</h4>
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
|
|
{assets.map((asset) => (
|
|
<div key={asset.id} className="relative border rounded-md p-2 group">
|
|
<button
|
|
onClick={() => handleRemoveAsset(asset.id)}
|
|
className="absolute top-1 right-1 bg-white rounded-full p-1 shadow-sm opacity-0 group-hover:opacity-100 transition-opacity"
|
|
title="Remove asset"
|
|
>
|
|
<X className="h-3 w-3 text-slate-500" />
|
|
</button>
|
|
|
|
<div className="aspect-square bg-slate-100 rounded flex items-center justify-center mb-2">
|
|
{asset.previewUrl ? (
|
|
<img
|
|
src={asset.previewUrl}
|
|
alt={asset.file.name}
|
|
className="max-h-full max-w-full object-contain"
|
|
/>
|
|
) : (
|
|
getAssetIcon(asset.type)
|
|
)}
|
|
</div>
|
|
<p className="text-xs truncate">{asset.file.name}</p>
|
|
<p className="text-xs text-slate-500 truncate">
|
|
{(asset.file.size / 1024).toFixed(1)} KB
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|