feat: Implement comprehensive plan

This commit is contained in:
gpt-engineer-app[bot]
2025-09-29 20:31:28 +00:00
parent 07b036bb7d
commit f586b31954
6 changed files with 990 additions and 19 deletions

View File

@@ -0,0 +1,207 @@
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Building2, Save, X } from 'lucide-react';
import { Combobox } from '@/components/ui/combobox';
import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData';
const manufacturerSchema = z.object({
name: z.string().min(1, 'Name is required'),
slug: z.string().min(1, 'Slug is required'),
description: z.string().optional(),
person_type: z.enum(['company', 'individual', 'firm', 'organization']),
website_url: z.string().url().optional().or(z.literal('')),
founded_year: z.number().min(1800).max(new Date().getFullYear()).optional(),
headquarters_location: z.string().optional()
});
type ManufacturerFormData = z.infer<typeof manufacturerSchema>;
interface ManufacturerFormProps {
onSubmit: (data: ManufacturerFormData) => void;
onCancel: () => void;
initialData?: Partial<ManufacturerFormData>;
}
export function ManufacturerForm({ onSubmit, onCancel, initialData }: ManufacturerFormProps) {
const { headquarters } = useCompanyHeadquarters();
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors }
} = useForm<ManufacturerFormData>({
resolver: zodResolver(manufacturerSchema),
defaultValues: {
name: initialData?.name || '',
slug: initialData?.slug || '',
description: initialData?.description || '',
person_type: initialData?.person_type || 'company',
website_url: initialData?.website_url || '',
founded_year: initialData?.founded_year || undefined,
headquarters_location: initialData?.headquarters_location || ''
}
});
const generateSlug = (name: string) => {
return name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
};
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const name = e.target.value;
const slug = generateSlug(name);
setValue('slug', slug);
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building2 className="w-5 h-5" />
{initialData ? 'Edit Manufacturer' : 'Create New Manufacturer'}
</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Basic Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="name">Name *</Label>
<Input
id="name"
{...register('name')}
onChange={(e) => {
register('name').onChange(e);
handleNameChange(e);
}}
placeholder="Enter manufacturer name"
/>
{errors.name && (
<p className="text-sm text-destructive">{errors.name.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="slug">URL Slug *</Label>
<Input
id="slug"
{...register('slug')}
placeholder="manufacturer-slug"
/>
{errors.slug && (
<p className="text-sm text-destructive">{errors.slug.message}</p>
)}
</div>
</div>
{/* Description */}
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
{...register('description')}
placeholder="Describe the manufacturer..."
rows={3}
/>
</div>
{/* Person Type */}
<div className="space-y-2">
<Label>Entity Type *</Label>
<RadioGroup
value={watch('person_type')}
onValueChange={(value) => setValue('person_type', value as any)}
className="grid grid-cols-2 md:grid-cols-4 gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="company" id="company" />
<Label htmlFor="company" className="cursor-pointer">Company</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="individual" id="individual" />
<Label htmlFor="individual" className="cursor-pointer">Individual</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="firm" id="firm" />
<Label htmlFor="firm" className="cursor-pointer">Firm</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="organization" id="organization" />
<Label htmlFor="organization" className="cursor-pointer">Organization</Label>
</div>
</RadioGroup>
</div>
{/* Additional Details */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="founded_year">Founded Year</Label>
<Input
id="founded_year"
type="number"
min="1800"
max={new Date().getFullYear()}
{...register('founded_year', { valueAsNumber: true })}
placeholder="e.g. 1972"
/>
{errors.founded_year && (
<p className="text-sm text-destructive">{errors.founded_year.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="headquarters_location">Headquarters Location</Label>
<Combobox
options={headquarters}
value={watch('headquarters_location')}
onValueChange={(value) => setValue('headquarters_location', value)}
placeholder="Select or type location"
searchPlaceholder="Search locations..."
emptyText="No locations found"
/>
</div>
</div>
{/* Website */}
<div className="space-y-2">
<Label htmlFor="website_url">Website URL</Label>
<Input
id="website_url"
type="url"
{...register('website_url')}
placeholder="https://example.com"
/>
{errors.website_url && (
<p className="text-sm text-destructive">{errors.website_url.message}</p>
)}
</div>
{/* Actions */}
<div className="flex gap-3 justify-end">
<Button type="button" variant="outline" onClick={onCancel}>
<X className="w-4 h-4 mr-2" />
Cancel
</Button>
<Button type="submit">
<Save className="w-4 h-4 mr-2" />
Save Manufacturer
</Button>
</div>
</form>
</CardContent>
</Card>
);
}

View File

@@ -7,12 +7,18 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { PhotoUpload } from '@/components/upload/PhotoUpload';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { DatePicker } from '@/components/ui/date-picker';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Combobox } from '@/components/ui/combobox';
import { toast } from '@/hooks/use-toast';
import { Zap, Save, X } from 'lucide-react';
import { Plus, Zap, Save, X } from 'lucide-react';
import { useUnitPreferences } from '@/hooks/useUnitPreferences';
import { useManufacturers, useRideModels } from '@/hooks/useAutocompleteData';
import { ManufacturerForm } from './ManufacturerForm';
import { RideModelForm } from './RideModelForm';
import {
convertSpeed,
convertDistance,
@@ -50,7 +56,10 @@ const rideSchema = z.object({
max_g_force: z.number().optional(),
former_names: z.string().optional(),
coaster_stats: z.string().optional(),
technical_specs: z.string().optional()
technical_specs: z.string().optional(),
// Manufacturer and model
manufacturer_id: z.string().uuid().optional(),
ride_model_id: z.string().uuid().optional()
});
type RideFormData = z.infer<typeof rideSchema>;
@@ -111,6 +120,20 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
const { preferences } = useUnitPreferences();
const measurementSystem = preferences.measurement_system;
// Manufacturer and model state
const [selectedManufacturerId, setSelectedManufacturerId] = useState<string>(
initialData?.manufacturer_id || ''
);
const [selectedManufacturerName, setSelectedManufacturerName] = useState<string>('');
const [tempNewManufacturer, setTempNewManufacturer] = useState<any>(null);
const [tempNewRideModel, setTempNewRideModel] = useState<any>(null);
const [isManufacturerModalOpen, setIsManufacturerModalOpen] = useState(false);
const [isModelModalOpen, setIsModelModalOpen] = useState(false);
// Fetch data
const { manufacturers, loading: manufacturersLoading } = useManufacturers();
const { rideModels, loading: modelsLoading } = useRideModels(selectedManufacturerId);
const {
register,
handleSubmit,
@@ -154,7 +177,9 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
max_g_force: initialData?.max_g_force || undefined,
former_names: initialData?.former_names || '',
coaster_stats: initialData?.coaster_stats || '',
technical_specs: initialData?.technical_specs || ''
technical_specs: initialData?.technical_specs || '',
manufacturer_id: initialData?.manufacturer_id || undefined,
ride_model_id: initialData?.ride_model_id || undefined
}
});
@@ -199,13 +224,36 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
image_url: rideImage || undefined
};
await onSubmit(metricData);
// Build composite submission if new entities were created
const submissionContent: any = {
ride: metricData,
};
// Add new manufacturer if created
if (tempNewManufacturer) {
submissionContent.new_manufacturer = tempNewManufacturer;
submissionContent.ride.manufacturer_id = null; // Clear since using new
}
// Add new ride model if created
if (tempNewRideModel) {
submissionContent.new_ride_model = tempNewRideModel;
submissionContent.ride.ride_model_id = null; // Clear since using new
}
// Pass composite data to parent
await onSubmit({
...metricData,
_compositeSubmission: submissionContent
} as any);
toast({
title: isEditing ? "Ride Updated" : "Ride Created",
title: isEditing ? "Ride Updated" : "Submission Sent",
description: isEditing
? "The ride information has been updated successfully."
: "The new ride has been created successfully."
: tempNewManufacturer
? "Ride, manufacturer, and model submitted for review"
: "Ride submitted for review"
});
} catch (error: any) {
toast({
@@ -320,6 +368,144 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
</div>
</div>
{/* Manufacturer & Model Selection */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Manufacturer & Model</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Manufacturer Column */}
<div className="space-y-2">
<Label>Manufacturer</Label>
{tempNewManufacturer ? (
// Show temp manufacturer badge
<div className="flex items-center gap-2 p-3 border rounded-md bg-blue-50 dark:bg-blue-950">
<Badge variant="secondary">New</Badge>
<span className="font-medium">{tempNewManufacturer.name}</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setTempNewManufacturer(null);
setTempNewRideModel(null); // Clear model too
}}
>
<X className="w-4 h-4" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setIsManufacturerModalOpen(true)}
>
Edit
</Button>
</div>
) : (
// Show combobox for existing manufacturers
<Combobox
options={manufacturers}
value={watch('manufacturer_id')}
onValueChange={(value) => {
setValue('manufacturer_id', value);
setSelectedManufacturerId(value);
// Find and set manufacturer name
const mfr = manufacturers.find(m => m.value === value);
setSelectedManufacturerName(mfr?.label || '');
// Clear model when manufacturer changes
setValue('ride_model_id', undefined);
setTempNewRideModel(null);
}}
placeholder="Select manufacturer"
searchPlaceholder="Search manufacturers..."
emptyText="No manufacturers found"
loading={manufacturersLoading}
/>
)}
{/* Create New Manufacturer Button */}
{!tempNewManufacturer && (
<Button
type="button"
variant="outline"
size="sm"
className="w-full"
onClick={() => setIsManufacturerModalOpen(true)}
>
<Plus className="w-4 h-4 mr-2" />
Create New Manufacturer
</Button>
)}
</div>
{/* Ride Model Column - Conditional */}
{(selectedManufacturerId || tempNewManufacturer) && (
<div className="space-y-2">
<Label>Ride Model (Optional)</Label>
{tempNewRideModel ? (
// Show temp model badge
<div className="flex items-center gap-2 p-3 border rounded-md bg-purple-50 dark:bg-purple-950">
<Badge variant="secondary">New</Badge>
<span className="font-medium">{tempNewRideModel.name}</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setTempNewRideModel(null)}
>
<X className="w-4 h-4" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setIsModelModalOpen(true)}
>
Edit
</Button>
</div>
) : (
// Show combobox for existing models
<>
<Combobox
options={rideModels}
value={watch('ride_model_id')}
onValueChange={(value) => setValue('ride_model_id', value)}
placeholder="Select model"
searchPlaceholder="Search models..."
emptyText={tempNewManufacturer
? "Create the manufacturer first to add models"
: "No models found for this manufacturer"}
loading={modelsLoading}
disabled={!!tempNewManufacturer}
/>
{/* Create New Model Button */}
<Button
type="button"
variant="outline"
size="sm"
className="w-full"
onClick={() => setIsModelModalOpen(true)}
>
<Plus className="w-4 h-4 mr-2" />
Create New Model
</Button>
</>
)}
<p className="text-xs text-muted-foreground">
{tempNewManufacturer
? "New models will be created after manufacturer approval"
: "Select a specific model or leave blank"}
</p>
</div>
)}
</div>
</div>
{/* Dates */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
@@ -565,6 +751,61 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
)}
</div>
</form>
{/* Manufacturer Modal */}
<Dialog open={isManufacturerModalOpen} onOpenChange={setIsManufacturerModalOpen}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{tempNewManufacturer ? 'Edit New Manufacturer' : 'Create New Manufacturer'}
</DialogTitle>
<DialogDescription>
This manufacturer will be submitted for moderation along with the ride.
</DialogDescription>
</DialogHeader>
<ManufacturerForm
initialData={tempNewManufacturer}
onSubmit={(data) => {
setTempNewManufacturer(data);
setSelectedManufacturerName(data.name);
setIsManufacturerModalOpen(false);
// Clear existing manufacturer selection
setValue('manufacturer_id', undefined);
setSelectedManufacturerId('');
// Clear any existing model
setValue('ride_model_id', undefined);
setTempNewRideModel(null);
}}
onCancel={() => setIsManufacturerModalOpen(false)}
/>
</DialogContent>
</Dialog>
{/* Ride Model Modal */}
<Dialog open={isModelModalOpen} onOpenChange={setIsModelModalOpen}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{tempNewRideModel ? 'Edit New Ride Model' : 'Create New Ride Model'}
</DialogTitle>
<DialogDescription>
Creating a model for: <strong>{selectedManufacturerName || tempNewManufacturer?.name}</strong>
</DialogDescription>
</DialogHeader>
<RideModelForm
manufacturerName={selectedManufacturerName || tempNewManufacturer?.name}
manufacturerId={selectedManufacturerId}
initialData={tempNewRideModel}
onSubmit={(data) => {
setTempNewRideModel(data);
setIsModelModalOpen(false);
// Clear existing model selection
setValue('ride_model_id', undefined);
}}
onCancel={() => setIsModelModalOpen(false)}
/>
</DialogContent>
</Dialog>
</CardContent>
</Card>
);

