mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 16:31:13 -05:00
Refactor: Fix type safety and auth
This commit is contained in:
@@ -134,6 +134,20 @@ export interface CompanyFormData {
|
|||||||
card_image_id?: string;
|
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 timeline types
|
||||||
import type { TimelineEventFormData, TimelineSubmissionData, EntityType } from '@/types/timeline';
|
import type { TimelineEventFormData, TimelineSubmissionData, EntityType } from '@/types/timeline';
|
||||||
|
|
||||||
@@ -463,6 +477,77 @@ export async function submitRideUpdate(
|
|||||||
return { submitted: true, submissionId: submissionData.id };
|
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 ⚠️
|
* ⚠️ CRITICAL SECURITY PATTERN ⚠️
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Header } from '@/components/layout/Header';
|
import { Header } from '@/components/layout/Header';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -28,13 +28,7 @@ export default function DesignerRides() {
|
|||||||
const [filterStatus, setFilterStatus] = useState('all');
|
const [filterStatus, setFilterStatus] = useState('all');
|
||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchData = useCallback(async () => {
|
||||||
if (designerSlug) {
|
|
||||||
fetchData();
|
|
||||||
}
|
|
||||||
}, [designerSlug, sortBy, filterCategory, filterStatus]);
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
try {
|
||||||
// Fetch designer
|
// Fetch designer
|
||||||
const { data: designerData, error: designerError } = await supabase
|
const { data: designerData, error: designerError } = await supabase
|
||||||
@@ -91,7 +85,13 @@ export default function DesignerRides() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [designerSlug, sortBy, filterCategory, filterStatus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (designerSlug) {
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
}, [designerSlug, fetchData]);
|
||||||
|
|
||||||
const filteredRides = rides.filter(ride =>
|
const filteredRides = rides.filter(ride =>
|
||||||
ride.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
ride.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
@@ -105,7 +105,16 @@ export default function DesignerRides() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const submissionData = {
|
if (!designer) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Designer information is missing.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submissionData: RideSubmissionData = {
|
||||||
...data,
|
...data,
|
||||||
designer_id: designer.id,
|
designer_id: designer.id,
|
||||||
};
|
};
|
||||||
@@ -200,10 +209,12 @@ export default function DesignerRides() {
|
|||||||
<FerrisWheel className="w-8 h-8 text-primary" />
|
<FerrisWheel className="w-8 h-8 text-primary" />
|
||||||
<h1 className="text-4xl font-bold">Rides by {designer.name}</h1>
|
<h1 className="text-4xl font-bold">Rides by {designer.name}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
{user && (
|
||||||
<Button onClick={() => setIsCreateModalOpen(true)}>
|
<Button onClick={() => setIsCreateModalOpen(true)}>
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
Add Ride
|
Add Ride
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-lg text-muted-foreground mb-4">
|
<p className="text-lg text-muted-foreground mb-4">
|
||||||
Explore all rides designed by {designer.name}
|
Explore all rides designed by {designer.name}
|
||||||
|
|||||||
@@ -98,12 +98,22 @@ export default function ManufacturerModels() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now, just show a toast since ride model submission isn't implemented yet
|
if (!manufacturer) {
|
||||||
toast({
|
toast({
|
||||||
title: "Coming Soon",
|
title: "Error",
|
||||||
description: "Ride model submission is not yet available.",
|
description: "Manufacturer information is missing.",
|
||||||
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submissionData: RideModelSubmissionData = {
|
||||||
|
...data,
|
||||||
|
manufacturer_id: manufacturer.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { submitRideModelCreation } = await import('@/lib/entitySubmissionHelpers');
|
||||||
|
await submitRideModelCreation(submissionData, user.id);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Ride Model Submitted",
|
title: "Ride Model Submitted",
|
||||||
@@ -184,10 +194,12 @@ export default function ManufacturerModels() {
|
|||||||
<FerrisWheel className="w-8 h-8 text-primary" />
|
<FerrisWheel className="w-8 h-8 text-primary" />
|
||||||
<h1 className="text-4xl font-bold">Models by {manufacturer.name}</h1>
|
<h1 className="text-4xl font-bold">Models by {manufacturer.name}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
{user && (
|
||||||
<Button onClick={() => setIsCreateModalOpen(true)}>
|
<Button onClick={() => setIsCreateModalOpen(true)}>
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
Add Ride Model
|
Add Ride Model
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-lg text-muted-foreground mb-4">
|
<p className="text-lg text-muted-foreground mb-4">
|
||||||
Explore all ride models manufactured by {manufacturer.name}
|
Explore all ride models manufactured by {manufacturer.name}
|
||||||
|
|||||||
@@ -105,7 +105,16 @@ export default function ManufacturerRides() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const submissionData = {
|
if (!manufacturer) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Manufacturer information is missing.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submissionData: RideSubmissionData = {
|
||||||
...data,
|
...data,
|
||||||
manufacturer_id: manufacturer.id,
|
manufacturer_id: manufacturer.id,
|
||||||
};
|
};
|
||||||
@@ -200,10 +209,12 @@ export default function ManufacturerRides() {
|
|||||||
<FerrisWheel className="w-8 h-8 text-primary" />
|
<FerrisWheel className="w-8 h-8 text-primary" />
|
||||||
<h1 className="text-4xl font-bold">Rides by {manufacturer.name}</h1>
|
<h1 className="text-4xl font-bold">Rides by {manufacturer.name}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
{user && (
|
||||||
<Button onClick={() => setIsCreateModalOpen(true)}>
|
<Button onClick={() => setIsCreateModalOpen(true)}>
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
Add Ride
|
Add Ride
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-lg text-muted-foreground mb-4">
|
<p className="text-lg text-muted-foreground mb-4">
|
||||||
Explore all rides manufactured by {manufacturer.name}
|
Explore all rides manufactured by {manufacturer.name}
|
||||||
|
|||||||
@@ -13,10 +13,18 @@ import { RideCard } from '@/components/rides/RideCard';
|
|||||||
import { AutocompleteSearch } from '@/components/search/AutocompleteSearch';
|
import { AutocompleteSearch } from '@/components/search/AutocompleteSearch';
|
||||||
import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils';
|
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<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
export default function RideModelDetail() {
|
export default function RideModelDetail() {
|
||||||
const { manufacturerSlug, modelSlug } = useParams<{ manufacturerSlug: string; modelSlug: string }>();
|
const { manufacturerSlug, modelSlug } = useParams<{ manufacturerSlug: string; modelSlug: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [model, setModel] = useState<RideModel | null>(null);
|
const [model, setModel] = useState<RideModelWithImages | null>(null);
|
||||||
const [manufacturer, setManufacturer] = useState<Company | null>(null);
|
const [manufacturer, setManufacturer] = useState<Company | null>(null);
|
||||||
const [rides, setRides] = useState<Ride[]>([]);
|
const [rides, setRides] = useState<Ride[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -47,7 +55,7 @@ export default function RideModelDetail() {
|
|||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
if (modelError) throw modelError;
|
if (modelError) throw modelError;
|
||||||
setModel(modelData as RideModel);
|
setModel(modelData as RideModelWithImages);
|
||||||
|
|
||||||
if (modelData) {
|
if (modelData) {
|
||||||
// Fetch rides using this model
|
// Fetch rides using this model
|
||||||
@@ -162,13 +170,6 @@ export default function RideModelDetail() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const extendedModel = model as RideModel & {
|
|
||||||
card_image_url?: string;
|
|
||||||
card_image_id?: string;
|
|
||||||
banner_image_url?: string;
|
|
||||||
banner_image_id?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<Header />
|
<Header />
|
||||||
@@ -183,10 +184,10 @@ export default function RideModelDetail() {
|
|||||||
|
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
{(extendedModel.banner_image_url || extendedModel.banner_image_id) && (
|
{(model.banner_image_url || model.banner_image_id) && (
|
||||||
<div className="relative w-full h-64 mb-6 rounded-lg overflow-hidden">
|
<div className="relative w-full h-64 mb-6 rounded-lg overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src={extendedModel.banner_image_url || getCloudflareImageUrl(extendedModel.banner_image_id, 'banner')}
|
src={model.banner_image_url || (model.banner_image_id ? getCloudflareImageUrl(model.banner_image_id, 'banner') : '')}
|
||||||
alt={model.name}
|
alt={model.name}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
@@ -242,12 +243,12 @@ export default function RideModelDetail() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(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 && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<h2 className="text-2xl font-semibold mb-4">Technical Specifications</h2>
|
<h2 className="text-2xl font-semibold mb-4">Technical Specifications</h2>
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
{Object.entries((model as any).technical_specs as Record<string, any>).map(([key, value]) => (
|
{Object.entries(model.technical_specs as Record<string, any>).map(([key, value]) => (
|
||||||
<div key={key} className="flex justify-between py-2 border-b">
|
<div key={key} className="flex justify-between py-2 border-b">
|
||||||
<span className="font-medium capitalize">{key.replace(/_/g, ' ')}</span>
|
<span className="font-medium capitalize">{key.replace(/_/g, ' ')}</span>
|
||||||
<span className="text-muted-foreground">{String(value)}</span>
|
<span className="text-muted-foreground">{String(value)}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user