Files
thrilltrack-explorer/src-old/components/upload/EntityImageUploader.tsx

212 lines
6.8 KiB
TypeScript

import { useState } from 'react';
import { Card } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Image as ImageIcon, ImagePlus, X } from 'lucide-react';
import { UppyPhotoUploadLazy } from './UppyPhotoUploadLazy';
import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils';
export type ImageType = 'logo' | 'banner' | 'card';
interface ImageSlot {
type: ImageType;
url?: string;
id?: string;
}
interface EntityImageUploaderProps {
images: {
logo?: { url?: string; id?: string };
banner?: { url?: string; id?: string };
card?: { url?: string; id?: string };
};
onImagesChange: (images: {
logo_url?: string;
banner_image_id?: string;
banner_image_url?: string;
card_image_id?: string;
card_image_url?: string;
}) => void;
showLogo?: boolean;
entityType?: string;
}
const IMAGE_SPECS = {
logo: { label: 'Logo', aspect: '1:1', dimensions: '400x400', description: 'Square logo image' },
banner: { label: 'Banner', aspect: '21:9', dimensions: '1920x820', description: 'Wide header image' },
card: { label: 'Card', aspect: '16:9', dimensions: '1200x675', description: 'Preview thumbnail' }
};
export function EntityImageUploader({
images,
onImagesChange,
showLogo = true,
entityType = 'entity'
}: EntityImageUploaderProps) {
const [activeSlot, setActiveSlot] = useState<ImageType | null>(null);
const handleUploadComplete = (type: ImageType, urls: string[]) => {
if (urls.length === 0) return;
const url = urls[0];
const id = url.split('/').pop()?.split('?')[0] || '';
const updates: any = {};
if (type === 'logo') {
updates.logo_url = url;
} else if (type === 'banner') {
updates.banner_image_id = id;
updates.banner_image_url = url;
} else if (type === 'card') {
updates.card_image_id = id;
updates.card_image_url = url;
}
onImagesChange({
logo_url: type === 'logo' ? url : images.logo?.url,
banner_image_id: type === 'banner' ? id : images.banner?.id,
banner_image_url: type === 'banner' ? url : images.banner?.url,
card_image_id: type === 'card' ? id : images.card?.id,
card_image_url: type === 'card' ? url : images.card?.url
});
setActiveSlot(null);
};
const handleRemoveImage = (type: ImageType) => {
const updates: any = {
logo_url: images.logo?.url,
banner_image_id: images.banner?.id,
banner_image_url: images.banner?.url,
card_image_id: images.card?.id,
card_image_url: images.card?.url
};
if (type === 'logo') {
updates.logo_url = undefined;
} else if (type === 'banner') {
updates.banner_image_id = undefined;
updates.banner_image_url = undefined;
} else if (type === 'card') {
updates.card_image_id = undefined;
updates.card_image_url = undefined;
}
onImagesChange(updates);
};
const renderImageSlot = (type: ImageType) => {
if (type === 'logo' && !showLogo) return null;
const spec = IMAGE_SPECS[type];
const currentImage = type === 'logo' ? images.logo : type === 'banner' ? images.banner : images.card;
const hasImage = currentImage?.url;
if (activeSlot === type) {
return (
<Card key={type} className="p-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Label className="text-base font-semibold">{spec.label}</Label>
<Badge variant="outline" className="text-xs">
{spec.aspect} {spec.dimensions}
</Badge>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setActiveSlot(null)}
>
<X className="w-4 h-4" />
</Button>
</div>
<p className="text-sm text-muted-foreground">{spec.description}</p>
<UppyPhotoUploadLazy
onUploadComplete={(urls) => handleUploadComplete(type, urls)}
maxFiles={1}
variant="compact"
allowedFileTypes={['image/jpeg', 'image/jpg', 'image/png', 'image/webp']}
/>
</div>
</Card>
);
}
return (
<Card key={type} className="overflow-hidden">
{hasImage ? (
<div className="relative aspect-[16/9] bg-muted">
<img
src={
currentImage.url || (
type === 'logo'
? getCloudflareImageUrl(currentImage.id, 'logo')
: type === 'banner'
? getCloudflareImageUrl(currentImage.id, 'banner')
: getCloudflareImageUrl(currentImage.id, 'card')
)
}
alt={`${spec.label} preview`}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black/50 opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<Button
type="button"
variant="secondary"
size="sm"
onClick={() => setActiveSlot(type)}
>
<ImagePlus className="w-4 h-4 mr-2" />
Replace
</Button>
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => handleRemoveImage(type)}
>
<X className="w-4 h-4 mr-2" />
Remove
</Button>
</div>
<Badge className="absolute top-2 left-2">{spec.label}</Badge>
</div>
) : (
<button
type="button"
onClick={() => setActiveSlot(type)}
className="w-full aspect-[16/9] flex flex-col items-center justify-center gap-2 bg-muted hover:bg-muted/80 transition-colors"
>
<ImageIcon className="w-8 h-8 text-muted-foreground" />
<div className="text-center">
<p className="text-sm font-medium">{spec.label}</p>
<p className="text-xs text-muted-foreground">{spec.dimensions}</p>
</div>
</button>
)}
</Card>
);
};
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Label className="text-base">Images</Label>
<Badge variant="secondary" className="text-xs">
Recommended formats: JPG, PNG, WebP
</Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{showLogo && renderImageSlot('logo')}
{renderImageSlot('banner')}
{renderImageSlot('card')}
</div>
</div>
);
}