mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-28 16:07:03 -05:00
Compare commits
2 Commits
c1683f9b02
...
fc7c2d5adc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc7c2d5adc | ||
|
|
98fbc94476 |
@@ -14,17 +14,27 @@ interface LocationResult {
|
|||||||
lat: string;
|
lat: string;
|
||||||
lon: string;
|
lon: string;
|
||||||
address: {
|
address: {
|
||||||
|
house_number?: string;
|
||||||
|
road?: string;
|
||||||
city?: string;
|
city?: string;
|
||||||
town?: string;
|
town?: string;
|
||||||
village?: string;
|
village?: string;
|
||||||
|
municipality?: string;
|
||||||
state?: string;
|
state?: string;
|
||||||
|
province?: string;
|
||||||
|
state_district?: string;
|
||||||
|
county?: string;
|
||||||
|
region?: string;
|
||||||
|
territory?: string;
|
||||||
country?: string;
|
country?: string;
|
||||||
|
country_code?: string;
|
||||||
postcode?: string;
|
postcode?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectedLocation {
|
interface SelectedLocation {
|
||||||
name: string;
|
name: string;
|
||||||
|
street_address?: string;
|
||||||
city?: string;
|
city?: string;
|
||||||
state_province?: string;
|
state_province?: string;
|
||||||
country: string;
|
country: string;
|
||||||
@@ -61,13 +71,14 @@ export function LocationSearch({ onLocationSelect, initialLocationId, className
|
|||||||
const loadInitialLocation = async (locationId: string): Promise<void> => {
|
const loadInitialLocation = async (locationId: string): Promise<void> => {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('locations')
|
.from('locations')
|
||||||
.select('id, name, city, state_province, country, postal_code, latitude, longitude, timezone')
|
.select('id, name, street_address, city, state_province, country, postal_code, latitude, longitude, timezone')
|
||||||
.eq('id', locationId)
|
.eq('id', locationId)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
if (data && !error) {
|
if (data && !error) {
|
||||||
setSelectedLocation({
|
setSelectedLocation({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
street_address: data.street_address || undefined,
|
||||||
city: data.city || undefined,
|
city: data.city || undefined,
|
||||||
state_province: data.state_province || undefined,
|
state_province: data.state_province || undefined,
|
||||||
country: data.country,
|
country: data.country,
|
||||||
@@ -150,21 +161,38 @@ export function LocationSearch({ onLocationSelect, initialLocationId, className
|
|||||||
|
|
||||||
// Safely access address properties with fallback
|
// Safely access address properties with fallback
|
||||||
const address = result.address || {};
|
const address = result.address || {};
|
||||||
const city = address.city || address.town || address.village;
|
|
||||||
const state = address.state || '';
|
|
||||||
const country = address.country || 'Unknown';
|
|
||||||
|
|
||||||
const locationName = city
|
// Extract street address components
|
||||||
? `${city}, ${state} ${country}`.trim()
|
const houseNumber = address.house_number || '';
|
||||||
: result.display_name;
|
const road = address.road || '';
|
||||||
|
const streetAddress = [houseNumber, road].filter(Boolean).join(' ').trim() || undefined;
|
||||||
|
|
||||||
|
// Extract city
|
||||||
|
const city = address.city || address.town || address.village || address.municipality;
|
||||||
|
|
||||||
|
// Extract state/province (try multiple fields for international support)
|
||||||
|
const state = address.state ||
|
||||||
|
address.province ||
|
||||||
|
address.state_district ||
|
||||||
|
address.county ||
|
||||||
|
address.region ||
|
||||||
|
address.territory;
|
||||||
|
|
||||||
|
const country = address.country || 'Unknown';
|
||||||
|
const postalCode = address.postcode;
|
||||||
|
|
||||||
|
// Build location name
|
||||||
|
const locationParts = [streetAddress, city, state, country].filter(Boolean);
|
||||||
|
const locationName = locationParts.join(', ');
|
||||||
|
|
||||||
// Build location data object (no database operations)
|
// Build location data object (no database operations)
|
||||||
const locationData: SelectedLocation = {
|
const locationData: SelectedLocation = {
|
||||||
name: locationName,
|
name: locationName,
|
||||||
|
street_address: streetAddress,
|
||||||
city: city || undefined,
|
city: city || undefined,
|
||||||
state_province: state || undefined,
|
state_province: state || undefined,
|
||||||
country: country,
|
country: country,
|
||||||
postal_code: address.postcode || undefined,
|
postal_code: postalCode || undefined,
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
timezone: undefined, // Will be set by server during approval if needed
|
timezone: undefined, // Will be set by server during approval if needed
|
||||||
@@ -249,6 +277,7 @@ export function LocationSearch({ onLocationSelect, initialLocationId, className
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="font-medium">{selectedLocation.name}</p>
|
<p className="font-medium">{selectedLocation.name}</p>
|
||||||
<div className="text-sm text-muted-foreground space-y-1 mt-1">
|
<div className="text-sm text-muted-foreground space-y-1 mt-1">
|
||||||
|
{selectedLocation.street_address && <p>Street: {selectedLocation.street_address}</p>}
|
||||||
{selectedLocation.city && <p>City: {selectedLocation.city}</p>}
|
{selectedLocation.city && <p>City: {selectedLocation.city}</p>}
|
||||||
{selectedLocation.state_province && <p>State/Province: {selectedLocation.state_province}</p>}
|
{selectedLocation.state_province && <p>State/Province: {selectedLocation.state_province}</p>}
|
||||||
<p>Country: {selectedLocation.country}</p>
|
<p>Country: {selectedLocation.country}</p>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Star, TrendingUp, Award, Castle, FerrisWheel, Waves, Tent, LucideIcon } from 'lucide-react';
|
import { Star, TrendingUp, Award, Castle, FerrisWheel, Waves, Tent, LucideIcon } from 'lucide-react';
|
||||||
|
import { formatLocationShort } from '@/lib/locationFormatter';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -82,7 +83,7 @@ export function FeaturedParks() {
|
|||||||
|
|
||||||
{park.location && (
|
{park.location && (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{park.location.city}, {park.location.country}
|
{formatLocationShort(park.location)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -109,9 +109,11 @@ export function RichParkDisplay({ data, actionType, showAllFields = true }: Rich
|
|||||||
<span className="text-sm font-semibold text-foreground">Location</span>
|
<span className="text-sm font-semibold text-foreground">Location</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm space-y-1 ml-6">
|
<div className="text-sm space-y-1 ml-6">
|
||||||
|
{location.street_address && <div><span className="text-muted-foreground">Street:</span> <span className="font-medium">{location.street_address}</span></div>}
|
||||||
{location.city && <div><span className="text-muted-foreground">City:</span> <span className="font-medium">{location.city}</span></div>}
|
{location.city && <div><span className="text-muted-foreground">City:</span> <span className="font-medium">{location.city}</span></div>}
|
||||||
{location.state_province && <div><span className="text-muted-foreground">State/Province:</span> <span className="font-medium">{location.state_province}</span></div>}
|
{location.state_province && <div><span className="text-muted-foreground">State/Province:</span> <span className="font-medium">{location.state_province}</span></div>}
|
||||||
{location.country && <div><span className="text-muted-foreground">Country:</span> <span className="font-medium">{location.country}</span></div>}
|
{location.country && <div><span className="text-muted-foreground">Country:</span> <span className="font-medium">{location.country}</span></div>}
|
||||||
|
{location.postal_code && <div><span className="text-muted-foreground">Postal Code:</span> <span className="font-medium">{location.postal_code}</span></div>}
|
||||||
{location.formatted_address && (
|
{location.formatted_address && (
|
||||||
<div className="text-xs text-muted-foreground mt-2">{location.formatted_address}</div>
|
<div className="text-xs text-muted-foreground mt-2">{location.formatted_address}</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { MapPin, Star, Users, Clock, Castle, FerrisWheel, Waves, Tent } from 'lucide-react';
|
import { MapPin, Star, Users, Clock, Castle, FerrisWheel, Waves, Tent } from 'lucide-react';
|
||||||
|
import { formatLocationShort } from '@/lib/locationFormatter';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@@ -102,7 +103,7 @@ export function ParkCard({ park }: ParkCardProps) {
|
|||||||
<div className="flex items-center gap-1 text-sm text-muted-foreground min-w-0">
|
<div className="flex items-center gap-1 text-sm text-muted-foreground min-w-0">
|
||||||
<MapPin className="w-3 h-3 flex-shrink-0" />
|
<MapPin className="w-3 h-3 flex-shrink-0" />
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{park.location.city && `${park.location.city}, `}{park.location.country}
|
{formatLocationShort(park.location)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
|
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
|
||||||
import { useGlobalSearch } from '@/hooks/search/useGlobalSearch';
|
import { useGlobalSearch } from '@/hooks/search/useGlobalSearch';
|
||||||
|
import { formatLocationShort } from '@/lib/locationFormatter';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -87,7 +88,7 @@ export function SearchResults({ query, onClose }: SearchResultsProps) {
|
|||||||
switch (result.type) {
|
switch (result.type) {
|
||||||
case 'park':
|
case 'park':
|
||||||
const park = result.data as Park;
|
const park = result.data as Park;
|
||||||
return park.location ? `${park.location.city}, ${park.location.country}` : 'Theme Park';
|
return park.location ? formatLocationShort(park.location) : 'Theme Park';
|
||||||
case 'ride':
|
case 'ride':
|
||||||
const ride = result.data as Ride;
|
const ride = result.data as Ride;
|
||||||
return ride.park && typeof ride.park === 'object' && 'name' in ride.park
|
return ride.park && typeof ride.park === 'object' && 'name' in ride.park
|
||||||
|
|||||||
@@ -1615,6 +1615,7 @@ export type Database = {
|
|||||||
name: string
|
name: string
|
||||||
postal_code: string | null
|
postal_code: string | null
|
||||||
state_province: string | null
|
state_province: string | null
|
||||||
|
street_address: string | null
|
||||||
timezone: string | null
|
timezone: string | null
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
@@ -1627,6 +1628,7 @@ export type Database = {
|
|||||||
name: string
|
name: string
|
||||||
postal_code?: string | null
|
postal_code?: string | null
|
||||||
state_province?: string | null
|
state_province?: string | null
|
||||||
|
street_address?: string | null
|
||||||
timezone?: string | null
|
timezone?: string | null
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
@@ -1639,6 +1641,7 @@ export type Database = {
|
|||||||
name?: string
|
name?: string
|
||||||
postal_code?: string | null
|
postal_code?: string | null
|
||||||
state_province?: string | null
|
state_province?: string | null
|
||||||
|
street_address?: string | null
|
||||||
timezone?: string | null
|
timezone?: string | null
|
||||||
}
|
}
|
||||||
Relationships: []
|
Relationships: []
|
||||||
|
|||||||
64
src/lib/locationFormatter.ts
Normal file
64
src/lib/locationFormatter.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Location Formatting Utilities
|
||||||
|
*
|
||||||
|
* Centralized utilities for formatting location data consistently across the app.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface LocationData {
|
||||||
|
street_address?: string | null;
|
||||||
|
city?: string | null;
|
||||||
|
state_province?: string | null;
|
||||||
|
country?: string | null;
|
||||||
|
postal_code?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format location for display
|
||||||
|
* @param location - Location data object
|
||||||
|
* @param includeStreet - Whether to include street address in the output
|
||||||
|
* @returns Formatted location string or null if no location data
|
||||||
|
*/
|
||||||
|
export function formatLocationDisplay(
|
||||||
|
location: LocationData | null | undefined,
|
||||||
|
includeStreet: boolean = false
|
||||||
|
): string | null {
|
||||||
|
if (!location) return null;
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (includeStreet && location.street_address) {
|
||||||
|
parts.push(location.street_address);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.city) {
|
||||||
|
parts.push(location.city);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.state_province) {
|
||||||
|
parts.push(location.state_province);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.country) {
|
||||||
|
parts.push(location.country);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts.join(', ') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format full address including street
|
||||||
|
* @param location - Location data object
|
||||||
|
* @returns Formatted full address or null if no location data
|
||||||
|
*/
|
||||||
|
export function formatFullAddress(location: LocationData | null | undefined): string | null {
|
||||||
|
return formatLocationDisplay(location, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format location without street address (city, state, country only)
|
||||||
|
* @param location - Location data object
|
||||||
|
* @returns Formatted location without street or null if no location data
|
||||||
|
*/
|
||||||
|
export function formatLocationShort(location: LocationData | null | undefined): string | null {
|
||||||
|
return formatLocationDisplay(location, false);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { MapPin, Star, Clock, Phone, Globe, Calendar, ArrowLeft, Users, Zap, Camera, Castle, FerrisWheel, Waves, Tent, Plus } from 'lucide-react';
|
import { MapPin, Star, Clock, Phone, Globe, Calendar, ArrowLeft, Users, Zap, Camera, Castle, FerrisWheel, Waves, Tent, Plus } from 'lucide-react';
|
||||||
|
import { formatLocationShort } from '@/lib/locationFormatter';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { ReviewsSection } from '@/components/reviews/ReviewsSection';
|
import { ReviewsSection } from '@/components/reviews/ReviewsSection';
|
||||||
import { RideCard } from '@/components/rides/RideCard';
|
import { RideCard } from '@/components/rides/RideCard';
|
||||||
@@ -248,7 +249,7 @@ export default function ParkDetail() {
|
|||||||
</h1>
|
</h1>
|
||||||
{park.location && <div className="flex items-center text-white/90 text-lg">
|
{park.location && <div className="flex items-center text-white/90 text-lg">
|
||||||
<MapPin className="w-5 h-5 mr-2" />
|
<MapPin className="w-5 h-5 mr-2" />
|
||||||
{park.location.city && `${park.location.city}, `}{park.location.country}
|
{formatLocationShort(park.location)}
|
||||||
</div>}
|
</div>}
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<VersionIndicator
|
<VersionIndicator
|
||||||
@@ -470,11 +471,25 @@ export default function ParkDetail() {
|
|||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
<div className="font-medium text-foreground mb-1">Address:</div>
|
<div className="font-medium text-foreground mb-1">Address:</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{park.location.name && <div>{park.location.name}</div>}
|
{/* Street Address on its own line if it exists */}
|
||||||
{park.location.city && <div>{park.location.city}</div>}
|
{park.location.street_address && (
|
||||||
{park.location.state_province && <div>{park.location.state_province}</div>}
|
<div>{park.location.street_address}</div>
|
||||||
{park.location.postal_code && <div>{park.location.postal_code}</div>}
|
)}
|
||||||
|
|
||||||
|
{/* City, State Postal on same line */}
|
||||||
|
{(park.location.city || park.location.state_province || park.location.postal_code) && (
|
||||||
|
<div>
|
||||||
|
{park.location.city}
|
||||||
|
{park.location.city && park.location.state_province && ', '}
|
||||||
|
{park.location.state_province}
|
||||||
|
{park.location.postal_code && ` ${park.location.postal_code}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Country on its own line */}
|
||||||
|
{park.location.country && (
|
||||||
<div>{park.location.country}</div>
|
<div>{park.location.country}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -57,11 +57,13 @@ export interface LocationInfoSettings {
|
|||||||
* Location data structure
|
* Location data structure
|
||||||
*/
|
*/
|
||||||
export interface LocationData {
|
export interface LocationData {
|
||||||
|
street_address?: string;
|
||||||
country?: string;
|
country?: string;
|
||||||
state_province?: string;
|
state_province?: string;
|
||||||
city?: string;
|
city?: string;
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
|
postal_code?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,10 +73,12 @@ export function isLocationData(data: unknown): data is LocationData {
|
|||||||
if (typeof data !== 'object' || data === null) return false;
|
if (typeof data !== 'object' || data === null) return false;
|
||||||
const loc = data as Record<string, unknown>;
|
const loc = data as Record<string, unknown>;
|
||||||
return (
|
return (
|
||||||
|
(loc.street_address === undefined || typeof loc.street_address === 'string') &&
|
||||||
(loc.country === undefined || typeof loc.country === 'string') &&
|
(loc.country === undefined || typeof loc.country === 'string') &&
|
||||||
(loc.state_province === undefined || typeof loc.state_province === 'string') &&
|
(loc.state_province === undefined || typeof loc.state_province === 'string') &&
|
||||||
(loc.city === undefined || typeof loc.city === 'string') &&
|
(loc.city === undefined || typeof loc.city === 'string') &&
|
||||||
(loc.latitude === undefined || typeof loc.latitude === 'number') &&
|
(loc.latitude === undefined || typeof loc.latitude === 'number') &&
|
||||||
(loc.longitude === undefined || typeof loc.longitude === 'number')
|
(loc.longitude === undefined || typeof loc.longitude === 'number') &&
|
||||||
|
(loc.postal_code === undefined || typeof loc.postal_code === 'string')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- Add street_address column to locations table
|
||||||
|
ALTER TABLE locations
|
||||||
|
ADD COLUMN street_address TEXT;
|
||||||
|
|
||||||
|
-- Add comment explaining the column
|
||||||
|
COMMENT ON COLUMN locations.street_address IS 'Street address including house number and road name (e.g., "375 North Lagoon Drive")';
|
||||||
|
|
||||||
|
-- Add index for potential searches
|
||||||
|
CREATE INDEX idx_locations_street_address ON locations(street_address);
|
||||||
|
|
||||||
|
-- Update existing records: extract from name if it looks like an address
|
||||||
|
-- (This is best-effort cleanup for existing data)
|
||||||
|
UPDATE locations
|
||||||
|
SET street_address = CASE
|
||||||
|
WHEN name ~ '^\d+\s+.*' THEN
|
||||||
|
regexp_replace(name, ',.*$', '') -- Extract everything before first comma
|
||||||
|
ELSE NULL
|
||||||
|
END
|
||||||
|
WHERE street_address IS NULL;
|
||||||
Reference in New Issue
Block a user