From f55b0ea2301e8a4645c371219bc27db9509ac20a Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 6 Mar 2026 13:50:27 +0000 Subject: [PATCH] fix: enforce dd/mm/yyyy, 24h time, and locale-aware location search - Replace all 'en-US' and undefined locales with 'en-GB' in date formatting across 15+ frontend files (dateUtils.ts, cards, routes, Luxon calls) to consistently output day-first dates and 24h times - Set hour12: false in all Intl.DateTimeFormat and toLocaleDateString calls that previously used 12h format - Pass user's svelte-i18n locale as &lang= query param from LocationSearchMap and LocationQuickStart to the reverse-geocode API - Extract lang param in reverse_geocode_view and forward to both search_osm and search_google - Add Accept-Language header to Nominatim requests so searches return results in the user's language (e.g. Prague not Praha) - Add languageCode field to Google Places API payload for same effect --- .megamemory/knowledge.db | Bin 135168 -> 135168 bytes .megamemory/knowledge.db-shm | Bin 32768 -> 32768 bytes .megamemory/knowledge.db-wal | Bin 0 -> 206032 bytes AGENTS.md | 0 backend/server/adventures/geocoding.py | 277 ++++++++++++------ .../adventures/views/reverse_geocode_view.py | 88 +++--- .../lib/components/StravaActivityCard.svelte | 5 +- .../lib/components/cards/ChecklistCard.svelte | 4 +- .../components/cards/CollectionCard.svelte | 4 +- .../src/lib/components/cards/NoteCard.svelte | 6 +- .../src/lib/components/cards/TrailCard.svelte | 2 +- .../src/lib/components/cards/UserCard.svelte | 2 +- .../lib/components/cards/WandererCard.svelte | 2 +- .../collections/CollectionStats.svelte | 2 +- .../components/locations/LocationMedia.svelte | 2 +- .../locations/LocationQuickStart.svelte | 8 +- .../locations/LocationVisits.svelte | 10 +- .../shared/LocationSearchMap.svelte | 8 +- frontend/src/lib/dateUtils.ts | 8 +- frontend/src/routes/collections/+page.svelte | 2 +- .../src/routes/collections/[id]/+page.svelte | 2 +- frontend/src/routes/invites/+page.svelte | 2 +- .../src/routes/locations/[id]/+page.svelte | 36 +-- frontend/src/routes/lodging/[id]/+page.svelte | 4 +- .../src/routes/profile/[uuid]/+page.svelte | 12 +- .../routes/transportations/[id]/+page.svelte | 4 +- frontend/src/routes/user/[uuid]/+page.svelte | 2 +- 27 files changed, 302 insertions(+), 190 deletions(-) create mode 100644 AGENTS.md diff --git a/.megamemory/knowledge.db b/.megamemory/knowledge.db index 0749e7562f6c39848de135a63d8ea1d7fe9f48c7..a1a9c9707c636543828871f2f3b1d38eebff0285 100644 GIT binary patch delta 958 zcma))%Wl&^6oxw_YMmywn?Pw0BAKzo0x2OE_W~{|T_7a7X<4Ecp{YGdOdU_KC#gc! zBov7ka8Z^N@d7PfWWh@y-l7Xu6%wF`GB%eINF_EibN(~`8O?W2XHV$t3Hv9@0KHua z?}t$A=;aug7sP^iNd&Q@Lj+&Ny7xpdv7&0~j5d7(O@&lY%ok^~c{Q7#g@vCw9HLKa z>RkF?y)dWE+)VX&)pzGYNLuQSP4FkBiu6F*l$PEgDBY4m2-IzZI4;Asf&9=iUG~VK zl(}#)Ob=d8^eg~# zcl3v+%kQb8Z>;^Y9>y0FXE=Mj-tK;KJklDG=cSssD`N3_>>#Q|N5?-zTAbi|q`h+= zMMqThOhtn5{OS;Vr=Q(Yp45rcR5Zn)dc8_)hM7rixJjLjl0`S6L3GpQikpEB(Rm)~ z6>OIYk88v>OuGyRoXU1#ArGsV=@sIxDmt|+qWdMYZqsF?s$`S~ZQ?S?)*Sknr?|iz z+#pugsTRHp&hbyC3%lRD6}VacI3Ble_`TgUnaC(l9u=Ip)ydcj*f i{)3p8|8n`x-(>%<8zz6J>;z-rccar!_73`B`TTd07ag1c delta 137 zcmZozz|pXPW5a)cM%hgPEE||*c|E7HGg@y}RAA?w{*s4LV0$Dl<7y^mS-!UE>;hm3 z7ryOna*TD1T(bNH4E%ritN2T&a~UuiZ}(JS6y~3}L1ub^KI0T-pzL&Z1E3B;2R;7n m1@??nHVClsY-HfK<}2Wx!kfse$FmV=&0L=C8~-r+3j+XNL?&qf diff --git a/.megamemory/knowledge.db-shm b/.megamemory/knowledge.db-shm index fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10..5257bf6cd0c2dca98d266f6970d6f37cf5670efe 100644 GIT binary patch literal 32768 zcmeI*IZ6ak5CG5`b=;S6AD3|(b=^kyxza_&Ym$1=w=Co-2Z|LVNf=HGK|?(#|yAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+Kue$$Ju#Gh=4N6s*5k7SUhC54UkWrk3Gpg;m$In%fjQ!yVau^9&is$8iA$}y1h Tw5OvM_1KC-AOr{y_#yBPY<4b+ delta 84 zcmZo@U}|V!;+1%$%K!t6lLHy0MJ?DR*i0wi=ERT@fyuzs{|ADJIWn6Q7^nFHiARP2 DvXB$d diff --git a/.megamemory/knowledge.db-wal b/.megamemory/knowledge.db-wal index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..69e8f9bb488f789b872d7e7b11f5949e7f04a4bd 100644 GIT binary patch literal 206032 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 M{z6Cajo~T$KMmq#fdBvi literal 0 HcmV?d00001 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..e69de29b diff --git a/backend/server/adventures/geocoding.py b/backend/server/adventures/geocoding.py index ad2c94c2..47147082 100644 --- a/backend/server/adventures/geocoding.py +++ b/backend/server/adventures/geocoding.py @@ -6,33 +6,37 @@ import unicodedata from worldtravel.models import Region, City, VisitedRegion, VisitedCity from django.conf import settings + # ----------------- # SEARCHING -def search_google(query): +def search_google(query, lang="en"): try: api_key = settings.GOOGLE_MAPS_API_KEY if not api_key: - return {"error": "Geocoding service unavailable. Please check configuration."} + return { + "error": "Geocoding service unavailable. Please check configuration." + } # Updated to use the new Places API (New) endpoint url = "https://places.googleapis.com/v1/places:searchText" - + headers = { - 'Content-Type': 'application/json', - 'X-Goog-Api-Key': api_key, - 'X-Goog-FieldMask': 'places.displayName.text,places.formattedAddress,places.location,places.types,places.rating,places.userRatingCount' + "Content-Type": "application/json", + "X-Goog-Api-Key": api_key, + "X-Goog-FieldMask": "places.displayName.text,places.formattedAddress,places.location,places.types,places.rating,places.userRatingCount", } - + payload = { "textQuery": query, - "maxResultCount": 20 # Adjust as needed + "maxResultCount": 20, # Adjust as needed + "languageCode": lang, } - + response = requests.post(url, json=payload, headers=headers, timeout=(2, 5)) response.raise_for_status() data = response.json() - + # Check if we have places in the response places = data.get("places", []) if not places: @@ -56,42 +60,62 @@ def search_google(query): display_name_obj = place.get("displayName", {}) name = display_name_obj.get("text") if display_name_obj else None - results.append({ - "lat": location.get("latitude"), - "lon": location.get("longitude"), - "name": name, - "display_name": place.get("formattedAddress"), - "type": primary_type, - "category": category, - "importance": importance, - "addresstype": addresstype, - "powered_by": "google", - }) + results.append( + { + "lat": location.get("latitude"), + "lon": location.get("longitude"), + "name": name, + "display_name": place.get("formattedAddress"), + "type": primary_type, + "category": category, + "importance": importance, + "addresstype": addresstype, + "powered_by": "google", + } + ) if results: - results.sort(key=lambda r: r["importance"] if r["importance"] is not None else 0, reverse=True) + results.sort( + key=lambda r: r["importance"] if r["importance"] is not None else 0, + reverse=True, + ) return results except requests.exceptions.Timeout: - return {"error": "Request timed out while contacting Google Maps. Please try again."} + return { + "error": "Request timed out while contacting Google Maps. Please try again." + } except requests.exceptions.ConnectionError: - return {"error": "Unable to connect to Google Maps service. Please check your internet connection."} + return { + "error": "Unable to connect to Google Maps service. Please check your internet connection." + } except requests.exceptions.HTTPError as e: if response.status_code == 400: return {"error": "Invalid request to Google Maps. Please check your query."} elif response.status_code == 401: - return {"error": "Authentication failed with Google Maps. Please check API configuration."} + return { + "error": "Authentication failed with Google Maps. Please check API configuration." + } elif response.status_code == 403: - return {"error": "Access forbidden to Google Maps. Please check API permissions."} + return { + "error": "Access forbidden to Google Maps. Please check API permissions." + } elif response.status_code == 429: - return {"error": "Too many requests to Google Maps. Please try again later."} + return { + "error": "Too many requests to Google Maps. Please try again later." + } else: return {"error": "Google Maps service error. Please try again later."} except requests.exceptions.RequestException: - return {"error": "Network error while contacting Google Maps. Please try again."} + return { + "error": "Network error while contacting Google Maps. Please try again." + } except Exception: - return {"error": "An unexpected error occurred during Google search. Please try again."} + return { + "error": "An unexpected error occurred during Google search. Please try again." + } + def _extract_google_category(types): # Basic category inference based on common place types @@ -126,56 +150,74 @@ def _infer_addresstype(type_): return mapping.get(type_, None) -def search_osm(query): +def search_osm(query, lang="en"): try: url = f"https://nominatim.openstreetmap.org/search?q={query}&format=jsonv2" - headers = {'User-Agent': 'Voyage Server'} + headers = {"User-Agent": "Voyage Server", "Accept-Language": lang} response = requests.get(url, headers=headers, timeout=(2, 5)) response.raise_for_status() data = response.json() - return [{ - "lat": item.get("lat"), - "lon": item.get("lon"), - "name": item.get("name"), - "display_name": item.get("display_name"), - "type": item.get("type"), - "category": item.get("category"), - "importance": item.get("importance"), - "addresstype": item.get("addresstype"), - "powered_by": "nominatim", - } for item in data] + return [ + { + "lat": item.get("lat"), + "lon": item.get("lon"), + "name": item.get("name"), + "display_name": item.get("display_name"), + "type": item.get("type"), + "category": item.get("category"), + "importance": item.get("importance"), + "addresstype": item.get("addresstype"), + "powered_by": "nominatim", + } + for item in data + ] except requests.exceptions.Timeout: - return {"error": "Request timed out while contacting OpenStreetMap. Please try again."} + return { + "error": "Request timed out while contacting OpenStreetMap. Please try again." + } except requests.exceptions.ConnectionError: - return {"error": "Unable to connect to OpenStreetMap service. Please check your internet connection."} + return { + "error": "Unable to connect to OpenStreetMap service. Please check your internet connection." + } except requests.exceptions.HTTPError as e: if response.status_code == 400: - return {"error": "Invalid request to OpenStreetMap. Please check your query."} + return { + "error": "Invalid request to OpenStreetMap. Please check your query." + } elif response.status_code == 429: - return {"error": "Too many requests to OpenStreetMap. Please try again later."} + return { + "error": "Too many requests to OpenStreetMap. Please try again later." + } else: return {"error": "OpenStreetMap service error. Please try again later."} except requests.exceptions.RequestException: - return {"error": "Network error while contacting OpenStreetMap. Please try again."} + return { + "error": "Network error while contacting OpenStreetMap. Please try again." + } except Exception: - return {"error": "An unexpected error occurred during OpenStreetMap search. Please try again."} + return { + "error": "An unexpected error occurred during OpenStreetMap search. Please try again." + } + def search(query): """ Unified search function that tries Google Maps first, then falls back to OpenStreetMap. """ - if getattr(settings, 'GOOGLE_MAPS_API_KEY', None): + if getattr(settings, "GOOGLE_MAPS_API_KEY", None): google_result = search_google(query) if "error" not in google_result: return google_result # If Google fails, fallback to OSM return search_osm(query) + # ----------------- # REVERSE GEOCODING # ----------------- + def extractIsoCode(user, data): """ Extract the ISO code from the response data. @@ -188,10 +230,10 @@ def extractIsoCode(user, data): visited_city = None location_name = None - if 'name' in data.keys(): - location_name = data['name'] + if "name" in data.keys(): + location_name = data["name"] - address = data.get('address', {}) or {} + address = data.get("address", {}) or {} # Capture country code early for ISO selection and name fallback. country_code = address.get("ISO3166-1") @@ -275,17 +317,17 @@ def extractIsoCode(user, data): # ordered preference for best-effort locality matching locality_keys = [ - 'suburb', - 'neighbourhood', - 'neighborhood', # alternate spelling - 'city', - 'city_district', - 'town', - 'village', - 'hamlet', - 'locality', - 'municipality', - 'county', + "suburb", + "neighbourhood", + "neighborhood", # alternate spelling + "city", + "city_district", + "town", + "village", + "hamlet", + "locality", + "municipality", + "county", ] def _normalize_name(value): @@ -305,13 +347,13 @@ def extractIsoCode(user, data): return exact_match normalized_value = _normalize_name(value) - for candidate in qs.values_list('id', 'name'): + for candidate in qs.values_list("id", "name"): candidate_id, candidate_name = candidate if _normalize_name(candidate_name) == normalized_value: return qs.filter(id=candidate_id).first() # Allow partial matching for most locality fields but keep county strict. - if key_name == 'county': + if key_name == "county": return None return qs.filter(name__icontains=value).first() @@ -333,7 +375,9 @@ def extractIsoCode(user, data): region_visited = bool(visited_region) if city: - display_name = f"{city.name}, {region.name}, {country_code or region.country.country_code}" + display_name = ( + f"{city.name}, {region.name}, {country_code or region.country.country_code}" + ) visited_city = VisitedCity.objects.filter(city=city, user=user).first() city_visited = bool(visited_city) else: @@ -349,9 +393,10 @@ def extractIsoCode(user, data): "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited, - 'location_name': location_name, + "location_name": location_name, } + def is_host_resolvable(hostname: str) -> bool: try: socket.gethostbyname(hostname) @@ -359,8 +404,9 @@ def is_host_resolvable(hostname: str) -> bool: except socket.error: return False + def reverse_geocode(lat, lon, user): - if getattr(settings, 'GOOGLE_MAPS_API_KEY', None): + if getattr(settings, "GOOGLE_MAPS_API_KEY", None): google_result = reverse_geocode_google(lat, lon, user) if "error" not in google_result: return google_result @@ -368,39 +414,59 @@ def reverse_geocode(lat, lon, user): return reverse_geocode_osm(lat, lon, user) return reverse_geocode_osm(lat, lon, user) + def reverse_geocode_osm(lat, lon, user): - url = f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}" - headers = {'User-Agent': 'Voyage Server'} + url = ( + f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}" + ) + headers = {"User-Agent": "Voyage Server"} connect_timeout = 1 read_timeout = 5 if not is_host_resolvable("nominatim.openstreetmap.org"): - return {"error": "Unable to resolve OpenStreetMap service. Please check your internet connection."} + return { + "error": "Unable to resolve OpenStreetMap service. Please check your internet connection." + } try: - response = requests.get(url, headers=headers, timeout=(connect_timeout, read_timeout)) + response = requests.get( + url, headers=headers, timeout=(connect_timeout, read_timeout) + ) response.raise_for_status() data = response.json() return extractIsoCode(user, data) except requests.exceptions.Timeout: - return {"error": "Request timed out while contacting OpenStreetMap. Please try again."} + return { + "error": "Request timed out while contacting OpenStreetMap. Please try again." + } except requests.exceptions.ConnectionError: - return {"error": "Unable to connect to OpenStreetMap service. Please check your internet connection."} + return { + "error": "Unable to connect to OpenStreetMap service. Please check your internet connection." + } except requests.exceptions.HTTPError as e: if response.status_code == 400: - return {"error": "Invalid request to OpenStreetMap. Please check coordinates."} + return { + "error": "Invalid request to OpenStreetMap. Please check coordinates." + } elif response.status_code == 429: - return {"error": "Too many requests to OpenStreetMap. Please try again later."} + return { + "error": "Too many requests to OpenStreetMap. Please try again later." + } else: return {"error": "OpenStreetMap service error. Please try again later."} except requests.exceptions.RequestException: - return {"error": "Network error while contacting OpenStreetMap. Please try again."} + return { + "error": "Network error while contacting OpenStreetMap. Please try again." + } except Exception: - return {"error": "An unexpected error occurred during OpenStreetMap geocoding. Please try again."} + return { + "error": "An unexpected error occurred during OpenStreetMap geocoding. Please try again." + } + def reverse_geocode_google(lat, lon, user): api_key = settings.GOOGLE_MAPS_API_KEY - + # Updated to use the new Geocoding API endpoint (this one is still supported) # The Geocoding API is separate from Places API and still uses the old format url = "https://maps.googleapis.com/maps/api/geocode/json" @@ -416,11 +482,17 @@ def reverse_geocode_google(lat, lon, user): if status == "ZERO_RESULTS": return {"error": "No location found for the given coordinates."} elif status == "OVER_QUERY_LIMIT": - return {"error": "Query limit exceeded for Google Maps. Please try again later."} + return { + "error": "Query limit exceeded for Google Maps. Please try again later." + } elif status == "REQUEST_DENIED": - return {"error": "Request denied by Google Maps. Please check API configuration."} + return { + "error": "Request denied by Google Maps. Please check API configuration." + } elif status == "INVALID_REQUEST": - return {"error": "Invalid request to Google Maps. Please check coordinates."} + return { + "error": "Invalid request to Google Maps. Please check coordinates." + } else: return {"error": "Geocoding failed. Please try again."} @@ -428,28 +500,47 @@ def reverse_geocode_google(lat, lon, user): first_result = data.get("results", [])[0] result_data = { "name": first_result.get("formatted_address"), - "address": _parse_google_address_components(first_result.get("address_components", [])) + "address": _parse_google_address_components( + first_result.get("address_components", []) + ), } return extractIsoCode(user, result_data) except requests.exceptions.Timeout: - return {"error": "Request timed out while contacting Google Maps. Please try again."} + return { + "error": "Request timed out while contacting Google Maps. Please try again." + } except requests.exceptions.ConnectionError: - return {"error": "Unable to connect to Google Maps service. Please check your internet connection."} + return { + "error": "Unable to connect to Google Maps service. Please check your internet connection." + } except requests.exceptions.HTTPError as e: if response.status_code == 400: - return {"error": "Invalid request to Google Maps. Please check coordinates."} + return { + "error": "Invalid request to Google Maps. Please check coordinates." + } elif response.status_code == 401: - return {"error": "Authentication failed with Google Maps. Please check API configuration."} + return { + "error": "Authentication failed with Google Maps. Please check API configuration." + } elif response.status_code == 403: - return {"error": "Access forbidden to Google Maps. Please check API permissions."} + return { + "error": "Access forbidden to Google Maps. Please check API permissions." + } elif response.status_code == 429: - return {"error": "Too many requests to Google Maps. Please try again later."} + return { + "error": "Too many requests to Google Maps. Please try again later." + } else: return {"error": "Google Maps service error. Please try again later."} except requests.exceptions.RequestException: - return {"error": "Network error while contacting Google Maps. Please try again."} + return { + "error": "Network error while contacting Google Maps. Please try again." + } except Exception: - return {"error": "An unexpected error occurred during Google geocoding. Please try again."} + return { + "error": "An unexpected error occurred during Google geocoding. Please try again." + } + def _parse_google_address_components(components): parsed = {} @@ -476,7 +567,9 @@ def _parse_google_address_components(components): parsed["city"] = long_name if "postal_town" in types: parsed.setdefault("city", long_name) - if "sublocality" in types or any(t.startswith("sublocality_level_") for t in types): + if "sublocality" in types or any( + t.startswith("sublocality_level_") for t in types + ): parsed["suburb"] = long_name if "neighborhood" in types: parsed["neighbourhood"] = long_name diff --git a/backend/server/adventures/views/reverse_geocode_view.py b/backend/server/adventures/views/reverse_geocode_view.py index b0635300..43e68149 100644 --- a/backend/server/adventures/views/reverse_geocode_view.py +++ b/backend/server/adventures/views/reverse_geocode_view.py @@ -9,41 +9,51 @@ from adventures.geocoding import reverse_geocode from django.conf import settings from adventures.geocoding import search_google, search_osm + class ReverseGeocodeViewSet(viewsets.ViewSet): permission_classes = [IsAuthenticated] - @action(detail=False, methods=['get']) + @action(detail=False, methods=["get"]) def reverse_geocode(self, request): - lat = request.query_params.get('lat', '') - lon = request.query_params.get('lon', '') + lat = request.query_params.get("lat", "") + lon = request.query_params.get("lon", "") if not lat or not lon: - return Response({"error": "Latitude and longitude are required"}, status=400) + return Response( + {"error": "Latitude and longitude are required"}, status=400 + ) try: lat = float(lat) lon = float(lon) except ValueError: return Response({"error": "Invalid latitude or longitude"}, status=400) data = reverse_geocode(lat, lon, self.request.user) - if 'error' in data: - return Response({"error": "An internal error occurred while processing the request"}, status=400) + if "error" in data: + return Response( + {"error": "An internal error occurred while processing the request"}, + status=400, + ) return Response(data) - - @action(detail=False, methods=['get']) + + @action(detail=False, methods=["get"]) def search(self, request): - query = request.query_params.get('query', '') + query = request.query_params.get("query", "") + lang = request.query_params.get("lang", "en") if not query: return Response({"error": "Query parameter is required"}, status=400) try: - if getattr(settings, 'GOOGLE_MAPS_API_KEY', None): - results = search_google(query) + if getattr(settings, "GOOGLE_MAPS_API_KEY", None): + results = search_google(query, lang=lang) else: - results = search_osm(query) + results = search_osm(query, lang=lang) return Response(results) except Exception: - return Response({"error": "An internal error occurred while processing the request"}, status=500) + return Response( + {"error": "An internal error occurred while processing the request"}, + status=500, + ) - @action(detail=False, methods=['post']) + @action(detail=False, methods=["post"]) def mark_visited_region(self, request): """ Marks regions and cities as visited based on user's visited locations. @@ -53,37 +63,36 @@ class ReverseGeocodeViewSet(viewsets.ViewSet): new_regions = {} new_city_count = 0 new_cities = {} - + # Get all visited locations with their region and city data visited_locations = Location.objects.filter( user=self.request.user - ).select_related('region', 'city') - + ).select_related("region", "city") + # Track unique regions and cities to create VisitedRegion/VisitedCity entries regions_to_mark = set() cities_to_mark = set() - + for location in visited_locations: # Only process locations that are marked as visited if not location.is_visited_status(): continue - + # Collect regions if location.region: regions_to_mark.add(location.region.id) - + # Collect cities if location.city: cities_to_mark.add(location.city.id) - + # Get existing visited regions for this user existing_visited_regions = set( VisitedRegion.objects.filter( - user=self.request.user, - region_id__in=regions_to_mark - ).values_list('region_id', flat=True) + user=self.request.user, region_id__in=regions_to_mark + ).values_list("region_id", flat=True) ) - + # Create new VisitedRegion entries new_visited_regions = [] for region_id in regions_to_mark: @@ -91,7 +100,7 @@ class ReverseGeocodeViewSet(viewsets.ViewSet): new_visited_regions.append( VisitedRegion(region_id=region_id, user=self.request.user) ) - + if new_visited_regions: VisitedRegion.objects.bulk_create(new_visited_regions) new_region_count = len(new_visited_regions) @@ -100,15 +109,14 @@ class ReverseGeocodeViewSet(viewsets.ViewSet): id__in=[vr.region_id for vr in new_visited_regions] ) new_regions = {r.id: r.name for r in regions} - + # Get existing visited cities for this user existing_visited_cities = set( VisitedCity.objects.filter( - user=self.request.user, - city_id__in=cities_to_mark - ).values_list('city_id', flat=True) + user=self.request.user, city_id__in=cities_to_mark + ).values_list("city_id", flat=True) ) - + # Create new VisitedCity entries new_visited_cities = [] for city_id in cities_to_mark: @@ -116,7 +124,7 @@ class ReverseGeocodeViewSet(viewsets.ViewSet): new_visited_cities.append( VisitedCity(city_id=city_id, user=self.request.user) ) - + if new_visited_cities: VisitedCity.objects.bulk_create(new_visited_cities) new_city_count = len(new_visited_cities) @@ -125,10 +133,12 @@ class ReverseGeocodeViewSet(viewsets.ViewSet): id__in=[vc.city_id for vc in new_visited_cities] ) new_cities = {c.id: c.name for c in cities} - - return Response({ - "new_regions": new_region_count, - "regions": new_regions, - "new_cities": new_city_count, - "cities": new_cities - }) \ No newline at end of file + + return Response( + { + "new_regions": new_region_count, + "regions": new_regions, + "new_cities": new_city_count, + "cities": new_cities, + } + ) diff --git a/frontend/src/lib/components/StravaActivityCard.svelte b/frontend/src/lib/components/StravaActivityCard.svelte index 8598d8ff..5d2a4b63 100644 --- a/frontend/src/lib/components/StravaActivityCard.svelte +++ b/frontend/src/lib/components/StravaActivityCard.svelte @@ -42,12 +42,13 @@ } function formatDate(dateString: string): string { - return new Date(dateString).toLocaleDateString('en-US', { + return new Date(dateString).toLocaleDateString('en-GB', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', - minute: '2-digit' + minute: '2-digit', + hour12: false }); } diff --git a/frontend/src/lib/components/cards/ChecklistCard.svelte b/frontend/src/lib/components/cards/ChecklistCard.svelte index aef3c290..d575e803 100644 --- a/frontend/src/lib/components/cards/ChecklistCard.svelte +++ b/frontend/src/lib/components/cards/ChecklistCard.svelte @@ -178,7 +178,7 @@

- {new Date(checklist.date).toLocaleDateString(undefined, { timeZone: 'UTC' })} + {new Date(checklist.date).toLocaleDateString('en-GB', { timeZone: 'UTC' })}
{/if} @@ -440,7 +440,7 @@ {#if checklist.date && checklist.date !== ''}
- {new Date(checklist.date).toLocaleDateString(undefined, { timeZone: 'UTC' })} + {new Date(checklist.date).toLocaleDateString('en-GB', { timeZone: 'UTC' })}
{/if} diff --git a/frontend/src/lib/components/cards/CollectionCard.svelte b/frontend/src/lib/components/cards/CollectionCard.svelte index 4b59ce96..af327f96 100644 --- a/frontend/src/lib/components/cards/CollectionCard.svelte +++ b/frontend/src/lib/components/cards/CollectionCard.svelte @@ -285,12 +285,12 @@ {#if collection.start_date && collection.end_date}
- {new Date(collection.start_date).toLocaleDateString(undefined, { + {new Date(collection.start_date).toLocaleDateString('en-GB', { timeZone: 'UTC', month: 'short', day: 'numeric', year: 'numeric' - })} – {new Date(collection.end_date).toLocaleDateString(undefined, { + })} – {new Date(collection.end_date).toLocaleDateString('en-GB', { timeZone: 'UTC', month: 'short', day: 'numeric', diff --git a/frontend/src/lib/components/cards/NoteCard.svelte b/frontend/src/lib/components/cards/NoteCard.svelte index 7f13c362..45e57d3c 100644 --- a/frontend/src/lib/components/cards/NoteCard.svelte +++ b/frontend/src/lib/components/cards/NoteCard.svelte @@ -120,8 +120,8 @@ {#if note.date && note.date !== ''}
- {new Date(note.date).toLocaleDateString(undefined, { timeZone: 'UTC' })} + {new Date(note.date).toLocaleDateString('en-GB', { timeZone: 'UTC' })}
{/if} {#if note.links && note.links?.length > 0} @@ -319,7 +319,7 @@ {#if note.date && note.date !== ''}
- {new Date(note.date).toLocaleDateString(undefined, { timeZone: 'UTC' })} + {new Date(note.date).toLocaleDateString('en-GB', { timeZone: 'UTC' })}
{/if} diff --git a/frontend/src/lib/components/cards/TrailCard.svelte b/frontend/src/lib/components/cards/TrailCard.svelte index 294ea9fc..30cde956 100644 --- a/frontend/src/lib/components/cards/TrailCard.svelte +++ b/frontend/src/lib/components/cards/TrailCard.svelte @@ -32,7 +32,7 @@ } function formatDate(date: string | number | Date) { - return new Date(date).toLocaleDateString(); + return new Date(date).toLocaleDateString('en-GB'); } diff --git a/frontend/src/lib/components/cards/UserCard.svelte b/frontend/src/lib/components/cards/UserCard.svelte index a4f63afe..366841ba 100644 --- a/frontend/src/lib/components/cards/UserCard.svelte +++ b/frontend/src/lib/components/cards/UserCard.svelte @@ -83,7 +83,7 @@ {user.date_joined - ? `${$t('adventures.joined')} ` + new Date(user.date_joined).toLocaleDateString() + ? `${$t('adventures.joined')} ` + new Date(user.date_joined).toLocaleDateString('en-GB') : ''}
diff --git a/frontend/src/lib/components/cards/WandererCard.svelte b/frontend/src/lib/components/cards/WandererCard.svelte index a4067627..0c1be136 100644 --- a/frontend/src/lib/components/cards/WandererCard.svelte +++ b/frontend/src/lib/components/cards/WandererCard.svelte @@ -50,7 +50,7 @@ */ function formatDate(dateString: string | number | Date) { if (!dateString) return ''; - return new Date(dateString).toLocaleDateString(); + return new Date(dateString).toLocaleDateString('en-GB'); } /** diff --git a/frontend/src/lib/components/collections/CollectionStats.svelte b/frontend/src/lib/components/collections/CollectionStats.svelte index 7337a158..951dd998 100644 --- a/frontend/src/lib/components/collections/CollectionStats.svelte +++ b/frontend/src/lib/components/collections/CollectionStats.svelte @@ -264,7 +264,7 @@ $: windowLabel = tripStart && tripEnd - ? `${tripStart.toLocaleString(DateTime.DATE_MED)} - ${tripEnd.toLocaleString(DateTime.DATE_MED)}` + ? `${tripStart.toLocaleString(DateTime.DATE_MED, { locale: 'en-GB' })} - ${tripEnd.toLocaleString(DateTime.DATE_MED, { locale: 'en-GB' })}` : null; function normalizeTransportType(type?: string | null): string { diff --git a/frontend/src/lib/components/locations/LocationMedia.svelte b/frontend/src/lib/components/locations/LocationMedia.svelte index ce7346d6..c86aa65d 100644 --- a/frontend/src/lib/components/locations/LocationMedia.svelte +++ b/frontend/src/lib/components/locations/LocationMedia.svelte @@ -152,7 +152,7 @@ } function formatDate(dateString: string | number | Date) { - return new Date(dateString).toLocaleDateString(); + return new Date(dateString).toLocaleDateString('en-GB'); } async function fetchWandererTrails(filter = '') { diff --git a/frontend/src/lib/components/locations/LocationQuickStart.svelte b/frontend/src/lib/components/locations/LocationQuickStart.svelte index ac11484e..776d263a 100644 --- a/frontend/src/lib/components/locations/LocationQuickStart.svelte +++ b/frontend/src/lib/components/locations/LocationQuickStart.svelte @@ -1,7 +1,7 @@ diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 8cfd8257..4c9d7563 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -684,7 +684,7 @@ function formatDate(dateString: string | null) { if (!dateString) return ''; - return DateTime.fromISO(dateString).toLocaleString(DateTime.DATE_MED); + return DateTime.fromISO(dateString).toLocaleString(DateTime.DATE_MED, { locale: 'en-GB' }); } function collaboratorDisplayName(person: Collaborator | null | undefined): string { diff --git a/frontend/src/routes/invites/+page.svelte b/frontend/src/routes/invites/+page.svelte index dec0cbf6..3eddc6fe 100644 --- a/frontend/src/routes/invites/+page.svelte +++ b/frontend/src/routes/invites/+page.svelte @@ -79,7 +79,7 @@ } function formatDate(dateString: string): string { - return new Date(dateString).toLocaleDateString(); + return new Date(dateString).toLocaleDateString('en-GB'); } onMount(() => { diff --git a/frontend/src/routes/locations/[id]/+page.svelte b/frontend/src/routes/locations/[id]/+page.svelte index 150dcd40..fb35754f 100644 --- a/frontend/src/routes/locations/[id]/+page.svelte +++ b/frontend/src/routes/locations/[id]/+page.svelte @@ -528,22 +528,24 @@
{#if visit.timezone} {$t('adventures.start')}: - {DateTime.fromISO(visit.start_date, { zone: 'utc' }) - .setZone(visit.timezone) - .toLocaleString(DateTime.DATETIME_MED)}
- {$t('adventures.end')}: - {DateTime.fromISO(visit.end_date, { zone: 'utc' }) - .setZone(visit.timezone) - .toLocaleString(DateTime.DATETIME_MED)} - {:else} - Start: - {DateTime.fromISO(visit.start_date).toLocaleString( - DateTime.DATETIME_MED - )}
- End: - {DateTime.fromISO(visit.end_date).toLocaleString( - DateTime.DATETIME_MED - )} + {DateTime.fromISO(visit.start_date, { zone: 'utc' }) + .setZone(visit.timezone) + .toLocaleString(DateTime.DATETIME_MED, { locale: 'en-GB' })}
+ {$t('adventures.end')}: + {DateTime.fromISO(visit.end_date, { zone: 'utc' }) + .setZone(visit.timezone) + .toLocaleString(DateTime.DATETIME_MED, { locale: 'en-GB' })} + {:else} + Start: + {DateTime.fromISO(visit.start_date).toLocaleString( + DateTime.DATETIME_MED, + { locale: 'en-GB' } + )}
+ End: + {DateTime.fromISO(visit.end_date).toLocaleString( + DateTime.DATETIME_MED, + { locale: 'en-GB' } + )} {/if}
@@ -900,7 +902,7 @@ {#each adventure.sun_times as sun_time}
- {new Date(sun_time.date).toLocaleDateString()} + {new Date(sun_time.date).toLocaleDateString('en-GB')}
{$t('adventures.sunrise')}: {sun_time.sunrise} • {$t('adventures.sunset')}: {sun_time.sunset} diff --git a/frontend/src/routes/lodging/[id]/+page.svelte b/frontend/src/routes/lodging/[id]/+page.svelte index bcfc6faf..54fe03a8 100644 --- a/frontend/src/routes/lodging/[id]/+page.svelte +++ b/frontend/src/routes/lodging/[id]/+page.svelte @@ -101,7 +101,9 @@ if (!dateStr || isAllDay(dateStr)) return null; const dt = DateTime.fromISO(dateStr, { zone: timezone ?? 'UTC' }); if (!dt.isValid) return null; - return dt.setZone(localTimeZone).toLocaleString(DateTime.DATETIME_MED); + return dt.setZone(localTimeZone).toLocaleString(DateTime.DATETIME_MED, { + locale: 'en-GB' + }); }; const inLocal = formatLocal(checkIn); diff --git a/frontend/src/routes/profile/[uuid]/+page.svelte b/frontend/src/routes/profile/[uuid]/+page.svelte index f678a8e4..6ae42243 100644 --- a/frontend/src/routes/profile/[uuid]/+page.svelte +++ b/frontend/src/routes/profile/[uuid]/+page.svelte @@ -333,12 +333,12 @@
- {$t('profile.member_since')} - {new Date(user.date_joined).toLocaleDateString(undefined, { - timeZone: 'UTC', - year: 'numeric', - month: 'long' - })} + {$t('profile.member_since')} + {new Date(user.date_joined).toLocaleDateString('en-GB', { + timeZone: 'UTC', + year: 'numeric', + month: 'long' + })}
{/if} diff --git a/frontend/src/routes/transportations/[id]/+page.svelte b/frontend/src/routes/transportations/[id]/+page.svelte index 0e42fa62..dad0560c 100644 --- a/frontend/src/routes/transportations/[id]/+page.svelte +++ b/frontend/src/routes/transportations/[id]/+page.svelte @@ -263,7 +263,9 @@ if (!dateStr || isAllDay(dateStr)) return null; const dt = DateTime.fromISO(dateStr, { zone: zone ?? 'UTC' }); if (!dt.isValid) return null; - return dt.setZone(localTimeZone).toLocaleString(DateTime.DATETIME_MED); + return dt.setZone(localTimeZone).toLocaleString(DateTime.DATETIME_MED, { + locale: 'en-GB' + }); }; const startLocal = formatLocal(start, startTimezone); diff --git a/frontend/src/routes/user/[uuid]/+page.svelte b/frontend/src/routes/user/[uuid]/+page.svelte index ddb96bd3..832248ab 100644 --- a/frontend/src/routes/user/[uuid]/+page.svelte +++ b/frontend/src/routes/user/[uuid]/+page.svelte @@ -25,7 +25,7 @@

- {user.date_joined ? 'Joined ' + new Date(user.date_joined).toLocaleDateString() : ''} + {user.date_joined ? 'Joined ' + new Date(user.date_joined).toLocaleDateString('en-GB') : ''}