mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 12:51:16 -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 === 'designer' ||
|
||||
item.submission_type === 'operator' ||
|
||||
item.submission_type === 'property_owner') ? (
|
||||
item.submission_type === 'property_owner' ||
|
||||
item.submission_type === 'park' ||
|
||||
item.submission_type === 'ride') ? (
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-3 flex items-center gap-2">
|
||||
<Badge variant="outline" className="capitalize">
|
||||
@@ -1224,114 +1226,8 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Basic Information */}
|
||||
<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 className="text-sm text-muted-foreground mb-2">
|
||||
This is a complex submission with items. Click "Review Items" to see details.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -28,14 +28,29 @@ export async function submitCompanyCreation(
|
||||
};
|
||||
}
|
||||
|
||||
// All users submit for moderation
|
||||
const { error } = await supabase
|
||||
// Create the main submission record
|
||||
const { data: submissionData, error: submissionError } = await supabase
|
||||
.from('content_submissions')
|
||||
.insert([{
|
||||
.insert({
|
||||
user_id: userId,
|
||||
submission_type: companyType, // Use entity-specific type: 'manufacturer', 'designer', 'operator', or 'property_owner'
|
||||
submission_type: companyType,
|
||||
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,
|
||||
slug: data.slug,
|
||||
description: data.description,
|
||||
@@ -44,13 +59,15 @@ export async function submitCompanyCreation(
|
||||
founded_year: data.founded_year,
|
||||
headquarters_location: data.headquarters_location,
|
||||
company_type: companyType,
|
||||
images: processedImages as any // Include uploaded image assignments in submission
|
||||
} as any,
|
||||
status: 'pending'
|
||||
}]);
|
||||
images: processedImages as any
|
||||
},
|
||||
status: 'pending',
|
||||
order_index: 0
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
return { submitted: true };
|
||||
if (itemError) throw itemError;
|
||||
|
||||
return { submitted: true, submissionId: submissionData.id };
|
||||
}
|
||||
|
||||
export async function submitCompanyUpdate(
|
||||
@@ -78,14 +95,30 @@ export async function submitCompanyUpdate(
|
||||
};
|
||||
}
|
||||
|
||||
// All users submit for moderation
|
||||
const { error } = await supabase
|
||||
// Create the main submission record
|
||||
const { data: submissionData, error: submissionError } = await supabase
|
||||
.from('content_submissions')
|
||||
.insert([{
|
||||
.insert({
|
||||
user_id: userId,
|
||||
submission_type: existingCompany.company_type, // Use entity-specific type from existing company
|
||||
submission_type: existingCompany.company_type,
|
||||
content: {
|
||||
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,
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
@@ -94,11 +127,13 @@ export async function submitCompanyUpdate(
|
||||
website_url: data.website_url,
|
||||
founded_year: data.founded_year,
|
||||
headquarters_location: data.headquarters_location,
|
||||
images: processedImages as any // Include uploaded image role assignments in submission
|
||||
} as any,
|
||||
status: 'pending'
|
||||
}]);
|
||||
images: processedImages as any
|
||||
},
|
||||
status: 'pending',
|
||||
order_index: 0
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
return { submitted: true };
|
||||
if (itemError) throw itemError;
|
||||
|
||||
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();
|
||||
} else {
|
||||
// Regular users submit for moderation
|
||||
const { error } = await supabase
|
||||
.from('content_submissions')
|
||||
.insert({
|
||||
user_id: user.id,
|
||||
submission_type: 'park_edit',
|
||||
status: 'pending',
|
||||
content: {
|
||||
park_id: park.id,
|
||||
...parkData
|
||||
}
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
const { submitParkUpdate } = await import('@/lib/entitySubmissionHelpers');
|
||||
await submitParkUpdate(park.id, parkData, user.id);
|
||||
|
||||
toast({
|
||||
title: "Edit Submitted",
|
||||
|
||||
@@ -250,17 +250,8 @@ export default function Parks() {
|
||||
}
|
||||
|
||||
try {
|
||||
// All users submit for moderation
|
||||
const { error } = await supabase
|
||||
.from('content_submissions')
|
||||
.insert({
|
||||
user_id: user.id,
|
||||
submission_type: 'park',
|
||||
status: 'pending',
|
||||
content: parkData
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
const { submitParkCreation } = await import('@/lib/entitySubmissionHelpers');
|
||||
await submitParkCreation(parkData, user.id);
|
||||
|
||||
toast({
|
||||
title: "Park Submitted",
|
||||
|
||||
Reference in New Issue
Block a user