mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
212 lines
6.8 KiB
TypeScript
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 { UppyPhotoUpload } from './UppyPhotoUpload';
|
|
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>
|
|
<UppyPhotoUpload
|
|
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>
|
|
);
|
|
}
|