mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 22:11:11 -05:00
feat: Implement comprehensive entity submission architecture
This commit is contained in:
@@ -1213,7 +1213,9 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
) : (item.submission_type === 'manufacturer' ||
|
) : (item.submission_type === 'manufacturer' ||
|
||||||
item.submission_type === 'designer' ||
|
item.submission_type === 'designer' ||
|
||||||
item.submission_type === 'operator' ||
|
item.submission_type === 'operator' ||
|
||||||
item.submission_type === 'property_owner') ? (
|
item.submission_type === 'property_owner' ||
|
||||||
|
item.submission_type === 'park' ||
|
||||||
|
item.submission_type === 'ride') ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-muted-foreground mb-3 flex items-center gap-2">
|
<div className="text-sm text-muted-foreground mb-3 flex items-center gap-2">
|
||||||
<Badge variant="outline" className="capitalize">
|
<Badge variant="outline" className="capitalize">
|
||||||
@@ -1224,114 +1226,8 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="text-sm text-muted-foreground mb-2">
|
||||||
{/* Basic Information */}
|
This is a complex submission with items. Click "Review Items" to see details.
|
||||||
<div className="space-y-2">
|
|
||||||
<div>
|
|
||||||
<span className="text-sm font-medium">Name: </span>
|
|
||||||
<span className="text-sm">{item.content.name}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-sm font-medium">Slug: </span>
|
|
||||||
<span className="text-sm font-mono text-muted-foreground">{item.content.slug}</span>
|
|
||||||
</div>
|
|
||||||
{item.content.person_type && (
|
|
||||||
<div>
|
|
||||||
<span className="text-sm font-medium">Type: </span>
|
|
||||||
<span className="text-sm capitalize">{item.content.person_type}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{item.content.description && (
|
|
||||||
<div>
|
|
||||||
<span className="text-sm font-medium">Description: </span>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">{item.content.description}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{item.content.website_url && (
|
|
||||||
<div>
|
|
||||||
<span className="text-sm font-medium">Website: </span>
|
|
||||||
<a href={item.content.website_url} target="_blank" rel="noopener noreferrer"
|
|
||||||
className="text-sm text-primary hover:underline">
|
|
||||||
{item.content.website_url}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{item.content.founded_year && (
|
|
||||||
<div>
|
|
||||||
<span className="text-sm font-medium">Founded: </span>
|
|
||||||
<span className="text-sm">{item.content.founded_year}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{item.content.headquarters_location && (
|
|
||||||
<div>
|
|
||||||
<span className="text-sm font-medium">Headquarters: </span>
|
|
||||||
<span className="text-sm">{item.content.headquarters_location}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Images */}
|
|
||||||
{item.content.images?.uploaded && item.content.images.uploaded.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-sm font-medium">Images ({item.content.images.uploaded.length}):</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{item.content.images.uploaded.map((image: any, index: number) => (
|
|
||||||
<div key={index} className="space-y-1">
|
|
||||||
<div className="relative min-h-[100px] bg-muted/30 rounded border overflow-hidden cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedPhotos(item.content.images.uploaded.map((img: any, i: number) => ({
|
|
||||||
id: `${item.id}-${i}`,
|
|
||||||
url: img.url,
|
|
||||||
filename: `Image ${i + 1}`,
|
|
||||||
caption: img.caption
|
|
||||||
})));
|
|
||||||
setSelectedPhotoIndex(index);
|
|
||||||
setPhotoModalOpen(true);
|
|
||||||
}}>
|
|
||||||
<img
|
|
||||||
src={image.url}
|
|
||||||
alt={image.caption || `Image ${index + 1}`}
|
|
||||||
className="w-full h-32 object-cover rounded hover:opacity-80 transition-opacity"
|
|
||||||
onError={(e) => {
|
|
||||||
console.error('Failed to load company image:', image.url);
|
|
||||||
(e.target as HTMLImageElement).style.display = 'none';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50 text-white opacity-0 hover:opacity-100 transition-opacity">
|
|
||||||
<Eye className="w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{image.caption && (
|
|
||||||
<p className="text-xs text-muted-foreground">{image.caption}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Image Role Assignments */}
|
|
||||||
{(item.content.images.banner || item.content.images.card) && (
|
|
||||||
<div className="mt-2 pt-2 border-t space-y-1">
|
|
||||||
<div className="text-xs font-medium text-muted-foreground">Image Assignments:</div>
|
|
||||||
{item.content.images.banner !== undefined && (
|
|
||||||
<div className="text-xs">
|
|
||||||
<span className="font-medium">Banner: </span>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{item.content.images.banner === null ? 'None' : `Image ${item.content.images.banner + 1}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{item.content.images.card !== undefined && (
|
|
||||||
<div className="text-xs">
|
|
||||||
<span className="font-medium">Card: </span>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{item.content.images.card === null ? 'None' : `Image ${item.content.images.card + 1}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -28,14 +28,29 @@ export async function submitCompanyCreation(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// All users submit for moderation
|
// Create the main submission record
|
||||||
const { error } = await supabase
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
.insert([{
|
.insert({
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
submission_type: companyType, // Use entity-specific type: 'manufacturer', 'designer', 'operator', or 'property_owner'
|
submission_type: companyType,
|
||||||
content: {
|
content: {
|
||||||
action: 'create',
|
action: 'create'
|
||||||
|
},
|
||||||
|
status: 'pending'
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (submissionError) throw submissionError;
|
||||||
|
|
||||||
|
// Create the submission item with actual company data
|
||||||
|
const { error: itemError } = await supabase
|
||||||
|
.from('submission_items')
|
||||||
|
.insert({
|
||||||
|
submission_id: submissionData.id,
|
||||||
|
item_type: companyType,
|
||||||
|
item_data: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
slug: data.slug,
|
slug: data.slug,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
@@ -44,13 +59,15 @@ export async function submitCompanyCreation(
|
|||||||
founded_year: data.founded_year,
|
founded_year: data.founded_year,
|
||||||
headquarters_location: data.headquarters_location,
|
headquarters_location: data.headquarters_location,
|
||||||
company_type: companyType,
|
company_type: companyType,
|
||||||
images: processedImages as any // Include uploaded image assignments in submission
|
images: processedImages as any
|
||||||
} as any,
|
},
|
||||||
status: 'pending'
|
status: 'pending',
|
||||||
}]);
|
order_index: 0
|
||||||
|
});
|
||||||
|
|
||||||
if (error) throw error;
|
if (itemError) throw itemError;
|
||||||
return { submitted: true };
|
|
||||||
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitCompanyUpdate(
|
export async function submitCompanyUpdate(
|
||||||
@@ -78,14 +95,30 @@ export async function submitCompanyUpdate(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// All users submit for moderation
|
// Create the main submission record
|
||||||
const { error } = await supabase
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
.insert([{
|
.insert({
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
submission_type: existingCompany.company_type, // Use entity-specific type from existing company
|
submission_type: existingCompany.company_type,
|
||||||
content: {
|
content: {
|
||||||
action: 'edit',
|
action: 'edit',
|
||||||
|
company_id: companyId
|
||||||
|
},
|
||||||
|
status: 'pending'
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (submissionError) throw submissionError;
|
||||||
|
|
||||||
|
// Create the submission item with actual company data
|
||||||
|
const { error: itemError } = await supabase
|
||||||
|
.from('submission_items')
|
||||||
|
.insert({
|
||||||
|
submission_id: submissionData.id,
|
||||||
|
item_type: existingCompany.company_type,
|
||||||
|
item_data: {
|
||||||
company_id: companyId,
|
company_id: companyId,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
slug: data.slug,
|
slug: data.slug,
|
||||||
@@ -94,11 +127,13 @@ export async function submitCompanyUpdate(
|
|||||||
website_url: data.website_url,
|
website_url: data.website_url,
|
||||||
founded_year: data.founded_year,
|
founded_year: data.founded_year,
|
||||||
headquarters_location: data.headquarters_location,
|
headquarters_location: data.headquarters_location,
|
||||||
images: processedImages as any // Include uploaded image role assignments in submission
|
images: processedImages as any
|
||||||
} as any,
|
},
|
||||||
status: 'pending'
|
status: 'pending',
|
||||||
}]);
|
order_index: 0
|
||||||
|
});
|
||||||
|
|
||||||
if (error) throw error;
|
if (itemError) throw itemError;
|
||||||
return { submitted: true };
|
|
||||||
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
}
|
}
|
||||||
|
|||||||
262
src/lib/entitySubmissionHelpers.ts
Normal file
262
src/lib/entitySubmissionHelpers.ts
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader';
|
||||||
|
import { uploadPendingImages } from './imageUploadHelper';
|
||||||
|
|
||||||
|
export interface ParkFormData {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
|
park_type: string;
|
||||||
|
status: string;
|
||||||
|
opening_date?: string;
|
||||||
|
closing_date?: string;
|
||||||
|
website_url?: string;
|
||||||
|
phone?: string;
|
||||||
|
email?: string;
|
||||||
|
operator_id?: string;
|
||||||
|
property_owner_id?: string;
|
||||||
|
location_id?: string;
|
||||||
|
images?: ImageAssignments;
|
||||||
|
banner_image_url?: string;
|
||||||
|
banner_image_id?: string;
|
||||||
|
card_image_url?: string;
|
||||||
|
card_image_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RideFormData {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
|
category: string;
|
||||||
|
status: string;
|
||||||
|
park_id: string;
|
||||||
|
manufacturer_id?: string;
|
||||||
|
designer_id?: string;
|
||||||
|
ride_model_id?: string;
|
||||||
|
opening_date?: string;
|
||||||
|
closing_date?: string;
|
||||||
|
max_speed_kmh?: number;
|
||||||
|
max_height_meters?: number;
|
||||||
|
length_meters?: number;
|
||||||
|
duration_seconds?: number;
|
||||||
|
capacity_per_hour?: number;
|
||||||
|
height_requirement?: number;
|
||||||
|
age_requirement?: number;
|
||||||
|
inversions?: number;
|
||||||
|
drop_height_meters?: number;
|
||||||
|
max_g_force?: number;
|
||||||
|
intensity_level?: string;
|
||||||
|
coaster_type?: string;
|
||||||
|
seating_type?: string;
|
||||||
|
ride_sub_type?: string;
|
||||||
|
coaster_stats?: any;
|
||||||
|
technical_specs?: any;
|
||||||
|
former_names?: any;
|
||||||
|
images?: ImageAssignments;
|
||||||
|
banner_image_url?: string;
|
||||||
|
banner_image_id?: string;
|
||||||
|
card_image_url?: string;
|
||||||
|
card_image_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitParkCreation(
|
||||||
|
data: ParkFormData,
|
||||||
|
userId: string
|
||||||
|
) {
|
||||||
|
// Upload any pending local images first
|
||||||
|
let processedImages = data.images;
|
||||||
|
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
||||||
|
const uploadedImages = await uploadPendingImages(data.images.uploaded);
|
||||||
|
processedImages = {
|
||||||
|
...data.images,
|
||||||
|
uploaded: uploadedImages
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the main submission record
|
||||||
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.insert({
|
||||||
|
user_id: userId,
|
||||||
|
submission_type: 'park',
|
||||||
|
content: {
|
||||||
|
action: 'create'
|
||||||
|
},
|
||||||
|
status: 'pending'
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (submissionError) throw submissionError;
|
||||||
|
|
||||||
|
// Create the submission item with actual park data
|
||||||
|
const { error: itemError } = await supabase
|
||||||
|
.from('submission_items')
|
||||||
|
.insert({
|
||||||
|
submission_id: submissionData.id,
|
||||||
|
item_type: 'park',
|
||||||
|
item_data: {
|
||||||
|
...data,
|
||||||
|
images: processedImages as any
|
||||||
|
},
|
||||||
|
status: 'pending',
|
||||||
|
order_index: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (itemError) throw itemError;
|
||||||
|
|
||||||
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitParkUpdate(
|
||||||
|
parkId: string,
|
||||||
|
data: ParkFormData,
|
||||||
|
userId: string
|
||||||
|
) {
|
||||||
|
// Upload any pending local images first
|
||||||
|
let processedImages = data.images;
|
||||||
|
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
||||||
|
const uploadedImages = await uploadPendingImages(data.images.uploaded);
|
||||||
|
processedImages = {
|
||||||
|
...data.images,
|
||||||
|
uploaded: uploadedImages
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the main submission record
|
||||||
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.insert({
|
||||||
|
user_id: userId,
|
||||||
|
submission_type: 'park',
|
||||||
|
content: {
|
||||||
|
action: 'edit',
|
||||||
|
park_id: parkId
|
||||||
|
},
|
||||||
|
status: 'pending'
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (submissionError) throw submissionError;
|
||||||
|
|
||||||
|
// Create the submission item with actual park data
|
||||||
|
const { error: itemError } = await supabase
|
||||||
|
.from('submission_items')
|
||||||
|
.insert({
|
||||||
|
submission_id: submissionData.id,
|
||||||
|
item_type: 'park',
|
||||||
|
item_data: {
|
||||||
|
...data,
|
||||||
|
park_id: parkId,
|
||||||
|
images: processedImages as any
|
||||||
|
},
|
||||||
|
status: 'pending',
|
||||||
|
order_index: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (itemError) throw itemError;
|
||||||
|
|
||||||
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitRideCreation(
|
||||||
|
data: RideFormData,
|
||||||
|
userId: string
|
||||||
|
) {
|
||||||
|
// Upload any pending local images first
|
||||||
|
let processedImages = data.images;
|
||||||
|
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
||||||
|
const uploadedImages = await uploadPendingImages(data.images.uploaded);
|
||||||
|
processedImages = {
|
||||||
|
...data.images,
|
||||||
|
uploaded: uploadedImages
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the main submission record
|
||||||
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.insert({
|
||||||
|
user_id: userId,
|
||||||
|
submission_type: 'ride',
|
||||||
|
content: {
|
||||||
|
action: 'create'
|
||||||
|
},
|
||||||
|
status: 'pending'
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (submissionError) throw submissionError;
|
||||||
|
|
||||||
|
// Create the submission item with actual ride data
|
||||||
|
const { error: itemError } = await supabase
|
||||||
|
.from('submission_items')
|
||||||
|
.insert({
|
||||||
|
submission_id: submissionData.id,
|
||||||
|
item_type: 'ride',
|
||||||
|
item_data: {
|
||||||
|
...data,
|
||||||
|
images: processedImages as any
|
||||||
|
},
|
||||||
|
status: 'pending',
|
||||||
|
order_index: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (itemError) throw itemError;
|
||||||
|
|
||||||
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitRideUpdate(
|
||||||
|
rideId: string,
|
||||||
|
data: RideFormData,
|
||||||
|
userId: string
|
||||||
|
) {
|
||||||
|
// Upload any pending local images first
|
||||||
|
let processedImages = data.images;
|
||||||
|
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
||||||
|
const uploadedImages = await uploadPendingImages(data.images.uploaded);
|
||||||
|
processedImages = {
|
||||||
|
...data.images,
|
||||||
|
uploaded: uploadedImages
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the main submission record
|
||||||
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.insert({
|
||||||
|
user_id: userId,
|
||||||
|
submission_type: 'ride',
|
||||||
|
content: {
|
||||||
|
action: 'edit',
|
||||||
|
ride_id: rideId
|
||||||
|
},
|
||||||
|
status: 'pending'
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (submissionError) throw submissionError;
|
||||||
|
|
||||||
|
// Create the submission item with actual ride data
|
||||||
|
const { error: itemError } = await supabase
|
||||||
|
.from('submission_items')
|
||||||
|
.insert({
|
||||||
|
submission_id: submissionData.id,
|
||||||
|
item_type: 'ride',
|
||||||
|
item_data: {
|
||||||
|
...data,
|
||||||
|
ride_id: rideId,
|
||||||
|
images: processedImages as any
|
||||||
|
},
|
||||||
|
status: 'pending',
|
||||||
|
order_index: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (itemError) throw itemError;
|
||||||
|
|
||||||
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
|
}
|
||||||
@@ -215,19 +215,8 @@ export default function ParkDetail() {
|
|||||||
fetchParkData();
|
fetchParkData();
|
||||||
} else {
|
} else {
|
||||||
// Regular users submit for moderation
|
// Regular users submit for moderation
|
||||||
const { error } = await supabase
|
const { submitParkUpdate } = await import('@/lib/entitySubmissionHelpers');
|
||||||
.from('content_submissions')
|
await submitParkUpdate(park.id, parkData, user.id);
|
||||||
.insert({
|
|
||||||
user_id: user.id,
|
|
||||||
submission_type: 'park_edit',
|
|
||||||
status: 'pending',
|
|
||||||
content: {
|
|
||||||
park_id: park.id,
|
|
||||||
...parkData
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Edit Submitted",
|
title: "Edit Submitted",
|
||||||
|
|||||||
@@ -250,17 +250,8 @@ export default function Parks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// All users submit for moderation
|
const { submitParkCreation } = await import('@/lib/entitySubmissionHelpers');
|
||||||
const { error } = await supabase
|
await submitParkCreation(parkData, user.id);
|
||||||
.from('content_submissions')
|
|
||||||
.insert({
|
|
||||||
user_id: user.id,
|
|
||||||
submission_type: 'park',
|
|
||||||
status: 'pending',
|
|
||||||
content: parkData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Park Submitted",
|
title: "Park Submitted",
|
||||||
|
|||||||
Reference in New Issue
Block a user