mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 04:31:13 -05:00
Add robust error handling for image uploads, improve navigation logic in AutocompleteSearch with toast notifications for missing identifiers, refine useIsMobile hook return type, and update Supabase function error reporting to handle non-Error types. Replit-Commit-Author: Agent Replit-Commit-Session-Id: a759d451-40bf-440d-96f5-a19ad6af18a8 Replit-Commit-Checkpoint-Type: intermediate_checkpoint
694 lines
22 KiB
TypeScript
694 lines
22 KiB
TypeScript
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
|
|
|
const corsHeaders = {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
|
};
|
|
|
|
interface ApprovalRequest {
|
|
itemIds: string[];
|
|
userId: string;
|
|
submissionId: string;
|
|
}
|
|
|
|
// Allowed database fields for each entity type
|
|
const RIDE_FIELDS = [
|
|
'name', 'slug', 'description', 'park_id', 'ride_model_id',
|
|
'manufacturer_id', 'designer_id', 'category', 'status',
|
|
'opening_date', 'closing_date', 'height_requirement', 'age_requirement',
|
|
'capacity_per_hour', 'duration_seconds', 'max_speed_kmh',
|
|
'max_height_meters', 'length_meters', 'inversions',
|
|
'ride_sub_type', 'coaster_type', 'seating_type', 'intensity_level',
|
|
'drop_height_meters', 'max_g_force', 'image_url',
|
|
'banner_image_url', 'banner_image_id', 'card_image_url', 'card_image_id'
|
|
];
|
|
|
|
const PARK_FIELDS = [
|
|
'name', 'slug', 'description', 'park_type', 'status',
|
|
'opening_date', 'closing_date', 'location_id', 'operator_id',
|
|
'property_owner_id', 'website_url', 'phone', 'email',
|
|
'banner_image_url', 'banner_image_id', 'card_image_url', 'card_image_id'
|
|
];
|
|
|
|
const COMPANY_FIELDS = [
|
|
'name', 'slug', 'description', 'company_type', 'person_type',
|
|
'founded_year', 'headquarters_location', 'website_url', 'logo_url',
|
|
'banner_image_url', 'banner_image_id', 'card_image_url', 'card_image_id'
|
|
];
|
|
|
|
const RIDE_MODEL_FIELDS = [
|
|
'name', 'slug', 'description', 'category', 'ride_type',
|
|
'manufacturer_id', 'banner_image_url', 'banner_image_id',
|
|
'card_image_url', 'card_image_id'
|
|
];
|
|
|
|
serve(async (req) => {
|
|
if (req.method === 'OPTIONS') {
|
|
return new Response(null, { headers: corsHeaders });
|
|
}
|
|
|
|
try {
|
|
const supabase = createClient(
|
|
Deno.env.get('SUPABASE_URL') ?? '',
|
|
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
|
);
|
|
|
|
const { itemIds, userId, submissionId }: ApprovalRequest = await req.json();
|
|
|
|
// UUID validation regex
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
|
// Validate itemIds
|
|
if (!itemIds || !Array.isArray(itemIds)) {
|
|
return new Response(
|
|
JSON.stringify({ error: 'itemIds is required and must be an array' }),
|
|
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
if (itemIds.length === 0) {
|
|
return new Response(
|
|
JSON.stringify({ error: 'itemIds must be a non-empty array' }),
|
|
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
// Validate userId
|
|
if (!userId || typeof userId !== 'string' || userId.trim() === '') {
|
|
return new Response(
|
|
JSON.stringify({ error: 'userId is required and must be a non-empty string' }),
|
|
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
if (!uuidRegex.test(userId)) {
|
|
return new Response(
|
|
JSON.stringify({ error: 'userId must be a valid UUID format' }),
|
|
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
// Validate submissionId
|
|
if (!submissionId || typeof submissionId !== 'string' || submissionId.trim() === '') {
|
|
return new Response(
|
|
JSON.stringify({ error: 'submissionId is required and must be a non-empty string' }),
|
|
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
if (!uuidRegex.test(submissionId)) {
|
|
return new Response(
|
|
JSON.stringify({ error: 'submissionId must be a valid UUID format' }),
|
|
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
console.log('Processing selective approval:', { itemIds, userId, submissionId });
|
|
|
|
// Fetch all items for the submission
|
|
const { data: items, error: fetchError } = await supabase
|
|
.from('submission_items')
|
|
.select('*')
|
|
.in('id', itemIds);
|
|
|
|
if (fetchError) {
|
|
throw new Error(`Failed to fetch items: ${fetchError.message}`);
|
|
}
|
|
|
|
// Get the submitter's user_id from the submission
|
|
const { data: submission, error: submissionError } = await supabase
|
|
.from('content_submissions')
|
|
.select('user_id')
|
|
.eq('id', submissionId)
|
|
.single();
|
|
|
|
if (submissionError || !submission) {
|
|
throw new Error(`Failed to fetch submission: ${submissionError?.message}`);
|
|
}
|
|
|
|
const submitterId = submission.user_id;
|
|
|
|
// Topologically sort items by dependencies
|
|
const sortedItems = topologicalSort(items);
|
|
const dependencyMap = new Map<string, string>();
|
|
const approvalResults: Array<{
|
|
itemId: string;
|
|
entityId?: string | null;
|
|
itemType: string;
|
|
success: boolean;
|
|
error?: string;
|
|
}> = [];
|
|
|
|
// Process items in order
|
|
for (const item of sortedItems) {
|
|
try {
|
|
console.log(`Processing item ${item.id} of type ${item.item_type}`);
|
|
|
|
// Set user context for versioning trigger
|
|
// This allows auto_create_entity_version() to capture the submitter
|
|
const { error: setConfigError } = await supabase.rpc('set_config_value', {
|
|
setting_name: 'app.current_user_id',
|
|
setting_value: submitterId,
|
|
is_local: false
|
|
});
|
|
|
|
if (setConfigError) {
|
|
console.error('Failed to set user context:', setConfigError);
|
|
}
|
|
|
|
// Resolve dependencies in item data
|
|
const resolvedData = resolveDependencies(item.item_data, dependencyMap);
|
|
|
|
// Add submitter ID to the data for photo tracking
|
|
resolvedData._submitter_id = submitterId;
|
|
|
|
let entityId: string | null = null;
|
|
|
|
// Create entity based on type
|
|
switch (item.item_type) {
|
|
case 'park':
|
|
entityId = await createPark(supabase, resolvedData);
|
|
break;
|
|
case 'ride':
|
|
entityId = await createRide(supabase, resolvedData);
|
|
break;
|
|
case 'manufacturer':
|
|
case 'operator':
|
|
case 'property_owner':
|
|
case 'designer':
|
|
entityId = await createCompany(supabase, resolvedData, item.item_type);
|
|
break;
|
|
case 'ride_model':
|
|
entityId = await createRideModel(supabase, resolvedData);
|
|
break;
|
|
case 'photo':
|
|
await approvePhotos(supabase, resolvedData, item.id);
|
|
entityId = item.id; // Use item ID as entity ID for photos
|
|
break;
|
|
case 'photo_edit':
|
|
await editPhoto(supabase, resolvedData);
|
|
entityId = resolvedData.photo_id;
|
|
break;
|
|
case 'photo_delete':
|
|
await deletePhoto(supabase, resolvedData);
|
|
entityId = resolvedData.photo_id;
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown item type: ${item.item_type}`);
|
|
}
|
|
|
|
if (entityId) {
|
|
dependencyMap.set(item.id, entityId);
|
|
}
|
|
|
|
// Update item status
|
|
const { error: updateError } = await supabase
|
|
.from('submission_items')
|
|
.update({
|
|
status: 'approved',
|
|
approved_entity_id: entityId,
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq('id', item.id);
|
|
|
|
if (updateError) {
|
|
throw new Error(`Failed to update item status: ${updateError.message}`);
|
|
}
|
|
|
|
approvalResults.push({
|
|
itemId: item.id,
|
|
entityId,
|
|
itemType: item.item_type,
|
|
success: true
|
|
});
|
|
|
|
console.log(`Successfully approved item ${item.id} -> entity ${entityId}`);
|
|
} catch (error) {
|
|
console.error(`Error processing item ${item.id}:`, error);
|
|
approvalResults.push({
|
|
itemId: item.id,
|
|
itemType: item.item_type,
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Update submission status
|
|
const allApproved = approvalResults.every(r => r.success);
|
|
const { error: updateError } = await supabase
|
|
.from('content_submissions')
|
|
.update({
|
|
status: allApproved ? 'approved' : 'partially_approved',
|
|
reviewer_id: userId,
|
|
reviewed_at: new Date().toISOString()
|
|
})
|
|
.eq('id', submissionId);
|
|
|
|
if (updateError) {
|
|
console.error('Failed to update submission status:', updateError);
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: true,
|
|
results: approvalResults,
|
|
submissionStatus: allApproved ? 'approved' : 'partially_approved'
|
|
}),
|
|
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
);
|
|
} catch (error) {
|
|
console.error('Error in process-selective-approval:', error);
|
|
return new Response(
|
|
JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }),
|
|
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
});
|
|
|
|
// Helper functions
|
|
function topologicalSort(items: any[]): any[] {
|
|
const sorted: any[] = [];
|
|
const visited = new Set<string>();
|
|
const visiting = new Set<string>();
|
|
|
|
const visit = (item: any) => {
|
|
if (visited.has(item.id)) return;
|
|
if (visiting.has(item.id)) {
|
|
throw new Error(`Circular dependency detected for item ${item.id}`);
|
|
}
|
|
|
|
visiting.add(item.id);
|
|
|
|
if (item.depends_on) {
|
|
const parent = items.find(i => i.id === item.depends_on);
|
|
if (parent) {
|
|
visit(parent);
|
|
}
|
|
}
|
|
|
|
visiting.delete(item.id);
|
|
visited.add(item.id);
|
|
sorted.push(item);
|
|
};
|
|
|
|
items.forEach(item => visit(item));
|
|
return sorted;
|
|
}
|
|
|
|
function resolveDependencies(data: any, dependencyMap: Map<string, string>): any {
|
|
if (typeof data !== 'object' || data === null) {
|
|
return data;
|
|
}
|
|
|
|
if (Array.isArray(data)) {
|
|
return data.map(item => resolveDependencies(item, dependencyMap));
|
|
}
|
|
|
|
const resolved: any = {};
|
|
for (const [key, value] of Object.entries(data)) {
|
|
if (typeof value === 'string' && dependencyMap.has(value)) {
|
|
resolved[key] = dependencyMap.get(value);
|
|
} else {
|
|
resolved[key] = resolveDependencies(value, dependencyMap);
|
|
}
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
function sanitizeDateFields(data: any): any {
|
|
const dateFields = ['opening_date', 'closing_date', 'date_changed', 'date_taken', 'visit_date'];
|
|
const sanitized = { ...data };
|
|
|
|
for (const field of dateFields) {
|
|
if (field in sanitized && sanitized[field] === '') {
|
|
sanitized[field] = null;
|
|
}
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
function filterDatabaseFields(data: any, allowedFields: string[]): any {
|
|
const filtered: any = {};
|
|
for (const field of allowedFields) {
|
|
if (field in data && data[field] !== undefined) {
|
|
filtered[field] = data[field];
|
|
}
|
|
}
|
|
return filtered;
|
|
}
|
|
|
|
function normalizeStatusValue(data: any): any {
|
|
if (data.status) {
|
|
// Map display values to database values
|
|
const statusMap: Record<string, string> = {
|
|
'Operating': 'operating',
|
|
'Seasonal': 'operating',
|
|
'Closed Temporarily': 'maintenance',
|
|
'Closed Permanently': 'closed',
|
|
'Under Construction': 'under_construction',
|
|
'Planned': 'under_construction',
|
|
'SBNO': 'sbno',
|
|
// Also handle already-lowercase values
|
|
'operating': 'operating',
|
|
'closed': 'closed',
|
|
'under_construction': 'under_construction',
|
|
'maintenance': 'maintenance',
|
|
'sbno': 'sbno'
|
|
};
|
|
|
|
data.status = statusMap[data.status] || 'operating';
|
|
}
|
|
return data;
|
|
}
|
|
|
|
async function createPark(supabase: any, data: any): Promise<string> {
|
|
const submitterId = data._submitter_id;
|
|
let uploadedPhotos: any[] = [];
|
|
|
|
// Transform images object if present
|
|
if (data.images) {
|
|
const { uploaded, banner_assignment, card_assignment } = data.images;
|
|
|
|
if (uploaded && Array.isArray(uploaded)) {
|
|
// Store uploaded photos for later insertion into photos table
|
|
uploadedPhotos = uploaded;
|
|
|
|
// Assign banner image
|
|
if (banner_assignment !== undefined && uploaded[banner_assignment]) {
|
|
data.banner_image_id = uploaded[banner_assignment].cloudflare_id;
|
|
data.banner_image_url = uploaded[banner_assignment].url;
|
|
}
|
|
|
|
// Assign card image
|
|
if (card_assignment !== undefined && uploaded[card_assignment]) {
|
|
data.card_image_id = uploaded[card_assignment].cloudflare_id;
|
|
data.card_image_url = uploaded[card_assignment].url;
|
|
}
|
|
}
|
|
|
|
// Remove images object
|
|
delete data.images;
|
|
}
|
|
|
|
// Remove internal fields
|
|
delete data._submitter_id;
|
|
|
|
let parkId: string;
|
|
|
|
// Check if this is an edit (has park_id) or a new creation
|
|
if (data.park_id) {
|
|
console.log(`Updating existing park ${data.park_id}`);
|
|
parkId = data.park_id;
|
|
delete data.park_id; // Remove ID from update data
|
|
|
|
const normalizedData = normalizeStatusValue(data);
|
|
const sanitizedData = sanitizeDateFields(normalizedData);
|
|
const filteredData = filterDatabaseFields(sanitizedData, PARK_FIELDS);
|
|
const { error } = await supabase
|
|
.from('parks')
|
|
.update(filteredData)
|
|
.eq('id', parkId);
|
|
|
|
if (error) throw new Error(`Failed to update park: ${error.message}`);
|
|
} else {
|
|
console.log('Creating new park');
|
|
const normalizedData = normalizeStatusValue(data);
|
|
const sanitizedData = sanitizeDateFields(normalizedData);
|
|
const filteredData = filterDatabaseFields(sanitizedData, PARK_FIELDS);
|
|
const { data: park, error } = await supabase
|
|
.from('parks')
|
|
.insert(filteredData)
|
|
.select('id')
|
|
.single();
|
|
|
|
if (error) throw new Error(`Failed to create park: ${error.message}`);
|
|
parkId = park.id;
|
|
}
|
|
|
|
// Insert photos into photos table
|
|
if (uploadedPhotos.length > 0 && submitterId) {
|
|
console.log(`Inserting ${uploadedPhotos.length} photos for park ${parkId}`);
|
|
for (let i = 0; i < uploadedPhotos.length; i++) {
|
|
const photo = uploadedPhotos[i];
|
|
if (photo.cloudflare_id && photo.url) {
|
|
const { error: photoError } = await supabase.from('photos').insert({
|
|
entity_id: parkId,
|
|
entity_type: 'park',
|
|
cloudflare_image_id: photo.cloudflare_id,
|
|
cloudflare_image_url: photo.url,
|
|
caption: photo.caption || null,
|
|
title: null,
|
|
submitted_by: submitterId,
|
|
approved_at: new Date().toISOString(),
|
|
order_index: i,
|
|
});
|
|
|
|
if (photoError) {
|
|
console.error(`Failed to insert photo ${i}:`, photoError);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return parkId;
|
|
}
|
|
|
|
async function createRide(supabase: any, data: any): Promise<string> {
|
|
const submitterId = data._submitter_id;
|
|
let uploadedPhotos: any[] = [];
|
|
|
|
// Transform images object if present
|
|
if (data.images) {
|
|
const { uploaded, banner_assignment, card_assignment } = data.images;
|
|
|
|
if (uploaded && Array.isArray(uploaded)) {
|
|
// Store uploaded photos for later insertion into photos table
|
|
uploadedPhotos = uploaded;
|
|
|
|
// Assign banner image
|
|
if (banner_assignment !== undefined && uploaded[banner_assignment]) {
|
|
data.banner_image_id = uploaded[banner_assignment].cloudflare_id;
|
|
data.banner_image_url = uploaded[banner_assignment].url;
|
|
}
|
|
|
|
// Assign card image
|
|
if (card_assignment !== undefined && uploaded[card_assignment]) {
|
|
data.card_image_id = uploaded[card_assignment].cloudflare_id;
|
|
data.card_image_url = uploaded[card_assignment].url;
|
|
}
|
|
}
|
|
|
|
// Remove images object
|
|
delete data.images;
|
|
}
|
|
|
|
// Remove internal fields and store park_id before filtering
|
|
delete data._submitter_id;
|
|
const parkId = data.park_id;
|
|
|
|
let rideId: string;
|
|
|
|
// Check if this is an edit (has ride_id) or a new creation
|
|
if (data.ride_id) {
|
|
console.log(`Updating existing ride ${data.ride_id}`);
|
|
rideId = data.ride_id;
|
|
delete data.ride_id; // Remove ID from update data
|
|
|
|
const normalizedData = normalizeStatusValue(data);
|
|
const sanitizedData = sanitizeDateFields(normalizedData);
|
|
const filteredData = filterDatabaseFields(sanitizedData, RIDE_FIELDS);
|
|
const { error } = await supabase
|
|
.from('rides')
|
|
.update(filteredData)
|
|
.eq('id', rideId);
|
|
|
|
if (error) throw new Error(`Failed to update ride: ${error.message}`);
|
|
|
|
// Update park ride counts after successful ride update
|
|
if (parkId) {
|
|
console.log(`Updating ride counts for park ${parkId}`);
|
|
const { error: countError } = await supabase.rpc('update_park_ride_counts', {
|
|
target_park_id: parkId
|
|
});
|
|
|
|
if (countError) {
|
|
console.error('Failed to update park counts:', countError);
|
|
}
|
|
}
|
|
} else {
|
|
console.log('Creating new ride');
|
|
const normalizedData = normalizeStatusValue(data);
|
|
const sanitizedData = sanitizeDateFields(normalizedData);
|
|
const filteredData = filterDatabaseFields(sanitizedData, RIDE_FIELDS);
|
|
const { data: ride, error } = await supabase
|
|
.from('rides')
|
|
.insert(filteredData)
|
|
.select('id')
|
|
.single();
|
|
|
|
if (error) throw new Error(`Failed to create ride: ${error.message}`);
|
|
rideId = ride.id;
|
|
|
|
// Update park ride counts after successful ride creation
|
|
if (parkId) {
|
|
console.log(`Updating ride counts for park ${parkId}`);
|
|
const { error: countError } = await supabase.rpc('update_park_ride_counts', {
|
|
target_park_id: parkId
|
|
});
|
|
|
|
if (countError) {
|
|
console.error('Failed to update park counts:', countError);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Insert photos into photos table
|
|
if (uploadedPhotos.length > 0 && submitterId) {
|
|
console.log(`Inserting ${uploadedPhotos.length} photos for ride ${rideId}`);
|
|
for (let i = 0; i < uploadedPhotos.length; i++) {
|
|
const photo = uploadedPhotos[i];
|
|
if (photo.cloudflare_id && photo.url) {
|
|
const { error: photoError } = await supabase.from('photos').insert({
|
|
entity_id: rideId,
|
|
entity_type: 'ride',
|
|
cloudflare_image_id: photo.cloudflare_id,
|
|
cloudflare_image_url: photo.url,
|
|
caption: photo.caption || null,
|
|
title: null,
|
|
submitted_by: submitterId,
|
|
approved_at: new Date().toISOString(),
|
|
order_index: i,
|
|
});
|
|
|
|
if (photoError) {
|
|
console.error(`Failed to insert photo ${i}:`, photoError);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return rideId;
|
|
}
|
|
|
|
async function createCompany(supabase: any, data: any, companyType: string): Promise<string> {
|
|
// Transform images object if present
|
|
if (data.images) {
|
|
const { uploaded, banner_assignment, card_assignment } = data.images;
|
|
|
|
if (uploaded && Array.isArray(uploaded)) {
|
|
// Assign banner image
|
|
if (banner_assignment !== undefined && uploaded[banner_assignment]) {
|
|
data.banner_image_id = uploaded[banner_assignment].cloudflare_id;
|
|
data.banner_image_url = uploaded[banner_assignment].url;
|
|
}
|
|
|
|
// Assign card image
|
|
if (card_assignment !== undefined && uploaded[card_assignment]) {
|
|
data.card_image_id = uploaded[card_assignment].cloudflare_id;
|
|
data.card_image_url = uploaded[card_assignment].url;
|
|
}
|
|
}
|
|
|
|
// Remove images object
|
|
delete data.images;
|
|
}
|
|
|
|
// Check if this is an edit (has company_id or id) or a new creation
|
|
const companyId = data.company_id || data.id;
|
|
|
|
if (companyId) {
|
|
console.log(`Updating existing company ${companyId}`);
|
|
const updateData = sanitizeDateFields({ ...data, company_type: companyType });
|
|
delete updateData.company_id;
|
|
delete updateData.id; // Remove ID from update data
|
|
const filteredData = filterDatabaseFields(updateData, COMPANY_FIELDS);
|
|
|
|
const { error } = await supabase
|
|
.from('companies')
|
|
.update(filteredData)
|
|
.eq('id', companyId);
|
|
|
|
if (error) throw new Error(`Failed to update company: ${error.message}`);
|
|
return companyId;
|
|
} else {
|
|
console.log('Creating new company');
|
|
const companyData = sanitizeDateFields({ ...data, company_type: companyType });
|
|
const filteredData = filterDatabaseFields(companyData, COMPANY_FIELDS);
|
|
const { data: company, error } = await supabase
|
|
.from('companies')
|
|
.insert(filteredData)
|
|
.select('id')
|
|
.single();
|
|
|
|
if (error) throw new Error(`Failed to create company: ${error.message}`);
|
|
return company.id;
|
|
}
|
|
}
|
|
|
|
async function createRideModel(supabase: any, data: any): Promise<string> {
|
|
const sanitizedData = sanitizeDateFields(data);
|
|
const filteredData = filterDatabaseFields(sanitizedData, RIDE_MODEL_FIELDS);
|
|
const { data: model, error } = await supabase
|
|
.from('ride_models')
|
|
.insert(filteredData)
|
|
.select('id')
|
|
.single();
|
|
|
|
if (error) throw new Error(`Failed to create ride model: ${error.message}`);
|
|
return model.id;
|
|
}
|
|
|
|
async function approvePhotos(supabase: any, data: any, submissionItemId: string): Promise<void> {
|
|
const photos = data.photos || [];
|
|
|
|
for (const photo of photos) {
|
|
const photoData = {
|
|
entity_id: data.entity_id,
|
|
entity_type: data.context,
|
|
cloudflare_image_id: extractImageId(photo.url),
|
|
cloudflare_image_url: photo.url,
|
|
title: photo.title,
|
|
caption: photo.caption,
|
|
date_taken: photo.date,
|
|
order_index: photo.order,
|
|
submission_id: submissionItemId
|
|
};
|
|
|
|
const { error } = await supabase.from('photos').insert(photoData);
|
|
if (error) {
|
|
console.error('Failed to insert photo:', error);
|
|
throw new Error(`Failed to insert photo: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function extractImageId(url: string): string {
|
|
const matches = url.match(/\/([^\/]+)\/public$/);
|
|
return matches ? matches[1] : url;
|
|
}
|
|
|
|
async function editPhoto(supabase: any, data: any): Promise<void> {
|
|
console.log(`Editing photo ${data.photo_id}`);
|
|
const { error } = await supabase
|
|
.from('photos')
|
|
.update({
|
|
caption: data.new_caption,
|
|
})
|
|
.eq('id', data.photo_id);
|
|
|
|
if (error) throw new Error(`Failed to edit photo: ${error.message}`);
|
|
}
|
|
|
|
async function deletePhoto(supabase: any, data: any): Promise<void> {
|
|
console.log(`Deleting photo ${data.photo_id}`);
|
|
const { error } = await supabase
|
|
.from('photos')
|
|
.delete()
|
|
.eq('id', data.photo_id);
|
|
|
|
if (error) throw new Error(`Failed to delete photo: ${error.message}`);
|
|
}
|