mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:31:13 -05:00
Refactor: Update image upload components
This commit is contained in:
@@ -12,6 +12,7 @@ import { Ruler, Save, X } from 'lucide-react';
|
|||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
|
import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
|
import { EntityImageUploader } from '@/components/upload/EntityImageUploader';
|
||||||
|
|
||||||
const designerSchema = z.object({
|
const designerSchema = z.object({
|
||||||
name: z.string().min(1, 'Name is required'),
|
name: z.string().min(1, 'Name is required'),
|
||||||
@@ -20,7 +21,12 @@ const designerSchema = z.object({
|
|||||||
person_type: z.enum(['company', 'individual', 'firm', 'organization']),
|
person_type: z.enum(['company', 'individual', 'firm', 'organization']),
|
||||||
website_url: z.string().url().optional().or(z.literal('')),
|
website_url: z.string().url().optional().or(z.literal('')),
|
||||||
founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(),
|
founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(),
|
||||||
headquarters_location: z.string().optional()
|
headquarters_location: z.string().optional(),
|
||||||
|
logo_url: z.string().optional(),
|
||||||
|
banner_image_id: z.string().optional(),
|
||||||
|
banner_image_url: z.string().optional(),
|
||||||
|
card_image_id: z.string().optional(),
|
||||||
|
card_image_url: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type DesignerFormData = z.infer<typeof designerSchema>;
|
type DesignerFormData = z.infer<typeof designerSchema>;
|
||||||
@@ -50,7 +56,12 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
|
|||||||
person_type: initialData?.person_type || 'company',
|
person_type: initialData?.person_type || 'company',
|
||||||
website_url: initialData?.website_url || '',
|
website_url: initialData?.website_url || '',
|
||||||
founded_year: initialData?.founded_year || undefined,
|
founded_year: initialData?.founded_year || undefined,
|
||||||
headquarters_location: initialData?.headquarters_location || ''
|
headquarters_location: initialData?.headquarters_location || '',
|
||||||
|
logo_url: initialData?.logo_url || '',
|
||||||
|
banner_image_id: initialData?.banner_image_id || '',
|
||||||
|
banner_image_url: initialData?.banner_image_url || '',
|
||||||
|
card_image_id: initialData?.card_image_id || '',
|
||||||
|
card_image_url: initialData?.card_image_url || ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,6 +180,24 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Images */}
|
||||||
|
<EntityImageUploader
|
||||||
|
images={{
|
||||||
|
logo: { url: watch('logo_url') },
|
||||||
|
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
|
||||||
|
card: { url: watch('card_image_url'), id: watch('card_image_id') }
|
||||||
|
}}
|
||||||
|
onImagesChange={(images) => {
|
||||||
|
if (images.logo_url !== undefined) setValue('logo_url', images.logo_url);
|
||||||
|
if (images.banner_image_id !== undefined) setValue('banner_image_id', images.banner_image_id);
|
||||||
|
if (images.banner_image_url !== undefined) setValue('banner_image_url', images.banner_image_url);
|
||||||
|
if (images.card_image_id !== undefined) setValue('card_image_id', images.card_image_id);
|
||||||
|
if (images.card_image_url !== undefined) setValue('card_image_url', images.card_image_url);
|
||||||
|
}}
|
||||||
|
showLogo={true}
|
||||||
|
entityType="designer"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-3 justify-end">
|
<div className="flex gap-3 justify-end">
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Building2, Save, X } from 'lucide-react';
|
|||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
|
import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
|
import { EntityImageUploader } from '@/components/upload/EntityImageUploader';
|
||||||
|
|
||||||
const manufacturerSchema = z.object({
|
const manufacturerSchema = z.object({
|
||||||
name: z.string().min(1, 'Name is required'),
|
name: z.string().min(1, 'Name is required'),
|
||||||
@@ -20,7 +21,12 @@ const manufacturerSchema = z.object({
|
|||||||
person_type: z.enum(['company', 'individual', 'firm', 'organization']),
|
person_type: z.enum(['company', 'individual', 'firm', 'organization']),
|
||||||
website_url: z.string().url().optional().or(z.literal('')),
|
website_url: z.string().url().optional().or(z.literal('')),
|
||||||
founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(),
|
founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(),
|
||||||
headquarters_location: z.string().optional()
|
headquarters_location: z.string().optional(),
|
||||||
|
logo_url: z.string().optional(),
|
||||||
|
banner_image_id: z.string().optional(),
|
||||||
|
banner_image_url: z.string().optional(),
|
||||||
|
card_image_id: z.string().optional(),
|
||||||
|
card_image_url: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type ManufacturerFormData = z.infer<typeof manufacturerSchema>;
|
type ManufacturerFormData = z.infer<typeof manufacturerSchema>;
|
||||||
@@ -50,7 +56,12 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
|||||||
person_type: initialData?.person_type || 'company',
|
person_type: initialData?.person_type || 'company',
|
||||||
website_url: initialData?.website_url || '',
|
website_url: initialData?.website_url || '',
|
||||||
founded_year: initialData?.founded_year || undefined,
|
founded_year: initialData?.founded_year || undefined,
|
||||||
headquarters_location: initialData?.headquarters_location || ''
|
headquarters_location: initialData?.headquarters_location || '',
|
||||||
|
logo_url: initialData?.logo_url || '',
|
||||||
|
banner_image_id: initialData?.banner_image_id || '',
|
||||||
|
banner_image_url: initialData?.banner_image_url || '',
|
||||||
|
card_image_id: initialData?.card_image_id || '',
|
||||||
|
card_image_url: initialData?.card_image_url || ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,6 +180,24 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Images */}
|
||||||
|
<EntityImageUploader
|
||||||
|
images={{
|
||||||
|
logo: { url: watch('logo_url') },
|
||||||
|
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
|
||||||
|
card: { url: watch('card_image_url'), id: watch('card_image_id') }
|
||||||
|
}}
|
||||||
|
onImagesChange={(images) => {
|
||||||
|
if (images.logo_url !== undefined) setValue('logo_url', images.logo_url);
|
||||||
|
if (images.banner_image_id !== undefined) setValue('banner_image_id', images.banner_image_id);
|
||||||
|
if (images.banner_image_url !== undefined) setValue('banner_image_url', images.banner_image_url);
|
||||||
|
if (images.card_image_id !== undefined) setValue('card_image_id', images.card_image_id);
|
||||||
|
if (images.card_image_url !== undefined) setValue('card_image_url', images.card_image_url);
|
||||||
|
}}
|
||||||
|
showLogo={true}
|
||||||
|
entityType="manufacturer"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-3 justify-end">
|
<div className="flex gap-3 justify-end">
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { FerrisWheel, Save, X } from 'lucide-react';
|
|||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
|
import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
|
import { EntityImageUploader } from '@/components/upload/EntityImageUploader';
|
||||||
|
|
||||||
const operatorSchema = z.object({
|
const operatorSchema = z.object({
|
||||||
name: z.string().min(1, 'Name is required'),
|
name: z.string().min(1, 'Name is required'),
|
||||||
@@ -20,7 +21,12 @@ const operatorSchema = z.object({
|
|||||||
person_type: z.enum(['company', 'individual', 'firm', 'organization']),
|
person_type: z.enum(['company', 'individual', 'firm', 'organization']),
|
||||||
website_url: z.string().url().optional().or(z.literal('')),
|
website_url: z.string().url().optional().or(z.literal('')),
|
||||||
founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(),
|
founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(),
|
||||||
headquarters_location: z.string().optional()
|
headquarters_location: z.string().optional(),
|
||||||
|
logo_url: z.string().optional(),
|
||||||
|
banner_image_id: z.string().optional(),
|
||||||
|
banner_image_url: z.string().optional(),
|
||||||
|
card_image_id: z.string().optional(),
|
||||||
|
card_image_url: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type OperatorFormData = z.infer<typeof operatorSchema>;
|
type OperatorFormData = z.infer<typeof operatorSchema>;
|
||||||
@@ -50,7 +56,12 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
|||||||
person_type: initialData?.person_type || 'company',
|
person_type: initialData?.person_type || 'company',
|
||||||
website_url: initialData?.website_url || '',
|
website_url: initialData?.website_url || '',
|
||||||
founded_year: initialData?.founded_year || undefined,
|
founded_year: initialData?.founded_year || undefined,
|
||||||
headquarters_location: initialData?.headquarters_location || ''
|
headquarters_location: initialData?.headquarters_location || '',
|
||||||
|
logo_url: initialData?.logo_url || '',
|
||||||
|
banner_image_id: initialData?.banner_image_id || '',
|
||||||
|
banner_image_url: initialData?.banner_image_url || '',
|
||||||
|
card_image_id: initialData?.card_image_id || '',
|
||||||
|
card_image_url: initialData?.card_image_url || ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,6 +180,24 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Images */}
|
||||||
|
<EntityImageUploader
|
||||||
|
images={{
|
||||||
|
logo: { url: watch('logo_url') },
|
||||||
|
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
|
||||||
|
card: { url: watch('card_image_url'), id: watch('card_image_id') }
|
||||||
|
}}
|
||||||
|
onImagesChange={(images) => {
|
||||||
|
if (images.logo_url !== undefined) setValue('logo_url', images.logo_url);
|
||||||
|
if (images.banner_image_id !== undefined) setValue('banner_image_id', images.banner_image_id);
|
||||||
|
if (images.banner_image_url !== undefined) setValue('banner_image_url', images.banner_image_url);
|
||||||
|
if (images.card_image_id !== undefined) setValue('card_image_id', images.card_image_id);
|
||||||
|
if (images.card_image_url !== undefined) setValue('card_image_url', images.card_image_url);
|
||||||
|
}}
|
||||||
|
showLogo={true}
|
||||||
|
entityType="operator"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-3 justify-end">
|
<div className="flex gap-3 justify-end">
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Building2, Save, X } from 'lucide-react';
|
|||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
|
import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
|
import { EntityImageUploader } from '@/components/upload/EntityImageUploader';
|
||||||
|
|
||||||
const propertyOwnerSchema = z.object({
|
const propertyOwnerSchema = z.object({
|
||||||
name: z.string().min(1, 'Name is required'),
|
name: z.string().min(1, 'Name is required'),
|
||||||
@@ -20,7 +21,12 @@ const propertyOwnerSchema = z.object({
|
|||||||
person_type: z.enum(['company', 'individual', 'firm', 'organization']),
|
person_type: z.enum(['company', 'individual', 'firm', 'organization']),
|
||||||
website_url: z.string().url().optional().or(z.literal('')),
|
website_url: z.string().url().optional().or(z.literal('')),
|
||||||
founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(),
|
founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(),
|
||||||
headquarters_location: z.string().optional()
|
headquarters_location: z.string().optional(),
|
||||||
|
logo_url: z.string().optional(),
|
||||||
|
banner_image_id: z.string().optional(),
|
||||||
|
banner_image_url: z.string().optional(),
|
||||||
|
card_image_id: z.string().optional(),
|
||||||
|
card_image_url: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type PropertyOwnerFormData = z.infer<typeof propertyOwnerSchema>;
|
type PropertyOwnerFormData = z.infer<typeof propertyOwnerSchema>;
|
||||||
@@ -50,7 +56,12 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
|||||||
person_type: initialData?.person_type || 'company',
|
person_type: initialData?.person_type || 'company',
|
||||||
website_url: initialData?.website_url || '',
|
website_url: initialData?.website_url || '',
|
||||||
founded_year: initialData?.founded_year || undefined,
|
founded_year: initialData?.founded_year || undefined,
|
||||||
headquarters_location: initialData?.headquarters_location || ''
|
headquarters_location: initialData?.headquarters_location || '',
|
||||||
|
logo_url: initialData?.logo_url || '',
|
||||||
|
banner_image_id: initialData?.banner_image_id || '',
|
||||||
|
banner_image_url: initialData?.banner_image_url || '',
|
||||||
|
card_image_id: initialData?.card_image_id || '',
|
||||||
|
card_image_url: initialData?.card_image_url || ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,6 +180,24 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Images */}
|
||||||
|
<EntityImageUploader
|
||||||
|
images={{
|
||||||
|
logo: { url: watch('logo_url') },
|
||||||
|
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
|
||||||
|
card: { url: watch('card_image_url'), id: watch('card_image_id') }
|
||||||
|
}}
|
||||||
|
onImagesChange={(images) => {
|
||||||
|
if (images.logo_url !== undefined) setValue('logo_url', images.logo_url);
|
||||||
|
if (images.banner_image_id !== undefined) setValue('banner_image_id', images.banner_image_id);
|
||||||
|
if (images.banner_image_url !== undefined) setValue('banner_image_url', images.banner_image_url);
|
||||||
|
if (images.card_image_id !== undefined) setValue('card_image_id', images.card_image_id);
|
||||||
|
if (images.card_image_url !== undefined) setValue('card_image_url', images.card_image_url);
|
||||||
|
}}
|
||||||
|
showLogo={true}
|
||||||
|
entityType="property_owner"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-3 justify-end">
|
<div className="flex gap-3 justify-end">
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { SlugField } from '@/components/ui/slug-field';
|
import { SlugField } from '@/components/ui/slug-field';
|
||||||
import { Layers, Save, X } from 'lucide-react';
|
import { Layers, Save, X } from 'lucide-react';
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
|
import { EntityImageUploader } from '@/components/upload/EntityImageUploader';
|
||||||
|
|
||||||
const rideModelSchema = z.object({
|
const rideModelSchema = z.object({
|
||||||
name: z.string().min(1, 'Name is required'),
|
name: z.string().min(1, 'Name is required'),
|
||||||
@@ -18,7 +19,11 @@ const rideModelSchema = z.object({
|
|||||||
category: z.string().min(1, 'Category is required'),
|
category: z.string().min(1, 'Category is required'),
|
||||||
ride_type: z.string().min(1, 'Ride type is required'),
|
ride_type: z.string().min(1, 'Ride type is required'),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
technical_specs: z.string().optional()
|
technical_specs: z.string().optional(),
|
||||||
|
banner_image_id: z.string().optional(),
|
||||||
|
banner_image_url: z.string().optional(),
|
||||||
|
card_image_id: z.string().optional(),
|
||||||
|
card_image_url: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type RideModelFormData = z.infer<typeof rideModelSchema>;
|
type RideModelFormData = z.infer<typeof rideModelSchema>;
|
||||||
@@ -63,7 +68,11 @@ export function RideModelForm({
|
|||||||
category: initialData?.category || '',
|
category: initialData?.category || '',
|
||||||
ride_type: initialData?.ride_type || '',
|
ride_type: initialData?.ride_type || '',
|
||||||
description: initialData?.description || '',
|
description: initialData?.description || '',
|
||||||
technical_specs: initialData?.technical_specs || ''
|
technical_specs: initialData?.technical_specs || '',
|
||||||
|
banner_image_id: initialData?.banner_image_id || '',
|
||||||
|
banner_image_url: initialData?.banner_image_url || '',
|
||||||
|
card_image_id: initialData?.card_image_id || '',
|
||||||
|
card_image_url: initialData?.card_image_url || ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,6 +175,22 @@ export function RideModelForm({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Images */}
|
||||||
|
<EntityImageUploader
|
||||||
|
images={{
|
||||||
|
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
|
||||||
|
card: { url: watch('card_image_url'), id: watch('card_image_id') }
|
||||||
|
}}
|
||||||
|
onImagesChange={(images) => {
|
||||||
|
if (images.banner_image_id !== undefined) setValue('banner_image_id', images.banner_image_id);
|
||||||
|
if (images.banner_image_url !== undefined) setValue('banner_image_url', images.banner_image_url);
|
||||||
|
if (images.card_image_id !== undefined) setValue('card_image_id', images.card_image_id);
|
||||||
|
if (images.card_image_url !== undefined) setValue('card_image_url', images.card_image_url);
|
||||||
|
}}
|
||||||
|
showLogo={false}
|
||||||
|
entityType="ride_model"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-3 justify-end">
|
<div className="flex gap-3 justify-end">
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
|
|||||||
202
src/components/upload/EntityImageUploader.tsx
Normal file
202
src/components/upload/EntityImageUploader.tsx
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -77,6 +77,10 @@ export type Database = {
|
|||||||
companies: {
|
companies: {
|
||||||
Row: {
|
Row: {
|
||||||
average_rating: number | null
|
average_rating: number | null
|
||||||
|
banner_image_id: string | null
|
||||||
|
banner_image_url: string | null
|
||||||
|
card_image_id: string | null
|
||||||
|
card_image_url: string | null
|
||||||
company_type: string
|
company_type: string
|
||||||
created_at: string
|
created_at: string
|
||||||
description: string | null
|
description: string | null
|
||||||
@@ -93,6 +97,10 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
average_rating?: number | null
|
average_rating?: number | null
|
||||||
|
banner_image_id?: string | null
|
||||||
|
banner_image_url?: string | null
|
||||||
|
card_image_id?: string | null
|
||||||
|
card_image_url?: string | null
|
||||||
company_type: string
|
company_type: string
|
||||||
created_at?: string
|
created_at?: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
@@ -109,6 +117,10 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
average_rating?: number | null
|
average_rating?: number | null
|
||||||
|
banner_image_id?: string | null
|
||||||
|
banner_image_url?: string | null
|
||||||
|
card_image_id?: string | null
|
||||||
|
card_image_url?: string | null
|
||||||
company_type?: string
|
company_type?: string
|
||||||
created_at?: string
|
created_at?: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
@@ -806,6 +818,10 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
ride_models: {
|
ride_models: {
|
||||||
Row: {
|
Row: {
|
||||||
|
banner_image_id: string | null
|
||||||
|
banner_image_url: string | null
|
||||||
|
card_image_id: string | null
|
||||||
|
card_image_url: string | null
|
||||||
category: string
|
category: string
|
||||||
created_at: string
|
created_at: string
|
||||||
description: string | null
|
description: string | null
|
||||||
@@ -818,6 +834,10 @@ export type Database = {
|
|||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
|
banner_image_id?: string | null
|
||||||
|
banner_image_url?: string | null
|
||||||
|
card_image_id?: string | null
|
||||||
|
card_image_url?: string | null
|
||||||
category: string
|
category: string
|
||||||
created_at?: string
|
created_at?: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
@@ -830,6 +850,10 @@ export type Database = {
|
|||||||
updated_at?: string
|
updated_at?: string
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
|
banner_image_id?: string | null
|
||||||
|
banner_image_url?: string | null
|
||||||
|
card_image_id?: string | null
|
||||||
|
card_image_url?: string | null
|
||||||
category?: string
|
category?: string
|
||||||
created_at?: string
|
created_at?: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- Add image fields to companies table
|
||||||
|
ALTER TABLE public.companies
|
||||||
|
ADD COLUMN IF NOT EXISTS banner_image_id TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS banner_image_url TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS card_image_id TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS card_image_url TEXT;
|
||||||
|
|
||||||
|
-- Add image fields to ride_models table
|
||||||
|
ALTER TABLE public.ride_models
|
||||||
|
ADD COLUMN IF NOT EXISTS banner_image_id TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS banner_image_url TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS card_image_id TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS card_image_url TEXT;
|
||||||
Reference in New Issue
Block a user