diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts
index 0d3b2d4e..e73422bd 100644
--- a/src/lib/entitySubmissionHelpers.ts
+++ b/src/lib/entitySubmissionHelpers.ts
@@ -134,6 +134,20 @@ export interface CompanyFormData {
card_image_id?: string;
}
+export interface RideModelFormData {
+ name: string;
+ slug: string;
+ manufacturer_id: string;
+ category: string;
+ ride_type?: string;
+ description?: string;
+ images?: ImageAssignments;
+ banner_image_url?: string;
+ banner_image_id?: string;
+ card_image_url?: string;
+ card_image_id?: string;
+}
+
// Import timeline types
import type { TimelineEventFormData, TimelineSubmissionData, EntityType } from '@/types/timeline';
@@ -463,6 +477,77 @@ export async function submitRideUpdate(
return { submitted: true, submissionId: submissionData.id };
}
+/**
+ * ⚠️ CRITICAL SECURITY PATTERN ⚠️
+ *
+ * Submits a new ride model for creation through the moderation queue.
+ * This is the ONLY correct way to create ride models.
+ *
+ * DO NOT use direct database inserts:
+ * ❌ await supabase.from('ride_models').insert(data) // BYPASSES MODERATION!
+ * ✅ await submitRideModelCreation(data, userId) // CORRECT
+ *
+ * Flow: User Submit → Moderation Queue → Approval → Versioning → Live
+ *
+ * @param data - The ride model form data to submit
+ * @param userId - The ID of the user submitting the ride model
+ * @returns Object containing submitted boolean and submissionId
+ */
+export async function submitRideModelCreation(
+ data: RideModelFormData,
+ userId: string
+): Promise<{ submitted: boolean; submissionId: string }> {
+ // Upload any pending local images first
+ let processedImages = data.images;
+ if (data.images?.uploaded && data.images.uploaded.length > 0) {
+ try {
+ const uploadedImages = await uploadPendingImages(data.images.uploaded);
+ processedImages = {
+ ...data.images,
+ uploaded: uploadedImages
+ };
+ } catch (error) {
+ console.error('Failed to upload images for ride model creation:', error);
+ throw new Error('Failed to upload images. Please check your connection and try again.');
+ }
+ }
+
+ // Create the main submission record
+ const { data: submissionData, error: submissionError } = await supabase
+ .from('content_submissions')
+ .insert({
+ user_id: userId,
+ submission_type: 'ride_model',
+ content: {
+ action: 'create'
+ },
+ status: 'pending'
+ })
+ .select()
+ .single();
+
+ if (submissionError) throw submissionError;
+
+ // Create the submission item with actual ride model data
+ const { error: itemError } = await supabase
+ .from('submission_items')
+ .insert({
+ submission_id: submissionData.id,
+ item_type: 'ride_model',
+ action_type: 'create',
+ item_data: {
+ ...data,
+ images: processedImages as unknown as Json
+ },
+ status: 'pending',
+ order_index: 0
+ });
+
+ if (itemError) throw itemError;
+
+ return { submitted: true, submissionId: submissionData.id };
+}
+
/**
* ⚠️ CRITICAL SECURITY PATTERN ⚠️
*
diff --git a/src/pages/DesignerRides.tsx b/src/pages/DesignerRides.tsx
index ba07d61d..a5e76836 100644
--- a/src/pages/DesignerRides.tsx
+++ b/src/pages/DesignerRides.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Header } from '@/components/layout/Header';
import { Button } from '@/components/ui/button';
@@ -28,13 +28,7 @@ export default function DesignerRides() {
const [filterStatus, setFilterStatus] = useState('all');
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
- useEffect(() => {
- if (designerSlug) {
- fetchData();
- }
- }, [designerSlug, sortBy, filterCategory, filterStatus]);
-
- const fetchData = async () => {
+ const fetchData = useCallback(async () => {
try {
// Fetch designer
const { data: designerData, error: designerError } = await supabase
@@ -91,7 +85,13 @@ export default function DesignerRides() {
} finally {
setLoading(false);
}
- };
+ }, [designerSlug, sortBy, filterCategory, filterStatus]);
+
+ useEffect(() => {
+ if (designerSlug) {
+ fetchData();
+ }
+ }, [designerSlug, fetchData]);
const filteredRides = rides.filter(ride =>
ride.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -105,7 +105,16 @@ export default function DesignerRides() {
return;
}
- const submissionData = {
+ if (!designer) {
+ toast({
+ title: "Error",
+ description: "Designer information is missing.",
+ variant: "destructive"
+ });
+ return;
+ }
+
+ const submissionData: RideSubmissionData = {
...data,
designer_id: designer.id,
};
@@ -200,10 +209,12 @@ export default function DesignerRides() {
Explore all rides designed by {designer.name}
diff --git a/src/pages/ManufacturerModels.tsx b/src/pages/ManufacturerModels.tsx
index 88612b69..08a2e7f0 100644
--- a/src/pages/ManufacturerModels.tsx
+++ b/src/pages/ManufacturerModels.tsx
@@ -98,12 +98,22 @@ export default function ManufacturerModels() {
return;
}
- // For now, just show a toast since ride model submission isn't implemented yet
- toast({
- title: "Coming Soon",
- description: "Ride model submission is not yet available.",
- });
- return;
+ if (!manufacturer) {
+ toast({
+ title: "Error",
+ description: "Manufacturer information is missing.",
+ variant: "destructive"
+ });
+ return;
+ }
+
+ const submissionData: RideModelSubmissionData = {
+ ...data,
+ manufacturer_id: manufacturer.id,
+ };
+
+ const { submitRideModelCreation } = await import('@/lib/entitySubmissionHelpers');
+ await submitRideModelCreation(submissionData, user.id);
toast({
title: "Ride Model Submitted",
@@ -184,10 +194,12 @@ export default function ManufacturerModels() {
Explore all ride models manufactured by {manufacturer.name}
diff --git a/src/pages/ManufacturerRides.tsx b/src/pages/ManufacturerRides.tsx
index 328503c7..a1253f50 100644
--- a/src/pages/ManufacturerRides.tsx
+++ b/src/pages/ManufacturerRides.tsx
@@ -105,7 +105,16 @@ export default function ManufacturerRides() {
return;
}
- const submissionData = {
+ if (!manufacturer) {
+ toast({
+ title: "Error",
+ description: "Manufacturer information is missing.",
+ variant: "destructive"
+ });
+ return;
+ }
+
+ const submissionData: RideSubmissionData = {
...data,
manufacturer_id: manufacturer.id,
};
@@ -200,10 +209,12 @@ export default function ManufacturerRides() {
Explore all rides manufactured by {manufacturer.name}
diff --git a/src/pages/RideModelDetail.tsx b/src/pages/RideModelDetail.tsx
index 417fe55d..915fb636 100644
--- a/src/pages/RideModelDetail.tsx
+++ b/src/pages/RideModelDetail.tsx
@@ -13,10 +13,18 @@ import { RideCard } from '@/components/rides/RideCard';
import { AutocompleteSearch } from '@/components/search/AutocompleteSearch';
import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils';
+interface RideModelWithImages extends RideModel {
+ card_image_url?: string;
+ card_image_id?: string;
+ banner_image_url?: string;
+ banner_image_id?: string;
+ technical_specs?: Record
@@ -242,12 +243,12 @@ export default function RideModelDetail() {
)}
- {(model as any).technical_specs && Object.keys((model as any).technical_specs).length > 0 && (
+ {model.technical_specs && typeof model.technical_specs === 'object' && Object.keys(model.technical_specs).length > 0 && (
Technical Specifications