semblance-dev/src/components/AssetUploader.tsx
2025-08-04 09:07:59 -05:00

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>
);
}