View File

@@ -0,0 +1,203 @@
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Layers, Save, X } from 'lucide-react';
const rideModelSchema = z.object({
name: z.string().min(1, 'Name is required'),
slug: z.string().min(1, 'Slug is required'),
category: z.string().min(1, 'Category is required'),
ride_type: z.string().min(1, 'Ride type is required'),
description: z.string().optional(),
technical_specs: z.string().optional()
});
type RideModelFormData = z.infer<typeof rideModelSchema>;
interface RideModelFormProps {
manufacturerName: string;
manufacturerId?: string;
onSubmit: (data: RideModelFormData) => void;
onCancel: () => void;
initialData?: Partial<RideModelFormData>;
}
const categories = [
'roller_coaster',
'flat_ride',
'water_ride',
'dark_ride',
'kiddie_ride',
'transportation'
];
export function RideModelForm({
manufacturerName,
manufacturerId,
onSubmit,
onCancel,
initialData
}: RideModelFormProps) {
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors }
} = useForm<RideModelFormData>({
resolver: zodResolver(rideModelSchema),
defaultValues: {
name: initialData?.name || '',
slug: initialData?.slug || '',
category: initialData?.category || '',
ride_type: initialData?.ride_type || '',
description: initialData?.description || '',
technical_specs: initialData?.technical_specs || ''
}
});
const generateSlug = (name: string) => {
return name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
};
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const name = e.target.value;
const slug = generateSlug(name);
setValue('slug', slug);
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Layers className="w-5 h-5" />
{initialData ? 'Edit Ride Model' : 'Create New Ride Model'}
</CardTitle>
<div className="flex items-center gap-2 mt-2">
<span className="text-sm text-muted-foreground">For manufacturer:</span>
<Badge variant="secondary">{manufacturerName}</Badge>
</div>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Basic Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="name">Model Name *</Label>
<Input
id="name"
{...register('name')}
onChange={(e) => {
register('name').onChange(e);
handleNameChange(e);
}}
placeholder="e.g. Mega Coaster, Sky Screamer"
/>
{errors.name && (
<p className="text-sm text-destructive">{errors.name.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="slug">URL Slug *</Label>
<Input
id="slug"
{...register('slug')}
placeholder="model-slug"
/>
{errors.slug && (
<p className="text-sm text-destructive">{errors.slug.message}</p>
)}
</div>
</div>
{/* Category and Type */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label>Category *</Label>
<Select
onValueChange={(value) => setValue('category', value)}
defaultValue={initialData?.category}
>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem key={category} value={category}>
{category.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.category && (
<p className="text-sm text-destructive">{errors.category.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="ride_type">Ride Type *</Label>
<Input
id="ride_type"
{...register('ride_type')}
placeholder="e.g. Inverted, Wing Coaster, Pendulum"
/>
{errors.ride_type && (
<p className="text-sm text-destructive">{errors.ride_type.message}</p>
)}
</div>
</div>
{/* Description */}
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
{...register('description')}
placeholder="Describe the ride model features and characteristics..."
rows={3}
/>
</div>
{/* Technical Specs */}
<div className="space-y-2">
<Label htmlFor="technical_specs">Technical Specifications</Label>
<Textarea
id="technical_specs"
{...register('technical_specs')}
placeholder="Enter technical specifications (e.g., track length, inversions, typical speed range)..."
rows={3}
/>
<p className="text-xs text-muted-foreground">
General specifications for this model that apply to all installations
</p>
</div>
{/* Actions */}
<div className="flex gap-3 justify-end">
<Button type="button" variant="outline" onClick={onCancel}>
<X className="w-4 h-4 mr-2" />
Cancel
</Button>
<Button type="submit">
<Save className="w-4 h-4 mr-2" />
Save Model
</Button>
</div>
</form>
</CardContent>
</Card>
);
}

View File

@@ -355,6 +355,117 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
setActionLoading(item.id);
try {
// Handle composite ride submissions with sequential entity creation
if (action === 'approved' && item.type === 'content_submission' &&
(item.submission_type === 'ride_with_manufacturer' ||
item.submission_type === 'ride_with_model' ||
item.submission_type === 'ride_with_manufacturer_and_model')) {
let manufacturerId = item.content.ride?.manufacturer_id;
let rideModelId = item.content.ride?.ride_model_id;
// STEP 1: Create manufacturer if needed
if (item.content.new_manufacturer) {
const { data: newManufacturer, error: mfrError } = await supabase
.from('companies')
.insert({
name: item.content.new_manufacturer.name,
slug: item.content.new_manufacturer.slug,
description: item.content.new_manufacturer.description,
company_type: 'manufacturer',
person_type: item.content.new_manufacturer.person_type || 'company',
website_url: item.content.new_manufacturer.website_url,
founded_year: item.content.new_manufacturer.founded_year,
headquarters_location: item.content.new_manufacturer.headquarters_location
})
.select()
.single();
if (mfrError) {
throw new Error(`Failed to create manufacturer: ${mfrError.message}`);
}
manufacturerId = newManufacturer.id;
toast({
title: "Manufacturer Created",
description: `Created ${newManufacturer.name}`,
});
}
// STEP 2: Create ride model if needed
if (item.content.new_ride_model) {
const modelManufacturerId = manufacturerId || item.content.new_ride_model.manufacturer_id;
if (!modelManufacturerId) {
throw new Error('Cannot create ride model: No manufacturer ID available');
}
const { data: newModel, error: modelError } = await supabase
.from('ride_models')
.insert({
name: item.content.new_ride_model.name,
slug: item.content.new_ride_model.slug,
manufacturer_id: modelManufacturerId,
category: item.content.new_ride_model.category,
ride_type: item.content.new_ride_model.ride_type,
description: item.content.new_ride_model.description,
technical_specs: item.content.new_ride_model.technical_specs
})
.select()
.single();
if (modelError) {
throw new Error(`Failed to create ride model: ${modelError.message}`);
}
rideModelId = newModel.id;
toast({
title: "Ride Model Created",
description: `Created ${newModel.name}`,
});
}
// STEP 3: Create the ride
const { error: rideError } = await supabase
.from('rides')
.insert({
...item.content.ride,
manufacturer_id: manufacturerId,
ride_model_id: rideModelId,
park_id: item.content.park_id
});
if (rideError) {
throw new Error(`Failed to create ride: ${rideError.message}`);
}
// STEP 4: Update submission status
const { data: { user } } = await supabase.auth.getUser();
const { error: updateError } = await supabase
.from('content_submissions')
.update({
status: 'approved',
reviewer_id: user?.id,
reviewed_at: new Date().toISOString(),
reviewer_notes: moderatorNotes
})
.eq('id', item.id);
if (updateError) throw updateError;
toast({
title: "Submission Approved",
description: "All entities created successfully",
});
// Refresh the queue
fetchItems(activeEntityFilter, activeStatusFilter);
return;
}
// Standard moderation flow for other items
const table = item.type === 'review' ? 'reviews' : 'content_submissions';
const statusField = item.type === 'review' ? 'moderation_status' : 'status';
@@ -925,6 +1036,146 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
</div>
)}
</div>
) : (
<div>
{/* Composite Submissions (Ride with Manufacturer/Model) */}
{(item.submission_type === 'ride_with_manufacturer' ||
item.submission_type === 'ride_with_model' ||
item.submission_type === 'ride_with_manufacturer_and_model') ? (
<div className="space-y-4">
{/* New Manufacturer Card */}
{item.content.new_manufacturer && (
<Card className="border-blue-300 dark:border-blue-700">
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Badge variant="secondary" className="bg-blue-100 dark:bg-blue-900">
New Manufacturer
</Badge>
<span className="font-semibold">{item.content.new_manufacturer.name}</span>
</div>
</CardHeader>
<CardContent className="space-y-2 text-sm">
{item.content.new_manufacturer.description && (
<div>
<span className="font-medium">Description: </span>
<span className="text-muted-foreground">{item.content.new_manufacturer.description}</span>
</div>
)}
<div className="grid grid-cols-2 gap-2">
{item.content.new_manufacturer.person_type && (
<div>
<span className="font-medium">Type: </span>
<span className="text-muted-foreground capitalize">{item.content.new_manufacturer.person_type}</span>
</div>
)}
{item.content.new_manufacturer.founded_year && (
<div>
<span className="font-medium">Founded: </span>
<span className="text-muted-foreground">{item.content.new_manufacturer.founded_year}</span>
</div>
)}
{item.content.new_manufacturer.headquarters_location && (
<div>
<span className="font-medium">HQ: </span>
<span className="text-muted-foreground">{item.content.new_manufacturer.headquarters_location}</span>
</div>
)}
{item.content.new_manufacturer.website_url && (
<div>
<span className="font-medium">Website: </span>
<a href={item.content.new_manufacturer.website_url} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline text-xs">
{item.content.new_manufacturer.website_url}
</a>
</div>
)}
</div>
</CardContent>
</Card>
)}
{/* New Ride Model Card */}
{item.content.new_ride_model && (
<Card className="border-purple-300 dark:border-purple-700">
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Badge variant="secondary" className="bg-purple-100 dark:bg-purple-900">
New Ride Model
</Badge>
<span className="font-semibold">{item.content.new_ride_model.name}</span>
</div>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<div>
<span className="font-medium">Manufacturer: </span>
<span className="text-muted-foreground">
{item.content.new_manufacturer
? item.content.new_manufacturer.name
: 'Existing manufacturer'}
</span>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<span className="font-medium">Category: </span>
<span className="text-muted-foreground capitalize">{item.content.new_ride_model.category?.replace('_', ' ')}</span>
</div>
<div>
<span className="font-medium">Type: </span>
<span className="text-muted-foreground">{item.content.new_ride_model.ride_type}</span>
</div>
</div>
{item.content.new_ride_model.description && (
<div>
<span className="font-medium">Description: </span>
<span className="text-muted-foreground">{item.content.new_ride_model.description}</span>
</div>
)}
</CardContent>
</Card>
)}
{/* Ride Details Card */}
<Card className="border-green-300 dark:border-green-700">
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Badge variant="secondary" className="bg-green-100 dark:bg-green-900">
Ride
</Badge>
<span className="font-semibold">{item.content.ride?.name}</span>
</div>
</CardHeader>
<CardContent className="space-y-2 text-sm">
{item.content.ride?.description && (
<p className="text-muted-foreground">{item.content.ride.description}</p>
)}
<div className="grid grid-cols-2 gap-2">
{item.content.ride?.category && (
<div>
<span className="font-medium">Category: </span>
<span className="text-muted-foreground capitalize">{item.content.ride.category.replace('_', ' ')}</span>
</div>
)}
{item.content.ride?.status && (
<div>
<span className="font-medium">Status: </span>
<span className="text-muted-foreground">{item.content.ride.status}</span>
</div>
)}
{item.content.ride?.max_speed_kmh && (
<div>
<span className="font-medium">Max Speed: </span>
<span className="text-muted-foreground">{item.content.ride.max_speed_kmh} km/h</span>
</div>
)}
{item.content.ride?.max_height_meters && (
<div>
<span className="font-medium">Max Height: </span>
<span className="text-muted-foreground">{item.content.ride.max_height_meters} m</span>
</div>
)}
</div>
</CardContent>
</Card>
</div>
) : (
<div>
<div className="text-sm text-muted-foreground mb-2">
@@ -936,6 +1187,8 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
</div>
)}
</div>
)}
</div>
{/* Action buttons based on status */}
{(item.status === 'pending' || item.status === 'flagged') && (

View File

@@ -96,7 +96,7 @@ export function useManufacturers() {
try {
const { data, error } = await supabase
.from('companies')
.select('name')
.select('id, name')
.eq('company_type', 'manufacturer')
.order('name');
@@ -105,7 +105,7 @@ export function useManufacturers() {
setManufacturers(
(data || []).map(company => ({
label: company.name,
value: company.name.toLowerCase().replace(/\s+/g, '_')
value: company.id
}))
);
} catch (error) {
@@ -122,6 +122,47 @@ export function useManufacturers() {
return { manufacturers, loading };
}
export function useRideModels(manufacturerId?: string) {
const [rideModels, setRideModels] = useState<ComboboxOption[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!manufacturerId) {
setRideModels([]);
return;
}
async function fetchRideModels() {
setLoading(true);
try {
const { data, error } = await supabase
.from('ride_models')
.select('id, name')
.eq('manufacturer_id', manufacturerId)
.order('name');
if (error) throw error;
setRideModels(
(data || []).map(model => ({
label: model.name,
value: model.id
}))
);
} catch (error) {
console.error('Error fetching ride models:', error);
setRideModels([]);
} finally {
setLoading(false);
}
}
fetchRideModels();
}, [manufacturerId]);
return { rideModels, loading };
}
export function useCompanyHeadquarters() {
const [headquarters, setHeadquarters] = useState<ComboboxOption[]>([]);
const [loading, setLoading] = useState(false);

View File

@@ -99,17 +99,34 @@ export default function ParkDetail() {
};
const handleRideSubmit = async (rideData: any) => {
if (!user) return;
if (!user) {
navigate('/auth');
return;
}
try {
// Extract composite submission data
const compositeData = rideData._compositeSubmission;
delete rideData._compositeSubmission;
// Determine submission type based on what's being created
let submissionType = 'ride';
if (compositeData?.new_manufacturer && compositeData?.new_ride_model) {
submissionType = 'ride_with_manufacturer_and_model';
} else if (compositeData?.new_manufacturer) {
submissionType = 'ride_with_manufacturer';
} else if (compositeData?.new_ride_model) {
submissionType = 'ride_with_model';
}
const { error } = await supabase
.from('content_submissions')
.insert({
user_id: user.id,
submission_type: 'ride',
submission_type: submissionType,
status: 'pending',
content: {
...rideData,
...(compositeData || { ride: rideData }),
park_id: park?.id,
park_slug: park?.slug
}
@@ -117,9 +134,18 @@ export default function ParkDetail() {
if (error) throw error;
let message = "Your ride submission has been sent for moderation review.";
if (compositeData?.new_manufacturer && compositeData?.new_ride_model) {
message = "Your ride, new manufacturer, and new model have been submitted for review.";
} else if (compositeData?.new_manufacturer) {
message = "Your ride and new manufacturer have been submitted for review.";
} else if (compositeData?.new_ride_model) {
message = "Your ride and new model have been submitted for review.";
}
toast({
title: "Ride Submitted",
description: "Your ride submission has been sent for moderation review.",
title: "Submission Sent",
description: message,
});
setIsAddRideModalOpen(false);