diff --git a/src/components/admin/LocationSearch.tsx b/src/components/admin/LocationSearch.tsx index e9923583..66c397e0 100644 --- a/src/components/admin/LocationSearch.tsx +++ b/src/components/admin/LocationSearch.tsx @@ -23,7 +23,6 @@ interface LocationResult { } interface SelectedLocation { - id?: string; name: string; city?: string; state_province?: string; @@ -32,6 +31,7 @@ interface SelectedLocation { latitude: number; longitude: number; timezone?: string; + display_name: string; // Full OSM display name for reference } interface LocationSearchProps { @@ -61,11 +61,10 @@ export function LocationSearch({ onLocationSelect, initialLocationId, className .from('locations') .select('*') .eq('id', locationId) - .single(); + .maybeSingle(); if (data && !error) { setSelectedLocation({ - id: data.id, name: data.name, city: data.city || undefined, state_province: data.state_province || undefined, @@ -74,6 +73,7 @@ export function LocationSearch({ onLocationSelect, initialLocationId, className latitude: parseFloat(data.latitude?.toString() || '0'), longitude: parseFloat(data.longitude?.toString() || '0'), timezone: data.timezone || undefined, + display_name: data.name, // Use name as display for existing locations }); } }; @@ -94,12 +94,30 @@ export function LocationSearch({ onLocationSelect, initialLocationId, className }, } ); + + // Check if response is OK and content-type is JSON + if (!response.ok) { + console.error('OpenStreetMap API error:', response.status); + setResults([]); + setShowResults(false); + return; + } + + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + console.error('Invalid response format from OpenStreetMap'); + setResults([]); + setShowResults(false); + return; + } + const data = await response.json(); setResults(data); setShowResults(true); } catch (error) { console.error('Error searching locations:', error); setResults([]); + setShowResults(false); } finally { setIsSearching(false); } @@ -123,61 +141,18 @@ export function LocationSearch({ onLocationSelect, initialLocationId, className ? `${city}, ${result.address.state || ''} ${result.address.country}`.trim() : result.display_name; - // Check if location exists in database - const { data: existingLocation } = await supabase - .from('locations') - .select('*') - .eq('latitude', latitude) - .eq('longitude', longitude) - .maybeSingle(); - - let locationData: SelectedLocation; - - if (existingLocation) { - locationData = { - id: existingLocation.id, - name: existingLocation.name, - city: existingLocation.city || undefined, - state_province: existingLocation.state_province || undefined, - country: existingLocation.country, - postal_code: existingLocation.postal_code || undefined, - latitude: parseFloat(existingLocation.latitude?.toString() || '0'), - longitude: parseFloat(existingLocation.longitude?.toString() || '0'), - timezone: existingLocation.timezone || undefined, - }; - } else { - // Create new location - const { data: newLocation, error } = await supabase - .from('locations') - .insert({ - name: locationName, - city: city || null, - state_province: result.address.state || null, - country: result.address.country || '', - postal_code: result.address.postcode || null, - latitude, - longitude, - }) - .select() - .single(); - - if (error || !newLocation) { - console.error('Error creating location:', error); - return; - } - - locationData = { - id: newLocation.id, - name: newLocation.name, - city: newLocation.city || undefined, - state_province: newLocation.state_province || undefined, - country: newLocation.country, - postal_code: newLocation.postal_code || undefined, - latitude: parseFloat(newLocation.latitude?.toString() || '0'), - longitude: parseFloat(newLocation.longitude?.toString() || '0'), - timezone: newLocation.timezone || undefined, - }; - } + // Build location data object (no database operations) + const locationData: SelectedLocation = { + name: locationName, + city: city || undefined, + state_province: result.address.state || undefined, + country: result.address.country || '', + postal_code: result.address.postcode || undefined, + latitude, + longitude, + timezone: undefined, // Will be set by server during approval if needed + display_name: result.display_name, + }; setSelectedLocation(locationData); setSearchQuery(''); diff --git a/src/components/admin/ParkForm.tsx b/src/components/admin/ParkForm.tsx index 4fc1ae87..eb40dff2 100644 --- a/src/components/admin/ParkForm.tsx +++ b/src/components/admin/ParkForm.tsx @@ -28,6 +28,17 @@ const parkSchema = z.object({ status: z.string().min(1, 'Status is required'), opening_date: z.string().optional(), closing_date: z.string().optional(), + location: z.object({ + name: z.string(), + city: z.string().optional(), + state_province: z.string().optional(), + country: z.string(), + postal_code: z.string().optional(), + latitude: z.number(), + longitude: z.number(), + timezone: z.string().optional(), + display_name: z.string(), + }).optional(), location_id: z.string().uuid().optional(), website_url: z.string().url().optional().or(z.literal('')), phone: z.string().optional(), @@ -289,12 +300,12 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: { - setValue('location_id', location.id); + setValue('location', location); }} initialLocationId={watch('location_id')} />

- Search for the park's location using OpenStreetMap + Search for the park's location using OpenStreetMap. Location will be created when submission is approved.

diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index 5e9a160d..a3647855 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -61,7 +61,21 @@ export interface ParkFormData { email?: string; operator_id?: string; property_owner_id?: string; + + // Location can be stored as object for new submissions or ID for editing + location?: { + name: string; + city?: string; + state_province?: string; + country: string; + postal_code?: string; + latitude: number; + longitude: number; + timezone?: string; + display_name: string; + }; location_id?: string; + images?: ImageAssignments; banner_image_url?: string; banner_image_id?: string; diff --git a/src/lib/submissionItemsService.ts b/src/lib/submissionItemsService.ts index 4e9dc6a2..b9ac6a8d 100644 --- a/src/lib/submissionItemsService.ts +++ b/src/lib/submissionItemsService.ts @@ -294,6 +294,12 @@ async function createPark(data: any, dependencyMap: Map): Promis // Handle park edit const resolvedData = resolveDependencies(data, dependencyMap); + // Resolve location_id if location data is provided + let locationId = resolvedData.location_id; + if (resolvedData.location && !locationId) { + locationId = await resolveLocationId(resolvedData.location); + } + // Extract image assignments from ImageAssignments structure const imageData = extractImageAssignments(resolvedData.images); @@ -311,7 +317,7 @@ async function createPark(data: any, dependencyMap: Map): Promis email: resolvedData.email || null, operator_id: resolvedData.operator_id || null, property_owner_id: resolvedData.property_owner_id || null, - location_id: resolvedData.location_id || null, + location_id: locationId || null, ...imageData, updated_at: new Date().toISOString() }; @@ -333,6 +339,12 @@ async function createPark(data: any, dependencyMap: Map): Promis validateSubmissionData(data, 'Park'); const resolvedData = resolveDependencies(data, dependencyMap); + // Resolve location_id if location data is provided + let locationId = resolvedData.location_id; + if (resolvedData.location && !locationId) { + locationId = await resolveLocationId(resolvedData.location); + } + // Ensure unique slug const uniqueSlug = await ensureUniqueSlug(resolvedData.slug, 'parks'); resolvedData.slug = uniqueSlug; @@ -341,7 +353,11 @@ async function createPark(data: any, dependencyMap: Map): Promis const imageData = extractImageAssignments(resolvedData.images); // Transform to database format - const parkData = { ...transformParkData(resolvedData), ...imageData }; + const parkData = { + ...transformParkData(resolvedData), + ...imageData, + location_id: locationId || null, + }; // Insert into database const { data: park, error } = await supabase @@ -358,6 +374,51 @@ async function createPark(data: any, dependencyMap: Map): Promis return park.id; } +/** + * Resolve location data to a location_id + * Checks for existing locations by coordinates, creates new ones if needed + */ +async function resolveLocationId(locationData: any): Promise { + if (!locationData || !locationData.latitude || !locationData.longitude) { + return null; + } + + // Check if location already exists by coordinates + const { data: existingLocation } = await supabase + .from('locations') + .select('id') + .eq('latitude', locationData.latitude) + .eq('longitude', locationData.longitude) + .maybeSingle(); + + if (existingLocation) { + return existingLocation.id; + } + + // Create new location (moderator has permission via RLS) + const { data: newLocation, error } = await supabase + .from('locations') + .insert({ + name: locationData.name, + city: locationData.city || null, + state_province: locationData.state_province || null, + country: locationData.country, + postal_code: locationData.postal_code || null, + latitude: locationData.latitude, + longitude: locationData.longitude, + timezone: locationData.timezone || null, + }) + .select('id') + .single(); + + if (error) { + console.error('Error creating location:', error); + throw new Error(`Failed to create location: ${error.message}`); + } + + return newLocation.id; +} + async function createRide(data: any, dependencyMap: Map): Promise { const { transformRideData, validateSubmissionData } = await import('./entityTransformers'); const { ensureUniqueSlug } = await import('./slugUtils');