mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-19 12:59:36 +02:00
Adventure Times, Collection Ordering, Trip Maps
This commit is contained in:
commit
908d31a4a3
64 changed files with 5152 additions and 2389 deletions
16
.github/.docker-compose-database.yml
vendored
Normal file
16
.github/.docker-compose-database.yml
vendored
Normal file
|
@ -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:
|
64
.github/workflows/backend-test.yml
vendored
Normal file
64
.github/workflows/backend-test.yml
vendored
Normal file
|
@ -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/
|
32
.github/workflows/frontend-test.yml
vendored
Normal file
32
.github/workflows/frontend-test.yml
vendored
Normal file
|
@ -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
|
|
@ -12,7 +12,7 @@ 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 +31,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 +44,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"]
|
||||
# Command to start Supervisor (which starts Nginx and Gunicorn)
|
||||
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||
|
|
|
@ -62,5 +62,8 @@ fi
|
|||
|
||||
cat /code/adventurelog.txt
|
||||
|
||||
# Start gunicorn
|
||||
gunicorn main.wsgi:application --bind [::]:8000 --timeout 120 --workers 2
|
||||
# Start Gunicorn in foreground
|
||||
exec gunicorn main.wsgi:application \
|
||||
--bind [::]:8000 \
|
||||
--workers 2 \
|
||||
--timeout 120
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
18
backend/server/adventures/migrations/0026_visit_timezone.py
Normal file
18
backend/server/adventures/migrations/0026_visit_timezone.py
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -14,7 +14,7 @@ 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 +43,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 +496,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)
|
||||
|
@ -191,6 +612,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)
|
||||
|
@ -352,6 +775,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 +787,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:
|
||||
|
|
|
@ -72,7 +72,7 @@ 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):
|
||||
|
@ -136,9 +136,11 @@ class AdventureSerializer(CustomModelSerializer):
|
|||
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):
|
||||
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 visit.start_date and not visit.end_date and (visit.start_date <= current_date):
|
||||
elif start_date and not end_date and (start_date <= current_date):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -199,7 +201,7 @@ class TransportationSerializer(CustomModelSerializer):
|
|||
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'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
|
||||
|
||||
|
@ -210,7 +212,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']
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ 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
|
||||
import requests
|
||||
|
||||
class AdventureViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = AdventureSerializer
|
||||
|
@ -170,48 +171,38 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
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()
|
||||
@action(detail=True, methods=['get'], url_path='additional-info')
|
||||
def additional_info(self, request, pk=None):
|
||||
adventure = self.get_object()
|
||||
|
||||
# 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
|
||||
# )
|
||||
# Permission check: owner or shared collection member
|
||||
if adventure.user_id != request.user:
|
||||
if not (adventure.collection and adventure.collection.shared_with.filter(id=request.user.id).exists()):
|
||||
return Response({"error": "User does not have permission to access this adventure"},
|
||||
status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# # Define the overlapping fields that both the Adventure and target models share.
|
||||
# overlapping_fields = ["name", "description", "is_public", 'collection']
|
||||
serializer = self.get_serializer(adventure)
|
||||
response_data = serializer.data
|
||||
|
||||
# # 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)
|
||||
visits = response_data.get('visits', [])
|
||||
sun_times = []
|
||||
|
||||
# # Make sure to include the user reference
|
||||
# conversion_data["user_id"] = adventure.user_id
|
||||
for visit in visits:
|
||||
date = visit.get('start_date')
|
||||
if date and adventure.longitude and adventure.latitude:
|
||||
api_url = f'https://api.sunrisesunset.io/json?lat={adventure.latitude}&lng={adventure.longitude}&date={date}'
|
||||
res = requests.get(api_url)
|
||||
if res.status_code == 200:
|
||||
data = res.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')
|
||||
})
|
||||
|
||||
# # 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)
|
||||
response_data['sun_times'] = sun_times
|
||||
return Response(response_data)
|
|
@ -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'
|
||||
|
|
|
@ -3,7 +3,6 @@ from allauth.socialaccount.models import SocialAccount
|
|||
|
||||
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:
|
||||
|
|
16
backend/supervisord.conf
Normal file
16
backend/supervisord.conf
Normal file
|
@ -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
|
|
@ -140,10 +140,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,
|
||||
|
@ -194,6 +205,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",
|
||||
|
|
67
documentation/docs/install/caddy.md
Normal file
67
documentation/docs/install/caddy.md
Normal file
|
@ -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.
|
|
@ -33,7 +33,7 @@ Here is a summary of the configuration options available in the `docker-compose.
|
|||
| ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
|
||||
| `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``` |
|
||||
| `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 **bytes**. | ```Infinity``` |
|
||||
|
||||
### Backend Container (server)
|
||||
|
||||
|
|
|
@ -12,3 +12,4 @@ AdventureLog can be installed in a variety of ways. The following are the most c
|
|||
|
||||
- [Nginx Proxy Manager](nginx_proxy_manager.md) 🛡
|
||||
- [Traefik](traefik.md) 🚀
|
||||
- [Caddy](caddy.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 a Computer Science 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! 🌍
|
||||
|
|
18
documentation/docs/troubleshooting/login_unresponsive.md
Normal file
18
documentation/docs/troubleshooting/login_unresponsive.md
Normal file
|
@ -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!
|
33
documentation/docs/usage/usage.md
Normal file
33
documentation/docs/usage/usage.md
Normal file
|
@ -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
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/4Y2LvxG3xn4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
|
@ -1,5 +1,5 @@
|
|||
# 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"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "adventurelog-frontend",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"django": "cd .. && cd backend/server && python3 manage.py runserver",
|
||||
|
@ -43,6 +43,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",
|
||||
|
|
2349
frontend/pnpm-lock.yaml
generated
2349
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
1
frontend/src/lib/assets/undraw_server_error.svg
Normal file
1
frontend/src/lib/assets/undraw_server_error.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="799.031" height="618.112" viewBox="0 0 799.031 618.112" xmlns:xlink="http://www.w3.org/1999/xlink" role="img" artist="Katerina Limpitsouni" source="https://undraw.co/"><g transform="translate(-893 -197)"><path d="M15.18,488.763c0,.872.478,1.573,1.073,1.573h535.1c.6,0,1.073-.7,1.073-1.573s-.478-1.573-1.073-1.573H16.253C15.658,487.191,15.18,487.891,15.18,488.763Z" transform="translate(1007.711 324.776)" fill="#ccc"/><rect width="19.105" height="3.371" transform="translate(1198.162 808.354)" fill="#b6b3c5"/><rect width="19.105" height="3.371" transform="translate(1367.295 808.917)" fill="#b6b3c5"/><path d="M352.955,370.945a27.529,27.529,0,0,1-54.321,0H229.146V521.536h193.3V370.945Z" transform="translate(966.721 287.378)" fill="#d6d6e3"/><rect width="193.296" height="5.242" transform="translate(1196.43 796.983)" fill="#090814"/><path d="M788.255,487.17H10.776A10.788,10.788,0,0,1,0,476.394V32.688A10.788,10.788,0,0,1,10.776,21.911H788.255a10.789,10.789,0,0,1,10.776,10.776V476.394a10.789,10.789,0,0,1-10.776,10.776Z" transform="translate(893 175.089)" fill="#090814"/><rect width="760.822" height="429.297" transform="translate(911.104 213.968)" fill="#fff"/><g transform="translate(20.477 16.308)"><path d="M604.463,379.271H317.442a8.655,8.655,0,0,1-8.645-8.645V273.8a8.655,8.655,0,0,1,8.645-8.645H604.463a8.655,8.655,0,0,1,8.645,8.645v96.826a8.655,8.655,0,0,1-8.645,8.645Z" transform="translate(811.648 85.826)" fill="#6c63ff"/><rect width="76.078" height="8.645" rx="2" transform="translate(1165.4 380.374)" fill="#fff"/><ellipse cx="5.187" cy="5.187" rx="5.187" ry="5.187" transform="translate(1336.576 380.374)" fill="#090814"/><ellipse cx="5.187" cy="5.187" rx="5.187" ry="5.187" transform="translate(1353.865 380.374)" fill="#090814"/><ellipse cx="5.187" cy="5.187" rx="5.187" ry="5.187" transform="translate(1371.156 380.374)" fill="#090814"/></g><ellipse cx="40.952" cy="40.952" rx="40.952" ry="40.952" transform="translate(1404.281 440.452)" fill="#090814"/><path d="M10.863-57.7l-.524-29.6h8.246l-.554,29.6Zm3.613,14.307a4.7,4.7,0,0,1-3.409-1.3,4.368,4.368,0,0,1-1.34-3.278,4.39,4.39,0,0,1,1.34-3.322,4.732,4.732,0,0,1,3.409-1.282,4.732,4.732,0,0,1,3.409,1.282,4.39,4.39,0,0,1,1.34,3.322,4.368,4.368,0,0,1-1.34,3.278A4.7,4.7,0,0,1,14.476-43.394Z" transform="translate(1430.76 546.754)" fill="#fff"/></g></svg>
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -196,7 +196,7 @@
|
|||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-64 p-2 shadow"
|
||||
>
|
||||
<button
|
||||
class="btn btn-neutral mb-2"
|
||||
|
|
|
@ -6,6 +6,20 @@
|
|||
import { t } from 'svelte-i18n';
|
||||
export let collection: Collection | null = null;
|
||||
|
||||
let fullStartDate: string = '';
|
||||
let fullEndDate: string = '';
|
||||
let fullStartDateOnly: string = '';
|
||||
let fullEndDateOnly: string = '';
|
||||
let allDay: boolean = true;
|
||||
|
||||
// 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 }[] = [];
|
||||
|
@ -60,9 +74,8 @@
|
|||
'.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
|
||||
|
@ -72,13 +85,14 @@
|
|||
|
||||
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 = '';
|
||||
|
@ -376,35 +390,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');
|
||||
}
|
||||
|
@ -430,6 +415,13 @@
|
|||
event.preventDefault();
|
||||
triggerMarkVisted = 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 == '') {
|
||||
if (categories.some((category) => category.name === 'general')) {
|
||||
|
@ -446,6 +438,7 @@
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
let res = await fetch('/api/adventures', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -652,138 +645,8 @@
|
|||
<ActivityComplete bind:activities={adventure.activity_types} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
{$t('adventures.visits')} ({adventure.visits.length})
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<label class="label cursor-pointer flex items-start space-x-2">
|
||||
{#if adventure.collection && collection && collection.start_date && collection.end_date}
|
||||
<span class="label-text">{$t('adventures.date_constrain')}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
id="constrain_dates"
|
||||
name="constrain_dates"
|
||||
on:change={() => (constrainDates = !constrainDates)}
|
||||
/>
|
||||
{/if}
|
||||
</label>
|
||||
<div class="flex gap-2 mb-1">
|
||||
{#if !constrainDates}
|
||||
<input
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Start Date"
|
||||
bind:value={new_start_date}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addNewVisit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
placeholder={$t('adventures.end_date')}
|
||||
bind:value={new_end_date}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addNewVisit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
placeholder={$t('adventures.start_date')}
|
||||
min={collection?.start_date}
|
||||
max={collection?.end_date}
|
||||
bind:value={new_start_date}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addNewVisit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
placeholder={$t('adventures.end_date')}
|
||||
bind:value={new_end_date}
|
||||
min={collection?.start_date}
|
||||
max={collection?.end_date}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addNewVisit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex gap-2 mb-1">
|
||||
<!-- textarea for notes -->
|
||||
<textarea
|
||||
class="textarea textarea-bordered w-full"
|
||||
placeholder={$t('adventures.add_notes')}
|
||||
bind:value={new_notes}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addNewVisit();
|
||||
}
|
||||
}}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button type="button" class="btn btn-neutral" on:click={addNewVisit}
|
||||
>{$t('adventures.add')}</button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if adventure.visits.length > 0}
|
||||
<h2 class=" font-bold text-xl mt-2">{$t('adventures.my_visits')}</h2>
|
||||
{#each adventure.visits as visit}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<p>
|
||||
{new Date(visit.start_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})}
|
||||
</p>
|
||||
{#if visit.end_date && visit.end_date !== visit.start_date}
|
||||
<p>
|
||||
{new Date(visit.end_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-error"
|
||||
on:click={() => {
|
||||
adventure.visits = adventure.visits.filter((v) => v !== visit);
|
||||
}}
|
||||
>
|
||||
{$t('adventures.remove')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="whitespace-pre-wrap -mt-2 mb-2">{visit.notes}</p>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<DateRangeCollapse type="adventure" {collection} bind:visits={adventure.visits} />
|
||||
|
||||
<div>
|
||||
<div class="mt-4">
|
||||
|
@ -805,8 +668,10 @@
|
|||
<span>{$t('adventures.warning')}: {warningMessage}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<button type="submit" class="btn btn-primary">{$t('adventures.save_next')}</button>
|
||||
<button type="button" class="btn" on:click={close}>{$t('about.close')}</button>
|
||||
<div class="flex flex-row gap-2">
|
||||
<button type="submit" class="btn btn-primary">{$t('adventures.save_next')}</button>
|
||||
<button type="button" class="btn" on:click={close}>{$t('about.close')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -87,7 +87,7 @@
|
|||
{/if}
|
||||
|
||||
<div
|
||||
class="card min-w-max lg:w-96 md:w-80 sm:w-60 xs:w-40 bg-neutral text-neutral-content shadow-xl"
|
||||
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl overflow-hidden"
|
||||
>
|
||||
<CardCarousel {adventures} />
|
||||
<div class="card-body">
|
||||
|
|
|
@ -189,10 +189,31 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Actions -->
|
||||
|
||||
{#if !collection.start_date && !collection.end_date}
|
||||
<div class="mt-4">
|
||||
<div role="alert" class="alert alert-neutral">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>{$t('adventures.collection_no_start_end_date')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{$t('adventures.save_next')}
|
||||
{$t('notes.save')}
|
||||
</button>
|
||||
<button type="button" class="btn" on:click={close}>
|
||||
{$t('about.close')}
|
||||
|
|
518
frontend/src/lib/components/DateRangeCollapse.svelte
Normal file
518
frontend/src/lib/components/DateRangeCollapse.svelte
Normal file
|
@ -0,0 +1,518 @@
|
|||
<script lang="ts">
|
||||
import type { Collection } from '$lib/types';
|
||||
import TimezoneSelector from './TimezoneSelector.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
export let collection: Collection | null = null;
|
||||
import { updateLocalDate, updateUTCDate, validateDateRange, formatUTCDate } from '$lib/dateUtils';
|
||||
import { onMount } from 'svelte';
|
||||
import { isAllDay } from '$lib';
|
||||
|
||||
export let type: 'adventure' | 'transportation' | 'lodging' = 'adventure';
|
||||
|
||||
// Initialize with browser's timezone
|
||||
export let selectedStartTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
export let selectedEndTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
let allDay: boolean = false;
|
||||
|
||||
// Store the UTC dates as source of truth
|
||||
export let utcStartDate: string | null = null;
|
||||
export let utcEndDate: string | null = null;
|
||||
|
||||
export let note: string | null = null;
|
||||
type Visit = {
|
||||
id: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
notes: string;
|
||||
timezone: string | null;
|
||||
};
|
||||
|
||||
type TransportationVisit = {
|
||||
id: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
notes: string;
|
||||
start_timezone: string;
|
||||
end_timezone: string;
|
||||
};
|
||||
|
||||
export let visits: (Visit | TransportationVisit)[] | null = null;
|
||||
|
||||
// Local display values
|
||||
let localStartDate: string = '';
|
||||
let localEndDate: string = '';
|
||||
|
||||
let fullStartDate: string = '';
|
||||
let fullEndDate: string = '';
|
||||
|
||||
let constrainDates: boolean = false;
|
||||
|
||||
let isEditing = false; // Disable reactivity when editing
|
||||
|
||||
onMount(async () => {
|
||||
// Initialize UTC dates
|
||||
localStartDate = updateLocalDate({
|
||||
utcDate: utcStartDate,
|
||||
timezone: selectedStartTimezone
|
||||
}).localDate;
|
||||
|
||||
localEndDate = updateLocalDate({
|
||||
utcDate: utcEndDate,
|
||||
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone
|
||||
}).localDate;
|
||||
|
||||
if (!selectedStartTimezone) {
|
||||
selectedStartTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
if (!selectedEndTimezone) {
|
||||
selectedEndTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
});
|
||||
|
||||
// Set the full date range for constraining purposes
|
||||
$: if (collection && collection.start_date && collection.end_date) {
|
||||
fullStartDate = `${collection.start_date}T00:00`;
|
||||
fullEndDate = `${collection.end_date}T23:59`;
|
||||
}
|
||||
|
||||
function formatDateInTimezone(utcDate: string, timezone: string): string {
|
||||
try {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
timeZone: timezone,
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
}).format(new Date(utcDate));
|
||||
} catch {
|
||||
return new Date(utcDate).toLocaleString(); // fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Get constraint dates in the right format based on allDay setting
|
||||
$: constraintStartDate = allDay
|
||||
? fullStartDate && fullStartDate.includes('T')
|
||||
? fullStartDate.split('T')[0]
|
||||
: ''
|
||||
: fullStartDate || '';
|
||||
$: constraintEndDate = allDay
|
||||
? fullEndDate && fullEndDate.includes('T')
|
||||
? fullEndDate.split('T')[0]
|
||||
: ''
|
||||
: fullEndDate || '';
|
||||
|
||||
// Update local display dates whenever timezone or UTC dates change
|
||||
$: if (!isEditing) {
|
||||
if (allDay) {
|
||||
localStartDate = utcStartDate?.substring(0, 10) ?? '';
|
||||
localEndDate = utcEndDate?.substring(0, 10) ?? '';
|
||||
} else {
|
||||
const start = updateLocalDate({
|
||||
utcDate: utcStartDate,
|
||||
timezone: selectedStartTimezone
|
||||
}).localDate;
|
||||
|
||||
const end = updateLocalDate({
|
||||
utcDate: utcEndDate,
|
||||
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone
|
||||
}).localDate;
|
||||
|
||||
localStartDate = start;
|
||||
localEndDate = end;
|
||||
}
|
||||
}
|
||||
|
||||
// Update UTC dates when local dates change
|
||||
function handleLocalDateChange() {
|
||||
utcStartDate = updateUTCDate({
|
||||
localDate: localStartDate,
|
||||
timezone: selectedStartTimezone,
|
||||
allDay
|
||||
}).utcDate;
|
||||
|
||||
utcEndDate = updateUTCDate({
|
||||
localDate: localEndDate,
|
||||
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone,
|
||||
allDay
|
||||
}).utcDate;
|
||||
}
|
||||
|
||||
function createVisitObject(): Visit | TransportationVisit {
|
||||
if (type === 'transportation') {
|
||||
const transportVisit: TransportationVisit = {
|
||||
id: crypto.randomUUID(),
|
||||
start_date: utcStartDate ?? '',
|
||||
end_date: utcEndDate ?? utcStartDate ?? '',
|
||||
notes: note ?? '',
|
||||
start_timezone: selectedStartTimezone,
|
||||
end_timezone: selectedEndTimezone
|
||||
};
|
||||
return transportVisit;
|
||||
} else {
|
||||
const regularVisit: Visit = {
|
||||
id: crypto.randomUUID(),
|
||||
start_date: utcStartDate ?? '',
|
||||
end_date: utcEndDate ?? utcStartDate ?? '',
|
||||
notes: note ?? '',
|
||||
timezone: selectedStartTimezone
|
||||
};
|
||||
return regularVisit;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="collapse collapse-plus bg-base-200 mb-4 rounded-lg">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-semibold">
|
||||
{$t('adventures.date_information')}
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<!-- Timezone Selector Section -->
|
||||
<div class="rounded-xl border border-base-300 bg-base-100 p-4 space-y-4 shadow-sm mb-4">
|
||||
<!-- Group Header -->
|
||||
<h3 class="text-md font-semibold">{$t('navbar.settings')}</h3>
|
||||
|
||||
{#if type === 'transportation'}
|
||||
<!-- Dual timezone selectors for transportation -->
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="text-sm font-medium block mb-1">
|
||||
{$t('adventures.departure_timezone')}
|
||||
</label>
|
||||
<TimezoneSelector bind:selectedTimezone={selectedStartTimezone} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-sm font-medium block mb-1">
|
||||
{$t('adventures.arrival_timezone')}
|
||||
</label>
|
||||
<TimezoneSelector bind:selectedTimezone={selectedEndTimezone} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Single timezone selector for other types -->
|
||||
<TimezoneSelector bind:selectedTimezone={selectedStartTimezone} />
|
||||
{/if}
|
||||
|
||||
<!-- All Day Toggle -->
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm">{$t('adventures.all_day')}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
id="all_day"
|
||||
name="all_day"
|
||||
bind:checked={allDay}
|
||||
on:change={() => {
|
||||
if (allDay) {
|
||||
localStartDate = localStartDate ? localStartDate.split('T')[0] : '';
|
||||
localEndDate = localEndDate ? localEndDate.split('T')[0] : '';
|
||||
} else {
|
||||
localStartDate = localStartDate + 'T00:00';
|
||||
localEndDate = localEndDate + 'T23:59';
|
||||
}
|
||||
utcStartDate = updateUTCDate({
|
||||
localDate: localStartDate,
|
||||
timezone: selectedStartTimezone,
|
||||
allDay
|
||||
}).utcDate;
|
||||
utcEndDate = updateUTCDate({
|
||||
localDate: localEndDate,
|
||||
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone,
|
||||
allDay
|
||||
}).utcDate;
|
||||
localStartDate = updateLocalDate({
|
||||
utcDate: utcStartDate,
|
||||
timezone: selectedStartTimezone
|
||||
}).localDate;
|
||||
localEndDate = updateLocalDate({
|
||||
utcDate: utcEndDate,
|
||||
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone
|
||||
}).localDate;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Constrain Dates Toggle -->
|
||||
{#if collection?.start_date && collection?.end_date}
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm">{$t('adventures.date_constrain')}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="constrain_dates"
|
||||
name="constrain_dates"
|
||||
class="toggle toggle-primary"
|
||||
on:change={() => (constrainDates = !constrainDates)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Dates Input Section -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Start Date -->
|
||||
<div class="space-y-2">
|
||||
<label for="date" class="text-sm font-medium">
|
||||
{type === 'transportation'
|
||||
? $t('adventures.departure_date')
|
||||
: $t('adventures.start_date')}
|
||||
</label>
|
||||
|
||||
{#if allDay}
|
||||
<input
|
||||
type="date"
|
||||
id="date"
|
||||
name="date"
|
||||
bind:value={localStartDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? constraintStartDate : ''}
|
||||
max={constrainDates ? constraintEndDate : ''}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
type="datetime-local"
|
||||
id="date"
|
||||
name="date"
|
||||
bind:value={localStartDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? constraintStartDate : ''}
|
||||
max={constrainDates ? constraintEndDate : ''}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- End Date -->
|
||||
{#if localStartDate}
|
||||
<div class="space-y-2">
|
||||
<label for="end_date" class="text-sm font-medium">
|
||||
{type === 'transportation' ? $t('adventures.arrival_date') : $t('adventures.end_date')}
|
||||
</label>
|
||||
|
||||
{#if allDay}
|
||||
<input
|
||||
type="date"
|
||||
id="end_date"
|
||||
name="end_date"
|
||||
bind:value={localEndDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? localStartDate : ''}
|
||||
max={constrainDates ? constraintEndDate : ''}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
type="datetime-local"
|
||||
id="end_date"
|
||||
name="end_date"
|
||||
bind:value={localEndDate}
|
||||
on:change={handleLocalDateChange}
|
||||
min={constrainDates ? localStartDate : ''}
|
||||
max={constrainDates ? constraintEndDate : ''}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Notes (for adventures only) -->
|
||||
{#if type === 'adventure'}
|
||||
<div class="md:col-span-2">
|
||||
<label for="note" class="text-sm font-medium block mb-1">
|
||||
{$t('adventures.add_notes')}
|
||||
</label>
|
||||
<textarea
|
||||
id="note"
|
||||
name="note"
|
||||
class="textarea textarea-bordered w-full"
|
||||
placeholder={$t('adventures.add_notes')}
|
||||
bind:value={note}
|
||||
rows="4"
|
||||
></textarea>
|
||||
</div>
|
||||
{/if}
|
||||
{#if type === 'adventure'}
|
||||
<button
|
||||
class="btn btn-primary mb-2"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
const newVisit = createVisitObject();
|
||||
|
||||
// Ensure reactivity by assigning a *new* array
|
||||
if (visits) {
|
||||
visits = [...visits, newVisit];
|
||||
} else {
|
||||
visits = [newVisit];
|
||||
}
|
||||
|
||||
// Optionally clear the form
|
||||
note = '';
|
||||
localStartDate = '';
|
||||
localEndDate = '';
|
||||
utcStartDate = null;
|
||||
utcEndDate = null;
|
||||
}}
|
||||
>
|
||||
{$t('adventures.add')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Validation Message -->
|
||||
{#if !validateDateRange(localStartDate, localEndDate).valid}
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{$t('adventures.invalid_date_range')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if type === 'adventure'}
|
||||
<div class="border-t border-neutral pt-4 mb-2">
|
||||
<h3 class="text-xl font-semibold">
|
||||
{$t('adventures.visits')}
|
||||
</h3>
|
||||
|
||||
<!-- Visits List -->
|
||||
{#if visits && visits.length === 0}
|
||||
<p class="text-sm text-base-content opacity-70">
|
||||
{$t('adventures.no_visits')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if visits && visits.length > 0}
|
||||
<div class="space-y-4">
|
||||
{#each visits as visit}
|
||||
<div
|
||||
class="p-4 border border-neutral rounded-lg bg-base-100 shadow-sm flex flex-col gap-2"
|
||||
>
|
||||
<p class="text-sm text-base-content font-medium">
|
||||
{#if isAllDay(visit.start_date)}
|
||||
<span class="badge badge-outline mr-2">{$t('adventures.all_day')}</span>
|
||||
{visit.start_date ? visit.start_date.split('T')[0] : ''} – {visit.end_date
|
||||
? visit.end_date.split('T')[0]
|
||||
: ''}
|
||||
{:else if 'start_timezone' in visit}
|
||||
{formatDateInTimezone(visit.start_date, visit.start_timezone)} – {formatDateInTimezone(
|
||||
visit.end_date,
|
||||
visit.end_timezone
|
||||
)}
|
||||
{:else if visit.timezone}
|
||||
{formatDateInTimezone(visit.start_date, visit.timezone)} – {formatDateInTimezone(
|
||||
visit.end_date,
|
||||
visit.timezone
|
||||
)}
|
||||
{:else}
|
||||
{new Date(visit.start_date).toLocaleString()} – {new Date(
|
||||
visit.end_date
|
||||
).toLocaleString()}
|
||||
<!-- showe timezones badge -->
|
||||
{/if}
|
||||
{#if 'timezone' in visit && visit.timezone}
|
||||
<span class="badge badge-outline ml-2">{visit.timezone}</span>
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<!-- -->
|
||||
|
||||
<!-- Display timezone information for transportation visits -->
|
||||
{#if 'start_timezone' in visit && 'end_timezone' in visit && visit.start_timezone !== visit.end_timezone}
|
||||
<p class="text-xs text-base-content">
|
||||
{visit.start_timezone} → {visit.end_timezone}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if visit.notes}
|
||||
<p class="text-sm text-base-content opacity-70 italic">
|
||||
"{visit.notes}"
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
isEditing = true;
|
||||
const isAllDayEvent = isAllDay(visit.start_date);
|
||||
allDay = isAllDayEvent;
|
||||
|
||||
// Set timezone information if available
|
||||
if ('start_timezone' in visit) {
|
||||
// TransportationVisit
|
||||
selectedStartTimezone = visit.start_timezone;
|
||||
selectedEndTimezone = visit.end_timezone;
|
||||
} else if (visit.timezone) {
|
||||
// Visit
|
||||
selectedStartTimezone = visit.timezone;
|
||||
}
|
||||
|
||||
if (isAllDayEvent) {
|
||||
localStartDate = visit.start_date.split('T')[0];
|
||||
localEndDate = visit.end_date.split('T')[0];
|
||||
} else {
|
||||
// Update with timezone awareness
|
||||
localStartDate = updateLocalDate({
|
||||
utcDate: visit.start_date,
|
||||
timezone: selectedStartTimezone
|
||||
}).localDate;
|
||||
|
||||
localEndDate = updateLocalDate({
|
||||
utcDate: visit.end_date,
|
||||
timezone:
|
||||
'end_timezone' in visit ? visit.end_timezone : selectedStartTimezone
|
||||
}).localDate;
|
||||
}
|
||||
|
||||
// remove it from visits
|
||||
if (visits) {
|
||||
visits = visits.filter((v) => v.id !== visit.id);
|
||||
}
|
||||
|
||||
note = visit.notes;
|
||||
constrainDates = true;
|
||||
utcStartDate = visit.start_date;
|
||||
utcEndDate = visit.end_date;
|
||||
|
||||
setTimeout(() => {
|
||||
isEditing = false;
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
{$t('lodging.edit')}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-error btn-sm"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (visits) {
|
||||
visits = visits.filter((v) => v.id !== visit.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{$t('adventures.remove')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
|
@ -18,6 +18,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
function formatDateInTimezone(utcDate: string, timezone?: string): string {
|
||||
if (!utcDate) return '';
|
||||
try {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
timeZone: timezone,
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
}).format(new Date(utcDate));
|
||||
} catch {
|
||||
return new Date(utcDate).toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
export let lodging: Lodging;
|
||||
export let user: User | null = null;
|
||||
export let collection: Collection | null = null;
|
||||
|
@ -96,20 +113,15 @@
|
|||
>
|
||||
<div class="card-body space-y-4">
|
||||
<!-- Title and Type -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="card-title text-lg font-semibold truncate">{lodging.name}</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="badge badge-secondary">
|
||||
{$t(`lodging.${lodging.type}`) + ' ' + getLodgingIcon(lodging.type)}
|
||||
</div>
|
||||
<!-- {#if hotel.type == 'plane' && hotel.flight_number}
|
||||
<div class="badge badge-neutral-200">{hotel.flight_number}</div>
|
||||
{/if} -->
|
||||
<h2 class="text-2xl font-semibold">{lodging.name}</h2>
|
||||
<div>
|
||||
<div class="badge badge-secondary">
|
||||
{$t(`lodging.${lodging.type}`) + ' ' + getLodgingIcon(lodging.type)}
|
||||
</div>
|
||||
{#if unlinked}
|
||||
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if unlinked}
|
||||
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||
{/if}
|
||||
|
||||
<!-- Location -->
|
||||
<div class="space-y-2">
|
||||
|
@ -124,21 +136,11 @@
|
|||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm">{$t('adventures.dates')}:</span>
|
||||
<p>
|
||||
{new Date(lodging.check_in).toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
})}
|
||||
-
|
||||
{new Date(lodging.check_out).toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
})}
|
||||
{formatDateInTimezone(lodging.check_in ?? '', lodging.timezone ?? undefined)} –
|
||||
{formatDateInTimezone(lodging.check_out ?? '', lodging.timezone ?? undefined)}
|
||||
{#if lodging.timezone}
|
||||
<span class="text-xs opacity-60 ml-1">({lodging.timezone})</span>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||
import type { Collection, Lodging } from '$lib/types';
|
||||
import LocationDropdown from './LocationDropdown.svelte';
|
||||
import DateRangeCollapse from './DateRangeCollapse.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
@ -12,22 +13,10 @@
|
|||
export let lodgingToEdit: Lodging | null = null;
|
||||
|
||||
let modal: HTMLDialogElement;
|
||||
let constrainDates: boolean = false;
|
||||
let lodging: Lodging = { ...initializeLodging(lodgingToEdit) };
|
||||
let fullStartDate: string = '';
|
||||
let fullEndDate: string = '';
|
||||
|
||||
// Format date as local datetime
|
||||
// Convert an ISO date to a datetime-local value in local time.
|
||||
function toLocalDatetime(value: string | null): string {
|
||||
if (!value) return '';
|
||||
const date = new Date(value);
|
||||
// Adjust the time by subtracting the timezone offset.
|
||||
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
|
||||
// Return format YYYY-MM-DDTHH:mm
|
||||
return date.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
type LodgingType = {
|
||||
value: string;
|
||||
label: string;
|
||||
|
@ -47,27 +36,28 @@
|
|||
{ value: 'other', label: 'Other' }
|
||||
];
|
||||
|
||||
// Initialize hotel with values from hotelToEdit or default values
|
||||
function initializeLodging(hotelToEdit: Lodging | null): Lodging {
|
||||
// Initialize hotel with values from lodgingToEdit or default values
|
||||
function initializeLodging(lodgingToEdit: Lodging | null): Lodging {
|
||||
return {
|
||||
id: hotelToEdit?.id || '',
|
||||
user_id: hotelToEdit?.user_id || '',
|
||||
name: hotelToEdit?.name || '',
|
||||
type: hotelToEdit?.type || 'other',
|
||||
description: hotelToEdit?.description || '',
|
||||
rating: hotelToEdit?.rating || NaN,
|
||||
link: hotelToEdit?.link || '',
|
||||
check_in: hotelToEdit?.check_in ? toLocalDatetime(hotelToEdit.check_in) : null,
|
||||
check_out: hotelToEdit?.check_out ? toLocalDatetime(hotelToEdit.check_out) : null,
|
||||
reservation_number: hotelToEdit?.reservation_number || '',
|
||||
price: hotelToEdit?.price || null,
|
||||
latitude: hotelToEdit?.latitude || null,
|
||||
longitude: hotelToEdit?.longitude || null,
|
||||
location: hotelToEdit?.location || '',
|
||||
is_public: hotelToEdit?.is_public || false,
|
||||
collection: hotelToEdit?.collection || collection.id,
|
||||
created_at: hotelToEdit?.created_at || '',
|
||||
updated_at: hotelToEdit?.updated_at || ''
|
||||
id: lodgingToEdit?.id || '',
|
||||
user_id: lodgingToEdit?.user_id || '',
|
||||
name: lodgingToEdit?.name || '',
|
||||
type: lodgingToEdit?.type || 'other',
|
||||
description: lodgingToEdit?.description || '',
|
||||
rating: lodgingToEdit?.rating || NaN,
|
||||
link: lodgingToEdit?.link || '',
|
||||
check_in: lodgingToEdit?.check_in || null,
|
||||
check_out: lodgingToEdit?.check_out || null,
|
||||
reservation_number: lodgingToEdit?.reservation_number || '',
|
||||
price: lodgingToEdit?.price || null,
|
||||
latitude: lodgingToEdit?.latitude || null,
|
||||
longitude: lodgingToEdit?.longitude || null,
|
||||
location: lodgingToEdit?.location || '',
|
||||
is_public: lodgingToEdit?.is_public || false,
|
||||
collection: lodgingToEdit?.collection || collection.id,
|
||||
created_at: lodgingToEdit?.created_at || '',
|
||||
updated_at: lodgingToEdit?.updated_at || '',
|
||||
timezone: lodgingToEdit?.timezone || ''
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -104,27 +94,6 @@
|
|||
async function handleSubmit(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (lodging.check_in && !lodging.check_out) {
|
||||
const checkInDate = new Date(lodging.check_in);
|
||||
checkInDate.setDate(checkInDate.getDate() + 1);
|
||||
lodging.check_out = checkInDate.toISOString();
|
||||
}
|
||||
|
||||
if (lodging.check_in && lodging.check_out && lodging.check_in > lodging.check_out) {
|
||||
addToast('error', $t('adventures.start_before_end_error'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Only convert to UTC if the time is still in local format.
|
||||
if (lodging.check_in && !lodging.check_in.includes('Z')) {
|
||||
// new Date(lodging.check_in) interprets the input as local time.
|
||||
lodging.check_in = new Date(lodging.check_in).toISOString();
|
||||
}
|
||||
if (lodging.check_out && !lodging.check_out.includes('Z')) {
|
||||
lodging.check_out = new Date(lodging.check_out).toISOString();
|
||||
}
|
||||
console.log(lodging.check_in, lodging.check_out);
|
||||
|
||||
// Create or update lodging...
|
||||
const url = lodging.id === '' ? '/api/lodging' : `/api/lodging/${lodging.id}`;
|
||||
const method = lodging.id === '' ? 'POST' : 'PATCH';
|
||||
|
@ -323,6 +292,7 @@
|
|||
id="price"
|
||||
name="price"
|
||||
bind:value={lodging.price}
|
||||
step="0.01"
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
@ -330,85 +300,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
{$t('adventures.date_information')}
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<!-- Check In -->
|
||||
<div>
|
||||
<label for="date">
|
||||
{$t('lodging.check_in')}
|
||||
</label>
|
||||
|
||||
{#if collection && collection.start_date && collection.end_date}<label
|
||||
class="label cursor-pointer flex items-start space-x-2"
|
||||
>
|
||||
<span class="label-text">{$t('adventures.date_constrain')}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
id="constrain_dates"
|
||||
name="constrain_dates"
|
||||
on:change={() => (constrainDates = !constrainDates)}
|
||||
/></label
|
||||
>
|
||||
{/if}
|
||||
<div>
|
||||
<input
|
||||
type="datetime-local"
|
||||
id="date"
|
||||
name="date"
|
||||
bind:value={lodging.check_in}
|
||||
min={constrainDates ? fullStartDate : ''}
|
||||
max={constrainDates ? fullEndDate : ''}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Date -->
|
||||
<div>
|
||||
<label for="end_date">
|
||||
{$t('lodging.check_out')}
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
type="datetime-local"
|
||||
id="end_date"
|
||||
name="end_date"
|
||||
min={constrainDates ? lodging.check_in : ''}
|
||||
max={constrainDates ? fullEndDate : ''}
|
||||
bind:value={lodging.check_out}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div role="alert" class="alert shadow-lg bg-neutral mt-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-info h-6 w-6 shrink-0"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>
|
||||
{$t('lodging.current_timezone')}:
|
||||
{(() => {
|
||||
const tz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const [continent, city] = tz.split('/');
|
||||
return `${continent} (${city.replace('_', ' ')})`;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DateRangeCollapse
|
||||
type="lodging"
|
||||
bind:utcStartDate={lodging.check_in}
|
||||
bind:utcEndDate={lodging.check_out}
|
||||
bind:selectedStartTimezone={lodging.timezone}
|
||||
/>
|
||||
|
||||
<!-- Location Information -->
|
||||
<LocationDropdown bind:item={lodging} />
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
import { onMount } from 'svelte';
|
||||
let inputElement: HTMLInputElement | null = null;
|
||||
|
||||
let theme = '';
|
||||
|
||||
// Event listener for focusing input
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
// Ignore any keypresses in an input/textarea field, so we don't interfere with typing.
|
||||
|
@ -38,6 +40,8 @@
|
|||
// Attach event listener on component mount
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
|
||||
theme = document.documentElement.getAttribute('data-theme');
|
||||
|
||||
// Cleanup event listener on component destruction
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
|
@ -54,7 +58,8 @@
|
|||
sv: 'Svenska',
|
||||
zh: '中文',
|
||||
pl: 'Polski',
|
||||
ko: '한국어'
|
||||
ko: '한국어',
|
||||
no: "Norsk"
|
||||
};
|
||||
|
||||
let query: string = '';
|
||||
|
@ -68,9 +73,14 @@
|
|||
locale.set(newLocale);
|
||||
window.location.reload();
|
||||
};
|
||||
const submitThemeChange = (event: Event) => {
|
||||
const theme = event.target.value;
|
||||
const themeForm = event.target.parentNode;
|
||||
themeForm.action = `/?/setTheme&theme=${theme}`;
|
||||
themeForm.submit();
|
||||
};
|
||||
const submitUpdateTheme: SubmitFunction = ({ action }) => {
|
||||
const theme = action.searchParams.get('theme');
|
||||
console.log('theme', theme);
|
||||
if (theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
|
@ -116,11 +126,10 @@
|
|||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu dropdown-content mt-3 z-[1] p-2 shadow bg-neutral text-neutral-content rounded-box gap-2 w-96"
|
||||
class="menu dropdown-content mt-3 z-[1] p-2 shadow bg-neutral text-base text-neutral-content rounded-box gap-2 w-96"
|
||||
>
|
||||
{#if data.user}
|
||||
<li>
|
||||
<MapMarker />
|
||||
<button on:click={() => goto('/adventures')}>{$t('navbar.adventures')}</button>
|
||||
</li>
|
||||
<li>
|
||||
|
@ -178,8 +187,9 @@
|
|||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
<a class="btn btn-ghost text-2xl font-bold tracking-normal" href="/">
|
||||
AdventureLog <img src="/favicon.png" alt="Map Logo" class="w-10" />
|
||||
<a class="btn btn-ghost p-0 text-2xl font-bold tracking-normal" href="/">
|
||||
<span class="sm:inline hidden">AdventureLog</span>
|
||||
<img src="/favicon.png" alt="Map Logo" class="w-10" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-center hidden lg:flex">
|
||||
|
@ -264,7 +274,7 @@
|
|||
<Avatar user={data.user} />
|
||||
{/if}
|
||||
<div class="dropdown dropdown-bottom dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn m-1 ml-4">
|
||||
<div tabindex="0" role="button" class="btn m-1 p-2">
|
||||
<DotsHorizontal class="w-6 h-6" />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
|
@ -302,13 +312,15 @@
|
|||
</form>
|
||||
<p class="font-bold m-4 text-lg text-center">{$t('navbar.theme_selection')}</p>
|
||||
<form method="POST" use:enhance={submitUpdateTheme}>
|
||||
{#each themes as theme}
|
||||
<li>
|
||||
<button formaction="/?/setTheme&theme={theme.name}"
|
||||
>{$t(`navbar.themes.${theme.name}`)}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
<select
|
||||
class="select select-bordered w-full max-w-xs bg-base-100 text-base-content"
|
||||
bind:value={theme}
|
||||
on:change={submitThemeChange}
|
||||
>
|
||||
{#each themes as theme}
|
||||
<option value={theme.name} class="text-base-content">{$t(`navbar.themes.${theme.name}`)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</form>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -71,11 +71,28 @@
|
|||
{#if unlinked}
|
||||
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||
{/if}
|
||||
{#if note.content && note.content.length > 0}
|
||||
<p class="line-clamp-6">
|
||||
{note.content}
|
||||
</p>
|
||||
{/if}
|
||||
{#if note.links && note.links.length > 0}
|
||||
<p>
|
||||
{note.links.length}
|
||||
{note.links.length > 1 ? $t('adventures.links') : $t('adventures.link')}
|
||||
</p>
|
||||
<ul class="list-disc pl-6">
|
||||
{#each note.links.slice(0, 3) as link}
|
||||
<li>
|
||||
<a class="link link-primary" href={link}>
|
||||
{link.split('//')[1].split('/', 1)[0]}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
{#if note.links.length > 3}
|
||||
<li>…</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if note.date && note.date !== ''}
|
||||
<div class="inline-flex items-center">
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
if (modal) {
|
||||
modal.showModal();
|
||||
}
|
||||
let res = await fetch(`/auth/users/`);
|
||||
let res = await fetch(`/auth/users`);
|
||||
if (res.ok) {
|
||||
let data = await res.json();
|
||||
allUsers = data;
|
||||
|
|
125
frontend/src/lib/components/TimezoneSelector.svelte
Normal file
125
frontend/src/lib/components/TimezoneSelector.svelte
Normal file
|
@ -0,0 +1,125 @@
|
|||
<script lang="ts">
|
||||
import { t } from 'svelte-i18n';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let selectedTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
// Generate a unique ID for this component instance
|
||||
const instanceId = `tz-selector-${crypto.randomUUID().substring(0, 8)}`;
|
||||
|
||||
let dropdownOpen = false;
|
||||
let searchQuery = '';
|
||||
let searchInput: HTMLInputElement | null = null;
|
||||
const timezones = Intl.supportedValuesOf('timeZone');
|
||||
|
||||
// Filter timezones based on search query
|
||||
$: filteredTimezones = searchQuery
|
||||
? timezones.filter((tz) => tz.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
: timezones;
|
||||
|
||||
function selectTimezone(tz: string) {
|
||||
selectedTimezone = tz;
|
||||
dropdownOpen = false;
|
||||
searchQuery = '';
|
||||
}
|
||||
|
||||
// Focus search input when dropdown opens - with proper null check
|
||||
$: if (dropdownOpen && searchInput) {
|
||||
// Use setTimeout to delay focus until after the element is rendered
|
||||
setTimeout(() => {
|
||||
if (searchInput) searchInput.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent, tz?: string) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
if (tz) selectTimezone(tz);
|
||||
else dropdownOpen = !dropdownOpen;
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
dropdownOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Close dropdown if clicked outside
|
||||
onMount(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const dropdown = document.getElementById(instanceId);
|
||||
if (dropdown && !dropdown.contains(e.target as Node)) dropdownOpen = false;
|
||||
};
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="form-control w-full max-w-xs relative" id={instanceId}>
|
||||
<label class="label" for={`timezone-display-${instanceId}`}>
|
||||
<span class="label-text">{$t('adventures.timezone')}</span>
|
||||
</label>
|
||||
|
||||
<!-- Trigger -->
|
||||
<div
|
||||
id={`timezone-display-${instanceId}`}
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={dropdownOpen}
|
||||
class="input input-bordered flex justify-between items-center cursor-pointer"
|
||||
on:click={() => (dropdownOpen = !dropdownOpen)}
|
||||
on:keydown={handleKeydown}
|
||||
>
|
||||
<span class="truncate">{selectedTimezone}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-4 h-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown -->
|
||||
{#if dropdownOpen}
|
||||
<div
|
||||
class="absolute mt-1 z-10 bg-base-100 shadow-lg rounded-box w-full max-h-60 overflow-y-auto"
|
||||
role="listbox"
|
||||
aria-labelledby={`timezone-display-${instanceId}`}
|
||||
>
|
||||
<!-- Search -->
|
||||
<div class="sticky top-0 bg-base-100 p-2 border-b">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search timezone"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={searchQuery}
|
||||
bind:this={searchInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Timezone list -->
|
||||
{#if filteredTimezones.length > 0}
|
||||
<ul class="menu p-2 space-y-1">
|
||||
{#each filteredTimezones as tz}
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class={`w-full text-left truncate ${tz === selectedTimezone ? 'active font-bold' : ''}`}
|
||||
on:click={() => selectTimezone(tz)}
|
||||
on:keydown={(e) => handleKeydown(e, tz)}
|
||||
role="option"
|
||||
aria-selected={tz === selectedTimezone}
|
||||
>
|
||||
{tz}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<div class="p-2 text-sm text-center opacity-60">No timezones found</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
|
@ -7,9 +7,34 @@
|
|||
import { t } from 'svelte-i18n';
|
||||
import DeleteWarning from './DeleteWarning.svelte';
|
||||
// import ArrowDownThick from '~icons/mdi/arrow-down-thick';
|
||||
import { TRANSPORTATION_TYPES_ICONS } from '$lib';
|
||||
|
||||
function getTransportationIcon(type: string) {
|
||||
if (type in TRANSPORTATION_TYPES_ICONS) {
|
||||
return TRANSPORTATION_TYPES_ICONS[type as keyof typeof TRANSPORTATION_TYPES_ICONS];
|
||||
} else {
|
||||
return '🚗';
|
||||
}
|
||||
}
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function formatDateInTimezone(utcDate: string, timezone?: string): string {
|
||||
if (!utcDate) return '';
|
||||
try {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
timeZone: timezone,
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
}).format(new Date(utcDate));
|
||||
} catch {
|
||||
return new Date(utcDate).toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
export let transportation: Transportation;
|
||||
export let user: User | null = null;
|
||||
export let collection: Collection | null = null;
|
||||
|
@ -102,20 +127,20 @@
|
|||
>
|
||||
<div class="card-body space-y-4">
|
||||
<!-- Title and Type -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="card-title text-lg font-semibold truncate">{transportation.name}</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="badge badge-secondary">
|
||||
{$t(`transportation.modes.${transportation.type}`)}
|
||||
</div>
|
||||
{#if transportation.type == 'plane' && transportation.flight_number}
|
||||
<div class="badge badge-neutral-200">{transportation.flight_number}</div>
|
||||
{/if}
|
||||
<h2 class="card-title text-lg font-semibold truncate">{transportation.name}</h2>
|
||||
<div>
|
||||
<div class="badge badge-secondary">
|
||||
{$t(`transportation.modes.${transportation.type}`) +
|
||||
' ' +
|
||||
getTransportationIcon(transportation.type)}
|
||||
</div>
|
||||
{#if transportation.type == 'plane' && transportation.flight_number}
|
||||
<div class="badge badge-neutral-200">{transportation.flight_number}</div>
|
||||
{/if}
|
||||
{#if unlinked}
|
||||
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if unlinked}
|
||||
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||
{/if}
|
||||
|
||||
<!-- Locations -->
|
||||
<div class="space-y-2">
|
||||
|
@ -128,7 +153,12 @@
|
|||
{#if transportation.date}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm">{$t('adventures.start')}:</span>
|
||||
<p>{new Date(transportation.date).toLocaleString(undefined, { timeZone: 'UTC' })}</p>
|
||||
<p>
|
||||
{formatDateInTimezone(transportation.date, transportation.start_timezone ?? undefined)}
|
||||
{#if transportation.start_timezone}
|
||||
<span class="text-xs opacity-60 ml-1">({transportation.start_timezone})</span>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -146,7 +176,15 @@
|
|||
{#if transportation.end_date}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm">{$t('adventures.end')}:</span>
|
||||
<p>{new Date(transportation.end_date).toLocaleString(undefined, { timeZone: 'UTC' })}</p>
|
||||
<p>
|
||||
{formatDateInTimezone(
|
||||
transportation.end_date,
|
||||
transportation.end_timezone || undefined
|
||||
)}
|
||||
{#if transportation.end_timezone}
|
||||
<span class="text-xs opacity-60 ml-1">({transportation.end_timezone})</span>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -10,27 +10,19 @@
|
|||
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||
import { appVersion } from '$lib/config';
|
||||
import { DefaultMarker, MapLibre } from 'svelte-maplibre';
|
||||
import DateRangeCollapse from './DateRangeCollapse.svelte';
|
||||
|
||||
export let collection: Collection;
|
||||
export let transportationToEdit: Transportation | null = null;
|
||||
|
||||
let constrainDates: boolean = false;
|
||||
|
||||
function toLocalDatetime(value: string | null): string {
|
||||
if (!value) return '';
|
||||
const date = new Date(value);
|
||||
return date.toISOString().slice(0, 16); // Format: YYYY-MM-DDTHH:mm
|
||||
}
|
||||
|
||||
// Initialize transportation object
|
||||
let transportation: Transportation = {
|
||||
id: transportationToEdit?.id || '',
|
||||
type: transportationToEdit?.type || '',
|
||||
name: transportationToEdit?.name || '',
|
||||
description: transportationToEdit?.description || '',
|
||||
date: transportationToEdit?.date ? toLocalDatetime(transportationToEdit.date) : null,
|
||||
end_date: transportationToEdit?.end_date
|
||||
? toLocalDatetime(transportationToEdit.end_date)
|
||||
: null,
|
||||
date: transportationToEdit?.date || null,
|
||||
end_date: transportationToEdit?.end_date || null,
|
||||
rating: transportationToEdit?.rating || 0,
|
||||
link: transportationToEdit?.link || '',
|
||||
flight_number: transportationToEdit?.flight_number || '',
|
||||
|
@ -44,28 +36,27 @@
|
|||
origin_latitude: transportationToEdit?.origin_latitude || NaN,
|
||||
origin_longitude: transportationToEdit?.origin_longitude || NaN,
|
||||
destination_latitude: transportationToEdit?.destination_latitude || NaN,
|
||||
destination_longitude: transportationToEdit?.destination_longitude || NaN
|
||||
destination_longitude: transportationToEdit?.destination_longitude || NaN,
|
||||
start_timezone: transportationToEdit?.start_timezone || '',
|
||||
end_timezone: transportationToEdit?.end_timezone || ''
|
||||
};
|
||||
|
||||
let fullStartDate: string = '';
|
||||
let fullEndDate: string = '';
|
||||
let startTimezone: string | undefined = transportation.start_timezone ?? undefined;
|
||||
let endTimezone: string | undefined = transportation.end_timezone ?? undefined;
|
||||
|
||||
// Later, you should manually sync these back to `transportation` if needed
|
||||
$: transportation.start_timezone = startTimezone ?? '';
|
||||
$: transportation.end_timezone = endTimezone ?? '';
|
||||
|
||||
let starting_airport: string = '';
|
||||
let ending_airport: string = '';
|
||||
|
||||
if (collection.start_date && collection.end_date) {
|
||||
fullStartDate = `${collection.start_date}T00:00`;
|
||||
fullEndDate = `${collection.end_date}T23:59`;
|
||||
}
|
||||
|
||||
$: {
|
||||
if (!transportation.rating) {
|
||||
transportation.rating = NaN;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(transportation);
|
||||
|
||||
onMount(async () => {
|
||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||
if (modal) {
|
||||
|
@ -84,6 +75,7 @@
|
|||
}
|
||||
|
||||
async function geocode(e: Event | null) {
|
||||
// Geocoding logic unchanged
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
@ -167,39 +159,27 @@
|
|||
Math.round(transportation.destination_longitude * 1e6) / 1e6;
|
||||
}
|
||||
|
||||
if (transportation.end_date && !transportation.date) {
|
||||
transportation.date = null;
|
||||
transportation.end_date = null;
|
||||
}
|
||||
|
||||
if (transportation.date && !transportation.end_date) {
|
||||
transportation.end_date = transportation.date;
|
||||
}
|
||||
|
||||
if (
|
||||
transportation.date &&
|
||||
transportation.end_date &&
|
||||
transportation.date > transportation.end_date
|
||||
) {
|
||||
addToast('error', $t('adventures.start_before_end_error'));
|
||||
return;
|
||||
}
|
||||
// Use the stored UTC dates for submission
|
||||
const submissionData = {
|
||||
...transportation
|
||||
};
|
||||
|
||||
if (transportation.type != 'plane') {
|
||||
transportation.flight_number = '';
|
||||
submissionData.flight_number = '';
|
||||
}
|
||||
|
||||
if (transportation.id === '') {
|
||||
if (submissionData.id === '') {
|
||||
let res = await fetch('/api/transportations', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(transportation)
|
||||
body: JSON.stringify(submissionData)
|
||||
});
|
||||
let data = await res.json();
|
||||
if (data.id) {
|
||||
transportation = data as Transportation;
|
||||
|
||||
addToast('success', $t('adventures.adventure_created'));
|
||||
dispatch('save', transportation);
|
||||
} else {
|
||||
|
@ -212,11 +192,12 @@
|
|||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(transportation)
|
||||
body: JSON.stringify(submissionData)
|
||||
});
|
||||
let data = await res.json();
|
||||
if (data.id) {
|
||||
transportation = data as Transportation;
|
||||
|
||||
addToast('success', $t('adventures.adventure_updated'));
|
||||
dispatch('save', transportation);
|
||||
} else {
|
||||
|
@ -366,67 +347,17 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
{$t('adventures.date_information')}
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<!-- Start Date -->
|
||||
<div>
|
||||
<label for="date">
|
||||
{$t('adventures.start_date')}
|
||||
</label>
|
||||
|
||||
{#if collection && collection.start_date && collection.end_date}<label
|
||||
class="label cursor-pointer flex items-start space-x-2"
|
||||
>
|
||||
<span class="label-text">{$t('adventures.date_constrain')}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
id="constrain_dates"
|
||||
name="constrain_dates"
|
||||
on:change={() => (constrainDates = !constrainDates)}
|
||||
/></label
|
||||
>
|
||||
{/if}
|
||||
<div>
|
||||
<input
|
||||
type="datetime-local"
|
||||
id="date"
|
||||
name="date"
|
||||
bind:value={transportation.date}
|
||||
min={constrainDates ? fullStartDate : ''}
|
||||
max={constrainDates ? fullEndDate : ''}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Date -->
|
||||
{#if transportation.date}
|
||||
<div>
|
||||
<label for="end_date">
|
||||
{$t('adventures.end_date')}
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
type="datetime-local"
|
||||
id="end_date"
|
||||
name="end_date"
|
||||
min={constrainDates ? transportation.date : ''}
|
||||
max={constrainDates ? fullEndDate : ''}
|
||||
bind:value={transportation.end_date}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<DateRangeCollapse
|
||||
type="transportation"
|
||||
bind:utcStartDate={transportation.date}
|
||||
bind:utcEndDate={transportation.end_date}
|
||||
bind:selectedStartTimezone={startTimezone}
|
||||
bind:selectedEndTimezone={endTimezone}
|
||||
{collection}
|
||||
/>
|
||||
|
||||
<!-- Flight Information -->
|
||||
|
||||
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
|
@ -549,11 +480,6 @@
|
|||
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full rounded-lg"
|
||||
standardControls
|
||||
>
|
||||
<!-- MapEvents gives you access to map events even from other components inside the map,
|
||||
where you might not have access to the top-level `MapLibre` component. In this case
|
||||
it would also work to just use on:click on the MapLibre component itself. -->
|
||||
<!-- @ts-ignore -->
|
||||
|
||||
{#if transportation.origin_latitude && transportation.origin_longitude}
|
||||
<DefaultMarker
|
||||
lngLat={[transportation.origin_longitude, transportation.origin_latitude]}
|
||||
|
@ -568,7 +494,6 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
|||
/>
|
||||
{/if}
|
||||
</MapLibre>
|
||||
<!-- button to clear to and from location -->
|
||||
</div>
|
||||
{#if transportation.from_location || transportation.to_location}
|
||||
<button
|
||||
|
|
550
frontend/src/lib/dateUtils.ts
Normal file
550
frontend/src/lib/dateUtils.ts
Normal file
|
@ -0,0 +1,550 @@
|
|||
// @ts-ignore
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
/**
|
||||
* Convert a UTC ISO date to a datetime-local value in the specified timezone
|
||||
* @param utcDate - UTC date in ISO format or null
|
||||
* @param timezone - Target timezone (defaults to browser timezone)
|
||||
* @returns Formatted local datetime string for input fields (YYYY-MM-DDTHH:MM)
|
||||
*/
|
||||
export function toLocalDatetime(
|
||||
utcDate: string | null,
|
||||
timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
): string {
|
||||
if (!utcDate) return '';
|
||||
|
||||
const dt = DateTime.fromISO(utcDate, { zone: 'UTC' });
|
||||
if (!dt.isValid) return '';
|
||||
|
||||
const isoString = dt.setZone(timezone).toISO({
|
||||
suppressSeconds: true,
|
||||
includeOffset: false
|
||||
});
|
||||
|
||||
return isoString ? isoString.slice(0, 16) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a local datetime to UTC
|
||||
* @param localDate - Local datetime string in ISO format
|
||||
* @param timezone - Source timezone (defaults to browser timezone)
|
||||
* @returns UTC datetime in ISO format or null
|
||||
*/
|
||||
export function toUTCDatetime(
|
||||
localDate: string,
|
||||
timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
allDay: boolean = false
|
||||
): string | null {
|
||||
if (!localDate) return null;
|
||||
|
||||
if (allDay) {
|
||||
// Treat input as date-only, set UTC midnight manually
|
||||
return DateTime.fromISO(localDate, { zone: 'UTC' })
|
||||
.startOf('day')
|
||||
.toISO({ suppressMilliseconds: true });
|
||||
}
|
||||
|
||||
// Normal timezone conversion for datetime-local input
|
||||
return DateTime.fromISO(localDate, { zone: timezone })
|
||||
.toUTC()
|
||||
.toISO({ suppressMilliseconds: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates local datetime values based on UTC date and timezone
|
||||
* @param params Object containing UTC date and timezone
|
||||
* @returns Object with updated local datetime string
|
||||
*/
|
||||
export function updateLocalDate({
|
||||
utcDate,
|
||||
timezone
|
||||
}: {
|
||||
utcDate: string | null;
|
||||
timezone: string;
|
||||
}) {
|
||||
return {
|
||||
localDate: toLocalDatetime(utcDate, timezone)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates UTC datetime values based on local datetime and timezone
|
||||
* @param params Object containing local date and timezone
|
||||
* @returns Object with updated UTC datetime string
|
||||
*/
|
||||
export function updateUTCDate({
|
||||
localDate,
|
||||
timezone,
|
||||
allDay = false
|
||||
}: {
|
||||
localDate: string;
|
||||
timezone: string;
|
||||
allDay?: boolean;
|
||||
}) {
|
||||
return {
|
||||
utcDate: toUTCDatetime(localDate, timezone, allDay)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate date ranges
|
||||
* @param startDate - Start date string
|
||||
* @param endDate - End date string
|
||||
* @returns Object with validation result and error message
|
||||
*/
|
||||
export function validateDateRange(
|
||||
startDate: string,
|
||||
endDate: string
|
||||
): { valid: boolean; error?: string } {
|
||||
if (endDate && !startDate) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Start date is required when end date is provided'
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
startDate &&
|
||||
endDate &&
|
||||
DateTime.fromISO(startDate).toMillis() > DateTime.fromISO(endDate).toMillis()
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Start date must be before end date'
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Format UTC date for display
|
||||
* @param utcDate - UTC date in ISO format
|
||||
* @returns Formatted date string without seconds (YYYY-MM-DD HH:MM)
|
||||
*/
|
||||
export function formatUTCDate(utcDate: string | null): string {
|
||||
if (!utcDate) return '';
|
||||
const dateTime = DateTime.fromISO(utcDate);
|
||||
if (!dateTime.isValid) return '';
|
||||
return dateTime.toISO()?.slice(0, 16).replace('T', ' ') || '';
|
||||
}
|
||||
|
||||
export const VALID_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'
|
||||
];
|
|
@ -70,34 +70,65 @@ export function groupAdventuresByDate(
|
|||
// Initialize all days in the range
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
groupedAdventures[dateString] = [];
|
||||
}
|
||||
|
||||
adventures.forEach((adventure) => {
|
||||
adventure.visits.forEach((visit) => {
|
||||
if (visit.start_date) {
|
||||
const adventureDate = new Date(visit.start_date).toISOString().split('T')[0];
|
||||
if (visit.end_date) {
|
||||
const endDate = new Date(visit.end_date).toISOString().split('T')[0];
|
||||
// Check if this is an all-day event (both start and end at midnight)
|
||||
const isAllDayEvent =
|
||||
isAllDay(visit.start_date) && (visit.end_date ? isAllDay(visit.end_date) : false);
|
||||
|
||||
// Loop through all days and include adventure if it falls within the range
|
||||
// For all-day events, we need to handle dates differently
|
||||
if (isAllDayEvent && visit.end_date) {
|
||||
// Extract just the date parts without time
|
||||
const startDateStr = visit.start_date.split('T')[0];
|
||||
const endDateStr = visit.end_date.split('T')[0];
|
||||
|
||||
// Loop through all days in the range
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const currentDateStr = getLocalDateString(currentDate);
|
||||
|
||||
// Include the current day if it falls within the adventure date range
|
||||
if (dateString >= adventureDate && dateString <= endDate) {
|
||||
if (groupedAdventures[dateString]) {
|
||||
groupedAdventures[dateString].push(adventure);
|
||||
if (currentDateStr >= startDateStr && currentDateStr <= endDateStr) {
|
||||
if (groupedAdventures[currentDateStr]) {
|
||||
groupedAdventures[currentDateStr].push(adventure);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (groupedAdventures[adventureDate]) {
|
||||
// If there's no end date, add adventure to the start date only
|
||||
groupedAdventures[adventureDate].push(adventure);
|
||||
} else {
|
||||
// Handle regular events with time components
|
||||
const adventureStartDate = new Date(visit.start_date);
|
||||
const adventureDateStr = getLocalDateString(adventureStartDate);
|
||||
|
||||
if (visit.end_date) {
|
||||
const adventureEndDate = new Date(visit.end_date);
|
||||
const endDateStr = getLocalDateString(adventureEndDate);
|
||||
|
||||
// Loop through all days and include adventure if it falls within the range
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
|
||||
// Include the current day if it falls within the adventure date range
|
||||
if (dateString >= adventureDateStr && dateString <= endDateStr) {
|
||||
if (groupedAdventures[dateString]) {
|
||||
groupedAdventures[dateString].push(adventure);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If there's no end date, add adventure to the start date only
|
||||
if (groupedAdventures[adventureDateStr]) {
|
||||
groupedAdventures[adventureDateStr].push(adventure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -106,6 +137,20 @@ export function groupAdventuresByDate(
|
|||
return groupedAdventures;
|
||||
}
|
||||
|
||||
function getLocalDateString(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
// Helper to check if a given date string represents midnight (all-day)
|
||||
// Improved isAllDay function to handle different ISO date formats
|
||||
export function isAllDay(dateStr: string): boolean {
|
||||
// Check for various midnight formats in UTC
|
||||
return dateStr.endsWith('T00:00:00Z') || dateStr.endsWith('T00:00:00.000Z');
|
||||
}
|
||||
|
||||
export function groupTransportationsByDate(
|
||||
transportations: Transportation[],
|
||||
startDate: Date,
|
||||
|
@ -116,22 +161,22 @@ export function groupTransportationsByDate(
|
|||
// Initialize all days in the range
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
groupedTransportations[dateString] = [];
|
||||
}
|
||||
|
||||
transportations.forEach((transportation) => {
|
||||
if (transportation.date) {
|
||||
const transportationDate = new Date(transportation.date).toISOString().split('T')[0];
|
||||
const transportationDate = getLocalDateString(new Date(transportation.date));
|
||||
if (transportation.end_date) {
|
||||
const endDate = new Date(transportation.end_date).toISOString().split('T')[0];
|
||||
|
||||
// Loop through all days and include transportation if it falls within the range
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
|
||||
// Include the current day if it falls within the transportation date range
|
||||
if (dateString >= transportationDate && dateString <= endDate) {
|
||||
|
@ -157,35 +202,32 @@ export function groupLodgingByDate(
|
|||
): Record<string, Lodging[]> {
|
||||
const groupedTransportations: Record<string, Lodging[]> = {};
|
||||
|
||||
// Initialize all days in the range
|
||||
// Initialize all days in the range using local dates
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
groupedTransportations[dateString] = [];
|
||||
}
|
||||
|
||||
transportations.forEach((transportation) => {
|
||||
if (transportation.check_in) {
|
||||
const transportationDate = new Date(transportation.check_in).toISOString().split('T')[0];
|
||||
// Use local date string conversion
|
||||
const transportationDate = getLocalDateString(new Date(transportation.check_in));
|
||||
if (transportation.check_out) {
|
||||
const endDate = new Date(transportation.check_out).toISOString().split('T')[0];
|
||||
const endDate = getLocalDateString(new Date(transportation.check_out));
|
||||
|
||||
// Loop through all days and include transportation if it falls within the range
|
||||
// Loop through all days and include transportation if it falls within the transportation date range
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
|
||||
// Include the current day if it falls within the transportation date range
|
||||
if (dateString >= transportationDate && dateString <= endDate) {
|
||||
if (groupedTransportations[dateString]) {
|
||||
groupedTransportations[dateString].push(transportation);
|
||||
}
|
||||
groupedTransportations[dateString].push(transportation);
|
||||
}
|
||||
}
|
||||
} else if (groupedTransportations[transportationDate]) {
|
||||
// If there's no end date, add transportation to the start date only
|
||||
groupedTransportations[transportationDate].push(transportation);
|
||||
}
|
||||
}
|
||||
|
@ -201,19 +243,18 @@ export function groupNotesByDate(
|
|||
): Record<string, Note[]> {
|
||||
const groupedNotes: Record<string, Note[]> = {};
|
||||
|
||||
// Initialize all days in the range
|
||||
// Initialize all days in the range using local dates
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
groupedNotes[dateString] = [];
|
||||
}
|
||||
|
||||
notes.forEach((note) => {
|
||||
if (note.date) {
|
||||
const noteDate = new Date(note.date).toISOString().split('T')[0];
|
||||
|
||||
// Add note to the appropriate date group if it exists
|
||||
// Use the date string as is since it's already in "YYYY-MM-DD" format.
|
||||
const noteDate = note.date;
|
||||
if (groupedNotes[noteDate]) {
|
||||
groupedNotes[noteDate].push(note);
|
||||
}
|
||||
|
@ -230,19 +271,18 @@ export function groupChecklistsByDate(
|
|||
): Record<string, Checklist[]> {
|
||||
const groupedChecklists: Record<string, Checklist[]> = {};
|
||||
|
||||
// Initialize all days in the range
|
||||
// Initialize all days in the range using local dates
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
currentDate.setDate(startDate.getDate() + i);
|
||||
const dateString = getLocalDateString(currentDate);
|
||||
groupedChecklists[dateString] = [];
|
||||
}
|
||||
|
||||
checklists.forEach((checklist) => {
|
||||
if (checklist.date) {
|
||||
const checklistDate = new Date(checklist.date).toISOString().split('T')[0];
|
||||
|
||||
// Add checklist to the appropriate date group if it exists
|
||||
// Use the date string as is since it's already in "YYYY-MM-DD" format.
|
||||
const checklistDate = checklist.date;
|
||||
if (groupedChecklists[checklistDate]) {
|
||||
groupedChecklists[checklistDate].push(checklist);
|
||||
}
|
||||
|
@ -338,6 +378,17 @@ export let LODGING_TYPES_ICONS = {
|
|||
other: '❓'
|
||||
};
|
||||
|
||||
export let TRANSPORTATION_TYPES_ICONS = {
|
||||
car: '🚗',
|
||||
plane: '✈️',
|
||||
train: '🚆',
|
||||
bus: '🚌',
|
||||
boat: '⛵',
|
||||
bike: '🚲',
|
||||
walking: '🚶',
|
||||
other: '❓'
|
||||
};
|
||||
|
||||
export function getAdventureTypeLabel(type: string) {
|
||||
// return the emoji ADVENTURE_TYPE_ICONS label for the given type if not found return ? emoji
|
||||
if (type in ADVENTURE_TYPE_ICONS) {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { VALID_TIMEZONES } from './dateUtils';
|
||||
|
||||
export type User = {
|
||||
pk: number;
|
||||
username: string;
|
||||
|
@ -30,6 +32,7 @@ export type Adventure = {
|
|||
id: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
timezone: string | null;
|
||||
notes: string;
|
||||
}[];
|
||||
collection?: string | null;
|
||||
|
@ -44,6 +47,15 @@ export type Adventure = {
|
|||
user?: User | null;
|
||||
};
|
||||
|
||||
export type AdditionalAdventure = Adventure & {
|
||||
sun_times: {
|
||||
date: string;
|
||||
visit_id: string;
|
||||
sunrise: string;
|
||||
sunset: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type Country = {
|
||||
id: number;
|
||||
name: string;
|
||||
|
@ -151,6 +163,8 @@ export type Transportation = {
|
|||
link: string | null;
|
||||
date: string | null; // ISO 8601 date string
|
||||
end_date: string | null; // ISO 8601 date string
|
||||
start_timezone: string | null;
|
||||
end_timezone: string | null;
|
||||
flight_number: string | null;
|
||||
from_location: string | null;
|
||||
to_location: string | null;
|
||||
|
@ -279,6 +293,7 @@ export type Lodging = {
|
|||
link: string | null;
|
||||
check_in: string | null; // ISO 8601 date string
|
||||
check_out: string | null; // ISO 8601 date string
|
||||
timezone: string | null;
|
||||
reservation_number: string | null;
|
||||
price: number | null;
|
||||
latitude: number | null;
|
||||
|
|
|
@ -247,7 +247,19 @@
|
|||
"price": "Preis",
|
||||
"reservation_number": "Reservierungsnummer",
|
||||
"welcome_map_info": "Frei zugängliche Abenteuer auf diesem Server",
|
||||
"open_in_maps": "In Karten öffnen"
|
||||
"open_in_maps": "In Karten öffnen",
|
||||
"all_day": "Den ganzen Tag",
|
||||
"collection_no_start_end_date": "Durch das Hinzufügen eines Start- und Enddatums zur Sammlung werden Reiseroutenplanungsfunktionen auf der Sammlungsseite freigegeben.",
|
||||
"date_itinerary": "Datumstrecke",
|
||||
"no_ordered_items": "Fügen Sie der Sammlung Elemente mit Daten hinzu, um sie hier zu sehen.",
|
||||
"ordered_itinerary": "Reiseroute bestellt",
|
||||
"additional_info": "Weitere Informationen",
|
||||
"invalid_date_range": "Ungültiger Datumsbereich",
|
||||
"sunrise_sunset": "Sonnenaufgang",
|
||||
"timezone": "Zeitzone",
|
||||
"no_visits": "Keine Besuche",
|
||||
"arrival_timezone": "Ankunftszeitzone",
|
||||
"departure_timezone": "Abfahrtszeit"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Entdecken, planen und erkunden Sie mühelos",
|
||||
|
|
|
@ -62,6 +62,11 @@
|
|||
"collection_remove_success": "Adventure removed from collection successfully!",
|
||||
"collection_remove_error": "Error removing adventure from collection",
|
||||
"collection_link_success": "Adventure linked to collection successfully!",
|
||||
"invalid_date_range": "Invalid date range",
|
||||
"timezone": "Timezone",
|
||||
"no_visits": "No visits",
|
||||
"departure_timezone": "Departure Timezone",
|
||||
"arrival_timezone": "Arrival Timezone",
|
||||
"no_image_found": "No image found",
|
||||
"collection_link_error": "Error linking adventure to collection",
|
||||
"adventure_delete_confirm": "Are you sure you want to delete this adventure? This action cannot be undone.",
|
||||
|
@ -90,6 +95,8 @@
|
|||
"visits": "Visits",
|
||||
"create_new": "Create New...",
|
||||
"adventure": "Adventure",
|
||||
"additional_info": "Additional Information",
|
||||
"sunrise_sunset": "Sunrise & Sunset",
|
||||
"count_txt": "results matching your search",
|
||||
"sort": "Sort",
|
||||
"order_by": "Order By",
|
||||
|
@ -130,7 +137,8 @@
|
|||
"location": "Location",
|
||||
"search_for_location": "Search for a location",
|
||||
"clear_map": "Clear map",
|
||||
"search_results": "Searh results",
|
||||
"search_results": "Search results",
|
||||
"collection_no_start_end_date": "Adding a start and end date to the collection will unlock itinerary planning features in the collection page.",
|
||||
"no_results": "No results found",
|
||||
"wiki_desc": "Pulls excerpt from Wikipedia article matching the name of the adventure.",
|
||||
"attachments": "Attachments",
|
||||
|
@ -250,6 +258,10 @@
|
|||
"show_map": "Show Map",
|
||||
"emoji_picker": "Emoji Picker",
|
||||
"download_calendar": "Download Calendar",
|
||||
"all_day": "All Day",
|
||||
"ordered_itinerary": "Ordered Itinerary",
|
||||
"date_itinerary": "Date Itinerary",
|
||||
"no_ordered_items": "Add items with dates to the collection to see them here.",
|
||||
"date_information": "Date Information",
|
||||
"flight_information": "Flight Information",
|
||||
"out_of_range": "Not in itinerary date range",
|
||||
|
|
|
@ -295,7 +295,19 @@
|
|||
"region": "Región",
|
||||
"reservation_number": "Número de reserva",
|
||||
"welcome_map_info": "Aventuras públicas en este servidor",
|
||||
"open_in_maps": "Abrir en mapas"
|
||||
"open_in_maps": "Abrir en mapas",
|
||||
"all_day": "Todo el día",
|
||||
"collection_no_start_end_date": "Agregar una fecha de inicio y finalización a la colección desbloqueará las funciones de planificación del itinerario en la página de colección.",
|
||||
"date_itinerary": "Itinerario de fecha",
|
||||
"no_ordered_items": "Agregue elementos con fechas a la colección para verlos aquí.",
|
||||
"ordered_itinerary": "Itinerario ordenado",
|
||||
"additional_info": "información adicional",
|
||||
"invalid_date_range": "Rango de fechas no válido",
|
||||
"sunrise_sunset": "Amanecer",
|
||||
"timezone": "Zona horaria",
|
||||
"no_visits": "No hay visitas",
|
||||
"arrival_timezone": "Zona horaria de llegada",
|
||||
"departure_timezone": "Zona horaria de salida"
|
||||
},
|
||||
"worldtravel": {
|
||||
"all": "Todo",
|
||||
|
|
|
@ -50,19 +50,19 @@
|
|||
"adventure_delete_success": "Aventure supprimée avec succès !",
|
||||
"adventure_details": "Détails de l'aventure",
|
||||
"adventure_type": "Type d'aventure",
|
||||
"archive": "Archive",
|
||||
"archived": "Archivé",
|
||||
"archive": "Archiver",
|
||||
"archived": "Archivée",
|
||||
"archived_collection_message": "Collection archivée avec succès !",
|
||||
"archived_collections": "Collections archivées",
|
||||
"ascending": "Ascendant",
|
||||
"cancel": "Annuler",
|
||||
"category_filter": "Filtre de catégorie",
|
||||
"clear": "Clair",
|
||||
"category_filter": "Filtres de catégorie",
|
||||
"clear": "Réinitialiser",
|
||||
"close_filters": "Fermer les filtres",
|
||||
"collection": "Collection",
|
||||
"collection_adventures": "Inclure les aventures de collection",
|
||||
"collection_adventures": "Inclure les aventures liées à une collection",
|
||||
"count_txt": "résultats correspondant à votre recherche",
|
||||
"create_new": "Créer un nouveau...",
|
||||
"create_new": "Créer une nouvelle aventure...",
|
||||
"date": "Date",
|
||||
"dates": "Dates",
|
||||
"delete_adventure": "Supprimer l'aventure",
|
||||
|
@ -72,7 +72,7 @@
|
|||
"descending": "Descendant",
|
||||
"duration": "Durée",
|
||||
"edit_collection": "Modifier la collection",
|
||||
"filter": "Filtre",
|
||||
"filter": "Filtrer",
|
||||
"homepage": "Page d'accueil",
|
||||
"image_removed_error": "Erreur lors de la suppression de l'image",
|
||||
"image_removed_success": "Image supprimée avec succès !",
|
||||
|
@ -84,12 +84,12 @@
|
|||
"name": "Nom",
|
||||
"no_image_url": "Aucune image trouvée à cette URL.",
|
||||
"not_found": "Aventure introuvable",
|
||||
"not_found_desc": "L'aventure que vous cherchiez est introuvable. \nVeuillez essayer une autre aventure ou revenez plus tard.",
|
||||
"not_found_desc": "L'aventure que vous cherchez est introuvable. \nVeuillez essayer une autre aventure ou revenez plus tard.",
|
||||
"open_filters": "Ouvrir les filtres",
|
||||
"order_by": "Commander par",
|
||||
"order_direction": "Direction de la commande",
|
||||
"planned": "Prévu",
|
||||
"private": "Privé",
|
||||
"order_by": "Trier par",
|
||||
"order_direction": "Direction du tri",
|
||||
"planned": "Prévue",
|
||||
"private": "Privée",
|
||||
"public": "Publique",
|
||||
"rating": "Notation",
|
||||
"share": "Partager",
|
||||
|
@ -98,12 +98,12 @@
|
|||
"start_before_end_error": "La date de début doit être antérieure à la date de fin",
|
||||
"unarchive": "Désarchiver",
|
||||
"unarchived_collection_message": "Collection désarchivée avec succès !",
|
||||
"updated": "Mis à jour",
|
||||
"updated": "Mise à jour",
|
||||
"visit": "Visite",
|
||||
"visited": "Visité",
|
||||
"visited": "Visitée",
|
||||
"visits": "Visites",
|
||||
"wiki_image_error": "Erreur lors de la récupération de l'image depuis Wikipédia",
|
||||
"actions": "Actes",
|
||||
"actions": "Actions",
|
||||
"activity": "Activité",
|
||||
"activity_types": "Types d'activités",
|
||||
"add": "Ajouter",
|
||||
|
@ -117,7 +117,7 @@
|
|||
"category": "Catégorie",
|
||||
"clear_map": "Effacer la carte",
|
||||
"copy_link": "Copier le lien",
|
||||
"date_constrain": "Contraindre aux dates de collecte",
|
||||
"date_constrain": "Limiter aux dates de la collection",
|
||||
"description": "Description",
|
||||
"end_date": "Date de fin",
|
||||
"fetch_image": "Récupérer une image",
|
||||
|
@ -125,7 +125,7 @@
|
|||
"image": "Image",
|
||||
"image_fetch_failed": "Échec de la récupération de l'image",
|
||||
"link": "Lien",
|
||||
"location": "Emplacement",
|
||||
"location": "Lieu",
|
||||
"location_information": "Informations de localisation",
|
||||
"my_images": "Mes images",
|
||||
"my_visits": "Mes visites",
|
||||
|
@ -138,7 +138,7 @@
|
|||
"public_adventure": "Aventure publique",
|
||||
"remove": "Retirer",
|
||||
"save_next": "Sauvegarder",
|
||||
"search_for_location": "Rechercher un emplacement",
|
||||
"search_for_location": "Rechercher un lieu",
|
||||
"search_results": "Résultats de la recherche",
|
||||
"see_adventures": "Voir les aventures",
|
||||
"select_adventure_category": "Sélectionnez la catégorie d'aventure",
|
||||
|
@ -148,73 +148,73 @@
|
|||
"upload_images_here": "Téléchargez des images ici",
|
||||
"url": "URL",
|
||||
"warning": "Avertissement",
|
||||
"wiki_desc": "Extrait un extrait de l'article Wikipédia correspondant au nom de l'aventure.",
|
||||
"wiki_desc": "Obtient un extrait de l'article Wikipédia correspondant au nom de l'aventure.",
|
||||
"wikipedia": "Wikipédia",
|
||||
"adventure_not_found": "Il n'y a aucune aventure à afficher. \nAjoutez-en en utilisant le bouton plus en bas à droite ou essayez de changer les filtres !",
|
||||
"adventure_not_found": "Il n'y a aucune aventure à afficher. \nAjoutez-en en utilisant le bouton '+' en bas à droite ou essayez de changer les filtres !",
|
||||
"all": "Tous",
|
||||
"error_updating_regions": "Erreur lors de la mise à jour des régions",
|
||||
"mark_region_as_visited": "Marquer la région {region}, {country} comme visitée ?",
|
||||
"mark_visited": "Mark a visité",
|
||||
"mark_visited": "Marquer comme visité",
|
||||
"my_adventures": "Mes aventures",
|
||||
"no_adventures_found": "Aucune aventure trouvée",
|
||||
"no_collections_found": "Aucune collection trouvée pour ajouter cette aventure.",
|
||||
"no_linkable_adventures": "Aucune aventure trouvée pouvant être liée à cette collection.",
|
||||
"not_visited": "Non visité",
|
||||
"not_visited": "Non visitée",
|
||||
"regions_updated": "régions mises à jour",
|
||||
"update_visited_regions": "Mettre à jour les régions visitées",
|
||||
"update_visited_regions_disclaimer": "Cela peut prendre un certain temps en fonction du nombre d'aventures que vous avez visitées.",
|
||||
"visited_region_check": "Vérification de la région visitée",
|
||||
"visited_region_check_desc": "En sélectionnant cette option, le serveur vérifiera toutes vos aventures visitées et marquera les régions dans lesquelles elles se trouvent comme visitées lors des voyages dans le monde.",
|
||||
"visited_region_check_desc": "En sélectionnant cette option, le serveur vérifiera toutes vos aventures visitées et marquera les régions correspondantes comme visitées dans la section 'Voyage dans le monde'.",
|
||||
"add_new": "Ajouter un nouveau...",
|
||||
"checklists": "Listes de contrôle",
|
||||
"collection_archived": "Cette collection a été archivée.",
|
||||
"collection_completed": "Vous avez terminé cette collection !",
|
||||
"collection_stats": "Statistiques de collecte",
|
||||
"collection_stats": "Statistiques de la collection",
|
||||
"days": "jours",
|
||||
"itineary_by_date": "Itinéraire par date",
|
||||
"itineary_by_date": "Itinéraire trié par date",
|
||||
"keep_exploring": "Continuez à explorer !",
|
||||
"link_new": "Lien Nouveau...",
|
||||
"link_new": "Ajouter un lien vers...",
|
||||
"linked_adventures": "Aventures liées",
|
||||
"links": "Links",
|
||||
"links": "Liens",
|
||||
"no_end_date": "Veuillez saisir une date de fin",
|
||||
"note": "Note",
|
||||
"notes": "Remarques",
|
||||
"notes": "Notes",
|
||||
"nothing_planned": "Rien de prévu pour cette journée. \nBon voyage !",
|
||||
"transportation": "Transport",
|
||||
"transportations": "Transports",
|
||||
"visit_link": "Visitez le lien",
|
||||
"transportation": "Déplacement",
|
||||
"transportations": "Déplacements",
|
||||
"visit_link": "Visiter le lien",
|
||||
"checklist": "Liste de contrôle",
|
||||
"day": "Jour",
|
||||
"add_a_tag": "Ajouter une balise",
|
||||
"tags": "Balises",
|
||||
"set_to_pin": "Définir sur Épingler",
|
||||
"set_to_pin": "Épingler",
|
||||
"category_fetch_error": "Erreur lors de la récupération des catégories",
|
||||
"copied_to_clipboard": "Copié dans le presse-papier !",
|
||||
"copy_failed": "Échec de la copie",
|
||||
"adventure_calendar": "Calendrier d'aventure",
|
||||
"adventure_calendar": "Calendrier des aventures",
|
||||
"emoji_picker": "Sélecteur d'émoticônes",
|
||||
"hide": "Cacher",
|
||||
"show": "Montrer",
|
||||
"download_calendar": "Télécharger le calendrier",
|
||||
"md_instructions": "Écrivez votre démarque ici...",
|
||||
"md_instructions": "Écrivez ici au format Markdown...",
|
||||
"preview": "Aperçu",
|
||||
"checklist_delete_confirm": "Êtes-vous sûr de vouloir supprimer cette liste de contrôle ? \nCette action ne peut pas être annulée.",
|
||||
"clear_location": "Effacer l'emplacement",
|
||||
"date_information": "Informations sur les dates",
|
||||
"clear_location": "Effacer le lieu",
|
||||
"date_information": "Dates",
|
||||
"delete_checklist": "Supprimer la liste de contrôle",
|
||||
"delete_note": "Supprimer la note",
|
||||
"delete_transportation": "Supprimer le transport",
|
||||
"end": "Fin",
|
||||
"ending_airport": "Aéroport de fin",
|
||||
"delete_transportation": "Supprimer le déplacement",
|
||||
"end": "Arrivée",
|
||||
"ending_airport": "Aéroport d'arrivée",
|
||||
"flight_information": "Informations sur le vol",
|
||||
"from": "Depuis",
|
||||
"no_location_found": "Aucun emplacement trouvé",
|
||||
"no_location_found": "Aucun lieu trouvé",
|
||||
"note_delete_confirm": "Êtes-vous sûr de vouloir supprimer cette note ? \nCette action ne peut pas être annulée.",
|
||||
"out_of_range": "Pas dans la plage de dates de l'itinéraire",
|
||||
"show_region_labels": "Afficher les étiquettes de région",
|
||||
"start": "Commencer",
|
||||
"start": "Départ",
|
||||
"starting_airport": "Aéroport de départ",
|
||||
"to": "À",
|
||||
"to": "Vers",
|
||||
"transportation_delete_confirm": "Etes-vous sûr de vouloir supprimer ce transport ? \nCette action ne peut pas être annulée.",
|
||||
"show_map": "Afficher la carte",
|
||||
"will_be_marked": "sera marqué comme visité une fois l’aventure sauvegardée.",
|
||||
|
@ -232,22 +232,34 @@
|
|||
"attachments": "Pièces jointes",
|
||||
"gpx_tip": "Téléchargez des fichiers GPX en pièces jointes pour les afficher sur la carte !",
|
||||
"images": "Images",
|
||||
"primary": "Primaire",
|
||||
"primary": "Principale",
|
||||
"upload": "Télécharger",
|
||||
"view_attachment": "Voir la pièce jointe",
|
||||
"of": "de",
|
||||
"city": "Ville",
|
||||
"delete_lodging": "Supprimer l'hébergement",
|
||||
"display_name": "Nom d'affichage",
|
||||
"location_details": "Détails de l'emplacement",
|
||||
"location_details": "Détails du lieu",
|
||||
"lodging": "Hébergement",
|
||||
"lodging_delete_confirm": "Êtes-vous sûr de vouloir supprimer cet emplacement d'hébergement? \nCette action ne peut pas être annulée.",
|
||||
"lodging_delete_confirm": "Êtes-vous sûr de vouloir supprimer cet hébergement? \nCette action ne peut pas être annulée.",
|
||||
"lodging_information": "Informations sur l'hébergement",
|
||||
"price": "Prix",
|
||||
"region": "Région",
|
||||
"reservation_number": "Numéro de réservation",
|
||||
"welcome_map_info": "Aventures publiques sur ce serveur",
|
||||
"open_in_maps": "Ouvert dans les cartes"
|
||||
"open_in_maps": "Ouvert dans les cartes",
|
||||
"all_day": "Journée complète",
|
||||
"collection_no_start_end_date": "L'ajout d'une date de début et de fin à la collection débloquera les fonctionnalités de planification de l'itinéraire dans la page de collection.",
|
||||
"date_itinerary": "Itinéraire trié par date",
|
||||
"no_ordered_items": "Ajoutez des éléments avec des dates de visite à la collection pour les voir ici.",
|
||||
"ordered_itinerary": "Itinéraire trié par activité",
|
||||
"additional_info": "Informations Complémentaires",
|
||||
"invalid_date_range": "Plage de dates non valide",
|
||||
"sunrise_sunset": "Lever du soleil",
|
||||
"timezone": "Fuseau horaire",
|
||||
"no_visits": "Pas de visites",
|
||||
"arrival_timezone": "Fuseau horaire d'arrivée",
|
||||
"departure_timezone": "Fuseau horaire de départ"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
|
||||
|
@ -267,7 +279,7 @@
|
|||
"about": "À propos de AdventureLog",
|
||||
"adventures": "Aventures",
|
||||
"collections": "Collections",
|
||||
"discord": "Discorde",
|
||||
"discord": "Discord",
|
||||
"documentation": "Documentation",
|
||||
"greeting": "Salut",
|
||||
"logout": "Déconnexion",
|
||||
|
@ -280,12 +292,12 @@
|
|||
"theme_selection": "Sélection de thèmes",
|
||||
"themes": {
|
||||
"forest": "Forêt",
|
||||
"light": "Lumière",
|
||||
"light": "Clair",
|
||||
"night": "Nuit",
|
||||
"aqua": "Aqua",
|
||||
"dark": "Sombre",
|
||||
"aestheticDark": "Esthétique sombre",
|
||||
"aestheticLight": "Lumière esthétique",
|
||||
"aestheticDark": "Esthétique (sombre)",
|
||||
"aestheticLight": "Esthétique (clair)",
|
||||
"northernLights": "Aurores boréales"
|
||||
},
|
||||
"users": "Utilisateurs",
|
||||
|
@ -298,7 +310,7 @@
|
|||
"admin_panel": "Panneau d'administration"
|
||||
},
|
||||
"auth": {
|
||||
"confirm_password": "Confirmez le mot de passe",
|
||||
"confirm_password": "Confirmer le mot de passe",
|
||||
"email": "E-mail",
|
||||
"first_name": "Prénom",
|
||||
"forgot_password": "Mot de passe oublié ?",
|
||||
|
@ -312,18 +324,18 @@
|
|||
"profile_picture": "Photo de profil",
|
||||
"public_profile": "Profil public",
|
||||
"public_tooltip": "Avec un profil public, les utilisateurs peuvent partager des collections avec vous et afficher votre profil sur la page des utilisateurs.",
|
||||
"email_required": "L'e-mail est requis",
|
||||
"email_required": "Le courriel est requis",
|
||||
"both_passwords_required": "Les deux mots de passe sont requis",
|
||||
"new_password": "Nouveau mot de passe",
|
||||
"reset_failed": "Échec de la réinitialisation du mot de passe",
|
||||
"or_3rd_party": "Ou connectez-vous avec un service tiers",
|
||||
"no_public_adventures": "Aucune aventure publique trouvée",
|
||||
"no_public_collections": "Aucune collection publique trouvée",
|
||||
"user_adventures": "Aventures utilisateur",
|
||||
"user_collections": "Collections d'utilisateurs"
|
||||
"user_adventures": "Aventures de l'utilisateur",
|
||||
"user_collections": "Collections de l'utilisateur"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "Aucun utilisateur trouvé avec des profils publics."
|
||||
"no_users_found": "Aucun utilisateur trouvé avec un profil public."
|
||||
},
|
||||
"worldtravel": {
|
||||
"all": "Tous",
|
||||
|
@ -352,27 +364,27 @@
|
|||
"settings": {
|
||||
"account_settings": "Paramètres du compte utilisateur",
|
||||
"confirm_new_password": "Confirmer le nouveau mot de passe",
|
||||
"current_email": "Courriel actuel",
|
||||
"email_change": "Changer l'e-mail",
|
||||
"new_email": "Nouvel e-mail",
|
||||
"current_email": "Adresse de courriel actuel",
|
||||
"email_change": "Changer l'adresse de courriel",
|
||||
"new_email": "Nouvelle adresse de courriel",
|
||||
"new_password": "Nouveau mot de passe",
|
||||
"no_email_set": "Aucune adresse e-mail définie",
|
||||
"no_email_set": "Aucune adresse de courriel définie",
|
||||
"password_change": "Changer le mot de passe",
|
||||
"settings_page": "Page Paramètres",
|
||||
"settings_page": "Page de paramétrage",
|
||||
"update": "Mise à jour",
|
||||
"update_error": "Erreur lors de la mise à jour des paramètres",
|
||||
"update_success": "Paramètres mis à jour avec succès !",
|
||||
"change_password": "Changer le mot de passe",
|
||||
"invalid_token": "Le jeton n'est pas valide ou a expiré",
|
||||
"login_redir": "Vous serez alors redirigé vers la page de connexion.",
|
||||
"missing_email": "Veuillez entrer une adresse e-mail",
|
||||
"missing_email": "Veuillez entrer une adresse de courriel",
|
||||
"password_does_not_match": "Les mots de passe ne correspondent pas",
|
||||
"password_is_required": "Le mot de passe est requis",
|
||||
"possible_reset": "Si l'adresse e-mail que vous avez fournie est associée à un compte, vous recevrez un e-mail avec des instructions pour réinitialiser votre mot de passe !",
|
||||
"possible_reset": "Si l'adresse de courriel que vous avez fournie est associée à un compte, vous recevrez un courriel avec des instructions pour réinitialiser votre mot de passe !",
|
||||
"reset_password": "Réinitialiser le mot de passe",
|
||||
"submit": "Soumettre",
|
||||
"token_required": "Le jeton et l'UID sont requis pour la réinitialisation du mot de passe.",
|
||||
"about_this_background": "À propos de ce contexte",
|
||||
"submit": "Valider",
|
||||
"token_required": "Le jeton et l'identifiant utilisateur sont requis pour la réinitialisation du mot de passe.",
|
||||
"about_this_background": "À propos de cette photo",
|
||||
"join_discord": "Rejoignez le Discord",
|
||||
"join_discord_desc": "pour partager vos propres photos. \nPostez-les dans le",
|
||||
"photo_by": "Photo par",
|
||||
|
@ -380,77 +392,77 @@
|
|||
"current_password": "Mot de passe actuel",
|
||||
"password_change_lopout_warning": "Vous serez déconnecté après avoir modifié votre mot de passe.",
|
||||
"authenticator_code": "Code d'authentification",
|
||||
"copy": "Copie",
|
||||
"disable_mfa": "Désactiver MFA",
|
||||
"email_added": "E-mail ajouté avec succès !",
|
||||
"email_added_error": "Erreur lors de l'ajout de l'e-mail",
|
||||
"email_removed": "E-mail supprimé avec succès !",
|
||||
"email_removed_error": "Erreur lors de la suppression de l'e-mail",
|
||||
"email_set_primary": "E-mail défini comme principal avec succès !",
|
||||
"email_set_primary_error": "Erreur lors de la définition de l'adresse e-mail comme adresse principale",
|
||||
"email_verified": "E-mail vérifié avec succès !",
|
||||
"email_verified_erorr_desc": "Votre email n'a pas pu être vérifié. \nVeuillez réessayer.",
|
||||
"email_verified_error": "Erreur lors de la vérification de l'e-mail",
|
||||
"email_verified_success": "Votre email a été vérifié. \nVous pouvez maintenant vous connecter.",
|
||||
"enable_mfa": "Activer l'authentification multifacteur",
|
||||
"copy": "Copier",
|
||||
"disable_mfa": "Désactiver l'authentification multi-facteurs",
|
||||
"email_added": "Adresse de courriel ajoutée avec succès !",
|
||||
"email_added_error": "Erreur lors de l'ajout de l'adresse de courriel",
|
||||
"email_removed": "Adresse de courriel supprimée avec succès !",
|
||||
"email_removed_error": "Erreur lors de la suppression de l'adresse de courriel",
|
||||
"email_set_primary": "Adresse de courriel principale définie avec succès !",
|
||||
"email_set_primary_error": "Erreur lors de la définition de l'adresse de courriel principale",
|
||||
"email_verified": "Adresse de courriel vérifiée avec succès !",
|
||||
"email_verified_erorr_desc": "Votre adresse de courriel n'a pas pu être vérifiée. \nVeuillez réessayer.",
|
||||
"email_verified_error": "Erreur lors de la vérification de l'adresse de courriel",
|
||||
"email_verified_success": "Votre adresse de courriel a été vérifiée. \nVous pouvez maintenant vous connecter.",
|
||||
"enable_mfa": "Activer l'authentification multi-facteurs",
|
||||
"error_change_password": "Erreur lors du changement de mot de passe. \nVeuillez vérifier votre mot de passe actuel et réessayer.",
|
||||
"generic_error": "Une erreur s'est produite lors du traitement de votre demande.",
|
||||
"invalid_code": "Code MFA invalide",
|
||||
"invalid_code": "Code d'authentification multi-facteurs invalide",
|
||||
"invalid_credentials": "Nom d'utilisateur ou mot de passe invalide",
|
||||
"make_primary": "Rendre primaire",
|
||||
"mfa_disabled": "Authentification multifacteur désactivée avec succès !",
|
||||
"mfa_enabled": "Authentification multifacteur activée avec succès !",
|
||||
"mfa_not_enabled": "MFA n'est pas activé",
|
||||
"mfa_page_title": "Authentification multifacteur",
|
||||
"mfa_required": "Une authentification multifacteur est requise",
|
||||
"no_emai_set": "Aucune adresse e-mail définie",
|
||||
"not_verified": "Non vérifié",
|
||||
"primary": "Primaire",
|
||||
"make_primary": "Définir comme adresse de courriel principale",
|
||||
"mfa_disabled": "Authentification multi-facteurs désactivée avec succès !",
|
||||
"mfa_enabled": "Authentification multi-facteurs activée avec succès !",
|
||||
"mfa_not_enabled": "L'authentification multi-facteurs n'est pas activée",
|
||||
"mfa_page_title": "Authentification multi-facteurs",
|
||||
"mfa_required": "Une authentification multi-facteurs est requise",
|
||||
"no_emai_set": "Aucune adresse de courriel définie",
|
||||
"not_verified": "Non vérifiée",
|
||||
"primary": "Principale",
|
||||
"recovery_codes": "Codes de récupération",
|
||||
"recovery_codes_desc": "Ce sont vos codes de récupération. \nGardez-les en sécurité. \nVous ne pourrez plus les revoir.",
|
||||
"recovery_codes_desc": "Ce sont vos codes de récupération. \nGardez-les en sécurité. \nIls ne pourront plus vous être affichés.",
|
||||
"reset_session_error": "Veuillez vous déconnecter, puis vous reconnecter pour actualiser votre session et réessayer.",
|
||||
"verified": "Vérifié",
|
||||
"verified": "Vérifiée",
|
||||
"verify": "Vérifier",
|
||||
"verify_email_error": "Erreur lors de la vérification de l'e-mail. \nRéessayez dans quelques minutes.",
|
||||
"verify_email_success": "Vérification par e-mail envoyée avec succès !",
|
||||
"add_email_blocked": "Vous ne pouvez pas ajouter une adresse e-mail à un compte protégé par une authentification à deux facteurs.",
|
||||
"verify_email_error": "Erreur lors de la vérification de l'adresse de courriel. \nRéessayez dans quelques minutes.",
|
||||
"verify_email_success": "Vérification par courriel envoyée avec succès !",
|
||||
"add_email_blocked": "Vous ne pouvez pas ajouter une adresse de courriel à un compte protégé par une authentification à deux facteurs.",
|
||||
"required": "Ce champ est obligatoire",
|
||||
"csrf_failed": "Échec de la récupération du jeton CSRF",
|
||||
"duplicate_email": "Cette adresse e-mail est déjà utilisée.",
|
||||
"email_taken": "Cette adresse e-mail est déjà utilisée.",
|
||||
"duplicate_email": "Cette adresse de courriel est déjà utilisée.",
|
||||
"email_taken": "Cette adresse de courriel est déjà utilisée.",
|
||||
"username_taken": "Ce nom d'utilisateur est déjà utilisé.",
|
||||
"administration_settings": "Paramètres d'administration",
|
||||
"documentation_link": "Lien vers la documentation",
|
||||
"launch_account_connections": "Lancer les connexions au compte",
|
||||
"launch_administration_panel": "Lancer le panneau d'administration",
|
||||
"no_verified_email_warning": "Vous devez disposer d'une adresse e-mail vérifiée pour activer l'authentification à deux facteurs.",
|
||||
"social_auth_desc": "Activez ou désactivez les fournisseurs d'authentification sociale et OIDC pour votre compte. \nCes connexions vous permettent de vous connecter avec des fournisseurs d'identité d'authentification auto-hébergés comme Authentik ou des fournisseurs tiers comme GitHub.",
|
||||
"no_verified_email_warning": "Vous devez disposer d'une adresse de courriel vérifiée pour activer l'authentification multi-facteurs.",
|
||||
"social_auth_desc": "Activez ou désactivez les fournisseurs d'authentification sociale et OIDC pour votre compte. \nCes connexions vous permettent de vous connecter avec des fournisseurs d'identité auto-hébergés comme Authentik ou des fournisseurs tiers comme GitHub.",
|
||||
"social_auth_desc_2": "Ces paramètres sont gérés sur le serveur AdventureLog et doivent être activés manuellement par l'administrateur.",
|
||||
"social_oidc_auth": "Authentification sociale et OIDC",
|
||||
"add_email": "Ajouter un e-mail",
|
||||
"add_email": "Ajouter une adresse de courriel",
|
||||
"password_too_short": "Le mot de passe doit contenir au moins 6 caractères",
|
||||
"disable_password": "Désactiver le mot de passe",
|
||||
"password_disable": "Désactiver l'authentification du mot de passe",
|
||||
"password_disable_desc": "La désactivation de l'authentification du mot de passe vous empêchera de vous connecter avec un mot de passe. \nVous devrez utiliser un fournisseur social ou OIDC pour vous connecter. Si votre fournisseur social est non lié, l'authentification du mot de passe sera automatiquement réactivé même si ce paramètre est désactivé.",
|
||||
"password_disable_warning": "Actuellement, l'authentification du mot de passe est désactivée. \nLa connexion via un fournisseur social ou OIDC est requise.",
|
||||
"password_disabled": "Authentification du mot de passe désactivé",
|
||||
"password_disabled_error": "Erreur de désactivation de l'authentification du mot de passe. \nAssurez-vous qu'un fournisseur social ou OIDC est lié à votre compte.",
|
||||
"password_enabled": "Authentification du mot de passe activé",
|
||||
"password_enabled_error": "Erreur permettant l'authentification du mot de passe."
|
||||
"password_disable": "Désactiver l'authentification par mot de passe",
|
||||
"password_disable_desc": "La désactivation de l'authentification par mot de passe vous empêchera de vous connecter avec un mot de passe. \nVous devrez utiliser un fournisseur social ou OIDC pour vous connecter. Si votre fournisseur social est non lié, l'authentification par mot de passe sera automatiquement réactivée même si ce paramètre est désactivé.",
|
||||
"password_disable_warning": "Actuellement, l'authentification par mot de passe est désactivée. \nLa connexion via un fournisseur social ou OIDC est requise.",
|
||||
"password_disabled": "Authentification par mot de passe désactivée",
|
||||
"password_disabled_error": "Erreur de désactivation de l'authentification par mot de passe. \nAssurez-vous qu'un fournisseur social ou OIDC est lié à votre compte.",
|
||||
"password_enabled": "Authentification par mot de passe activée",
|
||||
"password_enabled_error": "Erreur permettant l'authentification par mot de passe."
|
||||
},
|
||||
"checklist": {
|
||||
"add_item": "Ajouter un article",
|
||||
"add_item": "Ajouter un élément",
|
||||
"checklist_delete_error": "Erreur lors de la suppression de la liste de contrôle",
|
||||
"checklist_deleted": "Liste de contrôle supprimée avec succès !",
|
||||
"checklist_editor": "Éditeur de liste de contrôle",
|
||||
"checklist_public": "Cette liste de contrôle est publique car elle fait partie d’une collection publique.",
|
||||
"editing_checklist": "Liste de contrôle d'édition",
|
||||
"editing_checklist": "Édition de la liste de contrôle",
|
||||
"failed_to_save": "Échec de l'enregistrement de la liste de contrôle",
|
||||
"item": "Article",
|
||||
"item_already_exists": "L'article existe déjà",
|
||||
"item": "Élément",
|
||||
"item_already_exists": "L'élément existe déjà",
|
||||
"item_cannot_be_empty": "L'élément ne peut pas être vide",
|
||||
"items": "Articles",
|
||||
"new_item": "Nouvel article",
|
||||
"items": "Éléments",
|
||||
"new_item": "Nouvel élément",
|
||||
"save": "Sauvegarder",
|
||||
"checklist_viewer": "Visionneuse de liste de contrôle",
|
||||
"new_checklist": "Nouvelle liste de contrôle"
|
||||
|
@ -468,7 +480,7 @@
|
|||
"notes": {
|
||||
"add_a_link": "Ajouter un lien",
|
||||
"content": "Contenu",
|
||||
"editing_note": "Note d'édition",
|
||||
"editing_note": "Modification de la note",
|
||||
"failed_to_save": "Échec de l'enregistrement de la note",
|
||||
"note_delete_error": "Erreur lors de la suppression de la note",
|
||||
"note_deleted": "Note supprimée avec succès !",
|
||||
|
@ -480,13 +492,13 @@
|
|||
"note_viewer": "Visionneuse de notes"
|
||||
},
|
||||
"transportation": {
|
||||
"date_time": "Date de début",
|
||||
"date_time": "Date de départ",
|
||||
"edit": "Modifier",
|
||||
"edit_transportation": "Modifier le transport",
|
||||
"end_date_time": "Date de fin",
|
||||
"error_editing_transportation": "Erreur lors de la modification du transport",
|
||||
"edit_transportation": "Modifier le déplacement",
|
||||
"end_date_time": "Date d'arrivée",
|
||||
"error_editing_transportation": "Erreur lors de la modification du déplacement",
|
||||
"flight_number": "Numéro du vol",
|
||||
"from_location": "De l'emplacement",
|
||||
"from_location": "Du lieu",
|
||||
"modes": {
|
||||
"bike": "Vélo",
|
||||
"boat": "Bateau",
|
||||
|
@ -494,33 +506,33 @@
|
|||
"car": "Voiture",
|
||||
"other": "Autre",
|
||||
"plane": "Avion",
|
||||
"train": "Former",
|
||||
"train": "Train",
|
||||
"walking": "Marche"
|
||||
},
|
||||
"new_transportation": "Nouveau transport",
|
||||
"provide_start_date": "Veuillez fournir une date de début",
|
||||
"new_transportation": "Nouveau déplacement",
|
||||
"provide_start_date": "Veuillez fournir une date de départ",
|
||||
"start": "Commencer",
|
||||
"to_location": "Vers l'emplacement",
|
||||
"to_location": "Vers le lieu",
|
||||
"transport_type": "Type de transport",
|
||||
"type": "Taper",
|
||||
"date_and_time": "Date",
|
||||
"transportation_added": "Transport ajouté avec succès !",
|
||||
"transportation_delete_error": "Erreur lors de la suppression du transport",
|
||||
"transportation_deleted": "Transport supprimé avec succès !",
|
||||
"transportation_edit_success": "Transport modifié avec succès !",
|
||||
"ending_airport_desc": "Entrez la fin du code aéroportuaire (par exemple, laxiste)",
|
||||
"fetch_location_information": "Récupérer les informations de localisation",
|
||||
"starting_airport_desc": "Entrez le code aéroport de démarrage (par exemple, JFK)"
|
||||
"type": "Type",
|
||||
"date_and_time": "Date et heure",
|
||||
"transportation_added": "Déplacement ajouté avec succès !",
|
||||
"transportation_delete_error": "Erreur lors de la suppression du déplacement",
|
||||
"transportation_deleted": "Déplacement supprimé avec succès !",
|
||||
"transportation_edit_success": "Déplacement modifié avec succès !",
|
||||
"ending_airport_desc": "Entrez le code de l'aéroport de départ (par exemple, CDG)",
|
||||
"fetch_location_information": "Récupérer les informations sur les lieux",
|
||||
"starting_airport_desc": "Entrez le code de l'aéroport d'arrivée (par exemple, ORY)"
|
||||
},
|
||||
"search": {
|
||||
"adventurelog_results": "Résultats du journal d'aventure",
|
||||
"adventurelog_results": "Résultats dans AdventureLog",
|
||||
"online_results": "Résultats en ligne",
|
||||
"public_adventures": "Aventures publiques"
|
||||
},
|
||||
"map": {
|
||||
"add_adventure": "Ajouter une nouvelle aventure",
|
||||
"add_adventure_at_marker": "Ajouter une nouvelle aventure au marqueur",
|
||||
"adventure_map": "Carte d'aventure",
|
||||
"adventure_map": "Carte des aventures",
|
||||
"clear_marker": "Effacer le marqueur",
|
||||
"map_options": "Options de la carte",
|
||||
"show_visited_regions": "Afficher les régions visitées",
|
||||
|
@ -528,20 +540,20 @@
|
|||
},
|
||||
"languages": {},
|
||||
"share": {
|
||||
"no_users_shared": "Aucun utilisateur partagé avec",
|
||||
"not_shared_with": "Non partagé avec",
|
||||
"share_desc": "Partagez cette collection avec d'autres utilisateurs.",
|
||||
"shared": "Commun",
|
||||
"shared_with": "Partagé avec",
|
||||
"unshared": "Non partagé",
|
||||
"no_users_shared": "Aucun utilisateur",
|
||||
"not_shared_with": "Pas encore partagé avec",
|
||||
"share_desc": "Partager cette collection avec d'autres utilisateurs.",
|
||||
"shared": "Partagé",
|
||||
"shared_with": "Déjà partagé avec",
|
||||
"unshared": "Partage désactivé pour",
|
||||
"with": "avec",
|
||||
"go_to_settings": "Allez dans les paramètres",
|
||||
"no_shared_found": "Aucune collection trouvée partagée avec vous.",
|
||||
"set_public": "Afin de permettre aux utilisateurs de partager avec vous, vous devez définir votre profil comme public."
|
||||
"no_shared_found": "Aucune collection ne semble encore avoir été partagée avec vous.",
|
||||
"set_public": "Afin de permettre aux utilisateurs de partager avec vous, vous devez rendre votre profil public."
|
||||
},
|
||||
"profile": {
|
||||
"member_since": "Membre depuis",
|
||||
"user_stats": "Statistiques des utilisateurs",
|
||||
"user_stats": "Statistiques de l'utilisateur",
|
||||
"visited_countries": "Pays visités",
|
||||
"visited_regions": "Régions visitées",
|
||||
"visited_cities": "Villes visitées"
|
||||
|
@ -558,7 +570,7 @@
|
|||
"dashboard": {
|
||||
"add_some": "Pourquoi ne pas commencer à planifier votre prochaine aventure ? \nVous pouvez ajouter une nouvelle aventure en cliquant sur le bouton ci-dessous.",
|
||||
"countries_visited": "Pays visités",
|
||||
"no_recent_adventures": "Pas d'aventures récentes ?",
|
||||
"no_recent_adventures": "Pas d'aventure récente ?",
|
||||
"recent_adventures": "Aventures récentes",
|
||||
"total_adventures": "Aventures totales",
|
||||
"total_visited_regions": "Total des régions visitées",
|
||||
|
@ -566,8 +578,8 @@
|
|||
"total_visited_cities": "Total des villes visitées"
|
||||
},
|
||||
"immich": {
|
||||
"api_key": "Clé API Immich",
|
||||
"api_note": "Remarque : il doit s'agir de l'URL du serveur API Immich, elle se termine donc probablement par /api, sauf si vous disposez d'une configuration personnalisée.",
|
||||
"api_key": "Clé d'API Immich",
|
||||
"api_note": "Remarque : il doit s'agir de l'URL de base de l'API Immich, elle se termine donc généralement par /api, sauf si vous disposez d'une configuration personnalisée.",
|
||||
"disable": "Désactiver",
|
||||
"enable_immich": "Activer Immich",
|
||||
"imageid_required": "L'identifiant de l'image est requis",
|
||||
|
@ -587,7 +599,7 @@
|
|||
"server_down": "Le serveur Immich est actuellement en panne ou inaccessible",
|
||||
"server_url": "URL du serveur Immich",
|
||||
"update_integration": "Intégration des mises à jour",
|
||||
"documentation": "Documentation d'intégration Immich",
|
||||
"documentation": "Documentation de l'intégration Immich",
|
||||
"localhost_note": "Remarque : localhost ne fonctionnera probablement pas à moins que vous n'ayez configuré les réseaux Docker en conséquence. \nIl est recommandé d'utiliser l'adresse IP du serveur ou le nom de domaine."
|
||||
},
|
||||
"recomendations": {
|
||||
|
@ -599,31 +611,31 @@
|
|||
},
|
||||
"lodging": {
|
||||
"apartment": "Appartement",
|
||||
"bnb": "Bed and petit-déjeuner",
|
||||
"cabin": "Cabine",
|
||||
"bnb": "Bed and Breakfast",
|
||||
"cabin": "Châlet",
|
||||
"campground": "Camping",
|
||||
"check_in": "Enregistrement",
|
||||
"check_out": "Vérifier",
|
||||
"date_and_time": "Date",
|
||||
"check_out": "Checkout",
|
||||
"date_and_time": "Date et heure",
|
||||
"edit": "Modifier",
|
||||
"edit_lodging": "Modifier l'hébergement",
|
||||
"error_editing_lodging": "Édition d'erreurs Hébergement",
|
||||
"error_editing_lodging": "Erreur lors de la modification de l'hébergement",
|
||||
"hostel": "Auberge",
|
||||
"hotel": "Hôtel",
|
||||
"house": "Maison",
|
||||
"lodging_added": "L'hébergement a ajouté avec succès!",
|
||||
"lodging_added": "Hébergement ajouté avec succès!",
|
||||
"lodging_delete_error": "Erreur de suppression de l'hébergement",
|
||||
"lodging_deleted": "L'hébergement est supprimé avec succès!",
|
||||
"lodging_edit_success": "L'hébergement édité avec succès!",
|
||||
"lodging_deleted": "Hébergement supprimé avec succès!",
|
||||
"lodging_edit_success": "Hébergement modifié avec succès!",
|
||||
"lodging_type": "Type d'hébergement",
|
||||
"motel": "Motel",
|
||||
"new_lodging": "Nouveau logement",
|
||||
"new_lodging": "Nouvel hébergement",
|
||||
"other": "Autre",
|
||||
"provide_start_date": "Veuillez fournir une date de début",
|
||||
"reservation_number": "Numéro de réservation",
|
||||
"resort": "Station balnéaire",
|
||||
"resort": "Complexe touristique",
|
||||
"start": "Commencer",
|
||||
"type": "Taper",
|
||||
"type": "Type",
|
||||
"villa": "Villa",
|
||||
"current_timezone": "Fuseau horaire actuel"
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"about": {
|
||||
"about": "Di",
|
||||
"close": "Vicino",
|
||||
"close": "Chiudi",
|
||||
"license": "Concesso in licenza con la licenza GPL-3.0.",
|
||||
"message": "Realizzato con ❤️ negli Stati Uniti.",
|
||||
"nominatim_1": "La ricerca della posizione e la geocodifica sono fornite da",
|
||||
|
@ -13,10 +13,10 @@
|
|||
"adventures": {
|
||||
"activities": {
|
||||
"activity": "Attività 🏄",
|
||||
"art_museums": "Arte",
|
||||
"art_museums": "Arte e Musei",
|
||||
"attraction": "Attrazione 🎢",
|
||||
"culture": "Cultura 🎭",
|
||||
"dining": "Pranzo 🍽️",
|
||||
"dining": "Mangiare 🍽️",
|
||||
"event": "Evento 🎉",
|
||||
"festivals": "Festival 🎪",
|
||||
"fitness": "Forma fisica 🏋️",
|
||||
|
@ -35,34 +35,34 @@
|
|||
"water_sports": "Sport acquatici 🚤",
|
||||
"wildlife": "Fauna selvatica 🦒"
|
||||
},
|
||||
"add_to_collection": "Aggiungi alla raccolta",
|
||||
"add_to_collection": "Aggiungi alla collezione",
|
||||
"adventure": "Avventura",
|
||||
"adventure_delete_confirm": "Sei sicuro di voler eliminare questa avventura? \nQuesta azione non può essere annullata.",
|
||||
"adventure_details": "Dettagli dell'avventura",
|
||||
"adventure_type": "Tipo di avventura",
|
||||
"archive": "Archivio",
|
||||
"archived": "Archiviato",
|
||||
"archived_collection_message": "Raccolta archiviata con successo!",
|
||||
"archived_collection_message": "Collezione archiviata con successo!",
|
||||
"archived_collections": "Collezioni archiviate",
|
||||
"ascending": "Ascendente",
|
||||
"cancel": "Cancellare",
|
||||
"category_filter": "Filtro categoria",
|
||||
"clear": "Chiaro",
|
||||
"clear": "Rimuovere",
|
||||
"close_filters": "Chiudi filtri",
|
||||
"collection": "Collezione",
|
||||
"collection_link_error": "Errore nel collegamento dell'avventura alla raccolta",
|
||||
"collection_remove_error": "Errore durante la rimozione dell'avventura dalla raccolta",
|
||||
"collection_remove_success": "Avventura rimossa con successo dalla raccolta!",
|
||||
"collection_link_error": "Errore nel collegamento dell'avventura alla collezione",
|
||||
"collection_remove_error": "Errore durante la rimozione dell'avventura dalla collezione",
|
||||
"collection_remove_success": "Avventura rimossa con successo dalla collezione!",
|
||||
"count_txt": "risultati corrispondenti alla tua ricerca",
|
||||
"create_new": "Crea nuovo...",
|
||||
"date": "Data",
|
||||
"delete": "Eliminare",
|
||||
"delete_collection": "Elimina raccolta",
|
||||
"delete_collection_success": "Raccolta eliminata con successo!",
|
||||
"delete_collection_warning": "Sei sicuro di voler eliminare questa raccolta? \nCiò eliminerà anche tutte le avventure collegate. \nQuesta azione non può essere annullata.",
|
||||
"delete_collection": "Elimina collezione",
|
||||
"delete_collection_success": "Collezione eliminata con successo!",
|
||||
"delete_collection_warning": "Sei sicuro di voler eliminare questa collezione? \nCiò eliminerà anche tutte le avventure collegate. \nQuesta azione non può essere annullata.",
|
||||
"descending": "Discendente",
|
||||
"edit_adventure": "Modifica Avventura",
|
||||
"edit_collection": "Modifica raccolta",
|
||||
"edit_collection": "Modifica collezione",
|
||||
"filter": "Filtro",
|
||||
"homepage": "Home page",
|
||||
"latitude": "Latitudine",
|
||||
|
@ -79,18 +79,18 @@
|
|||
"private": "Privato",
|
||||
"public": "Pubblico",
|
||||
"rating": "Valutazione",
|
||||
"remove_from_collection": "Rimuovi dalla raccolta",
|
||||
"remove_from_collection": "Rimuovi dalla collezione",
|
||||
"share": "Condividere",
|
||||
"sort": "Ordinare",
|
||||
"sources": "Fonti",
|
||||
"unarchive": "Annulla l'archiviazione",
|
||||
"unarchived_collection_message": "Raccolta annullata con successo!",
|
||||
"unarchived_collection_message": "Collezione disarchiviata con successo!",
|
||||
"updated": "Aggiornato",
|
||||
"visit": "Visita",
|
||||
"visits": "Visite",
|
||||
"adventure_delete_success": "Avventura eliminata con successo!",
|
||||
"collection_adventures": "Includi avventure di raccolta",
|
||||
"collection_link_success": "Avventura collegata alla raccolta con successo!",
|
||||
"collection_adventures": "Includi avventure dalle raccolte",
|
||||
"collection_link_success": "Avventura collegata alla collezione con successo!",
|
||||
"dates": "Date",
|
||||
"delete_adventure": "Elimina avventura",
|
||||
"duration": "Durata",
|
||||
|
@ -114,9 +114,9 @@
|
|||
"adventure_updated": "Avventura aggiornata",
|
||||
"basic_information": "Informazioni di base",
|
||||
"category": "Categoria",
|
||||
"clear_map": "Mappa chiara",
|
||||
"clear_map": "Libera mappa",
|
||||
"copy_link": "Copia collegamento",
|
||||
"date_constrain": "Vincolare alle date di raccolta",
|
||||
"date_constrain": "Vincolare alle date di collezione",
|
||||
"description": "Descrizione",
|
||||
"end_date": "Data di fine",
|
||||
"fetch_image": "Recupera immagine",
|
||||
|
@ -140,7 +140,7 @@
|
|||
"search_for_location": "Cerca una posizione",
|
||||
"search_results": "Risultati della ricerca",
|
||||
"see_adventures": "Vedi Avventure",
|
||||
"select_adventure_category": "Seleziona la categoria Avventura",
|
||||
"select_adventure_category": "Seleziona la categoria per l'avventura",
|
||||
"share_adventure": "Condividi questa avventura!",
|
||||
"start_date": "Data di inizio",
|
||||
"upload_image": "Carica immagine",
|
||||
|
@ -154,11 +154,11 @@
|
|||
"all": "Tutto",
|
||||
"error_updating_regions": "Errore durante l'aggiornamento delle regioni",
|
||||
"mark_region_as_visited": "Contrassegnare la regione {regione}, {paese} come visitata?",
|
||||
"mark_visited": "Marco ha visitato",
|
||||
"mark_visited": "Segna come visitato",
|
||||
"my_adventures": "Le mie avventure",
|
||||
"no_adventures_found": "Nessuna avventura trovata",
|
||||
"no_collections_found": "Nessuna raccolta trovata a cui aggiungere questa avventura.",
|
||||
"no_linkable_adventures": "Non è stata trovata alcuna avventura che possa essere collegata a questa raccolta.",
|
||||
"no_collections_found": "Nessuna collezione trovata a cui aggiungere questa avventura.",
|
||||
"no_linkable_adventures": "Non è stata trovata alcuna avventura che possa essere collegata a questa collezione.",
|
||||
"not_visited": "Non visitato",
|
||||
"regions_updated": "regioni aggiornate",
|
||||
"update_visited_regions": "Aggiorna le regioni visitate",
|
||||
|
@ -168,20 +168,20 @@
|
|||
"add_new": "Aggiungi nuovo...",
|
||||
"checklist": "Lista di controllo",
|
||||
"checklists": "Liste di controllo",
|
||||
"collection_archived": "Questa raccolta è stata archiviata.",
|
||||
"collection_completed": "Hai completato questa raccolta!",
|
||||
"collection_stats": "Statistiche della raccolta",
|
||||
"collection_archived": "Questa collezione è stata archiviata.",
|
||||
"collection_completed": "Hai completato questa collezione!",
|
||||
"collection_stats": "Statistiche della collezione",
|
||||
"days": "giorni",
|
||||
"itineary_by_date": "Itinerario per data",
|
||||
"keep_exploring": "Continua a esplorare!",
|
||||
"link_new": "Collegamento Nuovo...",
|
||||
"link_new": "Collega Nuovo...",
|
||||
"linked_adventures": "Avventure collegate",
|
||||
"links": "Collegamenti",
|
||||
"no_end_date": "Inserisci una data di fine",
|
||||
"note": "Nota",
|
||||
"notes": "Note",
|
||||
"nothing_planned": "Niente in programma per questa giornata. \nBuon viaggio!",
|
||||
"transportation": "Trasporti",
|
||||
"transportation": "Trasporto",
|
||||
"transportations": "Trasporti",
|
||||
"visit_link": "Visita il collegamento",
|
||||
"day": "Giorno",
|
||||
|
@ -194,9 +194,9 @@
|
|||
"adventure_calendar": "Calendario delle avventure",
|
||||
"emoji_picker": "Selettore di emoji",
|
||||
"hide": "Nascondere",
|
||||
"show": "Spettacolo",
|
||||
"show": "Mostrare",
|
||||
"download_calendar": "Scarica Calendario",
|
||||
"md_instructions": "Scrivi qui il tuo ribasso...",
|
||||
"md_instructions": "Scrivi qui in markdown...",
|
||||
"preview": "Anteprima",
|
||||
"checklist_delete_confirm": "Sei sicuro di voler eliminare questa lista di controllo? \nQuesta azione non può essere annullata.",
|
||||
"clear_location": "Cancella posizione",
|
||||
|
@ -205,7 +205,7 @@
|
|||
"delete_note": "Elimina nota",
|
||||
"delete_transportation": "Elimina trasporto",
|
||||
"end": "FINE",
|
||||
"ending_airport": "Fine dell'aeroporto",
|
||||
"ending_airport": "Aeroporto di arrivo",
|
||||
"flight_information": "Informazioni sul volo",
|
||||
"from": "Da",
|
||||
"no_location_found": "Nessuna posizione trovata",
|
||||
|
@ -213,7 +213,7 @@
|
|||
"out_of_range": "Non nell'intervallo di date dell'itinerario",
|
||||
"show_region_labels": "Mostra etichette regione",
|
||||
"start": "Inizio",
|
||||
"starting_airport": "Inizio aeroporto",
|
||||
"starting_airport": "Aeroporto di partenza",
|
||||
"to": "A",
|
||||
"transportation_delete_confirm": "Sei sicuro di voler eliminare questo trasporto? \nQuesta azione non può essere annullata.",
|
||||
"show_map": "Mostra mappa",
|
||||
|
@ -221,7 +221,7 @@
|
|||
"cities_updated": "città aggiornate",
|
||||
"create_adventure": "Crea Avventura",
|
||||
"no_adventures_to_recommendations": "Nessuna avventura trovata. \nAggiungi almeno un'avventura per ricevere consigli.",
|
||||
"finding_recommendations": "Alla scoperta di gemme nascoste per la tua prossima avventura",
|
||||
"finding_recommendations": "Alla scoperta di tesori nascosti per la tua prossima avventura",
|
||||
"attachment": "Allegato",
|
||||
"attachment_delete_success": "Allegato eliminato con successo!",
|
||||
"attachment_name": "Nome dell'allegato",
|
||||
|
@ -247,7 +247,19 @@
|
|||
"region": "Regione",
|
||||
"welcome_map_info": "Avventure pubbliche su questo server",
|
||||
"reservation_number": "Numero di prenotazione",
|
||||
"open_in_maps": "Aperto in mappe"
|
||||
"open_in_maps": "Aprire in Mappe",
|
||||
"all_day": "Tutto il giorno",
|
||||
"collection_no_start_end_date": "L'aggiunta di una data di inizio e fine alla collezione sbloccherà le funzionalità di pianificazione dell'itinerario nella pagina della collezione.",
|
||||
"date_itinerary": "Data dell'itinerario",
|
||||
"no_ordered_items": "Aggiungi elementi con date alla collezione per vederli qui.",
|
||||
"ordered_itinerary": "Itinerario ordinato",
|
||||
"additional_info": "Ulteriori informazioni",
|
||||
"invalid_date_range": "Intervallo di date non valido",
|
||||
"sunrise_sunset": "Alba",
|
||||
"timezone": "Fuso orario",
|
||||
"no_visits": "Nessuna visita",
|
||||
"arrival_timezone": "Fuso orario di arrivo",
|
||||
"departure_timezone": "Fuso orario di partenza"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Scopri, pianifica ed esplora con facilità",
|
||||
|
@ -267,9 +279,9 @@
|
|||
"about": "Informazioni su AdventureLog",
|
||||
"adventures": "Avventure",
|
||||
"collections": "Collezioni",
|
||||
"discord": "Discordia",
|
||||
"discord": "Discord",
|
||||
"documentation": "Documentazione",
|
||||
"greeting": "CIAO",
|
||||
"greeting": "Ciao",
|
||||
"logout": "Esci",
|
||||
"map": "Mappa",
|
||||
"my_adventures": "Le mie avventure",
|
||||
|
@ -290,7 +302,7 @@
|
|||
},
|
||||
"users": "Utenti",
|
||||
"worldtravel": "Viaggio nel mondo",
|
||||
"my_tags": "I miei tag",
|
||||
"my_tags": "Le mie tag",
|
||||
"tag": "Etichetta",
|
||||
"language_selection": "Lingua",
|
||||
"support": "Supporto",
|
||||
|
@ -314,7 +326,7 @@
|
|||
"public_tooltip": "Con un profilo pubblico, gli utenti possono condividere raccolte con te e visualizzare il tuo profilo nella pagina degli utenti.",
|
||||
"email_required": "L'e-mail è obbligatoria",
|
||||
"both_passwords_required": "Sono necessarie entrambe le password",
|
||||
"new_password": "Nuova parola d'ordine",
|
||||
"new_password": "Nuova password",
|
||||
"reset_failed": "Impossibile reimpostare la password",
|
||||
"or_3rd_party": "Oppure accedi con un servizio di terze parti",
|
||||
"no_public_adventures": "Nessuna avventura pubblica trovata",
|
||||
|
@ -344,7 +356,7 @@
|
|||
"region_failed_visited": "Impossibile contrassegnare la regione come visitata",
|
||||
"region_stats": "Statistiche della regione",
|
||||
"regions_in": "Regioni dentro",
|
||||
"removed": "RIMOSSO",
|
||||
"removed": "Rimosso",
|
||||
"view_cities": "Visualizza città",
|
||||
"visit_remove_failed": "Impossibile rimuovere la visita",
|
||||
"visit_to": "Visita a"
|
||||
|
@ -355,7 +367,7 @@
|
|||
"current_email": "E-mail corrente",
|
||||
"email_change": "Cambia e-mail",
|
||||
"new_email": "Nuova e-mail",
|
||||
"new_password": "Nuova parola d'ordine",
|
||||
"new_password": "Nuova password",
|
||||
"no_email_set": "Nessuna e-mail impostata",
|
||||
"password_change": "Cambiare la password",
|
||||
"settings_page": "Pagina Impostazioni",
|
||||
|
@ -373,7 +385,7 @@
|
|||
"submit": "Invia",
|
||||
"token_required": "Token e UID sono necessari per la reimpostazione della password.",
|
||||
"about_this_background": "A proposito di questo contesto",
|
||||
"join_discord": "Unisciti alla Discordia",
|
||||
"join_discord": "Unisciti a Discord",
|
||||
"join_discord_desc": "per condividere le tue foto. \nPubblicateli in",
|
||||
"photo_by": "Foto di",
|
||||
"change_password_error": "Impossibile modificare la password. \nPassword attuale non valida o nuova password non valida.",
|
||||
|
@ -407,7 +419,7 @@
|
|||
"not_verified": "Non verificato",
|
||||
"primary": "Primario",
|
||||
"recovery_codes": "Codici di ripristino",
|
||||
"recovery_codes_desc": "Questi sono i tuoi codici di ripristino. \nTeneteli al sicuro. \nNon potrai vederli più.",
|
||||
"recovery_codes_desc": "Questi sono i tuoi codici di ripristino. \nTienili al sicuro. \nNon potrai vederli più.",
|
||||
"reset_session_error": "Esci, effettua nuovamente l'accesso per aggiornare la sessione e riprova.",
|
||||
"verified": "Verificato",
|
||||
"verify_email_success": "Verifica email inviata con successo!",
|
||||
|
@ -426,7 +438,7 @@
|
|||
"no_verified_email_warning": "È necessario disporre di un indirizzo e-mail verificato per abilitare l'autenticazione a due fattori.",
|
||||
"social_auth_desc": "Abilita o disabilita i provider di autenticazione social e OIDC per il tuo account. \nQueste connessioni ti consentono di accedere con provider di identità di autenticazione self-hosted come Authentik o provider di terze parti come GitHub.",
|
||||
"social_auth_desc_2": "Queste impostazioni sono gestite nel server AdventureLog e devono essere abilitate manualmente dall'amministratore.",
|
||||
"social_oidc_auth": "Autenticazione sociale e OIDC",
|
||||
"social_oidc_auth": "Autenticazione social e OIDC",
|
||||
"add_email": "Aggiungi e-mail",
|
||||
"password_too_short": "La password deve contenere almeno 6 caratteri",
|
||||
"disable_password": "Disabilita la password",
|
||||
|
@ -439,40 +451,40 @@
|
|||
"password_enabled_error": "Errore che abilita l'autenticazione della password."
|
||||
},
|
||||
"checklist": {
|
||||
"add_item": "Aggiungi articolo",
|
||||
"add_item": "Aggiungi elemento",
|
||||
"checklist_delete_error": "Errore durante l'eliminazione della lista di controllo",
|
||||
"checklist_deleted": "Lista di controllo eliminata con successo!",
|
||||
"checklist_editor": "Redattore della lista di controllo",
|
||||
"checklist_public": "Questa lista di controllo è pubblica perché è in una raccolta pubblica.",
|
||||
"checklist_public": "Questa lista di controllo è pubblica perché è in una collezione pubblica.",
|
||||
"editing_checklist": "Lista di controllo per la modifica",
|
||||
"failed_to_save": "Impossibile salvare la lista di controllo",
|
||||
"item": "Articolo",
|
||||
"item_already_exists": "L'articolo esiste già",
|
||||
"item_cannot_be_empty": "L'articolo non può essere vuoto",
|
||||
"item": "Elemento",
|
||||
"item_already_exists": "L'elemento esiste già",
|
||||
"item_cannot_be_empty": "L'elemento non può essere vuoto",
|
||||
"items": "Elementi",
|
||||
"save": "Salva",
|
||||
"new_item": "Nuovo articolo",
|
||||
"new_item": "Nuovo elemento",
|
||||
"checklist_viewer": "Visualizzatore della lista di controllo",
|
||||
"new_checklist": "Nuova lista di controllo"
|
||||
},
|
||||
"collection": {
|
||||
"edit_collection": "Modifica raccolta",
|
||||
"error_creating_collection": "Errore durante la creazione della raccolta",
|
||||
"error_editing_collection": "Errore durante la modifica della raccolta",
|
||||
"edit_collection": "Modifica collezione",
|
||||
"error_creating_collection": "Errore durante la creazione della collezione",
|
||||
"error_editing_collection": "Errore durante la modifica della collezione",
|
||||
"new_collection": "Nuova collezione",
|
||||
"collection_created": "Collezione creata con successo!",
|
||||
"collection_edit_success": "Raccolta modificata con successo!",
|
||||
"collection_edit_success": "Collezione modificata con successo!",
|
||||
"create": "Creare",
|
||||
"public_collection": "Collezione pubblica"
|
||||
},
|
||||
"notes": {
|
||||
"add_a_link": "Aggiungi un collegamento",
|
||||
"content": "Contenuto",
|
||||
"editing_note": "Nota di modifica",
|
||||
"editing_note": "Editor di modifica nota",
|
||||
"failed_to_save": "Impossibile salvare la nota",
|
||||
"note_delete_error": "Errore durante l'eliminazione della nota",
|
||||
"note_deleted": "Nota eliminata con successo!",
|
||||
"note_editor": "Redattore della nota",
|
||||
"note_editor": "Editor della nota",
|
||||
"note_public": "Questa nota è pubblica perché è in una collezione pubblica.",
|
||||
"open": "Aprire",
|
||||
"save": "Salva",
|
||||
|
@ -508,7 +520,7 @@
|
|||
"transportation_deleted": "Trasporto eliminato con successo!",
|
||||
"transportation_edit_success": "Trasporti modificati con successo!",
|
||||
"type": "Tipo",
|
||||
"ending_airport_desc": "Immettere il codice aeroportuale finale (ad es. LAX)",
|
||||
"ending_airport_desc": "Immettere il codice dell'aroporto di arrivo (ad es. LAX)",
|
||||
"fetch_location_information": "Informazioni sulla posizione di recupero",
|
||||
"starting_airport_desc": "Immettere il codice dell'aeroporto di partenza (ad es. JFK)"
|
||||
},
|
||||
|
@ -530,17 +542,17 @@
|
|||
"share": {
|
||||
"no_users_shared": "Nessun utente condiviso con",
|
||||
"not_shared_with": "Non condiviso con",
|
||||
"share_desc": "Condividi questa raccolta con altri utenti.",
|
||||
"share_desc": "Condividi questa collezione con altri utenti.",
|
||||
"shared": "Condiviso",
|
||||
"shared_with": "Condiviso con",
|
||||
"unshared": "Non condiviso",
|
||||
"with": "con",
|
||||
"go_to_settings": "Vai alle impostazioni",
|
||||
"no_shared_found": "Nessuna raccolta trovata condivisa con te.",
|
||||
"no_shared_found": "Nessuna collezione trovata condivisa con te.",
|
||||
"set_public": "Per consentire agli utenti di condividere con te, è necessario che il tuo profilo sia impostato su pubblico."
|
||||
},
|
||||
"profile": {
|
||||
"member_since": "Membro da allora",
|
||||
"member_since": "Membro da",
|
||||
"user_stats": "Statistiche utente",
|
||||
"visited_countries": "Paesi visitati",
|
||||
"visited_regions": "Regioni visitate",
|
||||
|
@ -602,12 +614,12 @@
|
|||
"bnb": "Bed and Breakfast",
|
||||
"cabin": "Cabina",
|
||||
"campground": "Campeggio",
|
||||
"check_in": "Check -in",
|
||||
"check_out": "Guardare",
|
||||
"date_and_time": "Data",
|
||||
"check_in": "Check-in",
|
||||
"check_out": "Check-out",
|
||||
"date_and_time": "Data e ora",
|
||||
"edit": "Modificare",
|
||||
"edit_lodging": "Modifica alloggio",
|
||||
"error_editing_lodging": "Alloggio di modifica degli errori",
|
||||
"error_editing_lodging": "Errore nella modifica dell'alloggio",
|
||||
"hostel": "Ostello",
|
||||
"hotel": "Hotel",
|
||||
"house": "Casa",
|
||||
|
@ -616,11 +628,11 @@
|
|||
"other": "Altro",
|
||||
"provide_start_date": "Si prega di fornire una data di inizio",
|
||||
"reservation_number": "Numero di prenotazione",
|
||||
"resort": "Ricorrere",
|
||||
"resort": "Resort",
|
||||
"start": "Inizio",
|
||||
"type": "Tipo",
|
||||
"villa": "Villa",
|
||||
"lodging_delete_error": "Errore di eliminazione dell'alloggio",
|
||||
"lodging_delete_error": "Errore nell'eliminazione dell'alloggio",
|
||||
"lodging_deleted": "Alloggio eliminato con successo!",
|
||||
"lodging_edit_success": "Alloggio modificato con successo!",
|
||||
"lodging_type": "Tipo di alloggio",
|
||||
|
|
|
@ -247,7 +247,19 @@
|
|||
"region": "지역",
|
||||
"reservation_number": "예약 번호",
|
||||
"welcome_map_info": "이 서버의 공개 모험",
|
||||
"open_in_maps": "지도에서 열립니다"
|
||||
"open_in_maps": "지도에서 열립니다",
|
||||
"all_day": "하루 종일",
|
||||
"collection_no_start_end_date": "컬렉션에 시작 및 종료 날짜를 추가하면 컬렉션 페이지에서 여정 계획 기능이 잠금 해제됩니다.",
|
||||
"date_itinerary": "날짜 일정",
|
||||
"no_ordered_items": "컬렉션에 날짜가있는 항목을 추가하여 여기에서 확인하십시오.",
|
||||
"ordered_itinerary": "주문한 여정",
|
||||
"additional_info": "추가 정보",
|
||||
"invalid_date_range": "잘못된 날짜 범위",
|
||||
"sunrise_sunset": "해돋이",
|
||||
"timezone": "시간대",
|
||||
"no_visits": "방문 없음",
|
||||
"arrival_timezone": "도착 시간대",
|
||||
"departure_timezone": "출발 시간대"
|
||||
},
|
||||
"auth": {
|
||||
"both_passwords_required": "두 암호 모두 필요합니다",
|
||||
|
|
|
@ -247,7 +247,19 @@
|
|||
"lodging_information": "Informatie overliggen",
|
||||
"price": "Prijs",
|
||||
"region": "Regio",
|
||||
"open_in_maps": "Open in kaarten"
|
||||
"open_in_maps": "Open in kaarten",
|
||||
"all_day": "De hele dag",
|
||||
"collection_no_start_end_date": "Als u een start- en einddatum aan de collectie toevoegt, ontgrendelt u de functies van de planning van de route ontgrendelen in de verzamelpagina.",
|
||||
"date_itinerary": "Datumroute",
|
||||
"no_ordered_items": "Voeg items toe met datums aan de collectie om ze hier te zien.",
|
||||
"ordered_itinerary": "Besteld reisschema",
|
||||
"additional_info": "Aanvullende informatie",
|
||||
"invalid_date_range": "Ongeldige datumbereik",
|
||||
"sunrise_sunset": "Zonsopgang",
|
||||
"timezone": "Tijdzone",
|
||||
"no_visits": "Geen bezoeken",
|
||||
"arrival_timezone": "Aankomsttijdzone",
|
||||
"departure_timezone": "Vertrektijdzone"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Ontdek, plan en verken met gemak",
|
||||
|
|
642
frontend/src/locales/no.json
Normal file
642
frontend/src/locales/no.json
Normal file
|
@ -0,0 +1,642 @@
|
|||
{
|
||||
"navbar": {
|
||||
"adventures": "Eventyr",
|
||||
"collections": "Samlinger",
|
||||
"worldtravel": "Verdensreiser",
|
||||
"map": "Kart",
|
||||
"users": "Brukere",
|
||||
"search": "Søk",
|
||||
"profile": "Profil",
|
||||
"greeting": "Hei",
|
||||
"my_adventures": "Mine Eventyr",
|
||||
"my_tags": "Mine Tags",
|
||||
"tag": "Tag",
|
||||
"shared_with_me": "Delt med meg",
|
||||
"settings": "Innstillinger",
|
||||
"logout": "Logg ut",
|
||||
"about": "Om AdventureLog",
|
||||
"documentation": "Dokumentasjon",
|
||||
"discord": "Discord",
|
||||
"language_selection": "Språk",
|
||||
"support": "Støtte",
|
||||
"calendar": "Kalender",
|
||||
"theme_selection": "Tema-valg",
|
||||
"admin_panel": "Admin Panel",
|
||||
"themes": {
|
||||
"light": "Lyst",
|
||||
"dark": "Mørkt",
|
||||
"night": "Natt",
|
||||
"forest": "Skog",
|
||||
"aestheticLight": "Estetisk Lyst",
|
||||
"aestheticDark": "Estetisk Mørkt",
|
||||
"aqua": "Aqua",
|
||||
"northernLights": "Nordlys"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"about": "Om",
|
||||
"license": "Lisensiert under GPL-3.0-lisensen.",
|
||||
"source_code": "Kildekode",
|
||||
"message": "Laget med ❤️ i USA.",
|
||||
"oss_attributions": "Open Source-attribusjoner",
|
||||
"nominatim_1": "Stedsøk og geokoding leveres av",
|
||||
"nominatim_2": "Deres data er lisensiert under ODbL-lisensen.",
|
||||
"other_attributions": "Ytterligere attribusjoner finnes i README-filen.",
|
||||
"close": "Lukk"
|
||||
},
|
||||
"home": {
|
||||
"hero_1": "Oppdag verdens mest spennende eventyr",
|
||||
"hero_2": "Oppdag og planlegg ditt neste eventyr med AdventureLog. Utforsk fantastiske destinasjoner, lag tilpassede reiseplaner, og hold kontakten på farten.",
|
||||
"go_to": "Gå til AdventureLog",
|
||||
"key_features": "Nøkkelfunksjoner",
|
||||
"desc_1": "Oppdag, planlegg og utforsk med letthet",
|
||||
"desc_2": "AdventureLog er designet for å forenkle reisen din, og gir deg verktøy og ressurser til å planlegge, pakke og navigere ditt neste uforglemmelige eventyr.",
|
||||
"feature_1": "Reiselogg",
|
||||
"feature_1_desc": "Før en personlig reiselogg for eventyrene dine og del opplevelsene dine med venner og familie.",
|
||||
"feature_2": "Reiseplanlegging",
|
||||
"feature_2_desc": "Lag enkelt tilpassede reiseplaner og få en dag-for-dag oversikt over turen din.",
|
||||
"feature_3": "Reisekart",
|
||||
"feature_3_desc": "Se reisene dine over hele verden med et interaktivt kart og utforsk nye destinasjoner."
|
||||
},
|
||||
"adventures": {
|
||||
"collection_remove_success": "Eventyret ble fjernet fra samlingen!",
|
||||
"collection_remove_error": "Feil ved fjerning av eventyr fra samling",
|
||||
"collection_link_success": "Eventyret ble lagt til samlingen!",
|
||||
"no_image_found": "Ingen bilde funnet",
|
||||
"collection_link_error": "Feil ved lenking av eventyr til samling",
|
||||
"adventure_delete_confirm": "Er du sikker på at du vil slette dette eventyret? Denne handlingen kan ikke angres.",
|
||||
"checklist_delete_confirm": "Er du sikker på at du vil slette denne sjekklisten? Denne handlingen kan ikke angres.",
|
||||
"note_delete_confirm": "Er du sikker på at du vil slette dette notatet? Denne handlingen kan ikke angres.",
|
||||
"transportation_delete_confirm": "Er du sikker på at du vil slette dette transportmiddelet? Denne handlingen kan ikke angres.",
|
||||
"lodging_delete_confirm": "Er du sikker på at du vil slette dette overnattingsstedet? Denne handlingen kan ikke angres.",
|
||||
"delete_checklist": "Slett sjekkliste",
|
||||
"delete_note": "Slett notat",
|
||||
"delete_transportation": "Slett transport",
|
||||
"delete_lodging": "Slett overnatting",
|
||||
"open_details": "Åpne detaljer",
|
||||
"edit_adventure": "Rediger eventyr",
|
||||
"remove_from_collection": "Fjern fra samling",
|
||||
"add_to_collection": "Legg til i samling",
|
||||
"delete": "Slett",
|
||||
"not_found": "Fant ikke eventyret",
|
||||
"not_found_desc": "Eventyret du leter etter, ble ikke funnet. Vennligst prøv et annet eventyr eller kom tilbake senere.",
|
||||
"homepage": "Hjemmeside",
|
||||
"adventure_details": "Eventyrdetaljer",
|
||||
"collection": "Samling",
|
||||
"adventure_type": "Eventyrtype",
|
||||
"longitude": "Lengdegrad",
|
||||
"latitude": "Breddegrad",
|
||||
"visit": "Besøk",
|
||||
"visits": "Besøk",
|
||||
"create_new": "Opprett nytt...",
|
||||
"adventure": "Eventyr",
|
||||
"count_txt": "resultater som samsvarer med søket ditt",
|
||||
"sort": "Sorter",
|
||||
"order_by": "Sorter etter",
|
||||
"order_direction": "Sorteringsretning",
|
||||
"ascending": "Stigende",
|
||||
"descending": "Synkende",
|
||||
"updated": "Oppdatert",
|
||||
"name": "Navn",
|
||||
"date": "Dato",
|
||||
"activity_types": "Aktivitetstyper",
|
||||
"tags": "Tags",
|
||||
"add_a_tag": "Legg til en tag",
|
||||
"date_constrain": "Begrens til samlingsdatoer",
|
||||
"rating": "Vurdering",
|
||||
"my_images": "Mine bilder",
|
||||
"add_an_activity": "Legg til en aktivitet",
|
||||
"show_region_labels": "Vis regionetiketter",
|
||||
"no_images": "Ingen bilder",
|
||||
"upload_images_here": "Last opp bilder her",
|
||||
"share_adventure": "Del dette eventyret!",
|
||||
"copy_link": "Kopier lenke",
|
||||
"image": "Bilde",
|
||||
"upload_image": "Last opp bilde",
|
||||
"open_in_maps": "Åpne i kart",
|
||||
"url": "URL",
|
||||
"fetch_image": "Hent bilde",
|
||||
"wikipedia": "Wikipedia",
|
||||
"add_notes": "Legg til notater",
|
||||
"warning": "Advarsel",
|
||||
"my_adventures": "Mine eventyr",
|
||||
"no_linkable_adventures": "Ingen eventyr funnet som kan legges til denne samlingen.",
|
||||
"add": "Legg til",
|
||||
"save_next": "Lagre og fortsett",
|
||||
"end_date": "Sluttdato",
|
||||
"my_visits": "Mine besøk",
|
||||
"start_date": "Startdato",
|
||||
"remove": "Fjern",
|
||||
"location": "Plassering",
|
||||
"search_for_location": "Søk etter sted",
|
||||
"clear_map": "Tøm kart",
|
||||
"search_results": "Søkeresultater",
|
||||
"no_results": "Ingen resultater funnet",
|
||||
"wiki_desc": "Henter utdrag fra Wikipedia-artikkelen som samsvarer med navnet på eventyret.",
|
||||
"attachments": "Vedlegg",
|
||||
"attachment": "Vedlegg",
|
||||
"images": "Bilder",
|
||||
"primary": "Primær",
|
||||
"view_attachment": "Vis vedlegg",
|
||||
"generate_desc": "Generer beskrivelse",
|
||||
"public_adventure": "Offentlig eventyr",
|
||||
"location_information": "Plasseringsinformasjon",
|
||||
"link": "Lenke",
|
||||
"links": "Lenker",
|
||||
"description": "Beskrivelse",
|
||||
"sources": "Kilder",
|
||||
"collection_adventures": "Inkluder eventyr i samlinger",
|
||||
"filter": "Filter",
|
||||
"category_filter": "Kategorifilter",
|
||||
"category": "Kategori",
|
||||
"select_adventure_category": "Velg eventyrkategori",
|
||||
"clear": "Tøm",
|
||||
"my_collections": "Mine samlinger",
|
||||
"open_filters": "Åpne filtre",
|
||||
"close_filters": "Lukk filtre",
|
||||
"archived_collections": "Arkiverte samlinger",
|
||||
"share": "Del",
|
||||
"private": "Privat",
|
||||
"public": "Offentlig",
|
||||
"archived": "Arkivert",
|
||||
"edit_collection": "Rediger samling",
|
||||
"unarchive": "Fjern fra arkiv",
|
||||
"archive": "Arkiver",
|
||||
"no_collections_found": "Ingen samlinger funnet for å legge dette eventyret til.",
|
||||
"not_visited": "Ikke besøkt",
|
||||
"archived_collection_message": "Samlingen ble arkivert!",
|
||||
"unarchived_collection_message": "Samlingen ble fjernet fra arkivet!",
|
||||
"delete_collection_success": "Samlingen ble slettet!",
|
||||
"delete_collection_warning": "Er du sikker på at du vil slette denne samlingen? Dette vil også slette alle lenkede eventyr. Denne handlingen kan ikke angres.",
|
||||
"cancel": "Avbryt",
|
||||
"of": "av",
|
||||
"delete_collection": "Slett samling",
|
||||
"delete_adventure": "Slett eventyr",
|
||||
"adventure_delete_success": "Eventyret ble slettet!",
|
||||
"visited": "Besøkt",
|
||||
"planned": "Planlagt",
|
||||
"duration": "Varighet",
|
||||
"all": "Alle",
|
||||
"image_removed_success": "Bilde ble fjernet!",
|
||||
"image_removed_error": "Feil ved fjerning av bilde",
|
||||
"no_image_url": "Finner ikke bilde på den oppgitte URL-en.",
|
||||
"image_upload_success": "Bilde opplastet!",
|
||||
"image_upload_error": "Feil ved opplasting av bilde",
|
||||
"dates": "Datoer",
|
||||
"wiki_image_error": "Feil ved henting av bilde fra Wikipedia",
|
||||
"start_before_end_error": "Startdato må være før sluttdato",
|
||||
"activity": "Aktivitet",
|
||||
"actions": "Handlinger",
|
||||
"no_end_date": "Vennligst angi en sluttdato",
|
||||
"see_adventures": "Se eventyr",
|
||||
"image_fetch_failed": "Kunne ikke hente bilde",
|
||||
"no_location": "Vennligst angi et sted",
|
||||
"no_start_date": "Vennligst angi en startdato",
|
||||
"no_description_found": "Fant ingen beskrivelse",
|
||||
"adventure_created": "Eventyr opprettet",
|
||||
"adventure_create_error": "Kunne ikke opprette eventyr",
|
||||
"lodging": "Overnatting",
|
||||
"create_adventure": "Opprett eventyr",
|
||||
"adventure_updated": "Eventyr oppdatert",
|
||||
"adventure_update_error": "Kunne ikke oppdatere eventyr",
|
||||
"set_to_pin": "Fest",
|
||||
"category_fetch_error": "Feil ved henting av kategorier",
|
||||
"new_adventure": "Nytt eventyr",
|
||||
"basic_information": "Grunnleggende informasjon",
|
||||
"no_adventures_to_recommendations": "Ingen eventyr funnet. Legg til minst ett eventyr for å få anbefalinger.",
|
||||
"display_name": "Visningsnavn",
|
||||
"adventure_not_found": "Det finnes ingen eventyr å vise. Legg til noen ved å trykke på plusstegnet nederst til høyre, eller prøv å endre filtre!",
|
||||
"no_adventures_found": "Ingen eventyr funnet",
|
||||
"mark_region_as_visited": "Merk regionen {region}, {country} som besøkt?",
|
||||
"mark_visited": "Merk som besøkt",
|
||||
"error_updating_regions": "Feil ved oppdatering av regioner",
|
||||
"regions_updated": "regioner oppdatert",
|
||||
"cities_updated": "byer oppdatert",
|
||||
"visited_region_check": "Sjekk besøkte regioner",
|
||||
"visited_region_check_desc": "Ved å markere denne, vil serveren sjekke alle dine besøkte eventyr og markere regionene de befinner seg i som besøkt i verdensreiser.",
|
||||
"update_visited_regions": "Oppdater besøkte regioner",
|
||||
"update_visited_regions_disclaimer": "Dette kan ta litt tid avhengig av hvor mange eventyr du har besøkt.",
|
||||
"link_new": "Lenk ny...",
|
||||
"add_new": "Legg til ny...",
|
||||
"transportation": "Transport",
|
||||
"note": "Notat",
|
||||
"checklist": "Sjekkliste",
|
||||
"collection_archived": "Denne samlingen er arkivert.",
|
||||
"visit_link": "Besøk lenke",
|
||||
"collection_completed": "Du har fullført denne samlingen!",
|
||||
"collection_stats": "Samlingsstatistikk",
|
||||
"keep_exploring": "Fortsett å utforske!",
|
||||
"linked_adventures": "Lenkede eventyr",
|
||||
"notes": "Notater",
|
||||
"checklists": "Sjekklister",
|
||||
"transportations": "Transportmidler",
|
||||
"adventure_calendar": "Eventyrkalender",
|
||||
"day": "Dag",
|
||||
"itineary_by_date": "Reiseplan etter dato",
|
||||
"nothing_planned": "Ingenting planlagt denne dagen. Nyt reisen!",
|
||||
"copied_to_clipboard": "Kopiert til utklippstavlen!",
|
||||
"copy_failed": "Kopiering mislyktes",
|
||||
"show": "Vis",
|
||||
"hide": "Skjul",
|
||||
"clear_location": "Fjern sted",
|
||||
"starting_airport": "Avreiseflyplass",
|
||||
"ending_airport": "Ankomsflyplass",
|
||||
"no_location_found": "Ingen sted funnet",
|
||||
"from": "Fra",
|
||||
"to": "Til",
|
||||
"will_be_marked": "vil bli markert som besøkt når eventyret er lagret.",
|
||||
"start": "Start",
|
||||
"end": "Slutt",
|
||||
"show_map": "Vis kart",
|
||||
"emoji_picker": "Emoji-velger",
|
||||
"download_calendar": "Last ned kalender",
|
||||
"date_information": "Dato-informasjon",
|
||||
"flight_information": "Flyinformasjon",
|
||||
"out_of_range": "Ikke i reiseplandatoer",
|
||||
"preview": "Forhåndsvisning",
|
||||
"finding_recommendations": "Oppdager skjulte perler for ditt neste eventyr",
|
||||
"location_details": "Stedsdetaljer",
|
||||
"city": "By",
|
||||
"region": "Region",
|
||||
"md_instructions": "Skriv markdown her...",
|
||||
"days": "dager",
|
||||
"attachment_upload_success": "Vedlegg lastet opp!",
|
||||
"attachment_upload_error": "Feil ved opplasting av vedlegg",
|
||||
"upload": "Last opp",
|
||||
"attachment_delete_success": "Vedlegg slettet!",
|
||||
"attachment_update_success": "Vedlegg oppdatert!",
|
||||
"attachment_name": "Vedleggsnavn",
|
||||
"gpx_tip": "Last opp GPX-filer i vedlegg for å se dem på kartet!",
|
||||
"welcome_map_info": "Offentlige eventyr på denne serveren",
|
||||
"attachment_update_error": "Feil ved oppdatering av vedlegg",
|
||||
"activities": {
|
||||
"general": "Generelt 🌍",
|
||||
"outdoor": "Utendørs 🏞️",
|
||||
"lodging": "Overnatting 🛌",
|
||||
"dining": "Servering 🍽️",
|
||||
"activity": "Aktivitet 🏄",
|
||||
"attraction": "Attraksjon 🎢",
|
||||
"shopping": "Shopping 🛍️",
|
||||
"nightlife": "Uteliv 🌃",
|
||||
"event": "Arrangement 🎉",
|
||||
"transportation": "Transport 🚗",
|
||||
"culture": "Kultur 🎭",
|
||||
"water_sports": "Vannsport 🚤",
|
||||
"hiking": "Fotturer 🥾",
|
||||
"wildlife": "Dyreliv 🦒",
|
||||
"historical_sites": "Historiske steder 🏛️",
|
||||
"music_concerts": "Musikk og konserter 🎶",
|
||||
"fitness": "Trening 🏋️",
|
||||
"art_museums": "Kunst og museer 🎨",
|
||||
"festivals": "Festivaler 🎪",
|
||||
"spiritual_journeys": "Spirituelle reiser 🧘♀️",
|
||||
"volunteer_work": "Frivillig arbeid 🤝",
|
||||
"other": "Annet"
|
||||
},
|
||||
"lodging_information": "Overnattingsinformasjon",
|
||||
"price": "Pris",
|
||||
"reservation_number": "Reservasjonsnummer",
|
||||
"additional_info": "Ytterligere informasjon",
|
||||
"all_day": "Hele dagen",
|
||||
"collection_no_start_end_date": "Å legge til en start- og sluttdato til samlingen vil låse opp reiseruteplanleggingsfunksjoner på innsamlingssiden.",
|
||||
"date_itinerary": "Dato reiserute",
|
||||
"invalid_date_range": "Ugyldig datoområde",
|
||||
"no_ordered_items": "Legg til varer med datoer i samlingen for å se dem her.",
|
||||
"ordered_itinerary": "Bestilt reiserute",
|
||||
"sunrise_sunset": "Soloppgang",
|
||||
"timezone": "Tidssone",
|
||||
"no_visits": "Ingen besøk",
|
||||
"arrival_timezone": "Ankomst tidssone",
|
||||
"departure_timezone": "Avgangstidssone"
|
||||
},
|
||||
"worldtravel": {
|
||||
"country_list": "Liste over land",
|
||||
"num_countries": "land funnet",
|
||||
"all": "Alle",
|
||||
"partially_visited": "Delvis besøkt",
|
||||
"not_visited": "Ikke besøkt",
|
||||
"completely_visited": "Fullstendig besøkt",
|
||||
"all_subregions": "Alle underregioner",
|
||||
"clear_search": "Tøm søk",
|
||||
"no_countries_found": "Ingen land funnet",
|
||||
"view_cities": "Vis byer",
|
||||
"no_cities_found": "Ingen byer funnet",
|
||||
"visit_to": "Besøk i",
|
||||
"region_failed_visited": "Kunne ikke markere region som besøkt",
|
||||
"failed_to_mark_visit": "Kunne ikke markere besøk i",
|
||||
"visit_remove_failed": "Kunne ikke fjerne besøk",
|
||||
"removed": "fjernet",
|
||||
"failed_to_remove_visit": "Kunne ikke fjerne besøk i",
|
||||
"marked_visited": "markert som besøkt",
|
||||
"regions_in": "Regioner i",
|
||||
"region_stats": "Regionstatistikk",
|
||||
"all_visited": "Du har besøkt alle regionene i",
|
||||
"cities": "byer"
|
||||
},
|
||||
"auth": {
|
||||
"username": "Brukernavn",
|
||||
"password": "Passord",
|
||||
"forgot_password": "Glemt passord?",
|
||||
"signup": "Registrer deg",
|
||||
"login_error": "Kan ikke logge inn med oppgitte legitimasjon.",
|
||||
"login": "Logg inn",
|
||||
"email": "E-post",
|
||||
"first_name": "Fornavn",
|
||||
"last_name": "Etternavn",
|
||||
"confirm_password": "Bekreft passord",
|
||||
"registration_disabled": "Registrering er for øyeblikket deaktivert.",
|
||||
"profile_picture": "Profilbilde",
|
||||
"public_profile": "Offentlig profil",
|
||||
"public_tooltip": "Med en offentlig profil kan brukere dele samlinger med deg og se profilen din på brukersiden.",
|
||||
"email_required": "E-post kreves",
|
||||
"new_password": "Nytt passord (6+ tegn)",
|
||||
"both_passwords_required": "Begge passord er påkrevd",
|
||||
"reset_failed": "Kunne ikke tilbakestille passord",
|
||||
"or_3rd_party": "Eller logg inn med en tredjepartstjeneste",
|
||||
"no_public_adventures": "Ingen offentlige eventyr funnet",
|
||||
"no_public_collections": "Ingen offentlige samlinger funnet",
|
||||
"user_adventures": "Brukerens eventyr",
|
||||
"user_collections": "Brukerens samlinger"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "Ingen brukere med offentlig profil funnet."
|
||||
},
|
||||
"settings": {
|
||||
"update_error": "Feil ved oppdatering av innstillinger",
|
||||
"update_success": "Innstillinger oppdatert!",
|
||||
"settings_page": "Innstillingsside",
|
||||
"account_settings": "Brukerkontoinnstillinger",
|
||||
"update": "Oppdater",
|
||||
"no_verified_email_warning": "Du må ha en verifisert e-postadresse for å aktivere tofaktorautentisering.",
|
||||
"password_change": "Bytt passord",
|
||||
"new_password": "Nytt passord",
|
||||
"confirm_new_password": "Bekreft nytt passord",
|
||||
"email_change": "Bytt e-post",
|
||||
"current_email": "Nåværende e-post",
|
||||
"no_email_set": "Ingen e-post angitt",
|
||||
"new_email": "Ny e-post",
|
||||
"change_password": "Bytt passord",
|
||||
"login_redir": "Du blir da omdirigert til innloggingssiden.",
|
||||
"token_required": "Token og UID kreves for tilbakestilling av passord.",
|
||||
"reset_password": "Tilbakestill passord",
|
||||
"possible_reset": "Hvis e-postadressen du oppga er knyttet til en konto, vil du motta en e-post med instruksjoner om å tilbakestille passordet ditt!",
|
||||
"missing_email": "Vennligst skriv inn en e-postadresse",
|
||||
"submit": "Send inn",
|
||||
"password_does_not_match": "Passordene samsvarer ikke",
|
||||
"password_is_required": "Passord er påkrevd",
|
||||
"invalid_token": "Token er ugyldig eller utløpt",
|
||||
"about_this_background": "Om denne bakgrunnen",
|
||||
"photo_by": "Foto av",
|
||||
"join_discord": "Bli med på Discord",
|
||||
"join_discord_desc": "for å dele dine egne bilder. Legg dem ut i #travel-share-kanalen.",
|
||||
"current_password": "Nåværende passord",
|
||||
"change_password_error": "Kan ikke endre passord. Ugyldig nåværende passord eller ugyldig nytt passord.",
|
||||
"password_change_lopout_warning": "Du vil bli logget ut etter å ha endret passordet.",
|
||||
"generic_error": "En feil oppsto under behandlingen av forespørselen din.",
|
||||
"email_removed": "E-post fjernet!",
|
||||
"email_removed_error": "Feil ved fjerning av e-post",
|
||||
"verify_email_success": "E-postbekreftelse sendt!",
|
||||
"verify_email_error": "Feil ved e-postbekreftelse. Prøv igjen om noen minutter.",
|
||||
"email_added": "E-post lagt til!",
|
||||
"email_added_error": "Feil ved legging til e-post",
|
||||
"email_set_primary": "E-post satt som primær!",
|
||||
"email_set_primary_error": "Feil ved innstilling av primær e-post",
|
||||
"verified": "Verifisert",
|
||||
"primary": "Primær",
|
||||
"not_verified": "Ikke verifisert",
|
||||
"make_primary": "Gjør til primær",
|
||||
"verify": "Verifiser",
|
||||
"no_emai_set": "Ingen e-post angitt",
|
||||
"error_change_password": "Feil ved endring av passord. Sjekk ditt nåværende passord og prøv igjen.",
|
||||
"mfa_disabled": "Tofaktorautentisering er deaktivert!",
|
||||
"mfa_page_title": "Tofaktorautentisering",
|
||||
"enable_mfa": "Aktiver MFA",
|
||||
"disable_mfa": "Deaktiver MFA",
|
||||
"mfa_not_enabled": "MFA er ikke aktivert",
|
||||
"mfa_enabled": "Tofaktorautentisering er aktivert!",
|
||||
"copy": "Kopier",
|
||||
"recovery_codes": "Gjenopprettingskoder",
|
||||
"recovery_codes_desc": "Dette er dine gjenopprettingskoder. Oppbevar dem trygt. Du vil ikke kunne se dem igjen.",
|
||||
"reset_session_error": "Logg ut og logg inn igjen for å oppdatere økten din, og prøv igjen.",
|
||||
"authenticator_code": "Autentiseringskode",
|
||||
"email_verified": "E-post verifisert!",
|
||||
"email_verified_success": "E-posten din er verifisert. Du kan nå logge inn.",
|
||||
"email_verified_error": "Feil ved verifisering av e-post",
|
||||
"email_verified_erorr_desc": "E-posten din kunne ikke verifiseres. Vennligst prøv igjen.",
|
||||
"invalid_code": "Ugyldig MFA-kode",
|
||||
"invalid_credentials": "Ugyldig brukernavn eller passord",
|
||||
"mfa_required": "Tofaktorautentisering er påkrevd",
|
||||
"required": "Dette feltet er påkrevd",
|
||||
"add_email_blocked": "Du kan ikke legge til en e-postadresse på en konto som er beskyttet av tofaktorautentisering.",
|
||||
"duplicate_email": "Denne e-postadressen er allerede i bruk.",
|
||||
"csrf_failed": "Kunne ikke hente CSRF-token",
|
||||
"email_taken": "Denne e-postadressen er allerede i bruk.",
|
||||
"username_taken": "Dette brukernavnet er allerede i bruk.",
|
||||
"administration_settings": "Administrasjonsinnstillinger",
|
||||
"launch_administration_panel": "Åpne administrasjonspanelet",
|
||||
"social_oidc_auth": "Social og OIDC-autentisering",
|
||||
"social_auth_desc": "Aktiver eller deaktiver sosiale og OIDC-autentiseringsleverandører for kontoen din. Disse koblingene lar deg logge inn med selvhostede autentiseringstjenester som Authentik eller tredjepartsleverandører som GitHub.",
|
||||
"social_auth_desc_2": "Disse innstillingene administreres på AdventureLog-serveren og må aktiveres manuelt av administratoren.",
|
||||
"documentation_link": "Dokumentasjonslenke",
|
||||
"launch_account_connections": "Åpne kontotilkoblinger",
|
||||
"password_too_short": "Passordet må være minst 6 tegn",
|
||||
"add_email": "Legg til e-post",
|
||||
"password_disable": "Deaktiver passordautentisering",
|
||||
"password_disable_desc": "Å deaktivere passordautentisering vil hindre deg fra å logge inn med et passord. Du må bruke en sosial eller OIDC-leverandør for å logge inn. Skulle leverandøren din fjernes, vil passordautentisering automatisk bli gjenaktivert, selv om denne innstillingen er deaktivert.",
|
||||
"disable_password": "Deaktiver passord",
|
||||
"password_enabled": "Passordautentisering er aktivert",
|
||||
"password_disabled": "Passordautentisering er deaktivert",
|
||||
"password_disable_warning": "Akkurat nå er passordautentisering deaktivert. Innlogging via en sosial eller OIDC-leverandør er påkrevd.",
|
||||
"password_disabled_error": "Feil ved deaktivering av passordautentisering. Sørg for at en sosial eller OIDC-leverandør er koblet til kontoen din.",
|
||||
"password_enabled_error": "Feil ved aktivering av passordautentisering."
|
||||
},
|
||||
"collection": {
|
||||
"collection_created": "Samling opprettet!",
|
||||
"error_creating_collection": "Feil ved oppretting av samling",
|
||||
"new_collection": "Ny samling",
|
||||
"create": "Opprett",
|
||||
"collection_edit_success": "Samling redigert!",
|
||||
"error_editing_collection": "Feil ved redigering av samling",
|
||||
"edit_collection": "Rediger samling",
|
||||
"public_collection": "Offentlig samling"
|
||||
},
|
||||
"notes": {
|
||||
"note_deleted": "Notat slettet!",
|
||||
"note_delete_error": "Feil ved sletting av notat",
|
||||
"open": "Åpne",
|
||||
"failed_to_save": "Kunne ikke lagre notat",
|
||||
"note_editor": "Notatredigerer",
|
||||
"note_viewer": "Notatviser",
|
||||
"editing_note": "Redigerer notat",
|
||||
"content": "Innhold",
|
||||
"save": "Lagre",
|
||||
"note_public": "Dette notatet er offentlig fordi det er i en offentlig samling.",
|
||||
"add_a_link": "Legg til en lenke",
|
||||
"invalid_url": "Ugyldig URL"
|
||||
},
|
||||
"checklist": {
|
||||
"checklist_deleted": "Sjekkliste slettet!",
|
||||
"checklist_delete_error": "Feil ved sletting av sjekkliste",
|
||||
"failed_to_save": "Kunne ikke lagre sjekkliste",
|
||||
"checklist_editor": "Sjekklisteredigerer",
|
||||
"checklist_viewer": "Sjekklisteviser",
|
||||
"editing_checklist": "Redigerer sjekkliste",
|
||||
"new_checklist": "Ny sjekkliste",
|
||||
"item": "Punkt",
|
||||
"items": "Punkter",
|
||||
"add_item": "Legg til punkt",
|
||||
"new_item": "Nytt punkt",
|
||||
"save": "Lagre",
|
||||
"checklist_public": "Denne sjekklisten er offentlig fordi den er i en offentlig samling.",
|
||||
"item_cannot_be_empty": "Punktet kan ikke være tomt",
|
||||
"item_already_exists": "Punktet finnes allerede"
|
||||
},
|
||||
"transportation": {
|
||||
"transportation_deleted": "Transport slettet!",
|
||||
"transportation_delete_error": "Feil ved sletting av transport",
|
||||
"provide_start_date": "Vennligst angi en startdato",
|
||||
"transport_type": "Transporttype",
|
||||
"type": "Type",
|
||||
"transportation_added": "Transport lagt til!",
|
||||
"error_editing_transportation": "Feil ved redigering av transport",
|
||||
"new_transportation": "Ny transport",
|
||||
"date_time": "Startdato og -tid",
|
||||
"end_date_time": "Sluttdato og -tid",
|
||||
"flight_number": "Flynummer",
|
||||
"from_location": "Fra sted",
|
||||
"to_location": "Til sted",
|
||||
"fetch_location_information": "Hent stedsinformasjon",
|
||||
"starting_airport_desc": "Skriv inn avreiseflyplasskode (f.eks. JFK)",
|
||||
"ending_airport_desc": "Skriv inn ankomsflyplasskode (f.eks. LAX)",
|
||||
"edit": "Rediger",
|
||||
"modes": {
|
||||
"car": "Bil",
|
||||
"plane": "Fly",
|
||||
"train": "Tog",
|
||||
"bus": "Buss",
|
||||
"boat": "Båt",
|
||||
"bike": "Sykkel",
|
||||
"walking": "Går",
|
||||
"other": "Annet"
|
||||
},
|
||||
"transportation_edit_success": "Transport redigert!",
|
||||
"edit_transportation": "Rediger transport",
|
||||
"start": "Start",
|
||||
"date_and_time": "Dato og tid"
|
||||
},
|
||||
"lodging": {
|
||||
"lodging_deleted": "Overnatting slettet!",
|
||||
"lodging_delete_error": "Feil ved sletting av overnatting",
|
||||
"provide_start_date": "Vennligst angi en startdato",
|
||||
"lodging_type": "Overnattingstype",
|
||||
"type": "Type",
|
||||
"lodging_added": "Overnatting lagt til!",
|
||||
"error_editing_lodging": "Feil ved redigering av overnatting",
|
||||
"new_lodging": "Ny overnatting",
|
||||
"check_in": "Innsjekking",
|
||||
"check_out": "Utsjekking",
|
||||
"edit": "Rediger",
|
||||
"lodging_edit_success": "Overnatting redigert!",
|
||||
"edit_lodging": "Rediger overnatting",
|
||||
"start": "Start",
|
||||
"date_and_time": "Dato og tid",
|
||||
"hotel": "Hotell",
|
||||
"hostel": "Hostell",
|
||||
"resort": "Resort",
|
||||
"bnb": "Bed & Breakfast",
|
||||
"campground": "Campingplass",
|
||||
"cabin": "Hytte",
|
||||
"apartment": "Leilighet",
|
||||
"house": "Hus",
|
||||
"villa": "Villa",
|
||||
"motel": "Motell",
|
||||
"other": "Annet",
|
||||
"reservation_number": "Reservasjonsnummer",
|
||||
"current_timezone": "Gjeldende tidssone"
|
||||
},
|
||||
"search": {
|
||||
"adventurelog_results": "AdventureLog-resultater",
|
||||
"public_adventures": "Offentlige eventyr",
|
||||
"online_results": "Nettresultater"
|
||||
},
|
||||
"map": {
|
||||
"view_details": "Vis detaljer",
|
||||
"adventure_map": "Eventyrkart",
|
||||
"map_options": "Kartalternativer",
|
||||
"show_visited_regions": "Vis besøkte regioner",
|
||||
"add_adventure_at_marker": "Legg til nytt eventyr ved markøren",
|
||||
"clear_marker": "Fjern markør",
|
||||
"add_adventure": "Legg til nytt eventyr"
|
||||
},
|
||||
"share": {
|
||||
"shared": "Delt",
|
||||
"with": "med",
|
||||
"unshared": "Udelt",
|
||||
"share_desc": "Del denne samlingen med andre brukere.",
|
||||
"shared_with": "Delt med",
|
||||
"no_users_shared": "Ingen brukere delt med",
|
||||
"not_shared_with": "Ikke delt med",
|
||||
"no_shared_found": "Ingen samlinger funnet som er delt med deg.",
|
||||
"set_public": "For å la brukere dele med deg, må profilen din være offentlig.",
|
||||
"go_to_settings": "Gå til innstillinger"
|
||||
},
|
||||
"languages": {},
|
||||
"profile": {
|
||||
"member_since": "Medlem siden",
|
||||
"user_stats": "Brukerstatistikk",
|
||||
"visited_countries": "Besøkte land",
|
||||
"visited_regions": "Besøkte regioner",
|
||||
"visited_cities": "Besøkte byer"
|
||||
},
|
||||
"categories": {
|
||||
"manage_categories": "Administrer kategorier",
|
||||
"no_categories_found": "Ingen kategorier funnet.",
|
||||
"edit_category": "Rediger kategori",
|
||||
"icon": "Ikon",
|
||||
"update_after_refresh": "Eventyrkortene vil oppdateres når du oppdaterer siden.",
|
||||
"select_category": "Velg kategori",
|
||||
"category_name": "Kategorinavn"
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome_back": "Velkommen tilbake",
|
||||
"countries_visited": "Land besøkt",
|
||||
"total_adventures": "Totalt antall eventyr",
|
||||
"total_visited_regions": "Totalt antall besøkte regioner",
|
||||
"total_visited_cities": "Totalt antall besøkte byer",
|
||||
"recent_adventures": "Nylige eventyr",
|
||||
"no_recent_adventures": "Ingen nylige eventyr?",
|
||||
"add_some": "Hvorfor ikke begynne å planlegge ditt neste eventyr? Du kan legge til et nytt eventyr ved å klikke på knappen nedenfor."
|
||||
},
|
||||
"immich": {
|
||||
"immich": "Immich",
|
||||
"integration_fetch_error": "Feil ved henting av data fra Immich-integrasjonen",
|
||||
"integration_missing": "Immich-integrasjonen mangler på backend",
|
||||
"query_required": "Forespørsel er påkrevd",
|
||||
"server_down": "Immich-serveren er nede eller utilgjengelig",
|
||||
"no_items_found": "Ingen elementer funnet",
|
||||
"imageid_required": "Bilde-ID er påkrevd",
|
||||
"load_more": "Last mer",
|
||||
"immich_updated": "Immich-innstillinger oppdatert!",
|
||||
"immich_enabled": "Immich-integrasjon aktivert!",
|
||||
"immich_error": "Feil ved oppdatering av Immich-integrasjon",
|
||||
"immich_disabled": "Immich-integrasjon deaktivert!",
|
||||
"immich_desc": "Integrer Immich-kontoen din med AdventureLog for å søke i bildebiblioteket ditt og importere bilder til eventyrene dine.",
|
||||
"integration_enabled": "Integrasjon aktivert",
|
||||
"disable": "Deaktiver",
|
||||
"server_url": "Immich-server-URL",
|
||||
"api_note": "Merk: dette må være URL-en til Immich API-serveren, så den slutter sannsynligvis med /api, med mindre du har en tilpasset konfig.",
|
||||
"api_key": "Immich API-nøkkel",
|
||||
"enable_immich": "Aktiver Immich",
|
||||
"update_integration": "Oppdater integrasjon",
|
||||
"immich_integration": "Immich-integrasjon",
|
||||
"localhost_note": "Merk: localhost vil sannsynligvis ikke fungere med mindre du har satt opp docker-nettverk. Det anbefales å bruke serverens IP-adresse eller domenenavn.",
|
||||
"documentation": "Immich-integrasjonsdokumentasjon"
|
||||
},
|
||||
"recomendations": {
|
||||
"address": "Adresse",
|
||||
"phone": "Telefon",
|
||||
"contact": "Kontakt",
|
||||
"website": "Nettsted",
|
||||
"recommendation": "Anbefaling"
|
||||
}
|
||||
}
|
|
@ -295,7 +295,19 @@
|
|||
"region": "Region",
|
||||
"reservation_number": "Numer rezerwacji",
|
||||
"welcome_map_info": "Publiczne przygody na tym serwerze",
|
||||
"open_in_maps": "Otwarte w mapach"
|
||||
"open_in_maps": "Otwarte w mapach",
|
||||
"all_day": "Cały dzień",
|
||||
"collection_no_start_end_date": "Dodanie daty rozpoczęcia i końca do kolekcji odblokuje funkcje planowania planu podróży na stronie kolekcji.",
|
||||
"date_itinerary": "Trasa daty",
|
||||
"no_ordered_items": "Dodaj przedmioty z datami do kolekcji, aby je zobaczyć tutaj.",
|
||||
"ordered_itinerary": "Zamówiono trasę",
|
||||
"additional_info": "Dodatkowe informacje",
|
||||
"invalid_date_range": "Niepoprawny zakres dat",
|
||||
"sunrise_sunset": "Wschód słońca",
|
||||
"timezone": "Strefa czasowa",
|
||||
"no_visits": "Brak wizyt",
|
||||
"arrival_timezone": "Strefa czasowa przyjazdu",
|
||||
"departure_timezone": "Strefa czasowa odlotu"
|
||||
},
|
||||
"worldtravel": {
|
||||
"country_list": "Lista krajów",
|
||||
|
|
|
@ -247,7 +247,19 @@
|
|||
"price": "Pris",
|
||||
"region": "Område",
|
||||
"reservation_number": "Bokningsnummer",
|
||||
"open_in_maps": "Kappas in"
|
||||
"open_in_maps": "Kappas in",
|
||||
"all_day": "Hela dagen",
|
||||
"collection_no_start_end_date": "Att lägga till ett start- och slutdatum till samlingen kommer att låsa upp planeringsfunktioner för resplan på insamlingssidan.",
|
||||
"date_itinerary": "Datum resplan",
|
||||
"no_ordered_items": "Lägg till objekt med datum i samlingen för att se dem här.",
|
||||
"ordered_itinerary": "Beställd resplan",
|
||||
"additional_info": "Ytterligare information",
|
||||
"invalid_date_range": "Ogiltigt datumintervall",
|
||||
"sunrise_sunset": "Soluppgång",
|
||||
"timezone": "Tidszon",
|
||||
"no_visits": "Inga besök",
|
||||
"arrival_timezone": "Ankomsttidszon",
|
||||
"departure_timezone": "Avgångstidszon"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Upptäck, planera och utforska med lätthet",
|
||||
|
|
|
@ -295,7 +295,19 @@
|
|||
"lodging_information": "住宿信息",
|
||||
"price": "价格",
|
||||
"reservation_number": "预订号",
|
||||
"open_in_maps": "在地图上打开"
|
||||
"open_in_maps": "在地图上打开",
|
||||
"all_day": "整天",
|
||||
"collection_no_start_end_date": "在集合页面中添加开始日期和结束日期将在“收集”页面中解锁行程计划功能。",
|
||||
"date_itinerary": "日期行程",
|
||||
"no_ordered_items": "将带有日期的项目添加到集合中,以便在此处查看它们。",
|
||||
"ordered_itinerary": "订购了行程",
|
||||
"additional_info": "附加信息",
|
||||
"invalid_date_range": "无效的日期范围",
|
||||
"sunrise_sunset": "日出",
|
||||
"timezone": "时区",
|
||||
"no_visits": "没有访问",
|
||||
"arrival_timezone": "到达时区",
|
||||
"departure_timezone": "离开时区"
|
||||
},
|
||||
"auth": {
|
||||
"forgot_password": "忘记密码?",
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import Lost from '$lib/assets/undraw_lost.svg';
|
||||
import ServerError from '$lib/assets/undraw_server_error.svg';
|
||||
</script>
|
||||
|
||||
{#if $page.status === 404}
|
||||
|
@ -24,3 +25,52 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $page.status === 500}
|
||||
<div
|
||||
class="flex min-h-[100dvh] flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8"
|
||||
>
|
||||
<div class="mx-auto max-w-md text-center">
|
||||
<img src={ServerError} alt="Lost in the forest" />
|
||||
<h1 class="text-center text-5xl font-extrabold mt-2">
|
||||
{$page.status}: {$page.error?.message}
|
||||
</h1>
|
||||
<h1 class="mt-4 text-xl font-bold tracking-tight text-foreground">
|
||||
Oops, looks like something went wrong.
|
||||
</h1>
|
||||
|
||||
<p class="mt-4">
|
||||
AdventureLog server encountered an error while processing your request.
|
||||
<br />
|
||||
Please check the server logs for more information.
|
||||
</p>
|
||||
|
||||
<div class="alert alert-warning mt-4">
|
||||
<p class="text-muted-foreground">
|
||||
<strong>Administrators:</strong> Please check your setup using the
|
||||
<a class="link link-primary" target="_blank" href="https://adventurelog.app"
|
||||
>documentation</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- If the route is /login give a hint as an alert -->
|
||||
{#if $page.url.pathname === '/login' || $page.url.pathname === '/signup'}
|
||||
<div class="alert alert-info mt-4">
|
||||
<p
|
||||
class="text-muted
|
||||
-foreground"
|
||||
>
|
||||
<strong>Hint:</strong> If you are an administrator, please check your PUBLIC_SERVER_URL
|
||||
in the frontend config to make sure it can reach the backend.
|
||||
<br />
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-6 flex flex-col items-center gap-4 sm:flex-row">
|
||||
<button class="btn btn-neutral" on:click={() => goto('/')}>Go to Homepage</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -15,8 +15,9 @@
|
|||
register('sv', () => import('../locales/sv.json'));
|
||||
register('pl', () => import('../locales/pl.json'));
|
||||
register('ko', () => import('../locales/ko.json'));
|
||||
register('no', () => import('../locales/no.json'));
|
||||
|
||||
let locales = ['en', 'es', 'fr', 'de', 'it', 'zh', 'nl', 'sv', 'pl', 'ko'];
|
||||
let locales = ['en', 'es', 'fr', 'de', 'it', 'zh', 'nl', 'sv', 'pl', 'ko', 'no'];
|
||||
|
||||
if (browser) {
|
||||
init({
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import type { PageServerLoad } from './$types';
|
||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
import type { Adventure, Collection } from '$lib/types';
|
||||
import type { AdditionalAdventure, Adventure, Collection } from '$lib/types';
|
||||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
export const load = (async (event) => {
|
||||
const id = event.params as { id: string };
|
||||
let request = await fetch(`${endpoint}/api/adventures/${id.id}/`, {
|
||||
let request = await fetch(`${endpoint}/api/adventures/${id.id}/additional-info/`, {
|
||||
headers: {
|
||||
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||
},
|
||||
|
@ -19,7 +19,7 @@ export const load = (async (event) => {
|
|||
}
|
||||
};
|
||||
} else {
|
||||
let adventure = (await request.json()) as Adventure;
|
||||
let adventure = (await request.json()) as AdditionalAdventure;
|
||||
let collection: Collection | null = null;
|
||||
|
||||
if (adventure.collection) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { Adventure } from '$lib/types';
|
||||
import type { AdditionalAdventure, Adventure } from '$lib/types';
|
||||
import { onMount } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
|
@ -10,8 +10,11 @@
|
|||
import DOMPurify from 'dompurify';
|
||||
// @ts-ignore
|
||||
import toGeoJSON from '@mapbox/togeojson';
|
||||
// @ts-ignore
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import LightbulbOn from '~icons/mdi/lightbulb-on';
|
||||
import WeatherSunset from '~icons/mdi/weather-sunset';
|
||||
|
||||
let geojson: any;
|
||||
|
||||
|
@ -75,7 +78,7 @@
|
|||
export let data: PageData;
|
||||
console.log(data);
|
||||
|
||||
let adventure: Adventure;
|
||||
let adventure: AdditionalAdventure;
|
||||
|
||||
let currentSlide = 0;
|
||||
|
||||
|
@ -91,6 +94,7 @@
|
|||
import AdventureModal from '$lib/components/AdventureModal.svelte';
|
||||
import ImageDisplayModal from '$lib/components/ImageDisplayModal.svelte';
|
||||
import AttachmentCard from '$lib/components/AttachmentCard.svelte';
|
||||
import { isAllDay } from '$lib';
|
||||
|
||||
onMount(async () => {
|
||||
if (data.props.adventure) {
|
||||
|
@ -111,7 +115,7 @@
|
|||
await getGpxFiles();
|
||||
});
|
||||
|
||||
async function saveEdit(event: CustomEvent<Adventure>) {
|
||||
async function saveEdit(event: CustomEvent<AdditionalAdventure>) {
|
||||
adventure = event.detail;
|
||||
isEditModalOpen = false;
|
||||
geojson = null;
|
||||
|
@ -410,23 +414,41 @@
|
|||
</p>
|
||||
<!-- show each visit start and end date as well as notes -->
|
||||
{#each adventure.visits as visit}
|
||||
<div class="grid gap-2">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{visit.start_date
|
||||
? new Date(visit.start_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})
|
||||
: ''}
|
||||
{visit.end_date &&
|
||||
visit.end_date !== '' &&
|
||||
visit.end_date !== visit.start_date
|
||||
? ' - ' +
|
||||
new Date(visit.end_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})
|
||||
: ''}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground -mt-2 mb-2">{visit.notes}</p>
|
||||
<div
|
||||
class="p-4 border border-neutral rounded-lg bg-base-100 shadow-sm flex flex-col gap-2 mb-1"
|
||||
>
|
||||
{#if isAllDay(visit.start_date)}
|
||||
<p class="text-sm text-base-content font-medium">
|
||||
<span class="badge badge-outline mr-2">All Day</span>
|
||||
{visit.start_date.split('T')[0]} – {visit.end_date.split('T')[0]}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-sm text-base-content font-medium">
|
||||
{#if visit.timezone}
|
||||
<!-- Use visit.timezone -->
|
||||
🕓 <strong>{visit.timezone}</strong><br />
|
||||
{DateTime.fromISO(visit.start_date, { zone: 'utc' })
|
||||
.setZone(visit.timezone)
|
||||
.toLocaleString(DateTime.DATETIME_MED)} –
|
||||
{DateTime.fromISO(visit.end_date, { zone: 'utc' })
|
||||
.setZone(visit.timezone)
|
||||
.toLocaleString(DateTime.DATETIME_MED)}
|
||||
{:else}
|
||||
<!-- Fallback to local browser time -->
|
||||
🕓 <strong>Local Time</strong><br />
|
||||
{DateTime.fromISO(visit.start_date).toLocaleString(
|
||||
DateTime.DATETIME_MED
|
||||
)} –
|
||||
{DateTime.fromISO(visit.end_date).toLocaleString(
|
||||
DateTime.DATETIME_MED
|
||||
)}
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if visit.notes}
|
||||
<p class="text-sm text-base-content opacity-70 italic">"{visit.notes}"</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -445,15 +467,34 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<a
|
||||
class="btn btn-neutral btn-sm max-w-32"
|
||||
href={`https://maps.apple.com/?q=${adventure.latitude},${adventure.longitude}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">{$t('adventures.open_in_maps')}</a
|
||||
>
|
||||
{#if adventure.longitude && adventure.latitude}
|
||||
<div>
|
||||
<p class="mb-1">{$t('adventures.open_in_maps')}:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a
|
||||
class="btn btn-neutral text-base btn-sm max-w-32"
|
||||
href={`https://maps.apple.com/?q=${adventure.latitude},${adventure.longitude}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Apple</a
|
||||
>
|
||||
<a
|
||||
class="btn btn-neutral text-base btn-sm max-w-32"
|
||||
href={`https://maps.google.com/?q=${adventure.latitude},${adventure.longitude}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Google</a
|
||||
>
|
||||
<a
|
||||
class="btn btn-neutral text-base btn-sm max-w-32"
|
||||
href={`https://www.openstreetmap.org/?mlat=${adventure.latitude}&mlon=${adventure.longitude}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">OSM</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<MapLibre
|
||||
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
||||
class="flex items-center self-center justify-center aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12 rounded-lg"
|
||||
class="flex items-center self-center justify-center aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-full md:w-10/12 rounded-lg"
|
||||
standardControls
|
||||
center={{ lng: adventure.longitude || 0, lat: adventure.latitude || 0 }}
|
||||
zoom={adventure.longitude ? 12 : 1}
|
||||
|
@ -484,22 +525,48 @@
|
|||
{adventure.category?.display_name + ' ' + adventure.category?.icon}
|
||||
</p>
|
||||
{#if adventure.visits.length > 0}
|
||||
<p class="text-black text-sm">
|
||||
<p>
|
||||
{#each adventure.visits as visit}
|
||||
{visit.start_date
|
||||
? new Date(visit.start_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})
|
||||
: ''}
|
||||
{visit.end_date &&
|
||||
visit.end_date !== '' &&
|
||||
visit.end_date !== visit.start_date
|
||||
? ' - ' +
|
||||
new Date(visit.end_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})
|
||||
: ''}
|
||||
<br />
|
||||
<div
|
||||
class="p-4 border border-neutral rounded-lg bg-base-100 shadow-sm flex flex-col gap-2"
|
||||
>
|
||||
<p class="text-sm text-base-content font-medium">
|
||||
{#if isAllDay(visit.start_date)}
|
||||
<span class="badge badge-outline mr-2">All Day</span>
|
||||
{visit.start_date.split('T')[0]} – {visit.end_date.split(
|
||||
'T'
|
||||
)[0]}
|
||||
{:else}
|
||||
<span>
|
||||
<strong>Local:</strong>
|
||||
{DateTime.fromISO(visit.start_date).toLocaleString(
|
||||
DateTime.DATETIME_MED
|
||||
)} –
|
||||
{DateTime.fromISO(visit.end_date).toLocaleString(
|
||||
DateTime.DATETIME_MED
|
||||
)}
|
||||
</span>
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
{#if !isAllDay(visit.start_date) && visit.timezone}
|
||||
<p class="text-sm text-base-content opacity-80">
|
||||
<strong>{visit.timezone}:</strong>
|
||||
{DateTime.fromISO(visit.start_date, { zone: 'utc' })
|
||||
.setZone(visit.timezone)
|
||||
.toLocaleString(DateTime.DATETIME_MED)} –
|
||||
{DateTime.fromISO(visit.end_date, { zone: 'utc' })
|
||||
.setZone(visit.timezone)
|
||||
.toLocaleString(DateTime.DATETIME_MED)}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if visit.notes}
|
||||
<p class="text-sm text-base-content opacity-70 italic">
|
||||
"{visit.notes}"
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</p>
|
||||
{/if}
|
||||
|
@ -509,60 +576,108 @@
|
|||
</MapLibre>
|
||||
{/if}
|
||||
</div>
|
||||
{#if adventure.attachments && adventure.attachments.length > 0}
|
||||
<div>
|
||||
<!-- attachments -->
|
||||
<h2 class="text-2xl font-bold mt-4">
|
||||
{$t('adventures.attachments')}
|
||||
<div class="tooltip z-10" data-tip={$t('adventures.gpx_tip')}>
|
||||
<button class="btn btn-sm btn-circle btn-neutral">
|
||||
<LightbulbOn class="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<div class="grid gap-4 mt-4">
|
||||
{#if adventure.attachments && adventure.attachments.length > 0}
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each adventure.attachments as attachment}
|
||||
<AttachmentCard {attachment} />
|
||||
{/each}
|
||||
<!-- Additional Info Display Section -->
|
||||
|
||||
<div>
|
||||
{#if adventure.sun_times && adventure.sun_times.length > 0}
|
||||
<h2 class="text-2xl font-bold mt-4 mb-4">{$t('adventures.additional_info')}</h2>
|
||||
{#if adventure.sun_times && adventure.sun_times.length > 0}
|
||||
<div class="collapse collapse-plus bg-base-200 mb-2 overflow-visible">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
<span>
|
||||
{$t('adventures.sunrise_sunset')}
|
||||
<WeatherSunset class="w-6 h-6 inline-block ml-2 -mt-1" />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if adventure.images && adventure.images.length > 0}
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold mt-4">{$t('adventures.images')}</h2>
|
||||
<div class="grid gap-4 mt-4">
|
||||
{#if adventure.images && adventure.images.length > 0}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{#each adventure.images as image}
|
||||
<div class="relative">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<!-- svelte-ignore a11y-missing-content -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="w-full h-48 bg-cover bg-center rounded-lg"
|
||||
style="background-image: url({image.image})"
|
||||
on:click={() => (image_url = image.image)}
|
||||
></div>
|
||||
{#if image.is_primary}
|
||||
<div
|
||||
class="absolute top-0 right-0 bg-primary text-white px-2 py-1 rounded-bl-lg"
|
||||
>
|
||||
{$t('adventures.primary')}
|
||||
|
||||
<div class="collapse-content">
|
||||
<div class="grid gap-4 mt-4">
|
||||
<!-- Sunrise and Sunset times -->
|
||||
{#each adventure.sun_times as sun_time}
|
||||
<div class="grid md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<p class="text-sm text-muted-foreground">Date</p>
|
||||
<p class="text-base font-medium">
|
||||
{new Date(sun_time.date).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<div>
|
||||
<p class="text-sm text-muted-foreground">Sunrise</p>
|
||||
<p class="text-base font-medium">
|
||||
{sun_time.sunrise}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-muted-foreground">Sunset</p>
|
||||
<p class="text-base font-medium">
|
||||
{sun_time.sunset}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if adventure.attachments && adventure.attachments.length > 0}
|
||||
<div>
|
||||
<!-- attachments -->
|
||||
<h2 class="text-2xl font-bold mt-4">
|
||||
{$t('adventures.attachments')}
|
||||
<div class="tooltip z-10" data-tip={$t('adventures.gpx_tip')}>
|
||||
<button class="btn btn-sm btn-circle btn-neutral">
|
||||
<LightbulbOn class="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<div class="grid gap-4 mt-4">
|
||||
{#if adventure.attachments && adventure.attachments.length > 0}
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each adventure.attachments as attachment}
|
||||
<AttachmentCard {attachment} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if adventure.images && adventure.images.length > 0}
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold mt-4">{$t('adventures.images')}</h2>
|
||||
<div class="grid gap-4 mt-4">
|
||||
{#if adventure.images && adventure.images.length > 0}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{#each adventure.images as image}
|
||||
<div class="relative">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<!-- svelte-ignore a11y-missing-content -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="w-full h-48 bg-cover bg-center rounded-lg"
|
||||
style="background-image: url({image.image})"
|
||||
on:click={() => (image_url = image.image)}
|
||||
></div>
|
||||
{#if image.is_primary}
|
||||
<div
|
||||
class="absolute top-0 right-0 bg-primary text-white px-2 py-1 rounded-bl-lg"
|
||||
>
|
||||
{$t('adventures.primary')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -208,7 +208,7 @@ export const actions: Actions = {
|
|||
const order_direction = formData.get('order_direction') as string;
|
||||
const order_by = formData.get('order_by') as string;
|
||||
|
||||
console.log(order_direction, order_by);
|
||||
// console.log(order_direction, order_by);
|
||||
|
||||
let adventures: Adventure[] = [];
|
||||
|
||||
|
@ -242,7 +242,7 @@ export const actions: Actions = {
|
|||
previous = res.previous;
|
||||
count = res.count;
|
||||
adventures = [...adventures, ...visited];
|
||||
console.log(next, previous, count);
|
||||
// console.log(next, previous, count);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
|
||||
let collections: Collection[] = data.props.adventures || [];
|
||||
|
||||
let currentSort = { attribute: 'name', order: 'asc' };
|
||||
|
||||
let newType: string = '';
|
||||
|
||||
let resultsPerPage: number = 25;
|
||||
|
@ -235,17 +233,36 @@
|
|||
aria-label={$t(`adventures.descending`)}
|
||||
/>
|
||||
</div>
|
||||
<p class="text-lg font-semibold mt-2 mb-2">{$t('adventures.order_by')}</p>
|
||||
<div class="join">
|
||||
<input
|
||||
class="join-item btn btn-neutral"
|
||||
type="radio"
|
||||
name="order_by"
|
||||
id="upated_at"
|
||||
value="upated_at"
|
||||
aria-label={$t('adventures.updated')}
|
||||
checked
|
||||
/>
|
||||
<input
|
||||
class="join-item btn btn-neutral"
|
||||
type="radio"
|
||||
name="order_by"
|
||||
id="start_date"
|
||||
value="start_date"
|
||||
aria-label={$t('adventures.start_date')}
|
||||
/>
|
||||
<input
|
||||
class="join-item btn btn-neutral"
|
||||
type="radio"
|
||||
name="order_by"
|
||||
id="name"
|
||||
value="name"
|
||||
aria-label={$t('adventures.name')}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
name="order_by"
|
||||
id="name"
|
||||
class="radio radio-primary"
|
||||
checked
|
||||
value="name"
|
||||
hidden
|
||||
/>
|
||||
<button type="submit" class="btn btn-success btn-primary mt-4"
|
||||
>{$t(`adventures.sort`)}</button
|
||||
>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { Adventure, Checklist, Collection, Lodging, Note, Transportation } from '$lib/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { marked } from 'marked'; // Import the markdown parser
|
||||
|
||||
|
@ -133,9 +133,70 @@
|
|||
}
|
||||
|
||||
let currentView: string = 'itinerary';
|
||||
let currentItineraryView: string = 'date';
|
||||
|
||||
let adventures: Adventure[] = [];
|
||||
|
||||
// Add this after your existing MapLibre markers
|
||||
|
||||
// Add this after your existing MapLibre markers
|
||||
|
||||
// Create line data from orderedItems
|
||||
$: lineData = createLineData(orderedItems);
|
||||
|
||||
// Function to create GeoJSON line data from ordered items
|
||||
function createLineData(
|
||||
items: Array<{
|
||||
item: Adventure | Transportation | Lodging | Note | Checklist;
|
||||
start: string;
|
||||
end: string;
|
||||
}>
|
||||
) {
|
||||
if (items.length < 2) return null;
|
||||
|
||||
const coordinates: [number, number][] = [];
|
||||
|
||||
// Extract coordinates from each item
|
||||
for (const orderItem of items) {
|
||||
const item = orderItem.item;
|
||||
|
||||
if (
|
||||
'origin_longitude' in item &&
|
||||
'origin_latitude' in item &&
|
||||
'destination_longitude' in item &&
|
||||
'destination_latitude' in item &&
|
||||
item.origin_longitude &&
|
||||
item.origin_latitude &&
|
||||
item.destination_longitude &&
|
||||
item.destination_latitude
|
||||
) {
|
||||
// For Transportation, add both origin and destination points
|
||||
coordinates.push([item.origin_longitude, item.origin_latitude]);
|
||||
coordinates.push([item.destination_longitude, item.destination_latitude]);
|
||||
} else if ('longitude' in item && 'latitude' in item && item.longitude && item.latitude) {
|
||||
// Handle Adventure and Lodging types
|
||||
coordinates.push([item.longitude, item.latitude]);
|
||||
}
|
||||
}
|
||||
|
||||
// Only create line data if we have at least 2 coordinates
|
||||
if (coordinates.length >= 2) {
|
||||
return {
|
||||
type: 'Feature' as const,
|
||||
properties: {
|
||||
name: 'Itinerary Path',
|
||||
description: 'Path connecting chronological items'
|
||||
},
|
||||
geometry: {
|
||||
type: 'LineString' as const,
|
||||
coordinates: coordinates
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
let numVisited: number = 0;
|
||||
let numAdventures: number = 0;
|
||||
|
||||
|
@ -169,6 +230,63 @@
|
|||
}
|
||||
}
|
||||
|
||||
let orderedItems: Array<{
|
||||
item: Adventure | Transportation | Lodging;
|
||||
type: 'adventure' | 'transportation' | 'lodging';
|
||||
start: string; // ISO date string
|
||||
end: string; // ISO date string
|
||||
}> = [];
|
||||
|
||||
$: {
|
||||
// Reset ordered items
|
||||
orderedItems = [];
|
||||
|
||||
// Add Adventures (using visit dates)
|
||||
adventures.forEach((adventure) => {
|
||||
adventure.visits.forEach((visit) => {
|
||||
orderedItems.push({
|
||||
item: adventure,
|
||||
start: visit.start_date,
|
||||
end: visit.end_date,
|
||||
type: 'adventure'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add Transportation
|
||||
transportations.forEach((transport) => {
|
||||
if (transport.date) {
|
||||
// Only add if date exists
|
||||
orderedItems.push({
|
||||
item: transport,
|
||||
start: transport.date,
|
||||
end: transport.end_date || transport.date, // Use end_date if available, otherwise use date,
|
||||
type: 'transportation'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add Lodging
|
||||
lodging.forEach((lodging) => {
|
||||
if (lodging.check_in) {
|
||||
// Only add if check_in exists
|
||||
orderedItems.push({
|
||||
item: lodging,
|
||||
start: lodging.check_in,
|
||||
end: lodging.check_out || lodging.check_in, // Use check_out if available, otherwise use check_in,
|
||||
type: 'lodging'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sort all items chronologically by start date
|
||||
orderedItems.sort((a, b) => {
|
||||
const dateA = new Date(a.start).getTime();
|
||||
const dateB = new Date(b.start).getTime();
|
||||
return dateA - dateB;
|
||||
});
|
||||
}
|
||||
|
||||
$: {
|
||||
numAdventures = adventures.length;
|
||||
numVisited = adventures.filter((adventure) => adventure.is_visited).length;
|
||||
|
@ -179,6 +297,21 @@
|
|||
let isShowingTransportationModal: boolean = false;
|
||||
let isShowingChecklistModal: boolean = false;
|
||||
|
||||
function handleHashChange() {
|
||||
const hash = window.location.hash.replace('#', '');
|
||||
if (hash) {
|
||||
currentView = hash;
|
||||
} else if (!collection.start_date) {
|
||||
currentView = 'all';
|
||||
} else {
|
||||
currentView = 'itinerary';
|
||||
}
|
||||
}
|
||||
|
||||
function changeHash(event: any) {
|
||||
window.location.hash = '#' + event.target.value;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (data.props.adventure) {
|
||||
collection = data.props.adventure;
|
||||
|
@ -186,6 +319,7 @@
|
|||
} else {
|
||||
notFound = true;
|
||||
}
|
||||
|
||||
if (collection.start_date && collection.end_date) {
|
||||
numberOfDays =
|
||||
Math.floor(
|
||||
|
@ -209,11 +343,12 @@
|
|||
if (collection.checklists) {
|
||||
checklists = collection.checklists;
|
||||
}
|
||||
if (!collection.start_date) {
|
||||
currentView = 'all';
|
||||
} else {
|
||||
currentView = 'itinerary';
|
||||
}
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
handleHashChange();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('hashchange', handleHashChange);
|
||||
});
|
||||
|
||||
function deleteAdventure(event: CustomEvent<string>) {
|
||||
|
@ -641,47 +776,52 @@
|
|||
{/if}
|
||||
|
||||
{#if collection.id}
|
||||
<div class="flex justify-center mx-auto">
|
||||
<select
|
||||
class="select select-bordered border-primary md:hidden w-full"
|
||||
value={currentView}
|
||||
on:change={changeHash}
|
||||
>
|
||||
<option value="itinerary">📅 Itinerary</option>
|
||||
<option value="all">🗒️ All Linked Items</option>
|
||||
<option value="calendar">🗓️ Calendar</option>
|
||||
<option value="map">🗺️ Map</option>
|
||||
<option value="recommendations">👍️ Recommendations</option>
|
||||
</select>
|
||||
<div class="md:flex justify-center mx-auto hidden">
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<div role="tablist" class="tabs tabs-boxed tabs-lg max-w-full">
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
{#if collection.start_date}
|
||||
<a
|
||||
href="#itinerary"
|
||||
role="tab"
|
||||
class="tab {currentView === 'itinerary' ? 'tab-active' : ''}"
|
||||
tabindex="0"
|
||||
on:click={() => (currentView = 'itinerary')}
|
||||
on:keydown={(e) => e.key === 'Enter' && (currentView = 'itinerary')}>Itinerary</a
|
||||
tabindex="0">Itinerary</a
|
||||
>
|
||||
{/if}
|
||||
<a
|
||||
href="#all"
|
||||
role="tab"
|
||||
class="tab {currentView === 'all' ? 'tab-active' : ''}"
|
||||
tabindex="0"
|
||||
on:click={() => (currentView = 'all')}
|
||||
on:keydown={(e) => e.key === 'Enter' && (currentView = 'all')}>All Linked Items</a
|
||||
tabindex="0">All Linked Items</a
|
||||
>
|
||||
<a
|
||||
href="#calendar"
|
||||
role="tab"
|
||||
class="tab {currentView === 'calendar' ? 'tab-active' : ''}"
|
||||
tabindex="0"
|
||||
on:click={() => (currentView = 'calendar')}
|
||||
on:keydown={(e) => e.key === 'Enter' && (currentView = 'calendar')}>Calendar</a
|
||||
tabindex="0">Calendar</a
|
||||
>
|
||||
<a
|
||||
href="#map"
|
||||
role="tab"
|
||||
class="tab {currentView === 'map' ? 'tab-active' : ''}"
|
||||
tabindex="0"
|
||||
on:click={() => (currentView = 'map')}
|
||||
on:keydown={(e) => e.key === 'Enter' && (currentView = 'map')}>Map</a
|
||||
tabindex="0">Map</a
|
||||
>
|
||||
<a
|
||||
href="#recommendations"
|
||||
role="tab"
|
||||
class="tab {currentView === 'recommendations' ? 'tab-active' : ''}"
|
||||
tabindex="0"
|
||||
on:click={() => (currentView = 'recommendations')}
|
||||
on:keydown={(e) => e.key === 'Enter' && (currentView = 'recommendations')}
|
||||
>Recommendations</a
|
||||
tabindex="0">Recommendations</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -806,133 +946,270 @@
|
|||
})}</span
|
||||
>
|
||||
</p>
|
||||
<div class="join mt-2">
|
||||
<input
|
||||
class="join-item btn btn-neutral"
|
||||
type="radio"
|
||||
name="options"
|
||||
aria-label={$t('adventures.date_itinerary')}
|
||||
checked={currentItineraryView == 'date'}
|
||||
on:change={() => (currentItineraryView = 'date')}
|
||||
/>
|
||||
<input
|
||||
class="join-item btn btn-neutral"
|
||||
type="radio"
|
||||
name="options"
|
||||
aria-label={$t('adventures.ordered_itinerary')}
|
||||
checked={currentItineraryView == 'ordered'}
|
||||
on:change={() => (currentItineraryView = 'ordered')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto px-4">
|
||||
{#each Array(numberOfDays) as _, i}
|
||||
{@const startDate = new Date(collection.start_date)}
|
||||
{@const tempDate = new Date(startDate.getTime())}
|
||||
{@const adjustedDate = new Date(tempDate.setUTCDate(tempDate.getUTCDate() + i))}
|
||||
{@const dateString = adjustedDate.toISOString().split('T')[0]}
|
||||
{#if currentItineraryView == 'date'}
|
||||
<div class="container mx-auto px-4">
|
||||
{#each Array(numberOfDays) as _, i}
|
||||
{@const startDate = new Date(collection.start_date)}
|
||||
{@const tempDate = new Date(startDate.getTime())}
|
||||
{@const adjustedDate = new Date(tempDate.setUTCDate(tempDate.getUTCDate() + i))}
|
||||
{@const dateString = adjustedDate.toISOString().split('T')[0]}
|
||||
|
||||
{@const dayAdventures =
|
||||
groupAdventuresByDate(adventures, new Date(collection.start_date), numberOfDays)[
|
||||
dateString
|
||||
] || []}
|
||||
{@const dayTransportations =
|
||||
groupTransportationsByDate(
|
||||
transportations,
|
||||
new Date(collection.start_date),
|
||||
numberOfDays
|
||||
)[dateString] || []}
|
||||
{@const dayLodging =
|
||||
groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays)[
|
||||
dateString
|
||||
] || []}
|
||||
{@const dayNotes =
|
||||
groupNotesByDate(notes, new Date(collection.start_date), numberOfDays)[dateString] ||
|
||||
[]}
|
||||
{@const dayChecklists =
|
||||
groupChecklistsByDate(checklists, new Date(collection.start_date), numberOfDays)[
|
||||
dateString
|
||||
] || []}
|
||||
{@const dayAdventures =
|
||||
groupAdventuresByDate(adventures, new Date(collection.start_date), numberOfDays + 1)[
|
||||
dateString
|
||||
] || []}
|
||||
{@const dayTransportations =
|
||||
groupTransportationsByDate(
|
||||
transportations,
|
||||
new Date(collection.start_date),
|
||||
numberOfDays + 1
|
||||
)[dateString] || []}
|
||||
{@const dayLodging =
|
||||
groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays + 1)[
|
||||
dateString
|
||||
] || []}
|
||||
{@const dayNotes =
|
||||
groupNotesByDate(notes, new Date(collection.start_date), numberOfDays + 1)[
|
||||
dateString
|
||||
] || []}
|
||||
{@const dayChecklists =
|
||||
groupChecklistsByDate(checklists, new Date(collection.start_date), numberOfDays + 1)[
|
||||
dateString
|
||||
] || []}
|
||||
|
||||
<div class="card bg-base-100 shadow-xl my-8">
|
||||
<div class="card-body bg-base-200">
|
||||
<h2 class="card-title text-3xl justify-center g">
|
||||
{$t('adventures.day')}
|
||||
{i + 1}
|
||||
<div class="badge badge-lg">
|
||||
{adjustedDate.toLocaleDateString(undefined, { timeZone: 'UTC' })}
|
||||
<div class="card bg-base-100 shadow-xl my-8">
|
||||
<div class="card-body bg-base-200">
|
||||
<h2 class="card-title text-3xl justify-center g">
|
||||
{$t('adventures.day')}
|
||||
{i + 1}
|
||||
<div class="badge badge-lg">
|
||||
{adjustedDate.toLocaleDateString(undefined, { timeZone: 'UTC' })}
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{#if dayAdventures.length > 0}
|
||||
{#each dayAdventures as adventure}
|
||||
<AdventureCard
|
||||
user={data.user}
|
||||
on:edit={editAdventure}
|
||||
on:delete={deleteAdventure}
|
||||
{adventure}
|
||||
{collection}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if dayTransportations.length > 0}
|
||||
{#each dayTransportations as transportation}
|
||||
<TransportationCard
|
||||
{transportation}
|
||||
user={data?.user}
|
||||
on:delete={(event) => {
|
||||
transportations = transportations.filter((t) => t.id != event.detail);
|
||||
}}
|
||||
on:edit={(event) => {
|
||||
transportationToEdit = event.detail;
|
||||
isShowingTransportationModal = true;
|
||||
}}
|
||||
{collection}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if dayNotes.length > 0}
|
||||
{#each dayNotes as note}
|
||||
<NoteCard
|
||||
{note}
|
||||
user={data.user || null}
|
||||
on:edit={(event) => {
|
||||
noteToEdit = event.detail;
|
||||
isNoteModalOpen = true;
|
||||
}}
|
||||
on:delete={(event) => {
|
||||
notes = notes.filter((n) => n.id != event.detail);
|
||||
}}
|
||||
{collection}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if dayLodging.length > 0}
|
||||
{#each dayLodging as hotel}
|
||||
<LodgingCard
|
||||
lodging={hotel}
|
||||
user={data?.user}
|
||||
on:delete={(event) => {
|
||||
lodging = lodging.filter((t) => t.id != event.detail);
|
||||
}}
|
||||
on:edit={editLodging}
|
||||
{collection}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if dayChecklists.length > 0}
|
||||
{#each dayChecklists as checklist}
|
||||
<ChecklistCard
|
||||
{checklist}
|
||||
user={data.user || null}
|
||||
on:delete={(event) => {
|
||||
notes = notes.filter((n) => n.id != event.detail);
|
||||
}}
|
||||
on:edit={(event) => {
|
||||
checklistToEdit = event.detail;
|
||||
isShowingChecklistModal = true;
|
||||
}}
|
||||
{collection}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{#if dayAdventures.length > 0}
|
||||
{#each dayAdventures as adventure}
|
||||
<AdventureCard
|
||||
user={data.user}
|
||||
on:edit={editAdventure}
|
||||
on:delete={deleteAdventure}
|
||||
{adventure}
|
||||
{collection}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if dayTransportations.length > 0}
|
||||
{#each dayTransportations as transportation}
|
||||
<TransportationCard
|
||||
{transportation}
|
||||
{collection}
|
||||
user={data?.user}
|
||||
on:delete={(event) => {
|
||||
transportations = transportations.filter((t) => t.id != event.detail);
|
||||
}}
|
||||
on:edit={(event) => {
|
||||
transportationToEdit = event.detail;
|
||||
isShowingTransportationModal = true;
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if dayNotes.length > 0}
|
||||
{#each dayNotes as note}
|
||||
<NoteCard
|
||||
{note}
|
||||
{collection}
|
||||
user={data.user || null}
|
||||
on:edit={(event) => {
|
||||
noteToEdit = event.detail;
|
||||
isNoteModalOpen = true;
|
||||
}}
|
||||
on:delete={(event) => {
|
||||
notes = notes.filter((n) => n.id != event.detail);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if dayLodging.length > 0}
|
||||
{#each dayLodging as hotel}
|
||||
<LodgingCard
|
||||
lodging={hotel}
|
||||
{collection}
|
||||
user={data?.user}
|
||||
on:delete={(event) => {
|
||||
lodging = lodging.filter((t) => t.id != event.detail);
|
||||
}}
|
||||
on:edit={editLodging}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if dayChecklists.length > 0}
|
||||
{#each dayChecklists as checklist}
|
||||
<ChecklistCard
|
||||
{checklist}
|
||||
{collection}
|
||||
user={data.user || null}
|
||||
on:delete={(event) => {
|
||||
notes = notes.filter((n) => n.id != event.detail);
|
||||
}}
|
||||
on:edit={(event) => {
|
||||
checklistToEdit = event.detail;
|
||||
isShowingChecklistModal = true;
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0 && dayLodging.length == 0}
|
||||
<p class="text-center text-lg mt-2 italic">{$t('adventures.nothing_planned')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0 && dayLodging.length == 0}
|
||||
<p class="text-center text-lg mt-2 italic">{$t('adventures.nothing_planned')}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-full max-w-4xl relative">
|
||||
<!-- Vertical timeline line that spans the entire height -->
|
||||
{#if orderedItems.length > 0}
|
||||
<div class="absolute left-8 top-0 bottom-0 w-1 bg-primary"></div>
|
||||
{/if}
|
||||
<ul class="relative">
|
||||
{#each orderedItems as orderedItem, index}
|
||||
<li class="relative pl-20 mb-8">
|
||||
<!-- Timeline Icon -->
|
||||
<div
|
||||
class="absolute left-0 top-0 flex items-center justify-center w-16 h-16 bg-base-200 rounded-full border-2 border-primary"
|
||||
>
|
||||
{#if orderedItem.type === 'adventure' && orderedItem.item && 'category' in orderedItem.item && orderedItem.item.category && 'icon' in orderedItem.item.category}
|
||||
<span class="text-2xl">{orderedItem.item.category.icon}</span>
|
||||
{:else if orderedItem.type === 'transportation' && orderedItem.item && 'origin_latitude' in orderedItem.item}
|
||||
<span class="text-2xl">{getTransportationEmoji(orderedItem.item.type)}</span
|
||||
>
|
||||
{:else if orderedItem.type === 'lodging' && orderedItem.item && 'reservation_number' in orderedItem.item}
|
||||
<span class="text-2xl">{getLodgingIcon(orderedItem.item.type)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Card Content -->
|
||||
<div class="bg-base-200 p-6 rounded-lg shadow-lg">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<span class="badge badge-lg">{$t(`adventures.${orderedItem.type}`)}</span>
|
||||
<div class="text-sm opacity-80 text-right">
|
||||
{new Date(orderedItem.start).toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
{#if orderedItem.start !== orderedItem.end}
|
||||
<div>
|
||||
{new Date(orderedItem.start).toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
-
|
||||
{new Date(orderedItem.end).toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<!-- Duration -->
|
||||
{Math.round(
|
||||
(new Date(orderedItem.end).getTime() -
|
||||
new Date(orderedItem.start).getTime()) /
|
||||
1000 /
|
||||
60 /
|
||||
60
|
||||
)}h
|
||||
{Math.round(
|
||||
((new Date(orderedItem.end).getTime() -
|
||||
new Date(orderedItem.start).getTime()) /
|
||||
1000 /
|
||||
60 /
|
||||
60 -
|
||||
Math.floor(
|
||||
(new Date(orderedItem.end).getTime() -
|
||||
new Date(orderedItem.start).getTime()) /
|
||||
1000 /
|
||||
60 /
|
||||
60
|
||||
)) *
|
||||
60
|
||||
)}m
|
||||
</div>
|
||||
{:else}
|
||||
<p>{$t('adventures.all_day')} ⏱️</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if orderedItem.type === 'adventure' && orderedItem.item && 'images' in orderedItem.item}
|
||||
<AdventureCard
|
||||
user={data.user}
|
||||
on:edit={editAdventure}
|
||||
on:delete={deleteAdventure}
|
||||
adventure={orderedItem.item}
|
||||
{collection}
|
||||
/>
|
||||
{:else if orderedItem.type === 'transportation' && orderedItem.item && 'origin_latitude' in orderedItem.item}
|
||||
<TransportationCard
|
||||
transportation={orderedItem.item}
|
||||
user={data?.user}
|
||||
on:delete={(event) => {
|
||||
transportations = transportations.filter((t) => t.id != event.detail);
|
||||
}}
|
||||
on:edit={editTransportation}
|
||||
{collection}
|
||||
/>
|
||||
{:else if orderedItem.type === 'lodging' && orderedItem.item && 'reservation_number' in orderedItem.item}
|
||||
<LodgingCard
|
||||
lodging={orderedItem.item}
|
||||
user={data?.user}
|
||||
on:delete={(event) => {
|
||||
lodging = lodging.filter((t) => t.id != event.detail);
|
||||
}}
|
||||
on:edit={editLodging}
|
||||
{collection}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if orderedItems.length === 0}
|
||||
<div class="alert alert-info">
|
||||
<p class="text-center text-lg">{$t('adventures.no_ordered_items')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
@ -999,6 +1276,19 @@
|
|||
</Marker>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if lineData}
|
||||
<GeoJSON data={lineData}>
|
||||
<LineLayer
|
||||
layout={{ 'line-cap': 'round', 'line-join': 'round' }}
|
||||
paint={{
|
||||
'line-width': 4,
|
||||
'line-color': '#0088CC', // Blue line to distinguish from transportation lines
|
||||
'line-opacity': 0.8,
|
||||
'line-dasharray': [2, 1] // Dashed line to differentiate from direct transportation lines
|
||||
}}
|
||||
/>
|
||||
</GeoJSON>
|
||||
{/if}
|
||||
{#each transportations as transportation}
|
||||
{#if transportation.origin_latitude && transportation.origin_longitude && transportation.destination_latitude && transportation.destination_longitude}
|
||||
<!-- Origin Marker -->
|
||||
|
@ -1040,34 +1330,6 @@
|
|||
</p>
|
||||
</Popup>
|
||||
</Marker>
|
||||
|
||||
<!-- Line connecting origin and destination -->
|
||||
<GeoJSON
|
||||
data={{
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
name: transportation.name,
|
||||
type: transportation.type
|
||||
},
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: [
|
||||
[transportation.origin_longitude, transportation.origin_latitude],
|
||||
[transportation.destination_longitude, transportation.destination_latitude]
|
||||
]
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LineLayer
|
||||
layout={{ 'line-cap': 'round', 'line-join': 'round' }}
|
||||
paint={{
|
||||
'line-width': 3,
|
||||
'line-color': '#898989', // customize your line color here
|
||||
'line-opacity': 0.8
|
||||
// 'line-dasharray': [5, 2]
|
||||
}}
|
||||
/>
|
||||
</GeoJSON>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
|
@ -1233,13 +1495,16 @@
|
|||
{recomendation.name || $t('recomendations.recommendation')}
|
||||
</h2>
|
||||
<div class="badge badge-primary">{recomendation.tag}</div>
|
||||
{#if recomendation.address}
|
||||
{#if recomendation.address && (recomendation.address.housenumber || recomendation.address.street || recomendation.address.city || recomendation.address.state || recomendation.address.postcode)}
|
||||
<p class="text-md">
|
||||
<strong>{$t('recomendations.address')}:</strong>
|
||||
{recomendation.address.housenumber}
|
||||
{recomendation.address.street}, {recomendation.address.city}, {recomendation
|
||||
.address.state}
|
||||
{recomendation.address.postcode}
|
||||
{#if recomendation.address.housenumber}{recomendation.address
|
||||
.housenumber}{/if}
|
||||
{#if recomendation.address.street}
|
||||
{recomendation.address.street}{/if}
|
||||
{#if recomendation.address.city}, {recomendation.address.city}{/if}
|
||||
{#if recomendation.address.state}, {recomendation.address.state}{/if}
|
||||
{#if recomendation.address.postcode}, {recomendation.address.postcode}{/if}
|
||||
</p>
|
||||
{/if}
|
||||
{#if recomendation.contact}
|
||||
|
|
|
@ -74,8 +74,6 @@ export const actions: Actions = {
|
|||
} else {
|
||||
const setCookieHeader = loginFetch.headers.get('Set-Cookie');
|
||||
|
||||
console.log('setCookieHeader:', setCookieHeader);
|
||||
|
||||
if (setCookieHeader) {
|
||||
// Regular expression to match sessionid cookie and its expiry
|
||||
const sessionIdRegex = /sessionid=([^;]+).*?expires=([^;]+)/;
|
||||
|
|
|
@ -9,7 +9,7 @@ export const load = (async (event) => {
|
|||
return redirect(302, '/login');
|
||||
}
|
||||
|
||||
const res = await fetch(`${serverEndpoint}/auth/users/`, {
|
||||
const res = await fetch(`${serverEndpoint}/auth/users`, {
|
||||
headers: {
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
|
|
|
@ -164,6 +164,14 @@
|
|||
|
||||
{#if filteredCountries.length === 0}
|
||||
<p class="text-center font-bold text-2xl mt-12">{$t('worldtravel.no_countries_found')}</p>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a
|
||||
class="link link-primary"
|
||||
href="https://adventurelog.app/docs/configuration/updating.html#updating-the-region-data"
|
||||
target="_blank">{$t('settings.documentation_link')}</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<svelte:head>
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
echo "The origin to be set is: $ORIGIN"
|
||||
# Start the application
|
||||
ORIGIN=$ORIGIN node build
|
||||
ORIGIN=$ORIGIN exec node build
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue