diff --git a/src/components/settings/LocationTab.tsx b/src/components/settings/LocationTab.tsx index c485174c..d3a06a2c 100644 --- a/src/components/settings/LocationTab.tsx +++ b/src/components/settings/LocationTab.tsx @@ -11,8 +11,9 @@ import { Separator } from '@/components/ui/separator'; import { Switch } from '@/components/ui/switch'; import { useToast } from '@/hooks/use-toast'; import { useAuth } from '@/hooks/useAuth'; +import { useUnitPreferences } from '@/hooks/useUnitPreferences'; import { supabase } from '@/integrations/supabase/client'; -import { MapPin, Calendar, Globe, Accessibility } from 'lucide-react'; +import { MapPin, Calendar, Globe, Accessibility, Ruler } from 'lucide-react'; const locationSchema = z.object({ preferred_pronouns: z.string().max(20).optional(), timezone: z.string(), @@ -35,6 +36,7 @@ export function LocationTab() { const { toast } = useToast(); + const { preferences: unitPreferences, updatePreferences: updateUnitPreferences } = useUnitPreferences(); const [loading, setLoading] = useState(false); const [parks, setParks] = useState([]); const [accessibility, setAccessibility] = useState({ @@ -115,10 +117,13 @@ export function LocationTab() { if (accessibilityError) throw accessibilityError; + // Save unit preferences + await updateUnitPreferences(unitPreferences); + await refreshProfile(); toast({ title: 'Settings saved', - description: 'Your location, personal information, and accessibility settings have been updated.' + description: 'Your location, personal information, accessibility, and unit preferences have been updated.' }); } catch (error: any) { toast({ @@ -243,6 +248,58 @@ export function LocationTab() { + {/* Unit Preferences */} +
+
+ +

Units & Measurements

+
+ + + + + Choose your preferred measurement system for displaying distances, speeds, and other measurements. + + + +
+ + +
+ +
+
+ +

+ Automatically set measurement system based on your location when you first visit +

+
+ + updateUnitPreferences({ auto_detect: checked }) + } + /> +
+
+
+
+ + + {/* Accessibility Options */}
diff --git a/src/components/ui/measurement-display.tsx b/src/components/ui/measurement-display.tsx new file mode 100644 index 00000000..f20c6941 --- /dev/null +++ b/src/components/ui/measurement-display.tsx @@ -0,0 +1,100 @@ +import { useMemo } from 'react'; +import { useAuth } from '@/hooks/useAuth'; +import { + convertSpeed, + convertDistance, + convertHeight, + getSpeedUnit, + getDistanceUnit, + getHeightUnit, + getShortDistanceUnit, + type MeasurementSystem, + type UnitPreferences +} from '@/lib/units'; + +interface MeasurementDisplayProps { + value: number; + type: 'speed' | 'distance' | 'height' | 'short_distance'; + showBothUnits?: boolean; + className?: string; +} + +export function MeasurementDisplay({ + value, + type, + showBothUnits = false, + className = "" +}: MeasurementDisplayProps) { + const { profile } = useAuth(); + + const unitPreferences = useMemo(() => { + // Get unit preferences from user profile or default to metric + const defaultPrefs: UnitPreferences = { + measurement_system: 'metric', + temperature: 'celsius', + auto_detect: true + }; + + // If no profile or no preferences, use default + if (!profile) return defaultPrefs; + + // Try to get preferences from profile (this will be populated from user_preferences) + const storedPrefs = (profile as any)?.unit_preferences; + if (storedPrefs && typeof storedPrefs === 'object') { + return { ...defaultPrefs, ...storedPrefs }; + } + + return defaultPrefs; + }, [profile]); + + const { displayValue, unit, alternateDisplay } = useMemo(() => { + const system = unitPreferences.measurement_system; + + let displayValue: number; + let unit: string; + let alternateValue: number; + let alternateUnit: string; + + switch (type) { + case 'speed': + displayValue = convertSpeed(value, system); + unit = getSpeedUnit(system); + alternateValue = convertSpeed(value, system === 'metric' ? 'imperial' : 'metric'); + alternateUnit = getSpeedUnit(system === 'metric' ? 'imperial' : 'metric'); + break; + case 'distance': + displayValue = convertDistance(value, system); + unit = getDistanceUnit(system); + alternateValue = convertDistance(value, system === 'metric' ? 'imperial' : 'metric'); + alternateUnit = getDistanceUnit(system === 'metric' ? 'imperial' : 'metric'); + break; + case 'height': + displayValue = convertHeight(value, system); + unit = getHeightUnit(system); + alternateValue = convertHeight(value, system === 'metric' ? 'imperial' : 'metric'); + alternateUnit = getHeightUnit(system === 'metric' ? 'imperial' : 'metric'); + break; + case 'short_distance': + displayValue = convertDistance(value, system); + unit = getShortDistanceUnit(system); + alternateValue = convertDistance(value, system === 'metric' ? 'imperial' : 'metric'); + alternateUnit = getShortDistanceUnit(system === 'metric' ? 'imperial' : 'metric'); + break; + default: + displayValue = value; + unit = ''; + alternateValue = value; + alternateUnit = ''; + } + + const alternateDisplay = showBothUnits ? ` (${alternateValue} ${alternateUnit})` : ''; + + return { displayValue, unit, alternateDisplay }; + }, [value, type, unitPreferences.measurement_system, showBothUnits]); + + return ( + + {displayValue} {unit}{alternateDisplay} + + ); +} \ No newline at end of file diff --git a/src/hooks/useUnitPreferences.ts b/src/hooks/useUnitPreferences.ts new file mode 100644 index 00000000..6cf0dcd7 --- /dev/null +++ b/src/hooks/useUnitPreferences.ts @@ -0,0 +1,119 @@ +import { useState, useEffect } from 'react'; +import { useAuth } from '@/hooks/useAuth'; +import { supabase } from '@/integrations/supabase/client'; +import { UnitPreferences, getMeasurementSystemFromCountry } from '@/lib/units'; + +const DEFAULT_PREFERENCES: UnitPreferences = { + measurement_system: 'metric', + temperature: 'celsius', + auto_detect: true +}; + +export function useUnitPreferences() { + const { user } = useAuth(); + const [preferences, setPreferences] = useState(DEFAULT_PREFERENCES); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadPreferences(); + }, [user]); + + const loadPreferences = async () => { + try { + if (user) { + // Load from database for logged-in users + const { data } = await supabase + .from('user_preferences') + .select('unit_preferences') + .eq('user_id', user.id) + .maybeSingle(); + + if (data?.unit_preferences && typeof data.unit_preferences === 'object') { + setPreferences({ ...DEFAULT_PREFERENCES, ...(data.unit_preferences as unknown as UnitPreferences) }); + } else { + // Auto-detect for new users + await autoDetectPreferences(); + } + } else { + // Check localStorage for anonymous users + const stored = localStorage.getItem('unit_preferences'); + if (stored) { + try { + const parsed = JSON.parse(stored); + setPreferences({ ...DEFAULT_PREFERENCES, ...parsed }); + } catch (e) { + await autoDetectPreferences(); + } + } else { + await autoDetectPreferences(); + } + } + } catch (error) { + console.error('Error loading unit preferences:', error); + await autoDetectPreferences(); + } finally { + setLoading(false); + } + }; + + const autoDetectPreferences = async () => { + try { + const response = await supabase.functions.invoke('detect-location'); + + if (response.data && response.data.measurementSystem) { + const newPreferences: UnitPreferences = { + ...DEFAULT_PREFERENCES, + measurement_system: response.data.measurementSystem, + }; + + setPreferences(newPreferences); + + // Save to localStorage for anonymous users + if (!user) { + localStorage.setItem('unit_preferences', JSON.stringify(newPreferences)); + } + + return newPreferences; + } + } catch (error) { + console.error('Error auto-detecting location:', error); + } + + // Fallback to default + setPreferences(DEFAULT_PREFERENCES); + return DEFAULT_PREFERENCES; + }; + + const updatePreferences = async (newPreferences: Partial) => { + const updated = { ...preferences, ...newPreferences }; + setPreferences(updated); + + try { + if (user) { + // Save to database for logged-in users + await supabase + .from('user_preferences') + .update({ + unit_preferences: updated, + updated_at: new Date().toISOString() + }) + .eq('user_id', user.id); + } else { + // Save to localStorage for anonymous users + localStorage.setItem('unit_preferences', JSON.stringify(updated)); + } + } catch (error) { + console.error('Error saving unit preferences:', error); + // Revert on error + setPreferences(preferences); + throw error; + } + }; + + return { + preferences, + loading, + updatePreferences, + autoDetectPreferences + }; +} \ No newline at end of file diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 9657dfff..b158eb6a 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -782,6 +782,7 @@ export type Database = { id: string privacy_settings: Json push_notifications: Json + unit_preferences: Json updated_at: string user_id: string } @@ -792,6 +793,7 @@ export type Database = { id?: string privacy_settings?: Json push_notifications?: Json + unit_preferences?: Json updated_at?: string user_id: string } @@ -802,6 +804,7 @@ export type Database = { id?: string privacy_settings?: Json push_notifications?: Json + unit_preferences?: Json updated_at?: string user_id?: string } diff --git a/src/lib/units.ts b/src/lib/units.ts new file mode 100644 index 00000000..39e01871 --- /dev/null +++ b/src/lib/units.ts @@ -0,0 +1,56 @@ +export type MeasurementSystem = 'metric' | 'imperial'; + +export interface UnitPreferences { + measurement_system: MeasurementSystem; + temperature: 'celsius' | 'fahrenheit'; + auto_detect: boolean; +} + +// Speed conversions +export function convertSpeed(kmh: number, system: MeasurementSystem): number { + if (system === 'imperial') { + return Math.round(kmh * 0.621371); + } + return Math.round(kmh); +} + +// Distance conversions (meters to feet) +export function convertDistance(meters: number, system: MeasurementSystem): number { + if (system === 'imperial') { + return Math.round(meters * 3.28084); + } + return Math.round(meters); +} + +// Height conversions (cm to inches) +export function convertHeight(cm: number, system: MeasurementSystem): number { + if (system === 'imperial') { + return Math.round(cm * 0.393701); + } + return Math.round(cm); +} + +// Get unit labels +export function getSpeedUnit(system: MeasurementSystem): string { + return system === 'imperial' ? 'mph' : 'km/h'; +} + +export function getDistanceUnit(system: MeasurementSystem): string { + return system === 'imperial' ? 'ft' : 'm'; +} + +export function getHeightUnit(system: MeasurementSystem): string { + return system === 'imperial' ? 'in' : 'cm'; +} + +export function getShortDistanceUnit(system: MeasurementSystem): string { + return system === 'imperial' ? 'ft' : 'm'; +} + +// Countries that primarily use imperial system +export const IMPERIAL_COUNTRIES = ['US', 'LR', 'MM']; + +// Detect measurement system from country code +export function getMeasurementSystemFromCountry(countryCode: string): MeasurementSystem { + return IMPERIAL_COUNTRIES.includes(countryCode.toUpperCase()) ? 'imperial' : 'metric'; +} \ No newline at end of file diff --git a/src/pages/ParkDetail.tsx b/src/pages/ParkDetail.tsx index 43214e25..1bcc6cba 100644 --- a/src/pages/ParkDetail.tsx +++ b/src/pages/ParkDetail.tsx @@ -19,6 +19,7 @@ import { Camera } from 'lucide-react'; import { ReviewsSection } from '@/components/reviews/ReviewsSection'; +import { MeasurementDisplay } from '@/components/ui/measurement-display'; import { Park, Ride } from '@/types/database'; import { supabase } from '@/integrations/supabase/client'; @@ -412,12 +413,12 @@ export default function ParkDetail() {
{ride.max_speed_kmh && ( - {ride.max_speed_kmh} km/h + )} {ride.max_height_meters && ( - {ride.max_height_meters}m + )}
diff --git a/src/pages/RideDetail.tsx b/src/pages/RideDetail.tsx index cd5a8c6c..e0d67c0c 100644 --- a/src/pages/RideDetail.tsx +++ b/src/pages/RideDetail.tsx @@ -21,6 +21,7 @@ import { AlertTriangle } from 'lucide-react'; import { ReviewsSection } from '@/components/reviews/ReviewsSection'; +import { MeasurementDisplay } from '@/components/ui/measurement-display'; import { Ride } from '@/types/database'; import { supabase } from '@/integrations/supabase/client'; @@ -206,8 +207,9 @@ export default function RideDetail() { -
{ride.max_speed_kmh}
-
km/h
+
+ +
)} @@ -216,8 +218,9 @@ export default function RideDetail() { -
{ride.max_height_meters}
-
meters
+
+ +
)} @@ -226,8 +229,10 @@ export default function RideDetail() { -
{Math.round(ride.length_meters)}
-
meters long
+
+ +
+
long
)} @@ -267,7 +272,9 @@ export default function RideDetail() {
⬇️
-
{ride.drop_height_meters}m
+
+ +
drop
@@ -296,7 +303,7 @@ export default function RideDetail() {
{ride.height_requirement && ( -
Minimum height: {ride.height_requirement}cm
+
Minimum height:
)} {ride.age_requirement && (
Minimum age: {ride.age_requirement} years
diff --git a/supabase/config.toml b/supabase/config.toml index 6057bb89..05e015f8 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -1 +1,6 @@ -project_id = "ydvtmnrszybqnbcqbdcy" \ No newline at end of file +project_id = "ydvtmnrszybqnbcqbdcy" + +[functions.detect-location] +verify_jwt = false + +[functions.upload-image] \ No newline at end of file diff --git a/supabase/functions/detect-location/index.ts b/supabase/functions/detect-location/index.ts new file mode 100644 index 00000000..f123d1dd --- /dev/null +++ b/supabase/functions/detect-location/index.ts @@ -0,0 +1,84 @@ +import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +}; + +interface IPLocationResponse { + country: string; + countryCode: string; + measurementSystem: 'metric' | 'imperial'; +} + +serve(async (req) => { + // Handle CORS preflight requests + if (req.method === 'OPTIONS') { + return new Response(null, { headers: corsHeaders }); + } + + try { + // Get the client's IP address + const forwarded = req.headers.get('x-forwarded-for'); + const realIP = req.headers.get('x-real-ip'); + const clientIP = forwarded?.split(',')[0] || realIP || '8.8.8.8'; // fallback to Google DNS for testing + + console.log('Detecting location for IP:', clientIP); + + // Use a free IP geolocation service + const geoResponse = await fetch(`http://ip-api.com/json/${clientIP}?fields=status,country,countryCode`); + + if (!geoResponse.ok) { + throw new Error('Failed to fetch location data'); + } + + const geoData = await geoResponse.json(); + + if (geoData.status !== 'success') { + throw new Error('Invalid location data received'); + } + + // Countries that primarily use imperial system + const imperialCountries = ['US', 'LR', 'MM']; // USA, Liberia, Myanmar + const measurementSystem = imperialCountries.includes(geoData.countryCode) ? 'imperial' : 'metric'; + + const result: IPLocationResponse = { + country: geoData.country, + countryCode: geoData.countryCode, + measurementSystem + }; + + console.log('Location detected:', result); + + return new Response( + JSON.stringify(result), + { + headers: { + ...corsHeaders, + 'Content-Type': 'application/json' + } + } + ); + + } catch (error) { + console.error('Error detecting location:', error); + + // Return default (metric) on error + const defaultResult: IPLocationResponse = { + country: 'Unknown', + countryCode: 'XX', + measurementSystem: 'metric' + }; + + return new Response( + JSON.stringify(defaultResult), + { + headers: { + ...corsHeaders, + 'Content-Type': 'application/json' + }, + status: 200 // Return 200 even on error to provide fallback + } + ); + } +}); \ No newline at end of file diff --git a/supabase/migrations/20250928212339_9f1c7f40-cf4e-4729-8156-d794d5a13922.sql b/supabase/migrations/20250928212339_9f1c7f40-cf4e-4729-8156-d794d5a13922.sql new file mode 100644 index 00000000..f2dd4f90 --- /dev/null +++ b/supabase/migrations/20250928212339_9f1c7f40-cf4e-4729-8156-d794d5a13922.sql @@ -0,0 +1,6 @@ +-- Add unit preferences to user preferences table +ALTER TABLE public.user_preferences +ADD COLUMN unit_preferences JSONB NOT NULL DEFAULT '{"measurement_system": "metric", "temperature": "celsius", "auto_detect": true}'::jsonb; + +-- Add comment for documentation +COMMENT ON COLUMN public.user_preferences.unit_preferences IS 'User preferences for measurement units: measurement_system (metric/imperial), temperature (celsius/fahrenheit), auto_detect (boolean)'; \ No newline at end of file