mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 04:11:13 -05:00
feat: Implement enhanced multi-image upload
This commit is contained in:
@@ -12,7 +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';
|
import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader';
|
||||||
|
|
||||||
const designerSchema = z.object({
|
const designerSchema = z.object({
|
||||||
name: z.string().min(1, 'Name is required'),
|
name: z.string().min(1, 'Name is required'),
|
||||||
@@ -22,11 +22,15 @@ const designerSchema = z.object({
|
|||||||
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(),
|
images: z.object({
|
||||||
banner_image_id: z.string().optional(),
|
uploaded: z.array(z.object({
|
||||||
banner_image_url: z.string().optional(),
|
url: z.string(),
|
||||||
card_image_id: z.string().optional(),
|
cloudflare_id: z.string(),
|
||||||
card_image_url: z.string().optional()
|
caption: z.string().optional()
|
||||||
|
})),
|
||||||
|
banner_assignment: z.number().optional(),
|
||||||
|
card_assignment: z.number().optional()
|
||||||
|
}).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type DesignerFormData = z.infer<typeof designerSchema>;
|
type DesignerFormData = z.infer<typeof designerSchema>;
|
||||||
@@ -57,11 +61,7 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
|
|||||||
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 || '',
|
images: initialData?.images || { uploaded: [] }
|
||||||
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 || ''
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,20 +181,10 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Images */}
|
{/* Images */}
|
||||||
<EntityImageUploader
|
<EntityMultiImageUploader
|
||||||
images={{
|
mode={initialData ? 'edit' : 'create'}
|
||||||
logo: { url: watch('logo_url') },
|
value={watch('images') || { uploaded: [] }}
|
||||||
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
|
onChange={(images) => setValue('images', images)}
|
||||||
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"
|
entityType="designer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +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';
|
import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader';
|
||||||
|
|
||||||
const manufacturerSchema = z.object({
|
const manufacturerSchema = z.object({
|
||||||
name: z.string().min(1, 'Name is required'),
|
name: z.string().min(1, 'Name is required'),
|
||||||
@@ -22,11 +22,15 @@ const manufacturerSchema = z.object({
|
|||||||
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(),
|
images: z.object({
|
||||||
banner_image_id: z.string().optional(),
|
uploaded: z.array(z.object({
|
||||||
banner_image_url: z.string().optional(),
|
url: z.string(),
|
||||||
card_image_id: z.string().optional(),
|
cloudflare_id: z.string(),
|
||||||
card_image_url: z.string().optional()
|
caption: z.string().optional()
|
||||||
|
})),
|
||||||
|
banner_assignment: z.number().optional(),
|
||||||
|
card_assignment: z.number().optional()
|
||||||
|
}).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type ManufacturerFormData = z.infer<typeof manufacturerSchema>;
|
type ManufacturerFormData = z.infer<typeof manufacturerSchema>;
|
||||||
@@ -57,11 +61,7 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
|||||||
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 || '',
|
images: initialData?.images || { uploaded: [] }
|
||||||
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 || ''
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,20 +181,10 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Images */}
|
{/* Images */}
|
||||||
<EntityImageUploader
|
<EntityMultiImageUploader
|
||||||
images={{
|
mode={initialData ? 'edit' : 'create'}
|
||||||
logo: { url: watch('logo_url') },
|
value={watch('images') || { uploaded: [] }}
|
||||||
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
|
onChange={(images) => setValue('images', images)}
|
||||||
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"
|
entityType="manufacturer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +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';
|
import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader';
|
||||||
|
|
||||||
const operatorSchema = z.object({
|
const operatorSchema = z.object({
|
||||||
name: z.string().min(1, 'Name is required'),
|
name: z.string().min(1, 'Name is required'),
|
||||||
@@ -22,11 +22,15 @@ const operatorSchema = z.object({
|
|||||||
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(),
|
images: z.object({
|
||||||
banner_image_id: z.string().optional(),
|
uploaded: z.array(z.object({
|
||||||
banner_image_url: z.string().optional(),
|
url: z.string(),
|
||||||
card_image_id: z.string().optional(),
|
cloudflare_id: z.string(),
|
||||||
card_image_url: z.string().optional()
|
caption: z.string().optional()
|
||||||
|
})),
|
||||||
|
banner_assignment: z.number().optional(),
|
||||||
|
card_assignment: z.number().optional()
|
||||||
|
}).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type OperatorFormData = z.infer<typeof operatorSchema>;
|
type OperatorFormData = z.infer<typeof operatorSchema>;
|
||||||
@@ -57,11 +61,7 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
|||||||
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 || '',
|
images: initialData?.images || { uploaded: [] }
|
||||||
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 || ''
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,20 +181,10 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Images */}
|
{/* Images */}
|
||||||
<EntityImageUploader
|
<EntityMultiImageUploader
|
||||||
images={{
|
mode={initialData ? 'edit' : 'create'}
|
||||||
logo: { url: watch('logo_url') },
|
value={watch('images') || { uploaded: [] }}
|
||||||
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
|
onChange={(images) => setValue('images', images)}
|
||||||
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"
|
entityType="operator"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +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';
|
import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader';
|
||||||
|
|
||||||
const propertyOwnerSchema = z.object({
|
const propertyOwnerSchema = z.object({
|
||||||
name: z.string().min(1, 'Name is required'),
|
name: z.string().min(1, 'Name is required'),
|
||||||
@@ -22,11 +22,15 @@ const propertyOwnerSchema = z.object({
|
|||||||
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(),
|
images: z.object({
|
||||||
banner_image_id: z.string().optional(),
|
uploaded: z.array(z.object({
|
||||||
banner_image_url: z.string().optional(),
|
url: z.string(),
|
||||||
card_image_id: z.string().optional(),
|
cloudflare_id: z.string(),
|
||||||
card_image_url: z.string().optional()
|
caption: z.string().optional()
|
||||||
|
})),
|
||||||
|
banner_assignment: z.number().optional(),
|
||||||
|
card_assignment: z.number().optional()
|
||||||
|
}).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type PropertyOwnerFormData = z.infer<typeof propertyOwnerSchema>;
|
type PropertyOwnerFormData = z.infer<typeof propertyOwnerSchema>;
|
||||||
@@ -57,11 +61,7 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
|||||||
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 || '',
|
images: initialData?.images || { uploaded: [] }
|
||||||
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 || ''
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,20 +181,10 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Images */}
|
{/* Images */}
|
||||||
<EntityImageUploader
|
<EntityMultiImageUploader
|
||||||
images={{
|
mode={initialData ? 'edit' : 'create'}
|
||||||
logo: { url: watch('logo_url') },
|
value={watch('images') || { uploaded: [] }}
|
||||||
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
|
onChange={(images) => setValue('images', images)}
|
||||||
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"
|
entityType="property_owner"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +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';
|
import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader';
|
||||||
|
|
||||||
const rideModelSchema = z.object({
|
const rideModelSchema = z.object({
|
||||||
name: z.string().min(1, 'Name is required'),
|
name: z.string().min(1, 'Name is required'),
|
||||||
@@ -20,10 +20,15 @@ const rideModelSchema = z.object({
|
|||||||
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(),
|
images: z.object({
|
||||||
banner_image_url: z.string().optional(),
|
uploaded: z.array(z.object({
|
||||||
card_image_id: z.string().optional(),
|
url: z.string(),
|
||||||
card_image_url: z.string().optional()
|
cloudflare_id: z.string(),
|
||||||
|
caption: z.string().optional()
|
||||||
|
})),
|
||||||
|
banner_assignment: z.number().optional(),
|
||||||
|
card_assignment: z.number().optional()
|
||||||
|
}).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type RideModelFormData = z.infer<typeof rideModelSchema>;
|
type RideModelFormData = z.infer<typeof rideModelSchema>;
|
||||||
@@ -69,10 +74,7 @@ export function RideModelForm({
|
|||||||
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 || '',
|
images: initialData?.images || { uploaded: [] }
|
||||||
banner_image_url: initialData?.banner_image_url || '',
|
|
||||||
card_image_id: initialData?.card_image_id || '',
|
|
||||||
card_image_url: initialData?.card_image_url || ''
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,18 +178,10 @@ export function RideModelForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Images */}
|
{/* Images */}
|
||||||
<EntityImageUploader
|
<EntityMultiImageUploader
|
||||||
images={{
|
mode={initialData ? 'edit' : 'create'}
|
||||||
banner: { url: watch('banner_image_url'), id: watch('banner_image_id') },
|
value={watch('images') || { uploaded: [] }}
|
||||||
card: { url: watch('card_image_url'), id: watch('card_image_id') }
|
onChange={(images) => setValue('images', images)}
|
||||||
}}
|
|
||||||
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"
|
entityType="ride_model"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
236
src/components/upload/EntityMultiImageUploader.tsx
Normal file
236
src/components/upload/EntityMultiImageUploader.tsx
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
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 { ImageIcon, Star, CreditCard, X } from 'lucide-react';
|
||||||
|
import { UppyPhotoUpload } from './UppyPhotoUpload';
|
||||||
|
|
||||||
|
export interface UploadedImage {
|
||||||
|
url: string;
|
||||||
|
cloudflare_id: string;
|
||||||
|
caption?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageAssignments {
|
||||||
|
uploaded: UploadedImage[];
|
||||||
|
banner_assignment?: number;
|
||||||
|
card_assignment?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntityMultiImageUploaderProps {
|
||||||
|
mode: 'create' | 'edit';
|
||||||
|
value: ImageAssignments;
|
||||||
|
onChange: (assignments: ImageAssignments) => void;
|
||||||
|
entityId?: string;
|
||||||
|
entityType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EntityMultiImageUploader({
|
||||||
|
mode,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
entityType = 'entity'
|
||||||
|
}: EntityMultiImageUploaderProps) {
|
||||||
|
const [showUploader, setShowUploader] = useState(false);
|
||||||
|
|
||||||
|
const maxImages = mode === 'create' ? 5 : 3;
|
||||||
|
const canUploadMore = value.uploaded.length < maxImages;
|
||||||
|
|
||||||
|
const handleUploadComplete = (urls: string[]) => {
|
||||||
|
const newImages: UploadedImage[] = urls.map(url => ({
|
||||||
|
url,
|
||||||
|
cloudflare_id: url.split('/').pop()?.split('?')[0] || ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updatedImages = [...value.uploaded, ...newImages].slice(0, maxImages);
|
||||||
|
|
||||||
|
// Auto-assign first image as banner and second as card if not assigned
|
||||||
|
const newAssignments: ImageAssignments = {
|
||||||
|
uploaded: updatedImages,
|
||||||
|
banner_assignment: value.banner_assignment ?? (updatedImages.length > 0 ? 0 : undefined),
|
||||||
|
card_assignment: value.card_assignment ?? (updatedImages.length > 1 ? 1 : undefined)
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange(newAssignments);
|
||||||
|
setShowUploader(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssignRole = (index: number, role: 'banner' | 'card') => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[role === 'banner' ? 'banner_assignment' : 'card_assignment']: index
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveImage = (index: number) => {
|
||||||
|
const newUploaded = value.uploaded.filter((_, i) => i !== index);
|
||||||
|
|
||||||
|
// Adjust assignments after removal
|
||||||
|
let newBannerAssignment = value.banner_assignment;
|
||||||
|
let newCardAssignment = value.card_assignment;
|
||||||
|
|
||||||
|
if (value.banner_assignment === index) {
|
||||||
|
newBannerAssignment = undefined;
|
||||||
|
} else if (value.banner_assignment !== undefined && value.banner_assignment > index) {
|
||||||
|
newBannerAssignment = value.banner_assignment - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.card_assignment === index) {
|
||||||
|
newCardAssignment = undefined;
|
||||||
|
} else if (value.card_assignment !== undefined && value.card_assignment > index) {
|
||||||
|
newCardAssignment = value.card_assignment - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
uploaded: newUploaded,
|
||||||
|
banner_assignment: newBannerAssignment,
|
||||||
|
card_assignment: newCardAssignment
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderImageCard = (image: UploadedImage, index: number) => {
|
||||||
|
const isBanner = value.banner_assignment === index;
|
||||||
|
const isCard = value.card_assignment === index;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={index} className="overflow-hidden relative group">
|
||||||
|
<div className="relative aspect-[16/9] bg-muted">
|
||||||
|
<img
|
||||||
|
src={image.url}
|
||||||
|
alt={`Upload ${index + 1}`}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Hover overlay with actions */}
|
||||||
|
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center justify-center gap-2 p-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={isBanner ? "default" : "secondary"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleAssignRole(index, 'banner')}
|
||||||
|
>
|
||||||
|
<Star className="w-3 h-3 mr-1" />
|
||||||
|
{isBanner ? 'Banner ✓' : 'Set Banner'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={isCard ? "default" : "secondary"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleAssignRole(index, 'card')}
|
||||||
|
>
|
||||||
|
<CreditCard className="w-3 h-3 mr-1" />
|
||||||
|
{isCard ? 'Card ✓' : 'Set Card'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleRemoveImage(index)}
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3 mr-1" />
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Role badges */}
|
||||||
|
<div className="absolute top-2 left-2 flex gap-1">
|
||||||
|
{isBanner && (
|
||||||
|
<Badge className="bg-primary text-primary-foreground">
|
||||||
|
<Star className="w-3 h-3 mr-1" />
|
||||||
|
Banner
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{isCard && (
|
||||||
|
<Badge className="bg-secondary text-secondary-foreground">
|
||||||
|
<CreditCard className="w-3 h-3 mr-1" />
|
||||||
|
Card
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label className="text-base">Images</Label>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{value.uploaded.length} / {maxImages} images
|
||||||
|
</Badge>
|
||||||
|
{mode === 'create' && (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
Upload up to 5, assign banner & card roles
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upload section */}
|
||||||
|
{canUploadMore && (
|
||||||
|
<div>
|
||||||
|
{!showUploader ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowUploader(true)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<ImageIcon className="w-4 h-4 mr-2" />
|
||||||
|
{value.uploaded.length === 0 ? 'Upload Images' : 'Upload More Images'}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-sm font-semibold">Upload Images</Label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowUploader(false)}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<UppyPhotoUpload
|
||||||
|
onUploadComplete={handleUploadComplete}
|
||||||
|
maxFiles={maxImages - value.uploaded.length}
|
||||||
|
variant="compact"
|
||||||
|
allowedFileTypes={['image/jpeg', 'image/jpg', 'image/png', 'image/webp']}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Image grid */}
|
||||||
|
{value.uploaded.length > 0 && (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||||
|
{value.uploaded.map((image, index) => renderImageCard(image, index))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Help text */}
|
||||||
|
{value.uploaded.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-muted-foreground text-sm">
|
||||||
|
<ImageIcon className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||||
|
<p>Upload images and assign which should be used as banner and card images</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{value.uploaded.length > 0 && (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
<p>• Click images to assign as Banner (header) or Card (thumbnail)</p>
|
||||||
|
<p>• Banner and Card can be the same image or different images</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader';
|
||||||
|
|
||||||
export interface CompanyFormData {
|
export interface CompanyFormData {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -8,6 +9,7 @@ export interface CompanyFormData {
|
|||||||
website_url?: string;
|
website_url?: string;
|
||||||
founded_year?: number;
|
founded_year?: number;
|
||||||
headquarters_location?: string;
|
headquarters_location?: string;
|
||||||
|
images?: ImageAssignments;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitCompanyCreation(
|
export async function submitCompanyCreation(
|
||||||
@@ -21,7 +23,13 @@ export async function submitCompanyCreation(
|
|||||||
const { data: newCompany, error } = await supabase
|
const { data: newCompany, error } = await supabase
|
||||||
.from('companies')
|
.from('companies')
|
||||||
.insert({
|
.insert({
|
||||||
...data,
|
name: data.name,
|
||||||
|
slug: data.slug,
|
||||||
|
description: data.description,
|
||||||
|
person_type: data.person_type,
|
||||||
|
website_url: data.website_url,
|
||||||
|
founded_year: data.founded_year,
|
||||||
|
headquarters_location: data.headquarters_location,
|
||||||
company_type: companyType
|
company_type: companyType
|
||||||
})
|
})
|
||||||
.select()
|
.select()
|
||||||
@@ -33,15 +41,22 @@ export async function submitCompanyCreation(
|
|||||||
// Regular users submit for moderation
|
// Regular users submit for moderation
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
.insert({
|
.insert([{
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
submission_type: 'company_create',
|
submission_type: 'company_create',
|
||||||
content: {
|
content: {
|
||||||
...data,
|
name: data.name,
|
||||||
company_type: companyType
|
slug: data.slug,
|
||||||
},
|
description: data.description,
|
||||||
|
person_type: data.person_type,
|
||||||
|
website_url: data.website_url,
|
||||||
|
founded_year: data.founded_year,
|
||||||
|
headquarters_location: data.headquarters_location,
|
||||||
|
company_type: companyType,
|
||||||
|
images: data.images as any // Include image assignments in submission
|
||||||
|
} as any,
|
||||||
status: 'pending'
|
status: 'pending'
|
||||||
});
|
}]);
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
return { company: null, submitted: true };
|
return { company: null, submitted: true };
|
||||||
@@ -59,7 +74,13 @@ export async function submitCompanyUpdate(
|
|||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('companies')
|
.from('companies')
|
||||||
.update({
|
.update({
|
||||||
...data,
|
name: data.name,
|
||||||
|
slug: data.slug,
|
||||||
|
description: data.description,
|
||||||
|
person_type: data.person_type,
|
||||||
|
website_url: data.website_url,
|
||||||
|
founded_year: data.founded_year,
|
||||||
|
headquarters_location: data.headquarters_location,
|
||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
})
|
})
|
||||||
.eq('id', companyId);
|
.eq('id', companyId);
|
||||||
@@ -70,15 +91,22 @@ export async function submitCompanyUpdate(
|
|||||||
// Regular users submit for moderation
|
// Regular users submit for moderation
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
.insert({
|
.insert([{
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
submission_type: 'company_edit',
|
submission_type: 'company_edit',
|
||||||
content: {
|
content: {
|
||||||
company_id: companyId,
|
company_id: companyId,
|
||||||
...data
|
name: data.name,
|
||||||
},
|
slug: data.slug,
|
||||||
|
description: data.description,
|
||||||
|
person_type: data.person_type,
|
||||||
|
website_url: data.website_url,
|
||||||
|
founded_year: data.founded_year,
|
||||||
|
headquarters_location: data.headquarters_location,
|
||||||
|
images: data.images as any // Include image role assignments in submission
|
||||||
|
} as any,
|
||||||
status: 'pending'
|
status: 'pending'
|
||||||
});
|
}]);
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
return { submitted: true };
|
return { submitted: true };
|
||||||
|
|||||||
Reference in New Issue
Block a user