feat: Implement comprehensive entity submission architecture

This commit is contained in:
gpt-engineer-app[bot]
2025-10-01 20:45:11 +00:00
parent ef4663b09f
commit 2324d276e4
5 changed files with 327 additions and 154 deletions

View File

@@ -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>
) : ( ) : (

View File

@@ -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 };
} }

View 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 };
}

View File

@@ -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",

View File

@@ -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",