Compare commits

..

2 Commits

Author SHA1 Message Date
gpt-engineer-app[bot]
fc7c2d5adc Refactor park detail address display
Implement the plan to refactor the address display in the park detail page. This includes updating the sidebar address to show the street address on its own line, followed by city, state, and postal code on the next line, and the country on a separate line. This change aims to create a more compact and natural address format.
2025-11-06 14:03:58 +00:00
gpt-engineer-app[bot]
98fbc94476 feat: Add street address to locations
Adds a street_address column to the locations table and updates the LocationSearch component to capture, store, and display full street addresses. This includes database migration, interface updates, and formatter logic.
2025-11-06 13:51:40 +00:00
10 changed files with 157 additions and 18 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: []

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

View File

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

View File

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

View File

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