diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8a923ae --- /dev/null +++ b/.env.example @@ -0,0 +1,47 @@ +# 🌐 Frontend +PUBLIC_SERVER_URL=http://server:8000 # PLEASE DON'T CHANGE :) - Should be the service name of the backend with port 8000, even if you change the port in the backend service. Only change if you are using a custom more complex setup. +ORIGIN=http://localhost:8015 +BODY_SIZE_LIMIT=Infinity +FRONTEND_PORT=8015 + +# 🐘 PostgreSQL Database +PGHOST=db +POSTGRES_DB=database +POSTGRES_USER=adventure +POSTGRES_PASSWORD=changeme123 + +# 🔒 Django Backend +SECRET_KEY=changeme123 +DJANGO_ADMIN_USERNAME=admin +DJANGO_ADMIN_PASSWORD=admin +DJANGO_ADMIN_EMAIL=admin@example.com +PUBLIC_URL=http://localhost:8016 # Match the outward port, used for the creation of image urls +CSRF_TRUSTED_ORIGINS=http://localhost:8016,http://localhost:8015 +DEBUG=False +FRONTEND_URL=http://localhost:8015 # Used for email generation. This should be the url of the frontend +BACKEND_PORT=8016 + +# Optional: use Google Maps integration +# https://adventurelog.app/docs/configuration/google_maps_integration.html +# GOOGLE_MAPS_API_KEY=your_google_maps_api_key + +# Optional: disable registration +# https://adventurelog.app/docs/configuration/disable_registration.html +DISABLE_REGISTRATION=False +# DISABLE_REGISTRATION_MESSAGE=Registration is disabled for this instance of AdventureLog. + +# Optional: Use email +# https://adventurelog.app/docs/configuration/email.html +# EMAIL_BACKEND=email +# EMAIL_HOST=smtp.gmail.com +# EMAIL_USE_TLS=True +# EMAIL_PORT=587 +# EMAIL_USE_SSL=False +# EMAIL_HOST_USER=user +# EMAIL_HOST_PASSWORD=password +# DEFAULT_FROM_EMAIL=user@example.com + +# Optional: Use Umami for analytics +# https://adventurelog.app/docs/configuration/analytics.html +# PUBLIC_UMAMI_SRC=https://cloud.umami.is/script.js # If you are using the hosted version of Umami +# PUBLIC_UMAMI_WEBSITE_ID= \ No newline at end of file diff --git a/.github/.docker-compose-database.yml b/.github/.docker-compose-database.yml new file mode 100644 index 0000000..aa187bd --- /dev/null +++ b/.github/.docker-compose-database.yml @@ -0,0 +1,16 @@ +services: + db: + image: postgis/postgis:15-3.3 + container_name: adventurelog-db + restart: unless-stopped + ports: + - "127.0.0.1:5432:5432" + environment: + POSTGRES_DB: database + POSTGRES_USER: adventure + POSTGRES_PASSWORD: changeme123 + volumes: + - postgres_data:/var/lib/postgresql/data/ + +volumes: + postgres_data: diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 569c95d..80c427d 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +github: seanmorley15 buy_me_a_coffee: seanmorley15 diff --git a/.github/workflows/backend-test.yml b/.github/workflows/backend-test.yml new file mode 100644 index 0000000..0ce9d7c --- /dev/null +++ b/.github/workflows/backend-test.yml @@ -0,0 +1,64 @@ +name: Test Backend + +permissions: + contents: read + +on: + pull_request: + paths: + - 'backend/server/**' + - '.github/workflows/backend-test.yml' + push: + paths: + - 'backend/server/**' + - '.github/workflows/backend-test.yml' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: set up python 3.12 + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: install dependencies + run: | + sudo apt update -q + sudo apt install -y -q \ + python3-gdal + + - name: start database + run: | + docker compose -f .github/.docker-compose-database.yml up -d + + - name: install python libreries + working-directory: backend/server + run: | + pip install -r requirements.txt + + - name: run server + working-directory: backend/server + env: + PGHOST: "127.0.0.1" + PGDATABASE: "database" + PGUSER: "adventure" + PGPASSWORD: "changeme123" + SECRET_KEY: "changeme123" + DJANGO_ADMIN_USERNAME: "admin" + DJANGO_ADMIN_PASSWORD: "admin" + DJANGO_ADMIN_EMAIL: "admin@example.com" + PUBLIC_URL: "http://localhost:8000" + CSRF_TRUSTED_ORIGINS: "http://localhost:5173,http://localhost:8000" + DEBUG: "True" + FRONTEND_URL: "http://localhost:5173" + run: | + python manage.py migrate + python manage.py runserver & + + - name: wait for backend to boot + run: > + curl -fisS --retry 60 --retry-delay 1 --retry-all-errors + http://localhost:8000/ diff --git a/.github/workflows/frontend-test.yml b/.github/workflows/frontend-test.yml new file mode 100644 index 0000000..73d9b24 --- /dev/null +++ b/.github/workflows/frontend-test.yml @@ -0,0 +1,32 @@ +name: Test Frontend + +permissions: + contents: read + +on: + pull_request: + paths: + - "frontend/**" + - ".github/workflows/frontend-test.yml" + push: + paths: + - "frontend/**" + - ".github/workflows/frontend-test.yml" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: install dependencies + working-directory: frontend + run: npm i + + - name: build frontend + working-directory: frontend + run: npm run build diff --git a/.gitignore b/.gitignore index 314a123..090b681 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Ignore everything in the .venv folder .venv/ .vscode/settings.json -.pnpm-store/ \ No newline at end of file +.pnpm-store/ +.env diff --git a/.vscode/settings.json b/.vscode/settings.json index 11f6a2d..80fe952 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,5 +25,16 @@ "backend/server/backend/lib/python3.12/site-packages/django/contrib/sites/locale", "backend/server/backend/lib/python3.12/site-packages/rest_framework/templates/rest_framework/docs/langs" ], - "i18n-ally.keystyle": "nested" + "i18n-ally.keystyle": "nested", + "i18n-ally.keysInUse": [ + "navbar.themes.dim", + "navbar.themes.northernLights", + "navbar.themes.aqua", + "navbar.themes.aestheticDark", + "navbar.themes.aestheticLight", + "navbar.themes.forest", + "navbar.themes.night", + "navbar.themes.dark", + "navbar.themes.light" + ] } diff --git a/README.md b/README.md index 3b48223..73aa31b 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ Hi! I'm Sean, the creator of AdventureLog. I'm a college student and software de ## 💎 Acknowledgements -- Logo Design by [nordtechtiger](https://github.com/nordtechtiger) +- Logo Design by [nordtektiger](https://github.com/nordtektiger) - WorldTravel Dataset [dr5hn/countries-states-cities-database](https://github.com/dr5hn/countries-states-cities-database) ### Top Supporters 💖 diff --git a/backend/Dockerfile b/backend/Dockerfile index aa0f9f4..b3f41b7 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,18 +1,30 @@ # Use the official Python slim image as the base image -FROM python:3.10-slim +FROM python:3.13-slim -LABEL Developers="Sean Morley" +# Metadata labels for the AdventureLog image +LABEL maintainer="Sean Morley" \ + version="v0.10.0" \ + description="AdventureLog — the ultimate self-hosted travel companion." \ + org.opencontainers.image.title="AdventureLog" \ + org.opencontainers.image.description="AdventureLog is a self-hosted travel companion that helps you plan, track, and share your adventures." \ + org.opencontainers.image.version="v0.10.0" \ + org.opencontainers.image.authors="Sean Morley" \ + org.opencontainers.image.url="https://raw.githubusercontent.com/seanmorley15/AdventureLog/refs/heads/main/brand/banner.png" \ + org.opencontainers.image.source="https://github.com/seanmorley15/AdventureLog" \ + org.opencontainers.image.vendor="Sean Morley" \ + org.opencontainers.image.created="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + org.opencontainers.image.licenses="GPL-3.0" # Set environment variables -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 # Set the working directory WORKDIR /code # Install system dependencies (Nginx included) RUN apt-get update \ - && apt-get install -y git postgresql-client gdal-bin libgdal-dev nginx \ + && apt-get install -y git postgresql-client gdal-bin libgdal-dev nginx supervisor \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* @@ -31,6 +43,9 @@ COPY ./server /code/ # Copy Nginx configuration COPY ./nginx.conf /etc/nginx/nginx.conf +# Copy Supervisor configuration +COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf + # Collect static files RUN python3 manage.py collectstatic --noinput --verbosity 2 @@ -41,5 +56,5 @@ RUN chmod +x /code/entrypoint.sh # Expose ports for NGINX and Gunicorn EXPOSE 80 8000 -# Command to start Nginx and Gunicorn -CMD ["bash", "-c", "service nginx start && /code/entrypoint.sh"] \ No newline at end of file +# Command to start Supervisor (which starts Nginx and Gunicorn) +CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index 9ca926c..1031cb1 100644 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -1,10 +1,32 @@ #!/bin/bash # Function to check PostgreSQL availability -check_postgres() { - PGPASSWORD=$PGPASSWORD psql -h "$PGHOST" -U "$PGUSER" -d "$PGDATABASE" -c '\q' >/dev/null 2>&1 +# Helper to get the first non-empty environment variable +get_env() { + for var in "$@"; do + value="${!var}" + if [ -n "$value" ]; then + echo "$value" + return + fi + done } +check_postgres() { + local db_host + local db_user + local db_name + local db_pass + + db_host=$(get_env PGHOST) + db_user=$(get_env PGUSER POSTGRES_USER) + db_name=$(get_env PGDATABASE POSTGRES_DB) + db_pass=$(get_env PGPASSWORD POSTGRES_PASSWORD) + + PGPASSWORD="$db_pass" psql -h "$db_host" -U "$db_user" -d "$db_name" -c '\q' >/dev/null 2>&1 +} + + # Wait for PostgreSQL to become available until check_postgres; do >&2 echo "PostgreSQL is unavailable - sleeping" @@ -62,5 +84,8 @@ fi cat /code/adventurelog.txt -# Start gunicorn -gunicorn main.wsgi:application --bind [::]:8000 --timeout 120 --workers 2 \ No newline at end of file +# Start Gunicorn in foreground +exec gunicorn main.wsgi:application \ + --bind [::]:8000 \ + --workers 2 \ + --timeout 120 diff --git a/backend/nginx.conf b/backend/nginx.conf index 8074aa6..23dba44 100644 --- a/backend/nginx.conf +++ b/backend/nginx.conf @@ -1,27 +1,20 @@ worker_processes 1; - events { worker_connections 1024; } - http { include /etc/nginx/mime.types; default_type application/octet-stream; - sendfile on; keepalive_timeout 65; - client_max_body_size 100M; - # The backend is running in the same container, so reference localhost upstream django { server 127.0.0.1:8000; # Use localhost to point to Gunicorn running internally } - server { listen 80; server_name localhost; - location / { proxy_pass http://django; # Forward to the upstream block proxy_set_header Host $host; @@ -29,17 +22,21 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } - location /static/ { alias /code/staticfiles/; # Serve static files directly } - # Serve protected media files with X-Accel-Redirect location /protectedMedia/ { internal; # Only internal requests are allowed alias /code/media/; # This should match Django MEDIA_ROOT try_files $uri =404; # Return a 404 if the file doesn't exist + + # Security headers for all protected files + add_header Content-Security-Policy "default-src 'self'; script-src 'none'; object-src 'none'; base-uri 'none'" always; + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options SAMEORIGIN always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; } - } -} +} \ No newline at end of file diff --git a/backend/server/.env.example b/backend/server/.env.example index 598aeb7..2c93208 100644 --- a/backend/server/.env.example +++ b/backend/server/.env.example @@ -22,6 +22,8 @@ EMAIL_BACKEND='console' # EMAIL_HOST_PASSWORD='password' # DEFAULT_FROM_EMAIL='user@example.com' +# GOOGLE_MAPS_API_KEY='key' + # ------------------- # # For Developers to start a Demo Database diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index 3210f7a..e23fa15 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -8,10 +8,25 @@ from allauth.account.decorators import secure_admin_login admin.autodiscover() admin.site.login = secure_admin_login(admin.site.login) +@admin.action(description="Trigger geocoding") +def trigger_geocoding(modeladmin, request, queryset): + count = 0 + for adventure in queryset: + try: + adventure.save() # Triggers geocoding logic in your model + count += 1 + except Exception as e: + modeladmin.message_user(request, f"Error geocoding {adventure}: {e}", level='error') + modeladmin.message_user(request, f"Geocoding triggered for {count} adventures.", level='success') + + + class AdventureAdmin(admin.ModelAdmin): list_display = ('name', 'get_category', 'get_visit_count', 'user_id', 'is_public') list_filter = ( 'user_id', 'is_public') search_fields = ('name',) + readonly_fields = ('city', 'region', 'country') + actions = [trigger_geocoding] def get_category(self, obj): if obj.category and obj.category.display_name and obj.category.icon: @@ -114,12 +129,9 @@ class CategoryAdmin(admin.ModelAdmin): search_fields = ('name', 'display_name') class CollectionAdmin(admin.ModelAdmin): - def adventure_count(self, obj): - return obj.adventure_set.count() + - adventure_count.short_description = 'Adventure Count' - - list_display = ('name', 'user_id', 'adventure_count', 'is_public') + list_display = ('name', 'user_id', 'is_public') admin.site.register(CustomUser, CustomUserAdmin) diff --git a/backend/server/adventures/apps.py b/backend/server/adventures/apps.py index 37a5920..e706a17 100644 --- a/backend/server/adventures/apps.py +++ b/backend/server/adventures/apps.py @@ -1,6 +1,8 @@ from django.apps import AppConfig -from django.conf import settings class AdventuresConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'adventures' \ No newline at end of file + name = 'adventures' + + def ready(self): + import adventures.signals # Import signals when the app is ready \ No newline at end of file diff --git a/backend/server/adventures/geocoding.py b/backend/server/adventures/geocoding.py new file mode 100644 index 0000000..f10c97a --- /dev/null +++ b/backend/server/adventures/geocoding.py @@ -0,0 +1,273 @@ +import requests +import time +import socket +from worldtravel.models import Region, City, VisitedRegion, VisitedCity +from django.conf import settings + +# ----------------- +# SEARCHING +def search_google(query): + try: + api_key = settings.GOOGLE_MAPS_API_KEY + if not api_key: + return {"error": "Missing Google Maps API key"} + + # 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' + } + + payload = { + "textQuery": query, + "maxResultCount": 20 # Adjust as needed + } + + 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: + return {"error": "No results found"} + + results = [] + for place in places: + location = place.get("location", {}) + types = place.get("types", []) + primary_type = types[0] if types else None + category = _extract_google_category(types) + addresstype = _infer_addresstype(primary_type) + + importance = None + rating = place.get("rating") + ratings_total = place.get("userRatingCount") + if rating is not None and ratings_total: + importance = round(float(rating) * ratings_total / 100, 2) + + # Extract display name from the new API structure + 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", + }) + + if results: + results.sort(key=lambda r: r["importance"] if r["importance"] is not None else 0, reverse=True) + + return results + + except requests.exceptions.RequestException as e: + return {"error": "Network error while contacting Google Maps", "details": str(e)} + + except Exception as e: + return {"error": "Unexpected error during Google search", "details": str(e)} + +def _extract_google_category(types): + # Basic category inference based on common place types + if not types: + return None + if "restaurant" in types: + return "food" + if "lodging" in types: + return "accommodation" + if "park" in types or "natural_feature" in types: + return "nature" + if "museum" in types or "tourist_attraction" in types: + return "attraction" + if "locality" in types or "administrative_area_level_1" in types: + return "region" + return types[0] # fallback to first type + + +def _infer_addresstype(type_): + # Rough mapping of Google place types to OSM-style addresstypes + mapping = { + "locality": "city", + "sublocality": "neighborhood", + "administrative_area_level_1": "region", + "administrative_area_level_2": "county", + "country": "country", + "premise": "building", + "point_of_interest": "poi", + "route": "road", + "street_address": "address", + } + return mapping.get(type_, None) + + +def search_osm(query): + url = f"https://nominatim.openstreetmap.org/search?q={query}&format=jsonv2" + headers = {'User-Agent': 'AdventureLog Server'} + response = requests.get(url, headers=headers) + 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] + +# ----------------- +# REVERSE GEOCODING +# ----------------- + +def extractIsoCode(user, data): + """ + Extract the ISO code from the response data. + Returns a dictionary containing the region name, country name, and ISO code if found. + """ + iso_code = None + town_city_or_county = None + display_name = None + country_code = None + city = None + visited_city = None + location_name = None + + # town = None + # city = None + # county = None + + if 'name' in data.keys(): + location_name = data['name'] + + if 'address' in data.keys(): + keys = data['address'].keys() + for key in keys: + if key.find("ISO") != -1: + iso_code = data['address'][key] + if 'town' in keys: + town_city_or_county = data['address']['town'] + if 'county' in keys: + town_city_or_county = data['address']['county'] + if 'city' in keys: + town_city_or_county = data['address']['city'] + if not iso_code: + return {"error": "No region found"} + + region = Region.objects.filter(id=iso_code).first() + visited_region = VisitedRegion.objects.filter(region=region, user_id=user).first() + + region_visited = False + city_visited = False + country_code = iso_code[:2] + + if region: + if town_city_or_county: + display_name = f"{town_city_or_county}, {region.name}, {country_code}" + city = City.objects.filter(name__contains=town_city_or_county, region=region).first() + visited_city = VisitedCity.objects.filter(city=city, user_id=user).first() + + if visited_region: + region_visited = True + if visited_city: + city_visited = True + if region: + return {"region_id": iso_code, "region": region.name, "country": region.country.name, "country_id": region.country.country_code, "region_visited": region_visited, "display_name": display_name, "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited, 'location_name': location_name} + return {"error": "No region found"} + +def is_host_resolvable(hostname: str) -> bool: + try: + socket.gethostbyname(hostname) + return True + except socket.error: + return False + +def reverse_geocode(lat, lon, user): + if getattr(settings, 'GOOGLE_MAPS_API_KEY', None): + return reverse_geocode_google(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': 'AdventureLog Server'} + connect_timeout = 1 + read_timeout = 5 + + if not is_host_resolvable("nominatim.openstreetmap.org"): + return {"error": "DNS resolution failed"} + + try: + response = requests.get(url, headers=headers, timeout=(connect_timeout, read_timeout)) + response.raise_for_status() + data = response.json() + return extractIsoCode(user, data) + except Exception: + return {"error": "An internal error occurred while processing the request"} + +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" + params = {"latlng": f"{lat},{lon}", "key": api_key} + + try: + response = requests.get(url, params=params) + response.raise_for_status() + data = response.json() + + if data.get("status") != "OK": + return {"error": "Geocoding failed"} + + # Convert Google schema to Nominatim-style for extractIsoCode + first_result = data.get("results", [])[0] + result_data = { + "name": first_result.get("formatted_address"), + "address": _parse_google_address_components(first_result.get("address_components", [])) + } + return extractIsoCode(user, result_data) + except Exception: + return {"error": "An internal error occurred while processing the request"} + +def _parse_google_address_components(components): + parsed = {} + country_code = None + state_code = None + + for comp in components: + types = comp.get("types", []) + long_name = comp.get("long_name") + short_name = comp.get("short_name") + + if "country" in types: + parsed["country"] = long_name + country_code = short_name + parsed["ISO3166-1"] = short_name + if "administrative_area_level_1" in types: + parsed["state"] = long_name + state_code = short_name + if "administrative_area_level_2" in types: + parsed["county"] = long_name + if "locality" in types: + parsed["city"] = long_name + if "sublocality" in types: + parsed["town"] = long_name + + # Build composite ISO 3166-2 code like US-ME + if country_code and state_code: + parsed["ISO3166-2-lvl1"] = f"{country_code}-{state_code}" + + return parsed \ No newline at end of file diff --git a/backend/server/adventures/managers.py b/backend/server/adventures/managers.py index 6d8d43c..4a12194 100644 --- a/backend/server/adventures/managers.py +++ b/backend/server/adventures/managers.py @@ -3,20 +3,15 @@ from django.db.models import Q class AdventureManager(models.Manager): def retrieve_adventures(self, user, include_owned=False, include_shared=False, include_public=False): - # Initialize the query with an empty Q object query = Q() - # Add owned adventures to the query if included if include_owned: - query |= Q(user_id=user.id) + query |= Q(user_id=user) - # Add shared adventures to the query if included if include_shared: - query |= Q(collection__shared_with=user.id) + query |= Q(collections__shared_with=user) - # Add public adventures to the query if included if include_public: query |= Q(is_public=True) - # Perform the query with the final Q object and remove duplicates return self.filter(query).distinct() diff --git a/backend/server/adventures/middleware.py b/backend/server/adventures/middleware.py index 10050b0..fae6b0b 100644 --- a/backend/server/adventures/middleware.py +++ b/backend/server/adventures/middleware.py @@ -29,4 +29,12 @@ class XSessionTokenMiddleware(MiddlewareMixin): class DisableCSRFForSessionTokenMiddleware(MiddlewareMixin): def process_request(self, request): if 'X-Session-Token' in request.headers: - setattr(request, '_dont_enforce_csrf_checks', True) \ No newline at end of file + setattr(request, '_dont_enforce_csrf_checks', True) + +class DisableCSRFForMobileLoginSignup(MiddlewareMixin): + def process_request(self, request): + is_mobile = request.headers.get('X-Is-Mobile', '').lower() == 'true' + is_login_or_signup = request.path in ['/auth/browser/v1/auth/login', '/auth/browser/v1/auth/signup'] + if is_mobile and is_login_or_signup: + setattr(request, '_dont_enforce_csrf_checks', True) + \ No newline at end of file diff --git a/backend/server/adventures/migrations/0025_alter_visit_end_date_alter_visit_start_date.py b/backend/server/adventures/migrations/0025_alter_visit_end_date_alter_visit_start_date.py new file mode 100644 index 0000000..668e968 --- /dev/null +++ b/backend/server/adventures/migrations/0025_alter_visit_end_date_alter_visit_start_date.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.8 on 2025-03-17 21:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0024_alter_attachment_file'), + ] + + operations = [ + migrations.AlterField( + model_name='visit', + name='end_date', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='visit', + name='start_date', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/backend/server/adventures/migrations/0026_visit_timezone.py b/backend/server/adventures/migrations/0026_visit_timezone.py new file mode 100644 index 0000000..f0642dd --- /dev/null +++ b/backend/server/adventures/migrations/0026_visit_timezone.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.11 on 2025-05-10 14:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0025_alter_visit_end_date_alter_visit_start_date'), + ] + + operations = [ + migrations.AddField( + model_name='visit', + name='timezone', + field=models.CharField(blank=True, choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmera', 'Africa/Asmera'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Buenos_Aires', 'America/Buenos_Aires'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Catamarca', 'America/Catamarca'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Ciudad_Juarez', 'America/Ciudad_Juarez'), ('America/Coral_Harbour', 'America/Coral_Harbour'), ('America/Cordoba', 'America/Cordoba'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Indianapolis', 'America/Indianapolis'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Jujuy', 'America/Jujuy'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Louisville', 'America/Louisville'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Mendoza', 'America/Mendoza'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Calcutta', 'Asia/Calcutta'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Katmandu', 'Asia/Katmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Rangoon', 'Asia/Rangoon'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Saigon', 'Asia/Saigon'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faeroe', 'Atlantic/Faeroe'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Sydney', 'Australia/Sydney'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zurich', 'Europe/Zurich'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Ponape', 'Pacific/Ponape'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Truk', 'Pacific/Truk'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis')], max_length=50, null=True), + ), + ] diff --git a/backend/server/adventures/migrations/0027_transportation_end_timezone_and_more.py b/backend/server/adventures/migrations/0027_transportation_end_timezone_and_more.py new file mode 100644 index 0000000..6331225 --- /dev/null +++ b/backend/server/adventures/migrations/0027_transportation_end_timezone_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.11 on 2025-05-10 15:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0026_visit_timezone'), + ] + + operations = [ + migrations.AddField( + model_name='transportation', + name='end_timezone', + field=models.CharField(blank=True, choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmera', 'Africa/Asmera'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Buenos_Aires', 'America/Buenos_Aires'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Catamarca', 'America/Catamarca'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Ciudad_Juarez', 'America/Ciudad_Juarez'), ('America/Coral_Harbour', 'America/Coral_Harbour'), ('America/Cordoba', 'America/Cordoba'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Indianapolis', 'America/Indianapolis'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Jujuy', 'America/Jujuy'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Louisville', 'America/Louisville'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Mendoza', 'America/Mendoza'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Calcutta', 'Asia/Calcutta'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Katmandu', 'Asia/Katmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Rangoon', 'Asia/Rangoon'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Saigon', 'Asia/Saigon'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faeroe', 'Atlantic/Faeroe'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Sydney', 'Australia/Sydney'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zurich', 'Europe/Zurich'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Ponape', 'Pacific/Ponape'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Truk', 'Pacific/Truk'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis')], max_length=50, null=True), + ), + migrations.AddField( + model_name='transportation', + name='start_timezone', + field=models.CharField(blank=True, choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmera', 'Africa/Asmera'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Buenos_Aires', 'America/Buenos_Aires'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Catamarca', 'America/Catamarca'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Ciudad_Juarez', 'America/Ciudad_Juarez'), ('America/Coral_Harbour', 'America/Coral_Harbour'), ('America/Cordoba', 'America/Cordoba'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Indianapolis', 'America/Indianapolis'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Jujuy', 'America/Jujuy'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Louisville', 'America/Louisville'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Mendoza', 'America/Mendoza'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Calcutta', 'Asia/Calcutta'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Katmandu', 'Asia/Katmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Rangoon', 'Asia/Rangoon'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Saigon', 'Asia/Saigon'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faeroe', 'Atlantic/Faeroe'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Sydney', 'Australia/Sydney'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zurich', 'Europe/Zurich'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Ponape', 'Pacific/Ponape'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Truk', 'Pacific/Truk'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis')], max_length=50, null=True), + ), + ] diff --git a/backend/server/adventures/migrations/0028_lodging_timezone.py b/backend/server/adventures/migrations/0028_lodging_timezone.py new file mode 100644 index 0000000..d9513f7 --- /dev/null +++ b/backend/server/adventures/migrations/0028_lodging_timezone.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.11 on 2025-05-10 15:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0027_transportation_end_timezone_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='lodging', + name='timezone', + field=models.CharField(blank=True, choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmera', 'Africa/Asmera'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Buenos_Aires', 'America/Buenos_Aires'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Catamarca', 'America/Catamarca'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Ciudad_Juarez', 'America/Ciudad_Juarez'), ('America/Coral_Harbour', 'America/Coral_Harbour'), ('America/Cordoba', 'America/Cordoba'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Indianapolis', 'America/Indianapolis'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Jujuy', 'America/Jujuy'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Louisville', 'America/Louisville'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Mendoza', 'America/Mendoza'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Calcutta', 'Asia/Calcutta'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Katmandu', 'Asia/Katmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Rangoon', 'Asia/Rangoon'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Saigon', 'Asia/Saigon'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faeroe', 'Atlantic/Faeroe'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Sydney', 'Australia/Sydney'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zurich', 'Europe/Zurich'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Ponape', 'Pacific/Ponape'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Truk', 'Pacific/Truk'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis')], max_length=50, null=True), + ), + ] diff --git a/backend/server/adventures/migrations/0029_adventure_city_adventure_country_adventure_region.py b/backend/server/adventures/migrations/0029_adventure_city_adventure_country_adventure_region.py new file mode 100644 index 0000000..3df78e0 --- /dev/null +++ b/backend/server/adventures/migrations/0029_adventure_city_adventure_country_adventure_region.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.11 on 2025-05-22 22:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0028_lodging_timezone'), + ('worldtravel', '0015_city_insert_id_country_insert_id_region_insert_id'), + ] + + operations = [ + migrations.AddField( + model_name='adventure', + name='city', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='worldtravel.city'), + ), + migrations.AddField( + model_name='adventure', + name='country', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='worldtravel.country'), + ), + migrations.AddField( + model_name='adventure', + name='region', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='worldtravel.region'), + ), + ] diff --git a/backend/server/adventures/migrations/0030_set_end_date_equal_start.py b/backend/server/adventures/migrations/0030_set_end_date_equal_start.py new file mode 100644 index 0000000..55d5f93 --- /dev/null +++ b/backend/server/adventures/migrations/0030_set_end_date_equal_start.py @@ -0,0 +1,18 @@ +from django.db import migrations + +def set_end_date_equal_to_start(apps, schema_editor): + Visit = apps.get_model('adventures', 'Visit') + for visit in Visit.objects.filter(end_date__isnull=True): + if visit.start_date: + visit.end_date = visit.start_date + visit.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0029_adventure_city_adventure_country_adventure_region'), + ] + + operations = [ + migrations.RunPython(set_end_date_equal_to_start), + ] diff --git a/backend/server/adventures/migrations/0031_adventureimage_immich_id_alter_adventureimage_image_and_more.py b/backend/server/adventures/migrations/0031_adventureimage_immich_id_alter_adventureimage_image_and_more.py new file mode 100644 index 0000000..75ebb60 --- /dev/null +++ b/backend/server/adventures/migrations/0031_adventureimage_immich_id_alter_adventureimage_image_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.1 on 2025-06-01 16:57 + +import adventures.models +import django_resized.forms +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0030_set_end_date_equal_start'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='adventureimage', + name='immich_id', + field=models.CharField(blank=True, max_length=200, null=True), + ), + migrations.AlterField( + model_name='adventureimage', + name='image', + field=django_resized.forms.ResizedImageField(blank=True, crop=None, force_format='WEBP', keep_meta=True, null=True, quality=75, scale=None, size=[1920, 1080], upload_to=adventures.models.PathAndRename('images/')), + ), + migrations.AddConstraint( + model_name='adventureimage', + constraint=models.CheckConstraint(condition=models.Q(models.Q(('image__isnull', False), ('immich_id__isnull', True)), models.Q(('image__isnull', True), ('immich_id__isnull', False)), _connector='OR'), name='image_xor_immich_id'), + ), + ] diff --git a/backend/server/adventures/migrations/0032_remove_adventureimage_image_xor_immich_id.py b/backend/server/adventures/migrations/0032_remove_adventureimage_image_xor_immich_id.py new file mode 100644 index 0000000..2ae8076 --- /dev/null +++ b/backend/server/adventures/migrations/0032_remove_adventureimage_image_xor_immich_id.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.1 on 2025-06-01 17:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0031_adventureimage_immich_id_alter_adventureimage_image_and_more'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='adventureimage', + name='image_xor_immich_id', + ), + ] diff --git a/backend/server/adventures/migrations/0033_adventureimage_unique_immich_id_per_user.py b/backend/server/adventures/migrations/0033_adventureimage_unique_immich_id_per_user.py new file mode 100644 index 0000000..d3b1bb5 --- /dev/null +++ b/backend/server/adventures/migrations/0033_adventureimage_unique_immich_id_per_user.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.1 on 2025-06-02 02:31 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0032_remove_adventureimage_image_xor_immich_id'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddConstraint( + model_name='adventureimage', + constraint=models.UniqueConstraint(fields=('immich_id', 'user_id'), name='unique_immich_id_per_user'), + ), + ] diff --git a/backend/server/adventures/migrations/0034_remove_adventureimage_unique_immich_id_per_user.py b/backend/server/adventures/migrations/0034_remove_adventureimage_unique_immich_id_per_user.py new file mode 100644 index 0000000..52b3e52 --- /dev/null +++ b/backend/server/adventures/migrations/0034_remove_adventureimage_unique_immich_id_per_user.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.1 on 2025-06-02 02:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0033_adventureimage_unique_immich_id_per_user'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='adventureimage', + name='unique_immich_id_per_user', + ), + ] diff --git a/backend/server/adventures/migrations/0035_remove_adventure_collection_adventure_collections.py b/backend/server/adventures/migrations/0035_remove_adventure_collection_adventure_collections.py new file mode 100644 index 0000000..59d3580 --- /dev/null +++ b/backend/server/adventures/migrations/0035_remove_adventure_collection_adventure_collections.py @@ -0,0 +1,59 @@ +# Generated by Django 5.2.1 on 2025-06-10 03:04 + +from django.db import migrations, models + + +def migrate_collection_relationships(apps, schema_editor): + """ + Migrate existing ForeignKey relationships to ManyToMany relationships + """ + Adventure = apps.get_model('adventures', 'Adventure') + + # Get all adventures that have a collection assigned + adventures_with_collections = Adventure.objects.filter(collection__isnull=False) + + for adventure in adventures_with_collections: + # Add the existing collection to the new many-to-many field + adventure.collections.add(adventure.collection_id) + + +def reverse_migrate_collection_relationships(apps, schema_editor): + """ + Reverse migration - convert first collection back to ForeignKey + Note: This will only preserve the first collection if an adventure has multiple + """ + Adventure = apps.get_model('adventures', 'Adventure') + + for adventure in Adventure.objects.all(): + first_collection = adventure.collections.first() + if first_collection: + adventure.collection = first_collection + adventure.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0034_remove_adventureimage_unique_immich_id_per_user'), + ] + + operations = [ + # First, add the new ManyToMany field + migrations.AddField( + model_name='adventure', + name='collections', + field=models.ManyToManyField(blank=True, related_name='adventures', to='adventures.collection'), + ), + + # Migrate existing data from old field to new field + migrations.RunPython( + migrate_collection_relationships, + reverse_migrate_collection_relationships + ), + + # Finally, remove the old ForeignKey field + migrations.RemoveField( + model_name='adventure', + name='collection', + ), + ] \ No newline at end of file diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index c7f78ca..40bb680 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -5,16 +5,60 @@ import uuid from django.db import models from django.utils.deconstruct import deconstructible from adventures.managers import AdventureManager +import threading from django.contrib.auth import get_user_model from django.contrib.postgres.fields import ArrayField from django.forms import ValidationError from django_resized import ResizedImageField +from worldtravel.models import City, Country, Region, VisitedCity, VisitedRegion +from django.core.exceptions import ValidationError +from django.utils import timezone + +def background_geocode_and_assign(adventure_id: str): + print(f"[Adventure Geocode Thread] Starting geocode for adventure {adventure_id}") + try: + adventure = Adventure.objects.get(id=adventure_id) + if not (adventure.latitude and adventure.longitude): + return + + from adventures.geocoding import reverse_geocode # or wherever you defined it + is_visited = adventure.is_visited_status() + result = reverse_geocode(adventure.latitude, adventure.longitude, adventure.user_id) + + if 'region_id' in result: + region = Region.objects.filter(id=result['region_id']).first() + if region: + adventure.region = region + if is_visited: + VisitedRegion.objects.get_or_create(user_id=adventure.user_id, region=region) + + if 'city_id' in result: + city = City.objects.filter(id=result['city_id']).first() + if city: + adventure.city = city + if is_visited: + VisitedCity.objects.get_or_create(user_id=adventure.user_id, city=city) + + if 'country_id' in result: + country = Country.objects.filter(country_code=result['country_id']).first() + if country: + adventure.country = country + + # Save updated location info + # Save updated location info, skip geocode threading + adventure.save(update_fields=["region", "city", "country"], _skip_geocode=True) + + # print(f"[Adventure Geocode Thread] Successfully processed {adventure_id}: {adventure.name} - {adventure.latitude}, {adventure.longitude}") + + except Exception as e: + # Optional: log or print the error + print(f"[Adventure Geocode Thread] Error processing {adventure_id}: {e}") def validate_file_extension(value): import os from django.core.exceptions import ValidationError ext = os.path.splitext(value.name)[1] # [0] returns path+filename - valid_extensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.mp4', '.mov', '.avi', '.mkv', '.mp3', '.wav', '.flac', '.ogg', '.m4a', '.wma', '.aac', '.opus', '.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz', '.zst', '.lz4', '.lzma', '.lzo', '.z', '.tar.gz', '.tar.bz2', '.tar.xz', '.tar.zst', '.tar.lz4', '.tar.lzma', '.tar.lzo', '.tar.z', 'gpx', 'md', 'pdf'] + valid_extensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.mp4', '.mov', '.avi', '.mkv', '.mp3', '.wav', '.flac', '.ogg', '.m4a', '.wma', '.aac', '.opus', '.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz', '.zst', '.lz4', '.lzma', '.lzo', '.z', '.tar.gz', '.tar.bz2', '.tar.xz', '.tar.zst', '.tar.lz4', '.tar.lzma', '.tar.lzo', '.tar.z', '.gpx', '.md'] if not ext.lower() in valid_extensions: raise ValidationError('Unsupported file extension.') @@ -43,6 +87,426 @@ ADVENTURE_TYPES = [ ('other', 'Other') ] +TIMEZONES = [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/La_Rioja", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Ciudad_Juarez", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Fort_Nelson", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Monticello", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Ojinaga", + "America/Panama", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Colombo", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Hebron", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ulaanbaatar", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/Perth", + "Australia/Sydney", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Ulyanovsk", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zurich", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis" +] + LODGING_TYPES = [ ('hotel', 'Hotel'), ('hostel', 'Hostel'), @@ -76,8 +540,9 @@ User = get_user_model() class Visit(models.Model): id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) adventure = models.ForeignKey('Adventure', on_delete=models.CASCADE, related_name='visits') - start_date = models.DateField(null=True, blank=True) - end_date = models.DateField(null=True, blank=True) + start_date = models.DateTimeField(null=True, blank=True) + end_date = models.DateTimeField(null=True, blank=True) + timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], null=True, blank=True) notes = models.TextField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -104,50 +569,92 @@ class Adventure(models.Model): rating = models.FloatField(blank=True, null=True) link = models.URLField(blank=True, null=True, max_length=2083) is_public = models.BooleanField(default=False) + longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) - collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True) + + city = models.ForeignKey(City, on_delete=models.SET_NULL, blank=True, null=True) + region = models.ForeignKey(Region, on_delete=models.SET_NULL, blank=True, null=True) + country = models.ForeignKey(Country, on_delete=models.SET_NULL, blank=True, null=True) + + # Changed from ForeignKey to ManyToManyField + collections = models.ManyToManyField('Collection', blank=True, related_name='adventures') + created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) objects = AdventureManager() - # DEPRECATED FIELDS - TO BE REMOVED IN FUTURE VERSIONS - # Migrations performed in this version will remove these fields - # image = ResizedImageField(force_format="WEBP", quality=75, null=True, blank=True, upload_to='images/') - # date = models.DateField(blank=True, null=True) - # end_date = models.DateField(blank=True, null=True) - # type = models.CharField(max_length=100, choices=ADVENTURE_TYPES, default='general') + def is_visited_status(self): + current_date = timezone.now().date() + for visit in self.visits.all(): + start_date = visit.start_date.date() if isinstance(visit.start_date, timezone.datetime) else visit.start_date + end_date = visit.end_date.date() if isinstance(visit.end_date, timezone.datetime) else visit.end_date + if start_date and end_date and (start_date <= current_date): + return True + elif start_date and not end_date and (start_date <= current_date): + return True + return False - def clean(self): - if self.collection: - if self.collection.is_public and not self.is_public: - raise ValidationError('Adventures associated with a public collection must be public. Collection: ' + self.trip.name + ' Adventure: ' + self.name) - if self.user_id != self.collection.user_id: - raise ValidationError('Adventures must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Adventure owner: ' + self.user_id.username) + def clean(self, skip_shared_validation=False): + """ + Validate model constraints. + skip_shared_validation: Skip validation when called by shared users + """ + # Skip validation if this is a shared user update + if skip_shared_validation: + return + + # Check collections after the instance is saved (in save method or separate validation) + if self.pk: # Only check if the instance has been saved + for collection in self.collections.all(): + if collection.is_public and not self.is_public: + raise ValidationError(f'Adventures associated with a public collection must be public. Collection: {collection.name} Adventure: {self.name}') + + # Only enforce same-user constraint for non-shared collections + if self.user_id != collection.user_id: + # Check if this is a shared collection scenario + # Allow if the adventure owner has access to the collection through sharing + if not collection.shared_with.filter(uuid=self.user_id.uuid).exists(): + raise ValidationError(f'Adventures must be associated with collections owned by the same user or shared collections. Collection owner: {collection.user_id.username} Adventure owner: {self.user_id.username}') + if self.category: if self.user_id != self.category.user_id: - raise ValidationError('Adventures must be associated with categories owned by the same user. Category owner: ' + self.category.user_id.username + ' Adventure owner: ' + self.user_id.username) + raise ValidationError(f'Adventures must be associated with categories owned by the same user. Category owner: {self.category.user_id.username} Adventure owner: {self.user_id.username}') - def save(self, force_insert: bool = False, force_update: bool = False, using: str | None = None, update_fields: Iterable[str] | None = None) -> None: - """ - Saves the current instance. If the instance is being inserted for the first time, it will be created in the database. - If it already exists, it will be updated. - """ + def save(self, force_insert=False, force_update=False, using=None, update_fields=None, _skip_geocode=False, _skip_shared_validation=False): if force_insert and force_update: raise ValueError("Cannot force both insert and updating in model saving.") + if not self.category: - category, created = Category.objects.get_or_create( - user_id=self.user_id, - name='general', - defaults={ - 'display_name': 'General', - 'icon': '🌍' - } - ) + category, _ = Category.objects.get_or_create( + user_id=self.user_id, + name='general', + defaults={'display_name': 'General', 'icon': '🌍'} + ) self.category = category - - return super().save(force_insert, force_update, using, update_fields) + + result = super().save(force_insert, force_update, using, update_fields) + + # Validate collections after saving (since M2M relationships require saved instance) + if self.pk: + try: + self.clean(skip_shared_validation=_skip_shared_validation) + except ValidationError as e: + # If validation fails, you might want to handle this differently + # For now, we'll re-raise the error + raise e + + # ⛔ Skip threading if called from geocode background thread + if _skip_geocode: + return result + + if self.latitude and self.longitude: + thread = threading.Thread(target=background_geocode_and_assign, args=(str(self.id),)) + thread.daemon = True # Allows the thread to exit when the main program ends + thread.start() + + return result def __str__(self): return self.name @@ -168,13 +675,13 @@ class Collection(models.Model): shared_with = models.ManyToManyField(User, related_name='shared_with', blank=True) link = models.URLField(blank=True, null=True, max_length=2083) - # if connected adventures are private and collection is public, raise an error def clean(self): if self.is_public and self.pk: # Only check if the instance has a primary key - for adventure in self.adventure_set.all(): + # Updated to use the new related_name 'adventures' + for adventure in self.adventures.all(): if not adventure.is_public: - raise ValidationError('Public collections cannot be associated with private adventures. Collection: ' + self.name + ' Adventure: ' + adventure.name) + raise ValidationError(f'Public collections cannot be associated with private adventures. Collection: {self.name} Adventure: {adventure.name}') def __str__(self): return self.name @@ -191,6 +698,8 @@ class Transportation(models.Model): link = models.URLField(blank=True, null=True) date = models.DateTimeField(blank=True, null=True) end_date = models.DateTimeField(blank=True, null=True) + start_timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], null=True, blank=True) + end_timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], null=True, blank=True) flight_number = models.CharField(max_length=100, blank=True, null=True) from_location = models.CharField(max_length=200, blank=True, null=True) origin_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) @@ -296,18 +805,41 @@ class PathAndRename: class AdventureImage(models.Model): id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) - user_id = models.ForeignKey( - User, on_delete=models.CASCADE, default=default_user_id) + user_id = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user_id) image = ResizedImageField( force_format="WEBP", quality=75, - upload_to=PathAndRename('images/') # Use the callable class here + upload_to=PathAndRename('images/'), + blank=True, + null=True, ) + immich_id = models.CharField(max_length=200, null=True, blank=True) adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE) is_primary = models.BooleanField(default=False) + def clean(self): + + # One of image or immich_id must be set, but not both + has_image = bool(self.image and str(self.image).strip()) + has_immich_id = bool(self.immich_id and str(self.immich_id).strip()) + + if has_image and has_immich_id: + raise ValidationError("Cannot have both image file and Immich ID. Please provide only one.") + if not has_image and not has_immich_id: + raise ValidationError("Must provide either an image file or an Immich ID.") + + def save(self, *args, **kwargs): + # Clean empty strings to None for proper database storage + if not self.image: + self.image = None + if not self.immich_id or not str(self.immich_id).strip(): + self.immich_id = None + + self.full_clean() # This calls clean() method + super().save(*args, **kwargs) + def __str__(self): - return self.image.url + return self.image.url if self.image else f"Immich ID: {self.immich_id or 'No image'}" class Attachment(models.Model): id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) @@ -352,6 +884,7 @@ class Lodging(models.Model): link = models.URLField(blank=True, null=True, max_length=2083) check_in = models.DateTimeField(blank=True, null=True) check_out = models.DateTimeField(blank=True, null=True) + timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], null=True, blank=True) reservation_number = models.CharField(max_length=100, blank=True, null=True) price = models.DecimalField(max_digits=9, decimal_places=2, blank=True, null=True) latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) @@ -363,8 +896,8 @@ class Lodging(models.Model): updated_at = models.DateTimeField(auto_now=True) def clean(self): - if self.date and self.end_date and self.date > self.end_date: - raise ValidationError('The start date must be before the end date. Start date: ' + str(self.date) + ' End date: ' + str(self.end_date)) + if self.check_in and self.check_out and self.check_in > self.check_out: + raise ValidationError('The start date must be before the end date. Start date: ' + str(self.check_in) + ' End date: ' + str(self.check_out)) if self.collection: if self.collection.is_public and not self.is_public: diff --git a/backend/server/adventures/permissions.py b/backend/server/adventures/permissions.py index 941fbc0..ce38f74 100644 --- a/backend/server/adventures/permissions.py +++ b/backend/server/adventures/permissions.py @@ -2,78 +2,99 @@ from rest_framework import permissions class IsOwnerOrReadOnly(permissions.BasePermission): """ - Custom permission to only allow owners of an object to edit it. + Owners can edit, others have read-only access. """ - def has_object_permission(self, request, view, obj): - # Read permissions are allowed to any request, - # so we'll always allow GET, HEAD or OPTIONS requests. if request.method in permissions.SAFE_METHODS: return True - - # Write permissions are only allowed to the owner of the object. + # obj.user_id is FK to User, compare with request.user return obj.user_id == request.user class IsPublicReadOnly(permissions.BasePermission): """ - Custom permission to only allow read-only access to public objects, - and write access to the owner of the object. + Read-only if public or owner, write only for owner. """ - def has_object_permission(self, request, view, obj): - # Read permissions are allowed if the object is public if request.method in permissions.SAFE_METHODS: return obj.is_public or obj.user_id == request.user - - # Write permissions are only allowed to the owner of the object return obj.user_id == request.user - + + class CollectionShared(permissions.BasePermission): """ - Custom permission to only allow read-only access to public objects, - and write access to the owner of the object. + Allow full access if user is in shared_with of collection(s) or owner, + read-only if public or shared_with, + write only if owner or shared_with. """ - def has_object_permission(self, request, view, obj): + user = request.user + if not user or not user.is_authenticated: + # Anonymous: only read public + return request.method in permissions.SAFE_METHODS and obj.is_public - # Read permissions are allowed if the object is shared with the user - if obj.shared_with and obj.shared_with.filter(id=request.user.id).exists(): - return True - - # Write permissions are allowed if the object is shared with the user - if request.method not in permissions.SAFE_METHODS and obj.shared_with.filter(id=request.user.id).exists(): - return True + # Check if user is in shared_with of any collections related to the obj + # If obj is a Collection itself: + if hasattr(obj, 'shared_with'): + if obj.shared_with.filter(id=user.id).exists(): + return True - # Read permissions are allowed if the object is public + # If obj is an Adventure (has collections M2M) + if hasattr(obj, 'collections'): + # Check if user is in shared_with of any related collection + shared_collections = obj.collections.filter(shared_with=user) + if shared_collections.exists(): + return True + + # Read permission if public or owner if request.method in permissions.SAFE_METHODS: - return obj.is_public or obj.user_id == request.user + return obj.is_public or obj.user_id == user + + # Write permission only if owner or shared user via collections + if obj.user_id == user: + return True + + if hasattr(obj, 'collections'): + if obj.collections.filter(shared_with=user).exists(): + return True + + # Default deny + return False - # Write permissions are only allowed to the owner of the object - return obj.user_id == request.user class IsOwnerOrSharedWithFullAccess(permissions.BasePermission): """ - Custom permission to allow: - - Full access for shared users - - Full access for owners - - Read-only access for others on safe methods + Full access for owners and users shared via collections, + read-only for others if public. """ - def has_object_permission(self, request, view, obj): - - # Allow GET only for a public object - if request.method in permissions.SAFE_METHODS and obj.is_public: - return True - # Check if the object has a collection - if hasattr(obj, 'collection') and obj.collection: - # Allow all actions for shared users - if request.user in obj.collection.shared_with.all(): - return True + user = request.user + if not user or not user.is_authenticated: + return request.method in permissions.SAFE_METHODS and obj.is_public - # Always allow GET, HEAD, or OPTIONS requests (safe methods) + # If safe method (read), allow if: if request.method in permissions.SAFE_METHODS: + if obj.is_public: + return True + if obj.user_id == user: + return True + # If user in shared_with of any collection related to obj + if hasattr(obj, 'collections') and obj.collections.filter(shared_with=user).exists(): + return True + if hasattr(obj, 'collection') and obj.collection and obj.collection.shared_with.filter(id=user.id).exists(): + return True + if hasattr(obj, 'shared_with') and obj.shared_with.filter(id=user.id).exists(): + return True + return False + + # For write methods, allow if owner or shared user + if obj.user_id == user: + return True + if hasattr(obj, 'collections') and obj.collections.filter(shared_with=user).exists(): + return True + if hasattr(obj, 'collection') and obj.collection and obj.collection.shared_with.filter(id=user.id).exists(): + return True + if hasattr(obj, 'shared_with') and obj.shared_with.filter(id=user.id).exists(): return True - # Allow all actions for the owner - return obj.user_id == request.user \ No newline at end of file + return False diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 97dd633..622e2eb 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -4,22 +4,38 @@ from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, from rest_framework import serializers from main.utils import CustomModelSerializer from users.serializers import CustomUserDetailsSerializer +from worldtravel.serializers import CountrySerializer, RegionSerializer, CitySerializer +from geopy.distance import geodesic +from integrations.models import ImmichIntegration class AdventureImageSerializer(CustomModelSerializer): class Meta: model = AdventureImage - fields = ['id', 'image', 'adventure', 'is_primary', 'user_id'] + fields = ['id', 'image', 'adventure', 'is_primary', 'user_id', 'immich_id'] read_only_fields = ['id', 'user_id'] def to_representation(self, instance): + # If immich_id is set, check for user integration once + integration = None + if instance.immich_id: + integration = ImmichIntegration.objects.filter(user=instance.user_id).first() + if not integration: + return None # Skip if Immich image but no integration + + # Base representation representation = super().to_representation(instance) - if instance.image: - public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/') - #print(public_url) - # remove any ' from the url - public_url = public_url.replace("'", "") + + # Prepare public URL once + public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/').replace("'", "") + + if instance.immich_id: + # Use Immich integration URL + representation['image'] = f"{public_url}/api/integrations/immich/{integration.id}/get/{instance.immich_id}" + elif instance.image: + # Use local image URL representation['image'] = f"{public_url}/media/{instance.image.name}" + return representation class AttachmentSerializer(CustomModelSerializer): @@ -72,26 +88,52 @@ class VisitSerializer(serializers.ModelSerializer): class Meta: model = Visit - fields = ['id', 'start_date', 'end_date', 'notes'] + fields = ['id', 'start_date', 'end_date', 'timezone', 'notes'] read_only_fields = ['id'] class AdventureSerializer(CustomModelSerializer): - images = AdventureImageSerializer(many=True, read_only=True) + images = serializers.SerializerMethodField() visits = VisitSerializer(many=True, read_only=False, required=False) attachments = AttachmentSerializer(many=True, read_only=True) category = CategorySerializer(read_only=False, required=False) is_visited = serializers.SerializerMethodField() user = serializers.SerializerMethodField() + country = CountrySerializer(read_only=True) + region = RegionSerializer(read_only=True) + city = CitySerializer(read_only=True) + collections = serializers.PrimaryKeyRelatedField( + many=True, + queryset=Collection.objects.all(), + required=False + ) class Meta: model = Adventure fields = [ 'id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location', - 'is_public', 'collection', 'created_at', 'updated_at', 'images', 'link', 'longitude', - 'latitude', 'visits', 'is_visited', 'category', 'attachments', 'user' + 'is_public', 'collections', 'created_at', 'updated_at', 'images', 'link', 'longitude', + 'latitude', 'visits', 'is_visited', 'category', 'attachments', 'user', 'city', 'country', 'region' ] read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'is_visited', 'user'] + def get_images(self, obj): + serializer = AdventureImageSerializer(obj.images.all(), many=True, context=self.context) + # Filter out None values from the serialized data + return [image for image in serializer.data if image is not None] + + def validate_collections(self, collections): + """Validate that collections belong to the same user""" + if not collections: + return collections + + user = self.context['request'].user + for collection in collections: + if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists(): + raise serializers.ValidationError( + f"Collection '{collection.name}' does not belong to the current user." + ) + return collections + def validate_category(self, category_data): if isinstance(category_data, Category): return category_data @@ -103,7 +145,7 @@ class AdventureSerializer(CustomModelSerializer): return existing_category category_data['name'] = name return category_data - + def get_or_create_category(self, category_data): user = self.context['request'].user @@ -113,7 +155,7 @@ class AdventureSerializer(CustomModelSerializer): if isinstance(category_data, dict): name = category_data.get('name', '').lower() display_name = category_data.get('display_name', name) - icon = category_data.get('icon', 'īŋŊ') + icon = category_data.get('icon', '🌍') else: name = category_data.name.lower() display_name = category_data.display_name @@ -134,26 +176,30 @@ class AdventureSerializer(CustomModelSerializer): return CustomUserDetailsSerializer(user).data def get_is_visited(self, obj): - current_date = timezone.now().date() - for visit in obj.visits.all(): - if visit.start_date and visit.end_date and (visit.start_date <= current_date): - return True - elif visit.start_date and not visit.end_date and (visit.start_date <= current_date): - return True - return False + return obj.is_visited_status() def create(self, validated_data): visits_data = validated_data.pop('visits', None) category_data = validated_data.pop('category', None) + collections_data = validated_data.pop('collections', []) + print(category_data) adventure = Adventure.objects.create(**validated_data) + + # Handle visits for visit_data in visits_data: Visit.objects.create(adventure=adventure, **visit_data) + # Handle category if category_data: category = self.get_or_create_category(category_data) adventure.category = category - adventure.save() + + # Handle collections - set after adventure is saved + if collections_data: + adventure.collections.set(collections_data) + + adventure.save() return adventure @@ -162,14 +208,25 @@ class AdventureSerializer(CustomModelSerializer): visits_data = validated_data.pop('visits', []) category_data = validated_data.pop('category', None) + collections_data = validated_data.pop('collections', None) + + # Update regular fields for attr, value in validated_data.items(): setattr(instance, attr, value) - if category_data: + # Handle category - ONLY allow the adventure owner to change categories + user = self.context['request'].user + if category_data and instance.user_id == user: + # Only the owner can set categories category = self.get_or_create_category(category_data) instance.category = category - instance.save() + # If not the owner, ignore category changes + # Handle collections - only update if collections were provided + if collections_data is not None: + instance.collections.set(collections_data) + + # Handle visits if has_visits: current_visits = instance.visits.all() current_visit_ids = set(current_visits.values_list('id', flat=True)) @@ -190,18 +247,37 @@ class AdventureSerializer(CustomModelSerializer): visits_to_delete = current_visit_ids - updated_visit_ids instance.visits.filter(id__in=visits_to_delete).delete() + # call save on the adventure to update the updated_at field and trigger any geocoding + instance.save() + return instance class TransportationSerializer(CustomModelSerializer): + distance = serializers.SerializerMethodField() class Meta: model = Transportation fields = [ 'id', 'user_id', 'type', 'name', 'description', 'rating', 'link', 'date', 'flight_number', 'from_location', 'to_location', - 'is_public', 'collection', 'created_at', 'updated_at', 'end_date', 'origin_latitude', 'origin_longitude', 'destination_latitude', 'destination_longitude' + 'is_public', 'collection', 'created_at', 'updated_at', 'end_date', + 'origin_latitude', 'origin_longitude', 'destination_latitude', 'destination_longitude', + 'start_timezone', 'end_timezone', 'distance' # ✅ Add distance here ] - read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] + read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'distance'] + + def get_distance(self, obj): + if ( + obj.origin_latitude and obj.origin_longitude and + obj.destination_latitude and obj.destination_longitude + ): + try: + origin = (float(obj.origin_latitude), float(obj.origin_longitude)) + destination = (float(obj.destination_latitude), float(obj.destination_longitude)) + return round(geodesic(origin, destination).km, 2) + except ValueError: + return None + return None class LodgingSerializer(CustomModelSerializer): @@ -210,7 +286,7 @@ class LodgingSerializer(CustomModelSerializer): fields = [ 'id', 'user_id', 'name', 'description', 'rating', 'link', 'check_in', 'check_out', 'reservation_number', 'price', 'latitude', 'longitude', 'location', 'is_public', - 'collection', 'created_at', 'updated_at', 'type' + 'collection', 'created_at', 'updated_at', 'type', 'timezone' ] read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] @@ -234,6 +310,7 @@ class ChecklistItemSerializer(CustomModelSerializer): class ChecklistSerializer(CustomModelSerializer): items = ChecklistItemSerializer(many=True, source='checklistitem_set') + class Meta: model = Checklist fields = [ @@ -244,8 +321,16 @@ class ChecklistSerializer(CustomModelSerializer): def create(self, validated_data): items_data = validated_data.pop('checklistitem_set') checklist = Checklist.objects.create(**validated_data) + for item_data in items_data: - ChecklistItem.objects.create(checklist=checklist, **item_data) + # Remove user_id from item_data to avoid constraint issues + item_data.pop('user_id', None) + # Set user_id from the parent checklist + ChecklistItem.objects.create( + checklist=checklist, + user_id=checklist.user_id, + **item_data + ) return checklist def update(self, instance, validated_data): @@ -263,6 +348,9 @@ class ChecklistSerializer(CustomModelSerializer): # Update or create items updated_item_ids = set() for item_data in items_data: + # Remove user_id from item_data to avoid constraint issues + item_data.pop('user_id', None) + item_id = item_data.get('id') if item_id: if item_id in current_item_ids: @@ -273,10 +361,18 @@ class ChecklistSerializer(CustomModelSerializer): updated_item_ids.add(item_id) else: # If ID is provided but doesn't exist, create new item - ChecklistItem.objects.create(checklist=instance, **item_data) + ChecklistItem.objects.create( + checklist=instance, + user_id=instance.user_id, + **item_data + ) else: # If no ID is provided, create new item - ChecklistItem.objects.create(checklist=instance, **item_data) + ChecklistItem.objects.create( + checklist=instance, + user_id=instance.user_id, + **item_data + ) # Delete items that are not in the updated data items_to_delete = current_item_ids - updated_item_ids @@ -292,11 +388,10 @@ class ChecklistSerializer(CustomModelSerializer): raise serializers.ValidationError( 'Checklists associated with a public collection must be public.' ) - return data class CollectionSerializer(CustomModelSerializer): - adventures = AdventureSerializer(many=True, read_only=True, source='adventure_set') + adventures = AdventureSerializer(many=True, read_only=True) transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set') notes = NoteSerializer(many=True, read_only=True, source='note_set') checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set') diff --git a/backend/server/adventures/signals.py b/backend/server/adventures/signals.py new file mode 100644 index 0000000..b8501c8 --- /dev/null +++ b/backend/server/adventures/signals.py @@ -0,0 +1,23 @@ +from django.db.models.signals import m2m_changed +from django.dispatch import receiver +from adventures.models import Adventure + +@receiver(m2m_changed, sender=Adventure.collections.through) +def update_adventure_publicity(sender, instance, action, **kwargs): + """ + Signal handler to update adventure publicity when collections are added/removed + """ + # Only process when collections are added or removed + if action in ('post_add', 'post_remove', 'post_clear'): + collections = instance.collections.all() + + if collections.exists(): + # If any collection is public, make the adventure public + has_public_collection = collections.filter(is_public=True).exists() + + if has_public_collection and not instance.is_public: + instance.is_public = True + instance.save(update_fields=['is_public']) + elif not has_public_collection and instance.is_public: + instance.is_public = False + instance.save(update_fields=['is_public']) diff --git a/backend/server/adventures/urls.py b/backend/server/adventures/urls.py index 1a98273..d1bf6cb 100644 --- a/backend/server/adventures/urls.py +++ b/backend/server/adventures/urls.py @@ -15,11 +15,10 @@ router.register(r'images', AdventureImageViewSet, basename='images') router.register(r'reverse-geocode', ReverseGeocodeViewSet, basename='reverse-geocode') router.register(r'categories', CategoryViewSet, basename='categories') router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-calendar') -router.register(r'overpass', OverpassViewSet, basename='overpass') router.register(r'search', GlobalSearchView, basename='search') router.register(r'attachments', AttachmentViewSet, basename='attachments') router.register(r'lodging', LodgingViewSet, basename='lodging') - +router.register(r'recommendations', RecommendationsViewSet, basename='recommendations') urlpatterns = [ # Include the router under the 'api/' prefix diff --git a/backend/server/adventures/utils/file_permissions.py b/backend/server/adventures/utils/file_permissions.py index 02971bc..b9a63f0 100644 --- a/backend/server/adventures/utils/file_permissions.py +++ b/backend/server/adventures/utils/file_permissions.py @@ -15,9 +15,12 @@ def checkFilePermission(fileId, user, mediaType): return True elif adventure.user_id == user: return True - elif adventure.collection: - if adventure.collection.shared_with.filter(id=user.id).exists(): - return True + elif adventure.collections.exists(): + # Check if the user is in any collection's shared_with list + for collection in adventure.collections.all(): + if collection.shared_with.filter(id=user.id).exists(): + return True + return False else: return False except AdventureImage.DoesNotExist: @@ -27,14 +30,18 @@ def checkFilePermission(fileId, user, mediaType): # Construct the full relative path to match the database field attachment_path = f"attachments/{fileId}" # Fetch the Attachment object - attachment = Attachment.objects.get(file=attachment_path).adventure - if attachment.is_public: + attachment = Attachment.objects.get(file=attachment_path) + adventure = attachment.adventure + if adventure.is_public: return True - elif attachment.user_id == user: + elif adventure.user_id == user: return True - elif attachment.collection: - if attachment.collection.shared_with.filter(id=user.id).exists(): - return True + elif adventure.collections.exists(): + # Check if the user is in any collection's shared_with list + for collection in adventure.collections.all(): + if collection.shared_with.filter(id=user.id).exists(): + return True + return False else: return False except Attachment.DoesNotExist: diff --git a/backend/server/adventures/views/__init__.py b/backend/server/adventures/views/__init__.py index 8f531f7..c9aedb0 100644 --- a/backend/server/adventures/views/__init__.py +++ b/backend/server/adventures/views/__init__.py @@ -7,10 +7,10 @@ from .collection_view import * from .generate_description_view import * from .ics_calendar_view import * from .note_view import * -from .overpass_view import * from .reverse_geocode_view import * from .stats_view import * from .transportation_view import * from .global_search_view import * from .attachment_view import * -from .lodging_view import * \ No newline at end of file +from .lodging_view import * +from .recommendations_view import * \ No newline at end of file diff --git a/backend/server/adventures/views/adventure_image_view.py b/backend/server/adventures/views/adventure_image_view.py index d76f6a5..ab7f8e1 100644 --- a/backend/server/adventures/views/adventure_image_view.py +++ b/backend/server/adventures/views/adventure_image_view.py @@ -3,9 +3,12 @@ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from django.db.models import Q +from django.core.files.base import ContentFile from adventures.models import Adventure, AdventureImage from adventures.serializers import AdventureImageSerializer +from integrations.models import ImmichIntegration import uuid +import requests class AdventureImageViewSet(viewsets.ModelViewSet): serializer_class = AdventureImageSerializer @@ -48,14 +51,92 @@ class AdventureImageViewSet(viewsets.ModelViewSet): return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND) if adventure.user_id != request.user: - # Check if the adventure has a collection - if adventure.collection: - # Check if the user is in the collection's shared_with list - if not adventure.collection.shared_with.filter(id=request.user.id).exists(): + # Check if the adventure has any collections + if adventure.collections.exists(): + # Check if the user is in the shared_with list of any of the adventure's collections + user_has_access = False + for collection in adventure.collections.all(): + if collection.shared_with.filter(id=request.user.id).exists(): + user_has_access = True + break + + if not user_has_access: return Response({"error": "User does not have permission to access this adventure"}, status=status.HTTP_403_FORBIDDEN) else: return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN) + # Handle Immich ID for shared users by downloading the image + if (request.user != adventure.user_id and + 'immich_id' in request.data and + request.data.get('immich_id')): + + immich_id = request.data.get('immich_id') + + # Get the shared user's Immich integration + try: + user_integration = ImmichIntegration.objects.get(user_id=request.user) + except ImmichIntegration.DoesNotExist: + return Response({ + "error": "No Immich integration found for your account. Please set up Immich integration first.", + "code": "immich_integration_not_found" + }, status=status.HTTP_400_BAD_REQUEST) + + # Download the image from the shared user's Immich server + try: + immich_response = requests.get( + f'{user_integration.server_url}/assets/{immich_id}/thumbnail?size=preview', + headers={'x-api-key': user_integration.api_key}, + timeout=10 + ) + immich_response.raise_for_status() + + # Create a temporary file with the downloaded content + content_type = immich_response.headers.get('Content-Type', 'image/jpeg') + if not content_type.startswith('image/'): + return Response({ + "error": "Invalid content type returned from Immich server.", + "code": "invalid_content_type" + }, status=status.HTTP_400_BAD_REQUEST) + + # Determine file extension from content type + ext_map = { + 'image/jpeg': '.jpg', + 'image/png': '.png', + 'image/webp': '.webp', + 'image/gif': '.gif' + } + file_ext = ext_map.get(content_type, '.jpg') + filename = f"immich_{immich_id}{file_ext}" + + # Create a Django ContentFile from the downloaded image + image_file = ContentFile(immich_response.content, name=filename) + + # Modify request data to use the downloaded image instead of immich_id + request_data = request.data.copy() + request_data.pop('immich_id', None) # Remove immich_id + request_data['image'] = image_file # Add the image file + + # Create the serializer with the modified data + serializer = self.get_serializer(data=request_data) + serializer.is_valid(raise_exception=True) + + # Save with the downloaded image + adventure = serializer.validated_data['adventure'] + serializer.save(user_id=adventure.user_id, image=image_file) + + return Response(serializer.data, status=status.HTTP_201_CREATED) + + except requests.exceptions.RequestException: + return Response({ + "error": f"Failed to fetch image from Immich server", + "code": "immich_fetch_failed" + }, status=status.HTTP_502_BAD_GATEWAY) + except Exception: + return Response({ + "error": f"Unexpected error processing Immich image", + "code": "immich_processing_error" + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return super().create(request, *args, **kwargs) def update(self, request, *args, **kwargs): @@ -110,15 +191,25 @@ class AdventureImageViewSet(viewsets.ModelViewSet): except ValueError: return Response({"error": "Invalid adventure ID"}, status=status.HTTP_400_BAD_REQUEST) + # Updated queryset to include images from adventures the user owns OR has shared access to queryset = AdventureImage.objects.filter( - Q(adventure__id=adventure_uuid) & Q(user_id=request.user) - ) + Q(adventure__id=adventure_uuid) & ( + Q(adventure__user_id=request.user) | # User owns the adventure + Q(adventure__collections__shared_with=request.user) # User has shared access via collection + ) + ).distinct() serializer = self.get_serializer(queryset, many=True, context={'request': request}) return Response(serializer.data) def get_queryset(self): - return AdventureImage.objects.filter(user_id=self.request.user) + # Updated to include images from adventures the user owns OR has shared access to + return AdventureImage.objects.filter( + Q(adventure__user_id=self.request.user) | # User owns the adventure + Q(adventure__collections__shared_with=self.request.user) # User has shared access via collection + ).distinct() def perform_create(self, serializer): - serializer.save(user_id=self.request.user) \ No newline at end of file + # Always set the image owner to the adventure owner, not the current user + adventure = serializer.validated_data['adventure'] + serializer.save(user_id=adventure.user_id) \ No newline at end of file diff --git a/backend/server/adventures/views/adventure_view.py b/backend/server/adventures/views/adventure_view.py index 2f7e1f1..b4bbfb9 100644 --- a/backend/server/adventures/views/adventure_view.py +++ b/backend/server/adventures/views/adventure_view.py @@ -3,70 +3,44 @@ from django.db import transaction from django.core.exceptions import PermissionDenied from django.db.models import Q, Max from django.db.models.functions import Lower -from rest_framework import viewsets +from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response +import requests + from adventures.models import Adventure, Category, Transportation, Lodging from adventures.permissions import IsOwnerOrSharedWithFullAccess from adventures.serializers import AdventureSerializer, TransportationSerializer, LodgingSerializer from adventures.utils import pagination + class AdventureViewSet(viewsets.ModelViewSet): + """ + ViewSet for managing Adventure objects with support for filtering, sorting, + and sharing functionality. + """ serializer_class = AdventureSerializer permission_classes = [IsOwnerOrSharedWithFullAccess] pagination_class = pagination.StandardResultsSetPagination - def apply_sorting(self, queryset): - order_by = self.request.query_params.get('order_by', 'updated_at') - order_direction = self.request.query_params.get('order_direction', 'asc') - include_collections = self.request.query_params.get('include_collections', 'true') - - valid_order_by = ['name', 'type', 'date', 'rating', 'updated_at'] - if order_by not in valid_order_by: - order_by = 'name' - - if order_direction not in ['asc', 'desc']: - order_direction = 'asc' - - if order_by == 'date': - queryset = queryset.annotate(latest_visit=Max('visits__start_date')).filter(latest_visit__isnull=False) - ordering = 'latest_visit' - elif order_by == 'name': - queryset = queryset.annotate(lower_name=Lower('name')) - ordering = 'lower_name' - elif order_by == 'rating': - queryset = queryset.filter(rating__isnull=False) - ordering = 'rating' - else: - ordering = order_by - - if order_direction == 'desc': - ordering = f'-{ordering}' - - if order_by == 'updated_at': - ordering = '-updated_at' if order_direction == 'asc' else 'updated_at' - - if include_collections == 'false': - queryset = queryset.filter(collection=None) - - return queryset.order_by(ordering) + # ==================== QUERYSET & PERMISSIONS ==================== def get_queryset(self): """ - Returns the queryset for the AdventureViewSet. Unauthenticated users can only - retrieve public adventures, while authenticated users can access their own, - shared, and public adventures depending on the action. + Returns queryset based on user authentication and action type. + Public actions allow unauthenticated access to public adventures. """ user = self.request.user + public_allowed_actions = {'retrieve', 'additional_info'} if not user.is_authenticated: - # Unauthenticated users can only access public adventures for retrieval - if self.action == 'retrieve': - return Adventure.objects.retrieve_adventures(user, include_public=True).order_by('-updated_at') + if self.action in public_allowed_actions: + return Adventure.objects.retrieve_adventures( + user, include_public=True + ).order_by('-updated_at') return Adventure.objects.none() - # Authenticated users: Handle retrieval separately - include_public = self.action == 'retrieve' + include_public = self.action in public_allowed_actions return Adventure.objects.retrieve_adventures( user, include_public=include_public, @@ -74,144 +48,294 @@ class AdventureViewSet(viewsets.ModelViewSet): include_shared=True ).order_by('-updated_at') + # ==================== SORTING & FILTERING ==================== + + def apply_sorting(self, queryset): + """Apply sorting and collection filtering to queryset.""" + order_by = self.request.query_params.get('order_by', 'updated_at') + order_direction = self.request.query_params.get('order_direction', 'asc') + include_collections = self.request.query_params.get('include_collections', 'true') + + # Validate parameters + valid_order_by = ['name', 'type', 'date', 'rating', 'updated_at'] + if order_by not in valid_order_by: + order_by = 'name' + + if order_direction not in ['asc', 'desc']: + order_direction = 'asc' + + # Apply sorting logic + queryset = self._apply_ordering(queryset, order_by, order_direction) + + # Filter adventures without collections if requested + if include_collections == 'false': + queryset = queryset.filter(collections__isnull=True) + + return queryset + + def _apply_ordering(self, queryset, order_by, order_direction): + """Apply ordering to queryset based on field type.""" + if order_by == 'date': + queryset = queryset.annotate( + latest_visit=Max('visits__start_date') + ).filter(latest_visit__isnull=False) + ordering = 'latest_visit' + elif order_by == 'name': + queryset = queryset.annotate(lower_name=Lower('name')) + ordering = 'lower_name' + elif order_by == 'rating': + queryset = queryset.filter(rating__isnull=False) + ordering = 'rating' + elif order_by == 'updated_at': + # Special handling for updated_at (reverse default order) + ordering = '-updated_at' if order_direction == 'asc' else 'updated_at' + return queryset.order_by(ordering) + else: + ordering = order_by + + # Apply direction + if order_direction == 'desc': + ordering = f'-{ordering}' + + return queryset.order_by(ordering) + + # ==================== CRUD OPERATIONS ==================== + + @transaction.atomic + def perform_create(self, serializer): + """Create adventure with collection validation and ownership logic.""" + collections = serializer.validated_data.get('collections', []) + + # Validate permissions for all collections + self._validate_collection_permissions(collections) + + # Determine what user to assign as owner + user_to_assign = self.request.user + + if collections: + # Use the current user as owner since ManyToMany allows multiple collection owners + user_to_assign = self.request.user + + serializer.save(user_id=user_to_assign) + def perform_update(self, serializer): - adventure = serializer.save() - if adventure.collection: - adventure.is_public = adventure.collection.is_public - adventure.save() + """Update adventure.""" + # Just save the adventure - the signal will handle publicity updates + serializer.save() + + def update(self, request, *args, **kwargs): + """Handle adventure updates with collection permission validation.""" + instance = self.get_object() + partial = kwargs.pop('partial', False) + + serializer = self.get_serializer(instance, data=request.data, partial=partial) + serializer.is_valid(raise_exception=True) + + # Validate collection permissions if collections are being updated + if 'collections' in serializer.validated_data: + self._validate_collection_update_permissions( + instance, serializer.validated_data['collections'] + ) + else: + # Remove collections from validated_data if not provided + serializer.validated_data.pop('collections', None) + + self.perform_update(serializer) + return Response(serializer.data) + + # ==================== CUSTOM ACTIONS ==================== @action(detail=False, methods=['get']) def filtered(self, request): + """Filter adventures by category types and visit status.""" types = request.query_params.get('types', '').split(',') - is_visited = request.query_params.get('is_visited', 'all') - + + # Handle 'all' types if 'all' in types: - types = Category.objects.filter(user_id=request.user).values_list('name', flat=True) + types = Category.objects.filter( + user_id=request.user + ).values_list('name', flat=True) else: + # Validate provided types if not types or not all( - Category.objects.filter(user_id=request.user, name=type).exists() for type in types + Category.objects.filter(user_id=request.user, name=type_name).exists() + for type_name in types ): - return Response({"error": "Invalid category or no types provided"}, status=400) + return Response( + {"error": "Invalid category or no types provided"}, + status=400 + ) + # Build base queryset queryset = Adventure.objects.filter( category__in=Category.objects.filter(name__in=types, user_id=request.user), user_id=request.user.id ) - is_visited_param = request.query_params.get('is_visited') - if is_visited_param is not None: - # Convert is_visited_param to a boolean - if is_visited_param.lower() == 'true': - is_visited_bool = True - elif is_visited_param.lower() == 'false': - is_visited_bool = False - else: - is_visited_bool = None - - # Filter logic: "visited" means at least one visit with start_date <= today - now = timezone.now().date() - if is_visited_bool is True: - queryset = queryset.filter(visits__start_date__lte=now).distinct() - elif is_visited_bool is False: - queryset = queryset.exclude(visits__start_date__lte=now).distinct() - + # Apply visit status filtering + queryset = self._apply_visit_filtering(queryset, request) queryset = self.apply_sorting(queryset) + return self.paginate_and_respond(queryset, request) @action(detail=False, methods=['get']) def all(self, request): + """Get all adventures (public and owned) with optional collection filtering.""" if not request.user.is_authenticated: return Response({"error": "User is not authenticated"}, status=400) include_collections = request.query_params.get('include_collections', 'false') == 'true' - queryset = Adventure.objects.filter( - Q(is_public=True) | Q(user_id=request.user.id), - collection=None if not include_collections else Q() - ) + + # Build queryset with collection filtering + base_filter = Q(user_id=request.user.id) + + if include_collections: + queryset = Adventure.objects.filter(base_filter) + else: + queryset = Adventure.objects.filter(base_filter, collections__isnull=True) queryset = self.apply_sorting(queryset) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) - def update(self, request, *args, **kwargs): - instance = self.get_object() - serializer = self.get_serializer(instance, data=request.data, partial=True) - serializer.is_valid(raise_exception=True) + @action(detail=True, methods=['get'], url_path='additional-info') + def additional_info(self, request, pk=None): + """Get adventure with additional sunrise/sunset information.""" + adventure = self.get_object() + user = request.user - new_collection = serializer.validated_data.get('collection') - if new_collection and new_collection!=instance.collection: - if new_collection.user_id != request.user or instance.user_id != request.user: - raise PermissionDenied("You do not have permission to use this collection.") - elif new_collection is None and instance.collection and instance.collection.user_id != request.user: - raise PermissionDenied("You cannot remove the collection as you are not the owner.") + # Validate access permissions + if not self._has_adventure_access(adventure, user): + return Response( + {"error": "User does not have permission to access this adventure"}, + status=status.HTTP_403_FORBIDDEN + ) - self.perform_update(serializer) - return Response(serializer.data) + # Get base adventure data + serializer = self.get_serializer(adventure) + response_data = serializer.data - @transaction.atomic - def perform_create(self, serializer): - collection = serializer.validated_data.get('collection') + # Add sunrise/sunset data + response_data['sun_times'] = self._get_sun_times(adventure, response_data.get('visits', [])) + + return Response(response_data) - if collection and not (collection.user_id == self.request.user or collection.shared_with.filter(id=self.request.user.id).exists()): - raise PermissionDenied("You do not have permission to use this collection.") - elif collection: - serializer.save(user_id=collection.user_id, is_public=collection.is_public) - return + # ==================== HELPER METHODS ==================== - serializer.save(user_id=self.request.user, is_public=collection.is_public if collection else False) + def _validate_collection_permissions(self, collections): + """Validate user has permission to use all provided collections. Only the owner or shared users can use collections.""" + for collection in collections: + if not (collection.user_id == self.request.user or + collection.shared_with.filter(uuid=self.request.user.uuid).exists()): + raise PermissionDenied( + f"You do not have permission to use collection '{collection.name}'." + ) + + def _validate_collection_update_permissions(self, instance, new_collections): + """Validate permissions for collection updates (add/remove).""" + # Check permissions for new collections being added + for collection in new_collections: + if (collection.user_id != self.request.user and + not collection.shared_with.filter(uuid=self.request.user.uuid).exists()): + raise PermissionDenied( + f"You do not have permission to use collection '{collection.name}'." + ) + + # Check permissions for collections being removed + current_collections = set(instance.collections.all()) + new_collections_set = set(new_collections) + collections_to_remove = current_collections - new_collections_set + + for collection in collections_to_remove: + if (collection.user_id != self.request.user and + not collection.shared_with.filter(uuid=self.request.user.uuid).exists()): + raise PermissionDenied( + f"You cannot remove the adventure from collection '{collection.name}' " + f"as you don't have permission." + ) + + def _apply_visit_filtering(self, queryset, request): + """Apply visit status filtering to queryset.""" + is_visited_param = request.query_params.get('is_visited') + if is_visited_param is None: + return queryset + + # Convert parameter to boolean + if is_visited_param.lower() == 'true': + is_visited_bool = True + elif is_visited_param.lower() == 'false': + is_visited_bool = False + else: + return queryset + + # Apply visit filtering + now = timezone.now().date() + if is_visited_bool: + queryset = queryset.filter(visits__start_date__lte=now).distinct() + else: + queryset = queryset.exclude(visits__start_date__lte=now).distinct() + + return queryset + + def _has_adventure_access(self, adventure, user): + """Check if user has access to adventure.""" + # Allow if public + if adventure.is_public: + return True + + # Check ownership + if user.is_authenticated and adventure.user_id == user: + return True + + # Check shared collection access + if user.is_authenticated: + for collection in adventure.collections.all(): + if collection.shared_with.filter(uuid=user.uuid).exists(): + return True + + return False + + def _get_sun_times(self, adventure, visits): + """Get sunrise/sunset times for adventure visits.""" + sun_times = [] + + for visit in visits: + date = visit.get('start_date') + if not (date and adventure.longitude and adventure.latitude): + continue + + api_url = ( + f'https://api.sunrisesunset.io/json?' + f'lat={adventure.latitude}&lng={adventure.longitude}&date={date}' + ) + + try: + response = requests.get(api_url) + if response.status_code == 200: + data = response.json() + results = data.get('results', {}) + + if results.get('sunrise') and results.get('sunset'): + sun_times.append({ + "date": date, + "visit_id": visit.get('id'), + "sunrise": results.get('sunrise'), + "sunset": results.get('sunset') + }) + except requests.RequestException: + # Skip this visit if API call fails + continue + + return sun_times def paginate_and_respond(self, queryset, request): + """Paginate queryset and return response.""" paginator = self.pagination_class() page = paginator.paginate_queryset(queryset, request) + if page is not None: serializer = self.get_serializer(page, many=True) return paginator.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) - - # @action(detail=True, methods=['post']) - # def convert(self, request, pk=None): - # """ - # Convert an Adventure instance into a Transportation or Lodging instance. - # Expects a JSON body with "target_type": "transportation" or "lodging". - # """ - # adventure = self.get_object() - # target_type = request.data.get("target_type", "").lower() - - # if target_type not in ["transportation", "lodging"]: - # return Response( - # {"error": "Invalid target type. Must be 'transportation' or 'lodging'."}, - # status=400 - # ) - # if not adventure.collection: - # return Response( - # {"error": "Adventure must be part of a collection to be converted."}, - # status=400 - # ) - - # # Define the overlapping fields that both the Adventure and target models share. - # overlapping_fields = ["name", "description", "is_public", 'collection'] - - # # Gather the overlapping data from the adventure instance. - # conversion_data = {} - # for field in overlapping_fields: - # if hasattr(adventure, field): - # conversion_data[field] = getattr(adventure, field) - - # # Make sure to include the user reference - # conversion_data["user_id"] = adventure.user_id - - # # Convert the adventure instance within an atomic transaction. - # with transaction.atomic(): - # if target_type == "transportation": - # new_instance = Transportation.objects.create(**conversion_data) - # serializer = TransportationSerializer(new_instance) - # else: # target_type == "lodging" - # new_instance = Lodging.objects.create(**conversion_data) - # serializer = LodgingSerializer(new_instance) - - # # Optionally, delete the original adventure to avoid duplicates. - # adventure.delete() - - # return Response(serializer.data) + return Response(serializer.data) \ No newline at end of file diff --git a/backend/server/adventures/views/attachment_view.py b/backend/server/adventures/views/attachment_view.py index 0292b16..2ca4770 100644 --- a/backend/server/adventures/views/attachment_view.py +++ b/backend/server/adventures/views/attachment_view.py @@ -26,10 +26,16 @@ class AttachmentViewSet(viewsets.ModelViewSet): return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND) if adventure.user_id != request.user: - # Check if the adventure has a collection - if adventure.collection: - # Check if the user is in the collection's shared_with list - if not adventure.collection.shared_with.filter(id=request.user.id).exists(): + # Check if the adventure has any collections + if adventure.collections.exists(): + # Check if the user is in the shared_with list of any of the adventure's collections + user_has_access = False + for collection in adventure.collections.all(): + if collection.shared_with.filter(id=request.user.id).exists(): + user_has_access = True + break + + if not user_has_access: return Response({"error": "User does not have permission to access this adventure"}, status=status.HTTP_403_FORBIDDEN) else: return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN) @@ -37,4 +43,14 @@ class AttachmentViewSet(viewsets.ModelViewSet): return super().create(request, *args, **kwargs) def perform_create(self, serializer): - serializer.save(user_id=self.request.user) \ No newline at end of file + adventure_id = self.request.data.get('adventure') + adventure = Adventure.objects.get(id=adventure_id) + + # If the adventure belongs to collections, set the owner to the collection owner + if adventure.collections.exists(): + # Get the first collection's owner (assuming all collections have the same owner) + collection = adventure.collections.first() + serializer.save(user_id=collection.user_id) + else: + # Otherwise, set the owner to the request user + serializer.save(user_id=self.request.user) \ No newline at end of file diff --git a/backend/server/adventures/views/collection_view.py b/backend/server/adventures/views/collection_view.py index f0529ee..40ebbd6 100644 --- a/backend/server/adventures/views/collection_view.py +++ b/backend/server/adventures/views/collection_view.py @@ -4,7 +4,7 @@ from django.db import transaction from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response -from adventures.models import Collection, Adventure, Transportation, Note +from adventures.models import Collection, Adventure, Transportation, Note, Checklist from adventures.permissions import CollectionShared from adventures.serializers import CollectionSerializer from users.models import CustomUser as User @@ -22,7 +22,7 @@ class CollectionViewSet(viewsets.ModelViewSet): order_by = self.request.query_params.get('order_by', 'name') order_direction = self.request.query_params.get('order_direction', 'asc') - valid_order_by = ['name', 'upated_at'] + valid_order_by = ['name', 'updated_at', 'start_date'] if order_by not in valid_order_by: order_by = 'updated_at' @@ -35,6 +35,12 @@ class CollectionViewSet(viewsets.ModelViewSet): ordering = 'lower_name' if order_direction == 'desc': ordering = f'-{ordering}' + elif order_by == 'start_date': + ordering = 'start_date' + if order_direction == 'asc': + ordering = 'start_date' + else: + ordering = '-start_date' else: order_by == 'updated_at' ordering = 'updated_at' @@ -49,7 +55,7 @@ class CollectionViewSet(viewsets.ModelViewSet): # make sure the user is authenticated if not request.user.is_authenticated: return Response({"error": "User is not authenticated"}, status=400) - queryset = Collection.objects.filter(user_id=request.user.id) + queryset = Collection.objects.filter(user_id=request.user.id, is_archived=False) queryset = self.apply_sorting(queryset) collections = self.paginate_and_respond(queryset, request) return collections @@ -100,23 +106,40 @@ class CollectionViewSet(viewsets.ModelViewSet): if 'is_public' in serializer.validated_data: new_public_status = serializer.validated_data['is_public'] - # if is_publuc has changed and the user is not the owner of the collection return an error + # if is_public has changed and the user is not the owner of the collection return an error if new_public_status != instance.is_public and instance.user_id != request.user: print(f"User {request.user.id} does not own the collection {instance.id} that is owned by {instance.user_id}") return Response({"error": "User does not own the collection"}, status=400) - # Update associated adventures to match the collection's is_public status - Adventure.objects.filter(collection=instance).update(is_public=new_public_status) + # Get all adventures in this collection + adventures_in_collection = Adventure.objects.filter(collections=instance) + + if new_public_status: + # If collection becomes public, make all adventures public + adventures_in_collection.update(is_public=True) + else: + # If collection becomes private, check each adventure + # Only set an adventure to private if ALL of its collections are private + # Collect adventures that do NOT belong to any other public collection (excluding the current one) + adventure_ids_to_set_private = [] - # do the same for transportations + for adventure in adventures_in_collection: + has_public_collection = adventure.collections.filter(is_public=True).exclude(id=instance.id).exists() + if not has_public_collection: + adventure_ids_to_set_private.append(adventure.id) + + # Bulk update those adventures + Adventure.objects.filter(id__in=adventure_ids_to_set_private).update(is_public=False) + + # Update transportations, notes, and checklists related to this collection + # These still use direct ForeignKey relationships Transportation.objects.filter(collection=instance).update(is_public=new_public_status) - - # do the same for notes Note.objects.filter(collection=instance).update(is_public=new_public_status) + Checklist.objects.filter(collection=instance).update(is_public=new_public_status) # Log the action (optional) action = "public" if new_public_status else "private" - print(f"Collection {instance.id} and its adventures were set to {action}") + print(f"Collection {instance.id} and its related objects were set to {action}") self.perform_update(serializer) @@ -203,7 +226,6 @@ class CollectionViewSet(viewsets.ModelViewSet): (Q(user_id=self.request.user.id) | Q(shared_with=self.request.user)) & Q(is_archived=False) ).distinct() - def perform_create(self, serializer): # This is ok because you cannot share a collection when creating it serializer.save(user_id=self.request.user) diff --git a/backend/server/adventures/views/overpass_view.py b/backend/server/adventures/views/overpass_view.py deleted file mode 100644 index a72b4a7..0000000 --- a/backend/server/adventures/views/overpass_view.py +++ /dev/null @@ -1,183 +0,0 @@ -from rest_framework import viewsets -from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -import requests - -class OverpassViewSet(viewsets.ViewSet): - permission_classes = [IsAuthenticated] - BASE_URL = "https://overpass-api.de/api/interpreter" - HEADERS = {'User-Agent': 'AdventureLog Server'} - - def make_overpass_query(self, query): - """ - Sends a query to the Overpass API and returns the response data. - Args: - query (str): The Overpass QL query string. - Returns: - dict: Parsed JSON response from the Overpass API. - Raises: - Response: DRF Response object with an error message in case of failure. - """ - url = f"{self.BASE_URL}?data={query}" - try: - response = requests.get(url, headers=self.HEADERS) - response.raise_for_status() # Raise an exception for HTTP errors - return response.json() - except requests.exceptions.RequestException: - return Response({"error": "Failed to connect to Overpass API"}, status=500) - except requests.exceptions.JSONDecodeError: - return Response({"error": "Invalid response from Overpass API"}, status=400) - - def parse_overpass_response(self, data, request): - """ - Parses the JSON response from the Overpass API and extracts relevant data, - turning it into an adventure-structured object. - - Args: - response (dict): The JSON response from the Overpass API. - - Returns: - list: A list of adventure objects with structured data. - """ - # Extract elements (nodes/ways/relations) from the response - nodes = data.get('elements', []) - adventures = [] - - # include all entries, even the ones that do not have lat long - all = request.query_params.get('all', False) - - for node in nodes: - # Ensure we are working with a "node" type (can also handle "way" or "relation" if needed) - if node.get('type') not in ['node', 'way', 'relation']: - continue - - # Extract tags and general data - tags = node.get('tags', {}) - adventure = { - "id": node.get('id'), # Include the unique OSM ID - "type": node.get('type'), # Type of element (node, way, relation) - "name": tags.get('name', tags.get('official_name', '')), # Fallback to 'official_name' - "description": tags.get('description', None), # Additional descriptive information - "latitude": node.get('lat', None), # Use None for consistency with missing values - "longitude": node.get('lon', None), - "address": { - "city": tags.get('addr:city', None), - "housenumber": tags.get('addr:housenumber', None), - "postcode": tags.get('addr:postcode', None), - "state": tags.get('addr:state', None), - "street": tags.get('addr:street', None), - "country": tags.get('addr:country', None), # Add 'country' if available - "suburb": tags.get('addr:suburb', None), # Add 'suburb' for more granularity - }, - "feature_id": tags.get('gnis:feature_id', None), - "tag": next((tags.get(key, None) for key in ['leisure', 'tourism', 'natural', 'historic', 'amenity'] if key in tags), None), - "contact": { - "phone": tags.get('phone', None), - "email": tags.get('contact:email', None), - "website": tags.get('website', None), - "facebook": tags.get('contact:facebook', None), # Social media links - "twitter": tags.get('contact:twitter', None), - }, - # "tags": tags, # Include all raw tags for future use - } - - # Filter out adventures with no name, latitude, or longitude - if (adventure["name"] and - adventure["latitude"] is not None and -90 <= adventure["latitude"] <= 90 and - adventure["longitude"] is not None and -180 <= adventure["longitude"] <= 180) or all: - adventures.append(adventure) - - return adventures - - - @action(detail=False, methods=['get']) - def query(self, request): - """ - Radius-based search for tourism-related locations around given coordinates. - """ - lat = request.query_params.get('lat') - lon = request.query_params.get('lon') - radius = request.query_params.get('radius', '1000') # Default radius: 1000 meters - - valid_categories = ['lodging', 'food', 'tourism'] - category = request.query_params.get('category', 'all') - if category not in valid_categories: - return Response({"error": f"Invalid category. Valid categories: {', '.join(valid_categories)}"}, status=400) - - if category == 'tourism': - query = f""" - [out:json]; - ( - node(around:{radius},{lat},{lon})["tourism"]; - node(around:{radius},{lat},{lon})["leisure"]; - node(around:{radius},{lat},{lon})["historic"]; - node(around:{radius},{lat},{lon})["sport"]; - node(around:{radius},{lat},{lon})["natural"]; - node(around:{radius},{lat},{lon})["attraction"]; - node(around:{radius},{lat},{lon})["museum"]; - node(around:{radius},{lat},{lon})["zoo"]; - node(around:{radius},{lat},{lon})["aquarium"]; - ); - out; - """ - if category == 'lodging': - query = f""" - [out:json]; - ( - node(around:{radius},{lat},{lon})["tourism"="hotel"]; - node(around:{radius},{lat},{lon})["tourism"="motel"]; - node(around:{radius},{lat},{lon})["tourism"="guest_house"]; - node(around:{radius},{lat},{lon})["tourism"="hostel"]; - node(around:{radius},{lat},{lon})["tourism"="camp_site"]; - node(around:{radius},{lat},{lon})["tourism"="caravan_site"]; - node(around:{radius},{lat},{lon})["tourism"="chalet"]; - node(around:{radius},{lat},{lon})["tourism"="alpine_hut"]; - node(around:{radius},{lat},{lon})["tourism"="apartment"]; - ); - out; - """ - if category == 'food': - query = f""" - [out:json]; - ( - node(around:{radius},{lat},{lon})["amenity"="restaurant"]; - node(around:{radius},{lat},{lon})["amenity"="cafe"]; - node(around:{radius},{lat},{lon})["amenity"="fast_food"]; - node(around:{radius},{lat},{lon})["amenity"="pub"]; - node(around:{radius},{lat},{lon})["amenity"="bar"]; - node(around:{radius},{lat},{lon})["amenity"="food_court"]; - node(around:{radius},{lat},{lon})["amenity"="ice_cream"]; - node(around:{radius},{lat},{lon})["amenity"="bakery"]; - node(around:{radius},{lat},{lon})["amenity"="confectionery"]; - ); - out; - """ - - # Validate required parameters - if not lat or not lon: - return Response( - {"error": "Latitude and longitude parameters are required."}, status=400 - ) - - data = self.make_overpass_query(query) - adventures = self.parse_overpass_response(data, request) - return Response(adventures) - - @action(detail=False, methods=['get'], url_path='search') - def search(self, request): - """ - Name-based search for nodes with the specified name. - """ - name = request.query_params.get('name') - - # Validate required parameter - if not name: - return Response({"error": "Name parameter is required."}, status=400) - - # Construct Overpass API query - query = f'[out:json];node["name"~"{name}",i];out;' - data = self.make_overpass_query(query) - - adventures = self.parse_overpass_response(data, request) - return Response(adventures) diff --git a/backend/server/adventures/views/recommendations_view.py b/backend/server/adventures/views/recommendations_view.py new file mode 100644 index 0000000..e759f9c --- /dev/null +++ b/backend/server/adventures/views/recommendations_view.py @@ -0,0 +1,258 @@ +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from django.conf import settings +import requests +from geopy.distance import geodesic +import time + + +class RecommendationsViewSet(viewsets.ViewSet): + permission_classes = [IsAuthenticated] + BASE_URL = "https://overpass-api.de/api/interpreter" + HEADERS = {'User-Agent': 'AdventureLog Server'} + + def parse_google_places(self, places, origin): + adventures = [] + + for place in places: + location = place.get('location', {}) + types = place.get('types', []) + + # Updated for new API response structure + formatted_address = place.get("formattedAddress") or place.get("shortFormattedAddress") + display_name = place.get("displayName", {}) + name = display_name.get("text") if isinstance(display_name, dict) else display_name + + lat = location.get('latitude') + lon = location.get('longitude') + + if not name or not lat or not lon: + continue + + distance_km = geodesic(origin, (lat, lon)).km + + adventure = { + "id": place.get('id'), + "type": 'place', + "name": name, + "description": place.get('businessStatus', None), + "latitude": lat, + "longitude": lon, + "address": formatted_address, + "tag": types[0] if types else None, + "distance_km": round(distance_km, 2), + } + + adventures.append(adventure) + + # Sort by distance ascending + adventures.sort(key=lambda x: x["distance_km"]) + + return adventures + + def parse_overpass_response(self, data, request): + nodes = data.get('elements', []) + adventures = [] + all = request.query_params.get('all', False) + + origin = None + try: + origin = ( + float(request.query_params.get('lat')), + float(request.query_params.get('lon')) + ) + except(ValueError, TypeError): + origin = None + + for node in nodes: + if node.get('type') not in ['node', 'way', 'relation']: + continue + + tags = node.get('tags', {}) + lat = node.get('lat') + lon = node.get('lon') + name = tags.get('name', tags.get('official_name', '')) + + if not name or lat is None or lon is None: + if not all: + continue + + # Flatten address + address_parts = [tags.get(f'addr:{k}') for k in ['housenumber', 'street', 'suburb', 'city', 'state', 'postcode', 'country']] + formatted_address = ", ".join(filter(None, address_parts)) or name + + # Calculate distance if possible + distance_km = None + if origin: + distance_km = round(geodesic(origin, (lat, lon)).km, 2) + + # Unified format + adventure = { + "id": f"osm:{node.get('id')}", + "type": "place", + "name": name, + "description": tags.get('description'), + "latitude": lat, + "longitude": lon, + "address": formatted_address, + "tag": next((tags.get(key) for key in ['leisure', 'tourism', 'natural', 'historic', 'amenity'] if key in tags), None), + "distance_km": distance_km, + "powered_by": "osm" + } + + adventures.append(adventure) + + # Sort by distance if available + if origin: + adventures.sort(key=lambda x: x.get("distance_km") or float("inf")) + + return adventures + + + def query_overpass(self, lat, lon, radius, category, request): + if category == 'tourism': + query = f""" + [out:json]; + ( + node(around:{radius},{lat},{lon})["tourism"]; + node(around:{radius},{lat},{lon})["leisure"]; + node(around:{radius},{lat},{lon})["historic"]; + node(around:{radius},{lat},{lon})["sport"]; + node(around:{radius},{lat},{lon})["natural"]; + node(around:{radius},{lat},{lon})["attraction"]; + node(around:{radius},{lat},{lon})["museum"]; + node(around:{radius},{lat},{lon})["zoo"]; + node(around:{radius},{lat},{lon})["aquarium"]; + ); + out; + """ + elif category == 'lodging': + query = f""" + [out:json]; + ( + node(around:{radius},{lat},{lon})["tourism"="hotel"]; + node(around:{radius},{lat},{lon})["tourism"="motel"]; + node(around:{radius},{lat},{lon})["tourism"="guest_house"]; + node(around:{radius},{lat},{lon})["tourism"="hostel"]; + node(around:{radius},{lat},{lon})["tourism"="camp_site"]; + node(around:{radius},{lat},{lon})["tourism"="caravan_site"]; + node(around:{radius},{lat},{lon})["tourism"="chalet"]; + node(around:{radius},{lat},{lon})["tourism"="alpine_hut"]; + node(around:{radius},{lat},{lon})["tourism"="apartment"]; + ); + out; + """ + elif category == 'food': + query = f""" + [out:json]; + ( + node(around:{radius},{lat},{lon})["amenity"="restaurant"]; + node(around:{radius},{lat},{lon})["amenity"="cafe"]; + node(around:{radius},{lat},{lon})["amenity"="fast_food"]; + node(around:{radius},{lat},{lon})["amenity"="pub"]; + node(around:{radius},{lat},{lon})["amenity"="bar"]; + node(around:{radius},{lat},{lon})["amenity"="food_court"]; + node(around:{radius},{lat},{lon})["amenity"="ice_cream"]; + node(around:{radius},{lat},{lon})["amenity"="bakery"]; + node(around:{radius},{lat},{lon})["amenity"="confectionery"]; + ); + out; + """ + else: + return Response({"error": "Invalid category."}, status=400) + + overpass_url = f"{self.BASE_URL}?data={query}" + try: + response = requests.get(overpass_url, headers=self.HEADERS) + response.raise_for_status() + data = response.json() + except Exception as e: + print("Overpass API error:", e) + return Response({"error": "Failed to retrieve data from Overpass API."}, status=500) + + adventures = self.parse_overpass_response(data, request) + return Response(adventures) + + def query_google_nearby(self, lat, lon, radius, category, request): + """Query Google Places API (New) for nearby places""" + api_key = settings.GOOGLE_MAPS_API_KEY + + # Updated to use new Places API endpoint + url = "https://places.googleapis.com/v1/places:searchNearby" + + 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,places.businessStatus,places.id' + } + + # Map categories to place types for the new API + type_mapping = { + 'lodging': 'lodging', + 'food': 'restaurant', + 'tourism': 'tourist_attraction', + } + + payload = { + "includedTypes": [type_mapping[category]], + "maxResultCount": 20, + "locationRestriction": { + "circle": { + "center": { + "latitude": float(lat), + "longitude": float(lon) + }, + "radius": float(radius) + } + } + } + + try: + response = requests.post(url, json=payload, headers=headers, timeout=10) + response.raise_for_status() + data = response.json() + + places = data.get('places', []) + origin = (float(lat), float(lon)) + adventures = self.parse_google_places(places, origin) + + return Response(adventures) + + except requests.exceptions.RequestException as e: + print(f"Google Places API error: {e}") + # Fallback to Overpass API + return self.query_overpass(lat, lon, radius, category, request) + except Exception as e: + print(f"Unexpected error with Google Places API: {e}") + # Fallback to Overpass API + return self.query_overpass(lat, lon, radius, category, request) + + @action(detail=False, methods=['get']) + def query(self, request): + lat = request.query_params.get('lat') + lon = request.query_params.get('lon') + radius = request.query_params.get('radius', '1000') + category = request.query_params.get('category', 'all') + + if not lat or not lon: + return Response({"error": "Latitude and longitude parameters are required."}, status=400) + + valid_categories = { + 'lodging': 'lodging', + 'food': 'restaurant', + 'tourism': 'tourist_attraction', + } + + if category not in valid_categories: + return Response({"error": f"Invalid category. Valid categories: {', '.join(valid_categories)}"}, status=400) + + api_key = getattr(settings, 'GOOGLE_MAPS_API_KEY', None) + + # Fallback to Overpass if no API key configured + if not api_key: + return self.query_overpass(lat, lon, radius, category, request) + + # Use the new Google Places API + return self.query_google_nearby(lat, lon, radius, category, request) \ No newline at end of file diff --git a/backend/server/adventures/views/reverse_geocode_view.py b/backend/server/adventures/views/reverse_geocode_view.py index 4dc1d6b..df45f13 100644 --- a/backend/server/adventures/views/reverse_geocode_view.py +++ b/backend/server/adventures/views/reverse_geocode_view.py @@ -6,77 +6,44 @@ from worldtravel.models import Region, City, VisitedRegion, VisitedCity from adventures.models import Adventure from adventures.serializers import AdventureSerializer import requests +from adventures.geocoding import reverse_geocode +from adventures.geocoding import extractIsoCode +from django.conf import settings +from adventures.geocoding import search_google, search_osm class ReverseGeocodeViewSet(viewsets.ViewSet): permission_classes = [IsAuthenticated] - def extractIsoCode(self, data): - """ - Extract the ISO code from the response data. - Returns a dictionary containing the region name, country name, and ISO code if found. - """ - iso_code = None - town_city_or_county = None - display_name = None - country_code = None - city = None - visited_city = None - location_name = None - - # town = None - # city = None - # county = None - - if 'name' in data.keys(): - location_name = data['name'] - - if 'address' in data.keys(): - keys = data['address'].keys() - for key in keys: - if key.find("ISO") != -1: - iso_code = data['address'][key] - if 'town' in keys: - town_city_or_county = data['address']['town'] - if 'county' in keys: - town_city_or_county = data['address']['county'] - if 'city' in keys: - town_city_or_county = data['address']['city'] - if not iso_code: - return {"error": "No region found"} - - region = Region.objects.filter(id=iso_code).first() - visited_region = VisitedRegion.objects.filter(region=region, user_id=self.request.user).first() - - region_visited = False - city_visited = False - country_code = iso_code[:2] - - if region: - if town_city_or_county: - display_name = f"{town_city_or_county}, {region.name}, {country_code}" - city = City.objects.filter(name__contains=town_city_or_county, region=region).first() - visited_city = VisitedCity.objects.filter(city=city, user_id=self.request.user).first() - - if visited_region: - region_visited = True - if visited_city: - city_visited = True - if region: - return {"region_id": iso_code, "region": region.name, "country": region.country.name, "region_visited": region_visited, "display_name": display_name, "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited, 'location_name': location_name} - return {"error": "No region found"} - @action(detail=False, methods=['get']) def reverse_geocode(self, request): lat = request.query_params.get('lat', '') lon = request.query_params.get('lon', '') - url = f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}" - headers = {'User-Agent': 'AdventureLog Server'} - response = requests.get(url, headers=headers) + if not lat or not lon: + return Response({"error": "Latitude and longitude are required"}, status=400) try: - data = response.json() - except requests.exceptions.JSONDecodeError: - return Response({"error": "Invalid response from geocoding service"}, status=400) - return Response(self.extractIsoCode(data)) + 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) + return Response(data) + + @action(detail=False, methods=['get']) + def search(self, request): + query = request.query_params.get('query', '') + 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) + else: + results = search_osm(query) + return Response(results) + except Exception: + return Response({"error": "An internal error occurred while processing the request"}, status=500) @action(detail=False, methods=['post']) def mark_visited_region(self, request): @@ -93,16 +60,15 @@ class ReverseGeocodeViewSet(viewsets.ViewSet): lon = adventure.longitude if not lat or not lon: continue - url = f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}" - headers = {'User-Agent': 'AdventureLog Server'} - response = requests.get(url, headers=headers) - try: - data = response.json() - except requests.exceptions.JSONDecodeError: - return Response({"error": "Invalid response from geocoding service"}, status=400) - extracted_region = self.extractIsoCode(data) - if 'error' not in extracted_region: - region = Region.objects.filter(id=extracted_region['region_id']).first() + + # Use the existing reverse_geocode function which handles both Google and OSM + data = reverse_geocode(lat, lon, self.request.user) + if 'error' in data: + continue + + # data already contains region_id and city_id + if 'region_id' in data and data['region_id'] is not None: + region = Region.objects.filter(id=data['region_id']).first() visited_region = VisitedRegion.objects.filter(region=region, user_id=self.request.user).first() if not visited_region: visited_region = VisitedRegion(region=region, user_id=self.request.user) @@ -110,12 +76,12 @@ class ReverseGeocodeViewSet(viewsets.ViewSet): new_region_count += 1 new_regions[region.id] = region.name - if extracted_region['city_id'] is not None: - city = City.objects.filter(id=extracted_region['city_id']).first() - visited_city = VisitedCity.objects.filter(city=city, user_id=self.request.user).first() - if not visited_city: - visited_city = VisitedCity(city=city, user_id=self.request.user) - visited_city.save() - new_city_count += 1 - new_cities[city.id] = city.name + if 'city_id' in data and data['city_id'] is not None: + city = City.objects.filter(id=data['city_id']).first() + visited_city = VisitedCity.objects.filter(city=city, user_id=self.request.user).first() + if not visited_city: + visited_city = VisitedCity(city=city, user_id=self.request.user) + visited_city.save() + new_city_count += 1 + new_cities[city.id] = city.name return Response({"new_regions": new_region_count, "regions": new_regions, "new_cities": new_city_count, "cities": new_cities}) \ No newline at end of file diff --git a/backend/server/adventures/views/stats_view.py b/backend/server/adventures/views/stats_view.py index 4b3a524..8b56f26 100644 --- a/backend/server/adventures/views/stats_view.py +++ b/backend/server/adventures/views/stats_view.py @@ -14,7 +14,7 @@ class StatsViewSet(viewsets.ViewSet): """ A simple ViewSet for listing the stats of a user. """ - @action(detail=False, methods=['get'], url_path='counts/(?P[\w.@+-]+)') + @action(detail=False, methods=['get'], url_path=r'counts/(?P[\w.@+-]+)') def counts(self, request, username): if request.user.username == username: user = get_object_or_404(User, username=username) diff --git a/backend/server/integrations/migrations/0002_immichintegration_copy_locally.py b/backend/server/integrations/migrations/0002_immichintegration_copy_locally.py new file mode 100644 index 0000000..cdd59cc --- /dev/null +++ b/backend/server/integrations/migrations/0002_immichintegration_copy_locally.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.1 on 2025-06-01 21:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('integrations', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='immichintegration', + name='copy_locally', + field=models.BooleanField(default=True, help_text='Copy image to local storage, instead of just linking to the remote URL.'), + ), + ] diff --git a/backend/server/integrations/models.py b/backend/server/integrations/models.py index 9db8a07..7b0400a 100644 --- a/backend/server/integrations/models.py +++ b/backend/server/integrations/models.py @@ -9,6 +9,7 @@ class ImmichIntegration(models.Model): api_key = models.CharField(max_length=255) user = models.ForeignKey( User, on_delete=models.CASCADE) + copy_locally = models.BooleanField(default=True, help_text="Copy image to local storage, instead of just linking to the remote URL.") id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) def __str__(self): diff --git a/backend/server/integrations/views.py b/backend/server/integrations/views.py index dbf383a..e5540b9 100644 --- a/backend/server/integrations/views.py +++ b/backend/server/integrations/views.py @@ -1,13 +1,19 @@ import os from rest_framework.response import Response from rest_framework import viewsets, status - from .serializers import ImmichIntegrationSerializer from .models import ImmichIntegration from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated import requests from rest_framework.pagination import PageNumberPagination +from django.conf import settings +from adventures.models import AdventureImage +from django.http import HttpResponse +from django.shortcuts import get_object_or_404 +import logging + +logger = logging.getLogger(__name__) class IntegrationView(viewsets.ViewSet): permission_classes = [IsAuthenticated] @@ -16,15 +22,16 @@ class IntegrationView(viewsets.ViewSet): RESTful GET method for listing all integrations. """ immich_integrations = ImmichIntegration.objects.filter(user=request.user) + google_map_integration = settings.GOOGLE_MAPS_API_KEY != '' return Response( { - 'immich': immich_integrations.exists() + 'immich': immich_integrations.exists(), + 'google_maps': google_map_integration }, status=status.HTTP_200_OK ) - class StandardResultsSetPagination(PageNumberPagination): page_size = 25 page_size_query_param = 'page_size' @@ -33,13 +40,24 @@ class StandardResultsSetPagination(PageNumberPagination): class ImmichIntegrationView(viewsets.ViewSet): permission_classes = [IsAuthenticated] pagination_class = StandardResultsSetPagination + def check_integration(self, request): """ Checks if the user has an active Immich integration. Returns: - - None if the integration exists. + - The integration object if it exists. - A Response with an error message if the integration is missing. """ + if not request.user.is_authenticated: + return Response( + { + 'message': 'You need to be authenticated to use this feature.', + 'error': True, + 'code': 'immich.authentication_required' + }, + status=status.HTTP_403_FORBIDDEN + ) + user_integrations = ImmichIntegration.objects.filter(user=request.user) if not user_integrations.exists(): return Response( @@ -50,7 +68,8 @@ class ImmichIntegrationView(viewsets.ViewSet): }, status=status.HTTP_403_FORBIDDEN ) - return ImmichIntegration.objects.first() + + return user_integrations.first() @action(detail=False, methods=['get'], url_path='search') def search(self, request): @@ -61,7 +80,7 @@ class ImmichIntegrationView(viewsets.ViewSet): integration = self.check_integration(request) if isinstance(integration, Response): return integration - + query = request.query_params.get('query', '') date = request.query_params.get('date', '') @@ -74,12 +93,30 @@ class ImmichIntegrationView(viewsets.ViewSet): }, status=status.HTTP_400_BAD_REQUEST ) - + arguments = {} if query: arguments['query'] = query if date: - arguments['takenBefore'] = date + # Create date range for the entire selected day + from datetime import datetime, timedelta + try: + # Parse the date and create start/end of day + selected_date = datetime.strptime(date, '%Y-%m-%d') + start_of_day = selected_date.strftime('%Y-%m-%d') + end_of_day = (selected_date + timedelta(days=1)).strftime('%Y-%m-%d') + + arguments['takenAfter'] = start_of_day + arguments['takenBefore'] = end_of_day + except ValueError: + return Response( + { + 'message': 'Invalid date format. Use YYYY-MM-DD.', + 'error': True, + 'code': 'immich.invalid_date_format' + }, + status=status.HTTP_400_BAD_REQUEST + ) # check so if the server is down, it does not tweak out like a madman and crash the server with a 500 error code try: @@ -99,14 +136,14 @@ class ImmichIntegrationView(viewsets.ViewSet): }, status=status.HTTP_503_SERVICE_UNAVAILABLE ) - + if 'assets' in res and 'items' in res['assets']: paginator = self.pagination_class() # for each item in the items, we need to add the image url to the item so we can display it in the frontend public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/') public_url = public_url.replace("'", "") for item in res['assets']['items']: - item['image_url'] = f'{public_url}/api/integrations/immich/get/{item["id"]}' + item['image_url'] = f'{public_url}/api/integrations/immich/{integration.id}/get/{item["id"]}' result_page = paginator.paginate_queryset(res['assets']['items'], request) return paginator.get_paginated_response(result_page) else: @@ -118,44 +155,6 @@ class ImmichIntegrationView(viewsets.ViewSet): }, status=status.HTTP_404_NOT_FOUND ) - - @action(detail=False, methods=['get'], url_path='get/(?P[^/.]+)') - def get(self, request, imageid=None): - """ - RESTful GET method for retrieving a specific Immich image by ID. - """ - # Check for integration before proceeding - integration = self.check_integration(request) - if isinstance(integration, Response): - return integration - - if not imageid: - return Response( - { - 'message': 'Image ID is required.', - 'error': True, - 'code': 'immich.imageid_required' - }, - status=status.HTTP_400_BAD_REQUEST - ) - - # check so if the server is down, it does not tweak out like a madman and crash the server with a 500 error code - try: - immich_fetch = requests.get(f'{integration.server_url}/assets/{imageid}/thumbnail?size=preview', headers={ - 'x-api-key': integration.api_key - }) - # should return the image file - from django.http import HttpResponse - return HttpResponse(immich_fetch.content, content_type='image/jpeg', status=status.HTTP_200_OK) - except requests.exceptions.ConnectionError: - return Response( - { - 'message': 'The Immich server is currently down or unreachable.', - 'error': True, - 'code': 'immich.server_down' - }, - status=status.HTTP_503_SERVICE_UNAVAILABLE - ) @action(detail=False, methods=['get']) def albums(self, request): @@ -187,7 +186,7 @@ class ImmichIntegrationView(viewsets.ViewSet): res, status=status.HTTP_200_OK ) - + @action(detail=False, methods=['get'], url_path='albums/(?P[^/.]+)') def album(self, request, albumid=None): """ @@ -195,6 +194,7 @@ class ImmichIntegrationView(viewsets.ViewSet): """ # Check for integration before proceeding integration = self.check_integration(request) + print(integration.user) if isinstance(integration, Response): return integration @@ -230,7 +230,7 @@ class ImmichIntegrationView(viewsets.ViewSet): public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/') public_url = public_url.replace("'", "") for item in res['assets']: - item['image_url'] = f'{public_url}/api/integrations/immich/get/{item["id"]}' + item['image_url'] = f'{public_url}/api/integrations/immich/{integration.id}/get/{item["id"]}' result_page = paginator.paginate_queryset(res['assets'], request) return paginator.get_paginated_response(result_page) else: @@ -243,6 +243,117 @@ class ImmichIntegrationView(viewsets.ViewSet): status=status.HTTP_404_NOT_FOUND ) + @action( + detail=False, + methods=['get'], + url_path='(?P[^/.]+)/get/(?P[^/.]+)', + permission_classes=[] + ) + def get_by_integration(self, request, integration_id=None, imageid=None): + """ + GET an Immich image using the integration and asset ID. + Access levels (in order of priority): + 1. Public adventures: accessible by anyone + 2. Private adventures in public collections: accessible by anyone + 3. Private adventures in private collections shared with user: accessible by shared users + 4. Private adventures: accessible only to the owner + 5. No AdventureImage: owner can still view via integration + """ + if not imageid or not integration_id: + return Response({ + 'message': 'Image ID and Integration ID are required.', + 'error': True, + 'code': 'immich.missing_params' + }, status=status.HTTP_400_BAD_REQUEST) + + # Lookup integration and user + integration = get_object_or_404(ImmichIntegration, id=integration_id) + owner_id = integration.user_id + + # Try to find the image entry with collections and sharing information + image_entry = ( + AdventureImage.objects + .filter(immich_id=imageid, user_id=owner_id) + .select_related('adventure') + .prefetch_related('adventure__collections', 'adventure__collections__shared_with') + .order_by('-adventure__is_public') # Public adventures first + .first() + ) + + # Access control + if image_entry: + adventure = image_entry.adventure + collections = adventure.collections.all() + + # Determine access level + is_authorized = False + + # Level 1: Public adventure (highest priority) + if adventure.is_public: + is_authorized = True + + # Level 2: Private adventure in any public collection + elif any(collection.is_public for collection in collections): + is_authorized = True + + # Level 3: Owner access + elif request.user.is_authenticated and request.user.id == owner_id: + is_authorized = True + + # Level 4: Shared collection access - check if user has access to any collection + elif (request.user.is_authenticated and + any(collection.shared_with.filter(id=request.user.id).exists() + for collection in collections)): + is_authorized = True + + if not is_authorized: + return Response({ + 'message': 'This image belongs to a private adventure and you are not authorized.', + 'error': True, + 'code': 'immich.permission_denied' + }, status=status.HTTP_403_FORBIDDEN) + else: + # No AdventureImage exists; allow only the integration owner + if not request.user.is_authenticated or request.user.id != owner_id: + return Response({ + 'message': 'Image is not linked to any adventure and you are not the owner.', + 'error': True, + 'code': 'immich.not_found' + }, status=status.HTTP_404_NOT_FOUND) + + # Fetch from Immich + try: + immich_response = requests.get( + f'{integration.server_url}/assets/{imageid}/thumbnail?size=preview', + headers={'x-api-key': integration.api_key}, + timeout=5 + ) + content_type = immich_response.headers.get('Content-Type', 'image/jpeg') + if not content_type.startswith('image/'): + return Response({ + 'message': 'Invalid content type returned from Immich.', + 'error': True, + 'code': 'immich.invalid_content' + }, status=status.HTTP_502_BAD_GATEWAY) + + response = HttpResponse(immich_response.content, content_type=content_type, status=200) + response['Cache-Control'] = 'public, max-age=86400, stale-while-revalidate=3600' + return response + + except requests.exceptions.ConnectionError: + return Response({ + 'message': 'The Immich server is unreachable.', + 'error': True, + 'code': 'immich.server_down' + }, status=status.HTTP_503_SERVICE_UNAVAILABLE) + + except requests.exceptions.Timeout: + return Response({ + 'message': 'The Immich server request timed out.', + 'error': True, + 'code': 'immich.timeout' + }, status=status.HTTP_504_GATEWAY_TIMEOUT) + class ImmichIntegrationViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] serializer_class = ImmichIntegrationSerializer @@ -251,11 +362,78 @@ class ImmichIntegrationViewSet(viewsets.ModelViewSet): def get_queryset(self): return ImmichIntegration.objects.filter(user=self.request.user) + def _validate_immich_connection(self, server_url, api_key): + """ + Validate connection to Immich server before saving integration. + Returns tuple: (is_valid, corrected_server_url, error_message) + """ + if not server_url or not api_key: + return False, server_url, "Server URL and API key are required" + + # Ensure server_url has proper format + if not server_url.startswith(('http://', 'https://')): + server_url = f"https://{server_url}" + + # Remove trailing slash if present + original_server_url = server_url.rstrip('/') + + # Try both with and without /api prefix + test_configs = [ + (original_server_url, f"{original_server_url}/users/me"), + (f"{original_server_url}/api", f"{original_server_url}/api/users/me") + ] + + headers = { + 'X-API-Key': api_key, + 'Content-Type': 'application/json' + } + + for corrected_url, test_endpoint in test_configs: + try: + response = requests.get( + test_endpoint, + headers=headers, + timeout=10, # 10 second timeout + verify=True # SSL verification + ) + + if response.status_code == 200: + try: + json_response = response.json() + # Validate expected Immich user response structure + required_fields = ['id', 'email', 'name', 'isAdmin', 'createdAt'] + if all(field in json_response for field in required_fields): + return True, corrected_url, None + else: + continue # Try next endpoint + except (ValueError, KeyError): + continue # Try next endpoint + elif response.status_code == 401: + return False, original_server_url, "Invalid API key or unauthorized access" + elif response.status_code == 403: + return False, original_server_url, "Access forbidden - check API key permissions" + # Continue to next endpoint for 404 errors + + except requests.exceptions.ConnectTimeout: + return False, original_server_url, "Connection timeout - server may be unreachable" + except requests.exceptions.ConnectionError: + return False, original_server_url, "Cannot connect to server - check URL and network connectivity" + except requests.exceptions.SSLError: + return False, original_server_url, "SSL certificate error - check server certificate" + except requests.exceptions.RequestException as e: + logger.error(f"RequestException during Immich connection validation: {str(e)}") + return False, original_server_url, "Connection failed due to a network error." + except Exception as e: + logger.error(f"Unexpected error during Immich connection validation: {str(e)}") + return False, original_server_url, "An unexpected error occurred while validating the connection." + + # If we get here, none of the endpoints worked + return False, original_server_url, "Immich server endpoint not found - check server URL" + def create(self, request): """ RESTful POST method for creating a new Immich integration. """ - # Check if the user already has an integration user_integrations = ImmichIntegration.objects.filter(user=request.user) if user_integrations.exists(): @@ -270,11 +448,76 @@ class ImmichIntegrationViewSet(viewsets.ModelViewSet): serializer = self.serializer_class(data=request.data) if serializer.is_valid(): - serializer.save(user=request.user) + # Validate Immich server connection before saving + server_url = serializer.validated_data.get('server_url') + api_key = serializer.validated_data.get('api_key') + + is_valid, corrected_server_url, error_message = self._validate_immich_connection(server_url, api_key) + + if not is_valid: + return Response( + { + 'message': f'Cannot connect to Immich server: {error_message}', + 'error': True, + 'code': 'immich.connection_failed', + 'details': error_message + }, + status=status.HTTP_400_BAD_REQUEST + ) + + # If validation passes, save the integration with the corrected URL + serializer.save(user=request.user, server_url=corrected_server_url) return Response( serializer.data, status=status.HTTP_201_CREATED ) + + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST + ) + + def update(self, request, pk=None): + """ + RESTful PUT method for updating an existing Immich integration. + """ + integration = ImmichIntegration.objects.filter(user=request.user, id=pk).first() + if not integration: + return Response( + { + 'message': 'Integration not found.', + 'error': True, + 'code': 'immich.integration_not_found' + }, + status=status.HTTP_404_NOT_FOUND + ) + + serializer = self.serializer_class(integration, data=request.data, partial=True) + if serializer.is_valid(): + # Validate Immich server connection before updating + server_url = serializer.validated_data.get('server_url', integration.server_url) + api_key = serializer.validated_data.get('api_key', integration.api_key) + + is_valid, corrected_server_url, error_message = self._validate_immich_connection(server_url, api_key) + + if not is_valid: + return Response( + { + 'message': f'Cannot connect to Immich server: {error_message}', + 'error': True, + 'code': 'immich.connection_failed', + 'details': error_message + }, + status=status.HTTP_400_BAD_REQUEST + ) + + # If validation passes, save the integration with the corrected URL + serializer.save(server_url=corrected_server_url) + return Response( + serializer.data, + status=status.HTTP_200_OK + ) + return Response( serializer.errors, status=status.HTTP_400_BAD_REQUEST @@ -301,10 +544,9 @@ class ImmichIntegrationViewSet(viewsets.ModelViewSet): }, status=status.HTTP_200_OK ) - + def list(self, request, *args, **kwargs): # If the user has an integration, we only want to return that integration - user_integrations = ImmichIntegration.objects.filter(user=request.user) if user_integrations.exists(): integration = user_integrations.first() diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 406e37a..073fd77 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -27,7 +27,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(__file__)) SECRET_KEY = getenv('SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = getenv('DEBUG', 'True') == 'True' +DEBUG = getenv('DEBUG', 'true').lower() == 'true' # ALLOWED_HOSTS = [ # 'localhost', @@ -71,6 +71,7 @@ MIDDLEWARE = ( 'whitenoise.middleware.WhiteNoiseMiddleware', 'adventures.middleware.XSessionTokenMiddleware', 'adventures.middleware.DisableCSRFForSessionTokenMiddleware', + 'adventures.middleware.DisableCSRFForMobileLoginSignup', 'corsheaders.middleware.CorsMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -101,22 +102,30 @@ ROOT_URLCONF = 'main.urls' # Database # https://docs.djangoproject.com/en/1.7/ref/settings/#databases +# Using legacy PG environment variables for compatibility with existing setups + +def env(*keys, default=None): + """Return the first non-empty environment variable from a list of keys.""" + for key in keys: + value = os.getenv(key) + if value: + return value + return default + DATABASES = { 'default': { 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'NAME': getenv('PGDATABASE'), - 'USER': getenv('PGUSER'), - 'PASSWORD': getenv('PGPASSWORD'), - 'HOST': getenv('PGHOST'), - 'PORT': getenv('PGPORT', 5432), + 'NAME': env('PGDATABASE', 'POSTGRES_DB'), + 'USER': env('PGUSER', 'POSTGRES_USER'), + 'PASSWORD': env('PGPASSWORD', 'POSTGRES_PASSWORD'), + 'HOST': env('PGHOST', default='localhost'), + 'PORT': int(env('PGPORT', default='5432')), 'OPTIONS': { 'sslmode': 'prefer', # Prefer SSL, but allow non-SSL connections }, } } - - # Internationalization # https://docs.djangoproject.com/en/1.7/topics/i18n/ @@ -138,6 +147,8 @@ SESSION_COOKIE_SAMESITE = 'Lax' SESSION_COOKIE_NAME = 'sessionid' SESSION_COOKIE_SECURE = FRONTEND_URL.startswith('https') +CSRF_COOKIE_SECURE = FRONTEND_URL.startswith('https') + hostname = urlparse(FRONTEND_URL).hostname is_ip_address = hostname.replace('.', '').isdigit() @@ -201,7 +212,7 @@ TEMPLATES = [ # Authentication settings -DISABLE_REGISTRATION = getenv('DISABLE_REGISTRATION', 'False') == 'True' +DISABLE_REGISTRATION = getenv('DISABLE_REGISTRATION', 'false').lower() == 'true' DISABLE_REGISTRATION_MESSAGE = getenv('DISABLE_REGISTRATION_MESSAGE', 'Registration is disabled. Please contact the administrator if you need an account.') AUTH_USER_MODEL = 'users.CustomUser' @@ -229,6 +240,8 @@ HEADLESS_FRONTEND_URLS = { AUTHENTICATION_BACKENDS = [ 'users.backends.NoPasswordAuthBackend', + # 'allauth.account.auth_backends.AuthenticationBackend', + # 'django.contrib.auth.backends.ModelBackend', ] EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' @@ -242,9 +255,9 @@ if getenv('EMAIL_BACKEND', 'console') == 'console': else: EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = getenv('EMAIL_HOST') - EMAIL_USE_TLS = getenv('EMAIL_USE_TLS', 'True') == 'True' + EMAIL_USE_TLS = getenv('EMAIL_USE_TLS', 'true').lower() == 'true' EMAIL_PORT = getenv('EMAIL_PORT', 587) - EMAIL_USE_SSL = getenv('EMAIL_USE_SSL', 'False') == 'True' + EMAIL_USE_SSL = getenv('EMAIL_USE_SSL', 'false').lower() == 'true' EMAIL_HOST_USER = getenv('EMAIL_HOST_USER') EMAIL_HOST_PASSWORD = getenv('EMAIL_HOST_PASSWORD') DEFAULT_FROM_EMAIL = getenv('DEFAULT_FROM_EMAIL') @@ -312,4 +325,6 @@ LOGGING = { # ADVENTURELOG_CDN_URL = getenv('ADVENTURELOG_CDN_URL', 'https://cdn.adventurelog.app') # https://github.com/dr5hn/countries-states-cities-database/tags -COUNTRY_REGION_JSON_VERSION = 'v2.5' \ No newline at end of file +COUNTRY_REGION_JSON_VERSION = 'v2.6' + +GOOGLE_MAPS_API_KEY = getenv('GOOGLE_MAPS_API_KEY', '') \ No newline at end of file diff --git a/backend/server/requirements.txt b/backend/server/requirements.txt index dcd0125..c679ecc 100644 --- a/backend/server/requirements.txt +++ b/backend/server/requirements.txt @@ -1,16 +1,16 @@ -Django==5.0.11 +Django==5.2.1 djangorestframework>=3.15.2 django-allauth==0.63.3 drf-yasg==1.21.4 django-cors-headers==4.4.0 coreapi==2.3.3 -python-dotenv -psycopg2-binary -Pillow -whitenoise -django-resized -django-geojson -setuptools +python-dotenv==1.1.0 +psycopg2-binary==2.9.10 +pillow==11.2.1 +whitenoise==6.9.0 +django-resized==1.0.3 +django-geojson==4.2.0 +setuptools==79.0.1 gunicorn==23.0.0 qrcode==8.0 slippers==0.6.2 @@ -21,4 +21,6 @@ icalendar==6.1.0 ijson==3.3.0 tqdm==4.67.1 overpy==0.7 -publicsuffix2==2.20191221 \ No newline at end of file +publicsuffix2==2.20191221 +geopy==2.4.1 +psutil==6.1.1 \ No newline at end of file diff --git a/backend/server/users/backends.py b/backend/server/users/backends.py index a099f11..8c985ce 100644 --- a/backend/server/users/backends.py +++ b/backend/server/users/backends.py @@ -1,16 +1,44 @@ from django.contrib.auth.backends import ModelBackend from allauth.socialaccount.models import SocialAccount +from allauth.account.auth_backends import AuthenticationBackend as AllauthBackend +from django.contrib.auth import get_user_model + +User = get_user_model() class NoPasswordAuthBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): - print("NoPasswordAuthBackend") - # First, attempt normal authentication - user = super().authenticate(request, username=username, password=password, **kwargs) - if user is None: + # Handle allauth-specific authentication (like email login) + allauth_backend = AllauthBackend() + allauth_user = allauth_backend.authenticate(request, username=username, password=password, **kwargs) + + # If allauth handled it, check our password disable logic + if allauth_user: + has_social_accounts = SocialAccount.objects.filter(user=allauth_user).exists() + if has_social_accounts and getattr(allauth_user, 'disable_password', False): + return None + if self.user_can_authenticate(allauth_user): + return allauth_user return None - if SocialAccount.objects.filter(user=user).exists() and user.disable_password: - # If yes, disable login via password + # Fallback to regular username/password authentication + if username is None or password is None: return None - return user + try: + # Get the user first + user = User.objects.get(username=username) + except User.DoesNotExist: + return None + + # Check if this user has social accounts and password is disabled + has_social_accounts = SocialAccount.objects.filter(user=user).exists() + + # If user has social accounts and disable_password is True, deny password login + if has_social_accounts and getattr(user, 'disable_password', False): + return None + + # Otherwise, proceed with normal password authentication + if user.check_password(password) and self.user_can_authenticate(user): + return user + + return None \ No newline at end of file diff --git a/backend/server/users/views.py b/backend/server/users/views.py index 7c763f5..d5a5a41 100644 --- a/backend/server/users/views.py +++ b/backend/server/users/views.py @@ -165,7 +165,7 @@ class EnabledSocialProvidersView(APIView): providers = [] for provider in social_providers: if provider.provider == 'openid_connect': - new_provider = f'oidc/{provider.client_id}' + new_provider = f'oidc/{provider.provider_id}' else: new_provider = provider.provider providers.append({ @@ -204,4 +204,4 @@ class DisablePasswordAuthenticationView(APIView): user.disable_password = False user.save() return Response({"detail": "Password authentication enabled."}, status=status.HTTP_200_OK) - \ No newline at end of file + diff --git a/backend/server/worldtravel/management/commands/bulk-adventure-geocode.py b/backend/server/worldtravel/management/commands/bulk-adventure-geocode.py new file mode 100644 index 0000000..1a5d7a5 --- /dev/null +++ b/backend/server/worldtravel/management/commands/bulk-adventure-geocode.py @@ -0,0 +1,26 @@ +from django.core.management.base import BaseCommand +from adventures.models import Adventure +import time + +class Command(BaseCommand): + help = 'Bulk geocode all adventures by triggering save on each one' + + def handle(self, *args, **options): + adventures = Adventure.objects.all() + total = adventures.count() + + self.stdout.write(self.style.SUCCESS(f'Starting bulk geocoding of {total} adventures')) + + for i, adventure in enumerate(adventures): + try: + self.stdout.write(f'Processing adventure {i+1}/{total}: {adventure}') + adventure.save() # This should trigger any geocoding in the save method + self.stdout.write(self.style.SUCCESS(f'Successfully processed adventure {i+1}/{total}')) + except Exception as e: + self.stdout.write(self.style.ERROR(f'Error processing adventure {i+1}/{total}: {adventure} - {e}')) + + # Sleep for 2 seconds between each save + if i < total - 1: # Don't sleep after the last one + time.sleep(2) + + self.stdout.write(self.style.SUCCESS('Finished processing all adventures')) diff --git a/backend/server/worldtravel/management/commands/download-countries.py b/backend/server/worldtravel/management/commands/download-countries.py index f5c5702..5b2f48e 100644 --- a/backend/server/worldtravel/management/commands/download-countries.py +++ b/backend/server/worldtravel/management/commands/download-countries.py @@ -3,8 +3,11 @@ from django.core.management.base import BaseCommand import requests from worldtravel.models import Country, Region, City from django.db import transaction -from tqdm import tqdm import ijson +import gc +import tempfile +import sqlite3 +from contextlib import contextmanager from django.conf import settings @@ -36,55 +39,112 @@ def saveCountryFlag(country_code): print(f'Error downloading flag for {country_code}') class Command(BaseCommand): - help = 'Imports the world travel data' + help = 'Imports the world travel data with minimal memory usage' def add_arguments(self, parser): parser.add_argument('--force', action='store_true', help='Force download the countries+regions+states.json file') + parser.add_argument('--batch-size', type=int, default=500, help='Batch size for database operations') + + @contextmanager + def _temp_db(self): + """Create a temporary SQLite database for intermediate storage""" + with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f: + temp_db_path = f.name + + try: + conn = sqlite3.connect(temp_db_path) + conn.execute('''CREATE TABLE temp_countries ( + country_code TEXT PRIMARY KEY, + name TEXT, + subregion TEXT, + capital TEXT, + longitude REAL, + latitude REAL + )''') + + conn.execute('''CREATE TABLE temp_regions ( + id TEXT PRIMARY KEY, + name TEXT, + country_code TEXT, + longitude REAL, + latitude REAL + )''') + + conn.execute('''CREATE TABLE temp_cities ( + id TEXT PRIMARY KEY, + name TEXT, + region_id TEXT, + longitude REAL, + latitude REAL + )''') + + conn.commit() + yield conn + finally: + conn.close() + try: + os.unlink(temp_db_path) + except OSError: + pass def handle(self, **options): force = options['force'] - batch_size = 100 + batch_size = options['batch_size'] countries_json_path = os.path.join(settings.MEDIA_ROOT, f'countries+regions+states-{COUNTRY_REGION_JSON_VERSION}.json') + + # Download or validate JSON file if not os.path.exists(countries_json_path) or force: + self.stdout.write('Downloading JSON file...') res = requests.get(f'https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/{COUNTRY_REGION_JSON_VERSION}/json/countries%2Bstates%2Bcities.json') if res.status_code == 200: with open(countries_json_path, 'w') as f: f.write(res.text) - self.stdout.write(self.style.SUCCESS('countries+regions+states.json downloaded successfully')) + self.stdout.write(self.style.SUCCESS('JSON file downloaded successfully')) else: - self.stdout.write(self.style.ERROR('Error downloading countries+regions+states.json')) + self.stdout.write(self.style.ERROR('Error downloading JSON file')) return elif not os.path.isfile(countries_json_path): - self.stdout.write(self.style.ERROR('countries+regions+states.json is not a file')) + self.stdout.write(self.style.ERROR('JSON file is not a file')) return elif os.path.getsize(countries_json_path) == 0: - self.stdout.write(self.style.ERROR('countries+regions+states.json is empty')) - elif Country.objects.count() == 0 or Region.objects.count() == 0 or City.objects.count() == 0: - self.stdout.write(self.style.WARNING('Some region data is missing. Re-importing all data.')) - else: - self.stdout.write(self.style.SUCCESS('Latest country, region, and state data already downloaded.')) + self.stdout.write(self.style.ERROR('JSON file is empty')) return + elif Country.objects.count() == 0 or Region.objects.count() == 0 or City.objects.count() == 0: + self.stdout.write(self.style.WARNING('Some data is missing. Re-importing all data.')) + else: + self.stdout.write(self.style.SUCCESS('Latest data already imported.')) + return + + self.stdout.write(self.style.SUCCESS('Starting ultra-memory-efficient import process...')) + + # Use temporary SQLite database for intermediate storage + with self._temp_db() as temp_conn: + self.stdout.write('Step 1: Parsing JSON and storing in temporary database...') + self._parse_and_store_temp(countries_json_path, temp_conn) - with open(countries_json_path, 'r') as f: - f = open(countries_json_path, 'rb') + self.stdout.write('Step 2: Processing countries...') + self._process_countries_from_temp(temp_conn, batch_size) + + self.stdout.write('Step 3: Processing regions...') + self._process_regions_from_temp(temp_conn, batch_size) + + self.stdout.write('Step 4: Processing cities...') + self._process_cities_from_temp(temp_conn, batch_size) + + self.stdout.write('Step 5: Cleaning up obsolete records...') + self._cleanup_obsolete_records(temp_conn) + + self.stdout.write(self.style.SUCCESS('All data imported successfully with minimal memory usage')) + + def _parse_and_store_temp(self, json_path, temp_conn): + """Parse JSON once and store in temporary SQLite database""" + country_count = 0 + region_count = 0 + city_count = 0 + + with open(json_path, 'rb') as f: parser = ijson.items(f, 'item') - - with transaction.atomic(): - existing_countries = {country.country_code: country for country in Country.objects.all()} - existing_regions = {region.id: region for region in Region.objects.all()} - existing_cities = {city.id: city for city in City.objects.all()} - - countries_to_create = [] - regions_to_create = [] - countries_to_update = [] - regions_to_update = [] - cities_to_create = [] - cities_to_update = [] - - processed_country_codes = set() - processed_region_ids = set() - processed_city_ids = set() - + for country in parser: country_code = country['iso2'] country_name = country['name'] @@ -93,137 +153,365 @@ class Command(BaseCommand): longitude = round(float(country['longitude']), 6) if country['longitude'] else None latitude = round(float(country['latitude']), 6) if country['latitude'] else None - processed_country_codes.add(country_code) + # Store country + temp_conn.execute('''INSERT OR REPLACE INTO temp_countries + (country_code, name, subregion, capital, longitude, latitude) + VALUES (?, ?, ?, ?, ?, ?)''', + (country_code, country_name, country_subregion, country_capital, longitude, latitude)) + + country_count += 1 + + # Download flag (do this during parsing to avoid extra pass) + saveCountryFlag(country_code) + # Process regions/states + if country['states']: + for state in country['states']: + state_id = f"{country_code}-{state['state_code']}" + state_name = state['name'] + state_lat = round(float(state['latitude']), 6) if state['latitude'] else None + state_lng = round(float(state['longitude']), 6) if state['longitude'] else None + + temp_conn.execute('''INSERT OR REPLACE INTO temp_regions + (id, name, country_code, longitude, latitude) + VALUES (?, ?, ?, ?, ?)''', + (state_id, state_name, country_code, state_lng, state_lat)) + + region_count += 1 + + # Process cities + if 'cities' in state and state['cities']: + for city in state['cities']: + city_id = f"{state_id}-{city['id']}" + city_name = city['name'] + city_lat = round(float(city['latitude']), 6) if city['latitude'] else None + city_lng = round(float(city['longitude']), 6) if city['longitude'] else None + + temp_conn.execute('''INSERT OR REPLACE INTO temp_cities + (id, name, region_id, longitude, latitude) + VALUES (?, ?, ?, ?, ?)''', + (city_id, city_name, state_id, city_lng, city_lat)) + + city_count += 1 + else: + # Country without states - create default region + state_id = f"{country_code}-00" + temp_conn.execute('''INSERT OR REPLACE INTO temp_regions + (id, name, country_code, longitude, latitude) + VALUES (?, ?, ?, ?, ?)''', + (state_id, country_name, country_code, None, None)) + region_count += 1 + + # Commit periodically to avoid memory buildup + if country_count % 100 == 0: + temp_conn.commit() + self.stdout.write(f' Parsed {country_count} countries, {region_count} regions, {city_count} cities...') + + temp_conn.commit() + self.stdout.write(f'✓ Parsing complete: {country_count} countries, {region_count} regions, {city_count} cities') + + def _process_countries_from_temp(self, temp_conn, batch_size): + """Process countries from temporary database""" + cursor = temp_conn.execute('SELECT country_code, name, subregion, capital, longitude, latitude FROM temp_countries') + + countries_to_create = [] + countries_to_update = [] + processed = 0 + + while True: + rows = cursor.fetchmany(batch_size) + if not rows: + break + + # Batch check for existing countries + country_codes_in_batch = [row[0] for row in rows] + existing_countries = { + c.country_code: c for c in + Country.objects.filter(country_code__in=country_codes_in_batch) + .only('country_code', 'name', 'subregion', 'capital', 'longitude', 'latitude') + } + + for row in rows: + country_code, name, subregion, capital, longitude, latitude = row + if country_code in existing_countries: + # Update existing country_obj = existing_countries[country_code] - country_obj.name = country_name - country_obj.subregion = country_subregion - country_obj.capital = country_capital + country_obj.name = name + country_obj.subregion = subregion + country_obj.capital = capital country_obj.longitude = longitude country_obj.latitude = latitude countries_to_update.append(country_obj) else: - country_obj = Country( - name=country_name, + countries_to_create.append(Country( country_code=country_code, - subregion=country_subregion, - capital=country_capital, + name=name, + subregion=subregion, + capital=capital, longitude=longitude, latitude=latitude + )) + + processed += 1 + + # Flush batches + if countries_to_create: + with transaction.atomic(): + Country.objects.bulk_create(countries_to_create, batch_size=batch_size, ignore_conflicts=True) + countries_to_create.clear() + + if countries_to_update: + with transaction.atomic(): + Country.objects.bulk_update( + countries_to_update, + ['name', 'subregion', 'capital', 'longitude', 'latitude'], + batch_size=batch_size ) - countries_to_create.append(country_obj) - - saveCountryFlag(country_code) - - if country['states']: - for state in country['states']: - name = state['name'] - state_id = f"{country_code}-{state['state_code']}" - latitude = round(float(state['latitude']), 6) if state['latitude'] else None - longitude = round(float(state['longitude']), 6) if state['longitude'] else None - - # Check for duplicate regions - if state_id in processed_region_ids: - # self.stdout.write(self.style.ERROR(f'State {state_id} already processed')) - continue - - processed_region_ids.add(state_id) - - if state_id in existing_regions: - region_obj = existing_regions[state_id] - region_obj.name = name - region_obj.country = country_obj - region_obj.longitude = longitude - region_obj.latitude = latitude - regions_to_update.append(region_obj) - else: - region_obj = Region( - id=state_id, - name=name, - country=country_obj, - longitude=longitude, - latitude=latitude - ) - regions_to_create.append(region_obj) - # self.stdout.write(self.style.SUCCESS(f'State {state_id} prepared')) - - if 'cities' in state and len(state['cities']) > 0: - for city in state['cities']: - city_id = f"{state_id}-{city['id']}" - city_name = city['name'] - latitude = round(float(city['latitude']), 6) if city['latitude'] else None - longitude = round(float(city['longitude']), 6) if city['longitude'] else None - - # Check for duplicate cities - if city_id in processed_city_ids: - # self.stdout.write(self.style.ERROR(f'City {city_id} already processed')) - continue - - processed_city_ids.add(city_id) - - if city_id in existing_cities: - city_obj = existing_cities[city_id] - city_obj.name = city_name - city_obj.region = region_obj - city_obj.longitude = longitude - city_obj.latitude = latitude - cities_to_update.append(city_obj) - else: - city_obj = City( - id=city_id, - name=city_name, - region=region_obj, - longitude=longitude, - latitude=latitude - ) - cities_to_create.append(city_obj) - # self.stdout.write(self.style.SUCCESS(f'City {city_id} prepared')) + countries_to_update.clear() + + if processed % 1000 == 0: + self.stdout.write(f' Processed {processed} countries...') + gc.collect() + + # Final flush + if countries_to_create: + with transaction.atomic(): + Country.objects.bulk_create(countries_to_create, batch_size=batch_size, ignore_conflicts=True) + if countries_to_update: + with transaction.atomic(): + Country.objects.bulk_update( + countries_to_update, + ['name', 'subregion', 'capital', 'longitude', 'latitude'], + batch_size=batch_size + ) + + self.stdout.write(f'✓ Countries complete: {processed} processed') + def _process_regions_from_temp(self, temp_conn, batch_size): + """Process regions from temporary database""" + # Get country mapping once + country_map = {c.country_code: c for c in Country.objects.only('id', 'country_code')} + + cursor = temp_conn.execute('SELECT id, name, country_code, longitude, latitude FROM temp_regions') + + regions_to_create = [] + regions_to_update = [] + processed = 0 + + while True: + rows = cursor.fetchmany(batch_size) + if not rows: + break + + # Batch check for existing regions + region_ids_in_batch = [row[0] for row in rows] + existing_regions = { + r.id: r for r in + Region.objects.filter(id__in=region_ids_in_batch) + .select_related('country') + .only('id', 'name', 'country', 'longitude', 'latitude') + } + + for row in rows: + region_id, name, country_code, longitude, latitude = row + country_obj = country_map.get(country_code) + + if not country_obj: + continue + + if region_id in existing_regions: + # Update existing + region_obj = existing_regions[region_id] + region_obj.name = name + region_obj.country = country_obj + region_obj.longitude = longitude + region_obj.latitude = latitude + regions_to_update.append(region_obj) else: - state_id = f"{country_code}-00" - processed_region_ids.add(state_id) - if state_id in existing_regions: - region_obj = existing_regions[state_id] - region_obj.name = country_name - region_obj.country = country_obj - regions_to_update.append(region_obj) - else: - region_obj = Region( - id=state_id, - name=country_name, - country=country_obj - ) - regions_to_create.append(region_obj) - # self.stdout.write(self.style.SUCCESS(f'Region {state_id} prepared for {country_name}')) - for i in tqdm(range(0, len(countries_to_create), batch_size), desc="Processing countries"): - batch = countries_to_create[i:i + batch_size] - Country.objects.bulk_create(batch) + regions_to_create.append(Region( + id=region_id, + name=name, + country=country_obj, + longitude=longitude, + latitude=latitude + )) + + processed += 1 + + # Flush batches + if regions_to_create: + with transaction.atomic(): + Region.objects.bulk_create(regions_to_create, batch_size=batch_size, ignore_conflicts=True) + regions_to_create.clear() + + if regions_to_update: + with transaction.atomic(): + Region.objects.bulk_update( + regions_to_update, + ['name', 'country', 'longitude', 'latitude'], + batch_size=batch_size + ) + regions_to_update.clear() + + if processed % 2000 == 0: + self.stdout.write(f' Processed {processed} regions...') + gc.collect() + + # Final flush + if regions_to_create: + with transaction.atomic(): + Region.objects.bulk_create(regions_to_create, batch_size=batch_size, ignore_conflicts=True) + if regions_to_update: + with transaction.atomic(): + Region.objects.bulk_update( + regions_to_update, + ['name', 'country', 'longitude', 'latitude'], + batch_size=batch_size + ) + + self.stdout.write(f'✓ Regions complete: {processed} processed') - for i in tqdm(range(0, len(regions_to_create), batch_size), desc="Processing regions"): - batch = regions_to_create[i:i + batch_size] - Region.objects.bulk_create(batch) + def _process_cities_from_temp(self, temp_conn, batch_size): + """Process cities from temporary database with optimized existence checking""" + # Get region mapping once + region_map = {r.id: r for r in Region.objects.only('id')} + + cursor = temp_conn.execute('SELECT id, name, region_id, longitude, latitude FROM temp_cities') + + cities_to_create = [] + cities_to_update = [] + processed = 0 + + while True: + rows = cursor.fetchmany(batch_size) + if not rows: + break + + # Fast existence check - only get IDs, no objects + city_ids_in_batch = [row[0] for row in rows] + existing_city_ids = set( + City.objects.filter(id__in=city_ids_in_batch) + .values_list('id', flat=True) + ) + + for row in rows: + city_id, name, region_id, longitude, latitude = row + region_obj = region_map.get(region_id) + + if not region_obj: + continue + + if city_id in existing_city_ids: + # For updates, just store the data - we'll do bulk update by raw SQL + cities_to_update.append({ + 'id': city_id, + 'name': name, + 'region_id': region_obj.id, + 'longitude': longitude, + 'latitude': latitude + }) + else: + cities_to_create.append(City( + id=city_id, + name=name, + region=region_obj, + longitude=longitude, + latitude=latitude + )) + + processed += 1 + + # Flush create batch (this is already fast) + if cities_to_create: + with transaction.atomic(): + City.objects.bulk_create(cities_to_create, batch_size=batch_size, ignore_conflicts=True) + cities_to_create.clear() + + # Flush update batch with raw SQL for speed + if cities_to_update: + self._bulk_update_cities_raw(cities_to_update) + cities_to_update.clear() + + if processed % 5000 == 0: + self.stdout.write(f' Processed {processed} cities...') + gc.collect() + + # Final flush + if cities_to_create: + with transaction.atomic(): + City.objects.bulk_create(cities_to_create, batch_size=batch_size, ignore_conflicts=True) + if cities_to_update: + self._bulk_update_cities_raw(cities_to_update) + + self.stdout.write(f'✓ Cities complete: {processed} processed') - for i in tqdm(range(0, len(cities_to_create), batch_size), desc="Processing cities"): - batch = cities_to_create[i:i + batch_size] - City.objects.bulk_create(batch) + def _bulk_update_cities_raw(self, cities_data): + """Fast bulk update using raw SQL""" + if not cities_data: + return + + from django.db import connection + + with connection.cursor() as cursor: + # Build the SQL for bulk update + # Using CASE statements for efficient bulk updates + when_clauses_name = [] + when_clauses_region = [] + when_clauses_lng = [] + when_clauses_lat = [] + city_ids = [] + + for city in cities_data: + city_id = city['id'] + city_ids.append(city_id) + when_clauses_name.append(f"WHEN id = %s THEN %s") + when_clauses_region.append(f"WHEN id = %s THEN %s") + when_clauses_lng.append(f"WHEN id = %s THEN %s") + when_clauses_lat.append(f"WHEN id = %s THEN %s") + + # Build parameters list + params = [] + for city in cities_data: + params.extend([city['id'], city['name']]) # for name + for city in cities_data: + params.extend([city['id'], city['region_id']]) # for region_id + for city in cities_data: + params.extend([city['id'], city['longitude']]) # for longitude + for city in cities_data: + params.extend([city['id'], city['latitude']]) # for latitude + params.extend(city_ids) # for WHERE clause + + # Execute the bulk update + sql = f""" + UPDATE worldtravel_city + SET + name = CASE {' '.join(when_clauses_name)} END, + region_id = CASE {' '.join(when_clauses_region)} END, + longitude = CASE {' '.join(when_clauses_lng)} END, + latitude = CASE {' '.join(when_clauses_lat)} END + WHERE id IN ({','.join(['%s'] * len(city_ids))}) + """ + + cursor.execute(sql, params) - # Process updates in batches - for i in range(0, len(countries_to_update), batch_size): - batch = countries_to_update[i:i + batch_size] - for i in tqdm(range(0, len(countries_to_update), batch_size), desc="Updating countries"): - batch = countries_to_update[i:i + batch_size] - Country.objects.bulk_update(batch, ['name', 'subregion', 'capital', 'longitude', 'latitude']) - - for i in tqdm(range(0, len(regions_to_update), batch_size), desc="Updating regions"): - batch = regions_to_update[i:i + batch_size] - Region.objects.bulk_update(batch, ['name', 'country', 'longitude', 'latitude']) - - for i in tqdm(range(0, len(cities_to_update), batch_size), desc="Updating cities"): - batch = cities_to_update[i:i + batch_size] - City.objects.bulk_update(batch, ['name', 'region', 'longitude', 'latitude']) - Country.objects.exclude(country_code__in=processed_country_codes).delete() - Region.objects.exclude(id__in=processed_region_ids).delete() - City.objects.exclude(id__in=processed_city_ids).delete() - - self.stdout.write(self.style.SUCCESS('All data imported successfully')) \ No newline at end of file + def _cleanup_obsolete_records(self, temp_conn): + """Clean up obsolete records using temporary database""" + # Get IDs from temp database to avoid loading large lists into memory + temp_country_codes = {row[0] for row in temp_conn.execute('SELECT country_code FROM temp_countries')} + temp_region_ids = {row[0] for row in temp_conn.execute('SELECT id FROM temp_regions')} + temp_city_ids = {row[0] for row in temp_conn.execute('SELECT id FROM temp_cities')} + + with transaction.atomic(): + countries_deleted = Country.objects.exclude(country_code__in=temp_country_codes).count() + regions_deleted = Region.objects.exclude(id__in=temp_region_ids).count() + cities_deleted = City.objects.exclude(id__in=temp_city_ids).count() + + Country.objects.exclude(country_code__in=temp_country_codes).delete() + Region.objects.exclude(id__in=temp_region_ids).delete() + City.objects.exclude(id__in=temp_city_ids).delete() + + if countries_deleted > 0 or regions_deleted > 0 or cities_deleted > 0: + self.stdout.write(f'✓ Deleted {countries_deleted} obsolete countries, {regions_deleted} regions, {cities_deleted} cities') + else: + self.stdout.write('✓ No obsolete records found to delete') \ No newline at end of file diff --git a/backend/server/worldtravel/migrations/0016_remove_city_insert_id_remove_country_insert_id_and_more.py b/backend/server/worldtravel/migrations/0016_remove_city_insert_id_remove_country_insert_id_and_more.py new file mode 100644 index 0000000..fbc07de --- /dev/null +++ b/backend/server/worldtravel/migrations/0016_remove_city_insert_id_remove_country_insert_id_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.1 on 2025-06-14 17:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('worldtravel', '0015_city_insert_id_country_insert_id_region_insert_id'), + ] + + operations = [ + migrations.RemoveField( + model_name='city', + name='insert_id', + ), + migrations.RemoveField( + model_name='country', + name='insert_id', + ), + migrations.RemoveField( + model_name='region', + name='insert_id', + ), + ] diff --git a/backend/server/worldtravel/models.py b/backend/server/worldtravel/models.py index 6c7ebb8..9e83f59 100644 --- a/backend/server/worldtravel/models.py +++ b/backend/server/worldtravel/models.py @@ -17,7 +17,6 @@ class Country(models.Model): capital = models.CharField(max_length=100, blank=True, null=True) longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) - insert_id = models.UUIDField(unique=False, blank=True, null=True) class Meta: verbose_name = "Country" @@ -32,7 +31,6 @@ class Region(models.Model): country = models.ForeignKey(Country, on_delete=models.CASCADE) longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) - insert_id = models.UUIDField(unique=False, blank=True, null=True) def __str__(self): return self.name @@ -43,7 +41,6 @@ class City(models.Model): region = models.ForeignKey(Region, on_delete=models.CASCADE) longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) - insert_id = models.UUIDField(unique=False, blank=True, null=True) class Meta: verbose_name_plural = "Cities" diff --git a/backend/server/worldtravel/serializers.py b/backend/server/worldtravel/serializers.py index 6472125..962914b 100644 --- a/backend/server/worldtravel/serializers.py +++ b/backend/server/worldtravel/serializers.py @@ -22,8 +22,11 @@ class CountrySerializer(serializers.ModelSerializer): def get_num_visits(self, obj): request = self.context.get('request') - if request and hasattr(request, 'user'): - return VisitedRegion.objects.filter(region__country=obj, user_id=request.user).count() + user = getattr(request, 'user', None) + + if user and user.is_authenticated: + return VisitedRegion.objects.filter(region__country=obj, user_id=user).count() + return 0 class Meta: diff --git a/backend/supervisord.conf b/backend/supervisord.conf new file mode 100644 index 0000000..e7adec7 --- /dev/null +++ b/backend/supervisord.conf @@ -0,0 +1,16 @@ +[supervisord] +nodaemon=true + +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" +autorestart=true +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr + +[program:gunicorn] +command=/code/entrypoint.sh +autorestart=true +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr +stdout_logfile_maxbytes = 0 +stderr_logfile_maxbytes = 0 diff --git a/brand/screenshots/adventures.png b/brand/screenshots/adventures.png index fd61d54..96204df 100644 Binary files a/brand/screenshots/adventures.png and b/brand/screenshots/adventures.png differ diff --git a/brand/screenshots/countries.png b/brand/screenshots/countries.png index 3b8e534..6eecc59 100644 Binary files a/brand/screenshots/countries.png and b/brand/screenshots/countries.png differ diff --git a/brand/screenshots/dashboard.png b/brand/screenshots/dashboard.png index af4d8bb..05ca982 100644 Binary files a/brand/screenshots/dashboard.png and b/brand/screenshots/dashboard.png differ diff --git a/brand/screenshots/details.png b/brand/screenshots/details.png index 6ae57eb..47c99fa 100644 Binary files a/brand/screenshots/details.png and b/brand/screenshots/details.png differ diff --git a/brand/screenshots/edit.png b/brand/screenshots/edit.png index 123160d..1fce199 100644 Binary files a/brand/screenshots/edit.png and b/brand/screenshots/edit.png differ diff --git a/brand/screenshots/itinerary.png b/brand/screenshots/itinerary.png index f153263..c11cf4e 100644 Binary files a/brand/screenshots/itinerary.png and b/brand/screenshots/itinerary.png differ diff --git a/brand/screenshots/map.png b/brand/screenshots/map.png index 22b13b9..081cb79 100644 Binary files a/brand/screenshots/map.png and b/brand/screenshots/map.png differ diff --git a/brand/screenshots/regions.png b/brand/screenshots/regions.png index 6092dc6..4aea537 100644 Binary files a/brand/screenshots/regions.png and b/brand/screenshots/regions.png differ diff --git a/docker-compose.yml b/docker-compose.yml index eca6a8c..034ec06 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,23 +4,17 @@ services: image: ghcr.io/seanmorley15/adventurelog-frontend:latest container_name: adventurelog-frontend restart: unless-stopped - environment: - - PUBLIC_SERVER_URL=http://server:8000 # Should be the service name of the backend with port 8000, even if you change the port in the backend service - - ORIGIN=http://localhost:8015 - - BODY_SIZE_LIMIT=Infinity + env_file: .env ports: - - "8015:3000" + - "${FRONTEND_PORT:-8015}:3000" depends_on: - server db: - image: postgis/postgis:15-3.3 + image: postgis/postgis:16-3.5 container_name: adventurelog-db restart: unless-stopped - environment: - POSTGRES_DB: database - POSTGRES_USER: adventure - POSTGRES_PASSWORD: changeme123 + env_file: .env volumes: - postgres_data:/var/lib/postgresql/data/ @@ -29,21 +23,9 @@ services: image: ghcr.io/seanmorley15/adventurelog-backend:latest container_name: adventurelog-backend restart: unless-stopped - environment: - - PGHOST=db - - PGDATABASE=database - - PGUSER=adventure - - PGPASSWORD=changeme123 - - SECRET_KEY=changeme123 - - DJANGO_ADMIN_USERNAME=admin - - DJANGO_ADMIN_PASSWORD=admin - - DJANGO_ADMIN_EMAIL=admin@example.com - - PUBLIC_URL=http://localhost:8016 # Match the outward port, used for the creation of image urls - - CSRF_TRUSTED_ORIGINS=http://localhost:8016,http://localhost:8015 # Comma separated list of trusted origins for CSRF - - DEBUG=False - - FRONTEND_URL=http://localhost:8015 # Used for email generation. This should be the url of the frontend + env_file: .env ports: - - "8016:80" + - "${BACKEND_PORT:-8016}:80" depends_on: - db volumes: diff --git a/documentation/.vitepress/config.mts b/documentation/.vitepress/config.mts index dbd9299..212eedb 100644 --- a/documentation/.vitepress/config.mts +++ b/documentation/.vitepress/config.mts @@ -1,7 +1,5 @@ import { defineConfig } from "vitepress"; -const inProd = process.env.NODE_ENV === "production"; - // https://vitepress.dev/reference/site-config export default defineConfig({ head: [ @@ -15,6 +13,14 @@ export default defineConfig({ "data-website-id": "a7552764-5a1d-4fe7-80c2-5331e1a53cb6", }, ], + + [ + "link", + { + rel: "me", + href: "https://mastodon.social/@adventurelog", + }, + ], ], ignoreDeadLinks: "localhostLinks", title: "AdventureLog", @@ -25,6 +31,67 @@ export default defineConfig({ hostname: "https://adventurelog.app", }, + transformPageData(pageData) { + if (pageData.relativePath === "index.md") { + const jsonLd = { + "@context": "https://schema.org", + "@type": "SoftwareApplication", + name: "AdventureLog", + url: "https://adventurelog.app", + applicationCategory: "TravelApplication", + operatingSystem: "Web, Docker, Linux", + description: + "AdventureLog is a self-hosted platform for tracking and planning travel experiences. Built for modern explorers, it offers trip planning, journaling, tracking and location mapping in one privacy-respecting package.", + creator: { + "@type": "Person", + name: "Sean Morley", + url: "https://seanmorley.com", + }, + offers: { + "@type": "Offer", + price: "0.00", + priceCurrency: "USD", + description: "Open-source version available for self-hosting.", + }, + softwareVersion: "v0.10.0", + license: + "https://github.com/seanmorley15/adventurelog/blob/main/LICENSE", + screenshot: + "https://raw.githubusercontent.com/seanmorley15/AdventureLog/refs/heads/main/brand/screenshots/adventures.png", + downloadUrl: "https://github.com/seanmorley15/adventurelog", + sameAs: ["https://github.com/seanmorley15/adventurelog"], + keywords: [ + "self-hosted travel log", + "open source trip planner", + "travel journaling app", + "docker travel diary", + "map-based travel tracker", + "privacy-focused travel app", + "adventure log software", + "travel experience tracker", + "self-hosted travel app", + "open source travel software", + "trip planning tool", + "travel itinerary manager", + "location-based travel app", + "travel experience sharing", + "travel log application", + ], + }; + + return { + frontmatter: { + ...pageData.frontmatter, + head: [ + ["script", { type: "application/ld+json" }, JSON.stringify(jsonLd)], + ], + }, + }; + } + + return {}; + }, + themeConfig: { // https://vitepress.dev/reference/default-theme-config nav: [ @@ -62,6 +129,7 @@ export default defineConfig({ collapsed: false, items: [ { text: "Getting Started", link: "/docs/install/getting_started" }, + { text: "Quick Start Script â˛ī¸", link: "/docs/install/quick_start" }, { text: "Docker 🐋", link: "/docs/install/docker" }, { text: "Proxmox LXC 🐧", link: "/docs/install/proxmox_lxc" }, { text: "Synology NAS â˜ī¸", link: "/docs/install/synology_nas" }, @@ -80,10 +148,21 @@ export default defineConfig({ link: "/docs/install/nginx_proxy_manager", }, { text: "Traefik", link: "/docs/install/traefik" }, + { text: "Caddy", link: "/docs/install/caddy" }, ], }, ], }, + { + text: "Usage", + collapsed: false, + items: [ + { + text: "How to use AdventureLog", + link: "/docs/usage/usage", + }, + ], + }, { text: "Configuration", collapsed: false, @@ -92,6 +171,10 @@ export default defineConfig({ text: "Immich Integration", link: "/docs/configuration/immich_integration", }, + { + text: "Google Maps Integration", + link: "/docs/configuration/google_maps_integration", + }, { text: "Social Auth and OIDC", link: "/docs/configuration/social_auth", @@ -108,6 +191,10 @@ export default defineConfig({ text: "GitHub", link: "/docs/configuration/social_auth/github", }, + { + text: "Authelia", + link: "https://www.authelia.com/integration/openid-connect/adventure-log/", + }, { text: "Open ID Connect", link: "/docs/configuration/social_auth/oidc", @@ -134,6 +221,10 @@ export default defineConfig({ text: "No Images Displaying", link: "/docs/troubleshooting/no_images", }, + { + text: "Login and Registration Unresponsive", + link: "/docs/troubleshooting/login_unresponsive", + }, { text: "Failed to Start Nginx", link: "/docs/troubleshooting/nginx_failed", @@ -158,6 +249,14 @@ export default defineConfig({ text: "Changelogs", collapsed: false, items: [ + { + text: "v0.10.0", + link: "/docs/changelogs/v0-10-0", + }, + { + text: "v0.9.0", + link: "/docs/changelogs/v0-9-0", + }, { text: "v0.8.0", link: "/docs/changelogs/v0-8-0", @@ -180,6 +279,7 @@ export default defineConfig({ { icon: "buymeacoffee", link: "https://buymeacoffee.com/seanmorley15" }, { icon: "x", link: "https://x.com/AdventureLogApp" }, { icon: "mastodon", link: "https://mastodon.social/@adventurelog" }, + { icon: "instagram", link: "https://www.instagram.com/adventurelogapp" }, ], }, }); diff --git a/documentation/docs/changelogs/v0-10-0.md b/documentation/docs/changelogs/v0-10-0.md new file mode 100644 index 0000000..8ecba82 --- /dev/null +++ b/documentation/docs/changelogs/v0-10-0.md @@ -0,0 +1,123 @@ +# AdventureLog v0.10.0 - Trip Maps, Google Maps Integration & Quick Deploy Script + +Released 06-10-2025 + +Hi everyone, + +I’m pleased to share **AdventureLog v0.10.0**, a focused update that brings timezone-aware planning, smoother maps, and simpler deployment. This release refines many of the core features you’ve been using and addresses feedback from the community. Thank you for your contributions, suggestions, and ongoing support! + +## 🧭 Time-Aware Travel Planning + +**Timezone-Aware Visits & Timeline Logic** + +- Exact start/end times with timezones for each visit, so your itinerary matches when and where events actually happen. +- Collections now auto-order by date, giving your timeline a clear, chronological flow. +- Lodging and transportation entries follow the same rules, making multi-city trips easier to visualize. +- A chronologically accurate map and timeline view shows your adventure in the right order. + +## đŸ—ēī¸ Smart Mapping & Location Tools + +**Google Maps Integration (Optional)** + +- Autocomplete-powered location search (via Google Maps) for faster, more accurate entries. +- Automatic geocoding ties new or updated adventures to the correct country, region, and city. +- Improved map performance and cleaner markers throughout the app. + +**Map Interaction Enhancements** + +- Open any adventure location in Apple Maps, Google Maps, or OpenStreetMap with one click. +- Full-width maps on mobile devices for better visibility. +- Tidied-up markers and updated category icons for clarity. + +## 🎨 UI & UX Refinements + +- Updated adventure forms with clearer labels and streamlined inputs. +- Smoother page transitions and consistent layouts on desktop and mobile. +- Design and spacing adjustments for a more balanced, polished appearance. +- Various bug fixes to address layout quirks and improve overall usability. + +## 🌍 Localization & Navigation + +- Expanded language support and updated locale files. +- Improved back/forward navigation so you don’t lose your place when browsing collections. +- Responsive collection cards that adapt to different screen sizes. +- Fixed minor layout issues related to collections and navigation. + +## 📷 Immich Integration Upgrades + +- Choose whether to copy Immich images into AdventureLog storage or reference them via URL to avoid duplicating files. +- Toggle “Copy Images” on or off to manage storage preferences. +- Updated logic to prevent duplicate image uploads when using Immich. + +## âš™ī¸ DevOps & Backend Enhancements + +- Switched to `supervisord` in Docker for reliable container startup and centralized logging. +- Restored IPv6 support for dual-stack deployments. +- Upgraded to Node.js v22 for better performance and compatibility. +- Added more tests, improved UTC-aware date validation, and refined ID generation. +- Optimized database migrations for smoother updates. + +## 📘 Documentation & Community Resources + +- New guide for deploying with Caddy web server, covering TLS setup and reverse proxy configuration. +- Updated instructions for Google Maps integration, including API key setup and troubleshooting. +- Follow our Mastodon profile at [@adventurelog@mastodon.social](https://mastodon.social/@adventurelog) for updates and discussion. +- Chat with other users on our [Discord server](https://discord.gg/wRbQ9Egr8C) to share feedback, ask questions, or swap travel tips. + +## ✨ NEW: Quick Deploy Script + +Based on community feedback, we’ve added a simple deployment script: + +1. Run: + + ```bash + curl -sSL https://get.adventurelog.app | bash + ``` + +2. Provide your domain/ip details when prompted. + The script handles Docker Compose, and environment configuration automatically. + +Self-hosting just got a bit easier—no more manual setup steps. + +## â„šī¸ Additional Notes + +- **Bulk Geocoding** + To geocode existing adventures in one go docker exec into the backend container and run: + + ``` + python manage.py bulk-adventure-geocode + ``` + + This will link all adventures to their correct country, region, and city. + +- **Timezone Migrations** + If you have older trips without explicit timezones, simply view a trip’s detail page and AdventureLog will auto-convert the dates. + +## đŸ‘Ĩ Thanks to Our Contributors + +Your pull requests, issue reports, and ongoing feedback have been instrumental. Special thanks to: + +- @ClumsyAdmin +- @eidsheim98 +- @andreatitolo +- @lesensei +- @theshaun +- @lkiesow +- @larsl-net +- @marcschumacher + +Every contribution helps make AdventureLog more reliable and user-friendly. + +## 💖 Support the Project + +If you find AdventureLog helpful, consider sponsoring me! Your support keeps this project going: + +[https://seanmorley.com/sponsor](https://seanmorley.com/sponsor) + +📖 [View the Full Changelog on GitHub](https://github.com/seanmorley15/AdventureLog/compare/v0.9.0...v0.10.0) + +Thanks for being part of the AdventureLog community. I appreciate your feedback and look forward to seeing where your next journey takes you! + +Happy travels, +**Sean Morley** (@seanmorley15) +Project Lead, AdventureLog diff --git a/documentation/docs/changelogs/v0-9-0.md b/documentation/docs/changelogs/v0-9-0.md new file mode 100644 index 0000000..18764ee --- /dev/null +++ b/documentation/docs/changelogs/v0-9-0.md @@ -0,0 +1,133 @@ +# AdventureLog v0.9.0 - Smart Recommendations, Attachments, and Maps + +Released 03-19-2025 + +Hi travelers! 🌍 +I’m excited to unveil **AdventureLog v0.9.0**, one of our most feature-packed updates yet! From Smart Recommendations to enhanced maps and a refreshed profile system, this release is all about improving your travel planning and adventure tracking experience. Let’s dive into what’s new! + +--- + +## What's New ✨ + +### 🧠 Smart Recommendations + +- **AdventureLog Smart Recommendations**: Get tailored suggestions for new adventures and activities based on your collection destinations. +- Leverages OpenStreetMap to recommend places and activities near your travel destinations. + +--- + +### đŸ—‚ī¸ Attachments, GPX Maps & Global Search + +- **Attachments System**: Attach files to your adventures to view key trip data like maps and tickets in AdventureLog! +- **GPX File Uploads & Maps**: Upload GPX tracks to adventures to visualize them directly on your maps. +- **Global Search**: A universal search bar to quickly find adventures, cities, countries, and more across your instance. + +--- + +### 🏨 Lodging & Itinerary + +- **Lodging Tracking**: Add and manage lodging accommodations as part of your collections, complete with check-in/check-out dates. +- **Improved Itinerary Views**: Better day-by-day itinerary display with clear UI enhancements. + +--- + +### đŸ—ēī¸ Maps & Locations + +- **Open Locations in Maps**: Directly open adventure locations and points of interest in your preferred mapping service. +- **Adventure Category Icons on Maps**: View custom category icons right on your adventure and collection maps. + +--- + +### đŸ—“ī¸ Calendar + +- **Collection Range View**: Improved calendar view showing the full date range of collections. + +--- + +### 🌐 Authentication & Security + +- **OIDC Authentication**: Added support for OpenID Connect (OIDC) for seamless integration with identity providers. +- **Secure Session Cookies**: Improved session cookie handling with dynamic domain detection and better security for IP addresses. +- **Disable Password Auth**: Option to disable password auth for users with connected OIDC/Social accounts. + +--- + +### đŸ–Ĩī¸ PWA Support + +- **Progressive Web App (PWA) Support**: Install AdventureLog as a PWA on your desktop or mobile device for a native app experience. + +--- + +### đŸ—ī¸ Infrastructure & DevOps + +- **Dual-Stack Backend**: IPv4 and IPv6 ready backend system (@larsl-net). +- **Kubernetes Configs** continue to be improved for scalable deployments. + +--- + +### 🌐 Localization + +- **Korean language support** (@seanmorley15). +- **Improved Dutch** (@ThomasDetemmerman), **Simplified Chinese** (@jyyyeung), **German** (@Cathnan and @marcschumacher) translations. +- **Polish and Swedish** translations improved in prior release! + +--- + +### 📝 Documentation + +- **New Unraid Installation Guide** with community-contributed updates (@ThunderLord956, @evertyang). +- Updated **OIDC** and **Immich integration** docs for clarity (@UndyingSoul, @motox986). +- General spell-check and documentation polish (@ThunderLord956, @mcguirepr89). + +--- + +### 🐛 Bug Fixes and Improvements + +- Fixed CSRF issues with admin tools. +- Backend ready for **dual-stack** environments. +- Improved itinerary element display and GPX file handling. +- Optimized session cookie handling for domain/IP setups. +- Various **small Python fixes** (@larsl-net). +- Fixed container relations (@bucherfa). +- Django updated to **5.0.11** for security and performance improvements. +- General **codebase clean-up** and UI polish. + +--- + +## 🌟 New Contributors + +A huge shoutout to our amazing new contributors! 🎉 + +- @larsl-net +- @bucherfa +- @UndyingSoul +- @ThunderLord956 +- @evertyang +- @Thiesjoo +- @motox986 +- @mcguirepr89 +- @ThomasDetemmerman +- @Cathnan +- @jyyyeung +- @marcschumacher + +Thank you for helping AdventureLog grow! 🙌 + +--- + +## Support My Work 💖 + +[![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/seanmorley15) +If AdventureLog has made your travels more organized or your trip memories richer, consider supporting my work on **Buy Me A Coffee**. Your support directly helps shape the future of this project! ☕ + +--- + +Enjoy this update and keep sharing your journeys with us! đŸŒâœˆī¸ +As always, drop your feedback and ideas in the [official Discord](https://discord.gg/wRbQ9Egr8) or in the discussions! + +Happy travels, +**Sean Morley** (@seanmorley15) + +--- + +**[Full Changelog](https://github.com/seanmorley15/AdventureLog/compare/v0.8.0...v0.9.0)** diff --git a/documentation/docs/configuration/email.md b/documentation/docs/configuration/email.md index e83ac6c..52a3a20 100644 --- a/documentation/docs/configuration/email.md +++ b/documentation/docs/configuration/email.md @@ -6,7 +6,7 @@ To change the email backend, you can set the following variable in your docker-c ```yaml environment: - - EMAIL_BACKEND='console' + - EMAIL_BACKEND=console ``` ## With SMTP diff --git a/documentation/docs/configuration/google_maps_integration.md b/documentation/docs/configuration/google_maps_integration.md new file mode 100644 index 0000000..2618d12 --- /dev/null +++ b/documentation/docs/configuration/google_maps_integration.md @@ -0,0 +1,36 @@ +# Google Maps Integration + +To enable Google Maps integration in AdventureLog, you'll need to create a Google Maps API key. This key allows AdventureLog to use Google Maps services such as geocoding and location search throughout the application. + +Follow the steps below to generate your own API key: + +## Google Cloud Console Setup + +1. Go to the [Google Cloud Console](https://console.cloud.google.com/). +2. Create an account if you don't have one in order to access the console. +3. Click on the project dropdown in the top bar. +4. Click **New Project**. +5. Name your project (e.g., `AdventureLog Maps`) and click **Create**. +6. Once the project is created, ensure it is selected in the project dropdown. +7. Click on the **Navigation menu** (three horizontal lines in the top left corner). +8. Navigate to **Google Maps Platform**. +9. Once in the Maps Platform, click on **Keys & Credentials** in the left sidebar. +10. Click on **Create credentials** and select **API key**. +11. A dialog will appear with your new API key. Copy this key for later use. + + + +## Configuration in AdventureLog + +Set the API key in your environment file or configuration under the backend service of AdventureLog. This is typically done in the `docker-compose.yml` file or directly in your environment variables `.env` file. + +```env +GOOGLE_MAPS_API_KEY=your_api_key_here +``` + +Once this is set, AdventureLog will be able to utilize Google Maps services for geocoding and location searches instead of relying on the default OpenStreetMap services. diff --git a/documentation/docs/configuration/social_auth.md b/documentation/docs/configuration/social_auth.md index 68214b9..9d4a231 100644 --- a/documentation/docs/configuration/social_auth.md +++ b/documentation/docs/configuration/social_auth.md @@ -9,6 +9,7 @@ The steps for each service varies so please refer to the specific service's docu - [Authentik](social_auth/authentik.md) (self-hosted) - [GitHub](social_auth/github.md) - [Open ID Connect](social_auth/oidc.md) +- [Authelia](https://www.authelia.com/integration/openid-connect/adventure-log/) ## Linking Existing Accounts diff --git a/documentation/docs/install/caddy.md b/documentation/docs/install/caddy.md new file mode 100644 index 0000000..d9d088a --- /dev/null +++ b/documentation/docs/install/caddy.md @@ -0,0 +1,67 @@ +# Installation with Caddy + +Caddy is a modern HTTP reverse proxy. It automatically integrates with Let's Encrypt (or other certificate providers) to generate TLS certificates for your site. + +As an example, if you want to add Caddy to your Docker compose configuration, add the following service to your `docker-compose.yml`: + +```yaml +services: + caddy: + image: docker.io/library/caddy:2 + container_name: adventurelog-caddy + restart: unless-stopped + cap_add: + - NET_ADMIN + ports: + - "80:80" + - "443:443" + - "443:443/udp" + volumes: + - ./caddy:/etc/caddy + - caddy_data:/data + - caddy_config:/config + + web: ... + server: ... + db: ... + +volumes: + caddy_data: + caddy_config: +``` + +Since all ingress traffic to the AdventureLog containsers now travels through Caddy, we can also remove the external ports configuration from those containsers in the `docker-compose.yml`. Just delete this configuration: + +```yaml + web: + ports: + - "8016:80" +â€Ļ + server: + ports: + - "8015:3000" +``` + +That's it for the Docker compose changes. Of course, there are other methods to run Caddy which are equally valid. + +However, we also need to configure Caddy. For this, create a file `./caddy/Caddyfile` in which you configure the requests which are proxied to the frontend and backend respectively and what domain Caddy should request a certificate for: + +``` +adventurelog.example.com { + + @frontend { + not path /media* /admin* /static* /accounts* + } + reverse_proxy @frontend web:3000 + + reverse_proxy server:80 +} +``` + +Once configured, you can start up the containsers: + +```bash +docker compose up +``` + +Your AdventureLog should now be up and running. diff --git a/documentation/docs/install/docker.md b/documentation/docs/install/docker.md index 0f987d2..b698502 100644 --- a/documentation/docs/install/docker.md +++ b/documentation/docs/install/docker.md @@ -1,7 +1,8 @@ # Docker 🐋 Docker is the preferred way to run AdventureLog on your local machine. It is a lightweight containerization technology that allows you to run applications in isolated environments called containers. -**Note**: This guide mainly focuses on installation with a linux based host machine, but the steps are similar for other operating systems. + +> **Note**: This guide mainly focuses on installation with a Linux-based host machine, but the steps are similar for other operating systems. ## Prerequisites @@ -9,7 +10,16 @@ Docker is the preferred way to run AdventureLog on your local machine. It is a l ## Getting Started -Get the `docker-compose.yml` file from the AdventureLog repository. You can download it from [here](https://github.com/seanmorley15/AdventureLog/blob/main/docker-compose.yml) or run this command to download it directly to your machine: +Get the `docker-compose.yml` and `.env.example` files from the AdventureLog repository. You can download them here: + +- [Docker Compose](https://github.com/seanmorley15/AdventureLog/blob/main/docker-compose.yml) +- [Environment Variables](https://github.com/seanmorley15/AdventureLog/blob/main/.env.example) + +```bash +wget https://raw.githubusercontent.com/seanmorley15/AdventureLog/main/docker-compose.yml +wget https://raw.githubusercontent.com/seanmorley15/AdventureLog/main/.env.example +cp .env.example .env +``` ::: tip @@ -17,46 +27,56 @@ If running on an ARM based machine, you will need to use a different PostGIS Ima ::: -```bash -wget https://raw.githubusercontent.com/seanmorley15/AdventureLog/main/docker-compose.yml -``` - ## Configuration -Here is a summary of the configuration options available in the `docker-compose.yml` file: +The `.env` file contains all the configuration settings for your AdventureLog instance. Here’s a breakdown of each section: - +### 🌐 Frontend (web) -### Frontend Container (web) +| Name | Required | Description | Default Value | +| ------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| `PUBLIC_SERVER_URL` | Yes | Used by the frontend SSR server to connect to the backend. Almost every user user will **never have to change this from default**! | `http://server:8000` | +| `ORIGIN` | Sometimes | Needed only if not using HTTPS. Set it to the domain or IP you'll use to access the frontend. | `http://localhost:8015` | +| `BODY_SIZE_LIMIT` | Yes | Maximum upload size in bytes. | `Infinity` | +| `FRONTEND_PORT` | Yes | Port that the frontend will run on inside Docker. | `8015` | -| Name | Required | Description | Default Value | -| ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | -| `PUBLIC_SERVER_URL` | Yes | What the frontend SSR server uses to connect to the backend. | ```http://server:8000``` | -| `ORIGIN` | Sometimes | Not needed if using HTTPS. If not, set it to the domain of what you will access the app from. | ```http://localhost:8015``` | -| `BODY_SIZE_LIMIT` | Yes | Used to set the maximum upload size to the server. Should be changed to prevent someone from uploading too much! Custom values must be set in **kilobytes**. | ```Infinity``` | +### 🐘 PostgreSQL Database -### Backend Container (server) +| Name | Required | Description | Default Value | +| ------------------- | -------- | --------------------- | ------------- | +| `PGHOST` | Yes | Internal DB hostname. | `db` | +| `POSTGRES_DB` | Yes | DB name. | `database` | +| `POSTGRES_USER` | Yes | DB user. | `adventure` | +| `POSTGRES_PASSWORD` | Yes | DB password. | `changeme123` | -| Name | Required | Description | Default Value | -| ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | -| `PGHOST` | Yes | Database host. | ```db``` | -| `PGDATABASE` | Yes | Database. | ```database``` | -| `PGUSER` | Yes | Database user. | ```adventure``` | -| `PGPASSWORD` | Yes | Database password. | ```changeme123``` | -| `PGPORT` | No | Database port. | ```5432``` | -| `DJANGO_ADMIN_USERNAME` | Yes | Default username. | ```admin``` | -| `DJANGO_ADMIN_PASSWORD` | Yes | Default password, change after initial login. | ```admin``` | -| `DJANGO_ADMIN_EMAIL` | Yes | Default user's email. | ```admin@example.com``` | -| `PUBLIC_URL` | Yes | This needs to match the outward port of the server and be accessible from where the app is used. It is used for the creation of image urls. | ```http://localhost:8016``` | -| `CSRF_TRUSTED_ORIGINS` | Yes | Need to be changed to the origins where you use your backend server and frontend. These values are comma separated. | ```http://localhost:8016``` | -| `FRONTEND_URL` | Yes | This is the publicly accessible url to the **frontend** container. This link should be accessible for all users. Used for email generation. | ```http://localhost:8015``` | +### 🔒 Backend (server) + +| Name | Required | Description | Default Value | +| ----------------------- | -------- | ---------------------------------------------------------------------------------- | --------------------------------------------- | +| `SECRET_KEY` | Yes | Django secret key. Change this in production! | `changeme123` | +| `DJANGO_ADMIN_USERNAME` | Yes | Default Django admin username. | `admin` | +| `DJANGO_ADMIN_PASSWORD` | Yes | Default Django admin password. | `admin` | +| `DJANGO_ADMIN_EMAIL` | Yes | Default admin email. | `admin@example.com` | +| `PUBLIC_URL` | Yes | Publicly accessible URL of the **backend**. Used for generating image URLs. | `http://localhost:8016` | +| `CSRF_TRUSTED_ORIGINS` | Yes | Comma-separated list of frontend/backend URLs that are allowed to submit requests. | `http://localhost:8016,http://localhost:8015` | +| `FRONTEND_URL` | Yes | URL to the **frontend**, used for email generation. | `http://localhost:8015` | +| `BACKEND_PORT` | Yes | Port that the backend will run on inside Docker. | `8016` | +| `DEBUG` | No | Should be `False` in production. | `False` | + +## Optional Configuration + +- [Disable Registration](../configuration/disable_registration.md) +- [Google Maps](../configuration/google_maps_integration.md) +- [Email Configuration](../configuration/email.md) +- [Immich Integration](../configuration/immich_integration.md) +- [Umami Analytics](../configuration/analytics.md) ## Running the Containers -To start the containers, run the following command: +Once you've configured `.env`, you can start AdventureLog with: ```bash docker compose up -d ``` -Enjoy AdventureLog! 🎉 +Enjoy using AdventureLog! 🎉 diff --git a/documentation/docs/install/getting_started.md b/documentation/docs/install/getting_started.md index aa5187c..ff8d4b5 100644 --- a/documentation/docs/install/getting_started.md +++ b/documentation/docs/install/getting_started.md @@ -1,14 +1,26 @@ -# Install Options for AdventureLog +# 🚀 Install Options for AdventureLog -AdventureLog can be installed in a variety of ways. The following are the most common methods: +AdventureLog can be installed in a variety of ways, depending on your platform or preference. -- [Docker](docker.md) đŸŗ -- [Proxmox LXC](proxmox_lxc.md) 🐧 -- [Synology NAS](synology_nas.md) â˜ī¸ -- [Kubernetes and Kustomize](kustomize.md) 🌐 -- [Unraid](unraid.md) 🧡 +## đŸ“Ļ Docker Quick Start -### Other Options +::: tip Quick Start Script +**The fastest way to get started:** +[Install AdventureLog with a single command →](quick_start.md) +Perfect for Docker beginners. +::: -- [Nginx Proxy Manager](nginx_proxy_manager.md) 🛡 -- [Traefik](traefik.md) 🚀 +## đŸŗ Popular Installation Methods + +- [Docker](docker.md) — Simple containerized setup +- [Proxmox LXC](proxmox_lxc.md) — Lightweight virtual environment +- [Synology NAS](synology_nas.md) — Self-host on your home NAS +- [Kubernetes + Kustomize](kustomize.md) — Advanced, scalable deployment +- [Unraid](unraid.md) — Easy integration for homelabbers +- [Umbrel](https://apps.umbrel.com/app/adventurelog) — Home server app store + +## âš™ī¸ Advanced & Alternative Setups + +- [Nginx Proxy Manager](nginx_proxy_manager.md) — Easy reverse proxy config +- [Traefik](traefik.md) — Dynamic reverse proxy with automation +- [Caddy](caddy.md) — Automatic HTTPS with a clean config diff --git a/documentation/docs/install/quick_start.md b/documentation/docs/install/quick_start.md new file mode 100644 index 0000000..64dbdce --- /dev/null +++ b/documentation/docs/install/quick_start.md @@ -0,0 +1,45 @@ +# 🚀 Quick Start Install + +Install **AdventureLog** in seconds using our automated script. + +## đŸ§Ē One-Liner Install + +```bash +curl -sSL https://get.adventurelog.app | bash +``` + +This will: + +- Check dependencies (Docker, Docker Compose) +- Set up project directory +- Download required files +- Prompt for basic configuration (like domain name) +- Start AdventureLog with Docker Compose + +## ✅ Requirements + +- Docker + Docker Compose +- Linux server or VPS +- Optional: Domain name for HTTPS + +## 🔍 What It Does + +The script automatically: + +1. Verifies Docker is installed and running +2. Downloads `docker-compose.yml` and `.env` +3. Prompts you for domain and port settings +4. Waits for services to start +5. Prints success info with next steps + +## đŸ§ŧ Uninstall + +To remove everything: + +```bash +cd adventurelog +docker compose down -v +rm -rf adventurelog +``` + +Need more control? Explore other [install options](getting_started.md) like Docker, Proxmox, Synology NAS, and more. diff --git a/documentation/docs/intro/adventurelog_overview.md b/documentation/docs/intro/adventurelog_overview.md index 310237f..80cf63d 100644 --- a/documentation/docs/intro/adventurelog_overview.md +++ b/documentation/docs/intro/adventurelog_overview.md @@ -27,4 +27,6 @@ AdventureLog is open-source software, licensed under the GPL-3.0 license. This m ## About the Maintainer -AdventureLog is created and maintained by [Sean Morley](https://seanmorley.com), a Computer Science student at the University of Connecticut. Sean is passionate about open-source software and building modern tools that help people solve real-world problems. +Hi, I'm [Sean Morley](https://seanmorley.com), the creator of AdventureLog. I'm an Electrical Engineering student at the University of Connecticut, and I'm passionate about open-source software and building modern tools that help people solve real-world problems. I created AdventureLog to solve a problem: the lack of a modern, open-source, user-friendly travel companion. Many existing travel apps are either too complex, too expensive, or too closed-off to be useful for the average traveler. AdventureLog aims to be the opposite: simple, beautiful, and open to everyone. + +I hope you enjoy using AdventureLog as much as I enjoy creating it! If you have any questions, feedback, or suggestions, feel free to reach out to me via the email address listed on my website. I'm always happy to hear from users and help in any way I can. Thank you for using AdventureLog, and happy travels! 🌍 diff --git a/documentation/docs/troubleshooting/login_unresponsive.md b/documentation/docs/troubleshooting/login_unresponsive.md new file mode 100644 index 0000000..9abf6a0 --- /dev/null +++ b/documentation/docs/troubleshooting/login_unresponsive.md @@ -0,0 +1,18 @@ +# Troubleshooting: Login and Registration Unresponsive + +When you encounter issues with the login and registration pages being unresponsive in AdventureLog, it can be due to various reasons. This guide will help you troubleshoot and resolve the unresponsive login and registration pages in AdventureLog. + +1. Check to make sure the backend container is running and accessible. + + - Check the backend container logs to see if there are any errors or issues blocking the container from running. +2. Check the connection between the frontend and backend containers. + + - Attempt login with the browser console network tab open to see if there are any errors or issues with the connection between the frontend and backend containers. If there is a connection issue, the code will show an error like `Failed to load resource: net::ERR_CONNECTION_REFUSED`. If this is the case, check the `PUBLIC_SERVER_URL` in the frontend container and refer to the installation docs to ensure the correct URL is set. + - If the error is `403`, continue to the next step. + +3. The error most likely is due to a CSRF security config issue in either the backend or frontend. + + - Check that the `ORIGIN` variable in the frontend is set to the URL where the frontend is access and you are accessing the app from currently. + - Check that the `CSRF_TRUSTED_ORIGINS` variable in the backend is set to a comma separated list of the origins where you use your backend server and frontend. One of these values should match the `ORIGIN` variable in the frontend. + +4. If you are still experiencing issues, please refer to the [AdventureLog Discord Server](https://discord.gg/wRbQ9Egr8C) for further assistance, providing as much detail as possible about the issue you are experiencing! diff --git a/documentation/docs/usage/usage.md b/documentation/docs/usage/usage.md new file mode 100644 index 0000000..aa27cf3 --- /dev/null +++ b/documentation/docs/usage/usage.md @@ -0,0 +1,33 @@ +# How to use AdventureLog + +Welcome to AdventureLog! This guide will help you get started with AdventureLog and provide you with an overview of the features available to you. + +## Key Terms + +#### Adventures + +- **Adventure**: think of an adventure as a point on a map, a location you want to visit, or a place you want to explore. An adventure can be anything you want it to be, from a local park to a famous landmark. +- **Visit**: a visit is added to an adventure. It contains a date and notes about when the adventure was visited. If an adventure is visited multiple times, multiple visits can be added. If there are no visits on an adventure or the date of all visits is in the future, the adventure is considered planned. If the date of the visit is in the past, the adventure is considered completed. +- **Category**: a category is a way to group adventures together. For example, you could have a category for parks, a category for museums, and a category for restaurants. +- **Tag**: a tag is a way to add additional information to an adventure. For example, you could have a tag for the type of cuisine at a restaurant or the type of art at a museum. Multiple tags can be added to an adventure. +- **Image**: an image is a photo that is added to an adventure. Images can be added to an adventure to provide a visual representation of the location or to capture a memory of the visit. These can be uploaded from your device or with a service like [Immich](/docs/configuration/immich_integration) if the integration is enabled. +- **Attachment**: an attachment is a file that is added to an adventure. Attachments can be added to an adventure to provide additional information, such as a map of the location or a brochure from the visit. + +#### Collections + +- **Collection**: a collection is a way to group adventures together. Collections are flexible and can be used in many ways. When no start or end date is added to a collection, it acts like a folder to group adventures together. When a start and end date is added to a collection, it acts like a trip to group adventures together that were visited during that time period. With start and end dates, the collection is transformed into a full itinerary with a map showing the route taken between adventures. +- **Transportation**: a transportation is a collection exclusive feature that allows you to add transportation information to your trip. This can be used to show the route taken between locations and the mode of transportation used. It can also be used to track flight information, such as flight number and departure time. +- **Lodging**: a lodging is a collection exclusive feature that allows you to add lodging information to your trip. This can be used to plan where you will stay during your trip and add notes about the lodging location. It can also be used to track reservation information, such as reservation number and check-in time. +- **Note**: a note is a collection exclusive feature that allows you to add notes to your trip. This can be used to add additional information about your trip, such as a summary of the trip or a list of things to do. Notes can be assigned to a specific day of the trip to help organize the information. +- **Checklist**: a checklist is a collection exclusive feature that allows you to add a checklist to your trip. This can be used to create a list of things to do during your trip or for planning purposes like packing lists. Checklists can be assigned to a specific day of the trip to help organize the information. + +#### World Travel + +- **World Travel**: the world travel feature of AdventureLog allows you to track the countries, regions, and cities you have visited during your lifetime. You can add visits to countries, regions, and cities, and view statistics about your travels. The world travel feature is a fun way to visualize where you have been and where you want to go next. + - **Country**: a country is a geographical area that is recognized as an independent nation. You can add visits to countries to track where you have been. + - **Region**: a region is a geographical area that is part of a country. You can add visits to regions to track where you have been within a country. + - **City**: a city is a geographical area that is a populated urban center. You can add visits to cities to track where you have been within a region. + +## Tutorial Video + + diff --git a/documentation/index.md b/documentation/index.md index 87f2640..8d5b4cf 100644 --- a/documentation/index.md +++ b/documentation/index.md @@ -31,3 +31,265 @@ features: details: "Share your adventures with friends and family and collaborate on trips together." icon: 📸 --- + +## âšĄī¸ Quick Start + +Get AdventureLog running in under 60 seconds: + +```bash [One-Line Install] +curl -sSL https://get.adventurelog.app | bash +``` + +You can also explore our [full installation guide](/docs/install/getting_started) for plenty of options, including Docker, Proxmox, Synology NAS, and more. + +## 📸 See It In Action + +::: details đŸ—‚ī¸ **Adventure Overview & Management** +Manage your full list of adventures with ease. View upcoming and past trips, filter and sort by status, date, or category to find exactly what you want quickly. +Adventure Overview +::: + +::: details 📋 **Detailed Adventure Logs** +Capture rich details for every adventure: name, dates, precise locations, vivid descriptions, personal ratings, photos, and customizable categories. Your memories deserve to be more than just map pins — keep them alive with full, organized logs. +Detailed Adventure Logs +::: + +::: details đŸ—ēī¸ **Interactive World Map** +Track every destination you’ve visited or plan to visit with our beautifully detailed, interactive world map. Easily filter locations by visit status — visitedor planned — and add new adventures by simply clicking on the map. Watch your travel story unfold visually as your journey grows. +Interactive World Map +::: + +::: details âœˆī¸ **Comprehensive Trip Planning** +Organize your multi-day trips with detailed itineraries, including flight information, daily activities, collaborative notes, packing checklists, and handy resource links. Stay on top of your plans and ensure every adventure runs smoothly. +Comprehensive Trip Planning +::: + +::: details 📊 **Travel Statistics Dashboard** +Unlock insights into your travel habits and milestones through elegant, easy-to-understand analytics. Track total countries visited, regions explored, cities logged, and more. Visualize your world travels with ease and celebrate your achievements. +Travel Statistics Dashboard +::: + +::: details âœī¸ **Edit & Customize Adventures** +Make quick updates or deep customizations to any adventure using a clean and intuitive editing interface. Add photos, update notes, adjust dates, and more—keeping your records accurate and personal. +Edit Adventure Modal +::: + +::: details 🌍 **Countries & Regions Explorer** +Explore and manage the countries you’ve visited or plan to visit with an organized list, filtering by visit status. Dive deeper into each country’s regions, complete with interactive maps to help you visually select and track your regional travels. +Countries List +Regions Explorer +::: + +## đŸ’Ŧ What People Are Saying + +::: details âœˆī¸ **XDA Travel Week Reviews** + +> “I stumbled upon AdventureLog. It's an open-source, self-hosted travel planner that's completely free to use and has a bunch of cool features that make it a treat to plan, organize, and log your journey across the world. Safe to say, it's become a mainstay in Docker for me.” +> +> — _Sumukh Rao, Senior Author at XDA_ + +[Article Link](https://www.xda-developers.com/i-self-hosted-this-app-to-plan-itinerary-when-traveling/) + +::: + +::: details đŸ§ŗ **Rich Edmonds, XDA** + +**Overall Ranking: #1** + +> “The most important part of travelling in this socially connected world is to log everything and showcase all of your adventures. AdventureLog is aptly named, as it allows you to do just that. It just so happens to be one of the best apps for the job and can be fully self-hosted at home.” +> +> — _Rich Edmonds, Lead PC Hardware Editor at XDA_ + +[Article Link](https://www.xda-developers.com/these-self-hosted-apps-are-perfect-for-those-on-the-go/) + +::: + +::: details 📆 **Open Source Daily** + +> “Your travel memories are your personal treasures—don’t let them be held hostage by closed platforms, hidden fees, or privacy risks. AdventureLog represents a new era of travel tracking: open, private, comprehensive, and truly yours. Whether you’re a casual traveler, digital nomad, family vacation planner, or anyone who values their adventures, AdventureLog offers a compelling alternative that puts you back in control.” +> +> — _Open Source Daily_ + +[Article Link](https://opensourcedaily.blog/adventurelog-private-open-source-travel-tracking-trip-planning/) + +::: + +## đŸ—ī¸ Built With Excellence + +
+ +
+ +### **Frontend Excellence** + +- 🎨 **SvelteKit** - Lightning-fast, modern web framework +- 💨 **TailwindCSS** - Utility-first styling for beautiful designs +- 🎭 **DaisyUI** - Beautiful, accessible component library +- đŸ—ēī¸ **MapLibre** - Interactive, customizable mapping + +
+ +
+ +### **Backend Power** + +- 🐍 **Django** - Robust, scalable web framework +- đŸ—ēī¸ **PostGIS** - Advanced geospatial database capabilities +- 🔌 **Django REST** - Modern API architecture +- 🔐 **AllAuth** - Comprehensive authentication system + +
+ +
+ +## 🌟 Join the Adventure + +
+ +
+ +### đŸŽ¯ **Active Development** + +Regular updates, new features, and community-driven improvements keep AdventureLog at the forefront of travel technology. + +
+ +
+ +### đŸ’Ŧ **Thriving Community** + +Join thousands of travelers sharing tips, contributing code, and building the future of travel documentation together. + +
+ +
+ +### 🚀 **Open Source Freedom** + +GPL 3.0 licensed, fully transparent, and built for the community. By travelers, for travelers. + +
+ +
+ +## 💖 Support the Project + +AdventureLog is lovingly maintained by passionate developers and supported by amazing users like you: + +- ⭐ [Star us on GitHub](https://github.com/seanmorley15/AdventureLog) +- đŸ’Ŧ [Join our Discord community](https://discord.gg/wRbQ9Egr8C) +- 💖 [Sponsor The Project](https://seanmorley.com/sponsor) to help us keep improving AdventureLog +- 🐛 [Report bugs & request features](https://github.com/seanmorley15/AdventureLog/issues) + +--- + + + + diff --git a/documentation/package.json b/documentation/package.json index c17d74f..e666f60 100644 --- a/documentation/package.json +++ b/documentation/package.json @@ -1,6 +1,6 @@ { "devDependencies": { - "vitepress": "^1.5.0" + "vitepress": "^1.6.3" }, "scripts": { "docs:dev": "vitepress dev", diff --git a/documentation/pnpm-lock.yaml b/documentation/pnpm-lock.yaml index c5fa66d..0d53d62 100644 --- a/documentation/pnpm-lock.yaml +++ b/documentation/pnpm-lock.yaml @@ -16,8 +16,8 @@ importers: version: 3.5.13 devDependencies: vitepress: - specifier: ^1.5.0 - version: 1.5.0(@algolia/client-search@5.15.0)(postcss@8.4.49)(search-insights@2.17.3) + specifier: ^1.6.3 + version: 1.6.3(@algolia/client-search@5.15.0)(postcss@8.4.49)(search-insights@2.17.3) packages: @@ -110,14 +110,14 @@ packages: resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} engines: {node: '>=6.9.0'} - '@docsearch/css@3.8.0': - resolution: {integrity: sha512-pieeipSOW4sQ0+bE5UFC51AOZp9NGxg89wAlZ1BAQFaiRAGK1IKUaPQ0UGZeNctJXyqZ1UvBtOQh2HH+U5GtmA==} + '@docsearch/css@3.8.2': + resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} - '@docsearch/js@3.8.0': - resolution: {integrity: sha512-PVuV629f5UcYRtBWqK7ID6vNL5647+2ADJypwTjfeBIrJfwPuHtzLy39hMGMfFK+0xgRyhTR0FZ83EkdEraBlg==} + '@docsearch/js@3.8.2': + resolution: {integrity: sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==} - '@docsearch/react@3.8.0': - resolution: {integrity: sha512-WnFK720+iwTVt94CxY3u+FgX6exb3BfN5kE9xUY6uuAH/9W/UFboBZFLlrw/zxFRHoHZCOXRtOylsXF+6LHI+Q==} + '@docsearch/react@3.8.2': + resolution: {integrity: sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==} peerDependencies: '@types/react': '>= 16.8.0 < 19.0.0' react: '>= 16.8.0 < 19.0.0' @@ -271,8 +271,8 @@ packages: cpu: [x64] os: [win32] - '@iconify-json/simple-icons@1.2.12': - resolution: {integrity: sha512-lRNORrIdeLStShxAjN6FgXE1iMkaAgiAHZdP0P0GZecX91FVYW58uZnRSlXLlSx5cxMoELulkAAixybPA2g52g==} + '@iconify-json/simple-icons@1.2.37': + resolution: {integrity: sha512-jZwTBznpYVDYKWyAuRpepPpCiHScVrX6f8WRX8ReX6pdii99LYVHwJywKcH2excWQrWmBomC9nkxGlEKzXZ/wQ==} '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -370,23 +370,29 @@ packages: cpu: [x64] os: [win32] - '@shikijs/core@1.23.1': - resolution: {integrity: sha512-NuOVgwcHgVC6jBVH5V7iblziw6iQbWWHrj5IlZI3Fqu2yx9awH7OIQkXIcsHsUmY19ckwSgUMgrqExEyP5A0TA==} + '@shikijs/core@2.5.0': + resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} - '@shikijs/engine-javascript@1.23.1': - resolution: {integrity: sha512-i/LdEwT5k3FVu07SiApRFwRcSJs5QM9+tod5vYCPig1Ywi8GR30zcujbxGQFJHwYD7A5BUqagi8o5KS+LEVgBg==} + '@shikijs/engine-javascript@2.5.0': + resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} - '@shikijs/engine-oniguruma@1.23.1': - resolution: {integrity: sha512-KQ+lgeJJ5m2ISbUZudLR1qHeH3MnSs2mjFg7bnencgs5jDVPeJ2NVDJ3N5ZHbcTsOIh0qIueyAJnwg7lg7kwXQ==} + '@shikijs/engine-oniguruma@2.5.0': + resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} - '@shikijs/transformers@1.23.1': - resolution: {integrity: sha512-yQ2Cn0M9i46p30KwbyIzLvKDk+dQNU+lj88RGO0XEj54Hn4Cof1bZoDb9xBRWxFE4R8nmK63w7oHnJwvOtt0NQ==} + '@shikijs/langs@2.5.0': + resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} - '@shikijs/types@1.23.1': - resolution: {integrity: sha512-98A5hGyEhzzAgQh2dAeHKrWW4HfCMeoFER2z16p5eJ+vmPeF6lZ/elEne6/UCU551F/WqkopqRsr1l2Yu6+A0g==} + '@shikijs/themes@2.5.0': + resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} - '@shikijs/vscode-textmate@9.3.0': - resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} + '@shikijs/transformers@2.5.0': + resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==} + + '@shikijs/types@2.5.0': + resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -409,17 +415,17 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@types/web-bluetooth@0.0.20': - resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@vitejs/plugin-vue@5.2.0': - resolution: {integrity: sha512-7n7KdUEtx/7Yl7I/WVAMZ1bEb0eVvXF3ummWTeLcs/9gvo9pJhuLdouSXGjdZ/MKD1acf1I272+X0RMua4/R3g==} + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: - vite: ^5.0.0 + vite: ^5.0.0 || ^6.0.0 vue: ^3.2.25 '@vue/compiler-core@3.5.13': @@ -434,14 +440,14 @@ packages: '@vue/compiler-ssr@3.5.13': resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} - '@vue/devtools-api@7.6.4': - resolution: {integrity: sha512-5AaJ5ELBIuevmFMZYYLuOO9HUuY/6OlkOELHE7oeDhy4XD/hSODIzktlsvBOsn+bto3aD0psj36LGzwVu5Ip8w==} + '@vue/devtools-api@7.7.6': + resolution: {integrity: sha512-b2Xx0KvXZObePpXPYHvBRRJLDQn5nhKjXh7vUhMEtWxz1AYNFOVIsh5+HLP8xDGL7sy+Q7hXeUxPHB/KgbtsPw==} - '@vue/devtools-kit@7.6.4': - resolution: {integrity: sha512-Zs86qIXXM9icU0PiGY09PQCle4TI750IPLmAJzW5Kf9n9t5HzSYf6Rz6fyzSwmfMPiR51SUKJh9sXVZu78h2QA==} + '@vue/devtools-kit@7.7.6': + resolution: {integrity: sha512-geu7ds7tem2Y7Wz+WgbnbZ6T5eadOvozHZ23Atk/8tksHMFOFylKi1xgGlQlVn0wlkEf4hu+vd5ctj1G4kFtwA==} - '@vue/devtools-shared@7.6.4': - resolution: {integrity: sha512-nD6CUvBEel+y7zpyorjiUocy0nh77DThZJ0k1GRnJeOmY3ATq2fWijEp7wk37gb023Cb0R396uYh5qMSBQ5WFg==} + '@vue/devtools-shared@7.7.6': + resolution: {integrity: sha512-yFEgJZ/WblEsojQQceuyK6FzpFDx4kqrz2ohInxNj5/DnhoX023upTv4OD6lNPLAA5LLkbwPVb10o/7b+Y4FVA==} '@vue/reactivity@3.5.13': resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} @@ -460,11 +466,11 @@ packages: '@vue/shared@3.5.13': resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} - '@vueuse/core@11.3.0': - resolution: {integrity: sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==} + '@vueuse/core@12.8.2': + resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} - '@vueuse/integrations@11.3.0': - resolution: {integrity: sha512-5fzRl0apQWrDezmobchoiGTkGw238VWESxZHazfhP3RM7pDSiyXy18QbfYkILoYNTd23HPAfQTJpkUc5QbkwTw==} + '@vueuse/integrations@12.8.2': + resolution: {integrity: sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==} peerDependencies: async-validator: ^4 axios: ^1 @@ -504,18 +510,18 @@ packages: universal-cookie: optional: true - '@vueuse/metadata@11.3.0': - resolution: {integrity: sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==} + '@vueuse/metadata@12.8.2': + resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} - '@vueuse/shared@11.3.0': - resolution: {integrity: sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==} + '@vueuse/shared@12.8.2': + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} algoliasearch@5.15.0: resolution: {integrity: sha512-Yf3Swz1s63hjvBVZ/9f2P1Uu48GjmjCN+Esxb6MAONMGtZB1fRX8/S1AhUTtsuTlcGovbYLxpHgc7wEzstDZBw==} engines: {node: '>= 14.0.0'} - birpc@0.2.19: - resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==} + birpc@2.3.0: + resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -558,16 +564,16 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - focus-trap@7.6.2: - resolution: {integrity: sha512-9FhUxK1hVju2+AiQIDJ5Dd//9R2n2RAfJ0qfhF4IHGHgcoEUTMpbTeG/zbEuwaiYXfuAH6XE0/aCyxDdRM+W5w==} + focus-trap@7.6.5: + resolution: {integrity: sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==} fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - hast-util-to-html@9.0.3: - resolution: {integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} @@ -617,8 +623,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - oniguruma-to-es@0.4.1: - resolution: {integrity: sha512-rNcEohFz095QKGRovP/yqPIKc+nP+Sjs4YTHMv33nMePGKrq/r2eu9Yh4646M5XluGJsUnmwoXuiXE69KDs+fQ==} + oniguruma-to-es@3.1.1: + resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -638,17 +644,17 @@ packages: engines: {node: '>=14'} hasBin: true - property-information@6.5.0: - resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - regex-recursion@4.2.1: - resolution: {integrity: sha512-QHNZyZAeKdndD1G3bKAbBEKOSSK4KOHQrAJ01N1LJeb0SoH4DJIeFhp0uUpETgONifS4+P3sOgoA1dhzgrQvhA==} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} regex-utilities@2.3.0: resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - regex@5.0.2: - resolution: {integrity: sha512-/pczGbKIQgfTMRV0XjABvc5RzLqQmwqxLHdQao2RTXPk+pmTXB2P0IaUHYdYyk412YLwUIkaeMd5T+RzVgTqnQ==} + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -661,8 +667,8 @@ packages: search-insights@2.17.3: resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} - shiki@1.23.1: - resolution: {integrity: sha512-8kxV9TH4pXgdKGxNOkrSMydn1Xf6It8lsle0fiqxf7a1149K1WGtdOu3Zb91T5r1JpvRPxqxU3C2XdZZXQnrig==} + shiki@2.5.0: + resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} @@ -678,8 +684,8 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - superjson@2.2.1: - resolution: {integrity: sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==} + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} engines: {node: '>=16'} tabbable@6.2.0: @@ -709,8 +715,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite@5.4.14: - resolution: {integrity: sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==} + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -740,8 +746,8 @@ packages: terser: optional: true - vitepress@1.5.0: - resolution: {integrity: sha512-q4Q/G2zjvynvizdB3/bupdYkCJe2umSAMv9Ju4d92E6/NXJ59z70xB0q5p/4lpRyAwflDsbwy1mLV9Q5+nlB+g==} + vitepress@1.6.3: + resolution: {integrity: sha512-fCkfdOk8yRZT8GD9BFqusW3+GggWYZ/rYncOfmgcDtP3ualNHCAg+Robxp2/6xfH1WwPHtGpPwv7mbA3qomtBw==} hasBin: true peerDependencies: markdown-it-mathjax3: ^4 @@ -752,17 +758,6 @@ packages: postcss: optional: true - vue-demi@0.14.10: - resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} - engines: {node: '>=12'} - hasBin: true - peerDependencies: - '@vue/composition-api': ^1.0.0-rc.1 - vue: ^3.0.0-0 || ^2.6.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - vue@3.5.13: resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} peerDependencies: @@ -894,11 +889,11 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@docsearch/css@3.8.0': {} + '@docsearch/css@3.8.2': {} - '@docsearch/js@3.8.0(@algolia/client-search@5.15.0)(search-insights@2.17.3)': + '@docsearch/js@3.8.2(@algolia/client-search@5.15.0)(search-insights@2.17.3)': dependencies: - '@docsearch/react': 3.8.0(@algolia/client-search@5.15.0)(search-insights@2.17.3) + '@docsearch/react': 3.8.2(@algolia/client-search@5.15.0)(search-insights@2.17.3) preact: 10.25.0 transitivePeerDependencies: - '@algolia/client-search' @@ -907,11 +902,11 @@ snapshots: - react-dom - search-insights - '@docsearch/react@3.8.0(@algolia/client-search@5.15.0)(search-insights@2.17.3)': + '@docsearch/react@3.8.2(@algolia/client-search@5.15.0)(search-insights@2.17.3)': dependencies: '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.15.0)(algoliasearch@5.15.0)(search-insights@2.17.3) '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.15.0)(algoliasearch@5.15.0) - '@docsearch/css': 3.8.0 + '@docsearch/css': 3.8.2 algoliasearch: 5.15.0 optionalDependencies: search-insights: 2.17.3 @@ -987,7 +982,7 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true - '@iconify-json/simple-icons@1.2.12': + '@iconify-json/simple-icons@1.2.37': dependencies: '@iconify/types': 2.0.0 @@ -1049,36 +1044,45 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.27.4': optional: true - '@shikijs/core@1.23.1': + '@shikijs/core@2.5.0': dependencies: - '@shikijs/engine-javascript': 1.23.1 - '@shikijs/engine-oniguruma': 1.23.1 - '@shikijs/types': 1.23.1 - '@shikijs/vscode-textmate': 9.3.0 + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - hast-util-to-html: 9.0.3 + hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@1.23.1': + '@shikijs/engine-javascript@2.5.0': dependencies: - '@shikijs/types': 1.23.1 - '@shikijs/vscode-textmate': 9.3.0 - oniguruma-to-es: 0.4.1 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 3.1.1 - '@shikijs/engine-oniguruma@1.23.1': + '@shikijs/engine-oniguruma@2.5.0': dependencies: - '@shikijs/types': 1.23.1 - '@shikijs/vscode-textmate': 9.3.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/transformers@1.23.1': + '@shikijs/langs@2.5.0': dependencies: - shiki: 1.23.1 + '@shikijs/types': 2.5.0 - '@shikijs/types@1.23.1': + '@shikijs/themes@2.5.0': dependencies: - '@shikijs/vscode-textmate': 9.3.0 + '@shikijs/types': 2.5.0 + + '@shikijs/transformers@2.5.0': + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/types': 2.5.0 + + '@shikijs/types@2.5.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - '@shikijs/vscode-textmate@9.3.0': {} + '@shikijs/vscode-textmate@10.0.2': {} '@types/estree@1.0.6': {} @@ -1101,13 +1105,13 @@ snapshots: '@types/unist@3.0.3': {} - '@types/web-bluetooth@0.0.20': {} + '@types/web-bluetooth@0.0.21': {} '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-vue@5.2.0(vite@5.4.14)(vue@3.5.13)': + '@vitejs/plugin-vue@5.2.4(vite@5.4.19)(vue@3.5.13)': dependencies: - vite: 5.4.14 + vite: 5.4.19 vue: 3.5.13 '@vue/compiler-core@3.5.13': @@ -1140,21 +1144,21 @@ snapshots: '@vue/compiler-dom': 3.5.13 '@vue/shared': 3.5.13 - '@vue/devtools-api@7.6.4': + '@vue/devtools-api@7.7.6': dependencies: - '@vue/devtools-kit': 7.6.4 + '@vue/devtools-kit': 7.7.6 - '@vue/devtools-kit@7.6.4': + '@vue/devtools-kit@7.7.6': dependencies: - '@vue/devtools-shared': 7.6.4 - birpc: 0.2.19 + '@vue/devtools-shared': 7.7.6 + birpc: 2.3.0 hookable: 5.5.3 mitt: 3.0.1 perfect-debounce: 1.0.0 speakingurl: 14.0.1 - superjson: 2.2.1 + superjson: 2.2.2 - '@vue/devtools-shared@7.6.4': + '@vue/devtools-shared@7.7.6': dependencies: rfdc: 1.4.1 @@ -1182,35 +1186,32 @@ snapshots: '@vue/shared@3.5.13': {} - '@vueuse/core@11.3.0(vue@3.5.13)': + '@vueuse/core@12.8.2': dependencies: - '@types/web-bluetooth': 0.0.20 - '@vueuse/metadata': 11.3.0 - '@vueuse/shared': 11.3.0(vue@3.5.13) - vue-demi: 0.14.10(vue@3.5.13) + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.13 transitivePeerDependencies: - - '@vue/composition-api' - - vue + - typescript - '@vueuse/integrations@11.3.0(focus-trap@7.6.2)(vue@3.5.13)': + '@vueuse/integrations@12.8.2(focus-trap@7.6.5)': dependencies: - '@vueuse/core': 11.3.0(vue@3.5.13) - '@vueuse/shared': 11.3.0(vue@3.5.13) - vue-demi: 0.14.10(vue@3.5.13) + '@vueuse/core': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.13 optionalDependencies: - focus-trap: 7.6.2 + focus-trap: 7.6.5 transitivePeerDependencies: - - '@vue/composition-api' - - vue + - typescript - '@vueuse/metadata@11.3.0': {} + '@vueuse/metadata@12.8.2': {} - '@vueuse/shared@11.3.0(vue@3.5.13)': + '@vueuse/shared@12.8.2': dependencies: - vue-demi: 0.14.10(vue@3.5.13) + vue: 3.5.13 transitivePeerDependencies: - - '@vue/composition-api' - - vue + - typescript algoliasearch@5.15.0: dependencies: @@ -1228,7 +1229,7 @@ snapshots: '@algolia/requester-fetch': 5.15.0 '@algolia/requester-node-http': 5.15.0 - birpc@0.2.19: {} + birpc@2.3.0: {} ccount@2.0.1: {} @@ -1282,14 +1283,14 @@ snapshots: estree-walker@2.0.2: {} - focus-trap@7.6.2: + focus-trap@7.6.5: dependencies: tabbable: 6.2.0 fsevents@2.3.3: optional: true - hast-util-to-html@9.0.3: + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 '@types/unist': 3.0.3 @@ -1298,7 +1299,7 @@ snapshots: hast-util-whitespace: 3.0.0 html-void-elements: 3.0.0 mdast-util-to-hast: 13.2.0 - property-information: 6.5.0 + property-information: 7.1.0 space-separated-tokens: 2.0.2 stringify-entities: 4.0.4 zwitch: 2.0.4 @@ -1354,11 +1355,11 @@ snapshots: nanoid@3.3.8: {} - oniguruma-to-es@0.4.1: + oniguruma-to-es@3.1.1: dependencies: emoji-regex-xs: 1.0.0 - regex: 5.0.2 - regex-recursion: 4.2.1 + regex: 6.0.1 + regex-recursion: 6.0.2 perfect-debounce@1.0.0: {} @@ -1374,15 +1375,15 @@ snapshots: prettier@3.3.3: {} - property-information@6.5.0: {} + property-information@7.1.0: {} - regex-recursion@4.2.1: + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 regex-utilities@2.3.0: {} - regex@5.0.2: + regex@6.0.1: dependencies: regex-utilities: 2.3.0 @@ -1414,13 +1415,15 @@ snapshots: search-insights@2.17.3: {} - shiki@1.23.1: + shiki@2.5.0: dependencies: - '@shikijs/core': 1.23.1 - '@shikijs/engine-javascript': 1.23.1 - '@shikijs/engine-oniguruma': 1.23.1 - '@shikijs/types': 1.23.1 - '@shikijs/vscode-textmate': 9.3.0 + '@shikijs/core': 2.5.0 + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/langs': 2.5.0 + '@shikijs/themes': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 source-map-js@1.2.1: {} @@ -1434,7 +1437,7 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 - superjson@2.2.1: + superjson@2.2.2: dependencies: copy-anything: 3.0.5 @@ -1475,7 +1478,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite@5.4.14: + vite@5.4.19: dependencies: esbuild: 0.21.5 postcss: 8.4.49 @@ -1483,25 +1486,25 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - vitepress@1.5.0(@algolia/client-search@5.15.0)(postcss@8.4.49)(search-insights@2.17.3): + vitepress@1.6.3(@algolia/client-search@5.15.0)(postcss@8.4.49)(search-insights@2.17.3): dependencies: - '@docsearch/css': 3.8.0 - '@docsearch/js': 3.8.0(@algolia/client-search@5.15.0)(search-insights@2.17.3) - '@iconify-json/simple-icons': 1.2.12 - '@shikijs/core': 1.23.1 - '@shikijs/transformers': 1.23.1 - '@shikijs/types': 1.23.1 + '@docsearch/css': 3.8.2 + '@docsearch/js': 3.8.2(@algolia/client-search@5.15.0)(search-insights@2.17.3) + '@iconify-json/simple-icons': 1.2.37 + '@shikijs/core': 2.5.0 + '@shikijs/transformers': 2.5.0 + '@shikijs/types': 2.5.0 '@types/markdown-it': 14.1.2 - '@vitejs/plugin-vue': 5.2.0(vite@5.4.14)(vue@3.5.13) - '@vue/devtools-api': 7.6.4 + '@vitejs/plugin-vue': 5.2.4(vite@5.4.19)(vue@3.5.13) + '@vue/devtools-api': 7.7.6 '@vue/shared': 3.5.13 - '@vueuse/core': 11.3.0(vue@3.5.13) - '@vueuse/integrations': 11.3.0(focus-trap@7.6.2)(vue@3.5.13) - focus-trap: 7.6.2 + '@vueuse/core': 12.8.2 + '@vueuse/integrations': 12.8.2(focus-trap@7.6.5) + focus-trap: 7.6.5 mark.js: 8.11.1 minisearch: 7.1.1 - shiki: 1.23.1 - vite: 5.4.14 + shiki: 2.5.0 + vite: 5.4.19 vue: 3.5.13 optionalDependencies: postcss: 8.4.49 @@ -1509,7 +1512,6 @@ snapshots: - '@algolia/client-search' - '@types/node' - '@types/react' - - '@vue/composition-api' - async-validator - axios - change-case @@ -1533,10 +1535,6 @@ snapshots: - typescript - universal-cookie - vue-demi@0.14.10(vue@3.5.13): - dependencies: - vue: 3.5.13 - vue@3.5.13: dependencies: '@vue/compiler-dom': 3.5.13 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 602960e..6a8ceb3 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,35 +1,49 @@ # Use this image as the platform to build the app -FROM node:18-alpine AS external-website +FROM node:22-alpine AS external-website -# A small line inside the image to show who made it -LABEL Developers="Sean Morley" +# Metadata labels for the AdventureLog image +LABEL maintainer="Sean Morley" \ + version="v0.10.0" \ + description="AdventureLog — the ultimate self-hosted travel companion." \ + org.opencontainers.image.title="AdventureLog" \ + org.opencontainers.image.description="AdventureLog is a self-hosted travel companion that helps you plan, track, and share your adventures." \ + org.opencontainers.image.version="v0.10.0" \ + org.opencontainers.image.authors="Sean Morley" \ + org.opencontainers.image.url="https://raw.githubusercontent.com/seanmorley15/AdventureLog/refs/heads/main/brand/banner.png" \ + org.opencontainers.image.source="https://github.com/seanmorley15/AdventureLog" \ + org.opencontainers.image.vendor="Sean Morley" \ + org.opencontainers.image.created="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + org.opencontainers.image.licenses="GPL-3.0" # The WORKDIR instruction sets the working directory for everything that will happen next WORKDIR /app -# Copy all local files into the image +# Install pnpm globally first +RUN npm install -g pnpm + +# Copy package files first for better Docker layer caching +COPY package.json pnpm-lock.yaml* ./ + +# Clean install all node modules using pnpm with frozen lockfile +RUN pnpm install --frozen-lockfile + +# Copy the rest of the application files COPY . . # Remove the development .env file if present RUN rm -f .env -# Install pnpm -RUN npm install -g pnpm - -# Clean install all node modules using pnpm -RUN pnpm install - # Build SvelteKit app RUN pnpm run build +# Make startup script executable +RUN chmod +x ./startup.sh + +# Change to non-root user for security +USER node:node + # Expose the port that the app is listening on EXPOSE 3000 -# Run the app -RUN chmod +x ./startup.sh - -# The USER instruction sets the user name to use as the default user for the remainder of the current stage -USER node:node - # Run startup.sh instead of the default command CMD ["./startup.sh"] \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index a2ed840..95ff6c9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "adventurelog-frontend", - "version": "0.8.0", + "version": "0.10.0", "scripts": { "dev": "vite dev", "django": "cd .. && cd backend/server && python3 manage.py runserver", @@ -14,6 +14,7 @@ "devDependencies": { "@event-calendar/core": "^3.7.1", "@event-calendar/day-grid": "^3.7.1", + "@event-calendar/interaction": "^3.12.0", "@event-calendar/time-grid": "^3.7.1", "@iconify-json/mdi": "^1.1.67", "@sveltejs/adapter-node": "^5.2.0", @@ -34,7 +35,7 @@ "tslib": "^2.6.3", "typescript": "^5.5.2", "unplugin-icons": "^0.19.0", - "vite": "^5.4.12" + "vite": "^5.4.19" }, "type": "module", "dependencies": { @@ -43,6 +44,7 @@ "dompurify": "^3.2.4", "emoji-picker-element": "^1.26.0", "gsap": "^3.12.7", + "luxon": "^3.6.1", "marked": "^15.0.4", "psl": "^1.15.0", "qrcode": "^1.5.4", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 318a11b..aa4490a 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -16,16 +16,19 @@ importers: version: 0.16.2 dompurify: specifier: ^3.2.4 - version: 3.2.4 + version: 3.2.5 emoji-picker-element: specifier: ^1.26.0 - version: 1.26.0 + version: 1.26.3 gsap: specifier: ^3.12.7 version: 3.12.7 + luxon: + specifier: ^3.6.1 + version: 3.6.1 marked: specifier: ^15.0.4 - version: 15.0.4 + version: 15.0.11 psl: specifier: ^1.15.0 version: 1.15.0 @@ -37,77 +40,80 @@ importers: version: 4.0.1(svelte@4.2.19) svelte-maplibre: specifier: ^0.9.8 - version: 0.9.8(svelte@4.2.19) + version: 0.9.14(svelte@4.2.19) devDependencies: '@event-calendar/core': specifier: ^3.7.1 - version: 3.7.1 + version: 3.12.0 '@event-calendar/day-grid': specifier: ^3.7.1 - version: 3.7.1 + version: 3.12.0 + '@event-calendar/interaction': + specifier: ^3.12.0 + version: 3.12.0 '@event-calendar/time-grid': specifier: ^3.7.1 - version: 3.7.1 + version: 3.12.0 '@iconify-json/mdi': specifier: ^1.1.67 - version: 1.1.67 + version: 1.2.3 '@sveltejs/adapter-node': specifier: ^5.2.0 - version: 5.2.0(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4))) + version: 5.2.12(@sveltejs/kit@2.20.7(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)))(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2))) '@sveltejs/adapter-vercel': specifier: ^5.4.1 - version: 5.4.1(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4))) + version: 5.7.0(@sveltejs/kit@2.20.7(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)))(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)))(rollup@4.40.2) '@sveltejs/kit': specifier: ^2.8.3 - version: 2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)) + version: 2.20.7(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)))(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)) '@sveltejs/vite-plugin-svelte': specifier: ^3.1.1 - version: 3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)) + version: 3.1.2(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)) '@tailwindcss/typography': specifier: ^0.5.13 - version: 0.5.13(tailwindcss@3.4.4) + version: 0.5.16(tailwindcss@3.4.17) '@types/node': specifier: ^22.5.4 - version: 22.5.4 + version: 22.15.2 '@types/qrcode': specifier: ^1.5.5 version: 1.5.5 autoprefixer: specifier: ^10.4.19 - version: 10.4.19(postcss@8.4.38) + version: 10.4.21(postcss@8.5.3) daisyui: specifier: ^4.12.6 - version: 4.12.6(postcss@8.4.38) + version: 4.12.24(postcss@8.5.3) postcss: specifier: ^8.4.38 - version: 8.4.38 + version: 8.5.3 prettier: specifier: ^3.3.2 - version: 3.3.2 + version: 3.5.3 prettier-plugin-svelte: specifier: ^3.2.5 - version: 3.2.5(prettier@3.3.2)(svelte@4.2.19) + version: 3.3.3(prettier@3.5.3)(svelte@4.2.19) svelte: specifier: ^4.2.19 version: 4.2.19 svelte-check: specifier: ^3.8.1 - version: 3.8.1(postcss-load-config@4.0.2(postcss@8.4.38))(postcss@8.4.38)(svelte@4.2.19) + version: 3.8.6(postcss-load-config@4.0.2(postcss@8.5.3))(postcss@8.5.3)(svelte@4.2.19) tailwindcss: specifier: ^3.4.4 - version: 3.4.4 + version: 3.4.17 tslib: specifier: ^2.6.3 - version: 2.6.3 + version: 2.8.1 typescript: specifier: ^5.5.2 - version: 5.5.2 + version: 5.8.3 unplugin-icons: specifier: ^0.19.0 - version: 0.19.0 + version: 0.19.3 vite: - specifier: ^5.4.12 - version: 5.4.12(@types/node@22.5.4) + specifier: ^5.4.19 + version: 5.4.19(@types/node@22.15.2) packages: @@ -119,14 +125,17 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@antfu/install-pkg@0.1.1': - resolution: {integrity: sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==} + '@antfu/install-pkg@0.4.1': + resolution: {integrity: sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==} - '@antfu/install-pkg@0.3.3': - resolution: {integrity: sha512-nHHsk3NXQ6xkCfiRRC8Nfrg8pU5kkr3P3Y9s9dKqiuRmBD0Yap7fymNDjGFKeWhZQHqqbCS5CfeMy9wtExM24w==} + '@antfu/install-pkg@1.0.0': + resolution: {integrity: sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==} - '@antfu/utils@0.7.8': - resolution: {integrity: sha512-rWQkqXRESdjXtc+7NRfK9lASQjpXJu1ayp7qi1d23zZorY+wBHVLHHoVcMsEnkqEBWTFqbztO7/QdJFzyEcLTg==} + '@antfu/utils@0.7.10': + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + + '@antfu/utils@8.1.1': + resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} @@ -140,6 +149,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.19.12': resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} engines: {node: '>=12'} @@ -152,6 +167,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.19.12': resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} engines: {node: '>=12'} @@ -164,6 +185,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.24.2': + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.19.12': resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} engines: {node: '>=12'} @@ -176,6 +203,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.19.12': resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} engines: {node: '>=12'} @@ -188,6 +221,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.19.12': resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} engines: {node: '>=12'} @@ -200,6 +239,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.19.12': resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} engines: {node: '>=12'} @@ -212,6 +257,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.19.12': resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} engines: {node: '>=12'} @@ -224,6 +275,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.19.12': resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} engines: {node: '>=12'} @@ -236,6 +293,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.19.12': resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} engines: {node: '>=12'} @@ -248,6 +311,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.24.2': + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.19.12': resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} engines: {node: '>=12'} @@ -260,6 +329,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.19.12': resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} engines: {node: '>=12'} @@ -272,6 +347,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.19.12': resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} engines: {node: '>=12'} @@ -284,6 +365,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.24.2': + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.19.12': resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} engines: {node: '>=12'} @@ -296,6 +383,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.19.12': resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} engines: {node: '>=12'} @@ -308,6 +401,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.19.12': resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} engines: {node: '>=12'} @@ -320,6 +419,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.19.12': resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} engines: {node: '>=12'} @@ -332,6 +437,18 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.19.12': resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} engines: {node: '>=12'} @@ -344,6 +461,18 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.19.12': resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} engines: {node: '>=12'} @@ -356,6 +485,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/sunos-x64@0.19.12': resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} engines: {node: '>=12'} @@ -368,6 +503,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.19.12': resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} engines: {node: '>=12'} @@ -380,6 +521,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.19.12': resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} engines: {node: '>=12'} @@ -392,6 +539,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.19.12': resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} engines: {node: '>=12'} @@ -404,45 +557,58 @@ packages: cpu: [x64] os: [win32] - '@event-calendar/core@3.7.1': - resolution: {integrity: sha512-S5D4arG7b47uhXmcT/rC7FT3UO9+KB+QhDuhfOzDgKCpAFlEBU1wt1UoHmPTbGy3J+yVMR+rmcresYUvM44+pA==} + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] - '@event-calendar/day-grid@3.7.1': - resolution: {integrity: sha512-kwmadkhUxtQDv+0azMkePrmilFp5dljWLHsluHl1uepfJa1yXlrvFy3GMFnYuPo2Gva0MV+HnU/GMqVG8vIcWw==} + '@event-calendar/core@3.12.0': + resolution: {integrity: sha512-aKtDwEKzWHOV2PLVdhR/f843ecW3C0w5G5VhGl1f0GBEgT8dZvoYreQ7QKTBWh7B7YEtEn2P0Dn36zFYj5XEXQ==} - '@event-calendar/time-grid@3.7.1': - resolution: {integrity: sha512-kPC4+XhFcSoNSnYG0TSQeGylpvrbFF1g+cTcFFIW6qH3wPIeBBCo0fRuD4Tr5/q4ewZQ5lNrCkZXOpZxHJxOfw==} + '@event-calendar/day-grid@3.12.0': + resolution: {integrity: sha512-gY6XvEIlwWI9uKWsXukyanDmrEWv1UDHdhikhchpe6iZP25p3+760qXIU2kdu91tXjb+hVbpFcn7sdNPPE4u7Q==} - '@formatjs/ecma402-abstract@2.2.1': - resolution: {integrity: sha512-O4ywpkdJybrjFc9zyL8qK5aklleIAi5O4nYhBVJaOFtCkNrnU+lKFeJOFC48zpsZQmR8Aok2V79hGpHnzbmFpg==} + '@event-calendar/interaction@3.12.0': + resolution: {integrity: sha512-+d3KqxNdcY/RfJrdai37XCoTx7KKpzqJIo/WAjH1p8ZiypsfrHgpWWuTtF76u3hpn/1qqWUM3VFJSTKbjJkWTg==} - '@formatjs/fast-memoize@2.2.2': - resolution: {integrity: sha512-mzxZcS0g1pOzwZTslJOBTmLzDXseMLLvnh25ymRilCm8QLMObsQ7x/rj9GNrH0iUhZMlFisVOD6J1n6WQqpKPQ==} + '@event-calendar/time-grid@3.12.0': + resolution: {integrity: sha512-n/IoFSq/ym6ad2k+H9RL2A8GpfOJy1zpKKLb1Edp/QEusexpPg8LNdSbxhmKGz6ip5ud0Bi/xgUa8xUqut8ooQ==} - '@formatjs/icu-messageformat-parser@2.9.1': - resolution: {integrity: sha512-7AYk4tjnLi5wBkxst2w7qFj38JLMJoqzj7BhdEl7oTlsWMlqwgx4p9oMmmvpXWTSDGNwOKBRc1SfwMh5MOHeNg==} + '@formatjs/ecma402-abstract@2.3.4': + resolution: {integrity: sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==} - '@formatjs/icu-skeleton-parser@1.8.5': - resolution: {integrity: sha512-zRZ/e3B5qY2+JCLs7puTzWS1Jb+t/K+8Jur/gEZpA2EjWeLDE17nsx8thyo9P48Mno7UmafnPupV2NCJXX17Dg==} + '@formatjs/fast-memoize@2.2.7': + resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} - '@formatjs/intl-localematcher@0.5.6': - resolution: {integrity: sha512-roz1+Ba5e23AHX6KUAWmLEyTRZegM5YDuxuvkHCyK3RJddf/UXB2f+s7pOMm9ktfPGla0g+mQXOn5vsuYirnaA==} + '@formatjs/icu-messageformat-parser@2.11.2': + resolution: {integrity: sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==} - '@iconify-json/mdi@1.1.67': - resolution: {integrity: sha512-00nllHES8hyACwIfgySlQgAE6MKgpr2wsKfpifMiZWZ9aXC5l4Jb0lR3lJSWwXgOW6kzAOdzC3T+2VOfBBZ13A==} + '@formatjs/icu-skeleton-parser@1.8.14': + resolution: {integrity: sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==} + + '@formatjs/intl-localematcher@0.6.1': + resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==} + + '@iconify-json/mdi@1.2.3': + resolution: {integrity: sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg==} '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - '@iconify/utils@2.1.25': - resolution: {integrity: sha512-Y+iGko8uv/Fz5bQLLJyNSZGOdMW0G7cnlEX1CiNcKsRXX9cq/y/vwxrIAtLCZhKHr3m0VJmsjVPsvnM4uX8YLg==} + '@iconify/utils@2.3.0': + resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} '@jridgewell/resolve-uri@3.1.2': @@ -453,16 +619,12 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.4.15': - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jsdevtools/ez-spawn@3.0.4': - resolution: {integrity: sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==} - engines: {node: '>=10'} - '@lukulent/svelte-umami@0.0.3': resolution: {integrity: sha512-4pL0sJapfy14yDj6CyZgewbRDadRoBJtk/dLqCJh7/tQuX7HO4hviBzhrVa4Osxaq2kcGEKdpkhAKAoaNdlNSA==} peerDependencies: @@ -476,8 +638,9 @@ packages: resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} engines: {node: '>= 0.6'} - '@mapbox/node-pre-gyp@1.0.11': - resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + '@mapbox/node-pre-gyp@2.0.0': + resolution: {integrity: sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==} + engines: {node: '>=18'} hasBin: true '@mapbox/point-geometry@0.1.0': @@ -500,8 +663,8 @@ packages: resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} engines: {node: '>=6.0.0'} - '@maplibre/maplibre-gl-style-spec@20.3.0': - resolution: {integrity: sha512-eSiQ3E5LUSxAOY9ABXGyfNhout2iEa6mUxKeaQ9nJ8NL1NuaQYU7zKqzx/LEYcXe1neT4uYAgM1wYZj3fTSXtA==} + '@maplibre/maplibre-gl-style-spec@20.4.0': + resolution: {integrity: sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==} hasBin: true '@nodelib/fs.scandir@2.1.5': @@ -520,11 +683,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@polka/url@1.0.0-next.25': - resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@rollup/plugin-commonjs@26.0.1': - resolution: {integrity: sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ==} + '@rollup/plugin-commonjs@28.0.3': + resolution: {integrity: sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==} engines: {node: '>=16.0.0 || 14 >= 14.17'} peerDependencies: rollup: ^2.68.0||^3.0.0||^4.0.0 @@ -541,8 +704,8 @@ packages: rollup: optional: true - '@rollup/plugin-node-resolve@15.2.3': - resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} + '@rollup/plugin-node-resolve@16.0.1': + resolution: {integrity: sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^2.78.0||^3.0.0||^4.0.0 @@ -550,12 +713,8 @@ packages: rollup: optional: true - '@rollup/pluginutils@4.2.1': - resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} - engines: {node: '>= 8.0.0'} - - '@rollup/pluginutils@5.1.0': - resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -563,199 +722,124 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.24.0': - resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==} + '@rollup/rollup-android-arm-eabi@4.40.2': + resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm-eabi@4.31.0': - resolution: {integrity: sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.24.0': - resolution: {integrity: sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==} + '@rollup/rollup-android-arm64@4.40.2': + resolution: {integrity: sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==} cpu: [arm64] os: [android] - '@rollup/rollup-android-arm64@4.31.0': - resolution: {integrity: sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.24.0': - resolution: {integrity: sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==} + '@rollup/rollup-darwin-arm64@4.40.2': + resolution: {integrity: sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-arm64@4.31.0': - resolution: {integrity: sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.24.0': - resolution: {integrity: sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==} + '@rollup/rollup-darwin-x64@4.40.2': + resolution: {integrity: sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-darwin-x64@4.31.0': - resolution: {integrity: sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.31.0': - resolution: {integrity: sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==} + '@rollup/rollup-freebsd-arm64@4.40.2': + resolution: {integrity: sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.31.0': - resolution: {integrity: sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==} + '@rollup/rollup-freebsd-x64@4.40.2': + resolution: {integrity: sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.24.0': - resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==} + '@rollup/rollup-linux-arm-gnueabihf@4.40.2': + resolution: {integrity: sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-gnueabihf@4.31.0': - resolution: {integrity: sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==} + '@rollup/rollup-linux-arm-musleabihf@4.40.2': + resolution: {integrity: sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.24.0': - resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.31.0': - resolution: {integrity: sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.24.0': - resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==} + '@rollup/rollup-linux-arm64-gnu@4.40.2': + resolution: {integrity: sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.31.0': - resolution: {integrity: sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==} + '@rollup/rollup-linux-arm64-musl@4.40.2': + resolution: {integrity: sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.24.0': - resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.31.0': - resolution: {integrity: sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loongarch64-gnu@4.31.0': - resolution: {integrity: sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==} + '@rollup/rollup-linux-loongarch64-gnu@4.40.2': + resolution: {integrity: sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.24.0': - resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==} + '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': + resolution: {integrity: sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.31.0': - resolution: {integrity: sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.24.0': - resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==} + '@rollup/rollup-linux-riscv64-gnu@4.40.2': + resolution: {integrity: sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.31.0': - resolution: {integrity: sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==} + '@rollup/rollup-linux-riscv64-musl@4.40.2': + resolution: {integrity: sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.24.0': - resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==} + '@rollup/rollup-linux-s390x-gnu@4.40.2': + resolution: {integrity: sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.31.0': - resolution: {integrity: sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.24.0': - resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==} + '@rollup/rollup-linux-x64-gnu@4.40.2': + resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.31.0': - resolution: {integrity: sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==} + '@rollup/rollup-linux-x64-musl@4.40.2': + resolution: {integrity: sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.24.0': - resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.31.0': - resolution: {integrity: sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.24.0': - resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==} + '@rollup/rollup-win32-arm64-msvc@4.40.2': + resolution: {integrity: sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-arm64-msvc@4.31.0': - resolution: {integrity: sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.24.0': - resolution: {integrity: sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==} + '@rollup/rollup-win32-ia32-msvc@4.40.2': + resolution: {integrity: sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.31.0': - resolution: {integrity: sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.24.0': - resolution: {integrity: sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==} + '@rollup/rollup-win32-x64-msvc@4.40.2': + resolution: {integrity: sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.31.0': - resolution: {integrity: sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==} - cpu: [x64] - os: [win32] - - '@sveltejs/adapter-node@5.2.0': - resolution: {integrity: sha512-HVZoei2078XSyPmvdTHE03VXDUD0ytTvMuMHMQP0j6zX4nPDpCcKrgvU7baEblMeCCMdM/shQvstFxOJPQKlUQ==} + '@sveltejs/adapter-node@5.2.12': + resolution: {integrity: sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==} peerDependencies: '@sveltejs/kit': ^2.4.0 - '@sveltejs/adapter-vercel@5.4.1': - resolution: {integrity: sha512-JLcD1OgMnu9lQ8EssxVGxv7w0waWuyVzItTT1eqIH98Krufd9qfr1uC9zgo82z3dJ9v1AfPEbvIX5tonceg7XQ==} + '@sveltejs/adapter-vercel@5.7.0': + resolution: {integrity: sha512-Bd/loKugyr12I576NaktLzIHa0PinS638wuWgVq4ctPg/qmkeU459jurWjs3NiRN/pbBpXOlk8i8HXgQF+dsUg==} peerDependencies: '@sveltejs/kit': ^2.4.0 - '@sveltejs/kit@2.8.3': - resolution: {integrity: sha512-DVBVwugfzzn0SxKA+eAmKqcZ7aHZROCHxH7/pyrOi+HLtQ721eEsctGb9MkhEuqj6q/9S/OFYdn37vdxzFPdvw==} + '@sveltejs/kit@2.20.7': + resolution: {integrity: sha512-dVbLMubpJJSLI4OYB+yWYNHGAhgc2bVevWuBjDj8jFUXIJOAnLwYP3vsmtcgoxNGUXoq0rHS5f7MFCsryb6nzg==} engines: {node: '>=18.13'} hasBin: true peerDependencies: - '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 svelte: ^4.0.0 || ^5.0.0-next.0 - vite: ^5.0.3 + vite: ^5.0.3 || ^6.0.0 '@sveltejs/vite-plugin-svelte-inspector@2.1.0': resolution: {integrity: sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==} @@ -765,35 +849,32 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 vite: ^5.0.0 - '@sveltejs/vite-plugin-svelte@3.1.1': - resolution: {integrity: sha512-rimpFEAboBBHIlzISibg94iP09k/KYdHgVhJlcsTfn7KMBhc70jFX/GRWkRdFCc2fdnk+4+Bdfej23cMDnJS6A==} + '@sveltejs/vite-plugin-svelte@3.1.2': + resolution: {integrity: sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==} engines: {node: ^18.0.0 || >=20} peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 vite: ^5.0.0 - '@tailwindcss/typography@0.5.13': - resolution: {integrity: sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==} + '@tailwindcss/typography@0.5.16': + resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} peerDependencies: - tailwindcss: '>=3.0.0 || insiders' + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} '@types/geojson-vt@3.2.5': resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==} - '@types/geojson@7946.0.14': - resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - '@types/junit-report-builder@3.0.2': - resolution: {integrity: sha512-R5M+SYhMbwBeQcNXYWNCZkl09vkVfAtcPIaCGdzIkkbeaTrVbGQ7HVgi4s+EmM/M1K4ZuWQH0jGcvMvNePfxYA==} - - '@types/leaflet@1.9.12': - resolution: {integrity: sha512-BK7XS+NyRI291HIo0HCfE18Lp8oA30H1gpi1tf0mF3TgiCEzanQjOqNZ4x126SXzzi2oNSZhZ5axJp1k0iM6jg==} + '@types/leaflet@1.9.17': + resolution: {integrity: sha512-IJ4K6t7I3Fh5qXbQ1uwL3CFVbCi6haW9+53oLWgdKlLP7EaS21byWFJxxqOx9y8I0AP0actXSJLVMbyvxhkUTA==} '@types/mapbox__point-geometry@0.1.4': resolution: {integrity: sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==} @@ -801,8 +882,8 @@ packages: '@types/mapbox__vector-tile@1.3.4': resolution: {integrity: sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==} - '@types/node@22.5.4': - resolution: {integrity: sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==} + '@types/node@22.15.2': + resolution: {integrity: sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==} '@types/pbf@3.0.5': resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} @@ -822,38 +903,39 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@vercel/nft@0.27.2': - resolution: {integrity: sha512-7LeioS1yE5hwPpQfD3DdH04tuugKjo5KrJk3yK5kAI3Lh76iSsK/ezoFQfzuT08X3ZASQOd1y9ePjLNI9+TxTQ==} - engines: {node: '>=16'} + '@vercel/nft@0.29.2': + resolution: {integrity: sha512-A/Si4mrTkQqJ6EXJKv5EYCDQ3NL6nJXxG8VGXePsaiQigsomHYQC9xSpX8qGk7AEZk4b1ssbYIqJ0ISQQ7bfcA==} + engines: {node: '>=18'} hasBin: true '@xmldom/xmldom@0.8.10': resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} engines: {node: '>=10.0.0'} - abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + abbrev@3.0.1: + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} + engines: {node: ^18.17.0 || >=20.5.0} acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: acorn: ^8 - acorn@8.12.0: - resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==} + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} engines: {node: '>=0.4.0'} hasBin: true - agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} ansi-styles@4.3.0: @@ -871,40 +953,26 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - aproba@2.0.0: - resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} - - are-we-there-yet@2.0.0: - resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} - engines: {node: '>=10'} - deprecated: This package is no longer supported. - arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - - arr-union@3.1.0: - resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} - engines: {node: '>=0.10.0'} - - assign-symbols@1.0.0: - resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} - engines: {node: '>=0.10.0'} + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} async-sema@3.1.1: resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} - autoprefixer@10.4.19: - resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: postcss: ^8.1.0 - axobject-query@4.0.0: - resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==} + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -916,18 +984,18 @@ packages: bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.23.1: - resolution: {integrity: sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==} + browserslist@4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -938,23 +1006,6 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - builtin-modules@3.3.0: - resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} - engines: {node: '>=6'} - - bytewise-core@1.2.3: - resolution: {integrity: sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==} - - bytewise@1.1.0: - resolution: {integrity: sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==} - - call-me-maybe@1.0.2: - resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} @@ -963,16 +1014,16 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - caniuse-lite@1.0.30001688: - resolution: {integrity: sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==} + caniuse-lite@1.0.30001715: + resolution: {integrity: sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==} chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} - chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} cli-color@2.0.4: resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==} @@ -991,10 +1042,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-support@1.1.3: - resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} - hasBin: true - commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1009,11 +1056,15 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} - confbox@0.1.7: - resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - console-control-strings@1.1.0: - resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} @@ -1051,12 +1102,12 @@ packages: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} - daisyui@4.12.6: - resolution: {integrity: sha512-Tz/rvi2ws7+7uh51JgGpsRqnASwI13t6Sz53ePaGkhLzhr4SQI4wwNxSypE8lj/d4gl/+lbHK1phIKUo+d2YNw==} + daisyui@4.12.24: + resolution: {integrity: sha512-JYg9fhQHOfXyLadrBrEqCDM6D5dWCSSiM6eTNCRrBRzx/VlOCrLS8eDfIw9RVvs64v2mJdLooKXY8EwQzoszAA==} engines: {node: '>=16.9.0'} - debug@4.3.5: - resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -1068,13 +1119,13 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + decimal.js@10.5.0: + resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - delegates@1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1083,8 +1134,8 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} devalue@5.1.1: @@ -1099,20 +1150,20 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - dompurify@3.2.4: - resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==} + dompurify@3.2.5: + resolution: {integrity: sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==} - earcut@2.2.4: - resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} + earcut@3.0.1: + resolution: {integrity: sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==} eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.4.810: - resolution: {integrity: sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==} + electron-to-chromium@1.5.143: + resolution: {integrity: sha512-QqklJMOFBMqe46k8iIOwA9l2hz57V2OKMmP5eSWcUvwx+mASAsbU+wkF1pHjn9ZVSBPrsYWr4/W/95y5SwYg2g==} - emoji-picker-element@1.26.0: - resolution: {integrity: sha512-IcffFc+LNymYScmMuxOJooZulOCOACGc1Xvj+s7XeKqpc+0EoZfWrV9o4rBjEiuM7XjsgcEjD+m5DHg0aIfnnA==} + emoji-picker-element@1.26.3: + resolution: {integrity: sha512-fOMG44d/3OqTe1pPqlu5H4ZtWg7gK4Le6Bt24JTKtDyce5+EO3Mo8WA95cKHbPSsSsg7ehM12M1x3Y6U6fgvTQ==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1147,12 +1198,17 @@ packages: engines: {node: '>=12'} hasBin: true - escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - esm-env@1.0.0: - resolution: {integrity: sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==} + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} esniff@2.0.1: resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} @@ -1167,30 +1223,29 @@ packages: event-emitter@0.3.5: resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} + exsolve@1.0.5: + resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==} ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} - extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - - extend-shallow@3.0.2: - resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} - engines: {node: '>=0.10.0'} - - fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} fastparse@1.1.2: resolution: {integrity: sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==} - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.4.4: + resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -1206,21 +1261,13 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - foreground-child@3.2.1: - resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1232,11 +1279,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - gauge@3.0.2: - resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} - engines: {node: '>=10'} - deprecated: This package is no longer supported. - geojson-vt@4.0.2: resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==} @@ -1248,10 +1290,6 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-value@2.0.6: - resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} - engines: {node: '>=0.10.0'} - gl-matrix@3.4.3: resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==} @@ -1263,18 +1301,21 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.4.2: - resolution: {integrity: sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==} - engines: {node: '>=16 || 14 >=14.18'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported - global-prefix@3.0.0: - resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} - engines: {node: '>=6'} + global-prefix@4.0.0: + resolution: {integrity: sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==} + engines: {node: '>=16'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} globalyzer@0.1.0: resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} @@ -1288,28 +1329,17 @@ packages: gsap@3.12.7: resolution: {integrity: sha512-V4GsyVamhmKefvcAKaoy0h6si0xX7ogwBoBSs2CTJwt7luW0oZzC0LhdkyuKV8PJAXr7Yaj8pMjCKD4GJ+eEMg==} - has-unicode@2.0.1: - resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} - hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - import-meta-resolve@4.1.0: resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} @@ -1320,36 +1350,25 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - intl-messageformat@10.7.3: - resolution: {integrity: sha512-AAo/3oyh7ROfPhDuh7DxTTydh97OC+lv7h1Eq5LuHWuLsUMKOhtzTYuyXlUReuwZ9vANDHo4CS1bGRrn7TZRtg==} + intl-messageformat@10.7.16: + resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==} is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - is-builtin-module@3.2.1: - resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} - engines: {node: '>=6'} - - is-core-module@2.14.0: - resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} - is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - - is-extendable@1.0.1: - resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} - engines: {node: '>=0.10.0'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1369,36 +1388,27 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-plain-object@2.0.4: - resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} - engines: {node: '>=0.10.0'} - is-promise@2.2.2: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} - is-reference@3.0.2: - resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isobject@3.0.1: - resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} - engines: {node: '>=0.10.0'} + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} - jackspeak@3.4.0: - resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==} - engines: {node: '>=14'} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jiti@1.21.6: - resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true json-stringify-pretty-compact@4.0.0: @@ -1424,19 +1434,19 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - - lilconfig@3.1.2: - resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - local-pkg@0.5.0: - resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + + local-pkg@1.1.1: + resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} engines: {node: '>=14'} locate-character@3.0.0: @@ -1446,10 +1456,6 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - lodash.castarray@4.4.0: resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} @@ -1459,26 +1465,25 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lru-cache@10.2.2: - resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} - engines: {node: 14 || >=16.14} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} lru-queue@0.1.0: resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} - magic-string@0.30.10: - resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + luxon@3.6.1: + resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} + engines: {node: '>=12'} - make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - maplibre-gl@4.5.0: - resolution: {integrity: sha512-qOS1hn4d/pn2i0uva4S5Oz+fACzTkgBKq+NpwT/Tqzi4MSyzcWNtDELzLUSgWqHfNIkGCl5CZ/w7dtis+t4RCw==} + maplibre-gl@4.7.1: + resolution: {integrity: sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==} engines: {node: '>=16.14.0', npm: '>=8.1.0'} - marked@15.0.4: - resolution: {integrity: sha512-TCHvDqmb3ZJ4PWG7VEGVgtefA5/euFmsIhxtD0XsBxI39gUSKL81mIRFdt0AiNQozUahd4ke98ZdirExd/vSEw==} + marked@15.0.11: + resolution: {integrity: sha512-1BEXAU2euRCG3xwgLVT1y0xbJEld1XOrmRJpUwRCcy7rxhSCwMrmEu9LXoPhHSCJG41V7YcQ2mjKRr5BA3ITIA==} engines: {node: '>= 18'} hasBin: true @@ -1489,9 +1494,6 @@ packages: resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==} engines: {node: '>=0.12'} - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1500,10 +1502,6 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -1511,51 +1509,43 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@9.0.4: - resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - - minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true - mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} hasBin: true - mlly@1.7.1: - resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} - mrmime@2.0.0: - resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} murmurhash-js@1.0.0: resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} @@ -1563,8 +1553,8 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1580,16 +1570,16 @@ packages: encoding: optional: true - node-gyp-build@4.8.1: - resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-releases@2.0.14: - resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - nopt@5.0.0: - resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} - engines: {node: '>=6'} + nopt@8.1.0: + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} + engines: {node: ^18.17.0 || >=20.5.0} hasBin: true normalize-path@3.0.0: @@ -1600,14 +1590,6 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - - npmlog@5.0.1: - resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} - deprecated: This package is no longer supported. - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1619,36 +1601,23 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - package-json-from-dist@1.0.0: - resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} @@ -1669,19 +1638,16 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pbf@3.2.1: - resolution: {integrity: sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==} + pbf@3.3.0: + resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} hasBin: true periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} - picocolors@1.0.1: - resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1689,19 +1655,26 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} - pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - pkg-types@1.1.1: - resolution: {integrity: sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - pmtiles@3.0.6: - resolution: {integrity: sha512-IdeMETd5lBIDVTLul1HFl0Q7l4KLJjzdxgcp+sN7pYvbipaV7o/0u0HiV06kaFCD0IGEN8KtUHyFZpY30WMflw==} + pkg-types@2.1.0: + resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==} + + pmtiles@3.2.1: + resolution: {integrity: sha512-3R4fBwwoli5mw7a6t1IGwOtfmcSAODq6Okz0zkXhS1zi9sz1ssjjIfslwPvcWw5TNhdjNBUg9fgfPLeqZlH6ng==} pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} @@ -1731,8 +1704,8 @@ packages: ts-node: optional: true - postcss-nested@6.0.1: - resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} engines: {node: '>=12.0'} peerDependencies: postcss: ^8.2.14 @@ -1741,32 +1714,28 @@ packages: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} - postcss-selector-parser@6.1.0: - resolution: {integrity: sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==} + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.4.38: - resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} - engines: {node: ^10 || ^12 || >=14} - - postcss@8.5.1: - resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} potpack@2.0.0: resolution: {integrity: sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==} - prettier-plugin-svelte@3.2.5: - resolution: {integrity: sha512-vP/M/Goc8z4iVIvrwXwbrYVjJgA0Hf8PO1G4LBh/ocSt6vUP6sLvyu9F3ABEGr+dbKyxZjEKLkeFsWy/yYl0HQ==} + prettier-plugin-svelte@3.3.3: + resolution: {integrity: sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==} peerDependencies: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier@3.3.2: - resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==} + prettier@3.5.3: + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} engines: {node: '>=14'} hasBin: true @@ -1785,12 +1754,18 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} quickselect@2.0.0: resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} + quickselect@3.0.0: + resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} + read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -1809,10 +1784,6 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -1820,12 +1791,13 @@ packages: resolve-protobuf-schema@2.1.0: resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} - resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} hasBin: true - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} rimraf@2.7.1: @@ -1833,18 +1805,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - rollup@4.24.0: - resolution: {integrity: sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - rollup@4.31.0: - resolution: {integrity: sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==} + rollup@4.40.2: + resolution: {integrity: sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1864,24 +1826,16 @@ packages: sander@0.5.1: resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.6.2: - resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} hasBin: true set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - set-cookie-parser@2.6.0: - resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} - - set-value@2.0.1: - resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} - engines: {node: '>=0.10.0'} + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -1891,49 +1845,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - sirv@3.0.0: - resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} engines: {node: '>=18'} sorcery@0.11.1: resolution: {integrity: sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==} hasBin: true - sort-asc@0.2.0: - resolution: {integrity: sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==} - engines: {node: '>=0.10.0'} - - sort-desc@0.2.0: - resolution: {integrity: sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==} - engines: {node: '>=0.10.0'} - - sort-object@3.0.3: - resolution: {integrity: sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==} - engines: {node: '>=0.10.0'} - - source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} - engines: {node: '>=0.10.0'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - split-string@3.1.0: - resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} - engines: {node: '>=0.10.0'} - - string-argv@0.3.2: - resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} - engines: {node: '>=0.6.19'} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1953,10 +1880,6 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -1973,8 +1896,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte-check@3.8.1: - resolution: {integrity: sha512-KlQ0TRVe01mdvh49Ylkr9FQxO/UWbQOtaIrccl3gjgkvby1TxY41VkT7ijCl6i29FjaJPE4m6YGmhdqov0MfkA==} + svelte-check@3.8.6: + resolution: {integrity: sha512-ij0u4Lw/sOTREP13BdWZjiXD/BlHE6/e2e34XzmVmsp5IN4kVa3PWP65NM32JAgwjZlwBg/+JtiNV1MM8khu0Q==} hasBin: true peerDependencies: svelte: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 @@ -1992,8 +1915,8 @@ packages: peerDependencies: svelte: ^3 || ^4 || ^5 - svelte-maplibre@0.9.8: - resolution: {integrity: sha512-z6YyJv1sT8AHJuzuzd+30M9PQMllFnGBpHvSJ5BlwFQF/yP4xdJY9+ynF9ziywJIzGMjuvTiCeEXZSY0fhsTAA==} + svelte-maplibre@0.9.14: + resolution: {integrity: sha512-5HBvibzU/Uf3g8eEz4Hty5XAwoBhW9Tp7NQEvb80U/glR/M1IHyzUKss6XMq8Zbci2wtsASeoPc6dA5R4+0e0w==} peerDependencies: '@deck.gl/core': ^8.8.0 '@deck.gl/layers': ^8.8.0 @@ -2048,14 +1971,14 @@ packages: resolution: {integrity: sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==} engines: {node: '>=16'} - tailwindcss@3.4.4: - resolution: {integrity: sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==} + tailwindcss@3.4.17: + resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} hasBin: true - tar@6.2.1: - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} - engines: {node: '>=10'} + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} @@ -2071,8 +1994,11 @@ packages: tiny-glob@0.2.9: resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} - tinyqueue@2.0.3: - resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyqueue@3.0.0: + resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -2088,12 +2014,8 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - tslib@2.6.3: - resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} - - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} @@ -2101,29 +2023,19 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript@5.5.2: - resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==} + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} hasBin: true - typewise-core@1.2.0: - resolution: {integrity: sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==} + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} - typewise@1.0.3: - resolution: {integrity: sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - ufo@1.5.3: - resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} - - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - - union-value@1.0.1: - resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} - engines: {node: '>=0.10.0'} - - unplugin-icons@0.19.0: - resolution: {integrity: sha512-u5g/gIZPZEj1wUGEQxe9nzftOSqmblhusc+sL3cawIRoIt/xWpE6XYcPOfAeFTYNjSbRrX/3QiX89PFiazgU1w==} + unplugin-icons@0.19.3: + resolution: {integrity: sha512-EUegRmsAI6+rrYr0vXjFlIP+lg4fSC4zb62zAZKx8FGXlWAGgEGBCa3JDe27aRAXhistObLPbBPhwa/0jYLFkQ==} peerDependencies: '@svgr/core': '>=7.0.0' '@svgx/core': ^1.0.1 @@ -2142,12 +2054,12 @@ packages: vue-template-es2015-compiler: optional: true - unplugin@1.10.1: - resolution: {integrity: sha512-d6Mhq8RJeGA8UfKCu54Um4lFA0eSaRa3XxdAJg8tIdxbu1ubW0hBCZUL7yI2uGyYCRndvbK8FLHzqy2XKfeMsg==} + unplugin@1.16.1: + resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} engines: {node: '>=14.0.0'} - update-browserslist-db@1.0.16: - resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -2155,8 +2067,8 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - vite@5.4.12: - resolution: {integrity: sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA==} + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -2200,10 +2112,6 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webpack-sources@3.2.3: - resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} - engines: {node: '>=10.13.0'} - webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -2213,17 +2121,15 @@ packages: which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true - wide-align@1.1.5: - resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} @@ -2243,11 +2149,12 @@ packages: y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} - yaml@2.4.5: - resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==} + yaml@2.7.1: + resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} engines: {node: '>= 14'} hasBin: true @@ -2259,29 +2166,28 @@ packages: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} engines: {node: '>=8'} - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - snapshots: '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/install-pkg@0.1.1': + '@antfu/install-pkg@0.4.1': dependencies: - execa: 5.1.1 - find-up: 5.0.0 + package-manager-detector: 0.2.11 + tinyexec: 0.3.2 - '@antfu/install-pkg@0.3.3': + '@antfu/install-pkg@1.0.0': dependencies: - '@jsdevtools/ez-spawn': 3.0.4 + package-manager-detector: 0.2.11 + tinyexec: 0.3.2 - '@antfu/utils@0.7.8': {} + '@antfu/utils@0.7.10': {} + + '@antfu/utils@8.1.1': {} '@esbuild/aix-ppc64@0.19.12': optional: true @@ -2289,192 +2195,274 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true + '@esbuild/aix-ppc64@0.24.2': + optional: true + '@esbuild/android-arm64@0.19.12': optional: true '@esbuild/android-arm64@0.21.5': optional: true + '@esbuild/android-arm64@0.24.2': + optional: true + '@esbuild/android-arm@0.19.12': optional: true '@esbuild/android-arm@0.21.5': optional: true + '@esbuild/android-arm@0.24.2': + optional: true + '@esbuild/android-x64@0.19.12': optional: true '@esbuild/android-x64@0.21.5': optional: true + '@esbuild/android-x64@0.24.2': + optional: true + '@esbuild/darwin-arm64@0.19.12': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true + '@esbuild/darwin-arm64@0.24.2': + optional: true + '@esbuild/darwin-x64@0.19.12': optional: true '@esbuild/darwin-x64@0.21.5': optional: true + '@esbuild/darwin-x64@0.24.2': + optional: true + '@esbuild/freebsd-arm64@0.19.12': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true + '@esbuild/freebsd-arm64@0.24.2': + optional: true + '@esbuild/freebsd-x64@0.19.12': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true + '@esbuild/freebsd-x64@0.24.2': + optional: true + '@esbuild/linux-arm64@0.19.12': optional: true '@esbuild/linux-arm64@0.21.5': optional: true + '@esbuild/linux-arm64@0.24.2': + optional: true + '@esbuild/linux-arm@0.19.12': optional: true '@esbuild/linux-arm@0.21.5': optional: true + '@esbuild/linux-arm@0.24.2': + optional: true + '@esbuild/linux-ia32@0.19.12': optional: true '@esbuild/linux-ia32@0.21.5': optional: true + '@esbuild/linux-ia32@0.24.2': + optional: true + '@esbuild/linux-loong64@0.19.12': optional: true '@esbuild/linux-loong64@0.21.5': optional: true + '@esbuild/linux-loong64@0.24.2': + optional: true + '@esbuild/linux-mips64el@0.19.12': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true + '@esbuild/linux-mips64el@0.24.2': + optional: true + '@esbuild/linux-ppc64@0.19.12': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true + '@esbuild/linux-ppc64@0.24.2': + optional: true + '@esbuild/linux-riscv64@0.19.12': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true + '@esbuild/linux-riscv64@0.24.2': + optional: true + '@esbuild/linux-s390x@0.19.12': optional: true '@esbuild/linux-s390x@0.21.5': optional: true + '@esbuild/linux-s390x@0.24.2': + optional: true + '@esbuild/linux-x64@0.19.12': optional: true '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/linux-x64@0.24.2': + optional: true + + '@esbuild/netbsd-arm64@0.24.2': + optional: true + '@esbuild/netbsd-x64@0.19.12': optional: true '@esbuild/netbsd-x64@0.21.5': optional: true + '@esbuild/netbsd-x64@0.24.2': + optional: true + + '@esbuild/openbsd-arm64@0.24.2': + optional: true + '@esbuild/openbsd-x64@0.19.12': optional: true '@esbuild/openbsd-x64@0.21.5': optional: true + '@esbuild/openbsd-x64@0.24.2': + optional: true + '@esbuild/sunos-x64@0.19.12': optional: true '@esbuild/sunos-x64@0.21.5': optional: true + '@esbuild/sunos-x64@0.24.2': + optional: true + '@esbuild/win32-arm64@0.19.12': optional: true '@esbuild/win32-arm64@0.21.5': optional: true + '@esbuild/win32-arm64@0.24.2': + optional: true + '@esbuild/win32-ia32@0.19.12': optional: true '@esbuild/win32-ia32@0.21.5': optional: true + '@esbuild/win32-ia32@0.24.2': + optional: true + '@esbuild/win32-x64@0.19.12': optional: true '@esbuild/win32-x64@0.21.5': optional: true - '@event-calendar/core@3.7.1': + '@esbuild/win32-x64@0.24.2': + optional: true + + '@event-calendar/core@3.12.0': dependencies: svelte: 4.2.19 - '@event-calendar/day-grid@3.7.1': + '@event-calendar/day-grid@3.12.0': dependencies: - '@event-calendar/core': 3.7.1 + '@event-calendar/core': 3.12.0 svelte: 4.2.19 - '@event-calendar/time-grid@3.7.1': + '@event-calendar/interaction@3.12.0': dependencies: - '@event-calendar/core': 3.7.1 + '@event-calendar/core': 3.12.0 svelte: 4.2.19 - '@formatjs/ecma402-abstract@2.2.1': + '@event-calendar/time-grid@3.12.0': dependencies: - '@formatjs/fast-memoize': 2.2.2 - '@formatjs/intl-localematcher': 0.5.6 - tslib: 2.6.3 + '@event-calendar/core': 3.12.0 + svelte: 4.2.19 - '@formatjs/fast-memoize@2.2.2': + '@formatjs/ecma402-abstract@2.3.4': dependencies: - tslib: 2.6.3 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/intl-localematcher': 0.6.1 + decimal.js: 10.5.0 + tslib: 2.8.1 - '@formatjs/icu-messageformat-parser@2.9.1': + '@formatjs/fast-memoize@2.2.7': dependencies: - '@formatjs/ecma402-abstract': 2.2.1 - '@formatjs/icu-skeleton-parser': 1.8.5 - tslib: 2.6.3 + tslib: 2.8.1 - '@formatjs/icu-skeleton-parser@1.8.5': + '@formatjs/icu-messageformat-parser@2.11.2': dependencies: - '@formatjs/ecma402-abstract': 2.2.1 - tslib: 2.6.3 + '@formatjs/ecma402-abstract': 2.3.4 + '@formatjs/icu-skeleton-parser': 1.8.14 + tslib: 2.8.1 - '@formatjs/intl-localematcher@0.5.6': + '@formatjs/icu-skeleton-parser@1.8.14': dependencies: - tslib: 2.6.3 + '@formatjs/ecma402-abstract': 2.3.4 + tslib: 2.8.1 - '@iconify-json/mdi@1.1.67': + '@formatjs/intl-localematcher@0.6.1': + dependencies: + tslib: 2.8.1 + + '@iconify-json/mdi@1.2.3': dependencies: '@iconify/types': 2.0.0 '@iconify/types@2.0.0': {} - '@iconify/utils@2.1.25': + '@iconify/utils@2.3.0': dependencies: - '@antfu/install-pkg': 0.1.1 - '@antfu/utils': 0.7.8 + '@antfu/install-pkg': 1.0.0 + '@antfu/utils': 8.1.1 '@iconify/types': 2.0.0 - debug: 4.3.5 + debug: 4.4.0 + globals: 15.15.0 kolorist: 1.8.0 - local-pkg: 0.5.0 - mlly: 1.7.1 + local-pkg: 1.1.1 + mlly: 1.7.4 transitivePeerDependencies: - supports-color @@ -2487,29 +2475,26 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@jridgewell/gen-mapping@0.3.5': + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/set-array@1.2.1': {} - '@jridgewell/sourcemap-codec@1.4.15': {} + '@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - - '@jsdevtools/ez-spawn@3.0.4': - dependencies: - call-me-maybe: 1.0.2 - cross-spawn: 7.0.6 - string-argv: 0.3.2 - type-detect: 4.0.8 + '@jridgewell/sourcemap-codec': 1.5.0 '@lukulent/svelte-umami@0.0.3(svelte@4.2.19)': dependencies: @@ -2522,17 +2507,15 @@ snapshots: '@mapbox/jsonlint-lines-primitives@2.0.2': {} - '@mapbox/node-pre-gyp@1.0.11': + '@mapbox/node-pre-gyp@2.0.0': dependencies: - detect-libc: 2.0.3 - https-proxy-agent: 5.0.1 - make-dir: 3.1.0 + consola: 3.4.2 + detect-libc: 2.0.4 + https-proxy-agent: 7.0.6 node-fetch: 2.7.0 - nopt: 5.0.0 - npmlog: 5.0.1 - rimraf: 3.0.2 - semver: 7.6.2 - tar: 6.2.1 + nopt: 8.1.0 + semver: 7.7.1 + tar: 7.4.3 transitivePeerDependencies: - encoding - supports-color @@ -2555,7 +2538,7 @@ snapshots: '@mapbox/whoots-js@3.1.0': {} - '@maplibre/maplibre-gl-style-spec@20.3.0': + '@maplibre/maplibre-gl-style-spec@20.4.0': dependencies: '@mapbox/jsonlint-lines-primitives': 2.0.2 '@mapbox/unitbezier': 0.0.1 @@ -2563,8 +2546,7 @@ snapshots: minimist: 1.2.8 quickselect: 2.0.0 rw: 1.3.3 - sort-object: 3.0.3 - tinyqueue: 2.0.3 + tinyqueue: 3.0.0 '@nodelib/fs.scandir@2.1.5': dependencies: @@ -2576,252 +2558,200 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 + fastq: 1.19.1 '@pkgjs/parseargs@0.11.0': optional: true - '@polka/url@1.0.0-next.25': {} + '@polka/url@1.0.0-next.29': {} - '@rollup/plugin-commonjs@26.0.1(rollup@4.24.0)': + '@rollup/plugin-commonjs@28.0.3(rollup@4.40.2)': dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.24.0) + '@rollup/pluginutils': 5.1.4(rollup@4.40.2) commondir: 1.0.1 estree-walker: 2.0.2 - glob: 10.4.2 + fdir: 6.4.4(picomatch@4.0.2) is-reference: 1.2.1 - magic-string: 0.30.10 + magic-string: 0.30.17 + picomatch: 4.0.2 optionalDependencies: - rollup: 4.24.0 + rollup: 4.40.2 - '@rollup/plugin-json@6.1.0(rollup@4.24.0)': + '@rollup/plugin-json@6.1.0(rollup@4.40.2)': dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.24.0) + '@rollup/pluginutils': 5.1.4(rollup@4.40.2) optionalDependencies: - rollup: 4.24.0 + rollup: 4.40.2 - '@rollup/plugin-node-resolve@15.2.3(rollup@4.24.0)': + '@rollup/plugin-node-resolve@16.0.1(rollup@4.40.2)': dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.24.0) + '@rollup/pluginutils': 5.1.4(rollup@4.40.2) '@types/resolve': 1.20.2 deepmerge: 4.3.1 - is-builtin-module: 3.2.1 is-module: 1.0.0 - resolve: 1.22.8 + resolve: 1.22.10 optionalDependencies: - rollup: 4.24.0 + rollup: 4.40.2 - '@rollup/pluginutils@4.2.1': + '@rollup/pluginutils@5.1.4(rollup@4.40.2)': dependencies: + '@types/estree': 1.0.7 estree-walker: 2.0.2 - picomatch: 2.3.1 - - '@rollup/pluginutils@5.1.0(rollup@4.24.0)': - dependencies: - '@types/estree': 1.0.6 - estree-walker: 2.0.2 - picomatch: 2.3.1 + picomatch: 4.0.2 optionalDependencies: - rollup: 4.24.0 + rollup: 4.40.2 - '@rollup/rollup-android-arm-eabi@4.24.0': + '@rollup/rollup-android-arm-eabi@4.40.2': optional: true - '@rollup/rollup-android-arm-eabi@4.31.0': + '@rollup/rollup-android-arm64@4.40.2': optional: true - '@rollup/rollup-android-arm64@4.24.0': + '@rollup/rollup-darwin-arm64@4.40.2': optional: true - '@rollup/rollup-android-arm64@4.31.0': + '@rollup/rollup-darwin-x64@4.40.2': optional: true - '@rollup/rollup-darwin-arm64@4.24.0': + '@rollup/rollup-freebsd-arm64@4.40.2': optional: true - '@rollup/rollup-darwin-arm64@4.31.0': + '@rollup/rollup-freebsd-x64@4.40.2': optional: true - '@rollup/rollup-darwin-x64@4.24.0': + '@rollup/rollup-linux-arm-gnueabihf@4.40.2': optional: true - '@rollup/rollup-darwin-x64@4.31.0': + '@rollup/rollup-linux-arm-musleabihf@4.40.2': optional: true - '@rollup/rollup-freebsd-arm64@4.31.0': + '@rollup/rollup-linux-arm64-gnu@4.40.2': optional: true - '@rollup/rollup-freebsd-x64@4.31.0': + '@rollup/rollup-linux-arm64-musl@4.40.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.24.0': + '@rollup/rollup-linux-loongarch64-gnu@4.40.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.31.0': + '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.24.0': + '@rollup/rollup-linux-riscv64-gnu@4.40.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.31.0': + '@rollup/rollup-linux-riscv64-musl@4.40.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.24.0': + '@rollup/rollup-linux-s390x-gnu@4.40.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.31.0': + '@rollup/rollup-linux-x64-gnu@4.40.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.24.0': + '@rollup/rollup-linux-x64-musl@4.40.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.31.0': + '@rollup/rollup-win32-arm64-msvc@4.40.2': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.31.0': + '@rollup/rollup-win32-ia32-msvc@4.40.2': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.24.0': + '@rollup/rollup-win32-x64-msvc@4.40.2': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.31.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.24.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.31.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.24.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.31.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.24.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.31.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.24.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.31.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.24.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.31.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.24.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.31.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.24.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.31.0': - optional: true - - '@sveltejs/adapter-node@5.2.0(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))': + '@sveltejs/adapter-node@5.2.12(@sveltejs/kit@2.20.7(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)))(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)))': dependencies: - '@rollup/plugin-commonjs': 26.0.1(rollup@4.24.0) - '@rollup/plugin-json': 6.1.0(rollup@4.24.0) - '@rollup/plugin-node-resolve': 15.2.3(rollup@4.24.0) - '@sveltejs/kit': 2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)) - rollup: 4.24.0 + '@rollup/plugin-commonjs': 28.0.3(rollup@4.40.2) + '@rollup/plugin-json': 6.1.0(rollup@4.40.2) + '@rollup/plugin-node-resolve': 16.0.1(rollup@4.40.2) + '@sveltejs/kit': 2.20.7(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)))(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)) + rollup: 4.40.2 - '@sveltejs/adapter-vercel@5.4.1(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))': + '@sveltejs/adapter-vercel@5.7.0(@sveltejs/kit@2.20.7(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)))(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)))(rollup@4.40.2)': dependencies: - '@sveltejs/kit': 2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)) - '@vercel/nft': 0.27.2 - esbuild: 0.21.5 + '@sveltejs/kit': 2.20.7(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)))(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)) + '@vercel/nft': 0.29.2(rollup@4.40.2) + esbuild: 0.24.2 transitivePeerDependencies: - encoding + - rollup - supports-color - '@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4))': + '@sveltejs/kit@2.20.7(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)))(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)) + '@sveltejs/vite-plugin-svelte': 3.1.2(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)) '@types/cookie': 0.6.0 cookie: 0.6.0 devalue: 5.1.1 - esm-env: 1.0.0 + esm-env: 1.2.2 import-meta-resolve: 4.1.0 kleur: 4.1.5 - magic-string: 0.30.10 - mrmime: 2.0.0 + magic-string: 0.30.17 + mrmime: 2.0.1 sade: 1.8.1 - set-cookie-parser: 2.6.0 - sirv: 3.0.0 + set-cookie-parser: 2.7.1 + sirv: 3.0.1 svelte: 4.2.19 - tiny-glob: 0.2.9 - vite: 5.4.12(@types/node@22.5.4) + vite: 5.4.19(@types/node@22.15.2) - '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4))': + '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)))(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)) - debug: 4.3.5 + '@sveltejs/vite-plugin-svelte': 3.1.2(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)) + debug: 4.4.0 svelte: 4.2.19 - vite: 5.4.12(@types/node@22.5.4) + vite: 5.4.19(@types/node@22.15.2) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4))': + '@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)) - debug: 4.3.5 + '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)))(svelte@4.2.19)(vite@5.4.19(@types/node@22.15.2)) + debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 - magic-string: 0.30.10 + magic-string: 0.30.17 svelte: 4.2.19 svelte-hmr: 0.16.0(svelte@4.2.19) - vite: 5.4.12(@types/node@22.5.4) - vitefu: 0.2.5(vite@5.4.12(@types/node@22.5.4)) + vite: 5.4.19(@types/node@22.15.2) + vitefu: 0.2.5(vite@5.4.19(@types/node@22.15.2)) transitivePeerDependencies: - supports-color - '@tailwindcss/typography@0.5.13(tailwindcss@3.4.4)': + '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17)': dependencies: lodash.castarray: 4.4.0 lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.4 + tailwindcss: 3.4.17 '@types/cookie@0.6.0': {} - '@types/estree@1.0.6': {} + '@types/estree@1.0.7': {} '@types/geojson-vt@3.2.5': dependencies: - '@types/geojson': 7946.0.14 + '@types/geojson': 7946.0.16 - '@types/geojson@7946.0.14': {} + '@types/geojson@7946.0.16': {} - '@types/junit-report-builder@3.0.2': {} - - '@types/leaflet@1.9.12': + '@types/leaflet@1.9.17': dependencies: - '@types/geojson': 7946.0.14 + '@types/geojson': 7946.0.16 '@types/mapbox__point-geometry@0.1.4': {} '@types/mapbox__vector-tile@1.3.4': dependencies: - '@types/geojson': 7946.0.14 + '@types/geojson': 7946.0.16 '@types/mapbox__point-geometry': 0.1.4 '@types/pbf': 3.0.5 - '@types/node@22.5.4': + '@types/node@22.15.2': dependencies: - undici-types: 6.19.8 + undici-types: 6.21.0 '@types/pbf@3.0.5': {} @@ -2829,54 +2759,51 @@ snapshots: '@types/qrcode@1.5.5': dependencies: - '@types/node': 22.5.4 + '@types/node': 22.15.2 '@types/resolve@1.20.2': {} '@types/supercluster@7.1.3': dependencies: - '@types/geojson': 7946.0.14 + '@types/geojson': 7946.0.16 '@types/trusted-types@2.0.7': optional: true - '@vercel/nft@0.27.2': + '@vercel/nft@0.29.2(rollup@4.40.2)': dependencies: - '@mapbox/node-pre-gyp': 1.0.11 - '@rollup/pluginutils': 4.2.1 - acorn: 8.12.0 - acorn-import-attributes: 1.9.5(acorn@8.12.0) + '@mapbox/node-pre-gyp': 2.0.0 + '@rollup/pluginutils': 5.1.4(rollup@4.40.2) + acorn: 8.14.1 + acorn-import-attributes: 1.9.5(acorn@8.14.1) async-sema: 3.1.1 bindings: 1.5.0 estree-walker: 2.0.2 - glob: 7.2.3 + glob: 10.4.5 graceful-fs: 4.2.11 - micromatch: 4.0.8 - node-gyp-build: 4.8.1 + node-gyp-build: 4.8.4 + picomatch: 4.0.2 resolve-from: 5.0.0 transitivePeerDependencies: - encoding + - rollup - supports-color '@xmldom/xmldom@0.8.10': {} - abbrev@1.1.1: {} + abbrev@3.0.1: {} - acorn-import-attributes@1.9.5(acorn@8.12.0): + acorn-import-attributes@1.9.5(acorn@8.14.1): dependencies: - acorn: 8.12.0 + acorn: 8.14.1 - acorn@8.12.0: {} + acorn@8.14.1: {} - agent-base@6.0.2: - dependencies: - debug: 4.3.5 - transitivePeerDependencies: - - supports-color + agent-base@7.1.3: {} ansi-regex@5.0.1: {} - ansi-regex@6.0.1: {} + ansi-regex@6.1.0: {} ansi-styles@4.3.0: dependencies: @@ -2891,38 +2818,23 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - aproba@2.0.0: {} - - are-we-there-yet@2.0.0: - dependencies: - delegates: 1.0.0 - readable-stream: 3.6.2 - arg@5.0.2: {} - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - - arr-union@3.1.0: {} - - assign-symbols@1.0.0: {} + aria-query@5.3.2: {} async-sema@3.1.1: {} - autoprefixer@10.4.19(postcss@8.4.38): + autoprefixer@10.4.21(postcss@8.5.3): dependencies: - browserslist: 4.23.1 - caniuse-lite: 1.0.30001688 + browserslist: 4.24.4 + caniuse-lite: 1.0.30001715 fraction.js: 4.3.7 normalize-range: 0.1.2 - picocolors: 1.0.1 - postcss: 8.4.38 + picocolors: 1.1.1 + postcss: 8.5.3 postcss-value-parser: 4.2.0 - axobject-query@4.0.0: - dependencies: - dequal: 2.0.3 + axobject-query@4.1.0: {} balanced-match@1.0.2: {} @@ -2932,12 +2844,12 @@ snapshots: dependencies: file-uri-to-path: 1.0.0 - brace-expansion@1.1.11: + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -2945,37 +2857,22 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.23.1: + browserslist@4.24.4: dependencies: - caniuse-lite: 1.0.30001688 - electron-to-chromium: 1.4.810 - node-releases: 2.0.14 - update-browserslist-db: 1.0.16(browserslist@4.23.1) + caniuse-lite: 1.0.30001715 + electron-to-chromium: 1.5.143 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.24.4) buffer-crc32@1.0.0: {} buffer-from@1.1.2: {} - builtin-modules@3.3.0: {} - - bytewise-core@1.2.3: - dependencies: - typewise-core: 1.2.0 - - bytewise@1.1.0: - dependencies: - bytewise-core: 1.2.3 - typewise: 1.0.3 - - call-me-maybe@1.0.2: {} - - callsites@3.1.0: {} - camelcase-css@2.0.1: {} camelcase@5.3.1: {} - caniuse-lite@1.0.30001688: {} + caniuse-lite@1.0.30001715: {} chokidar@3.6.0: dependencies: @@ -2989,7 +2886,7 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - chownr@2.0.0: {} + chownr@3.0.0: {} cli-color@2.0.4: dependencies: @@ -3007,9 +2904,9 @@ snapshots: code-red@1.0.4: dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - '@types/estree': 1.0.6 - acorn: 8.12.0 + '@jridgewell/sourcemap-codec': 1.5.0 + '@types/estree': 1.0.7 + acorn: 8.14.1 estree-walker: 3.0.3 periscopic: 3.1.0 @@ -3019,8 +2916,6 @@ snapshots: color-name@1.1.4: {} - color-support@1.1.3: {} - commander@4.1.1: {} commondir@1.0.1: {} @@ -3034,9 +2929,11 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 - confbox@0.1.7: {} + confbox@0.1.8: {} - console-control-strings@1.1.0: {} + confbox@0.2.2: {} + + consola@3.4.2: {} cookie@0.6.0: {} @@ -3054,7 +2951,7 @@ snapshots: css-tree@2.3.1: dependencies: mdn-data: 2.0.30 - source-map-js: 1.2.0 + source-map-js: 1.2.1 cssesc@3.0.0: {} @@ -3073,30 +2970,30 @@ snapshots: es5-ext: 0.10.64 type: 2.7.3 - daisyui@4.12.6(postcss@8.4.38): + daisyui@4.12.24(postcss@8.5.3): dependencies: css-selector-tokenizer: 0.8.0 culori: 3.3.0 - picocolors: 1.0.1 - postcss-js: 4.0.1(postcss@8.4.38) + picocolors: 1.1.1 + postcss-js: 4.0.1(postcss@8.5.3) transitivePeerDependencies: - postcss - debug@4.3.5: + debug@4.4.0: dependencies: - ms: 2.1.2 + ms: 2.1.3 decamelize@1.2.0: {} - deepmerge@4.3.1: {} + decimal.js@10.5.0: {} - delegates@1.0.0: {} + deepmerge@4.3.1: {} dequal@2.0.3: {} detect-indent@6.1.0: {} - detect-libc@2.0.3: {} + detect-libc@2.0.4: {} devalue@5.1.1: {} @@ -3106,17 +3003,17 @@ snapshots: dlv@1.1.3: {} - dompurify@3.2.4: + dompurify@3.2.5: optionalDependencies: '@types/trusted-types': 2.0.7 - earcut@2.2.4: {} + earcut@3.0.1: {} eastasianwidth@0.2.0: {} - electron-to-chromium@1.4.810: {} + electron-to-chromium@1.5.143: {} - emoji-picker-element@1.26.0: {} + emoji-picker-element@1.26.3: {} emoji-regex@8.0.0: {} @@ -3201,9 +3098,37 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - escalade@3.1.2: {} + esbuild@0.24.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 - esm-env@1.0.0: {} + escalade@3.2.0: {} + + esm-env@1.2.2: {} esniff@2.0.1: dependencies: @@ -3216,39 +3141,20 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.7 event-emitter@0.3.5: dependencies: d: 1.0.2 es5-ext: 0.10.64 - execa@5.1.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 + exsolve@1.0.5: {} ext@1.7.0: dependencies: type: 2.7.3 - extend-shallow@2.0.1: - dependencies: - is-extendable: 0.1.1 - - extend-shallow@3.0.2: - dependencies: - assign-symbols: 1.0.0 - is-extendable: 1.0.1 - - fast-glob@3.3.2: + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 '@nodelib/fs.walk': 1.2.8 @@ -3258,9 +3164,13 @@ snapshots: fastparse@1.1.2: {} - fastq@1.17.1: + fastq@1.19.1: dependencies: - reusify: 1.0.4 + reusify: 1.1.0 + + fdir@6.4.4(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 fflate@0.8.2: {} @@ -3275,22 +3185,13 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - foreground-child@3.2.1: + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 fraction.js@4.3.7: {} - fs-minipass@2.1.0: - dependencies: - minipass: 3.3.6 - fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -3298,26 +3199,12 @@ snapshots: function-bind@1.1.2: {} - gauge@3.0.2: - dependencies: - aproba: 2.0.0 - color-support: 1.1.3 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - object-assign: 4.1.1 - signal-exit: 3.0.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wide-align: 1.1.5 - geojson-vt@4.0.2: {} get-caller-file@2.0.5: {} get-stream@6.0.1: {} - get-value@2.0.6: {} - gl-matrix@3.4.3: {} glob-parent@5.1.2: @@ -3328,13 +3215,13 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.4.2: + glob@10.4.5: dependencies: - foreground-child: 3.2.1 - jackspeak: 3.4.0 - minimatch: 9.0.4 + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 minipass: 7.1.2 - package-json-from-dist: 1.0.0 + package-json-from-dist: 1.0.1 path-scurry: 1.11.1 glob@7.2.3: @@ -3346,11 +3233,13 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - global-prefix@3.0.0: + global-prefix@4.0.0: dependencies: - ini: 1.3.8 + ini: 4.1.3 kind-of: 6.0.3 - which: 1.3.1 + which: 4.0.0 + + globals@15.15.0: {} globalyzer@0.1.0: {} @@ -3360,28 +3249,19 @@ snapshots: gsap@3.12.7: {} - has-unicode@2.0.1: {} - hasown@2.0.2: dependencies: function-bind: 1.1.2 - https-proxy-agent@5.0.1: + https-proxy-agent@7.0.6: dependencies: - agent-base: 6.0.2 - debug: 4.3.5 + agent-base: 7.1.3 + debug: 4.4.0 transitivePeerDependencies: - supports-color - human-signals@2.1.0: {} - ieee754@1.2.1: {} - import-fresh@3.3.0: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - import-meta-resolve@4.1.0: {} inflight@1.0.6: @@ -3391,35 +3271,25 @@ snapshots: inherits@2.0.4: {} - ini@1.3.8: {} + ini@4.1.3: {} internmap@2.0.3: {} - intl-messageformat@10.7.3: + intl-messageformat@10.7.16: dependencies: - '@formatjs/ecma402-abstract': 2.2.1 - '@formatjs/fast-memoize': 2.2.2 - '@formatjs/icu-messageformat-parser': 2.9.1 - tslib: 2.6.3 + '@formatjs/ecma402-abstract': 2.3.4 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.2 + tslib: 2.8.1 is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 - is-builtin-module@3.2.1: - dependencies: - builtin-modules: 3.3.0 - - is-core-module@2.14.0: + is-core-module@2.16.1: dependencies: hasown: 2.0.2 - is-extendable@0.1.1: {} - - is-extendable@1.0.1: - dependencies: - is-plain-object: 2.0.4 - is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -3432,33 +3302,27 @@ snapshots: is-number@7.0.0: {} - is-plain-object@2.0.4: - dependencies: - isobject: 3.0.1 - is-promise@2.2.2: {} is-reference@1.2.1: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.7 - is-reference@3.0.2: + is-reference@3.0.3: dependencies: - '@types/estree': 1.0.6 - - is-stream@2.0.1: {} + '@types/estree': 1.0.7 isexe@2.0.0: {} - isobject@3.0.1: {} + isexe@3.1.1: {} - jackspeak@3.4.0: + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jiti@1.21.6: {} + jiti@1.21.7: {} json-stringify-pretty-compact@4.0.0: {} @@ -3474,16 +3338,20 @@ snapshots: kolorist@1.8.0: {} - lilconfig@2.1.0: {} - - lilconfig@3.1.2: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} - local-pkg@0.5.0: + local-pkg@0.5.1: dependencies: - mlly: 1.7.1 - pkg-types: 1.1.1 + mlly: 1.7.4 + pkg-types: 1.3.1 + + local-pkg@1.1.1: + dependencies: + mlly: 1.7.4 + pkg-types: 2.1.0 + quansync: 0.2.10 locate-character@3.0.0: {} @@ -3491,31 +3359,25 @@ snapshots: dependencies: p-locate: 4.1.0 - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - lodash.castarray@4.4.0: {} lodash.isplainobject@4.0.6: {} lodash.merge@4.6.2: {} - lru-cache@10.2.2: {} + lru-cache@10.4.3: {} lru-queue@0.1.0: dependencies: es5-ext: 0.10.64 - magic-string@0.30.10: - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 + luxon@3.6.1: {} - make-dir@3.1.0: + magic-string@0.30.17: dependencies: - semver: 6.3.1 + '@jridgewell/sourcemap-codec': 1.5.0 - maplibre-gl@4.5.0: + maplibre-gl@4.7.1: dependencies: '@mapbox/geojson-rewind': 0.5.2 '@mapbox/jsonlint-lines-primitives': 2.0.2 @@ -3524,28 +3386,27 @@ snapshots: '@mapbox/unitbezier': 0.0.1 '@mapbox/vector-tile': 1.3.1 '@mapbox/whoots-js': 3.1.0 - '@maplibre/maplibre-gl-style-spec': 20.3.0 - '@types/geojson': 7946.0.14 + '@maplibre/maplibre-gl-style-spec': 20.4.0 + '@types/geojson': 7946.0.16 '@types/geojson-vt': 3.2.5 - '@types/junit-report-builder': 3.0.2 '@types/mapbox__point-geometry': 0.1.4 '@types/mapbox__vector-tile': 1.3.4 '@types/pbf': 3.0.5 '@types/supercluster': 7.1.3 - earcut: 2.2.4 + earcut: 3.0.1 geojson-vt: 4.0.2 gl-matrix: 3.4.3 - global-prefix: 3.0.0 + global-prefix: 4.0.0 kdbush: 4.0.2 murmurhash-js: 1.0.0 - pbf: 3.2.1 + pbf: 3.3.0 potpack: 2.0.0 - quickselect: 2.0.0 + quickselect: 3.0.0 supercluster: 8.0.1 - tinyqueue: 2.0.3 + tinyqueue: 3.0.0 vt-pbf: 3.1.3 - marked@15.0.4: {} + marked@15.0.11: {} mdn-data@2.0.30: {} @@ -3560,8 +3421,6 @@ snapshots: next-tick: 1.1.0 timers-ext: 0.1.8 - merge-stream@2.0.0: {} - merge2@1.4.1: {} micromatch@4.0.8: @@ -3569,51 +3428,42 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mimic-fn@2.1.0: {} - min-indent@1.0.1: {} minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 - minimatch@9.0.4: + minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} - minipass@3.3.6: - dependencies: - yallist: 4.0.0 - - minipass@5.0.0: {} - minipass@7.1.2: {} - minizlib@2.1.2: + minizlib@3.0.2: dependencies: - minipass: 3.3.6 - yallist: 4.0.0 + minipass: 7.1.2 mkdirp@0.5.6: dependencies: minimist: 1.2.8 - mkdirp@1.0.4: {} + mkdirp@3.0.1: {} - mlly@1.7.1: + mlly@1.7.4: dependencies: - acorn: 8.12.0 - pathe: 1.1.2 - pkg-types: 1.1.1 - ufo: 1.5.3 + acorn: 8.14.1 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 mri@1.2.0: {} - mrmime@2.0.0: {} + mrmime@2.0.1: {} - ms@2.1.2: {} + ms@2.1.3: {} murmurhash-js@1.0.0: {} @@ -3623,7 +3473,7 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.8: {} + nanoid@3.3.11: {} next-tick@1.1.0: {} @@ -3631,29 +3481,18 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-gyp-build@4.8.1: {} + node-gyp-build@4.8.4: {} - node-releases@2.0.14: {} + node-releases@2.0.19: {} - nopt@5.0.0: + nopt@8.1.0: dependencies: - abbrev: 1.1.1 + abbrev: 3.0.1 normalize-path@3.0.0: {} normalize-range@0.1.2: {} - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - - npmlog@5.0.1: - dependencies: - are-we-there-yet: 2.0.0 - console-control-strings: 1.1.0 - gauge: 3.0.2 - set-blocking: 2.0.0 - object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -3662,33 +3501,21 @@ snapshots: dependencies: wrappy: 1.0.2 - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - p-limit@2.3.0: dependencies: p-try: 2.2.0 - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - p-locate@4.1.0: dependencies: p-limit: 2.3.0 - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - p-try@2.2.0: {} - package-json-from-dist@1.0.0: {} + package-json-from-dist@1.0.1: {} - parent-module@1.0.1: + package-manager-detector@0.2.11: dependencies: - callsites: 3.1.0 + quansync: 0.2.10 path-exists@4.0.0: {} @@ -3700,101 +3527,101 @@ snapshots: path-scurry@1.11.1: dependencies: - lru-cache: 10.2.2 + lru-cache: 10.4.3 minipass: 7.1.2 - pathe@1.1.2: {} + pathe@2.0.3: {} - pbf@3.2.1: + pbf@3.3.0: dependencies: ieee754: 1.2.1 resolve-protobuf-schema: 2.1.0 periscopic@3.1.0: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.7 estree-walker: 3.0.3 - is-reference: 3.0.2 - - picocolors@1.0.1: {} + is-reference: 3.0.3 picocolors@1.1.1: {} picomatch@2.3.1: {} + picomatch@4.0.2: {} + pify@2.3.0: {} - pirates@4.0.6: {} + pirates@4.0.7: {} - pkg-types@1.1.1: + pkg-types@1.3.1: dependencies: - confbox: 0.1.7 - mlly: 1.7.1 - pathe: 1.1.2 + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.3 - pmtiles@3.0.6: + pkg-types@2.1.0: dependencies: - '@types/leaflet': 1.9.12 + confbox: 0.2.2 + exsolve: 1.0.5 + pathe: 2.0.3 + + pmtiles@3.2.1: + dependencies: + '@types/leaflet': 1.9.17 fflate: 0.8.2 pngjs@5.0.0: {} - postcss-import@15.1.0(postcss@8.4.38): + postcss-import@15.1.0(postcss@8.5.3): dependencies: - postcss: 8.4.38 + postcss: 8.5.3 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.8 + resolve: 1.22.10 - postcss-js@4.0.1(postcss@8.4.38): + postcss-js@4.0.1(postcss@8.5.3): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.38 + postcss: 8.5.3 - postcss-load-config@4.0.2(postcss@8.4.38): + postcss-load-config@4.0.2(postcss@8.5.3): dependencies: - lilconfig: 3.1.2 - yaml: 2.4.5 + lilconfig: 3.1.3 + yaml: 2.7.1 optionalDependencies: - postcss: 8.4.38 + postcss: 8.5.3 - postcss-nested@6.0.1(postcss@8.4.38): + postcss-nested@6.2.0(postcss@8.5.3): dependencies: - postcss: 8.4.38 - postcss-selector-parser: 6.1.0 + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 postcss-selector-parser@6.0.10: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@6.1.0: + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 postcss-value-parser@4.2.0: {} - postcss@8.4.38: + postcss@8.5.3: dependencies: - nanoid: 3.3.8 - picocolors: 1.0.1 - source-map-js: 1.2.0 - - postcss@8.5.1: - dependencies: - nanoid: 3.3.8 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 potpack@2.0.0: {} - prettier-plugin-svelte@3.2.5(prettier@3.3.2)(svelte@4.2.19): + prettier-plugin-svelte@3.3.3(prettier@3.5.3)(svelte@4.2.19): dependencies: - prettier: 3.3.2 + prettier: 3.5.3 svelte: 4.2.19 - prettier@3.3.2: {} + prettier@3.5.3: {} protocol-buffers-schema@3.6.0: {} @@ -3810,10 +3637,14 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 + quansync@0.2.10: {} + queue-microtask@1.2.3: {} quickselect@2.0.0: {} + quickselect@3.0.0: {} + read-cache@1.0.0: dependencies: pify: 2.3.0 @@ -3832,75 +3663,48 @@ snapshots: require-main-filename@2.0.0: {} - resolve-from@4.0.0: {} - resolve-from@5.0.0: {} resolve-protobuf-schema@2.1.0: dependencies: protocol-buffers-schema: 3.6.0 - resolve@1.22.8: + resolve@1.22.10: dependencies: - is-core-module: 2.14.0 + is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - reusify@1.0.4: {} + reusify@1.1.0: {} rimraf@2.7.1: dependencies: glob: 7.2.3 - rimraf@3.0.2: + rollup@4.40.2: dependencies: - glob: 7.2.3 - - rollup@4.24.0: - dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.7 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.24.0 - '@rollup/rollup-android-arm64': 4.24.0 - '@rollup/rollup-darwin-arm64': 4.24.0 - '@rollup/rollup-darwin-x64': 4.24.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.24.0 - '@rollup/rollup-linux-arm-musleabihf': 4.24.0 - '@rollup/rollup-linux-arm64-gnu': 4.24.0 - '@rollup/rollup-linux-arm64-musl': 4.24.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.24.0 - '@rollup/rollup-linux-riscv64-gnu': 4.24.0 - '@rollup/rollup-linux-s390x-gnu': 4.24.0 - '@rollup/rollup-linux-x64-gnu': 4.24.0 - '@rollup/rollup-linux-x64-musl': 4.24.0 - '@rollup/rollup-win32-arm64-msvc': 4.24.0 - '@rollup/rollup-win32-ia32-msvc': 4.24.0 - '@rollup/rollup-win32-x64-msvc': 4.24.0 - fsevents: 2.3.3 - - rollup@4.31.0: - dependencies: - '@types/estree': 1.0.6 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.31.0 - '@rollup/rollup-android-arm64': 4.31.0 - '@rollup/rollup-darwin-arm64': 4.31.0 - '@rollup/rollup-darwin-x64': 4.31.0 - '@rollup/rollup-freebsd-arm64': 4.31.0 - '@rollup/rollup-freebsd-x64': 4.31.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.31.0 - '@rollup/rollup-linux-arm-musleabihf': 4.31.0 - '@rollup/rollup-linux-arm64-gnu': 4.31.0 - '@rollup/rollup-linux-arm64-musl': 4.31.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.31.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.31.0 - '@rollup/rollup-linux-riscv64-gnu': 4.31.0 - '@rollup/rollup-linux-s390x-gnu': 4.31.0 - '@rollup/rollup-linux-x64-gnu': 4.31.0 - '@rollup/rollup-linux-x64-musl': 4.31.0 - '@rollup/rollup-win32-arm64-msvc': 4.31.0 - '@rollup/rollup-win32-ia32-msvc': 4.31.0 - '@rollup/rollup-win32-x64-msvc': 4.31.0 + '@rollup/rollup-android-arm-eabi': 4.40.2 + '@rollup/rollup-android-arm64': 4.40.2 + '@rollup/rollup-darwin-arm64': 4.40.2 + '@rollup/rollup-darwin-x64': 4.40.2 + '@rollup/rollup-freebsd-arm64': 4.40.2 + '@rollup/rollup-freebsd-x64': 4.40.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.40.2 + '@rollup/rollup-linux-arm-musleabihf': 4.40.2 + '@rollup/rollup-linux-arm64-gnu': 4.40.2 + '@rollup/rollup-linux-arm64-musl': 4.40.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.40.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.40.2 + '@rollup/rollup-linux-riscv64-gnu': 4.40.2 + '@rollup/rollup-linux-riscv64-musl': 4.40.2 + '@rollup/rollup-linux-s390x-gnu': 4.40.2 + '@rollup/rollup-linux-x64-gnu': 4.40.2 + '@rollup/rollup-linux-x64-musl': 4.40.2 + '@rollup/rollup-win32-arm64-msvc': 4.40.2 + '@rollup/rollup-win32-ia32-msvc': 4.40.2 + '@rollup/rollup-win32-x64-msvc': 4.40.2 fsevents: 2.3.3 run-parallel@1.2.0: @@ -3922,20 +3726,11 @@ snapshots: mkdirp: 0.5.6 rimraf: 2.7.1 - semver@6.3.1: {} - - semver@7.6.2: {} + semver@7.7.1: {} set-blocking@2.0.0: {} - set-cookie-parser@2.6.0: {} - - set-value@2.0.1: - dependencies: - extend-shallow: 2.0.1 - is-extendable: 0.1.1 - is-plain-object: 2.0.4 - split-string: 3.1.0 + set-cookie-parser@2.7.1: {} shebang-command@2.0.0: dependencies: @@ -3943,46 +3738,23 @@ snapshots: shebang-regex@3.0.0: {} - signal-exit@3.0.7: {} - signal-exit@4.1.0: {} - sirv@3.0.0: + sirv@3.0.1: dependencies: - '@polka/url': 1.0.0-next.25 - mrmime: 2.0.0 + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 totalist: 3.0.1 sorcery@0.11.1: dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 buffer-crc32: 1.0.0 minimist: 1.2.8 sander: 0.5.1 - sort-asc@0.2.0: {} - - sort-desc@0.2.0: {} - - sort-object@3.0.3: - dependencies: - bytewise: 1.1.0 - get-value: 2.0.6 - is-extendable: 0.1.1 - sort-asc: 0.2.0 - sort-desc: 0.2.0 - union-value: 1.0.1 - - source-map-js@1.2.0: {} - source-map-js@1.2.1: {} - split-string@3.1.0: - dependencies: - extend-shallow: 3.0.2 - - string-argv@0.3.2: {} - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -4005,9 +3777,7 @@ snapshots: strip-ansi@7.1.0: dependencies: - ansi-regex: 6.0.1 - - strip-final-newline@2.0.0: {} + ansi-regex: 6.1.0 strip-indent@3.0.0: dependencies: @@ -4015,12 +3785,12 @@ snapshots: sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/gen-mapping': 0.3.8 commander: 4.1.1 - glob: 10.4.2 + glob: 10.4.5 lines-and-columns: 1.2.4 mz: 2.7.0 - pirates: 4.0.6 + pirates: 4.0.7 ts-interface-checker: 0.1.13 supercluster@8.0.1: @@ -4029,17 +3799,15 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@3.8.1(postcss-load-config@4.0.2(postcss@8.4.38))(postcss@8.4.38)(svelte@4.2.19): + svelte-check@3.8.6(postcss-load-config@4.0.2(postcss@8.5.3))(postcss@8.5.3)(svelte@4.2.19): dependencies: '@jridgewell/trace-mapping': 0.3.25 chokidar: 3.6.0 - fast-glob: 3.3.2 - import-fresh: 3.3.0 - picocolors: 1.0.1 + picocolors: 1.1.1 sade: 1.8.1 svelte: 4.2.19 - svelte-preprocess: 5.1.4(postcss-load-config@4.0.2(postcss@8.4.38))(postcss@8.4.38)(svelte@4.2.19)(typescript@5.5.2) - typescript: 5.5.2 + svelte-preprocess: 5.1.4(postcss-load-config@4.0.2(postcss@8.5.3))(postcss@8.5.3)(svelte@4.2.19)(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - '@babel/core' - coffeescript @@ -4061,85 +3829,86 @@ snapshots: deepmerge: 4.3.1 esbuild: 0.19.12 estree-walker: 2.0.2 - intl-messageformat: 10.7.3 + intl-messageformat: 10.7.16 sade: 1.8.1 svelte: 4.2.19 tiny-glob: 0.2.9 - svelte-maplibre@0.9.8(svelte@4.2.19): + svelte-maplibre@0.9.14(svelte@4.2.19): dependencies: d3-geo: 3.1.1 + dequal: 2.0.3 just-compare: 2.3.0 just-flush: 2.3.0 - maplibre-gl: 4.5.0 - pmtiles: 3.0.6 + maplibre-gl: 4.7.1 + pmtiles: 3.2.1 svelte: 4.2.19 - svelte-preprocess@5.1.4(postcss-load-config@4.0.2(postcss@8.4.38))(postcss@8.4.38)(svelte@4.2.19)(typescript@5.5.2): + svelte-preprocess@5.1.4(postcss-load-config@4.0.2(postcss@8.5.3))(postcss@8.5.3)(svelte@4.2.19)(typescript@5.8.3): dependencies: '@types/pug': 2.0.10 detect-indent: 6.1.0 - magic-string: 0.30.10 + magic-string: 0.30.17 sorcery: 0.11.1 strip-indent: 3.0.0 svelte: 4.2.19 optionalDependencies: - postcss: 8.4.38 - postcss-load-config: 4.0.2(postcss@8.4.38) - typescript: 5.5.2 + postcss: 8.5.3 + postcss-load-config: 4.0.2(postcss@8.5.3) + typescript: 5.8.3 svelte@4.2.19: dependencies: '@ampproject/remapping': 2.3.0 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 - '@types/estree': 1.0.6 - acorn: 8.12.0 - aria-query: 5.3.0 - axobject-query: 4.0.0 + '@types/estree': 1.0.7 + acorn: 8.14.1 + aria-query: 5.3.2 + axobject-query: 4.1.0 code-red: 1.0.4 css-tree: 2.3.1 estree-walker: 3.0.3 - is-reference: 3.0.2 + is-reference: 3.0.3 locate-character: 3.0.0 - magic-string: 0.30.10 + magic-string: 0.30.17 periscopic: 3.1.0 - tailwindcss@3.4.4: + tailwindcss@3.4.17: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 chokidar: 3.6.0 didyoumean: 1.2.2 dlv: 1.1.3 - fast-glob: 3.3.2 + fast-glob: 3.3.3 glob-parent: 6.0.2 is-glob: 4.0.3 - jiti: 1.21.6 - lilconfig: 2.1.0 + jiti: 1.21.7 + lilconfig: 3.1.3 micromatch: 4.0.8 normalize-path: 3.0.0 object-hash: 3.0.0 - picocolors: 1.0.1 - postcss: 8.4.38 - postcss-import: 15.1.0(postcss@8.4.38) - postcss-js: 4.0.1(postcss@8.4.38) - postcss-load-config: 4.0.2(postcss@8.4.38) - postcss-nested: 6.0.1(postcss@8.4.38) - postcss-selector-parser: 6.1.0 - resolve: 1.22.8 + picocolors: 1.1.1 + postcss: 8.5.3 + postcss-import: 15.1.0(postcss@8.5.3) + postcss-js: 4.0.1(postcss@8.5.3) + postcss-load-config: 4.0.2(postcss@8.5.3) + postcss-nested: 6.2.0(postcss@8.5.3) + postcss-selector-parser: 6.1.2 + resolve: 1.22.10 sucrase: 3.35.0 transitivePeerDependencies: - ts-node - tar@6.2.1: + tar@7.4.3: dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 thenify-all@1.6.0: dependencies: @@ -4159,7 +3928,9 @@ snapshots: globalyzer: 0.1.0 globrex: 0.1.2 - tinyqueue@2.0.3: {} + tinyexec@0.3.2: {} + + tinyqueue@3.0.0: {} to-regex-range@5.0.1: dependencies: @@ -4171,83 +3942,64 @@ snapshots: ts-interface-checker@0.1.13: {} - tslib@2.6.3: {} - - type-detect@4.0.8: {} + tslib@2.8.1: {} type@2.7.3: {} typedarray@0.0.6: {} - typescript@5.5.2: {} + typescript@5.8.3: {} - typewise-core@1.2.0: {} + ufo@1.6.1: {} - typewise@1.0.3: + undici-types@6.21.0: {} + + unplugin-icons@0.19.3: dependencies: - typewise-core: 1.2.0 - - ufo@1.5.3: {} - - undici-types@6.19.8: {} - - union-value@1.0.1: - dependencies: - arr-union: 3.1.0 - get-value: 2.0.6 - is-extendable: 0.1.1 - set-value: 2.0.1 - - unplugin-icons@0.19.0: - dependencies: - '@antfu/install-pkg': 0.3.3 - '@antfu/utils': 0.7.8 - '@iconify/utils': 2.1.25 - debug: 4.3.5 + '@antfu/install-pkg': 0.4.1 + '@antfu/utils': 0.7.10 + '@iconify/utils': 2.3.0 + debug: 4.4.0 kolorist: 1.8.0 - local-pkg: 0.5.0 - unplugin: 1.10.1 + local-pkg: 0.5.1 + unplugin: 1.16.1 transitivePeerDependencies: - supports-color - unplugin@1.10.1: + unplugin@1.16.1: dependencies: - acorn: 8.12.0 - chokidar: 3.6.0 - webpack-sources: 3.2.3 + acorn: 8.14.1 webpack-virtual-modules: 0.6.2 - update-browserslist-db@1.0.16(browserslist@4.23.1): + update-browserslist-db@1.1.3(browserslist@4.24.4): dependencies: - browserslist: 4.23.1 - escalade: 3.1.2 - picocolors: 1.0.1 + browserslist: 4.24.4 + escalade: 3.2.0 + picocolors: 1.1.1 util-deprecate@1.0.2: {} - vite@5.4.12(@types/node@22.5.4): + vite@5.4.19(@types/node@22.15.2): dependencies: esbuild: 0.21.5 - postcss: 8.5.1 - rollup: 4.31.0 + postcss: 8.5.3 + rollup: 4.40.2 optionalDependencies: - '@types/node': 22.5.4 + '@types/node': 22.15.2 fsevents: 2.3.3 - vitefu@0.2.5(vite@5.4.12(@types/node@22.5.4)): + vitefu@0.2.5(vite@5.4.19(@types/node@22.15.2)): optionalDependencies: - vite: 5.4.12(@types/node@22.5.4) + vite: 5.4.19(@types/node@22.15.2) vt-pbf@3.1.3: dependencies: '@mapbox/point-geometry': 0.1.0 '@mapbox/vector-tile': 1.3.1 - pbf: 3.2.1 + pbf: 3.3.0 webidl-conversions@3.0.1: {} - webpack-sources@3.2.3: {} - webpack-virtual-modules@0.6.2: {} whatwg-url@5.0.0: @@ -4257,17 +4009,13 @@ snapshots: which-module@2.0.1: {} - which@1.3.1: - dependencies: - isexe: 2.0.0 - which@2.0.2: dependencies: isexe: 2.0.0 - wide-align@1.1.5: + which@4.0.0: dependencies: - string-width: 4.2.3 + isexe: 3.1.1 wrap-ansi@6.2.0: dependencies: @@ -4291,9 +4039,9 @@ snapshots: y18n@4.0.3: {} - yallist@4.0.0: {} + yallist@5.0.0: {} - yaml@2.4.5: {} + yaml@2.7.1: {} yargs-parser@18.1.3: dependencies: @@ -4313,5 +4061,3 @@ snapshots: which-module: 2.0.1 y18n: 4.0.3 yargs-parser: 18.1.3 - - yocto-queue@0.1.0: {} diff --git a/frontend/src/lib/assets/google_maps.svg b/frontend/src/lib/assets/google_maps.svg new file mode 100644 index 0000000..9c0f945 --- /dev/null +++ b/frontend/src/lib/assets/google_maps.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/lib/assets/undraw_server_error.svg b/frontend/src/lib/assets/undraw_server_error.svg new file mode 100644 index 0000000..0daa182 --- /dev/null +++ b/frontend/src/lib/assets/undraw_server_error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/lib/components/AboutModal.svelte b/frontend/src/lib/components/AboutModal.svelte index 7b8fbf2..46ad5aa 100644 --- a/frontend/src/lib/components/AboutModal.svelte +++ b/frontend/src/lib/components/AboutModal.svelte @@ -7,11 +7,19 @@ const dispatch = createEventDispatcher(); let modal: HTMLDialogElement; - onMount(() => { + let integrations: Record | null = null; + + onMount(async () => { modal = document.getElementById('about_modal') as HTMLDialogElement; if (modal) { modal.showModal(); } + const response = await fetch('/api/integrations'); + if (response.ok) { + integrations = await response.json(); + } else { + integrations = null; + } }); function close() { @@ -90,18 +98,38 @@

{$t('about.oss_attributions')}

-

- {$t('about.nominatim_1')} - - OpenStreetMap - - . {$t('about.nominatim_2')} -

+ {#if integrations && integrations?.google_maps} +

+ {$t('about.nominatim_1')} + + Google Maps + + . +

+ {:else if integrations && !integrations?.google_maps} +

+ {$t('about.nominatim_1')} + + OpenStreetMap + + . {$t('about.nominatim_2')} +

+ {:else} +

+ {$t('about.generic_attributions')} +

+ {/if} +

{$t('about.other_attributions')}

diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index 2e9dd61..d0f6b98 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -18,6 +18,10 @@ import DeleteWarning from './DeleteWarning.svelte'; import CardCarousel from './CardCarousel.svelte'; import { t } from 'svelte-i18n'; + import Star from '~icons/mdi/star'; + import StarOutline from '~icons/mdi/star-outline'; + import Eye from '~icons/mdi/eye'; + import EyeOff from '~icons/mdi/eye-off'; export let type: string | null = null; export let user: User | null; @@ -28,15 +32,18 @@ let isWarningModalOpen: boolean = false; export let adventure: Adventure; - let activityTypes: string[] = []; - // makes it reactivty to changes so it updates automatically + let displayActivityTypes: string[] = []; + let remainingCount = 0; + + // Process activity types for display $: { if (adventure.activity_types) { - activityTypes = adventure.activity_types; - if (activityTypes.length > 3) { - activityTypes = activityTypes.slice(0, 3); - let remaining = adventure.activity_types.length - 3; - activityTypes.push('+' + remaining); + if (adventure.activity_types.length <= 3) { + displayActivityTypes = adventure.activity_types; + remainingCount = 0; + } else { + displayActivityTypes = adventure.activity_types.slice(0, 3); + remainingCount = adventure.activity_types.length - 3; } } } @@ -47,18 +54,28 @@ $: { if (collection && collection?.start_date && collection.end_date) { unlinked = adventure.visits.every((visit) => { - // Check if visit dates exist - if (!visit.start_date || !visit.end_date) return true; // Consider "unlinked" for incomplete visit data - - // Check if collection dates are completely outside this visit's range + if (!visit.start_date || !visit.end_date) return true; const isBeforeVisit = collection.end_date && collection.end_date < visit.start_date; const isAfterVisit = collection.start_date && collection.start_date > visit.end_date; - return isBeforeVisit || isAfterVisit; }); } } + // Helper functions for display + function formatVisitCount() { + const count = adventure.visits.length; + return count > 1 ? `${count} ${$t('adventures.visits')}` : `${count} ${$t('adventures.visit')}`; + } + + function renderStars(rating: number) { + const stars = []; + for (let i = 1; i <= 5; i++) { + stars.push(i <= rating); + } + return stars; + } + async function deleteAdventure() { let res = await fetch(`/api/adventures/${adventure.id}`, { method: 'DELETE' @@ -71,38 +88,61 @@ } } - async function removeFromCollection() { + async function linkCollection(event: CustomEvent) { + let collectionId = event.detail; + // Create a copy to avoid modifying the original directly + const updatedCollections = adventure.collections ? [...adventure.collections] : []; + + // Add the new collection if not already present + if (!updatedCollections.some((c) => String(c) === String(collectionId))) { + updatedCollections.push(collectionId); + } + let res = await fetch(`/api/adventures/${adventure.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ collection: null }) + body: JSON.stringify({ collections: updatedCollections }) }); + if (res.ok) { - addToast('info', `${$t('adventures.collection_remove_success')}`); - dispatch('delete', adventure.id); + // Only update the adventure.collections after server confirms success + adventure.collections = updatedCollections; + addToast('info', `${$t('adventures.collection_link_success')}`); } else { - addToast('error', `${$t('adventures.collection_remove_error')}`); + addToast('error', `${$t('adventures.collection_link_error')}`); } } - async function linkCollection(event: CustomEvent) { + async function removeFromCollection(event: CustomEvent) { let collectionId = event.detail; - let res = await fetch(`/api/adventures/${adventure.id}`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ collection: collectionId }) - }); - if (res.ok) { - console.log('Adventure linked to collection'); - addToast('info', `${$t('adventures.collection_link_success')}`); - isCollectionModalOpen = false; - dispatch('delete', adventure.id); - } else { - addToast('error', `${$t('adventures.collection_link_error')}`); + if (!collectionId) { + addToast('error', `${$t('adventures.collection_remove_error')}`); + return; + } + + // Create a copy to avoid modifying the original directly + if (adventure.collections) { + const updatedCollections = adventure.collections.filter( + (c) => String(c) !== String(collectionId) + ); + + let res = await fetch(`/api/adventures/${adventure.id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ collections: updatedCollections }) + }); + + if (res.ok) { + // Only update adventure.collections after server confirms success + adventure.collections = updatedCollections; + addToast('info', `${$t('adventures.collection_remove_success')}`); + } else { + addToast('error', `${$t('adventures.collection_remove_error')}`); + } } } @@ -116,7 +156,12 @@ {#if isCollectionModalOpen} - (isCollectionModalOpen = false)} /> + linkCollection(e)} + on:unlink={(e) => removeFromCollection(e)} + on:close={() => (isCollectionModalOpen = false)} + linkedCollectionList={adventure.collections} + /> {/if} {#if isWarningModalOpen} @@ -131,119 +176,172 @@ {/if}
- + +
+ -
-
+ +
+
+ {adventure.is_visited ? $t('adventures.visited') : $t('adventures.planned')} +
+ {#if unlinked} +
{$t('adventures.out_of_range')}
+ {/if} +
+ + +
+
+
+ {#if adventure.is_public} + + {:else} + + {/if} +
+
+
+ + + {#if adventure.category} +
+
+ {adventure.category.display_name} + {adventure.category.icon} +
+
+ {/if} +
+ + +
+ +
-
-
-
- {adventure.category?.display_name + ' ' + adventure.category?.icon} -
-
- {adventure.is_visited ? $t('adventures.visited') : $t('adventures.planned')} -
-
- {adventure.is_public ? $t('adventures.public') : $t('adventures.private')} -
-
- {#if unlinked} -
{$t('adventures.out_of_range')}
- {/if} - {#if adventure.location && adventure.location !== ''} -
- -

{adventure.location}

-
- {/if} - {#if adventure.visits.length > 0} - -
- -

- {adventure.visits.length} - {adventure.visits.length > 1 ? $t('adventures.visits') : $t('adventures.visit')} -

-
- {/if} - {#if adventure.activity_types && adventure.activity_types.length > 0} -
    - {#each activityTypes as activity} -
    - {activity} + + + {#if adventure.location} +
    + + {adventure.location} +
    + {/if} + + + {#if adventure.rating} +
    +
    + {#each renderStars(adventure.rating) as filled} + {#if filled} + + {:else} + + {/if} + {/each}
    - {/each} -
+ ({adventure.rating}/5) +
+ {/if} +
+ + + {#if adventure.visits.length > 0} +
+ + {formatVisitCount()} +
{/if} + + {#if !readOnly} -
- - +
{#if type != 'link'} - {#if adventure.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))} - - {:else} +
- {/if} - {/if} - {#if type == 'link'} - + + {$t('adventures.open_details')} + + + {#if adventure.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))} + + {/if} +
+ {:else} + {/if}
{/if}
+ + diff --git a/frontend/src/lib/components/AdventureLink.svelte b/frontend/src/lib/components/AdventureLink.svelte index 9d7d094..d967093 100644 --- a/frontend/src/lib/components/AdventureLink.svelte +++ b/frontend/src/lib/components/AdventureLink.svelte @@ -7,26 +7,83 @@ import AdventureCard from './AdventureCard.svelte'; let modal: HTMLDialogElement; - let adventures: Adventure[] = []; + // Icons - following the worldtravel pattern + import Adventures from '~icons/mdi/map-marker-path'; + import Search from '~icons/mdi/magnify'; + import Clear from '~icons/mdi/close'; + import Link from '~icons/mdi/link-variant'; + import Check from '~icons/mdi/check-circle'; + import Cancel from '~icons/mdi/cancel'; + import Public from '~icons/mdi/earth'; + import Private from '~icons/mdi/lock'; + let adventures: Adventure[] = []; + let filteredAdventures: Adventure[] = []; + let searchQuery: string = ''; + let filterOption: string = 'all'; let isLoading: boolean = true; export let user: User | null; + export let collectionId: string; + + // Search and filter functionality following worldtravel pattern + $: { + let filtered = adventures; + + // Apply search filter - include name and location + if (searchQuery !== '') { + filtered = filtered.filter((adventure) => { + const nameMatch = adventure.name.toLowerCase().includes(searchQuery.toLowerCase()); + const locationMatch = + adventure.location?.toLowerCase().includes(searchQuery.toLowerCase()) || false; + const descriptionMatch = + adventure.description?.toLowerCase().includes(searchQuery.toLowerCase()) || false; + return nameMatch || locationMatch || descriptionMatch; + }); + } + + // Apply status filter + if (filterOption === 'public') { + filtered = filtered.filter((adventure) => adventure.is_public); + } else if (filterOption === 'private') { + filtered = filtered.filter((adventure) => !adventure.is_public); + } else if (filterOption === 'visited') { + filtered = filtered.filter((adventure) => adventure.visits && adventure.visits.length > 0); + } else if (filterOption === 'not_visited') { + filtered = filtered.filter((adventure) => !adventure.visits || adventure.visits.length === 0); + } + + filteredAdventures = filtered; + } + + // Statistics following worldtravel pattern + $: totalAdventures = adventures.length; + $: publicAdventures = adventures.filter((a) => a.is_public).length; + $: privateAdventures = adventures.filter((a) => !a.is_public).length; + $: visitedAdventures = adventures.filter((a) => a.visits && a.visits.length > 0).length; + $: notVisitedAdventures = adventures.filter((a) => !a.visits || a.visits.length === 0).length; onMount(async () => { modal = document.getElementById('my_modal_1') as HTMLDialogElement; if (modal) { modal.showModal(); } - let res = await fetch(`/api/adventures/all/?include_collections=false`, { + + let res = await fetch(`/api/adventures/all/?include_collections=true`, { method: 'GET' }); const newAdventures = await res.json(); - if (res.ok && adventures) { + // Filter out adventures that are already linked to the collections + if (collectionId) { + adventures = newAdventures.filter((adventure: Adventure) => { + return !(adventure.collections ?? []).includes(collectionId); + }); + } else { adventures = newAdventures; } + isLoading = false; }); @@ -44,28 +101,200 @@ dispatch('close'); } } + + function clearFilters() { + searchQuery = ''; + filterOption = 'all'; + } - + - diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index c8b0743..504a360 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -6,9 +6,22 @@ import { t } from 'svelte-i18n'; export let collection: Collection | null = null; + let fullStartDate: string = ''; + let fullEndDate: string = ''; + let fullStartDateOnly: string = ''; + let fullEndDateOnly: string = ''; + + // Set full start and end dates from collection + if (collection && collection.start_date && collection.end_date) { + fullStartDate = `${collection.start_date}T00:00`; + fullEndDate = `${collection.end_date}T23:59`; + fullStartDateOnly = collection.start_date; + fullEndDateOnly = collection.end_date; + } + const dispatch = createEventDispatcher(); - let images: { id: string; image: string; is_primary: boolean }[] = []; + let images: { id: string; image: string; is_primary: boolean; immich_id: string | null }[] = []; let warningMessage: string = ''; let constrainDates: boolean = false; @@ -60,25 +73,26 @@ '.tar.lzma', '.tar.lzo', '.tar.z', - 'gpx', - 'md', - 'pdf' + '.gpx', + '.md' ]; export let initialLatLng: { lat: number; lng: number } | null = null; // Used to pass the location from the map selection to the modal let fileInput: HTMLInputElement; let immichIntegration: boolean = false; + let copyImmichLocally: boolean = false; import ActivityComplete from './ActivityComplete.svelte'; import CategoryDropdown from './CategoryDropdown.svelte'; - import { findFirstValue } from '$lib'; + import { findFirstValue, isAllDay } from '$lib'; import MarkdownEditor from './MarkdownEditor.svelte'; import ImmichSelect from './ImmichSelect.svelte'; import Star from '~icons/mdi/star'; import Crown from '~icons/mdi/crown'; import AttachmentCard from './AttachmentCard.svelte'; import LocationDropdown from './LocationDropdown.svelte'; + import DateRangeCollapse from './DateRangeCollapse.svelte'; let modal: HTMLDialogElement; let wikiError: string = ''; @@ -97,7 +111,6 @@ location: null, images: [], user_id: null, - collection: collection?.id || null, category: { id: '', name: '', @@ -123,7 +136,6 @@ location: adventureToEdit?.location || null, images: adventureToEdit?.images || [], user_id: adventureToEdit?.user_id || null, - collection: adventureToEdit?.collection || collection?.id || null, visits: adventureToEdit?.visits || [], is_visited: adventureToEdit?.is_visited || false, category: adventureToEdit?.category || { @@ -147,13 +159,19 @@ addToast('error', $t('adventures.category_fetch_error')); } // Check for Immich Integration - let res = await fetch('/api/integrations'); - if (!res.ok) { + let res = await fetch('/api/integrations/immich/'); + // If the response is not ok, we assume Immich integration is not available + if (!res.ok && res.status !== 404) { addToast('error', $t('immich.integration_fetch_error')); } else { let data = await res.json(); - if (data.immich) { + if (data.error) { + immichIntegration = false; + } else if (data.id) { immichIntegration = true; + copyImmichLocally = data.copy_locally || false; + } else { + immichIntegration = false; } } }); @@ -163,6 +181,8 @@ let wikiImageError: string = ''; let triggerMarkVisted: boolean = false; + let isLoading: boolean = false; + images = adventure.images || []; $: { if (!adventure.rating) { @@ -314,7 +334,12 @@ }); if (res.ok) { let newData = deserialize(await res.text()) as { data: { id: string; image: string } }; - let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false }; + let newImage = { + id: newData.data.id, + image: newData.data.image, + is_primary: false, + immich_id: null + }; images = [...images, newImage]; adventure.images = images; addToast('success', $t('adventures.image_upload_success')); @@ -365,7 +390,12 @@ }); if (res2.ok) { let newData = deserialize(await res2.text()) as { data: { id: string; image: string } }; - let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false }; + let newImage = { + id: newData.data.id, + image: newData.data.image, + is_primary: false, + immich_id: null + }; images = [...images, newImage]; adventure.images = images; addToast('success', $t('adventures.image_upload_success')); @@ -376,35 +406,6 @@ } } - let new_start_date: string = ''; - let new_end_date: string = ''; - let new_notes: string = ''; - function addNewVisit() { - if (new_start_date && !new_end_date) { - new_end_date = new_start_date; - } - if (new_start_date > new_end_date) { - addToast('error', $t('adventures.start_before_end_error')); - return; - } - if (new_end_date && !new_start_date) { - addToast('error', $t('adventures.no_start_date')); - return; - } - adventure.visits = [ - ...adventure.visits, - { - start_date: new_start_date, - end_date: new_end_date, - notes: new_notes, - id: '' - } - ]; - new_start_date = ''; - new_end_date = ''; - new_notes = ''; - } - function close() { dispatch('close'); } @@ -429,6 +430,14 @@ async function handleSubmit(event: Event) { event.preventDefault(); triggerMarkVisted = true; + isLoading = true; + + // if category icon is empty, set it to the default icon + if (adventure.category?.icon == '' || adventure.category?.icon == null) { + if (adventure.category) { + adventure.category.icon = '🌍'; + } + } if (adventure.id === '') { if (adventure.category?.display_name == '') { @@ -446,6 +455,12 @@ }; } } + + // add this collection to the adventure + if (collection && collection.id) { + adventure.collections = [collection.id]; + } + let res = await fetch('/api/adventures', { method: 'POST', headers: { @@ -484,6 +499,7 @@ } } imageSearch = adventure.name; + isLoading = false; } @@ -614,7 +630,7 @@

{wikiError}

- {#if !collection?.id} + {#if !adventureToEdit || (adventureToEdit.collections && adventureToEdit.collections.length === 0)}
-
- -
- {$t('adventures.visits')} ({adventure.visits.length}) -
-
- -
- {#if !constrainDates} - { - if (e.key === 'Enter') { - e.preventDefault(); - addNewVisit(); - } - }} - /> - { - if (e.key === 'Enter') { - e.preventDefault(); - addNewVisit(); - } - }} - /> - {:else} - { - if (e.key === 'Enter') { - e.preventDefault(); - addNewVisit(); - } - }} - /> - { - if (e.key === 'Enter') { - e.preventDefault(); - addNewVisit(); - } - }} - /> - {/if} -
-
- - -
-
- -
- - {#if adventure.visits.length > 0} -

{$t('adventures.my_visits')}

- {#each adventure.visits as visit} -
-
-

- {new Date(visit.start_date).toLocaleDateString(undefined, { - timeZone: 'UTC' - })} -

- {#if visit.end_date && visit.end_date !== visit.start_date} -

- {new Date(visit.end_date).toLocaleDateString(undefined, { - timeZone: 'UTC' - })} -

- {/if} - -
- -
-
-

{visit.notes}

-
- {/each} - {/if} -
-
+
@@ -805,8 +691,17 @@ {$t('adventures.warning')}: {warningMessage}
{/if} - - +
+ {#if !isLoading} + + {:else} + + {/if} + +
@@ -849,6 +744,23 @@ + + {#if attachmentToEdit}
{ @@ -941,6 +853,18 @@ url = e.detail; fetchImage(); }} + {copyImmichLocally} + on:remoteImmichSaved={(e) => { + const newImage = { + id: e.detail.id, + image: e.detail.image, + is_primary: e.detail.is_primary, + immich_id: e.detail.immich_id + }; + images = [...images, newImage]; + adventure.images = images; + addToast('success', $t('adventures.image_upload_success')); + }} /> {/if} diff --git a/frontend/src/lib/components/Avatar.svelte b/frontend/src/lib/components/Avatar.svelte index dbf6b22..28cb952 100644 --- a/frontend/src/lib/components/Avatar.svelte +++ b/frontend/src/lib/components/Avatar.svelte @@ -2,49 +2,172 @@ import { goto } from '$app/navigation'; import { t } from 'svelte-i18n'; + // Icons + import Account from '~icons/mdi/account'; + import MapMarker from '~icons/mdi/map-marker'; + import Shield from '~icons/mdi/shield-account'; + import Settings from '~icons/mdi/cog'; + import Logout from '~icons/mdi/logout'; + export let user: any; - let letter: string = user.first_name[0]; + let letter: string = user.first_name?.[0] || user.username?.[0] || '?'; - if (user && !user.first_name && user.username) { - letter = user.username[0]; - } + // Get display name + $: displayName = user.first_name + ? `${user.first_name} ${user.last_name || ''}`.trim() + : user.username || 'User'; + + // Get initials for fallback + $: initials = + user.first_name && user.last_name ? `${user.first_name[0]}${user.last_name[0]}` : letter; + + // Menu items for better organization + const menuItems = [ + { + path: `/profile/${user.username}`, + icon: Account, + label: 'navbar.profile', + section: 'main' + }, + { + path: '/adventures', + icon: MapMarker, + label: 'navbar.my_adventures', + section: 'main' + }, + { + path: '/settings', + icon: Settings, + label: 'navbar.settings', + section: 'secondary' + } + ]; + + // Add admin item if user is staff + $: adminMenuItem = user.is_staff + ? { + path: '/admin', + icon: Shield, + label: 'navbar.admin_panel', + section: 'secondary' + } + : null; -