1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-22 14:29:36 +02:00

Rename Adventures to Locations (#696)

* Refactor user_id to user in adventures and related models, views, and components

- Updated all instances of user_id to user in the adventures app, including models, serializers, views, and frontend components.
- Adjusted queries and filters to reflect the new user field naming convention.
- Ensured consistency across the codebase for user identification in adventures, collections, notes, and transportation entities.
- Modified frontend components to align with the updated data structure, ensuring proper access control and rendering based on user ownership.

* Refactor adventure-related views and components to use "Location" terminology

- Updated GlobalSearchView to replace AdventureSerializer with LocationSerializer.
- Modified IcsCalendarGeneratorViewSet to use LocationSerializer instead of AdventureSerializer.
- Created new LocationImageViewSet for managing location images, including primary image toggling and image deletion.
- Introduced LocationViewSet for managing locations with enhanced filtering, sorting, and sharing capabilities.
- Updated ReverseGeocodeViewSet to utilize LocationSerializer.
- Added ActivityTypesView to retrieve distinct activity types from locations.
- Refactored user views to replace AdventureSerializer with LocationSerializer.
- Updated frontend components to reflect changes from "adventure" to "location", including AdventureCard, AdventureLink, AdventureModal, and others.
- Adjusted API endpoints in frontend routes to align with new location-based structure.
- Ensured all references to adventures are replaced with locations across the codebase.

* refactor: rename adventures to locations across the application

- Updated localization files to replace adventure-related terms with location-related terms.
- Refactored TypeScript types and variables from Adventure to Location in various routes and components.
- Adjusted UI elements and labels to reflect the change from adventures to locations.
- Ensured all references to adventures in the codebase are consistent with the new location terminology.

* Refactor code structure for improved readability and maintainability

* feat: Implement location details page with server-side loading and deletion functionality

- Added +page.server.ts to handle server-side loading of additional location info.
- Created +page.svelte for displaying location details, including images, visits, and maps.
- Integrated GPX file handling and rendering on the map.
- Updated map route to link to locations instead of adventures.
- Refactored profile and search routes to use LocationCard instead of AdventureCard.

* docs: Update terminology from "Adventure" to "Location" and enhance project overview

* docs: Clarify collection examples in usage documentation

* feat: Enable credentials for GPX file fetch and add CORS_ALLOW_CREDENTIALS setting

* Refactor adventure references to locations across the backend and frontend

- Updated CategoryViewSet to reflect location context instead of adventures.
- Modified ChecklistViewSet to include locations in retrieval logic.
- Changed GlobalSearchView to search for locations instead of adventures.
- Adjusted IcsCalendarGeneratorViewSet to handle locations instead of adventures.
- Refactored LocationImageViewSet to remove unused import.
- Updated LocationViewSet to clarify public access for locations.
- Changed LodgingViewSet to reference locations instead of adventures.
- Modified NoteViewSet to prevent listing all locations.
- Updated RecommendationsViewSet to handle locations in parsing and response.
- Adjusted ReverseGeocodeViewSet to search through user locations.
- Updated StatsViewSet to count locations instead of adventures.
- Changed TagsView to reflect activity types for locations.
- Updated TransportationViewSet to reference locations instead of adventures.
- Added new translations for search results related to locations in multiple languages.
- Updated dashboard and profile pages to reflect location counts instead of adventure counts.
- Adjusted search routes to handle locations instead of adventures.

* Update banner image

* style: Update stats component background and border for improved visibility

* refactor: Rename AdventureCard and AdventureModal to LocationCard and LocationModal for consistency
This commit is contained in:
Sean Morley 2025-06-25 11:49:34 -04:00 committed by GitHub
parent 5308ec21d6
commit 493a13995c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
115 changed files with 3148 additions and 2759 deletions

View file

@ -6,7 +6,9 @@ Were excited to have you contribute to AdventureLog! To ensure that this comm
1. **Open an Issue First**: Discuss any changes or features you plan to implement by opening an issue. This helps to clarify your idea and ensures theres a shared understanding.
2. **Document Changes**: If your changes impact the user interface, add new environment variables, or introduce new container configurations, make sure to update the documentation accordingly. The documentation is located in the `documentation` folder.
3. **Pull Request**: Submit a pull request with your changes. Make sure to reference the issue you opened in the description.
3. **Pull Request**: Submit a pull request with your changes directed towards the `development` branch. Make sure to reference the issue you opened in the description. After your pull request is submitted, it will be reviewed by the maintainers.
4. **Review Process**: The maintainers will review your pull request. They may suggest changes or improvements. Please be open to feedback and ready to make adjustments as needed.
5. **Merge**: Once your pull request is approved, it will be merged into the `development` branch. This branch is where all new features and changes are integrated before being released to the main branch.
## Code of Conduct

View file

@ -38,7 +38,7 @@
## ⭐ About the Project
Starting from a simple idea of tracking travel locations (called adventures), AdventureLog has grown into a full-fledged travel companion. With AdventureLog, you can log your adventures, keep track of where you've been on the world map, plan your next trip collaboratively, and share your experiences with friends and family.
Starting from a simple idea of tracking travel locations, AdventureLog has grown into a full-fledged travel companion. With AdventureLog, you can log your adventures, keep track of where you've been on the world map, plan your next trip collaboratively, and share your experiences with friends and family.
AdventureLog was created 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.

32
adventurelog_overview.md Normal file
View file

@ -0,0 +1,32 @@
# About AdventureLog
Starting from a /locations, AdventureLog has grown into a full-fledged travel companion. With AdventureLog, you can log your adventures, keep track of where you've been on the world map, plan your next trip collaboratively, and share your experiences with friends and family. **AdventureLog is the ultimate travel companion for the modern-day explorer**.
## Features
- **Track Your Adventures** 🌍: Log your adventures and keep track of where you've been on the world map.
- Adventures can store a variety of information, including the location, date, and description.
- Adventures can be sorted into custom categories for easy organization.
- Adventures can be marked as private or public, allowing you to share your adventures with friends and family.
- Keep track of the countries and regions you've visited with the world travel book.
- **Plan Your Next Trip** 📃: Take the guesswork out of planning your next adventure with an easy-to-use itinerary planner.
- Itineraries can be created for any number of days and can include multiple destinations.
- Itineraries include many planning features like flight information, notes, checklists, and links to external resources.
- Itineraries can be shared with friends and family for collaborative planning.
- **Share Your Experiences** 📸: Share your adventures with friends and family and collaborate on trips together.
- Adventures and itineraries can be shared via a public link or directly with other AdventureLog users.
- Collaborators can view and edit shared itineraries (collections), making planning a breeze.
## Why AdventureLog?
AdventureLog was created 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.
### Open Source (GPL-3.0)
AdventureLog is open-source software, licensed under the GPL-3.0 license. This means that you are free to use, modify, and distribute AdventureLog as you see fit. The source code is available on GitHub, and contributions are welcome from anyone who wants to help improve the project.
## About the Maintainer
Hi, I'm [Sean Morley](https://seanmorley.com), the creator of AdventureLog. I'm an Electrical Engineering student at the University of Connecticut, and I'm passionate about open-source software and building modern tools that help people solve real-world problems. I created AdventureLog to solve a problem: the lack of a modern, open-source, user-friendly travel companion. Many existing travel apps are either too complex, too expensive, or too closed-off to be useful for the average traveler. AdventureLog aims to be the opposite: simple, beautiful, and open to everyone.
I hope you enjoy using AdventureLog as much as I enjoy creating it! If you have any questions, feedback, or suggestions, feel free to reach out to me via the email address listed on my website. I'm always happy to hear from users and help in any way I can. Thank you for using AdventureLog, and happy travels! 🌍

View file

@ -1,7 +1,7 @@
import os
from django.contrib import admin
from django.utils.html import mark_safe
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment, Lodging
from .models import Location, Checklist, ChecklistItem, Collection, Transportation, Note, LocationImage, Visit, Category, Attachment, Lodging
from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
from allauth.account.decorators import secure_admin_login
@ -11,19 +11,19 @@ admin.site.login = secure_admin_login(admin.site.login)
@admin.action(description="Trigger geocoding")
def trigger_geocoding(modeladmin, request, queryset):
count = 0
for adventure in queryset:
for location in queryset:
try:
adventure.save() # Triggers geocoding logic in your model
location.save() # Triggers geocoding logic in your model
count += 1
except Exception as e:
modeladmin.message_user(request, f"Error geocoding {adventure}: {e}", level='error')
modeladmin.message_user(request, f"Geocoding triggered for {count} adventures.", level='success')
modeladmin.message_user(request, f"Error geocoding {location}: {e}", level='error')
modeladmin.message_user(request, f"Geocoding triggered for {count} locations.", level='success')
class AdventureAdmin(admin.ModelAdmin):
list_display = ('name', 'get_category', 'get_visit_count', 'user_id', 'is_public')
list_filter = ( 'user_id', 'is_public')
class LocationAdmin(admin.ModelAdmin):
list_display = ('name', 'get_category', 'get_visit_count', 'user', 'is_public')
list_filter = ( 'user', 'is_public')
search_fields = ('name',)
readonly_fields = ('city', 'region', 'country')
actions = [trigger_geocoding]
@ -96,8 +96,8 @@ class CustomUserAdmin(UserAdmin):
else:
return
class AdventureImageAdmin(admin.ModelAdmin):
list_display = ('user_id', 'image_display')
class LocationImageAdmin(admin.ModelAdmin):
list_display = ('user', 'image_display')
def image_display(self, obj):
if obj.image:
@ -109,7 +109,7 @@ class AdventureImageAdmin(admin.ModelAdmin):
class VisitAdmin(admin.ModelAdmin):
list_display = ('adventure', 'start_date', 'end_date', 'notes')
list_display = ('location', 'start_date', 'end_date', 'notes')
list_filter = ('start_date', 'end_date')
search_fields = ('notes',)
@ -125,19 +125,19 @@ class VisitAdmin(admin.ModelAdmin):
image_display.short_description = 'Image Preview'
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'user_id', 'display_name', 'icon')
list_display = ('name', 'user', 'display_name', 'icon')
search_fields = ('name', 'display_name')
class CollectionAdmin(admin.ModelAdmin):
list_display = ('name', 'user_id', 'is_public')
list_display = ('name', 'user', 'is_public')
admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(Adventure, AdventureAdmin)
admin.site.register(Location, LocationAdmin)
admin.site.register(Collection, CollectionAdmin)
admin.site.register(Visit, VisitAdmin)
admin.site.register(Country, CountryAdmin)
@ -147,7 +147,7 @@ admin.site.register(Transportation)
admin.site.register(Note)
admin.site.register(Checklist)
admin.site.register(ChecklistItem)
admin.site.register(AdventureImage, AdventureImageAdmin)
admin.site.register(LocationImage, LocationImageAdmin)
admin.site.register(Category, CategoryAdmin)
admin.site.register(City, CityAdmin)
admin.site.register(VisitedCity)

View file

@ -167,7 +167,7 @@ def extractIsoCode(user, data):
return {"error": "No region found"}
region = Region.objects.filter(id=iso_code).first()
visited_region = VisitedRegion.objects.filter(region=region, user_id=user).first()
visited_region = VisitedRegion.objects.filter(region=region, user=user).first()
region_visited = False
city_visited = False
@ -177,7 +177,7 @@ def extractIsoCode(user, data):
if town_city_or_county:
display_name = f"{town_city_or_county}, {region.name}, {country_code}"
city = City.objects.filter(name__contains=town_city_or_county, region=region).first()
visited_city = VisitedCity.objects.filter(city=city, user_id=user).first()
visited_city = VisitedCity.objects.filter(city=city, user=user).first()
if visited_region:
region_visited = True

View file

@ -2,7 +2,7 @@
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from adventures.models import Adventure
from adventures.models import Location
class Command(BaseCommand):
@ -38,8 +38,8 @@ class Command(BaseCommand):
]
for name, location, type_ in adventures:
Adventure.objects.create(
user_id=user,
Location.objects.create(
user=user,
name=name,
location=location,
type=type_,

View file

@ -1,12 +1,12 @@
from django.db import models
from django.db.models import Q
class AdventureManager(models.Manager):
def retrieve_adventures(self, user, include_owned=False, include_shared=False, include_public=False):
class LocationManager(models.Manager):
def retrieve_locations(self, user, include_owned=False, include_shared=False, include_public=False):
query = Q()
if include_owned:
query |= Q(user_id=user)
query |= Q(user=user)
if include_shared:
query |= Q(collections__shared_with=user)

View file

@ -0,0 +1,91 @@
# Generated by Django 5.2.1 on 2025-06-19 20:29
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
replaces = [('adventures', '0036_rename_adventure_location'), ('adventures', '0037_rename_adventure_visit_location'), ('adventures', '0038_rename_adventureimage_locationimage'), ('adventures', '0039_rename_adventure_locationimage_location'), ('adventures', '0040_rename_adventure_attachment_location'), ('adventures', '0041_rename_user_id_location_user'), ('adventures', '0042_rename_user_id_locationimage_user'), ('adventures', '0043_rename_user_id_attachment_user'), ('adventures', '0044_rename_user_id_collection_user'), ('adventures', '0045_rename_user_id_transportation_user'), ('adventures', '0046_rename_user_id_note_user'), ('adventures', '0047_rename_user_id_checklist_user'), ('adventures', '0048_rename_user_id_checklistitem_user'), ('adventures', '0049_rename_user_id_category_user'), ('adventures', '0050_rename_user_id_lodging_user')]
dependencies = [
('adventures', '0035_remove_adventure_collection_adventure_collections'),
('worldtravel', '0016_remove_city_insert_id_remove_country_insert_id_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RenameModel(
old_name='Adventure',
new_name='Location',
),
migrations.RenameField(
model_name='visit',
old_name='adventure',
new_name='location',
),
migrations.RenameModel(
old_name='AdventureImage',
new_name='LocationImage',
),
migrations.RenameField(
model_name='locationimage',
old_name='adventure',
new_name='location',
),
migrations.RenameField(
model_name='attachment',
old_name='adventure',
new_name='location',
),
migrations.RenameField(
model_name='location',
old_name='user_id',
new_name='user',
),
migrations.RenameField(
model_name='locationimage',
old_name='user_id',
new_name='user',
),
migrations.RenameField(
model_name='attachment',
old_name='user_id',
new_name='user',
),
migrations.RenameField(
model_name='collection',
old_name='user_id',
new_name='user',
),
migrations.RenameField(
model_name='transportation',
old_name='user_id',
new_name='user',
),
migrations.RenameField(
model_name='note',
old_name='user_id',
new_name='user',
),
migrations.RenameField(
model_name='checklist',
old_name='user_id',
new_name='user',
),
migrations.RenameField(
model_name='checklistitem',
old_name='user_id',
new_name='user',
),
migrations.RenameField(
model_name='category',
old_name='user_id',
new_name='user',
),
migrations.RenameField(
model_name='lodging',
old_name='user_id',
new_name='user',
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 5.2.1 on 2025-06-20 15:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0036_rename_adventure_location_squashed_0050_rename_user_id_lodging_user'),
]
operations = [
migrations.RenameField(
model_name='location',
old_name='activity_types',
new_name='tags',
),
migrations.AlterField(
model_name='location',
name='collections',
field=models.ManyToManyField(blank=True, related_name='locations', to='adventures.collection'),
),
]

View file

@ -1,10 +1,9 @@
from django.core.exceptions import ValidationError
import os
from typing import Iterable
import uuid
from django.db import models
from django.utils.deconstruct import deconstructible
from adventures.managers import AdventureManager
from adventures.managers import LocationManager
import threading
from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import ArrayField
@ -13,46 +12,45 @@ from django_resized import ResizedImageField
from worldtravel.models import City, Country, Region, VisitedCity, VisitedRegion
from django.core.exceptions import ValidationError
from django.utils import timezone
from adventures.utils.timezones import TIMEZONES
def background_geocode_and_assign(adventure_id: str):
print(f"[Adventure Geocode Thread] Starting geocode for adventure {adventure_id}")
def background_geocode_and_assign(location_id: str):
print(f"[Location Geocode Thread] Starting geocode for location {location_id}")
try:
adventure = Adventure.objects.get(id=adventure_id)
if not (adventure.latitude and adventure.longitude):
location = Location.objects.get(id=location_id)
if not (location.latitude and location.longitude):
return
from adventures.geocoding import reverse_geocode # or wherever you defined it
is_visited = adventure.is_visited_status()
result = reverse_geocode(adventure.latitude, adventure.longitude, adventure.user_id)
is_visited = location.is_visited_status()
result = reverse_geocode(location.latitude, location.longitude, location.user)
if 'region_id' in result:
region = Region.objects.filter(id=result['region_id']).first()
if region:
adventure.region = region
location.region = region
if is_visited:
VisitedRegion.objects.get_or_create(user_id=adventure.user_id, region=region)
VisitedRegion.objects.get_or_create(user=location.user, region=region)
if 'city_id' in result:
city = City.objects.filter(id=result['city_id']).first()
if city:
adventure.city = city
location.city = city
if is_visited:
VisitedCity.objects.get_or_create(user_id=adventure.user_id, city=city)
VisitedCity.objects.get_or_create(user=location.user, city=city)
if 'country_id' in result:
country = Country.objects.filter(country_code=result['country_id']).first()
if country:
adventure.country = country
location.country = country
# Save updated location info
# Save updated location info, skip geocode threading
adventure.save(update_fields=["region", "city", "country"], _skip_geocode=True)
# print(f"[Adventure Geocode Thread] Successfully processed {adventure_id}: {adventure.name} - {adventure.latitude}, {adventure.longitude}")
location.save(update_fields=["region", "city", "country"], _skip_geocode=True)
except Exception as e:
# Optional: log or print the error
print(f"[Adventure Geocode Thread] Error processing {adventure_id}: {e}")
print(f"[Location Geocode Thread] Error processing {location_id}: {e}")
def validate_file_extension(value):
import os
@ -62,6 +60,7 @@ def validate_file_extension(value):
if not ext.lower() in valid_extensions:
raise ValidationError('Unsupported file extension.')
# Legacy support for old adventure types, not used in newer versions since custom categories are now used
ADVENTURE_TYPES = [
('general', 'General 🌍'),
('outdoor', 'Outdoor 🏞️'),
@ -87,426 +86,6 @@ 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'),
@ -533,13 +112,13 @@ TRANSPORTATION_TYPES = [
]
# Assuming you have a default user ID you want to use
default_user_id = 1 # Replace with an actual user ID
default_user = 1 # Replace with an actual user ID
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')
location = models.ForeignKey('Location', on_delete=models.CASCADE, related_name='visits')
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)
@ -552,18 +131,18 @@ class Visit(models.Model):
raise ValidationError('The start date must be before or equal to the end date.')
def __str__(self):
return f"{self.adventure.name} - {self.start_date} to {self.end_date}"
return f"{self.location.name} - {self.start_date} to {self.end_date}"
class Adventure(models.Model):
class Location(models.Model):
#id = models.AutoField(primary_key=True)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=True, null=True)
name = models.CharField(max_length=200)
location = models.CharField(max_length=200, blank=True, null=True)
activity_types = ArrayField(models.CharField(
tags = ArrayField(models.CharField(
max_length=100), blank=True, null=True)
description = models.TextField(blank=True, null=True)
rating = models.FloatField(blank=True, null=True)
@ -578,12 +157,12 @@ class Adventure(models.Model):
country = models.ForeignKey(Country, on_delete=models.SET_NULL, blank=True, null=True)
# Changed from ForeignKey to ManyToManyField
collections = models.ManyToManyField('Collection', blank=True, related_name='adventures')
collections = models.ManyToManyField('Collection', blank=True, related_name='locations')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = AdventureManager()
objects = LocationManager()
def is_visited_status(self):
current_date = timezone.now().date()
@ -609,18 +188,18 @@ class Adventure(models.Model):
if self.pk: # Only check if the instance has been saved
for collection in self.collections.all():
if collection.is_public and not self.is_public:
raise ValidationError(f'Adventures associated with a public collection must be public. Collection: {collection.name} Adventure: {self.name}')
raise ValidationError(f'Locations associated with a public collection must be public. Collection: {collection.name} Location: {self.name}')
# Only enforce same-user constraint for non-shared collections
if self.user_id != collection.user_id:
if self.user != collection.user:
# Check if this is a shared collection scenario
# Allow if the adventure owner has access to the collection through sharing
if not collection.shared_with.filter(uuid=self.user_id.uuid).exists():
raise ValidationError(f'Adventures must be associated with collections owned by the same user or shared collections. Collection owner: {collection.user_id.username} Adventure owner: {self.user_id.username}')
# Allow if the location owner has access to the collection through sharing
if not collection.shared_with.filter(uuid=self.user.uuid).exists():
raise ValidationError(f'Locations must be associated with collections owned by the same user or shared collections. Collection owner: {collection.user.username} Location owner: {self.user.username}')
if self.category:
if self.user_id != self.category.user_id:
raise ValidationError(f'Adventures must be associated with categories owned by the same user. Category owner: {self.category.user_id.username} Adventure owner: {self.user_id.username}')
if self.user != self.category.user:
raise ValidationError(f'Locations must be associated with categories owned by the same user. Category owner: {self.category.user.username} Location owner: {self.user.username}')
def save(self, force_insert=False, force_update=False, using=None, update_fields=None, _skip_geocode=False, _skip_shared_validation=False):
if force_insert and force_update:
@ -628,7 +207,7 @@ class Adventure(models.Model):
if not self.category:
category, _ = Category.objects.get_or_create(
user_id=self.user_id,
user=self.user,
name='general',
defaults={'display_name': 'General', 'icon': '🌍'}
)
@ -671,8 +250,8 @@ class Adventure(models.Model):
class Collection(models.Model):
#id = models.AutoField(primary_key=True)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
name = models.CharField(max_length=200)
description = models.TextField(blank=True, null=True)
is_public = models.BooleanField(default=False)
@ -684,13 +263,13 @@ class Collection(models.Model):
shared_with = models.ManyToManyField(User, related_name='shared_with', blank=True)
link = models.URLField(blank=True, null=True, max_length=2083)
# if connected adventures are private and collection is public, raise an error
# if connected locations are private and collection is public, raise an error
def clean(self):
if self.is_public and self.pk: # Only check if the instance has a primary key
# Updated to use the new related_name 'adventures'
for adventure in self.adventures.all():
if not adventure.is_public:
raise ValidationError(f'Public collections cannot be associated with private adventures. Collection: {self.name} Adventure: {adventure.name}')
# Updated to use the new related_name 'locations'
for location in self.locations.all():
if not location.is_public:
raise ValidationError(f'Public collections cannot be associated with private locations. Collection: {self.name} Location: {location.name}')
def __str__(self):
return self.name
@ -698,8 +277,8 @@ class Collection(models.Model):
class Transportation(models.Model):
#id = models.AutoField(primary_key=True)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
type = models.CharField(max_length=100, choices=TRANSPORTATION_TYPES)
name = models.CharField(max_length=200)
description = models.TextField(blank=True, null=True)
@ -729,8 +308,8 @@ class Transportation(models.Model):
if self.collection:
if self.collection.is_public and not self.is_public:
raise ValidationError('Transportations associated with a public collection must be public. Collection: ' + self.collection.name + ' Transportation: ' + self.name)
if self.user_id != self.collection.user_id:
raise ValidationError('Transportations must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Transportation owner: ' + self.user_id.username)
if self.user != self.collection.user:
raise ValidationError('Transportations must be associated with collections owned by the same user. Collection owner: ' + self.collection.user.username + ' Transportation owner: ' + self.user.username)
def __str__(self):
return self.name
@ -738,8 +317,8 @@ class Transportation(models.Model):
class Note(models.Model):
#id = models.AutoField(primary_key=True)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
name = models.CharField(max_length=200)
content = models.TextField(blank=True, null=True)
links = ArrayField(models.URLField(), blank=True, null=True)
@ -753,8 +332,8 @@ class Note(models.Model):
if self.collection:
if self.collection.is_public and not self.is_public:
raise ValidationError('Notes associated with a public collection must be public. Collection: ' + self.collection.name + ' Transportation: ' + self.name)
if self.user_id != self.collection.user_id:
raise ValidationError('Notes must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Transportation owner: ' + self.user_id.username)
if self.user != self.collection.user:
raise ValidationError('Notes must be associated with collections owned by the same user. Collection owner: ' + self.collection.user.username + ' Transportation owner: ' + self.user.username)
def __str__(self):
return self.name
@ -762,8 +341,8 @@ class Note(models.Model):
class Checklist(models.Model):
# id = models.AutoField(primary_key=True)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
name = models.CharField(max_length=200)
date = models.DateField(blank=True, null=True)
is_public = models.BooleanField(default=False)
@ -775,8 +354,8 @@ class Checklist(models.Model):
if self.collection:
if self.collection.is_public and not self.is_public:
raise ValidationError('Checklists associated with a public collection must be public. Collection: ' + self.collection.name + ' Checklist: ' + self.name)
if self.user_id != self.collection.user_id:
raise ValidationError('Checklists must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Checklist owner: ' + self.user_id.username)
if self.user != self.collection.user:
raise ValidationError('Checklists must be associated with collections owned by the same user. Collection owner: ' + self.collection.user.username + ' Checklist owner: ' + self.user.username)
def __str__(self):
return self.name
@ -784,8 +363,8 @@ class Checklist(models.Model):
class ChecklistItem(models.Model):
#id = models.AutoField(primary_key=True)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
name = models.CharField(max_length=200)
is_checked = models.BooleanField(default=False)
checklist = models.ForeignKey('Checklist', on_delete=models.CASCADE)
@ -795,8 +374,8 @@ class ChecklistItem(models.Model):
def clean(self):
if self.checklist.is_public and not self.checklist.is_public:
raise ValidationError('Checklist items associated with a public checklist must be public. Checklist: ' + self.checklist.name + ' Checklist item: ' + self.name)
if self.user_id != self.checklist.user_id:
raise ValidationError('Checklist items must be associated with checklists owned by the same user. Checklist owner: ' + self.checklist.user_id.username + ' Checklist item owner: ' + self.user_id.username)
if self.user != self.checklist.user:
raise ValidationError('Checklist items must be associated with checklists owned by the same user. Checklist owner: ' + self.checklist.user.username + ' Checklist item owner: ' + self.user.username)
def __str__(self):
return self.name
@ -812,9 +391,9 @@ class PathAndRename:
filename = f"{uuid.uuid4()}.{ext}"
return os.path.join(self.path, filename)
class AdventureImage(models.Model):
class LocationImage(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user_id)
user = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user)
image = ResizedImageField(
force_format="WEBP",
quality=75,
@ -823,7 +402,7 @@ class AdventureImage(models.Model):
null=True,
)
immich_id = models.CharField(max_length=200, null=True, blank=True)
adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE)
location = models.ForeignKey(Location, related_name='images', on_delete=models.CASCADE)
is_primary = models.BooleanField(default=False)
def clean(self):
@ -858,10 +437,10 @@ class AdventureImage(models.Model):
class Attachment(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
file = models.FileField(upload_to=PathAndRename('attachments/'),validators=[validate_file_extension])
adventure = models.ForeignKey(Adventure, related_name='attachments', on_delete=models.CASCADE)
location = models.ForeignKey(Location, related_name='attachments', on_delete=models.CASCADE)
name = models.CharField(max_length=200, null=True, blank=True)
def delete(self, *args, **kwargs):
@ -874,15 +453,15 @@ class Attachment(models.Model):
class Category(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
name = models.CharField(max_length=200)
display_name = models.CharField(max_length=200)
icon = models.CharField(max_length=200, default='🌍')
class Meta:
verbose_name_plural = 'Categories'
unique_together = ['name', 'user_id']
unique_together = ['name', 'user']
def clean(self) -> None:
self.name = self.name.lower().strip()
@ -895,8 +474,8 @@ class Category(models.Model):
class Lodging(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
name = models.CharField(max_length=200)
type = models.CharField(max_length=100, choices=LODGING_TYPES, default='other')
description = models.TextField(blank=True, null=True)
@ -921,9 +500,9 @@ class Lodging(models.Model):
if self.collection:
if self.collection.is_public and not self.is_public:
raise ValidationError('Lodging associated with a public collection must be public. Collection: ' + self.collection.name + ' Loging: ' + self.name)
if self.user_id != self.collection.user_id:
raise ValidationError('Lodging must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Lodging owner: ' + self.user_id.username)
raise ValidationError('Lodging associated with a public collection must be public. Collection: ' + self.collection.name + ' Lodging: ' + self.name)
if self.user != self.collection.user:
raise ValidationError('Lodging must be associated with collections owned by the same user. Collection owner: ' + self.collection.user.username + ' Lodging owner: ' + self.user.username)
def __str__(self):
return self.name

View file

@ -7,8 +7,8 @@ class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
# obj.user_id is FK to User, compare with request.user
return obj.user_id == request.user
# obj.user is FK to User, compare with request.user
return obj.user == request.user
class IsPublicReadOnly(permissions.BasePermission):
@ -17,8 +17,8 @@ class IsPublicReadOnly(permissions.BasePermission):
"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return obj.is_public or obj.user_id == request.user
return obj.user_id == request.user
return obj.is_public or obj.user == request.user
return obj.user == request.user
class CollectionShared(permissions.BasePermission):
@ -39,7 +39,7 @@ class CollectionShared(permissions.BasePermission):
if obj.shared_with.filter(id=user.id).exists():
return True
# If obj is an Adventure (has collections M2M)
# If obj is a Location (has collections M2M)
if hasattr(obj, 'collections'):
# Check if user is in shared_with of any related collection
shared_collections = obj.collections.filter(shared_with=user)
@ -48,10 +48,10 @@ class CollectionShared(permissions.BasePermission):
# Read permission if public or owner
if request.method in permissions.SAFE_METHODS:
return obj.is_public or obj.user_id == user
return obj.is_public or obj.user == user
# Write permission only if owner or shared user via collections
if obj.user_id == user:
if obj.user == user:
return True
if hasattr(obj, 'collections'):
@ -76,7 +76,7 @@ class IsOwnerOrSharedWithFullAccess(permissions.BasePermission):
if request.method in permissions.SAFE_METHODS:
if obj.is_public:
return True
if obj.user_id == user:
if obj.user == user:
return True
# If user in shared_with of any collection related to obj
if hasattr(obj, 'collections') and obj.collections.filter(shared_with=user).exists():
@ -88,7 +88,7 @@ class IsOwnerOrSharedWithFullAccess(permissions.BasePermission):
return False
# For write methods, allow if owner or shared user
if obj.user_id == user:
if obj.user == user:
return True
if hasattr(obj, 'collections') and obj.collections.filter(shared_with=user).exists():
return True

View file

@ -1,6 +1,6 @@
from django.utils import timezone
import os
from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, Attachment, Lodging
from .models import Location, LocationImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, Attachment, Lodging
from rest_framework import serializers
from main.utils import CustomModelSerializer
from users.serializers import CustomUserDetailsSerializer
@ -9,17 +9,17 @@ from geopy.distance import geodesic
from integrations.models import ImmichIntegration
class AdventureImageSerializer(CustomModelSerializer):
class LocationImageSerializer(CustomModelSerializer):
class Meta:
model = AdventureImage
fields = ['id', 'image', 'adventure', 'is_primary', 'user_id', 'immich_id']
read_only_fields = ['id', 'user_id']
model = LocationImage
fields = ['id', 'image', 'location', 'is_primary', 'user', 'immich_id']
read_only_fields = ['id', 'user']
def to_representation(self, instance):
# If immich_id is set, check for user integration once
integration = None
if instance.immich_id:
integration = ImmichIntegration.objects.filter(user=instance.user_id).first()
integration = ImmichIntegration.objects.filter(user=instance.user).first()
if not integration:
return None # Skip if Immich image but no integration
@ -42,8 +42,8 @@ class AttachmentSerializer(CustomModelSerializer):
extension = serializers.SerializerMethodField()
class Meta:
model = Attachment
fields = ['id', 'file', 'adventure', 'extension', 'name', 'user_id']
read_only_fields = ['id', 'user_id']
fields = ['id', 'file', 'location', 'extension', 'name', 'user']
read_only_fields = ['id', 'user']
def get_extension(self, obj):
return obj.file.name.split('.')[-1]
@ -59,11 +59,11 @@ class AttachmentSerializer(CustomModelSerializer):
return representation
class CategorySerializer(serializers.ModelSerializer):
num_adventures = serializers.SerializerMethodField()
num_locations = serializers.SerializerMethodField()
class Meta:
model = Category
fields = ['id', 'name', 'display_name', 'icon', 'num_adventures']
read_only_fields = ['id', 'num_adventures']
fields = ['id', 'name', 'display_name', 'icon', 'num_locations']
read_only_fields = ['id', 'num_locations']
def validate_name(self, value):
return value.lower()
@ -71,7 +71,7 @@ class CategorySerializer(serializers.ModelSerializer):
def create(self, validated_data):
user = self.context['request'].user
validated_data['name'] = validated_data['name'].lower()
return Category.objects.create(user_id=user, **validated_data)
return Category.objects.create(user=user, **validated_data)
def update(self, instance, validated_data):
for attr, value in validated_data.items():
@ -81,8 +81,8 @@ class CategorySerializer(serializers.ModelSerializer):
instance.save()
return instance
def get_num_adventures(self, obj):
return Adventure.objects.filter(category=obj, user_id=obj.user_id).count()
def get_num_locations(self, obj):
return Location.objects.filter(category=obj, user=obj.user).count()
class VisitSerializer(serializers.ModelSerializer):
@ -91,13 +91,12 @@ class VisitSerializer(serializers.ModelSerializer):
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes']
read_only_fields = ['id']
class AdventureSerializer(CustomModelSerializer):
class LocationSerializer(CustomModelSerializer):
images = serializers.SerializerMethodField()
visits = VisitSerializer(many=True, read_only=False, required=False)
attachments = AttachmentSerializer(many=True, read_only=True)
category = CategorySerializer(read_only=False, required=False)
is_visited = serializers.SerializerMethodField()
user = serializers.SerializerMethodField()
country = CountrySerializer(read_only=True)
region = RegionSerializer(read_only=True)
city = CitySerializer(read_only=True)
@ -108,16 +107,22 @@ class AdventureSerializer(CustomModelSerializer):
)
class Meta:
model = Adventure
model = Location
fields = [
'id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location',
'id', 'name', 'description', 'rating', 'tags', 'location',
'is_public', 'collections', 'created_at', 'updated_at', 'images', 'link', 'longitude',
'latitude', 'visits', 'is_visited', 'category', 'attachments', 'user', 'city', 'country', 'region'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'is_visited', 'user']
read_only_fields = ['id', 'created_at', 'updated_at', 'user', 'is_visited']
# Makes it so the whole user object is returned in the serializer instead of just the user uuid
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['user'] = CustomUserDetailsSerializer(instance.user, context=self.context).data
return representation
def get_images(self, obj):
serializer = AdventureImageSerializer(obj.images.all(), many=True, context=self.context)
serializer = LocationImageSerializer(obj.images.all(), many=True, context=self.context)
# Filter out None values from the serialized data
return [image for image in serializer.data if image is not None]
@ -128,7 +133,7 @@ class AdventureSerializer(CustomModelSerializer):
user = self.context['request'].user
for collection in collections:
if collection.user_id != user:
if collection.user != user:
raise serializers.ValidationError(
f"Collection '{collection.name}' does not belong to the current user."
)
@ -140,7 +145,7 @@ class AdventureSerializer(CustomModelSerializer):
if category_data:
user = self.context['request'].user
name = category_data.get('name', '').lower()
existing_category = Category.objects.filter(user_id=user, name=name).first()
existing_category = Category.objects.filter(user=user, name=name).first()
if existing_category:
return existing_category
category_data['name'] = name
@ -162,7 +167,7 @@ class AdventureSerializer(CustomModelSerializer):
icon = category_data.icon
category, created = Category.objects.get_or_create(
user_id=user,
user=user,
name=name,
defaults={
'display_name': display_name,
@ -171,10 +176,6 @@ class AdventureSerializer(CustomModelSerializer):
)
return category
def get_user(self, obj):
user = obj.user_id
return CustomUserDetailsSerializer(user).data
def get_is_visited(self, obj):
return obj.is_visited_status()
@ -184,24 +185,24 @@ class AdventureSerializer(CustomModelSerializer):
collections_data = validated_data.pop('collections', [])
print(category_data)
adventure = Adventure.objects.create(**validated_data)
location = Location.objects.create(**validated_data)
# Handle visits
for visit_data in visits_data:
Visit.objects.create(adventure=adventure, **visit_data)
Visit.objects.create(location=location, **visit_data)
# Handle category
if category_data:
category = self.get_or_create_category(category_data)
adventure.category = category
location.category = category
# Handle collections - set after adventure is saved
# Handle collections - set after location is saved
if collections_data:
adventure.collections.set(collections_data)
location.collections.set(collections_data)
adventure.save()
location.save()
return adventure
return location
def update(self, instance, validated_data):
has_visits = 'visits' in validated_data
@ -214,9 +215,9 @@ class AdventureSerializer(CustomModelSerializer):
for attr, value in validated_data.items():
setattr(instance, attr, value)
# Handle category - ONLY allow the adventure owner to change categories
# Handle category - ONLY allow the location owner to change categories
user = self.context['request'].user
if category_data and instance.user_id == user:
if category_data and instance.user == user:
# Only the owner can set categories
category = self.get_or_create_category(category_data)
instance.category = category
@ -241,13 +242,13 @@ class AdventureSerializer(CustomModelSerializer):
visit.save()
updated_visit_ids.add(visit_id)
else:
new_visit = Visit.objects.create(adventure=instance, **visit_data)
new_visit = Visit.objects.create(location=instance, **visit_data)
updated_visit_ids.add(new_visit.id)
visits_to_delete = current_visit_ids - updated_visit_ids
instance.visits.filter(id__in=visits_to_delete).delete()
# call save on the adventure to update the updated_at field and trigger any geocoding
# call save on the location to update the updated_at field and trigger any geocoding
instance.save()
return instance
@ -258,13 +259,13 @@ class TransportationSerializer(CustomModelSerializer):
class Meta:
model = Transportation
fields = [
'id', 'user_id', 'type', 'name', 'description', 'rating',
'id', 'user', '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',
'start_timezone', 'end_timezone', 'distance' # ✅ Add distance here
'start_timezone', 'end_timezone', 'distance'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'distance']
read_only_fields = ['id', 'created_at', 'updated_at', 'user', 'distance']
def get_distance(self, obj):
if (
@ -284,29 +285,29 @@ class LodgingSerializer(CustomModelSerializer):
class Meta:
model = Lodging
fields = [
'id', 'user_id', 'name', 'description', 'rating', 'link', 'check_in', 'check_out',
'id', 'user', 'name', 'description', 'rating', 'link', 'check_in', 'check_out',
'reservation_number', 'price', 'latitude', 'longitude', 'location', 'is_public',
'collection', 'created_at', 'updated_at', 'type', 'timezone'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
read_only_fields = ['id', 'created_at', 'updated_at', 'user']
class NoteSerializer(CustomModelSerializer):
class Meta:
model = Note
fields = [
'id', 'user_id', 'name', 'content', 'date', 'links',
'id', 'user', 'name', 'content', 'date', 'links',
'is_public', 'collection', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
read_only_fields = ['id', 'created_at', 'updated_at', 'user']
class ChecklistItemSerializer(CustomModelSerializer):
class Meta:
model = ChecklistItem
fields = [
'id', 'user_id', 'name', 'is_checked', 'checklist', 'created_at', 'updated_at'
'id', 'user', 'name', 'is_checked', 'checklist', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'checklist']
read_only_fields = ['id', 'created_at', 'updated_at', 'user', 'checklist']
class ChecklistSerializer(CustomModelSerializer):
items = ChecklistItemSerializer(many=True, source='checklistitem_set')
@ -314,21 +315,21 @@ class ChecklistSerializer(CustomModelSerializer):
class Meta:
model = Checklist
fields = [
'id', 'user_id', 'name', 'date', 'is_public', 'collection', 'created_at', 'updated_at', 'items'
'id', 'user', 'name', 'date', 'is_public', 'collection', 'created_at', 'updated_at', 'items'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
read_only_fields = ['id', 'created_at', 'updated_at', 'user']
def create(self, validated_data):
items_data = validated_data.pop('checklistitem_set')
checklist = Checklist.objects.create(**validated_data)
for item_data in items_data:
# Remove user_id from item_data to avoid constraint issues
item_data.pop('user_id', None)
# Set user_id from the parent checklist
# Remove user from item_data to avoid constraint issues
item_data.pop('user', None)
# Set user from the parent checklist
ChecklistItem.objects.create(
checklist=checklist,
user_id=checklist.user_id,
user=checklist.user,
**item_data
)
return checklist
@ -348,8 +349,8 @@ class ChecklistSerializer(CustomModelSerializer):
# Update or create items
updated_item_ids = set()
for item_data in items_data:
# Remove user_id from item_data to avoid constraint issues
item_data.pop('user_id', None)
# Remove user from item_data to avoid constraint issues
item_data.pop('user', None)
item_id = item_data.get('id')
if item_id:
@ -363,14 +364,14 @@ class ChecklistSerializer(CustomModelSerializer):
# If ID is provided but doesn't exist, create new item
ChecklistItem.objects.create(
checklist=instance,
user_id=instance.user_id,
user=instance.user,
**item_data
)
else:
# If no ID is provided, create new item
ChecklistItem.objects.create(
checklist=instance,
user_id=instance.user_id,
user=instance.user,
**item_data
)
@ -391,7 +392,7 @@ class ChecklistSerializer(CustomModelSerializer):
return data
class CollectionSerializer(CustomModelSerializer):
adventures = AdventureSerializer(many=True, read_only=True)
locations = LocationSerializer(many=True, read_only=True)
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')
notes = NoteSerializer(many=True, read_only=True, source='note_set')
checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set')
@ -399,8 +400,8 @@ class CollectionSerializer(CustomModelSerializer):
class Meta:
model = Collection
fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link', 'lodging']
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
fields = ['id', 'description', 'user', 'name', 'is_public', 'locations', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link', 'lodging']
read_only_fields = ['id', 'created_at', 'updated_at', 'user']
def to_representation(self, instance):
representation = super().to_representation(instance)

View file

@ -1,8 +1,8 @@
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from adventures.models import Adventure
from adventures.models import Location
@receiver(m2m_changed, sender=Adventure.collections.through)
@receiver(m2m_changed, sender=Location.collections.through)
def update_adventure_publicity(sender, instance, action, **kwargs):
"""
Signal handler to update adventure publicity when collections are added/removed

View file

@ -3,11 +3,11 @@ from rest_framework.routers import DefaultRouter
from adventures.views import *
router = DefaultRouter()
router.register(r'adventures', AdventureViewSet, basename='adventures')
router.register(r'locations', LocationViewSet, basename='locations')
router.register(r'collections', CollectionViewSet, basename='collections')
router.register(r'stats', StatsViewSet, basename='stats')
router.register(r'generate', GenerateDescription, basename='generate')
router.register(r'activity-types', ActivityTypesView, basename='activity-types')
router.register(r'tags', ActivityTypesView, basename='tags')
router.register(r'transportations', TransportationViewSet, basename='transportations')
router.register(r'notes', NoteViewSet, basename='notes')
router.register(r'checklists', ChecklistViewSet, basename='checklists')

View file

@ -1,4 +1,4 @@
from adventures.models import AdventureImage, Attachment
from adventures.models import LocationImage, Attachment
protected_paths = ['images/', 'attachments/']
@ -10,20 +10,20 @@ def checkFilePermission(fileId, user, mediaType):
# Construct the full relative path to match the database field
image_path = f"images/{fileId}"
# Fetch the AdventureImage object
adventure = AdventureImage.objects.get(image=image_path).adventure
if adventure.is_public:
location = LocationImage.objects.get(image=image_path).location
if location.is_public:
return True
elif adventure.user_id == user:
elif location.user == user:
return True
elif adventure.collections.exists():
elif location.collections.exists():
# Check if the user is in any collection's shared_with list
for collection in adventure.collections.all():
for collection in location.collections.all():
if collection.shared_with.filter(id=user.id).exists():
return True
return False
else:
return False
except AdventureImage.DoesNotExist:
except LocationImage.DoesNotExist:
return False
elif mediaType == 'attachments/':
try:
@ -31,14 +31,14 @@ def checkFilePermission(fileId, user, mediaType):
attachment_path = f"attachments/{fileId}"
# Fetch the Attachment object
attachment = Attachment.objects.get(file=attachment_path)
adventure = attachment.adventure
if adventure.is_public:
location = attachment.location
if location.is_public:
return True
elif adventure.user_id == user:
elif location.user == user:
return True
elif adventure.collections.exists():
elif location.collections.exists():
# Check if the user is in any collection's shared_with list
for collection in adventure.collections.all():
for collection in location.collections.all():
if collection.shared_with.filter(id=user.id).exists():
return True
return False

View file

@ -0,0 +1,419 @@
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"
]

View file

@ -1,6 +1,6 @@
from .activity_types_view import *
from .adventure_image_view import *
from .adventure_view import *
from .tags_view import *
from .location_image_view import *
from .location_view import *
from .category_view import *
from .checklist_view import *
from .collection_view import *

View file

@ -2,7 +2,7 @@ from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from adventures.models import Adventure, Attachment
from adventures.models import Location, Attachment
from adventures.serializers import AttachmentSerializer
class AttachmentViewSet(viewsets.ModelViewSet):
@ -10,47 +10,43 @@ class AttachmentViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Attachment.objects.filter(user_id=self.request.user)
@action(detail=True, methods=['post'])
def attachment_delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
return Attachment.objects.filter(user=self.request.user)
def create(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
adventure_id = request.data.get('adventure')
location_id = request.data.get('location')
try:
adventure = Adventure.objects.get(id=adventure_id)
except Adventure.DoesNotExist:
return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND)
location = Location.objects.get(id=location_id)
except Location.DoesNotExist:
return Response({"error": "Location not found"}, status=status.HTTP_404_NOT_FOUND)
if adventure.user_id != request.user:
# Check if the adventure has any collections
if adventure.collections.exists():
# Check if the user is in the shared_with list of any of the adventure's collections
if location.user != request.user:
# Check if the location has any collections
if location.collections.exists():
# Check if the user is in the shared_with list of any of the location's collections
user_has_access = False
for collection in adventure.collections.all():
for collection in location.collections.all():
if collection.shared_with.filter(id=request.user.id).exists():
user_has_access = True
break
if not user_has_access:
return Response({"error": "User does not have permission to access this adventure"}, status=status.HTTP_403_FORBIDDEN)
return Response({"error": "User does not have permission to access this location"}, status=status.HTTP_403_FORBIDDEN)
else:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
return Response({"error": "User does not own this location"}, status=status.HTTP_403_FORBIDDEN)
return super().create(request, *args, **kwargs)
def perform_create(self, serializer):
adventure_id = self.request.data.get('adventure')
adventure = Adventure.objects.get(id=adventure_id)
location_id = self.request.data.get('location')
location = Location.objects.get(id=location_id)
# If the adventure belongs to collections, set the owner to the collection owner
if adventure.collections.exists():
# If the location belongs to collections, set the owner to the collection owner
if location.collections.exists():
# Get the first collection's owner (assuming all collections have the same owner)
collection = adventure.collections.first()
serializer.save(user_id=collection.user_id)
collection = location.collections.first()
serializer.save(user=collection.user)
else:
# Otherwise, set the owner to the request user
serializer.save(user_id=self.request.user)
serializer.save(user=self.request.user)

View file

@ -2,21 +2,19 @@ from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from adventures.models import Category, Adventure
from adventures.models import Category, Location
from adventures.serializers import CategorySerializer
class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Category.objects.filter(user_id=self.request.user)
return Category.objects.filter(user=self.request.user)
@action(detail=False, methods=['get'])
def categories(self, request):
def list(self, request, *args, **kwargs):
"""
Retrieve a list of distinct categories for adventures associated with the current user.
Retrieve a list of distinct categories for locations associated with the current user.
"""
categories = self.get_queryset().distinct()
serializer = self.get_serializer(categories, many=True)
@ -24,19 +22,19 @@ class CategoryViewSet(viewsets.ModelViewSet):
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
if instance.user_id != request.user:
if instance.user != request.user:
return Response({"error": "User does not own this category"}, status
=400)
if instance.name == 'general':
return Response({"error": "Cannot delete the general category"}, status=400)
# set any adventures with this category to a default category called general before deleting the category, if general does not exist create it for the user
general_category = Category.objects.filter(user_id=request.user, name='general').first()
# set any locations with this category to a default category called general before deleting the category, if general does not exist create it for the user
general_category = Category.objects.filter(user=request.user, name='general').first()
if not general_category:
general_category = Category.objects.create(user_id=request.user, name='general', icon='🌍', display_name='General')
general_category = Category.objects.create(user=request.user, name='general', icon='🌍', display_name='General')
Adventure.objects.filter(category=instance).update(category=general_category)
Location.objects.filter(category=instance).update(category=general_category)
return super().destroy(request, *args, **kwargs)

View file

@ -6,32 +6,22 @@ from adventures.models import Checklist
from adventures.serializers import ChecklistSerializer
from rest_framework.exceptions import PermissionDenied
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from rest_framework.permissions import IsAuthenticated
class ChecklistViewSet(viewsets.ModelViewSet):
queryset = Checklist.objects.all()
serializer_class = ChecklistSerializer
permission_classes = [IsOwnerOrSharedWithFullAccess]
permission_classes = [IsAuthenticated, IsOwnerOrSharedWithFullAccess]
filterset_fields = ['is_public', 'collection']
# return error message if user is not authenticated on the root endpoint
def list(self, request, *args, **kwargs):
# Prevent listing all adventures
return Response({"detail": "Listing all checklists is not allowed."},
status=status.HTTP_403_FORBIDDEN)
@action(detail=False, methods=['get'])
def all(self, request):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
queryset = Checklist.objects.filter(
Q(user_id=request.user.id)
Q(user=request.user)
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def get_queryset(self):
# if the user is not authenticated return only public transportations for retrieve action
# if the user is not authenticated return only public checklists for retrieve action
if not self.request.user.is_authenticated:
if self.action == 'retrieve':
return Checklist.objects.filter(is_public=True).distinct().order_by('-updated_at')
@ -39,14 +29,14 @@ class ChecklistViewSet(viewsets.ModelViewSet):
if self.action == 'retrieve':
# For individual adventure retrieval, include public adventures
# For individual adventure retrieval, include public locations
return Checklist.objects.filter(
Q(is_public=True) | Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user)
Q(is_public=True) | Q(user=self.request.user) | Q(collection__shared_with=self.request.user)
).distinct().order_by('-updated_at')
else:
# For other actions, include user's own adventures and shared adventures
# For other actions, include user's own locations and shared locations
return Checklist.objects.filter(
Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user)
Q(user=self.request.user) | Q(collection__shared_with=self.request.user)
).distinct().order_by('-updated_at')
def partial_update(self, request, *args, **kwargs):
@ -65,11 +55,11 @@ class ChecklistViewSet(viewsets.ModelViewSet):
if new_collection is not None and new_collection!=instance.collection:
# Check if the user is the owner of the new collection
if new_collection.user_id != user or instance.user_id != user:
if new_collection.user != user or instance.user != user:
raise PermissionDenied("You do not have permission to use this collection.")
elif new_collection is None:
# Handle the case where the user is trying to set the collection to None
if instance.collection is not None and instance.collection.user_id != user:
if instance.collection is not None and instance.collection.user != user:
raise PermissionDenied("You cannot remove the collection as you are not the owner.")
# Perform the update
@ -94,11 +84,11 @@ class ChecklistViewSet(viewsets.ModelViewSet):
if new_collection is not None and new_collection!=instance.collection:
# Check if the user is the owner of the new collection
if new_collection.user_id != user or instance.user_id != user:
if new_collection.user != user or instance.user != user:
raise PermissionDenied("You do not have permission to use this collection.")
elif new_collection is None:
# Handle the case where the user is trying to set the collection to None
if instance.collection is not None and instance.collection.user_id != user:
if instance.collection is not None and instance.collection.user != user:
raise PermissionDenied("You cannot remove the collection as you are not the owner.")
# Perform the update
@ -119,12 +109,12 @@ class ChecklistViewSet(viewsets.ModelViewSet):
if collection:
user = self.request.user
# Check if the user is the owner or is in the shared_with list
if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists():
if collection.user != user and not collection.shared_with.filter(id=user.id).exists():
# Return an error response if the user does not have permission
raise PermissionDenied("You do not have permission to use this collection.")
# if collection the owner of the adventure is the owner of the collection
serializer.save(user_id=collection.user_id)
serializer.save(user=collection.user)
return
# Save the adventure with the current user as the owner
serializer.save(user_id=self.request.user)
serializer.save(user=self.request.user)

View file

@ -4,7 +4,7 @@ from django.db import transaction
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from adventures.models import Collection, Adventure, Transportation, Note, Checklist
from adventures.models import Collection, Location, Transportation, Note, Checklist
from adventures.permissions import CollectionShared
from adventures.serializers import CollectionSerializer
from users.models import CustomUser as User
@ -15,8 +15,6 @@ class CollectionViewSet(viewsets.ModelViewSet):
permission_classes = [CollectionShared]
pagination_class = pagination.StandardResultsSetPagination
# def get_queryset(self):
# return Collection.objects.filter(Q(user_id=self.request.user.id) & Q(is_archived=False))
def apply_sorting(self, queryset):
order_by = self.request.query_params.get('order_by', 'name')
@ -47,15 +45,13 @@ class CollectionViewSet(viewsets.ModelViewSet):
if order_direction == 'asc':
ordering = '-updated_at'
#print(f"Ordering by: {ordering}") # For debugging
return queryset.order_by(ordering)
def list(self, request, *args, **kwargs):
# make sure the user is authenticated
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
queryset = Collection.objects.filter(user_id=request.user.id, is_archived=False)
queryset = Collection.objects.filter(user=request.user, is_archived=False)
queryset = self.apply_sorting(queryset)
collections = self.paginate_and_respond(queryset, request)
return collections
@ -66,7 +62,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
return Response({"error": "User is not authenticated"}, status=400)
queryset = Collection.objects.filter(
Q(user_id=request.user.id)
Q(user=request.user)
)
queryset = self.apply_sorting(queryset)
@ -80,7 +76,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
return Response({"error": "User is not authenticated"}, status=400)
queryset = Collection.objects.filter(
Q(user_id=request.user.id) & Q(is_archived=True)
Q(user=request.user.id) & Q(is_archived=True)
)
queryset = self.apply_sorting(queryset)
@ -88,7 +84,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
return Response(serializer.data)
# this make the is_public field of the collection cascade to the adventures
# this make the is_public field of the collection cascade to the locations
@transaction.atomic
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
@ -99,7 +95,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
if 'collection' in serializer.validated_data:
new_collection = serializer.validated_data['collection']
# if the new collection is different from the old one and the user making the request is not the owner of the new collection return an error
if new_collection != instance.collection and new_collection.user_id != request.user:
if new_collection != instance.collection and new_collection.user != request.user:
return Response({"error": "User does not own the new collection"}, status=400)
# Check if the 'is_public' field is present in the update data
@ -107,29 +103,29 @@ class CollectionViewSet(viewsets.ModelViewSet):
new_public_status = serializer.validated_data['is_public']
# if is_public has changed and the user is not the owner of the collection return an error
if new_public_status != instance.is_public and instance.user_id != request.user:
print(f"User {request.user.id} does not own the collection {instance.id} that is owned by {instance.user_id}")
if new_public_status != instance.is_public and instance.user != request.user:
print(f"User {request.user.id} does not own the collection {instance.id} that is owned by {instance.user}")
return Response({"error": "User does not own the collection"}, status=400)
# Get all adventures in this collection
adventures_in_collection = Adventure.objects.filter(collections=instance)
# Get all locations in this collection
locations_in_collection = Location.objects.filter(collections=instance)
if new_public_status:
# If collection becomes public, make all adventures public
adventures_in_collection.update(is_public=True)
# If collection becomes public, make all locations public
locations_in_collection.update(is_public=True)
else:
# If collection becomes private, check each adventure
# Only set an adventure to private if ALL of its collections are private
# Collect adventures that do NOT belong to any other public collection (excluding the current one)
adventure_ids_to_set_private = []
# If collection becomes private, check each location
# Only set a location to private if ALL of its collections are private
# Collect locations that do NOT belong to any other public collection (excluding the current one)
location_ids_to_set_private = []
for adventure in adventures_in_collection:
has_public_collection = adventure.collections.filter(is_public=True).exclude(id=instance.id).exists()
for location in locations_in_collection:
has_public_collection = location.collections.filter(is_public=True).exclude(id=instance.id).exists()
if not has_public_collection:
adventure_ids_to_set_private.append(adventure.id)
location_ids_to_set_private.append(location.id)
# Bulk update those adventures
Adventure.objects.filter(id__in=adventure_ids_to_set_private).update(is_public=False)
# Bulk update those locations
Location.objects.filter(id__in=location_ids_to_set_private).update(is_public=False)
# Update transportations, notes, and checklists related to this collection
# These still use direct ForeignKey relationships
@ -150,7 +146,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
return Response(serializer.data)
# make an action to retreive all adventures that are shared with the user
# make an action to retreive all locations that are shared with the user
@action(detail=False, methods=['get'])
def shared(self, request):
if not request.user.is_authenticated:
@ -162,7 +158,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
# Adds a new user to the shared_with field of an adventure
# Adds a new user to the shared_with field of a location
@action(detail=True, methods=['post'], url_path='share/(?P<uuid>[^/.]+)')
def share(self, request, pk=None, uuid=None):
collection = self.get_object()
@ -177,7 +173,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
return Response({"error": "Cannot share with yourself"}, status=400)
if collection.shared_with.filter(id=user.id).exists():
return Response({"error": "Adventure is already shared with this user"}, status=400)
return Response({"error": "Location is already shared with this user"}, status=400)
collection.shared_with.add(user)
collection.save()
@ -207,28 +203,28 @@ class CollectionViewSet(viewsets.ModelViewSet):
def get_queryset(self):
if self.action == 'destroy':
return Collection.objects.filter(user_id=self.request.user.id)
return Collection.objects.filter(user=self.request.user.id)
if self.action in ['update', 'partial_update']:
return Collection.objects.filter(
Q(user_id=self.request.user.id) | Q(shared_with=self.request.user)
Q(user=self.request.user.id) | Q(shared_with=self.request.user)
).distinct()
if self.action == 'retrieve':
if not self.request.user.is_authenticated:
return Collection.objects.filter(is_public=True)
return Collection.objects.filter(
Q(is_public=True) | Q(user_id=self.request.user.id) | Q(shared_with=self.request.user)
Q(is_public=True) | Q(user=self.request.user.id) | Q(shared_with=self.request.user)
).distinct()
# For list action, include collections owned by the user or shared with the user, that are not archived
return Collection.objects.filter(
(Q(user_id=self.request.user.id) | Q(shared_with=self.request.user)) & Q(is_archived=False)
(Q(user=self.request.user.id) | Q(shared_with=self.request.user)) & Q(is_archived=False)
).distinct()
def perform_create(self, serializer):
# This is ok because you cannot share a collection when creating it
serializer.save(user_id=self.request.user)
serializer.save(user=self.request.user)
def paginate_and_respond(self, queryset, request):
paginator = self.pagination_class()

View file

@ -3,8 +3,8 @@ from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.db.models import Q
from django.contrib.postgres.search import SearchVector, SearchQuery
from adventures.models import Adventure, Collection
from adventures.serializers import AdventureSerializer, CollectionSerializer
from adventures.models import Location, Collection
from adventures.serializers import LocationSerializer, CollectionSerializer
from worldtravel.models import Country, Region, City, VisitedCity, VisitedRegion
from worldtravel.serializers import CountrySerializer, RegionSerializer, CitySerializer, VisitedCitySerializer, VisitedRegionSerializer
from users.models import CustomUser as User
@ -20,7 +20,7 @@ class GlobalSearchView(viewsets.ViewSet):
# Initialize empty results
results = {
"adventures": [],
"locations": [],
"collections": [],
"users": [],
"countries": [],
@ -30,15 +30,15 @@ class GlobalSearchView(viewsets.ViewSet):
"visited_cities": []
}
# Adventures: Full-Text Search
adventures = Adventure.objects.annotate(
# Locations: Full-Text Search
locations = Location.objects.annotate(
search=SearchVector('name', 'description', 'location')
).filter(search=SearchQuery(search_term), user_id=request.user)
results["adventures"] = AdventureSerializer(adventures, many=True).data
).filter(search=SearchQuery(search_term), user=request.user)
results["locations"] = LocationSerializer(locations, many=True).data
# Collections: Partial Match Search
collections = Collection.objects.filter(
Q(name__icontains=search_term) & Q(user_id=request.user)
Q(name__icontains=search_term) & Q(user=request.user)
)
results["collections"] = CollectionSerializer(collections, many=True).data
@ -64,10 +64,10 @@ class GlobalSearchView(viewsets.ViewSet):
results["cities"] = CitySerializer(cities, many=True).data
# Visited Regions and Cities
visited_regions = VisitedRegion.objects.filter(user_id=request.user)
visited_regions = VisitedRegion.objects.filter(user=request.user)
results["visited_regions"] = VisitedRegionSerializer(visited_regions, many=True).data
visited_cities = VisitedCity.objects.filter(user_id=request.user)
visited_cities = VisitedCity.objects.filter(user=request.user)
results["visited_cities"] = VisitedCitySerializer(visited_cities, many=True).data
return Response(results)

View file

@ -4,27 +4,26 @@ from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from icalendar import Calendar, Event, vText, vCalAddress
from datetime import datetime, timedelta
from adventures.models import Adventure
from adventures.serializers import AdventureSerializer
from adventures.models import Location
from adventures.serializers import LocationSerializer
class IcsCalendarGeneratorViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def generate(self, request):
adventures = Adventure.objects.filter(user_id=request.user)
serializer = AdventureSerializer(adventures, many=True)
locations = Location.objects.filter(user=request.user)
serializer = LocationSerializer(locations, many=True)
user = request.user
name = f"{user.first_name} {user.last_name}"
print(serializer.data)
cal = Calendar()
cal.add('prodid', '-//My Adventure Calendar//example.com//')
cal.add('version', '2.0')
for adventure in serializer.data:
if adventure['visits']:
for visit in adventure['visits']:
for location in serializer.data:
if location['visits']:
for visit in location['visits']:
# Skip if start_date is missing
if not visit.get('start_date'):
continue
@ -42,7 +41,7 @@ class IcsCalendarGeneratorViewSet(viewsets.ViewSet):
# Create event
event = Event()
event.add('summary', adventure['name'])
event.add('summary', location['name'])
event.add('dtstart', start_date)
event.add('dtend', end_date)
event.add('dtstamp', datetime.now())
@ -50,11 +49,11 @@ class IcsCalendarGeneratorViewSet(viewsets.ViewSet):
event.add('class', 'PUBLIC')
event.add('created', datetime.now())
event.add('last-modified', datetime.now())
event.add('description', adventure['description'])
if adventure.get('location'):
event.add('location', adventure['location'])
if adventure.get('link'):
event.add('url', adventure['link'])
event.add('description', location['description'])
if location.get('location'):
event.add('location', location['location'])
if location.get('link'):
event.add('url', location['link'])
organizer = vCalAddress(f'MAILTO:{user.email}')
organizer.params['cn'] = vText(name)

View file

@ -4,15 +4,14 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.db.models import Q
from django.core.files.base import ContentFile
from adventures.models import Adventure, AdventureImage
from adventures.serializers import AdventureImageSerializer
from adventures.models import Location, LocationImage
from adventures.serializers import LocationImageSerializer
from integrations.models import ImmichIntegration
import uuid
import requests
import os
class AdventureImageViewSet(viewsets.ModelViewSet):
serializer_class = AdventureImageSerializer
serializer_class = LocationImageSerializer
permission_classes = [IsAuthenticated]
@action(detail=True, methods=['post'])
@ -21,21 +20,21 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post'])
def toggle_primary(self, request, *args, **kwargs):
# Makes the image the primary image for the adventure, if there is already a primary image linked to the adventure, it is set to false and the new image is set to true. make sure that the permission is set to the owner of the adventure
# Makes the image the primary image for the location, if there is already a primary image linked to the location, it is set to false and the new image is set to true. make sure that the permission is set to the owner of the location
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
instance = self.get_object()
adventure = instance.adventure
if adventure.user_id != request.user:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
location = instance.location
if location.user != request.user:
return Response({"error": "User does not own this location"}, status=status.HTTP_403_FORBIDDEN)
# Check if the image is already the primary image
if instance.is_primary:
return Response({"error": "Image is already the primary image"}, status=status.HTTP_400_BAD_REQUEST)
# Set the current primary image to false
AdventureImage.objects.filter(adventure=adventure, is_primary=True).update(is_primary=False)
LocationImage.objects.filter(location=location, is_primary=True).update(is_primary=False)
# Set the new image to true
instance.is_primary = True
@ -45,29 +44,29 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
def create(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
adventure_id = request.data.get('adventure')
location_id = request.data.get('location')
try:
adventure = Adventure.objects.get(id=adventure_id)
except Adventure.DoesNotExist:
return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND)
location = Location.objects.get(id=location_id)
except Location.DoesNotExist:
return Response({"error": "location not found"}, status=status.HTTP_404_NOT_FOUND)
if adventure.user_id != request.user:
# Check if the adventure has any collections
if adventure.collections.exists():
# Check if the user is in the shared_with list of any of the adventure's collections
if location.user != request.user:
# Check if the location has any collections
if location.collections.exists():
# Check if the user is in the shared_with list of any of the location's collections
user_has_access = False
for collection in adventure.collections.all():
for collection in location.collections.all():
if collection.shared_with.filter(id=request.user.id).exists():
user_has_access = True
break
if not user_has_access:
return Response({"error": "User does not have permission to access this adventure"}, status=status.HTTP_403_FORBIDDEN)
return Response({"error": "User does not have permission to access this location"}, status=status.HTTP_403_FORBIDDEN)
else:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
return Response({"error": "User does not own this location"}, status=status.HTTP_403_FORBIDDEN)
# Handle Immich ID for shared users by downloading the image
if (request.user != adventure.user_id and
if (request.user != location.user and
'immich_id' in request.data and
request.data.get('immich_id')):
@ -75,7 +74,7 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
# Get the shared user's Immich integration
try:
user_integration = ImmichIntegration.objects.get(user_id=request.user)
user_integration = ImmichIntegration.objects.get(user=request.user)
except ImmichIntegration.DoesNotExist:
return Response({
"error": "No Immich integration found for your account. Please set up Immich integration first.",
@ -122,8 +121,8 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
serializer.is_valid(raise_exception=True)
# Save with the downloaded image
adventure = serializer.validated_data['adventure']
serializer.save(user_id=adventure.user_id, image=image_file)
location = serializer.validated_data['location']
serializer.save(user=location.user, image=image_file)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@ -144,14 +143,14 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
adventure_id = request.data.get('adventure')
location_id = request.data.get('location')
try:
adventure = Adventure.objects.get(id=adventure_id)
except Adventure.DoesNotExist:
return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND)
location = Location.objects.get(id=location_id)
except Location.DoesNotExist:
return Response({"error": "location not found"}, status=status.HTTP_404_NOT_FOUND)
if adventure.user_id != request.user:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
if location.user != request.user:
return Response({"error": "User does not own this location"}, status=status.HTTP_403_FORBIDDEN)
return super().update(request, *args, **kwargs)
@ -159,14 +158,13 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
return super().perform_destroy(instance)
def destroy(self, request, *args, **kwargs):
print("destroy")
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
instance = self.get_object()
adventure = instance.adventure
if adventure.user_id != request.user:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
location = instance.location
if location.user != request.user:
return Response({"error": "User does not own this location"}, status=status.HTTP_403_FORBIDDEN)
return super().destroy(request, *args, **kwargs)
@ -175,27 +173,27 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
instance = self.get_object()
adventure = instance.adventure
if adventure.user_id != request.user:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
location = instance.location
if location.user != request.user:
return Response({"error": "User does not own this location"}, status=status.HTTP_403_FORBIDDEN)
return super().partial_update(request, *args, **kwargs)
@action(detail=False, methods=['GET'], url_path='(?P<adventure_id>[0-9a-f-]+)')
def adventure_images(self, request, adventure_id=None, *args, **kwargs):
@action(detail=False, methods=['GET'], url_path='(?P<location_id>[0-9a-f-]+)')
def location_images(self, request, location_id=None, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
try:
adventure_uuid = uuid.UUID(adventure_id)
location_uuid = uuid.UUID(location_id)
except ValueError:
return Response({"error": "Invalid adventure ID"}, status=status.HTTP_400_BAD_REQUEST)
return Response({"error": "Invalid location ID"}, status=status.HTTP_400_BAD_REQUEST)
# Updated queryset to include images from adventures the user owns OR has shared access to
queryset = AdventureImage.objects.filter(
Q(adventure__id=adventure_uuid) & (
Q(adventure__user_id=request.user) | # User owns the adventure
Q(adventure__collections__shared_with=request.user) # User has shared access via collection
# Updated queryset to include images from locations the user owns OR has shared access to
queryset = LocationImage.objects.filter(
Q(location__id=location_uuid) & (
Q(location__user=request.user) | # User owns the location
Q(location__collections__shared_with=request.user) # User has shared access via collection
)
).distinct()
@ -203,13 +201,13 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
return Response(serializer.data)
def get_queryset(self):
# Updated to include images from adventures the user owns OR has shared access to
return AdventureImage.objects.filter(
Q(adventure__user_id=self.request.user) | # User owns the adventure
Q(adventure__collections__shared_with=self.request.user) # User has shared access via collection
# Updated to include images from locations the user owns OR has shared access to
return LocationImage.objects.filter(
Q(location__user=self.request.user) | # User owns the location
Q(location__collections__shared_with=self.request.user) # User has shared access via collection
).distinct()
def perform_create(self, serializer):
# Always set the image owner to the adventure owner, not the current user
adventure = serializer.validated_data['adventure']
serializer.save(user_id=adventure.user_id)
# Always set the image owner to the location owner, not the current user
location = serializer.validated_data['location']
serializer.save(user=location.user)

View file

@ -7,19 +7,18 @@ from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
import requests
from adventures.models import Adventure, Category, Transportation, Lodging
from adventures.models import Location, Category
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from adventures.serializers import AdventureSerializer, TransportationSerializer, LodgingSerializer
from adventures.serializers import LocationSerializer
from adventures.utils import pagination
class AdventureViewSet(viewsets.ModelViewSet):
class LocationViewSet(viewsets.ModelViewSet):
"""
ViewSet for managing Adventure objects with support for filtering, sorting,
and sharing functionality.
"""
serializer_class = AdventureSerializer
serializer_class = LocationSerializer
permission_classes = [IsOwnerOrSharedWithFullAccess]
pagination_class = pagination.StandardResultsSetPagination
@ -28,20 +27,20 @@ class AdventureViewSet(viewsets.ModelViewSet):
def get_queryset(self):
"""
Returns queryset based on user authentication and action type.
Public actions allow unauthenticated access to public adventures.
Public actions allow unauthenticated access to public locations.
"""
user = self.request.user
public_allowed_actions = {'retrieve', 'additional_info'}
if not user.is_authenticated:
if self.action in public_allowed_actions:
return Adventure.objects.retrieve_adventures(
return Location.objects.retrieve_locations(
user, include_public=True
).order_by('-updated_at')
return Adventure.objects.none()
return Location.objects.none()
include_public = self.action in public_allowed_actions
return Adventure.objects.retrieve_adventures(
return Location.objects.retrieve_locations(
user,
include_public=include_public,
include_owned=True,
@ -67,7 +66,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
# Apply sorting logic
queryset = self._apply_ordering(queryset, order_by, order_direction)
# Filter adventures without collections if requested
# Filter locations without collections if requested
if include_collections == 'false':
queryset = queryset.filter(collections__isnull=True)
@ -116,7 +115,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
# Use the current user as owner since ManyToMany allows multiple collection owners
user_to_assign = self.request.user
serializer.save(user_id=user_to_assign)
serializer.save(user=user_to_assign)
def perform_update(self, serializer):
"""Update adventure."""
@ -147,18 +146,18 @@ class AdventureViewSet(viewsets.ModelViewSet):
@action(detail=False, methods=['get'])
def filtered(self, request):
"""Filter adventures by category types and visit status."""
"""Filter locations by category types and visit status."""
types = request.query_params.get('types', '').split(',')
# Handle 'all' types
if 'all' in types:
types = Category.objects.filter(
user_id=request.user
user=request.user
).values_list('name', flat=True)
else:
# Validate provided types
if not types or not all(
Category.objects.filter(user_id=request.user, name=type_name).exists()
Category.objects.filter(user=request.user, name=type_name).exists()
for type_name in types
):
return Response(
@ -167,9 +166,9 @@ class AdventureViewSet(viewsets.ModelViewSet):
)
# Build base queryset
queryset = Adventure.objects.filter(
category__in=Category.objects.filter(name__in=types, user_id=request.user),
user_id=request.user.id
queryset = Location.objects.filter(
category__in=Category.objects.filter(name__in=types, user=request.user),
user=request.user.id
)
# Apply visit status filtering
@ -180,19 +179,19 @@ class AdventureViewSet(viewsets.ModelViewSet):
@action(detail=False, methods=['get'])
def all(self, request):
"""Get all adventures (public and owned) with optional collection filtering."""
"""Get all locations (public and owned) with optional collection filtering."""
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
include_collections = request.query_params.get('include_collections', 'false') == 'true'
# Build queryset with collection filtering
base_filter = Q(user_id=request.user.id)
base_filter = Q(user=request.user.id)
if include_collections:
queryset = Adventure.objects.filter(base_filter)
queryset = Location.objects.filter(base_filter)
else:
queryset = Adventure.objects.filter(base_filter, collections__isnull=True)
queryset = Location.objects.filter(base_filter, collections__isnull=True)
queryset = self.apply_sorting(queryset)
serializer = self.get_serializer(queryset, many=True)
@ -225,7 +224,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
def _validate_collection_permissions(self, collections):
"""Validate user has permission to use all provided collections. Only the owner or shared users can use collections."""
for collection in collections:
if not (collection.user_id == self.request.user or
if not (collection.user == self.request.user or
collection.shared_with.filter(uuid=self.request.user.uuid).exists()):
raise PermissionDenied(
f"You do not have permission to use collection '{collection.name}'."
@ -235,7 +234,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
"""Validate permissions for collection updates (add/remove)."""
# Check permissions for new collections being added
for collection in new_collections:
if (collection.user_id != self.request.user and
if (collection.user != self.request.user and
not collection.shared_with.filter(uuid=self.request.user.uuid).exists()):
raise PermissionDenied(
f"You do not have permission to use collection '{collection.name}'."
@ -247,7 +246,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
collections_to_remove = current_collections - new_collections_set
for collection in collections_to_remove:
if (collection.user_id != self.request.user and
if (collection.user != self.request.user and
not collection.shared_with.filter(uuid=self.request.user.uuid).exists()):
raise PermissionDenied(
f"You cannot remove the adventure from collection '{collection.name}' "
@ -284,7 +283,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
return True
# Check ownership
if user.is_authenticated and adventure.user_id == user:
if user.is_authenticated and adventure.user == user:
return True
# Check shared collection access

View file

@ -17,7 +17,7 @@ class LodgingViewSet(viewsets.ModelViewSet):
if not request.user.is_authenticated:
return Response(status=status.HTTP_403_FORBIDDEN)
queryset = Lodging.objects.filter(
Q(user_id=request.user.id)
Q(user=request.user.id)
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@ -25,13 +25,13 @@ class LodgingViewSet(viewsets.ModelViewSet):
def get_queryset(self):
user = self.request.user
if self.action == 'retrieve':
# For individual adventure retrieval, include public adventures, user's own adventures and shared adventures
# For individual adventure retrieval, include public locations, user's own locations and shared locations
return Lodging.objects.filter(
Q(is_public=True) | Q(user_id=user.id) | Q(collection__shared_with=user.id)
Q(is_public=True) | Q(user=user.id) | Q(collection__shared_with=user.id)
).distinct().order_by('-updated_at')
# For other actions, include user's own adventures and shared adventures
# For other actions, include user's own locations and shared locations
return Lodging.objects.filter(
Q(user_id=user.id) | Q(collection__shared_with=user.id)
Q(user=user.id) | Q(collection__shared_with=user.id)
).distinct().order_by('-updated_at')
def partial_update(self, request, *args, **kwargs):
@ -48,11 +48,11 @@ class LodgingViewSet(viewsets.ModelViewSet):
if new_collection is not None and new_collection != instance.collection:
# Check if the user is the owner of the new collection
if new_collection.user_id != user or instance.user_id != user:
if new_collection.user != user or instance.user != user:
raise PermissionDenied("You do not have permission to use this collection.")
elif new_collection is None:
# Handle the case where the user is trying to set the collection to None
if instance.collection is not None and instance.collection.user_id != user:
if instance.collection is not None and instance.collection.user != user:
raise PermissionDenied("You cannot remove the collection as you are not the owner.")
# Perform the update
@ -73,12 +73,12 @@ class LodgingViewSet(viewsets.ModelViewSet):
if collection:
user = self.request.user
# Check if the user is the owner or is in the shared_with list
if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists():
if collection.user != user and not collection.shared_with.filter(id=user.id).exists():
# Return an error response if the user does not have permission
raise PermissionDenied("You do not have permission to use this collection.")
# if collection the owner of the adventure is the owner of the collection
serializer.save(user_id=collection.user_id)
serializer.save(user=collection.user)
return
# Save the adventure with the current user as the owner
serializer.save(user_id=self.request.user)
serializer.save(user=self.request.user)

View file

@ -15,7 +15,7 @@ class NoteViewSet(viewsets.ModelViewSet):
# return error message if user is not authenticated on the root endpoint
def list(self, request, *args, **kwargs):
# Prevent listing all adventures
# Prevent listing all locations
return Response({"detail": "Listing all notes is not allowed."},
status=status.HTTP_403_FORBIDDEN)
@ -24,7 +24,7 @@ class NoteViewSet(viewsets.ModelViewSet):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
queryset = Note.objects.filter(
Q(user_id=request.user.id)
Q(user=request.user.id)
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@ -39,14 +39,14 @@ class NoteViewSet(viewsets.ModelViewSet):
if self.action == 'retrieve':
# For individual adventure retrieval, include public adventures
# For individual adventure retrieval, include public locations
return Note.objects.filter(
Q(is_public=True) | Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user)
Q(is_public=True) | Q(user=self.request.user.id) | Q(collection__shared_with=self.request.user)
).distinct().order_by('-updated_at')
else:
# For other actions, include user's own adventures and shared adventures
# For other actions, include user's own locations and shared locations
return Note.objects.filter(
Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user)
Q(user=self.request.user.id) | Q(collection__shared_with=self.request.user)
).distinct().order_by('-updated_at')
def partial_update(self, request, *args, **kwargs):
@ -65,11 +65,11 @@ class NoteViewSet(viewsets.ModelViewSet):
if new_collection is not None and new_collection!=instance.collection:
# Check if the user is the owner of the new collection
if new_collection.user_id != user or instance.user_id != user:
if new_collection.user != user or instance.user != user:
raise PermissionDenied("You do not have permission to use this collection.")
elif new_collection is None:
# Handle the case where the user is trying to set the collection to None
if instance.collection is not None and instance.collection.user_id != user:
if instance.collection is not None and instance.collection.user != user:
raise PermissionDenied("You cannot remove the collection as you are not the owner.")
# Perform the update
@ -94,11 +94,11 @@ class NoteViewSet(viewsets.ModelViewSet):
if new_collection is not None and new_collection!=instance.collection:
# Check if the user is the owner of the new collection
if new_collection.user_id != user or instance.user_id != user:
if new_collection.user != user or instance.user != user:
raise PermissionDenied("You do not have permission to use this collection.")
elif new_collection is None:
# Handle the case where the user is trying to set the collection to None
if instance.collection is not None and instance.collection.user_id != user:
if instance.collection is not None and instance.collection.user != user:
raise PermissionDenied("You cannot remove the collection as you are not the owner.")
# Perform the update
@ -119,12 +119,12 @@ class NoteViewSet(viewsets.ModelViewSet):
if collection:
user = self.request.user
# Check if the user is the owner or is in the shared_with list
if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists():
if collection.user != user and not collection.shared_with.filter(id=user.id).exists():
# Return an error response if the user does not have permission
raise PermissionDenied("You do not have permission to use this collection.")
# if collection the owner of the adventure is the owner of the collection
serializer.save(user_id=collection.user_id)
serializer.save(user=collection.user)
return
# Save the adventure with the current user as the owner
serializer.save(user_id=self.request.user)
serializer.save(user=self.request.user)

View file

@ -5,8 +5,6 @@ from rest_framework.response import Response
from django.conf import settings
import requests
from geopy.distance import geodesic
import time
class RecommendationsViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
@ -14,7 +12,7 @@ class RecommendationsViewSet(viewsets.ViewSet):
HEADERS = {'User-Agent': 'AdventureLog Server'}
def parse_google_places(self, places, origin):
adventures = []
locations = []
for place in places:
location = place.get('location', {})
@ -45,16 +43,16 @@ class RecommendationsViewSet(viewsets.ViewSet):
"distance_km": round(distance_km, 2),
}
adventures.append(adventure)
locations.append(adventure)
# Sort by distance ascending
adventures.sort(key=lambda x: x["distance_km"])
locations.sort(key=lambda x: x["distance_km"])
return adventures
return locations
def parse_overpass_response(self, data, request):
nodes = data.get('elements', [])
adventures = []
locations = []
all = request.query_params.get('all', False)
origin = None
@ -102,13 +100,13 @@ class RecommendationsViewSet(viewsets.ViewSet):
"powered_by": "osm"
}
adventures.append(adventure)
locations.append(adventure)
# Sort by distance if available
if origin:
adventures.sort(key=lambda x: x.get("distance_km") or float("inf"))
locations.sort(key=lambda x: x.get("distance_km") or float("inf"))
return adventures
return locations
def query_overpass(self, lat, lon, radius, category, request):
@ -172,8 +170,8 @@ class RecommendationsViewSet(viewsets.ViewSet):
print("Overpass API error:", e)
return Response({"error": "Failed to retrieve data from Overpass API."}, status=500)
adventures = self.parse_overpass_response(data, request)
return Response(adventures)
locations = self.parse_overpass_response(data, request)
return Response(locations)
def query_google_nearby(self, lat, lon, radius, category, request):
"""Query Google Places API (New) for nearby places"""
@ -216,9 +214,9 @@ class RecommendationsViewSet(viewsets.ViewSet):
places = data.get('places', [])
origin = (float(lat), float(lon))
adventures = self.parse_google_places(places, origin)
locations = self.parse_google_places(places, origin)
return Response(adventures)
return Response(locations)
except requests.exceptions.RequestException as e:
print(f"Google Places API error: {e}")

View file

@ -3,11 +3,9 @@ from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from worldtravel.models import Region, City, VisitedRegion, VisitedCity
from adventures.models import Adventure
from adventures.serializers import AdventureSerializer
import requests
from adventures.models import Location
from adventures.serializers import LocationSerializer
from adventures.geocoding import reverse_geocode
from adventures.geocoding import extractIsoCode
from django.conf import settings
from adventures.geocoding import search_google, search_osm
@ -47,14 +45,14 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
@action(detail=False, methods=['post'])
def mark_visited_region(self, request):
# searches through all of the users adventures, if the serialized data is_visited, is true, runs reverse geocode on the adventures and if a region is found, marks it as visited. Use the extractIsoCode function to get the region
# searches through all of the users locations, if the serialized data is_visited, is true, runs reverse geocode on the locations and if a region is found, marks it as visited. Use the extractIsoCode function to get the region
new_region_count = 0
new_regions = {}
new_city_count = 0
new_cities = {}
adventures = Adventure.objects.filter(user_id=self.request.user)
serializer = AdventureSerializer(adventures, many=True)
for adventure, serialized_adventure in zip(adventures, serializer.data):
locations = Location.objects.filter(user=self.request.user)
serializer = LocationSerializer(locations, many=True)
for adventure, serialized_adventure in zip(locations, serializer.data):
if serialized_adventure['is_visited'] == True:
lat = adventure.latitude
lon = adventure.longitude
@ -69,18 +67,18 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
# data already contains region_id and city_id
if 'region_id' in data and data['region_id'] is not None:
region = Region.objects.filter(id=data['region_id']).first()
visited_region = VisitedRegion.objects.filter(region=region, user_id=self.request.user).first()
visited_region = VisitedRegion.objects.filter(region=region, user=self.request.user).first()
if not visited_region:
visited_region = VisitedRegion(region=region, user_id=self.request.user)
visited_region = VisitedRegion(region=region, user=self.request.user)
visited_region.save()
new_region_count += 1
new_regions[region.id] = region.name
if 'city_id' in data and data['city_id'] is not None:
city = City.objects.filter(id=data['city_id']).first()
visited_city = VisitedCity.objects.filter(city=city, user_id=self.request.user).first()
visited_city = VisitedCity.objects.filter(city=city, user=self.request.user).first()
if not visited_city:
visited_city = VisitedCity(city=city, user_id=self.request.user)
visited_city = VisitedCity(city=city, user=self.request.user)
visited_city.save()
new_city_count += 1
new_cities[city.id] = city.name

View file

@ -1,11 +1,9 @@
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.decorators import action
from django.shortcuts import get_object_or_404
from worldtravel.models import City, Region, Country, VisitedCity, VisitedRegion
from adventures.models import Adventure, Collection
from users.serializers import CustomUserDetailsSerializer as PublicUserSerializer
from adventures.models import Location, Collection
from django.contrib.auth import get_user_model
User = get_user_model()
@ -26,21 +24,21 @@ class StatsViewSet(viewsets.ViewSet):
user.email = None
# get the counts for the user
adventure_count = Adventure.objects.filter(
user_id=user.id).count()
location_count = Location.objects.filter(
user=user.id).count()
trips_count = Collection.objects.filter(
user_id=user.id).count()
user=user.id).count()
visited_city_count = VisitedCity.objects.filter(
user_id=user.id).count()
user=user.id).count()
total_cities = City.objects.count()
visited_region_count = VisitedRegion.objects.filter(
user_id=user.id).count()
user=user.id).count()
total_regions = Region.objects.count()
visited_country_count = VisitedRegion.objects.filter(
user_id=user.id).values('region__country').distinct().count()
user=user.id).values('region__country').distinct().count()
total_countries = Country.objects.count()
return Response({
'adventure_count': adventure_count,
'location_count': location_count,
'trips_count': trips_count,
'visited_city_count': visited_city_count,
'total_cities': total_cities,

View file

@ -2,7 +2,7 @@ from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from adventures.models import Adventure
from adventures.models import Location
class ActivityTypesView(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
@ -10,7 +10,7 @@ class ActivityTypesView(viewsets.ViewSet):
@action(detail=False, methods=['get'])
def types(self, request):
"""
Retrieve a list of distinct activity types for adventures associated with the current user.
Retrieve a list of distinct activity types for locations associated with the current user.
Args:
request (HttpRequest): The HTTP request object.
@ -18,7 +18,7 @@ class ActivityTypesView(viewsets.ViewSet):
Returns:
Response: A response containing a list of distinct activity types.
"""
types = Adventure.objects.filter(user_id=request.user.id).values_list('activity_types', flat=True).distinct()
types = Location.objects.filter(user=request.user).values_list('tags', flat=True).distinct()
allTypes = []

View file

@ -1,12 +1,10 @@
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db.models import Q
from adventures.models import Transportation
from adventures.serializers import TransportationSerializer
from rest_framework.exceptions import PermissionDenied
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from rest_framework.permissions import IsAuthenticated
class TransportationViewSet(viewsets.ModelViewSet):
queryset = Transportation.objects.all()
@ -17,7 +15,7 @@ class TransportationViewSet(viewsets.ModelViewSet):
if not request.user.is_authenticated:
return Response(status=status.HTTP_403_FORBIDDEN)
queryset = Transportation.objects.filter(
Q(user_id=request.user.id)
Q(user=request.user.id)
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@ -25,13 +23,13 @@ class TransportationViewSet(viewsets.ModelViewSet):
def get_queryset(self):
user = self.request.user
if self.action == 'retrieve':
# For individual adventure retrieval, include public adventures, user's own adventures and shared adventures
# For individual adventure retrieval, include public locations, user's own locations and shared locations
return Transportation.objects.filter(
Q(is_public=True) | Q(user_id=user.id) | Q(collection__shared_with=user.id)
Q(is_public=True) | Q(user=user.id) | Q(collection__shared_with=user.id)
).distinct().order_by('-updated_at')
# For other actions, include user's own adventures and shared adventures
# For other actions, include user's own locations and shared locations
return Transportation.objects.filter(
Q(user_id=user.id) | Q(collection__shared_with=user.id)
Q(user=user.id) | Q(collection__shared_with=user.id)
).distinct().order_by('-updated_at')
def partial_update(self, request, *args, **kwargs):
@ -48,11 +46,11 @@ class TransportationViewSet(viewsets.ModelViewSet):
if new_collection is not None and new_collection != instance.collection:
# Check if the user is the owner of the new collection
if new_collection.user_id != user or instance.user_id != user:
if new_collection.user != user or instance.user != user:
raise PermissionDenied("You do not have permission to use this collection.")
elif new_collection is None:
# Handle the case where the user is trying to set the collection to None
if instance.collection is not None and instance.collection.user_id != user:
if instance.collection is not None and instance.collection.user != user:
raise PermissionDenied("You cannot remove the collection as you are not the owner.")
# Perform the update
@ -73,12 +71,12 @@ class TransportationViewSet(viewsets.ModelViewSet):
if collection:
user = self.request.user
# Check if the user is the owner or is in the shared_with list
if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists():
if collection.user != user and not collection.shared_with.filter(id=user.id).exists():
# Return an error response if the user does not have permission
raise PermissionDenied("You do not have permission to use this collection.")
# if collection the owner of the adventure is the owner of the collection
serializer.save(user_id=collection.user_id)
serializer.save(user=collection.user)
return
# Save the adventure with the current user as the owner
serializer.save(user_id=self.request.user)
serializer.save(user=self.request.user)

View file

@ -8,7 +8,7 @@ from rest_framework.permissions import IsAuthenticated
import requests
from rest_framework.pagination import PageNumberPagination
from django.conf import settings
from adventures.models import AdventureImage
from adventures.models import LocationImage
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
import logging
@ -253,11 +253,11 @@ class ImmichIntegrationView(viewsets.ViewSet):
"""
GET an Immich image using the integration and asset ID.
Access levels (in order of priority):
1. Public adventures: accessible by anyone
2. Private adventures in public collections: accessible by anyone
3. Private adventures in private collections shared with user: accessible by shared users
4. Private adventures: accessible only to the owner
5. No AdventureImage: owner can still view via integration
1. Public locations: accessible by anyone
2. Private locations in public collections: accessible by anyone
3. Private locations in private collections shared with user: accessible by shared users
4. Private locations: accessible only to the owner
5. No LocationImage: owner can still view via integration
"""
if not imageid or not integration_id:
return Response({
@ -268,31 +268,31 @@ class ImmichIntegrationView(viewsets.ViewSet):
# Lookup integration and user
integration = get_object_or_404(ImmichIntegration, id=integration_id)
owner_id = integration.user_id
owner_id = integration.user
# Try to find the image entry with collections and sharing information
image_entry = (
AdventureImage.objects
.filter(immich_id=imageid, user_id=owner_id)
.select_related('adventure')
.prefetch_related('adventure__collections', 'adventure__collections__shared_with')
.order_by('-adventure__is_public') # Public adventures first
LocationImage.objects
.filter(immich_id=imageid, user=owner_id)
.select_related('location')
.prefetch_related('location__collections', 'location__collections__shared_with')
.order_by('-location__is_public') # Public locations first
.first()
)
# Access control
if image_entry:
adventure = image_entry.adventure
collections = adventure.collections.all()
location = image_entry.location
collections = location.collections.all()
# Determine access level
is_authorized = False
# Level 1: Public adventure (highest priority)
if adventure.is_public:
# Level 1: Public location (highest priority)
if location.is_public:
is_authorized = True
# Level 2: Private adventure in any public collection
# Level 2: Private location in any public collection
elif any(collection.is_public for collection in collections):
is_authorized = True
@ -308,15 +308,15 @@ class ImmichIntegrationView(viewsets.ViewSet):
if not is_authorized:
return Response({
'message': 'This image belongs to a private adventure and you are not authorized.',
'message': 'This image belongs to a private location and you are not authorized.',
'error': True,
'code': 'immich.permission_denied'
}, status=status.HTTP_403_FORBIDDEN)
else:
# No AdventureImage exists; allow only the integration owner
# No LocationImage exists; allow only the integration owner
if not request.user.is_authenticated or request.user.id != owner_id:
return Response({
'message': 'Image is not linked to any adventure and you are not the owner.',
'message': 'Image is not linked to any location and you are not the owner.',
'error': True,
'code': 'immich.not_found'
}, status=status.HTTP_404_NOT_FOUND)

View file

@ -295,6 +295,8 @@ CORS_ALLOWED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGIN
CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()]
CORS_ALLOW_CREDENTIALS = True
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
LOGGING = {

View file

@ -6,5 +6,5 @@ def get_user_uuid(user):
class CustomModelSerializer(serializers.ModelSerializer):
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['user_id'] = get_user_uuid(instance.user_id)
representation['user'] = get_user_uuid(instance.user)
return representation

View file

@ -2,111 +2,199 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="AdventureLog Server" />
<meta name="description" content="AdventureLog API Server" />
<meta name="author" content="Sean Morley" />
<title>AdventureLog API Server</title>
<!-- Latest compiled and minified CSS -->
<!-- Bootstrap 5 CSS -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<!-- Bootstrap Icons -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"
/>
<!-- Optional theme -->
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css"
/>
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<style>
body {
background-color: #f9f9fb;
color: #222;
font-family: "Segoe UI", sans-serif;
}
.navbar {
background-color: #2c3e50;
}
.navbar-brand,
.nav-link {
color: #ecf0f1 !important;
}
.hero {
padding: 4rem 1rem;
background: linear-gradient(135deg, #2980b9, #6dd5fa);
color: white;
text-align: center;
border-radius: 0 0 1rem 1rem;
}
.hero h1 {
font-size: 3rem;
margin-bottom: 1rem;
}
.hero p {
font-size: 1.25rem;
margin-bottom: 2rem;
}
.api-response {
margin-top: 1rem;
font-family: monospace;
background-color: #eef2f7;
padding: 1rem;
border-radius: 0.5rem;
}
footer {
text-align: center;
padding: 2rem 0;
font-size: 0.9rem;
color: #888;
}
</style>
</head>
<body role="document">
<div class="navbar navbar-inverse" role="navigation">
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="/">AdventureLog API</a>
<button
class="navbar-toggler"
type="button"
class="navbar-toggle collapsed"
data-toggle="collapse"
data-target=".navbar-collapse"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
>
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="/">AdventureLog API Server</a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="/">Server Home</a></li>
<li>
<a target="_blank" href="http://adventurelog.app"
>Documentation</a
>
</li>
<li>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item">
<a
class="nav-link"
href="http://adventurelog.app"
target="_blank"
href="https://github.com/seanmorley15/AdventureLog"
>Source Code</a
>
Documentation
</a>
</li>
<li class="nav-item">
<a
class="nav-link"
href="https://github.com/seanmorley15/AdventureLog"
target="_blank"
>
Source Code
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/docs">API Docs</a>
</li>
<li><a href="/docs">API Docs</a></li>
</ul>
</div>
<!--/.nav-collapse -->
</div>
</nav>
<!-- Hero Section -->
<div class="hero">
<div class="container">
<h1><i class="bi bi-map"></i> AdventureLog API</h1>
<p>
The backend powering your travels — flexible, powerful, and open
source.
</p>
<a href="/docs" class="btn btn-light btn-lg shadow-sm"
><i class="bi bi-book"></i> Explore API Docs</a
>
</div>
</div>
<div class="container theme-showcase" role="main">
{% block content %}{% endblock %}
<!-- Main Content -->
<div class="container my-5">
{% block content %}
<div class="text-center">
<h2>Try a Sample Request</h2>
<p>Use the form below to test an API POST request.</p>
<form
class="ajax-post d-flex flex-column align-items-center"
action="/api/test"
method="post"
style="max-width: 500px; margin: auto"
>
<input
type="text"
name="example"
placeholder="Enter example data"
class="form-control mb-3"
required
/>
<button type="submit" class="btn btn-primary">
<i class="bi bi-send"></i> Send Request
</button>
</form>
<div class="api-response"></div>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<script type="text/javascript">
var error_response = function (data) {
{% endblock %}
</div>
<footer class="text-center text-muted py-4">
Open source with ❤️ by
<a href="https://seanmorley.com" target="_blank">Sean Morley</a> • View on
<a href="https://github.com/seanmorley15/AdventureLog" target="_blank"
>GitHub</a
>
<a href="https://adventurelog.app" target="_blank">adventurelog.app</a>
</footer>
<!-- Bootstrap JS -->
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"
></script>
<!-- jQuery (optional, used here for legacy script) -->
<script
src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha384-vtXRMe3mGCbOeY7l30aIg8H9p3GdeSe4IFlP6G8JMa7o7lXvnz3GFKzPxzJdPfGK"
crossorigin="anonymous"
></script>
<script>
const error_response = (data) => {
$(".api-response").html(
"API Response: " +
data.status +
" " +
data.statusText +
"<br/>Content: " +
data.responseText
`<strong>API Response:</strong> ${data.status} ${data.statusText}<br/><strong>Content:</strong> ${data.responseText}`
);
};
var susccess_response = function (data) {
const susccess_response = (data) => {
$(".api-response").html(
"API Response: OK<br/>Content: " + JSON.stringify(data)
`<strong>API Response:</strong> OK<br/><strong>Content:</strong> ${JSON.stringify(
data,
null,
2
)}`
);
};
$().ready(function () {
$(document).ready(() => {
$("form.ajax-post button[type=submit]").click(function () {
var form = $("form.ajax-post");
const form = $("form.ajax-post");
$.post(form.attr("action"), form.serialize())
.fail(function (data) {
error_response(data);
})
.done(function (data) {
susccess_response(data);
});
.fail(error_response)
.done(susccess_response);
return false;
});
});
</script>
{% block script %}{% endblock %}
</body>
</html>

View file

@ -11,8 +11,8 @@ from django.shortcuts import get_object_or_404
from django.contrib.auth import get_user_model
from .serializers import CustomUserDetailsSerializer as PublicUserSerializer
from allauth.socialaccount.models import SocialApp
from adventures.serializers import AdventureSerializer, CollectionSerializer
from adventures.models import Adventure, Collection
from adventures.serializers import LocationSerializer, CollectionSerializer
from adventures.models import Location, Collection
from allauth.socialaccount.models import SocialAccount
User = get_user_model()
@ -99,9 +99,9 @@ class PublicUserDetailView(APIView):
user.email = None
# Get the users adventures and collections to include in the response
adventures = Adventure.objects.filter(user_id=user, is_public=True)
collections = Collection.objects.filter(user_id=user, is_public=True)
adventure_serializer = AdventureSerializer(adventures, many=True)
adventures = Location.objects.filter(user=user, is_public=True)
collections = Collection.objects.filter(user=user, is_public=True)
adventure_serializer = LocationSerializer(adventures, many=True)
collection_serializer = CollectionSerializer(collections, many=True)
return Response({

View file

@ -1,12 +1,12 @@
from django.core.management.base import BaseCommand
from adventures.models import Adventure
from adventures.models import Location
import time
class Command(BaseCommand):
help = 'Bulk geocode all adventures by triggering save on each one'
def handle(self, *args, **options):
adventures = Adventure.objects.all()
adventures = Location.objects.all()
total = adventures.count()
self.stdout.write(self.style.SUCCESS(f'Starting bulk geocoding of {total} adventures'))

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-06-19 20:24
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('worldtravel', '0016_remove_city_insert_id_remove_country_insert_id_and_more'),
]
operations = [
migrations.RenameField(
model_name='visitedregion',
old_name='user_id',
new_name='user',
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-06-19 20:24
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('worldtravel', '0017_rename_user_id_visitedregion_user'),
]
operations = [
migrations.RenameField(
model_name='visitedcity',
old_name='user_id',
new_name='user',
),
]

View file

@ -6,7 +6,7 @@ from django.contrib.gis.db import models as gis_models
User = get_user_model()
default_user_id = 1 # Replace with an actual user ID
default_user = 1 # Replace with an actual user ID
class Country(models.Model):
@ -50,29 +50,29 @@ class City(models.Model):
class VisitedRegion(models.Model):
id = models.AutoField(primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
region = models.ForeignKey(Region, on_delete=models.CASCADE)
def __str__(self):
return f'{self.region.name} ({self.region.country.country_code}) visited by: {self.user_id.username}'
return f'{self.region.name} ({self.region.country.country_code}) visited by: {self.user.username}'
def save(self, *args, **kwargs):
if VisitedRegion.objects.filter(user_id=self.user_id, region=self.region).exists():
if VisitedRegion.objects.filter(user=self.user, region=self.region).exists():
raise ValidationError("Region already visited by user.")
super().save(*args, **kwargs)
class VisitedCity(models.Model):
id = models.AutoField(primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
city = models.ForeignKey(City, on_delete=models.CASCADE)
def __str__(self):
return f'{self.city.name} ({self.city.region.name}) visited by: {self.user_id.username}'
return f'{self.city.name} ({self.city.region.name}) visited by: {self.user.username}'
def save(self, *args, **kwargs):
if VisitedCity.objects.filter(user_id=self.user_id, city=self.city).exists():
if VisitedCity.objects.filter(user=self.user, city=self.city).exists():
raise ValidationError("City already visited by user.")
super().save(*args, **kwargs)

View file

@ -25,7 +25,7 @@ class CountrySerializer(serializers.ModelSerializer):
user = getattr(request, 'user', None)
if user and user.is_authenticated:
return VisitedRegion.objects.filter(region__country=obj, user_id=user).count()
return VisitedRegion.objects.filter(region__country=obj, user=user).count()
return 0
@ -62,8 +62,8 @@ class VisitedRegionSerializer(CustomModelSerializer):
class Meta:
model = VisitedRegion
fields = ['id', 'user_id', 'region', 'longitude', 'latitude', 'name']
read_only_fields = ['user_id', 'id', 'longitude', 'latitude', 'name']
fields = ['id', 'user', 'region', 'longitude', 'latitude', 'name']
read_only_fields = ['user', 'id', 'longitude', 'latitude', 'name']
class VisitedCitySerializer(CustomModelSerializer):
longitude = serializers.DecimalField(source='city.longitude', max_digits=9, decimal_places=6, read_only=True)
@ -72,5 +72,5 @@ class VisitedCitySerializer(CustomModelSerializer):
class Meta:
model = VisitedCity
fields = ['id', 'user_id', 'city', 'longitude', 'latitude', 'name']
read_only_fields = ['user_id', 'id', 'longitude', 'latitude', 'name']
fields = ['id', 'user', 'city', 'longitude', 'latitude', 'name']
read_only_fields = ['user', 'id', 'longitude', 'latitude', 'name']

View file

@ -13,7 +13,7 @@ from django.contrib.gis.geos import Point
from django.conf import settings
from rest_framework.decorators import action
from django.contrib.staticfiles import finders
from adventures.models import Adventure
from adventures.models import Location
@api_view(['GET'])
@permission_classes([IsAuthenticated])
@ -28,7 +28,7 @@ def regions_by_country(request, country_code):
@permission_classes([IsAuthenticated])
def visits_by_country(request, country_code):
country = get_object_or_404(Country, country_code=country_code)
visits = VisitedRegion.objects.filter(region__country=country, user_id=request.user.id)
visits = VisitedRegion.objects.filter(region__country=country, user=request.user.id)
serializer = VisitedRegionSerializer(visits, many=True)
return Response(serializer.data)
@ -45,7 +45,7 @@ def cities_by_region(request, region_id):
@permission_classes([IsAuthenticated])
def visits_by_region(request, region_id):
region = get_object_or_404(Region, id=region_id)
visits = VisitedCity.objects.filter(city__region=region, user_id=request.user.id)
visits = VisitedCity.objects.filter(city__region=region, user=request.user.id)
serializer = VisitedCitySerializer(visits, many=True)
return Response(serializer.data)
@ -71,7 +71,7 @@ class CountryViewSet(viewsets.ReadOnlyModelViewSet):
# make a post action that will get all of the users adventures and check if the point is in any of the regions if so make a visited region object for that user if it does not already exist
@action(detail=False, methods=['post'])
def region_check_all_adventures(self, request):
adventures = Adventure.objects.filter(user_id=request.user.id, type='visited')
adventures = Location.objects.filter(user=request.user.id, type='visited')
count = 0
for adventure in adventures:
if adventure.latitude is not None and adventure.longitude is not None:
@ -79,8 +79,8 @@ class CountryViewSet(viewsets.ReadOnlyModelViewSet):
point = Point(float(adventure.longitude), float(adventure.latitude), srid=4326)
region = Region.objects.filter(geometry__contains=point).first()
if region:
if not VisitedRegion.objects.filter(user_id=request.user.id, region=region).exists():
VisitedRegion.objects.create(user_id=request.user, region=region)
if not VisitedRegion.objects.filter(user=request.user.id, region=region).exists():
VisitedRegion.objects.create(user=request.user, region=region)
count += 1
except Exception as e:
print(f"Error processing adventure {adventure.id}: {e}")
@ -97,14 +97,14 @@ class VisitedRegionViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
def get_queryset(self):
return VisitedRegion.objects.filter(user_id=self.request.user.id)
return VisitedRegion.objects.filter(user=self.request.user.id)
def perform_create(self, serializer):
serializer.save(user_id=self.request.user)
serializer.save(user=self.request.user)
def create(self, request, *args, **kwargs):
request.data['user_id'] = request.user
if VisitedRegion.objects.filter(user_id=request.user.id, region=request.data['region']).exists():
request.data['user'] = request.user
if VisitedRegion.objects.filter(user=request.user.id, region=request.data['region']).exists():
return Response({"error": "Region already visited by user."}, status=400)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
@ -115,7 +115,7 @@ class VisitedRegionViewSet(viewsets.ModelViewSet):
def destroy(self, request, **kwargs):
# delete by region id
region = get_object_or_404(Region, id=kwargs['pk'])
visited_region = VisitedRegion.objects.filter(user_id=request.user.id, region=region)
visited_region = VisitedRegion.objects.filter(user=request.user.id, region=region)
if visited_region.exists():
visited_region.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@ -127,27 +127,27 @@ class VisitedCityViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
def get_queryset(self):
return VisitedCity.objects.filter(user_id=self.request.user.id)
return VisitedCity.objects.filter(user=self.request.user.id)
def perform_create(self, serializer):
serializer.save(user_id=self.request.user)
serializer.save(user=self.request.user)
def create(self, request, *args, **kwargs):
request.data['user_id'] = request.user
request.data['user'] = request.user
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
# if the region is not visited, visit it
region = serializer.validated_data['city'].region
if not VisitedRegion.objects.filter(user_id=request.user.id, region=region).exists():
VisitedRegion.objects.create(user_id=request.user, region=region)
if not VisitedRegion.objects.filter(user=request.user.id, region=region).exists():
VisitedRegion.objects.create(user=request.user, region=region)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def destroy(self, request, **kwargs):
# delete by city id
city = get_object_or_404(City, id=kwargs['pk'])
visited_city = VisitedCity.objects.filter(user_id=request.user.id, city=city)
visited_city = VisitedCity.objects.filter(user=request.user.id, city=city)
if visited_city.exists():
visited_city.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 882 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 809 KiB

After

Width:  |  Height:  |  Size: 804 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 2 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 637 KiB

After

Width:  |  Height:  |  Size: 591 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 KiB

After

Width:  |  Height:  |  Size: 431 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 337 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Before After
Before After

View file

@ -1,6 +1,6 @@
# About AdventureLog
Starting from a simple idea of tracking travel locations (called adventures), AdventureLog has grown into a full-fledged travel companion. With AdventureLog, you can log your adventures, keep track of where you've been on the world map, plan your next trip collaboratively, and share your experiences with friends and family. **AdventureLog is the ultimate travel companion for the modern-day explorer**.
Starting from a simple idea of tracking travel locations, AdventureLog has grown into a full-fledged travel companion. With AdventureLog, you can log your adventures, keep track of where you've been on the world map, plan your next trip collaboratively, and share your experiences with friends and family. **AdventureLog is the ultimate travel companion for the modern-day explorer**.
## Features

View file

@ -4,18 +4,23 @@ Welcome to AdventureLog! This guide will help you get started with AdventureLog
## Key Terms
#### Adventures
#### Locations
- **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.
::: tip Terminology Update
**Location has replaced Adventure:**
The term "Location" is now used instead of "Adventure" - the usage remains the same, just the name has changed to better reflect the purpose of the feature.
:::
- **Location**: think of a location as a point on a map, a place you want to visit, have visited, or a place you want to explore. A location can be anything you want it to be, from a local park to a famous landmark. These are the building blocks of AdventureLog and are the fundamental unit of information in the app.
- **Visit**: a visit is added to an location. It contains a date and notes about when the location was visited. If a location is visited multiple times, multiple visits can be added. If there are no visits on a location or the date of all visits is in the future, the location is considered planned. If the date of the visit is in the past, the location is considered completed.
- **Category**: a category is a way to group locations 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 a location. 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 a location.
- **Image**: an image is a photo that is added to a location. Images can be added to a location to provide a visual representation 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 a location. Attachments can be added to a location 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.
- **Collection**: a collection is a way to group locations 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 locations together. When a start and end date is added to a collection, it acts like a trip to group locations 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 locations. For example, you could have a collection for a trip to Europe with dates so you can plan where you want to visit, a collection of local hiking trails, or a collection for a list of restaurants you want to try.
- **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.

View file

@ -31,9 +31,9 @@
section: 'main'
},
{
path: '/adventures',
path: '/locations',
icon: MapMarker,
label: 'navbar.my_adventures',
label: 'locations.my_locations',
section: 'main'
},
{

View file

@ -1,9 +1,9 @@
<script lang="ts">
import type { Adventure } from '$lib/types';
import type { Location } from '$lib/types';
import ImageDisplayModal from './ImageDisplayModal.svelte';
import { t } from 'svelte-i18n';
export let adventures: Adventure[] = [];
export let adventures: Location[] = [];
let currentSlide = 0;
let image_url: string | null = null;

View file

@ -10,8 +10,8 @@
display_name: '',
icon: '',
id: '',
user_id: '',
num_adventures: 0
user: '',
num_locations: 0
};
let isOpen: boolean = false;
@ -44,7 +44,7 @@
let dropdownRef: HTMLDivElement;
onMount(() => {
categories = categories.sort((a, b) => (b.num_adventures || 0) - (a.num_adventures || 0));
categories = categories.sort((a, b) => (b.num_locations || 0) - (a.num_locations || 0));
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef && !dropdownRef.contains(event.target as Node)) {
isOpen = false;
@ -105,7 +105,7 @@
<!-- Sort the categories dynamically before rendering -->
{#each categories
.slice()
.sort((a, b) => (b.num_adventures || 0) - (a.num_adventures || 0)) as category}
.sort((a, b) => (b.num_locations || 0) - (a.num_locations || 0)) as category}
<button
type="button"
class="btn btn-neutral flex items-center space-x-2"
@ -113,7 +113,7 @@
role="option"
aria-selected={selected_category && selected_category.id === category.id}
>
<span>{category.display_name} {category.icon} ({category.num_adventures})</span>
<span>{category.display_name} {category.icon} ({category.num_locations})</span>
</button>
{/each}
</div>

View file

@ -8,7 +8,7 @@
let adventure_types: Category[] = [];
onMount(async () => {
let categoryFetch = await fetch('/api/categories/categories');
let categoryFetch = await fetch('/api/categories');
let categoryData = await categoryFetch.json();
adventure_types = categoryData;
console.log(categoryData);
@ -60,7 +60,7 @@
/>
<span>
{type.display_name}
{type.icon} ({type.num_adventures})
{type.icon} ({type.num_locations})
</span>
</label>
</li>

View file

@ -26,7 +26,7 @@
async function loadCategories() {
try {
const res = await fetch('/api/categories/categories');
const res = await fetch('/api/categories');
if (res.ok) {
categories = await res.json();
}
@ -334,7 +334,7 @@
{#if isChanged}
<div class="alert alert-success mb-4">
<span>{$t('categories.update_after_refresh')}</span>
<span>{$t('categories.location_update_after_refresh')}</span>
</div>
{/if}

View file

@ -99,7 +99,7 @@
<Launch class="w-5 h-5" />
{$t('notes.open')}
</button>
{#if checklist.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
{#if checklist.user == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
<button
id="delete_adventure"
data-umami-event="Delete Checklist"

View file

@ -19,7 +19,7 @@
let warning: string | null = '';
let isReadOnly =
!(checklist && user?.uuid == checklist?.user_id) &&
!(checklist && user?.uuid == checklist?.user) &&
!(user && collection && collection.shared_with && collection.shared_with.includes(user.uuid)) &&
!!checklist;
let newStatus: boolean = false;
@ -40,7 +40,7 @@
name: newItem,
is_checked: newStatus,
id: '',
user_id: '',
user: '',
checklist: 0,
created_at: '',
updated_at: ''

View file

@ -1,6 +1,6 @@
<script lang="ts">
import type {
Adventure,
Location,
Transportation,
Lodging,
Note,
@ -24,14 +24,14 @@
import Filter from '~icons/mdi/filter-variant';
// Component imports
import AdventureCard from './AdventureCard.svelte';
import LocationCard from './LocationCard.svelte';
import TransportationCard from './TransportationCard.svelte';
import LodgingCard from './LodgingCard.svelte';
import NoteCard from './NoteCard.svelte';
import ChecklistCard from './ChecklistCard.svelte';
// Props
export let adventures: Adventure[] = [];
export let adventures: Location[] = [];
export let transportations: Transportation[] = [];
export let lodging: Lodging[] = [];
export let notes: Note[] = [];
@ -45,7 +45,7 @@
let sortOption: string = 'name_asc';
// Filtered arrays
let filteredAdventures: Adventure[] = [];
let filteredAdventures: Location[] = [];
let filteredTransportations: Transportation[] = [];
let filteredLodging: Lodging[] = [];
let filteredNotes: Note[] = [];
@ -256,7 +256,7 @@
<div class="hidden md:flex items-center gap-2">
<div class="stats stats-horizontal bg-base-200/50 border border-base-300/50">
<div class="stat py-2 px-3">
<div class="stat-title text-xs">{$t('navbar.adventures')}</div>
<div class="stat-title text-xs">{$t('locations.locations')}</div>
<div class="stat-value text-sm text-info">{adventures.length}</div>
</div>
<div class="stat py-2 px-3">
@ -380,7 +380,7 @@
on:click={() => (filterOption = 'adventures')}
>
<Adventures class="w-3 h-3" />
{$t('navbar.adventures')}
{$t('locations.locations')}
</button>
<button
class="tab tab-sm gap-2 {filterOption === 'transportation'
@ -432,7 +432,7 @@
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mx-4">
{#each filteredAdventures as adventure}
<AdventureCard
<LocationCard
{user}
on:edit={handleEditAdventure}
on:delete={handleDeleteAdventure}

View file

@ -9,7 +9,7 @@
import ShareVariant from '~icons/mdi/share-variant';
import { goto } from '$app/navigation';
import type { Adventure, Collection, User } from '$lib/types';
import type { Location, Collection, User } from '$lib/types';
import { addToast } from '$lib/toasts';
import { t } from 'svelte-i18n';
@ -91,7 +91,7 @@
>
<!-- Image Carousel -->
<div class="relative overflow-hidden rounded-t-2xl">
<CardCarousel adventures={collection.adventures} />
<CardCarousel adventures={collection.locations} />
<!-- Badge Overlay -->
<div class="absolute top-4 left-4 flex flex-col gap-2">
@ -119,8 +119,8 @@
<!-- Adventure Count -->
<p class="text-sm text-base-content/70">
{collection.adventures.length}
{$t('navbar.adventures')}
{collection.locations.length}
{$t('locations.locations')}
</p>
<!-- Date Range -->
@ -170,7 +170,7 @@
<Launch class="w-4 h-4" />
{$t('adventures.open_details')}
</button>
{#if user && user.uuid == collection.user_id}
{#if user && user.uuid == collection.user}
<div class="dropdown dropdown-end">
<button type="button" class="btn btn-square btn-sm btn-base-300">
<DotsHorizontal class="w-5 h-5" />

View file

@ -1,5 +1,5 @@
<script lang="ts">
import type { Adventure, Collection } from '$lib/types';
import type { Location, Collection } from '$lib/types';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
@ -172,7 +172,7 @@
</div>
{#if searchQuery}
<h3 class="text-xl font-semibold text-base-content/70 mb-2">
{$t('adventures.no_collections_found')}
{$t('adventures.no_collections_to_add_location')}
</h3>
<p class="text-base-content/50 text-center max-w-md mb-6">
{$t('collection.try_different_search')}
@ -183,7 +183,7 @@
</button>
{:else}
<h3 class="text-xl font-semibold text-base-content/70 mb-2">
{$t('adventures.no_collections_found')}
{$t('adventures.no_collections_to_add_location')}
</h3>
<p class="text-base-content/50 text-center max-w-md">
{$t('adventures.create_collection_first')}

View file

@ -17,7 +17,7 @@
description: collectionToEdit?.description || '',
start_date: collectionToEdit?.start_date || null,
end_date: collectionToEdit?.end_date || null,
user_id: collectionToEdit?.user_id || '',
user: collectionToEdit?.user || '',
is_public: collectionToEdit?.is_public || false,
adventures: collectionToEdit?.adventures || [],
link: collectionToEdit?.link || '',

View file

@ -3,10 +3,10 @@
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
let modal: HTMLDialogElement;
import type { Adventure } from '$lib/types';
import type { Location } from '$lib/types';
export let image: string;
export let adventure: Adventure | null = null;
export let adventure: Location | null = null;
onMount(() => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;

View file

@ -2,7 +2,7 @@
import { createEventDispatcher, onMount } from 'svelte';
import { t } from 'svelte-i18n';
import ImmichLogo from '$lib/assets/immich.svg';
import type { Adventure, ImmichAlbum } from '$lib/types';
import type { Location, ImmichAlbum } from '$lib/types';
import { debounce } from '$lib';
let immichImages: any[] = [];
@ -12,7 +12,7 @@
let immichNextURL: string = '';
let loading = false;
export let adventure: Adventure | null = null;
export let adventure: Location | null = null;
export let copyImmichLocally: boolean = false;
const dispatch = createEventDispatcher();
@ -21,7 +21,7 @@
let currentAlbum: string = '';
let selectedDate: string =
(adventure as Adventure | null)?.visits
(adventure as Location | null)?.visits
.map((v) => new Date(v.end_date || v.start_date))
.sort((a, b) => +b - +a)[0]
?.toISOString()

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { goto } from '$app/navigation';
import type { Adventure, Collection, User } from '$lib/types';
import type { Location, Collection, User } from '$lib/types';
const dispatch = createEventDispatcher();
import Launch from '~icons/mdi/launch';
@ -31,19 +31,19 @@
let isCollectionModalOpen: boolean = false;
let isWarningModalOpen: boolean = false;
export let adventure: Adventure;
export let adventure: Location;
let displayActivityTypes: string[] = [];
let remainingCount = 0;
// Process activity types for display
$: {
if (adventure.activity_types) {
if (adventure.activity_types.length <= 3) {
displayActivityTypes = adventure.activity_types;
if (adventure.tags) {
if (adventure.tags.length <= 3) {
displayActivityTypes = adventure.tags;
remainingCount = 0;
} else {
displayActivityTypes = adventure.activity_types.slice(0, 3);
remainingCount = adventure.activity_types.length - 3;
displayActivityTypes = adventure.tags.slice(0, 3);
remainingCount = adventure.tags.length - 3;
}
}
}
@ -77,11 +77,11 @@
}
async function deleteAdventure() {
let res = await fetch(`/api/adventures/${adventure.id}`, {
let res = await fetch(`/api/locations/${adventure.id}`, {
method: 'DELETE'
});
if (res.ok) {
addToast('info', $t('adventures.adventure_delete_success'));
addToast('info', $t('adventures.location_delete_success'));
dispatch('delete', adventure.id);
} else {
console.log('Error deleting adventure');
@ -98,7 +98,7 @@
updatedCollections.push(collectionId);
}
let res = await fetch(`/api/adventures/${adventure.id}`, {
let res = await fetch(`/api/locations/${adventure.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
@ -109,16 +109,16 @@
if (res.ok) {
// Only update the adventure.collections after server confirms success
adventure.collections = updatedCollections;
addToast('info', `${$t('adventures.collection_link_success')}`);
addToast('info', `${$t('adventures.collection_link_location_success')}`);
} else {
addToast('error', `${$t('adventures.collection_link_error')}`);
addToast('error', `${$t('adventures.collection_link_location_error')}`);
}
}
async function removeFromCollection(event: CustomEvent<string>) {
let collectionId = event.detail;
if (!collectionId) {
addToast('error', `${$t('adventures.collection_remove_error')}`);
addToast('error', `${$t('adventures.collection_remove_location_error')}`);
return;
}
@ -128,7 +128,7 @@
(c) => String(c) !== String(collectionId)
);
let res = await fetch(`/api/adventures/${adventure.id}`, {
let res = await fetch(`/api/locations/${adventure.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
@ -139,9 +139,9 @@
if (res.ok) {
// Only update adventure.collections after server confirms success
adventure.collections = updatedCollections;
addToast('info', `${$t('adventures.collection_remove_success')}`);
addToast('info', `${$t('adventures.collection_remove_location_success')}`);
} else {
addToast('error', `${$t('adventures.collection_remove_error')}`);
addToast('error', `${$t('adventures.collection_remove_location_error')}`);
}
}
}
@ -166,9 +166,9 @@
{#if isWarningModalOpen}
<DeleteWarning
title={$t('adventures.delete_adventure')}
title={$t('adventures.delete_location')}
button_text="Delete"
description={$t('adventures.adventure_delete_confirm')}
description={$t('adventures.location_delete_confirm')}
is_warning={false}
on:close={() => (isWarningModalOpen = false)}
on:confirm={deleteAdventure}
@ -228,7 +228,7 @@
<!-- Header Section -->
<div class="space-y-3">
<button
on:click={() => goto(`/adventures/${adventure.id}`)}
on:click={() => goto(`/locations/${adventure.id}`)}
class="text-xl font-bold text-left hover:text-primary transition-colors duration-200 line-clamp-2 group-hover:underline"
>
{adventure.name}
@ -274,13 +274,13 @@
<div class="flex justify-between items-center">
<button
class="btn btn-neutral btn-sm flex-1 mr-2"
on:click={() => goto(`/adventures/${adventure.id}`)}
on:click={() => goto(`/locations/${adventure.id}`)}
>
<Launch class="w-4 h-4" />
{$t('adventures.open_details')}
</button>
{#if adventure.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
{#if (adventure.user && adventure.user.uuid == user?.uuid) || (collection && user && collection.shared_with?.includes(user.uuid))}
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-square btn-sm btn-base-300">
<DotsHorizontal class="w-5 h-5" />
@ -293,11 +293,11 @@
<li>
<button on:click={editAdventure} class="flex items-center gap-2">
<FileDocumentEdit class="w-4 h-4" />
{$t('adventures.edit_adventure')}
{$t('adventures.edit_location')}
</button>
</li>
{#if user?.uuid == adventure.user_id}
{#if user?.uuid == adventure.user?.uuid}
<li>
<button
on:click={() => (isCollectionModalOpen = true)}

View file

@ -2,12 +2,12 @@
import { getBasemapUrl } from '$lib';
import { appVersion } from '$lib/config';
import { addToast } from '$lib/toasts';
import type { Adventure, Lodging, GeocodeSearchResult, Point, ReverseGeocode } from '$lib/types';
import type { Location, Lodging, GeocodeSearchResult, Point, ReverseGeocode } from '$lib/types';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
export let item: Adventure | Lodging;
export let item: Location | Lodging;
export let triggerMarkVisted: boolean = false;
export let initialLatLng: { lat: number; lng: number } | null = null; // Used to pass the location from the map selection to the modal
@ -366,7 +366,7 @@ it would also work to just use on:click on the MapLibre component itself. -->
{reverseGeocodePlace.city
? reverseGeocodePlace.city + ', '
: ''}{reverseGeocodePlace.region}, {reverseGeocodePlace.country}
{$t('adventures.will_be_marked')}
{$t('adventures.will_be_marked_location')}
</span>
</div>
{/if}

View file

@ -1,10 +1,10 @@
<script lang="ts">
import type { Adventure, User } from '$lib/types';
import type { Location, User } from '$lib/types';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { t } from 'svelte-i18n';
import { onMount } from 'svelte';
import AdventureCard from './AdventureCard.svelte';
import LocationCard from './LocationCard.svelte';
let modal: HTMLDialogElement;
// Icons - following the worldtravel pattern
@ -17,8 +17,8 @@
import Public from '~icons/mdi/earth';
import Private from '~icons/mdi/lock';
let adventures: Adventure[] = [];
let filteredAdventures: Adventure[] = [];
let adventures: Location[] = [];
let filteredAdventures: Location[] = [];
let searchQuery: string = '';
let filterOption: string = 'all';
let isLoading: boolean = true;
@ -69,7 +69,7 @@
modal.showModal();
}
let res = await fetch(`/api/adventures/all/?include_collections=true`, {
let res = await fetch(`/api/locations/all/?include_collections=true`, {
method: 'GET'
});
@ -77,7 +77,7 @@
// Filter out adventures that are already linked to the collections
if (collectionId) {
adventures = newAdventures.filter((adventure: Adventure) => {
adventures = newAdventures.filter((adventure: Location) => {
return !(adventure.collections ?? []).includes(collectionId);
});
} else {
@ -91,7 +91,7 @@
dispatch('close');
}
function add(event: CustomEvent<Adventure>) {
function add(event: CustomEvent<Location>) {
adventures = adventures.filter((a) => a.id !== event.detail.id);
dispatch('add', event.detail);
}
@ -134,7 +134,7 @@
{filteredAdventures.length}
{$t('worldtravel.of')}
{totalAdventures}
{$t('navbar.adventures')}
{$t('locations.locations')}
</p>
</div>
</div>
@ -252,7 +252,7 @@
</div>
{#if searchQuery || filterOption !== 'all'}
<h3 class="text-xl font-semibold text-base-content/70 mb-2">
{$t('adventures.no_adventures_found')}
{$t('adventures.no_locations_found')}
</h3>
<p class="text-base-content/50 text-center max-w-md mb-6">
{$t('collection.try_different_search')}
@ -274,7 +274,7 @@
<!-- Adventures Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-6 p-4">
{#each filteredAdventures as adventure}
<AdventureCard {user} type="link" {adventure} on:link={add} />
<LocationCard {user} type="link" {adventure} on:link={add} />
{/each}
</div>
{/if}

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import type { Adventure, Attachment, Category, Collection } from '$lib/types';
import type { Location, Attachment, Category, Collection } from '$lib/types';
import { addToast } from '$lib/toasts';
import { deserialize } from '$app/forms';
import { t } from 'svelte-i18n';
@ -23,7 +23,6 @@
let images: { id: string; image: string; is_primary: boolean; immich_id: string | null }[] = [];
let warningMessage: string = '';
let constrainDates: boolean = false;
let categories: Category[] = [];
@ -97,62 +96,62 @@
let wikiError: string = '';
let adventure: Adventure = {
let location: Location = {
id: '',
name: '',
visits: [],
link: null,
description: null,
activity_types: [],
tags: [],
rating: NaN,
is_public: false,
latitude: NaN,
longitude: NaN,
location: null,
images: [],
user_id: null,
user: null,
category: {
id: '',
name: '',
display_name: '',
icon: '',
user_id: ''
user: ''
},
attachments: []
};
export let adventureToEdit: Adventure | null = null;
export let locationToEdit: Location | null = null;
adventure = {
id: adventureToEdit?.id || '',
name: adventureToEdit?.name || '',
link: adventureToEdit?.link || null,
description: adventureToEdit?.description || null,
activity_types: adventureToEdit?.activity_types || [],
rating: adventureToEdit?.rating || NaN,
is_public: adventureToEdit?.is_public || false,
latitude: adventureToEdit?.latitude || NaN,
longitude: adventureToEdit?.longitude || NaN,
location: adventureToEdit?.location || null,
images: adventureToEdit?.images || [],
user_id: adventureToEdit?.user_id || null,
visits: adventureToEdit?.visits || [],
is_visited: adventureToEdit?.is_visited || false,
category: adventureToEdit?.category || {
location = {
id: locationToEdit?.id || '',
name: locationToEdit?.name || '',
link: locationToEdit?.link || null,
description: locationToEdit?.description || null,
tags: locationToEdit?.tags || [],
rating: locationToEdit?.rating || NaN,
is_public: locationToEdit?.is_public || false,
latitude: locationToEdit?.latitude || NaN,
longitude: locationToEdit?.longitude || NaN,
location: locationToEdit?.location || null,
images: locationToEdit?.images || [],
user: locationToEdit?.user || null,
visits: locationToEdit?.visits || [],
is_visited: locationToEdit?.is_visited || false,
category: locationToEdit?.category || {
id: '',
name: '',
display_name: '',
icon: '',
user_id: ''
user: ''
},
attachments: adventureToEdit?.attachments || []
attachments: locationToEdit?.attachments || []
};
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
modal.showModal();
let categoryFetch = await fetch('/api/categories/categories');
let categoryFetch = await fetch('/api/categories');
if (categoryFetch.ok) {
categories = await categoryFetch.json();
} else {
@ -183,15 +182,15 @@
let isLoading: boolean = false;
images = adventure.images || [];
images = location.images || [];
$: {
if (!adventure.rating) {
adventure.rating = NaN;
if (!location.rating) {
location.rating = NaN;
}
}
function deleteAttachment(event: CustomEvent<string>) {
adventure.attachments = adventure.attachments.filter(
location.attachments = location.attachments.filter(
(attachment) => attachment.id !== event.detail
);
}
@ -210,7 +209,7 @@
});
if (res.ok) {
let newAttachment = (await res.json()) as Attachment;
adventure.attachments = adventure.attachments.map((attachment) => {
location.attachments = location.attachments.map((attachment) => {
if (attachment.id === newAttachment.id) {
return newAttachment;
}
@ -245,18 +244,18 @@
const formData = new FormData();
formData.append('file', file);
formData.append('adventure', adventure.id);
formData.append('location', location.id);
formData.append('name', attachmentName);
try {
const res = await fetch('/adventures?/attachment', {
const res = await fetch('/locations?/attachment', {
method: 'POST',
body: formData
});
if (res.ok) {
const newData = deserialize(await res.text()) as { data: Attachment };
adventure.attachments = [...adventure.attachments, newData.data];
location.attachments = [...location.attachments, newData.data];
addToast('success', $t('adventures.attachment_upload_success'));
attachmentName = '';
} else {
@ -273,7 +272,7 @@
}
}
let imageSearch: string = adventure.name || '';
let imageSearch: string = location.name || '';
async function removeImage(id: string) {
let res = await fetch(`/api/images/${id}/image_delete`, {
@ -281,7 +280,7 @@
});
if (res.status === 204) {
images = images.filter((image) => image.id !== id);
adventure.images = images;
location.images = images;
addToast('success', $t('adventures.image_removed_success'));
} else {
addToast('error', $t('adventures.image_removed_error'));
@ -291,7 +290,7 @@
let isDetails: boolean = true;
function saveAndClose() {
dispatch('save', adventure);
dispatch('save', location);
close();
}
@ -308,7 +307,7 @@
}
return image;
});
adventure.images = images;
location.images = images;
} else {
console.error('Error in makePrimaryImage:', res);
}
@ -326,9 +325,9 @@
async function uploadImage(file: File) {
let formData = new FormData();
formData.append('image', file);
formData.append('adventure', adventure.id);
formData.append('location', location.id);
let res = await fetch(`/adventures?/image`, {
let res = await fetch(`/locations?/image`, {
method: 'POST',
body: formData
});
@ -341,7 +340,7 @@
immich_id: null
};
images = [...images, newImage];
adventure.images = images;
location.images = images;
addToast('success', $t('adventures.image_upload_success'));
} else {
addToast('error', $t('adventures.image_upload_error'));
@ -359,7 +358,7 @@
let file = new File([data], 'image.jpg', { type: 'image/jpeg' });
let formData = new FormData();
formData.append('image', file);
formData.append('adventure', adventure.id);
formData.append('adventure', location.id);
await uploadImage(file);
url = '';
@ -383,8 +382,8 @@
wikiImageError = '';
let formData = new FormData();
formData.append('image', file);
formData.append('adventure', adventure.id);
let res2 = await fetch(`/adventures?/image`, {
formData.append('location', location.id);
let res2 = await fetch(`/locations?/image`, {
method: 'POST',
body: formData
});
@ -397,7 +396,7 @@
immich_id: null
};
images = [...images, newImage];
adventure.images = images;
location.images = images;
addToast('success', $t('adventures.image_upload_success'));
} else {
addToast('error', $t('adventures.image_upload_error'));
@ -417,10 +416,10 @@
}
async function generateDesc() {
let res = await fetch(`/api/generate/desc/?name=${adventure.name}`);
let res = await fetch(`/api/generate/desc/?name=${location.name}`);
let data = await res.json();
if (data.extract?.length > 0) {
adventure.description = data.extract;
location.description = data.extract;
wikiError = '';
} else {
wikiError = $t('adventures.no_description_found');
@ -433,72 +432,72 @@
isLoading = true;
// if category icon is empty, set it to the default icon
if (adventure.category?.icon == '' || adventure.category?.icon == null) {
if (adventure.category) {
adventure.category.icon = '🌍';
if (location.category?.icon == '' || location.category?.icon == null) {
if (location.category) {
location.category.icon = '🌍';
}
}
if (adventure.id === '') {
if (adventure.category?.display_name == '') {
if (location.id === '') {
if (location.category?.display_name == '') {
if (categories.some((category) => category.name === 'general')) {
adventure.category = categories.find(
location.category = categories.find(
(category) => category.name === 'general'
) as Category;
} else {
adventure.category = {
location.category = {
id: '',
name: 'general',
display_name: 'General',
icon: '🌍',
user_id: ''
user: ''
};
}
}
// add this collection to the adventure
if (collection && collection.id) {
adventure.collections = [collection.id];
location.collections = [collection.id];
}
let res = await fetch('/api/adventures', {
let res = await fetch('/api/locations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(adventure)
body: JSON.stringify(location)
});
let data = await res.json();
if (data.id) {
adventure = data as Adventure;
location = data as Location;
isDetails = false;
warningMessage = '';
addToast('success', $t('adventures.adventure_created'));
addToast('success', $t('adventures.location_created'));
} else {
warningMessage = findFirstValue(data) as string;
console.error(data);
addToast('error', $t('adventures.adventure_create_error'));
addToast('error', $t('adventures.location_create_error'));
}
} else {
let res = await fetch(`/api/adventures/${adventure.id}`, {
let res = await fetch(`/api/locations/${location.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(adventure)
body: JSON.stringify(location)
});
let data = await res.json();
if (data.id) {
adventure = data as Adventure;
location = data as Location;
isDetails = false;
warningMessage = '';
addToast('success', $t('adventures.adventure_updated'));
addToast('success', $t('adventures.location_updated'));
} else {
warningMessage = Object.values(data)[0] as string;
addToast('error', $t('adventures.adventure_update_error'));
addToast('error', $t('adventures.location_update_error'));
}
}
imageSearch = adventure.name;
imageSearch = location.name;
isLoading = false;
}
</script>
@ -509,9 +508,9 @@
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div class="modal-box w-11/12 max-w-3xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-2xl">
{adventureToEdit ? $t('adventures.edit_adventure') : $t('adventures.new_adventure')}
{locationToEdit ? $t('adventures.edit_location') : $t('adventures.new_location')}
</h3>
{#if adventure.id === '' || isDetails}
{#if location.id === '' || isDetails}
<div class="modal-action items-center">
<form method="post" style="width: 100%;" on:submit={handleSubmit}>
<!-- Grid layout for form fields -->
@ -529,7 +528,7 @@
type="text"
id="name"
name="name"
bind:value={adventure.name}
bind:value={location.name}
class="input input-bordered w-full"
required
/>
@ -539,7 +538,7 @@
>{$t('adventures.category')}<span class="text-red-500">*</span></label
><br />
<CategoryDropdown bind:categories bind:selected_category={adventure.category} />
<CategoryDropdown bind:categories bind:selected_category={location.category} />
</div>
<div>
<label for="rating">{$t('adventures.rating')}</label><br />
@ -548,7 +547,7 @@
min="0"
max="5"
hidden
bind:value={adventure.rating}
bind:value={location.rating}
id="rating"
name="rating"
class="input input-bordered w-full max-w-xs mt-1"
@ -558,48 +557,48 @@
type="radio"
name="rating-2"
class="rating-hidden"
checked={Number.isNaN(adventure.rating)}
checked={Number.isNaN(location.rating)}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (adventure.rating = 1)}
checked={adventure.rating === 1}
on:click={() => (location.rating = 1)}
checked={location.rating === 1}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (adventure.rating = 2)}
checked={adventure.rating === 2}
on:click={() => (location.rating = 2)}
checked={location.rating === 2}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (adventure.rating = 3)}
checked={adventure.rating === 3}
on:click={() => (location.rating = 3)}
checked={location.rating === 3}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (adventure.rating = 4)}
checked={adventure.rating === 4}
on:click={() => (location.rating = 4)}
checked={location.rating === 4}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (adventure.rating = 5)}
checked={adventure.rating === 5}
on:click={() => (location.rating = 5)}
checked={location.rating === 5}
/>
{#if adventure.rating}
{#if location.rating}
<button
type="button"
class="btn btn-sm btn-error ml-2"
on:click={() => (adventure.rating = NaN)}
on:click={() => (location.rating = NaN)}
>
{$t('adventures.remove')}
</button>
@ -613,16 +612,16 @@
type="text"
id="link"
name="link"
bind:value={adventure.link}
bind:value={location.link}
class="input input-bordered w-full"
/>
</div>
</div>
<div>
<label for="description">{$t('adventures.description')}</label><br />
<MarkdownEditor bind:text={adventure.description} />
<MarkdownEditor bind:text={location.description} />
<div class="mt-2">
<div class="tooltip tooltip-right" data-tip={$t('adventures.wiki_desc')}>
<div class="tooltip tooltip-right" data-tip={$t('adventures.wiki_location_desc')}>
<button type="button" class="btn btn-neutral mt-2" on:click={generateDesc}
>{$t('adventures.generate_desc')}</button
>
@ -630,17 +629,17 @@
<p class="text-red-500">{wikiError}</p>
</div>
</div>
{#if !adventureToEdit || (adventureToEdit.collections && adventureToEdit.collections.length === 0)}
{#if !locationToEdit || (locationToEdit.collections && locationToEdit.collections.length === 0)}
<div>
<div class="form-control flex items-start mt-1">
<label class="label cursor-pointer flex items-start space-x-2">
<span class="label-text">{$t('adventures.public_adventure')}</span>
<span class="label-text">{$t('adventures.public_location')}</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="is_public"
name="is_public"
bind:checked={adventure.is_public}
bind:checked={location.is_public}
/>
</label>
</div>
@ -649,27 +648,27 @@
</div>
</div>
<LocationDropdown bind:item={adventure} bind:triggerMarkVisted {initialLatLng} />
<LocationDropdown bind:item={location} bind:triggerMarkVisted {initialLatLng} />
<div class="collapse collapse-plus bg-base-200 mb-4 overflow-visible">
<input type="checkbox" />
<div class="collapse-title text-xl font-medium">
{$t('adventures.tags')} ({adventure.activity_types?.length || 0})
{$t('adventures.tags')} ({location.tags?.length || 0})
</div>
<div class="collapse-content">
<input
type="text"
id="activity_types"
name="activity_types"
id="tags"
name="tags"
hidden
bind:value={adventure.activity_types}
bind:value={location.tags}
class="input input-bordered w-full"
/>
<ActivityComplete bind:activities={adventure.activity_types} />
<ActivityComplete bind:activities={location.tags} />
</div>
</div>
<DateRangeCollapse type="adventure" {collection} bind:visits={adventure.visits} />
<DateRangeCollapse type="adventure" {collection} bind:visits={location.visits} />
<div>
<div class="mt-4">
@ -711,11 +710,11 @@
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" />
<div class="collapse-title text-xl font-medium">
{$t('adventures.attachments')} ({adventure.attachments?.length || 0})
{$t('adventures.attachments')} ({location.attachments?.length || 0})
</div>
<div class="collapse-content">
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each adventure.attachments as attachment}
{#each location.attachments as attachment}
<AttachmentCard
{attachment}
on:delete={deleteAttachment}
@ -785,7 +784,7 @@
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
{$t('adventures.images')} ({adventure.images?.length || 0})
{$t('adventures.images')} ({location.images?.length || 0})
</div>
<div class="collapse-content">
<label for="image" class="block font-medium mb-2">
@ -802,7 +801,7 @@
multiple
on:change={handleMultipleFiles}
/>
<input type="hidden" name="adventure" value={adventure.id} id="adventure" />
<input type="hidden" name="adventure" value={location.id} id="adventure" />
</form>
<div class="mb-4">
@ -848,7 +847,7 @@
{#if immichIntegration}
<ImmichSelect
{adventure}
adventure={location}
on:fetchImage={(e) => {
url = e.detail;
fetchImage();
@ -862,7 +861,7 @@
immich_id: e.detail.immich_id
};
images = [...images, newImage];
adventure.images = images;
location.images = images;
addToast('success', $t('adventures.image_upload_success'));
}}
/>
@ -917,17 +916,17 @@
</div>
{/if}
{#if adventure.is_public && adventure.id}
{#if location.is_public && location.id}
<div class="bg-neutral p-4 mt-2 rounded-md shadow-sm text-neutral-content">
<p class=" font-semibold">{$t('adventures.share_adventure')}</p>
<p class=" font-semibold">{$t('adventures.share_location')}</p>
<div class="flex items-center justify-between">
<p class="text-card-foreground font-mono">
{window.location.origin}/adventures/{adventure.id}
{window.location.origin}/locations/{location.id}
</p>
<button
type="button"
on:click={() => {
navigator.clipboard.writeText(`${window.location.origin}/adventures/${adventure.id}`);
navigator.clipboard.writeText(`${window.location.origin}/locations/${location.id}`);
}}
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2"
>

View file

@ -156,7 +156,7 @@
</div>
<!-- Reservation Info -->
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
{#if lodging.user == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
<div class="space-y-2">
{#if lodging.reservation_number}
<div class="flex items-center gap-2">
@ -174,7 +174,7 @@
{/if}
<!-- Actions -->
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
{#if lodging.user == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
<div class="pt-4 border-t border-base-300 flex justify-end gap-2">
<button
class="btn btn-neutral btn-sm flex items-center gap-1"

View file

@ -31,7 +31,7 @@
function initializeLodging(lodgingToEdit: Lodging | null): Lodging {
return {
id: lodgingToEdit?.id || '',
user_id: lodgingToEdit?.user_id || '',
user: lodgingToEdit?.user || '',
name: lodgingToEdit?.name || '',
type: lodgingToEdit?.type || 'other',
description: lodgingToEdit?.description || '',

View file

@ -103,7 +103,7 @@
// Navigation items for better organization
const navigationItems = [
{ path: '/adventures', icon: MapMarker, label: 'navbar.adventures' },
{ path: '/locations', icon: MapMarker, label: 'locations.locations' },
{ path: '/collections', icon: FormatListBulletedSquare, label: 'navbar.collections' },
{ path: '/worldtravel', icon: Earth, label: 'navbar.worldtravel' },
{ path: '/map', icon: Map, label: 'navbar.map' },

View file

@ -12,7 +12,7 @@
<img src={Lost} alt="Lost" class="w-1/2" />
</div>
<h1 class="mt-4 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
{$t('adventures.no_adventures_found')}
{$t('adventures.no_locations_found')}
</h1>
{#if !error}
<p class="mt-4 text-muted-foreground">

View file

@ -124,7 +124,7 @@
<Launch class="w-5 h-5" />
{$t('notes.open')}
</button>
{#if note.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
{#if note.user == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
<button
id="delete_adventure"
data-umami-event="Delete Adventure"

View file

@ -20,7 +20,7 @@
let constrainDates: boolean = false;
let isReadOnly =
!(note && user?.uuid == note?.user_id) &&
!(note && user?.uuid == note?.user) &&
!(user && collection && collection.shared_with && collection.shared_with.includes(user.uuid)) &&
!!note;

View file

@ -1,6 +1,6 @@
<script lang="ts">
// @ts-nocheck
import type { Adventure, GeocodeSearchResult, Point } from '$lib/types';
import type { Location, GeocodeSearchResult, Point } from '$lib/types';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
@ -13,7 +13,7 @@
let markers: Point[] = [];
export let query: string | null = null;
export let adventure: Adventure;
export let adventure: Location;
if (query) {
geocode();
@ -83,7 +83,7 @@
adventure.name = markers[0].name;
}
if (adventure.type == 'visited' || adventure.type == 'planned') {
adventure.activity_types = [...adventure.activity_types, markers[0].activity_type];
adventure.tags = [...adventure.tags, markers[0].activity_type];
}
dispatch('submit', adventure);
close();

View file

@ -192,7 +192,7 @@
</div>
<!-- Actions -->
{#if transportation.user_id === user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
{#if transportation.user === user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
<div class="pt-4 border-t border-base-300 flex justify-end gap-2">
<button
class="btn btn-neutral btn-sm flex items-center gap-1"

View file

@ -29,7 +29,7 @@
flight_number: transportationToEdit?.flight_number || '',
from_location: transportationToEdit?.from_location || '',
to_location: transportationToEdit?.to_location || '',
user_id: transportationToEdit?.user_id || '',
user: transportationToEdit?.user || '',
is_public: transportationToEdit?.is_public || false,
collection: transportationToEdit?.collection || collection.id,
created_at: transportationToEdit?.created_at || '',
@ -182,11 +182,11 @@
if (data.id) {
transportation = data as Transportation;
addToast('success', $t('adventures.adventure_created'));
addToast('success', $t('adventures.location_created'));
dispatch('save', transportation);
} else {
console.error(data);
addToast('error', $t('adventures.adventure_create_error'));
addToast('error', $t('adventures.location_create_error'));
}
} else {
let res = await fetch(`/api/transportations/${transportation.id}`, {
@ -200,10 +200,10 @@
if (data.id) {
transportation = data as Transportation;
addToast('success', $t('adventures.adventure_updated'));
addToast('success', $t('adventures.location_updated'));
dispatch('save', transportation);
} else {
addToast('error', $t('adventures.adventure_update_error'));
addToast('error', $t('adventures.location_update_error'));
}
}
}

View file

@ -4,7 +4,7 @@ import randomBackgrounds from './json/backgrounds.json';
// @ts-ignore
import { DateTime } from 'luxon';
import type {
Adventure,
Location,
Background,
Checklist,
Collection,
@ -34,26 +34,6 @@ export function checkLink(link: string) {
}
}
export async function exportData() {
let res = await fetch('/api/adventures/all');
let adventures = (await res.json()) as Adventure[];
res = await fetch('/api/collections/all');
let collections = (await res.json()) as Collection[];
res = await fetch('/api/visitedregion');
let visitedRegions = await res.json();
const data = {
adventures,
collections,
visitedRegions
};
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
return URL.createObjectURL(blob);
}
export function isValidUrl(url: string) {
try {
new URL(url);
@ -63,22 +43,22 @@ export function isValidUrl(url: string) {
}
}
export function groupAdventuresByDate(
adventures: Adventure[],
export function groupLocationsByDate(
locations: Location[],
startDate: Date,
numberOfDays: number
): Record<string, Adventure[]> {
const groupedAdventures: Record<string, Adventure[]> = {};
): Record<string, Location[]> {
const groupedLocations: Record<string, Location[]> = {};
// Initialize all days in the range using DateTime
for (let i = 0; i < numberOfDays; i++) {
const currentDate = DateTime.fromJSDate(startDate).plus({ days: i });
const dateString = currentDate.toISODate(); // 'YYYY-MM-DD'
groupedAdventures[dateString] = [];
groupedLocations[dateString] = [];
}
adventures.forEach((adventure) => {
adventure.visits.forEach((visit) => {
locations.forEach((location) => {
location.visits.forEach((visit: { start_date: string; end_date: string; timezone: any }) => {
if (visit.start_date) {
// Check if it's all-day: start has 00:00:00 AND (no end OR end also has 00:00:00)
const startHasZeros = isAllDay(visit.start_date);
@ -115,10 +95,10 @@ export function groupAdventuresByDate(
const currentDate = DateTime.fromJSDate(startDate).plus({ days: i });
const currentDateStr = currentDate.toISODate();
// Include the current day if it falls within the adventure date range
// Include the current day if it falls within the location date range
if (currentDateStr >= startDateStr && currentDateStr <= endDateStr) {
if (groupedAdventures[currentDateStr]) {
groupedAdventures[currentDateStr].push(adventure);
if (groupedLocations[currentDateStr]) {
groupedLocations[currentDateStr].push(location);
}
}
}
@ -126,7 +106,7 @@ export function groupAdventuresByDate(
});
});
return groupedAdventures;
return groupedLocations;
}
function getLocalDateString(date: Date): string {
@ -426,15 +406,6 @@ export let TRANSPORTATION_TYPES_ICONS = {
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) {
return ADVENTURE_TYPE_ICONS[type as keyof typeof ADVENTURE_TYPE_ICONS];
} else {
return '❓';
}
}
export function getRandomBackground() {
const today = new Date();

View file

@ -14,12 +14,11 @@ export type User = {
disable_password: boolean;
};
export type Adventure = {
export type Location = {
id: string;
user_id: string | null;
name: string;
location?: string | null;
activity_types?: string[] | null;
tags?: string[] | null;
description?: string | null;
rating?: number | null;
link?: string | null;
@ -45,13 +44,13 @@ export type Adventure = {
is_visited?: boolean;
category: Category | null;
attachments: Attachment[];
user?: User | null;
user: User | null;
city?: City | null;
region?: Region | null;
country?: Country | null;
};
export type AdditionalAdventure = Adventure & {
export type AdditionalLocation = Location & {
sun_times: {
date: string;
visit_id: string;
@ -96,7 +95,7 @@ export type City = {
export type VisitedRegion = {
id: number;
region: string;
user_id: string;
user: string;
longitude: number;
latitude: number;
name: string;
@ -105,7 +104,7 @@ export type VisitedRegion = {
export type VisitedCity = {
id: number;
city: string;
user_id: string;
user: string;
longitude: number;
latitude: number;
name: string;
@ -123,11 +122,11 @@ export type Point = {
export type Collection = {
id: string;
user_id: string;
user: string;
name: string;
description: string;
is_public: boolean;
adventures: Adventure[];
locations: Location[];
created_at?: string | null;
start_date: string | null;
end_date: string | null;
@ -153,7 +152,7 @@ export type GeocodeSearchResult = {
export type Transportation = {
id: string;
user_id: string;
user: string;
type: string;
name: string;
description: string | null;
@ -179,7 +178,7 @@ export type Transportation = {
export type Note = {
id: string;
user_id: string;
user: string;
name: string;
content: string | null;
links: string[] | null;
@ -192,7 +191,7 @@ export type Note = {
export type Checklist = {
id: string;
user_id: string;
user: string;
name: string;
items: ChecklistItem[];
date: string | null; // ISO 8601 date string
@ -204,7 +203,7 @@ export type Checklist = {
export type ChecklistItem = {
id: string;
user_id: string;
user: string;
name: string;
is_checked: boolean;
checklist: number;
@ -235,8 +234,8 @@ export type Category = {
name: string;
display_name: string;
icon: string;
user_id: string;
num_adventures?: number | null;
user: string;
num_locations?: number | null;
};
export type ImmichIntegration = {
@ -277,15 +276,15 @@ export type ImmichAlbum = {
export type Attachment = {
id: string;
file: string;
adventure: string;
location: string;
extension: string;
user_id: string;
user: string;
name: string;
};
export type Lodging = {
id: string;
user_id: string;
user: string;
name: string;
type: string;
description: string | null;

View file

@ -14,18 +14,12 @@
"adventures": {
"activities": {},
"add_to_collection": "Zur Sammlung hinzufügen",
"adventure_delete_confirm": "Sind Sie sicher, dass Sie dieses Abenteuer löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.",
"collection_link_error": "Fehler beim Verknüpfen des Abenteuers mit der Sammlung",
"collection_link_success": "Abenteuer erfolgreich mit Sammlung verknüpft!",
"collection_remove_error": "Beim Entfernen des Abenteuers aus der Sammlung ist ein Fehler aufgetreten",
"collection_remove_success": "Abenteuer erfolgreich aus der Sammlung entfernt!",
"delete": "Löschen",
"edit_adventure": "Abenteuer bearbeiten",
"no_image_found": "Kein Bild gefunden",
"open_details": "Details",
"remove_from_collection": "Aus Sammlung entfernen",
"adventure": "Abenteuer",
"adventure_delete_success": "Abenteuer erfolgreich gelöscht!",
"archive": "Archiv",
"archived": "Archiviert",
"archived_collection_message": "Sammlung erfolgreich archiviert!",
@ -39,7 +33,6 @@
"count_txt": "Suchergebnisse",
"date": "Datum",
"dates": "Termine",
"delete_adventure": "Abenteuer löschen",
"delete_collection": "Sammlung löschen",
"delete_collection_success": "Sammlung erfolgreich gelöscht!",
"descending": "Absteigend",
@ -56,8 +49,6 @@
"my_collections": "Meine Sammlungen",
"name": "Name",
"no_image_url": "Unter dieser URL wurde kein Bild gefunden.",
"not_found": "Abenteuer nicht gefunden",
"not_found_desc": "Das von Ihnen gesuchte Abenteuer konnte nicht gefunden werden. \nBitte versuchen Sie ein anderes Abenteuer aus oder schauen Sie später noch mal vorbei.",
"open_filters": "Filter öffnen",
"order_by": "Sortieren nach",
"order_direction": "Sortierreihenfolge",
@ -80,10 +71,6 @@
"activity_types": "Aktivitätstypen",
"add": "Hinzufügen",
"add_notes": "Notizen hinzufügen",
"adventure_create_error": "Das Abenteuer konnte nicht erstellt werden",
"adventure_created": "Abenteuer erstellt",
"adventure_update_error": "Das Abenteuer konnte nicht aktualisiert werden",
"adventure_updated": "Abenteuer aktualisiert",
"basic_information": "Basisdaten",
"category": "Kategorie",
"clear_map": "Karte leeren",
@ -100,23 +87,19 @@
"location": "Standort",
"location_information": "Standortinformationen",
"my_images": "Meine Bilder",
"new_adventure": "Neues Abenteuer",
"no_description_found": "Keine Beschreibung gefunden",
"no_images": "Keine Bilder",
"no_location": "Bitte geben Sie einen Ort ein",
"no_results": "Keine Ergebnisse gefunden",
"public_adventure": "Öffentliches Abenteuer",
"remove": "Entfernen",
"save_next": "Speichern & weiter",
"search_for_location": "Nach einem Ort suchen",
"search_results": "Suchergebnisse",
"see_adventures": "Siehe Abenteuer",
"share_adventure": "Teilen Sie dieses Abenteuer!",
"start_date": "Startdatum",
"upload_image": "Bild hochladen",
"url": "URL",
"warning": "Warnung",
"wiki_desc": "Ruft einen Auszug aus einem Wikipedia-Artikel ab, der zum Namen des Abenteuers passt.",
"wikipedia": "Wikipedia",
"adventure_not_found": "Keine Abenteuer vorhanden. \nFügen Sie welche über die Plus-Schaltfläche unten rechts hinzu oder versuchen Sie, die Filter zu ändern!",
"all": "Alle",
@ -124,7 +107,6 @@
"mark_visited": "als besucht markieren",
"my_adventures": "Meine Abenteuer",
"no_adventures_found": "Keine Abenteuer gefunden",
"no_collections_found": "Es wurden keine Sammlungen gefunden, die zu diesem Abenteuer hinzugefügt werden können.",
"no_linkable_adventures": "Es wurden keine Abenteuer gefunden, die mit dieser Sammlung verknüpft werden können.",
"not_visited": "Nicht besucht",
"regions_updated": "Regionen aktualisiert",
@ -181,10 +163,7 @@
"starting_airport": "Startflughafen",
"to": "Nach",
"transportation_delete_confirm": "Sind Sie sicher, dass Sie diesen Transport löschen möchten? \nDies lässt sich nicht rückgängig machen.",
"will_be_marked": "wird als besucht markiert, sobald das Abenteuer gespeichert wird.",
"cities_updated": "Städte aktualisiert",
"create_adventure": "Erstelle Abenteuer",
"no_adventures_to_recommendations": "Keine Abenteuer gefunden. \nFügen Sie mindestens ein Abenteuer hinzu, um Empfehlungen zu erhalten.",
"finding_recommendations": "Entdecken Sie verborgene Schätze für Ihr nächstes Abenteuer",
"attachment": "Anhang",
"attachment_delete_success": "Anhang gelöscht!",
@ -246,7 +225,32 @@
"name_location": "Name, Ort",
"collection_contents": "Sammelinhalt",
"check_in": "Einchecken",
"check_out": "Kasse"
"check_out": "Kasse",
"collection_link_location_error": "Fehlerverknüpfungsort zur Sammlung",
"collection_link_location_success": "Standort, die mit der Sammlung erfolgreich verknüpft sind!",
"collection_locations": "Sammelorte einbeziehen",
"collection_remove_location_error": "Fehler zur Entfernung des Standorts aus der Sammlung",
"collection_remove_location_success": "Standort erfolgreich aus der Sammlung entfernt!",
"create_location": "Standort erstellen",
"delete_location": "Position löschen",
"edit_location": "Standort bearbeiten",
"location_create_error": "Der Standort erstellt nicht",
"location_created": "Ort erstellt",
"location_delete_confirm": "Sind Sie sicher, dass Sie diesen Ort löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.",
"location_delete_success": "Standort erfolgreich gelöscht!",
"location_not_found": "Ort nicht gefunden",
"location_not_found_desc": "Der Ort, den Sie gesucht haben, konnte nicht gefunden werden. \nBitte probieren Sie einen anderen Ort aus oder schauen Sie später noch einmal vorbei.",
"location_update_error": "Der Standort nicht aktualisiert",
"location_updated": "Standort aktualisiert",
"new_location": "Neuer Standort",
"no_collections_to_add_location": "Keine Sammlungen, die diesen Ort hinzuzufügen.",
"no_locations_to_recommendations": "Keine Standorte gefunden. \nFügen Sie mindestens einen Ort hinzu, um Empfehlungen zu erhalten.",
"public_location": "Öffentliche Lage",
"share_location": "Teilen Sie diesen Ort!",
"visit_calendar": "Besuchen Sie den Kalender",
"wiki_location_desc": "Zieht Auszug aus dem Wikipedia -Artikel, der dem Namen des Standorts entspricht.",
"will_be_marked_location": "wird als besucht markiert, sobald der Standort gespeichert ist.",
"no_locations_found": "Keine Standorte gefunden"
},
"home": {
"desc_1": "Entdecken, planen und erkunden Sie mühelos",
@ -317,10 +321,10 @@
"public_tooltip": "Mit einem öffentlichen Profil können Benutzer Sammlungen mit Ihnen teilen und Ihr Profil auf der Benutzerseite anzeigen.",
"new_password": "Neues Passwort",
"or_3rd_party": "Oder melden Sie sich bei einem Drittanbieter an",
"no_public_adventures": "Keine öffentlichen Abenteuer gefunden",
"no_public_collections": "Keine öffentlichen Sammlungen gefunden",
"user_adventures": "Benutzerabenteuer",
"user_collections": "Benutzersammlungen"
"user_collections": "Benutzersammlungen",
"no_public_locations": "Keine öffentlichen Standorte gefunden",
"user_locations": "Benutzerstandorte"
},
"users": {
"no_users_found": "Keine Benutzer mit öffentlichem Profil gefunden."
@ -568,7 +572,13 @@
"search": {
"adventurelog_results": "AdventureLog-Ergebnisse",
"online_results": "Online-Ergebnisse",
"public_adventures": "Öffentliche Abenteuer"
"public_adventures": "Öffentliche Abenteuer",
"cities": "Städte",
"countries": "Länder",
"found": "gefunden",
"result": "Ergebnis",
"results": "Ergebnisse",
"try_searching_desc": "Versuchen Sie, nach Abenteuern, Sammlungen, Ländern, Regionen, Städten oder Nutzern zu suchen."
},
"map": {
"add_adventure": "Neues Abenteuer hinzufügen",
@ -579,13 +589,16 @@
"show_visited_regions": "Besuchte Regionen anzeigen",
"view_details": "Details anzeigen",
"adventure_stats": "Abenteuerstatistiken",
"adventures_shown": "Abenteuer gezeigt",
"completion": "Fertigstellung",
"display_options": "Anzeigenoptionen",
"map_controls": "Kartensteuerungen",
"marker_placed_on_map": "Marker auf der Karte platziert",
"place_marker_desc": "Klicken Sie auf die Karte, um einen Marker zu platzieren, oder fügen Sie ein Abenteuer ohne Ort hinzu.",
"regions": "Regionen"
"regions": "Regionen",
"add_location": "Neuen Standort hinzufügen",
"add_location_at_marker": "Fügen Sie einen neuen Standort bei Marker hinzu",
"location_map": "Standortkarte",
"locations_shown": "Standorte gezeigt",
"place_marker_desc_location": "Klicken Sie auf die Karte, um einen Marker zu platzieren."
},
"languages": {},
"share": {
@ -611,9 +624,9 @@
"no_shared_adventures": "Dieser Benutzer hat noch keine öffentlichen Abenteuer geteilt.",
"no_shared_collections": "Dieser Benutzer hat noch keine öffentlichen Sammlungen geteilt.",
"planned_trips": "Geplante Reisen",
"public_adventure_experiences": "Öffentliche Abenteuererlebnisse",
"travel_statistics": "Reisestatistik",
"your_journey_at_a_glance": "Ihre Abenteuerreise auf einen Blick"
"your_journey_at_a_glance": "Ihre Abenteuerreise auf einen Blick",
"public_location_experiences": "Öffentliche Standortlebnisse"
},
"categories": {
"category_name": "Kategoriename",
@ -622,9 +635,9 @@
"manage_categories": "Kategorien verwalten",
"no_categories_found": "Keine Kategorien gefunden.",
"select_category": "Kategorie wählen",
"update_after_refresh": "Die Abenteuerkarten werden aktualisiert, sobald Sie die Seite aktualisieren.",
"add_new_category": "Neue Kategorie hinzufügen",
"name_required": "Der Kategorienname ist erforderlich"
"name_required": "Der Kategorienname ist erforderlich",
"location_update_after_refresh": "Die Standortkarten werden aktualisiert, sobald Sie die Seite aktualisiert haben."
},
"dashboard": {
"add_some": "Warum nicht gleich Ihr nächstes Abenteuer planen? Sie können ein neues Abenteuer hinzufügen, indem Sie auf den Button unten klicken.",
@ -670,9 +683,9 @@
"recomendations": {
"recommendation": "Empfehlung",
"recommendations": "Empfehlungen",
"adventure_recommendations": "Abenteuerempfehlungen",
"food": "Essen",
"tourism": "Tourismus"
"tourism": "Tourismus",
"location_recommendations": "Standortempfehlungen"
},
"lodging": {
"apartment": "Wohnung",
@ -695,17 +708,19 @@
"google_maps_integration_desc": "Verbinden Sie Ihr Google Maps-Konto, um hochwertige Suchergebnisse und Empfehlungen für Standort zu erhalten."
},
"calendar": {
"all_categories": "Alle Kategorien",
"all_day_event": "Ganztägige Veranstaltung",
"calendar_overview": "Kalenderübersicht",
"categories": "Kategorien",
"day": "Tag",
"events_scheduled": "Veranstaltungen geplant",
"filter_by_category": "Filter nach Kategorie",
"filtered_results": "Gefilterte Ergebnisse",
"month": "Monat",
"today": "Heute",
"total_events": "Gesamtereignisse",
"week": "Woche"
},
"locations": {
"location": "Standort",
"locations": "Standorte",
"my_locations": "Meine Standorte"
}
}

View file

@ -64,9 +64,9 @@
"latest_travel_experiences": "Your latest travel experiences"
},
"adventures": {
"collection_remove_success": "Adventure removed from collection successfully!",
"collection_remove_error": "Error removing adventure from collection",
"collection_link_success": "Adventure linked to collection successfully!",
"collection_remove_location_success": "Location removed from collection successfully!",
"collection_remove_location_error": "Error removing location from collection",
"collection_link_location_success": "Location linked to collection successfully!",
"invalid_date_range": "Invalid date range",
"timezone": "Timezone",
"no_visits": "No visits",
@ -75,8 +75,8 @@
"departure_date": "Departure Date",
"arrival_date": "Arrival Date",
"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.",
"collection_link_location_error": "Error linking location to collection",
"location_delete_confirm": "Are you sure you want to delete this location? This action cannot be undone.",
"checklist_delete_confirm": "Are you sure you want to delete this checklist? This action cannot be undone.",
"note_delete_confirm": "Are you sure you want to delete this note? This action cannot be undone.",
"transportation_delete_confirm": "Are you sure you want to delete this transportation? This action cannot be undone.",
@ -87,11 +87,12 @@
"delete_lodging": "Delete Lodging",
"open_details": "Open Details",
"edit_adventure": "Edit Adventure",
"edit_location": "Edit Location",
"remove_from_collection": "Remove from Collection",
"add_to_collection": "Add to Collection",
"delete": "Delete",
"not_found": "Adventure not found",
"not_found_desc": "The adventure you were looking for could not be found. Please try a different adventure or check back later.",
"location_not_found": "Location not found",
"location_not_found_desc": "The location you were looking for could not be found. Please try a different location or check back later.",
"homepage": "Homepage",
"collection": "Collection",
"longitude": "Longitude",
@ -122,7 +123,7 @@
"my_images": "My Images",
"no_images": "No Images",
"distance": "Distance",
"share_adventure": "Share this Adventure!",
"share_location": "Share this Location!",
"share_collection": "Share this Collection!",
"copy_link": "Copy Link",
"sun_times": "Sun Times",
@ -149,18 +150,19 @@
"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.",
"wiki_location_desc": "Pulls excerpt from Wikipedia article matching the name of the location.",
"attachments": "Attachments",
"attachment": "Attachment",
"images": "Images",
"generate_desc": "Generate Description",
"public_adventure": "Public Adventure",
"public_location": "Public Location",
"location_information": "Location Information",
"link": "Link",
"links": "Links",
"description": "Description",
"sources": "Sources",
"collection_adventures": "Include Collection Adventures",
"collection_locations": "Include Collection Locations",
"filter": "Filter",
"category_filter": "Category Filter",
"category": "Category",
@ -178,7 +180,7 @@
"edit_collection": "Edit Collection",
"unarchive": "Unarchive",
"archive": "Archive",
"no_collections_found": "No collections found to add this adventure to.",
"no_collections_to_add_location": "No collections found to add this location to.",
"create_collection_first": "Create a collection first to organize your adventures and memories.",
"done": "Done",
"adventures_available": "Adventures Available",
@ -190,8 +192,8 @@
"cancel": "Cancel",
"delete_collection_warning": "Are you sure you want to delete this collection? This action cannot be undone.",
"delete_collection": "Delete Collection",
"delete_adventure": "Delete Adventure",
"adventure_delete_success": "Adventure deleted successfully!",
"delete_location": "Delete Location",
"location_delete_success": "Location deleted successfully!",
"visited": "Visited",
"planned": "Planned",
"duration": "Duration",
@ -209,21 +211,22 @@
"image_fetch_failed": "Failed to fetch image",
"no_location": "Please enter a location",
"no_description_found": "No description found",
"adventure_created": "Adventure created",
"adventure_create_error": "Failed to create adventure",
"location_created": "Location created",
"location_create_error": "Failed to create location",
"lodging": "Lodging",
"create_adventure": "Create Adventure",
"adventure_updated": "Adventure updated",
"adventure_update_error": "Failed to update adventure",
"create_location": "Create Location",
"location_updated": "Location updated",
"location_update_error": "Failed to update location",
"set_to_pin": "Set to Pin",
"category_fetch_error": "Error fetching categories",
"new_adventure": "New Adventure",
"new_location": "New Location",
"basic_information": "Basic Information",
"no_adventures_to_recommendations": "No adventures found. Add at least one adventure to get recommendations.",
"no_locations_to_recommendations": "No locations found. Add at least one location to get recommendations.",
"display_name": "Display Name",
"adventure_not_found": "There are no adventures to display. Add some using the plus button at the bottom right or try changing filters!",
"collection_contents": "Collection Contents",
"no_adventures_found": "No adventures found",
"no_locations_found": "No locations found",
"no_adventures_message": "Start documenting your adventures and planning new ones. Every journey has a story worth telling.",
"mark_visited": "Mark Visited",
"error_updating_regions": "Error updating regions",
@ -248,6 +251,7 @@
"checklists": "Checklists",
"transportations": "Transportations",
"adventure_calendar": "Adventure Calendar",
"visit_calendar": "Visit Calendar",
"day": "Day",
"itineary_by_date": "Itinerary by Date",
"nothing_planned": "Nothing planned for this day. Enjoy the journey!",
@ -263,7 +267,7 @@
"no_location_found": "No location found",
"from": "From",
"to": "To",
"will_be_marked": "will be marked as visited once the adventure is saved.",
"will_be_marked_location": "will be marked as visited once the location is saved.",
"start": "Start",
"end": "End",
"emoji_picker": "Emoji Picker",
@ -373,9 +377,9 @@
"public_tooltip": "With a public profile, users can share collections with you and view your profile on the users page.",
"new_password": "New Password (6+ characters)",
"or_3rd_party": "Or login with a third-party service",
"no_public_adventures": "No public adventures found",
"no_public_locations": "No public locations found",
"no_public_collections": "No public collections found",
"user_adventures": "User Adventures",
"user_locations": "User Locations",
"user_collections": "User Collections"
},
"users": {
@ -585,24 +589,33 @@
"search": {
"adventurelog_results": "AdventureLog Results",
"public_adventures": "Public Adventures",
"online_results": "Online Results"
"online_results": "Online Results",
"result": "Result",
"results": "Results",
"found": "found",
"try_searching_desc": "Try searching for adventures, collections, countries, regions, cities, or users.",
"countries": "Countries",
"cities": "Cities"
},
"map": {
"view_details": "View Details",
"adventure_map": "Adventure Map",
"location_map": "Location Map",
"map_options": "Map Options",
"show_visited_regions": "Show Visited Regions",
"add_adventure_at_marker": "Add New Adventure at Marker",
"add_location_at_marker": "Add New Location at Marker",
"clear_marker": "Clear Marker",
"add_adventure": "Add New Adventure",
"add_location": "Add New Location",
"adventure_stats": "Adventure Stats",
"map_controls": "Map Controls",
"regions": "Regions",
"completion": "Completion",
"display_options": "Display Options",
"marker_placed_on_map": "Marker placed on map",
"place_marker_desc": "Click on the map to place a marker, or add an adventure without location.",
"adventures_shown": "adventures shown"
"place_marker_desc_location": "Click on the map to place a marker.",
"locations_shown": "locations shown"
},
"share": {
"shared": "Shared",
@ -628,7 +641,7 @@
"planned_trips": "Planned trips",
"discovered": "discovered",
"explored": "explored",
"public_adventure_experiences": "Public adventure experiences",
"public_location_experiences": "Public location experiences",
"no_shared_adventures": "This user hasn't shared any public adventures yet.",
"no_shared_collections": "This user hasn't shared any public collections yet."
},
@ -637,7 +650,7 @@
"no_categories_found": "No categories found.",
"edit_category": "Edit Category",
"icon": "Icon",
"update_after_refresh": "The adventure cards will be updated once you refresh the page.",
"location_update_after_refresh": "The location cards will be updated once you refresh the page.",
"select_category": "Select Category",
"category_name": "Category Name",
"add_new_category": "Add New Category",
@ -690,7 +703,7 @@
"recomendations": {
"recommendation": "Recommendation",
"recommendations": "Recommendations",
"adventure_recommendations": "Adventure Recommendations",
"location_recommendations": "Location Recommendations",
"food": "Food",
"tourism": "Tourism"
},
@ -701,11 +714,13 @@
"day": "Day",
"events_scheduled": "events scheduled",
"total_events": "Total Events",
"all_categories": "All Categories",
"calendar_overview": "Calendar Overview",
"categories": "Categories",
"filtered_results": "Filtered Results",
"filter_by_category": "Filter by Category",
"all_day_event": "All Day Event"
},
"locations": {
"location": "Location",
"locations": "Locations",
"my_locations": "My Locations"
}
}

View file

@ -64,11 +64,6 @@
"of_world": "del mundo"
},
"adventures": {
"collection_remove_success": "¡Aventura eliminada de la colección con éxito!",
"collection_remove_error": "Error al eliminar la aventura de la colección",
"collection_link_success": "¡Aventura vinculada a la colección con éxito!",
"collection_link_error": "Error al vincular la aventura a la colección",
"adventure_delete_confirm": "¿Estás seguro de que quieres eliminar esta aventura? Esta acción no se puede deshacer.",
"open_details": "Abrir Detalles",
"edit_adventure": "Editar Aventura",
"remove_from_collection": "Eliminar de la Colección",
@ -80,8 +75,6 @@
"homepage": "Página principal",
"latitude": "Latitud",
"longitude": "Longitud",
"not_found": "Aventura no encontrada",
"not_found_desc": "La aventura que buscabas no se pudo encontrar. \nPruebe una aventura diferente o vuelva a consultar más tarde.",
"visit": "Visita",
"visits": "Visitas",
"adventure": "Aventura",
@ -116,8 +109,6 @@
"share": "Compartir",
"unarchive": "Desarchivar",
"cancel": "Cancelar",
"adventure_delete_success": "¡Aventura eliminada con éxito!",
"delete_adventure": "Eliminar aventura",
"planned": "Planificado",
"visited": "Visitado",
"dates": "Fechas",
@ -134,10 +125,6 @@
"activity_types": "Tipos de actividad",
"add": "Agregar",
"add_notes": "Agregar notas",
"adventure_create_error": "No se pudo crear la aventura",
"adventure_created": "Aventura creada",
"adventure_update_error": "No se pudo actualizar la aventura",
"adventure_updated": "Aventura actualizada",
"basic_information": "Información básica",
"category": "Categoría",
"clear_map": "Limpiar mapa",
@ -153,26 +140,21 @@
"location": "Ubicación",
"location_information": "Información de ubicación",
"my_images": "Mis imágenes",
"new_adventure": "Nueva aventura",
"no_description_found": "No se encontró ninguna descripción",
"no_images": "Sin imágenes",
"no_location": "Por favor ingresa una ubicación",
"no_results": "No se encontraron resultados",
"public_adventure": "Aventura pública",
"remove": "Eliminar",
"save_next": "Guardar y Siguiente",
"search_for_location": "Buscar una ubicación",
"search_results": "Resultados de búsqueda",
"share_adventure": "¡Comparte esta aventura!",
"start_date": "Fecha de inicio",
"upload_image": "Subir imagen",
"url": "URL",
"warning": "Advertencia",
"wiki_desc": "Extrae un extracto de un artículo de Wikipedia que coincide con el nombre de la aventura.",
"wikipedia": "Wikipedia",
"adventure_not_found": "No hay aventuras que mostrar. \n¡Agregue algunas usando el botón más en la parte inferior derecha o intente cambiar los filtros!",
"no_adventures_found": "No se encontraron aventuras",
"no_collections_found": "No se encontraron colecciones para agregar esta aventura.",
"my_adventures": "Mis aventuras",
"no_linkable_adventures": "No se encontraron aventuras que puedan vincularse a esta colección.",
"mark_visited": "Marcar como visitado",
@ -233,10 +215,7 @@
"starting_airport": "Aeropuerto de inicio",
"to": "A",
"transportation_delete_confirm": "¿Está seguro de que desea eliminar este transporte? \nEsta acción no se puede deshacer.",
"will_be_marked": "se marcará como visitado una vez guardada la aventura.",
"cities_updated": "ciudades actualizadas",
"create_adventure": "Crear aventura",
"no_adventures_to_recommendations": "No se encontraron aventuras. \nAñade al menos una aventura para obtener recomendaciones.",
"finding_recommendations": "Descubriendo gemas ocultas para tu próxima aventura",
"attachment": "Adjunto",
"attachment_delete_success": "¡El archivo adjunto se eliminó exitosamente!",
@ -297,8 +276,33 @@
"loading_adventures": "Cargando aventuras ...",
"name_location": "Nombre, ubicación",
"collection_contents": "Contenido de la colección",
"check_in": "Entrada",
"check_out": "Salida"
"check_in": "Registrarse",
"check_out": "Verificar",
"collection_link_location_error": "Error de vinculación de la ubicación para la recopilación",
"collection_link_location_success": "¡Ubicación vinculada a la colección con éxito!",
"collection_locations": "Incluir ubicaciones de colección",
"collection_remove_location_error": "Error de eliminación de la ubicación de la colección",
"collection_remove_location_success": "¡Ubicación eliminada de la colección con éxito!",
"create_location": "Crear ubicación",
"delete_location": "Eliminar la ubicación",
"edit_location": "Ubicación de edición",
"location_create_error": "No se pudo crear la ubicación",
"location_created": "Ubicación creada",
"location_delete_confirm": "¿Estás seguro de que quieres eliminar esta ubicación? \nEsta acción no se puede deshacer.",
"location_delete_success": "Ubicación eliminada con éxito!",
"location_not_found": "Ubicación no encontrada",
"location_not_found_desc": "No se podía encontrar la ubicación que estaba buscando. \nPruebe una ubicación diferente o vuelva a consultar más tarde.",
"location_update_error": "No se pudo actualizar la ubicación",
"location_updated": "Ubicación actualizada",
"new_location": "Nueva ubicación",
"no_collections_to_add_location": "No se encuentran colecciones para agregar esta ubicación a.",
"no_locations_to_recommendations": "No se encontraron ubicaciones. \nAgregue al menos una ubicación para obtener recomendaciones.",
"public_location": "Ubicación pública",
"share_location": "¡Comparte esta ubicación!",
"visit_calendar": "Visitar el calendario",
"wiki_location_desc": "Extrae extracto del artículo de Wikipedia que coincide con el nombre de la ubicación.",
"will_be_marked_location": "se marcará según lo visitado una vez que se guarde la ubicación.",
"no_locations_found": "No se encontraron ubicaciones"
},
"worldtravel": {
"all": "Todo",
@ -373,10 +377,10 @@
"public_tooltip": "Con un perfil público, los usuarios pueden compartir colecciones con usted y ver su perfil en la página de usuarios.",
"new_password": "Nueva contraseña",
"or_3rd_party": "O inicie sesión con un servicio de terceros",
"no_public_adventures": "No se encontraron aventuras públicas",
"no_public_collections": "No se encontraron colecciones públicas",
"user_adventures": "Aventuras de usuario",
"user_collections": "Colecciones de usuarios"
"user_collections": "Colecciones de usuarios",
"no_public_locations": "No se encontraron ubicaciones públicas",
"user_locations": "Ubicación de usuarios"
},
"users": {
"no_users_found": "No se encontraron usuarios con perfiles públicos."
@ -568,7 +572,13 @@
"search": {
"adventurelog_results": "Resultados del registro de aventuras",
"online_results": "Resultados en línea",
"public_adventures": "Aventuras públicas"
"public_adventures": "Aventuras públicas",
"cities": "Ciudades",
"countries": "Países",
"found": "encontró",
"result": "Resultado",
"results": "Resultados",
"try_searching_desc": "Intente buscar aventuras, colecciones, países, regiones, ciudades o usuarios."
},
"map": {
"add_adventure": "Agregar nueva aventura",
@ -583,9 +593,12 @@
"display_options": "Opciones de visualización",
"map_controls": "Controles de mapa",
"marker_placed_on_map": "Marcador colocado en el mapa",
"place_marker_desc": "Haga clic en el mapa para colocar un marcador o agregar una aventura sin ubicación.",
"regions": "Regiones",
"adventures_shown": "aventuras mostradas"
"add_location": "Agregar nueva ubicación",
"add_location_at_marker": "Agregar nueva ubicación en el marcador",
"location_map": "Mapa de ubicación",
"locations_shown": "ubicaciones mostradas",
"place_marker_desc_location": "Haga clic en el mapa para colocar un marcador."
},
"share": {
"no_users_shared": "Ningún usuario compartió con",
@ -611,9 +624,9 @@
"no_shared_adventures": "Este usuario aún no ha compartido ninguna aventura pública.",
"no_shared_collections": "Este usuario aún no ha compartido ninguna colección pública.",
"planned_trips": "Viajes planificados",
"public_adventure_experiences": "Experiencias de aventura pública",
"travel_statistics": "Estadísticas de viaje",
"your_journey_at_a_glance": "Tu viaje de aventura a un vistazo"
"your_journey_at_a_glance": "Tu viaje de aventura a un vistazo",
"public_location_experiences": "Experiencias de ubicación pública"
},
"categories": {
"category_name": "Nombre de categoría",
@ -622,9 +635,9 @@
"manage_categories": "Administrar categorías",
"no_categories_found": "No se encontraron categorías.",
"select_category": "Seleccionar categoría",
"update_after_refresh": "Las tarjetas de aventuras se actualizarán una vez que actualices la página.",
"add_new_category": "Agregar nueva categoría",
"name_required": "Se requiere el nombre de la categoría"
"name_required": "Se requiere el nombre de la categoría",
"location_update_after_refresh": "Las tarjetas de ubicación se actualizarán una vez que actualice la página."
},
"dashboard": {
"add_some": "¿Por qué no empezar a planificar tu próxima aventura? \nPuedes agregar una nueva aventura haciendo clic en el botón de abajo.",
@ -670,9 +683,9 @@
"recomendations": {
"recommendation": "Recomendación",
"recommendations": "Recomendaciones",
"adventure_recommendations": "Recomendaciones de aventura",
"food": "Comida",
"tourism": "Turismo"
"tourism": "Turismo",
"location_recommendations": "Recomendaciones de ubicación"
},
"lodging": {
"apartment": "Apartamento",
@ -695,17 +708,19 @@
"google_maps_integration_desc": "Conecte su cuenta de Google Maps para obtener resultados y recomendaciones de búsqueda de ubicación de alta calidad."
},
"calendar": {
"all_categories": "Todas las categorías",
"all_day_event": "Evento todo el día",
"calendar_overview": "Descripción general del calendario",
"categories": "Categorías",
"day": "Día",
"events_scheduled": "Eventos programados",
"filter_by_category": "Filtrar por categoría",
"filtered_results": "Resultados filtrados",
"month": "Mes",
"today": "Hoy",
"total_events": "Total de eventos",
"week": "Semana"
},
"locations": {
"location": "Ubicación",
"locations": "Ubicación",
"my_locations": "Mis ubicaciones"
}
}

View file

@ -14,18 +14,12 @@
"adventures": {
"activities": {},
"add_to_collection": "Ajouter à la collection",
"adventure_delete_confirm": "Êtes-vous sûr de vouloir supprimer cette aventure ? \nCette action ne peut pas être annulée.",
"collection_link_error": "Erreur lors de la liaison de l'aventure à la collection",
"collection_link_success": "Aventure liée à la collection avec succès !",
"collection_remove_error": "Erreur lors de la suppression de l'aventure de la collection",
"collection_remove_success": "Aventure supprimée de la collection avec succès !",
"delete": "Supprimer",
"edit_adventure": "Modifier l'aventure",
"no_image_found": "Aucune image trouvée",
"open_details": "Ouvrir les détails",
"remove_from_collection": "Supprimer de la collection",
"adventure": "Aventure",
"adventure_delete_success": "Aventure supprimée avec succès !",
"archive": "Archiver",
"archived": "Archivée",
"archived_collection_message": "Collection archivée avec succès !",
@ -40,7 +34,6 @@
"create_new": "Créer une nouvelle aventure...",
"date": "Date",
"dates": "Dates",
"delete_adventure": "Supprimer l'aventure",
"delete_collection": "Supprimer la collection",
"delete_collection_success": "Collection supprimée avec succès !",
"descending": "Descendant",
@ -57,8 +50,6 @@
"my_collections": "Mes collections",
"name": "Nom",
"no_image_url": "Aucune image trouvée à cette URL.",
"not_found": "Aventure introuvable",
"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": "Trier par",
"order_direction": "Direction du tri",
@ -81,10 +72,6 @@
"activity_types": "Types d'activités",
"add": "Ajouter",
"add_notes": "Ajouter des notes",
"adventure_create_error": "Échec de la création de l'aventure",
"adventure_created": "Aventure créée",
"adventure_update_error": "Échec de la mise à jour de l'aventure",
"adventure_updated": "Aventure mise à jour",
"basic_information": "Informations de base",
"category": "Catégorie",
"clear_map": "Effacer la carte",
@ -100,23 +87,19 @@
"location": "Lieu",
"location_information": "Informations de localisation",
"my_images": "Mes images",
"new_adventure": "Nouvelle aventure",
"no_description_found": "Aucune description trouvée",
"no_images": "Aucune image",
"no_location": "Veuillez entrer un emplacement",
"no_results": "Aucun résultat trouvé",
"public_adventure": "Aventure publique",
"remove": "Retirer",
"save_next": "Sauvegarder",
"search_for_location": "Rechercher un lieu",
"search_results": "Résultats de la recherche",
"see_adventures": "Voir les aventures",
"share_adventure": "Partagez cette aventure !",
"start_date": "Date de début",
"upload_image": "Télécharger une image",
"url": "URL",
"warning": "Avertissement",
"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 '+' en bas à droite ou essayez de changer les filtres !",
"all": "Tous",
@ -124,7 +107,6 @@
"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ée",
"regions_updated": "régions mises à jour",
@ -181,10 +163,7 @@
"starting_airport": "Aéroport de départ",
"to": "Vers",
"transportation_delete_confirm": "Etes-vous sûr de vouloir supprimer ce transport ? \nCette action ne peut pas être annulée.",
"will_be_marked": "sera marqué comme visité une fois laventure sauvegardée.",
"cities_updated": "villes mises à jour",
"create_adventure": "Créer une aventure",
"no_adventures_to_recommendations": "Aucune aventure trouvée. \nAjoutez au moins une aventure pour obtenir des recommandations.",
"finding_recommendations": "Découvrir des trésors cachés pour votre prochaine aventure",
"attachment": "Pièce jointe",
"attachment_delete_success": "Pièce jointe supprimée avec succès !",
@ -246,7 +225,32 @@
"name_location": "nom, emplacement",
"collection_contents": "Contenu de la collection",
"check_in": "Enregistrement",
"check_out": "Vérifier"
"check_out": "Vérifier",
"collection_link_location_error": "Erreur liant l'emplacement à la collection",
"collection_link_location_success": "Emplacement lié à la collection avec succès!",
"collection_locations": "Inclure les emplacements de collecte",
"collection_remove_location_error": "Erreur de suppression de l'emplacement de la collection",
"collection_remove_location_success": "Emplacement retiré de la collection avec succès!",
"create_location": "Créer un emplacement",
"delete_location": "Supprimer l'emplacement",
"edit_location": "Modifier l'emplacement",
"location_create_error": "Échec de la création de l'emplacement",
"location_created": "Emplacement créé",
"location_delete_confirm": "Êtes-vous sûr de vouloir supprimer cet emplacement? \nCette action ne peut pas être annulée.",
"location_delete_success": "Emplacement supprimé avec succès!",
"location_not_found": "Emplacement introuvable",
"location_not_found_desc": "L'emplacement que vous recherchiez n'a pas pu être trouvé. \nVeuillez essayer un autre emplacement ou revenir plus tard.",
"location_update_error": "Échec de la mise à jour de l'emplacement",
"location_updated": "Emplacement mis à jour",
"new_location": "Nouvel emplacement",
"no_collections_to_add_location": "Aucune collection n'a été trouvée pour ajouter cet emplacement à.",
"no_locations_to_recommendations": "Aucun emplacement trouvé. \nAjoutez au moins un emplacement pour obtenir des recommandations.",
"public_location": "Lieu public",
"share_location": "Partagez cet emplacement!",
"visit_calendar": "Visiter le calendrier",
"wiki_location_desc": "Tire un extrait de l'article de Wikipedia correspondant au nom de l'emplacement.",
"will_be_marked_location": "sera marqué comme visité une fois l'emplacement enregistré.",
"no_locations_found": "Aucun emplacement trouvé"
},
"home": {
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
@ -317,10 +321,10 @@
"public_tooltip": "Avec un profil public, les utilisateurs peuvent partager des collections avec vous et afficher votre profil sur la page des utilisateurs.",
"new_password": "Nouveau 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 de l'utilisateur",
"user_collections": "Collections de l'utilisateur"
"user_collections": "Collections de l'utilisateur",
"no_public_locations": "Aucun emplacement public trouvé",
"user_locations": "Emplacements des utilisateurs"
},
"users": {
"no_users_found": "Aucun utilisateur trouvé avec un profil public."
@ -568,7 +572,13 @@
"search": {
"adventurelog_results": "Résultats dans AdventureLog",
"online_results": "Résultats en ligne",
"public_adventures": "Aventures publiques"
"public_adventures": "Aventures publiques",
"cities": "Villes",
"countries": "Pays",
"found": "trouvé",
"result": "Résultat",
"results": "Résultats",
"try_searching_desc": "Essayez de rechercher des aventures, des collections, des pays, des régions, des villes ou des utilisateurs."
},
"map": {
"add_adventure": "Ajouter une nouvelle aventure",
@ -579,13 +589,16 @@
"show_visited_regions": "Afficher les régions visitées",
"view_details": "Afficher les détails",
"adventure_stats": "Statistiques d'aventure",
"adventures_shown": "aventures montrées",
"completion": "Achèvement",
"display_options": "Options d'affichage",
"map_controls": "Contrôles de cartes",
"marker_placed_on_map": "Marqueur placé sur la carte",
"place_marker_desc": "Cliquez sur la carte pour placer un marqueur ou ajouter une aventure sans emplacement.",
"regions": "Régions"
"regions": "Régions",
"add_location": "Ajouter un nouvel emplacement",
"add_location_at_marker": "Ajouter un nouvel emplacement chez Marker",
"location_map": "Carte de localisation",
"locations_shown": "Emplacements montrés",
"place_marker_desc_location": "Cliquez sur la carte pour placer un marqueur."
},
"languages": {},
"share": {
@ -611,9 +624,9 @@
"no_shared_adventures": "Cet utilisateur n'a encore partagé aucune aventure publique.",
"no_shared_collections": "Cet utilisateur n'a pas encore partagé de collections publiques.",
"planned_trips": "Voyages prévus",
"public_adventure_experiences": "Expériences d'aventure publique",
"travel_statistics": "Statistiques de voyage",
"your_journey_at_a_glance": "Votre voyage d'aventure en un coup d'œil"
"your_journey_at_a_glance": "Votre voyage d'aventure en un coup d'œil",
"public_location_experiences": "Expériences de localisation publique"
},
"categories": {
"category_name": "Nom de la catégorie",
@ -622,9 +635,9 @@
"manage_categories": "Gérer les catégories",
"no_categories_found": "Aucune catégorie trouvée.",
"select_category": "Sélectionnez une catégorie",
"update_after_refresh": "Les cartes d'aventure seront mises à jour une fois que vous aurez actualisé la page.",
"add_new_category": "Ajouter une nouvelle catégorie",
"name_required": "Le nom de catégorie est requis"
"name_required": "Le nom de catégorie est requis",
"location_update_after_refresh": "Les cartes de localisation seront mises à jour une fois que vous avez actualisé la page."
},
"dashboard": {
"add_some": "Pourquoi ne pas commencer à planifier votre prochaine aventure ? \nVous pouvez ajouter une nouvelle aventure en cliquant sur le bouton ci-dessous.",
@ -670,9 +683,9 @@
"recomendations": {
"recommendation": "Recommandation",
"recommendations": "Recommandations",
"adventure_recommendations": "Recommandations d'aventure",
"food": "Nourriture",
"tourism": "Tourisme"
"tourism": "Tourisme",
"location_recommendations": "Recommandations de localisation"
},
"lodging": {
"apartment": "Appartement",
@ -695,17 +708,19 @@
"google_maps_integration_desc": "Connectez votre compte Google Maps pour obtenir des résultats de recherche et recommandations de recherche de haute qualité."
},
"calendar": {
"all_categories": "Toutes les catégories",
"all_day_event": "Événement toute la journée",
"calendar_overview": "Aperçu du calendrier",
"categories": "Catégories",
"day": "Jour",
"events_scheduled": "événements prévus",
"filter_by_category": "Filtre par catégorie",
"filtered_results": "Résultats filtrés",
"month": "Mois",
"today": "Aujourd'hui",
"total_events": "Événements totaux",
"week": "Semaine"
},
"locations": {
"location": "Emplacement",
"locations": "Lieux",
"my_locations": "Mes emplacements"
}
}

View file

@ -15,7 +15,6 @@
"activities": {},
"add_to_collection": "Aggiungi alla collezione",
"adventure": "Avventura",
"adventure_delete_confirm": "Sei sicuro di voler eliminare questa avventura? \nQuesta azione non può essere annullata.",
"archive": "Archivio",
"archived": "Archiviato",
"archived_collection_message": "Collezione archiviata con successo!",
@ -25,9 +24,6 @@
"category_filter": "Filtro categoria",
"clear": "Rimuovere",
"collection": "Collezione",
"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",
@ -44,8 +40,6 @@
"my_collections": "Le mie collezioni",
"name": "Nome",
"no_image_found": "Nessuna immagine trovata",
"not_found": "Avventura non trovata",
"not_found_desc": "L'avventura che stavi cercando non è stata trovata. \nProva un'avventura diversa o riprova più tardi.",
"open_details": "Apri Dettagli",
"open_filters": "Apri filtri",
"order_by": "Ordina per",
@ -62,11 +56,8 @@
"updated": "Aggiornato",
"visit": "Visita",
"visits": "Visite",
"adventure_delete_success": "Avventura eliminata 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",
"image_removed_error": "Errore durante la rimozione dell'immagine",
"image_removed_success": "Immagine rimossa con successo!",
@ -80,10 +71,6 @@
"activity_types": "Tipi di attività",
"add": "Aggiungere",
"add_notes": "Aggiungi note",
"adventure_create_error": "Impossibile creare l'avventura",
"adventure_created": "Avventura creata",
"adventure_update_error": "Impossibile aggiornare l'avventura",
"adventure_updated": "Avventura aggiornata",
"basic_information": "Informazioni di base",
"category": "Categoria",
"clear_map": "Libera mappa",
@ -99,23 +86,19 @@
"location": "Posizione",
"location_information": "Informazioni sulla posizione",
"my_images": "Le mie immagini",
"new_adventure": "Nuova avventura",
"no_description_found": "Nessuna descrizione trovata",
"no_images": "Nessuna immagine",
"no_location": "Inserisci una località",
"no_results": "Nessun risultato trovato",
"public_adventure": "Avventura pubblica",
"remove": "Rimuovere",
"save_next": "Salva",
"search_for_location": "Cerca una posizione",
"search_results": "Risultati della ricerca",
"see_adventures": "Vedi Avventure",
"share_adventure": "Condividi questa avventura!",
"start_date": "Data di inizio",
"upload_image": "Carica immagine",
"url": "URL",
"warning": "Avvertimento",
"wiki_desc": "Estrae un estratto dall'articolo di Wikipedia corrispondente al nome dell'avventura.",
"wiki_image_error": "Errore durante il recupero dell'immagine da Wikipedia",
"wikipedia": "Wikipedia",
"adventure_not_found": "Non ci sono avventure da visualizzare. \nAggiungine alcuni utilizzando il pulsante più in basso a destra o prova a cambiare i filtri!",
@ -124,7 +107,6 @@
"mark_visited": "Segna come visitato",
"my_adventures": "Le mie avventure",
"no_adventures_found": "Nessuna avventura trovata",
"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",
@ -181,10 +163,7 @@
"starting_airport": "Aeroporto di partenza",
"to": "A",
"transportation_delete_confirm": "Sei sicuro di voler eliminare questo trasporto? \nQuesta azione non può essere annullata.",
"will_be_marked": "verrà contrassegnato come visitato una volta salvata l'avventura.",
"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 tesori nascosti per la tua prossima avventura",
"attachment": "Allegato",
"attachment_delete_success": "Allegato eliminato con successo!",
@ -246,7 +225,32 @@
"name_location": "Nome, posizione",
"collection_contents": "Contenuto di raccolta",
"check_in": "Check -in",
"check_out": "Guardare"
"check_out": "Guardare",
"collection_link_location_error": "Errore che collega la posizione alla raccolta",
"collection_link_location_success": "Posizione collegata alla raccolta con successo!",
"collection_locations": "Includi luoghi di raccolta",
"collection_remove_location_error": "Errore di rimozione della posizione dalla raccolta",
"collection_remove_location_success": "Posizione rimossa dalla raccolta con successo!",
"create_location": "Crea posizione",
"delete_location": "Elimina posizione",
"edit_location": "Modifica posizione",
"location_create_error": "Impossibile creare posizione",
"location_created": "Posizione creata",
"location_delete_confirm": "Sei sicuro di voler eliminare questa posizione? \nQuesta azione non può essere annullata.",
"location_delete_success": "Posizione eliminata con successo!",
"location_not_found": "Posizione non trovata",
"location_not_found_desc": "Non è stato possibile trovare la posizione che stavi cercando. \nProva una posizione diversa o ricontrolla più tardi.",
"location_update_error": "Impossibile aggiornare la posizione",
"location_updated": "Posizione aggiornata",
"new_location": "Nuova posizione",
"no_collections_to_add_location": "Nessuna collezione trovata per aggiungere questa posizione a.",
"no_locations_to_recommendations": "Nessuna posizione trovata. \nAggiungi almeno una posizione per ottenere consigli.",
"public_location": "Posizione pubblica",
"share_location": "Condividi questa posizione!",
"visit_calendar": "Visita il calendario",
"wiki_location_desc": "Estratto dall'articolo di Wikipedia che corrisponde al nome della posizione.",
"will_be_marked_location": "sarà contrassegnato come visitato una volta salvata la posizione.",
"no_locations_found": "Nessuna posizione trovata"
},
"home": {
"desc_1": "Scopri, pianifica ed esplora con facilità",
@ -317,10 +321,10 @@
"public_tooltip": "Con un profilo pubblico, gli utenti possono condividere raccolte con te e visualizzare il tuo profilo nella pagina degli utenti.",
"new_password": "Nuova password",
"or_3rd_party": "Oppure accedi con un servizio di terze parti",
"no_public_adventures": "Nessuna avventura pubblica trovata",
"no_public_collections": "Nessuna collezione pubblica trovata",
"user_adventures": "Avventure utente",
"user_collections": "Collezioni utente"
"user_collections": "Collezioni utente",
"no_public_locations": "Nessuna posizione pubblica trovata",
"user_locations": "Posizioni degli utenti"
},
"users": {
"no_users_found": "Nessun utente trovato con profili pubblici."
@ -568,7 +572,13 @@
"search": {
"adventurelog_results": "Risultati di AdventureLog",
"online_results": "Risultati in linea",
"public_adventures": "Avventure pubbliche"
"public_adventures": "Avventure pubbliche",
"cities": "Città",
"countries": "Paesi",
"found": "trovato",
"result": "Risultato",
"results": "Risultati",
"try_searching_desc": "Prova a cercare avventure, collezioni, paesi, regioni, città o utenti."
},
"map": {
"add_adventure": "Aggiungi nuova avventura",
@ -579,13 +589,16 @@
"show_visited_regions": "Mostra regioni visitate",
"view_details": "Visualizza dettagli",
"adventure_stats": "Statistiche di avventura",
"adventures_shown": "Avventure mostrate",
"completion": "Completamento",
"display_options": "Opzioni di visualizzazione",
"map_controls": "Controlli della mappa",
"marker_placed_on_map": "Marcatore posizionato sulla mappa",
"place_marker_desc": "Fai clic sulla mappa per posizionare un indicatore o aggiungi un'avventura senza posizione.",
"regions": "Regioni"
"regions": "Regioni",
"add_location": "Aggiungi nuova posizione",
"add_location_at_marker": "Aggiungi nuova posizione al marcatore",
"location_map": "Mappa della posizione",
"locations_shown": "posizioni mostrate",
"place_marker_desc_location": "Fai clic sulla mappa per posizionare un indicatore."
},
"languages": {},
"share": {
@ -611,9 +624,9 @@
"no_shared_adventures": "Questo utente non ha ancora condiviso avventure pubbliche.",
"no_shared_collections": "Questo utente non ha ancora condiviso alcuna collezione pubblica.",
"planned_trips": "Viaggi pianificati",
"public_adventure_experiences": "Esperienze di avventura pubblica",
"travel_statistics": "Statistiche di viaggio",
"your_journey_at_a_glance": "Il tuo viaggio d'avventura a colpo d'occhio"
"your_journey_at_a_glance": "Il tuo viaggio d'avventura a colpo d'occhio",
"public_location_experiences": "Esperienze di posizione pubblica"
},
"categories": {
"category_name": "Nome della categoria",
@ -622,9 +635,9 @@
"manage_categories": "Gestisci categorie",
"no_categories_found": "Nessuna categoria trovata.",
"select_category": "Seleziona Categoria",
"update_after_refresh": "Le carte avventura verranno aggiornate una volta aggiornata la pagina.",
"add_new_category": "Aggiungi nuova categoria",
"name_required": "È richiesto il nome della categoria"
"name_required": "È richiesto il nome della categoria",
"location_update_after_refresh": "Le schede di posizione verranno aggiornate dopo aver aggiornato la pagina."
},
"dashboard": {
"add_some": "Perché non iniziare a pianificare la tua prossima avventura? \nPuoi aggiungere una nuova avventura facendo clic sul pulsante in basso.",
@ -670,9 +683,9 @@
"recomendations": {
"recommendation": "Raccomandazione",
"recommendations": "Raccomandazioni",
"adventure_recommendations": "Consigli di avventura",
"food": "Cibo",
"tourism": "Turismo"
"tourism": "Turismo",
"location_recommendations": "Raccomandazioni sulla posizione"
},
"lodging": {
"apartment": "Appartamento",
@ -695,17 +708,19 @@
"google_maps_integration_desc": "Collega il tuo account Google Maps per ottenere risultati e consigli di ricerca sulla posizione di alta qualità."
},
"calendar": {
"all_categories": "Tutte le categorie",
"all_day_event": "Evento per tutto il giorno",
"calendar_overview": "Panoramica del calendario",
"categories": "Categorie",
"day": "Giorno",
"events_scheduled": "eventi programmati",
"filter_by_category": "Filtro per categoria",
"filtered_results": "Risultati filtrati",
"month": "Mese",
"today": "Oggi",
"total_events": "Eventi totali",
"week": "Settimana"
},
"locations": {
"location": "Posizione",
"locations": "Luoghi",
"my_locations": "Le mie posizioni"
}
}

View file

@ -22,13 +22,7 @@
"add_to_collection": "컬렉션에 추가하세요",
"adventure": "모험",
"adventure_calendar": "모험 달력",
"adventure_create_error": "모험을 만들지 못했습니다",
"adventure_created": "모험 생성됨",
"adventure_delete_confirm": "이 모험을 삭제 하시겠습니까? 이 행동은 취소 할 수 없습니다.",
"adventure_delete_success": "모험이 성공적으로 삭제되었습니다!",
"adventure_not_found": "표시할 모험이 없습니다. 오른쪽 하단의 플러스 버튼을 사용하여 추가하거나 필터를 변경하세요!",
"adventure_update_error": "모험을 업데이트하지 못했습니다",
"adventure_updated": "모험 업데이트됨",
"all": "모두",
"archive": "보관",
"archived": "보관됨",
@ -56,16 +50,11 @@
"collection_adventures": "컬렉션 모험을 추가하세요",
"collection_archived": "이 컬렉션은 보관되었습니다.",
"collection_completed": "이 컬렉션을 완성했습니다!",
"collection_link_error": "컬렉션에 모험 연결 중 오류",
"collection_link_success": "컬렉션에 모험이 성공적으로 연결되었습니다!",
"collection_remove_error": "컬렉션에서 모험을 제거 중 오류",
"collection_remove_success": "컬렉션에서 모험이 제거되었습니다!",
"collection_stats": "컬렉션 통계",
"copied_to_clipboard": "클립 보드에 복사됨!",
"copy_failed": "복사 실패",
"copy_link": "링크 복사",
"count_txt": "검색과 일치하는 결과",
"create_adventure": "모험 생성",
"create_new": "새로 만들기...",
"date": "일자",
"date_constrain": "컬렉션 일자로 제한",
@ -74,7 +63,6 @@
"day": "일",
"days": "일",
"delete": "삭제",
"delete_adventure": "모험 삭제",
"delete_checklist": "체크리스트 삭제",
"delete_collection": "컬렉션 삭제",
"delete_collection_success": "컬렉션이 성공적으로 삭제되었습니다!",
@ -123,10 +111,7 @@
"my_collections": "내 컬렉션",
"my_images": "내 이미지",
"name": "이름",
"new_adventure": "새로운 모험",
"no_adventures_found": "모험이 없습니다",
"no_adventures_to_recommendations": "모험이 없습니다. 장소를 추천받으려면 최소 하나 이상의 모험을 등록해야 합니다.",
"no_collections_found": "이 모험을 추가할 수 있는 컬렉션이 없습니다.",
"no_description_found": "설명이 없습니다",
"no_image_found": "이미지가 없습니다",
"no_image_url": "해당 URL에 이미지가 없습니다.",
@ -135,8 +120,6 @@
"no_location": "위치를 입력하세요",
"no_location_found": "위치가 없습니다",
"no_results": "결과가 없습니다",
"not_found": "모험이 없습니다",
"not_found_desc": "당신이 찾고 있던 모험을 찾을 수 없었습니다. 다른 모험을 찾아보거나 나중에 다시 해 보세요.",
"not_visited": "방문하지 않음",
"note": "노트",
"note_delete_confirm": "이 노트를 삭제 하시겠습니까? 이 행동은 취소 할 수 없습니다.",
@ -151,7 +134,6 @@
"preview": "미리보기",
"private": "비공개",
"public": "공개",
"public_adventure": "공개 모험",
"rating": "평가",
"regions_updated": "지역이 업데이트되었습니다",
"remove": "제거",
@ -162,7 +144,6 @@
"see_adventures": "모험 보기",
"set_to_pin": "고정하기",
"share": "공유",
"share_adventure": "이 모험을 공유하세요!",
"show": "보기",
"sort": "정렬",
"sources": "출처",
@ -190,10 +171,8 @@
"visited_region_check_desc": "이것을 선택하면 서버는 방문한 모든 모험을 확인하고 그 모험의 지역을 표시하여 세계 여행에서 방문 여부를 표시합니다.",
"visits": "방문",
"warning": "경고",
"wiki_desc": "모험의 이름과 일치하는 글을 위키백과에서 가져옵니다.",
"wiki_image_error": "위키백과 이미지 가져오기 오류",
"wikipedia": "위키백과",
"will_be_marked": "모험이 저장되면 방문했다고 표시합니다.",
"checklists": "체크리스트",
"cities_updated": "도시 업데이트됨",
"clear": "초기화",
@ -246,7 +225,32 @@
"name_location": "이름, 위치",
"collection_contents": "수집 내용",
"check_in": "체크인",
"check_out": "체크 아웃"
"check_out": "체크 아웃",
"collection_link_location_error": "오류 연결 위치 컬렉션",
"collection_link_location_success": "컬렉션에 링크 된 위치!",
"collection_locations": "수집 위치를 포함합니다",
"collection_remove_location_error": "수집에서 위치를 제거하는 오류",
"collection_remove_location_success": "컬렉션에서 제거 된 위치는 성공적으로!",
"create_location": "위치를 만듭니다",
"delete_location": "위치 삭제",
"edit_location": "위치 편집",
"location_create_error": "위치를 만들지 못했습니다",
"location_created": "생성 된 위치",
"location_delete_confirm": "이 위치를 삭제 하시겠습니까? \n이 조치는 취소 할 수 없습니다.",
"location_delete_success": "위치가 성공적으로 삭제되었습니다!",
"location_not_found": "위치를 찾을 수 없습니다",
"location_not_found_desc": "당신이 찾고 있던 위치는 찾을 수 없습니다. \n다른 위치를 시도하거나 나중에 다시 확인하십시오.",
"location_update_error": "위치를 업데이트하지 못했습니다",
"location_updated": "위치 업데이트",
"new_location": "새로운 위치",
"no_collections_to_add_location": "이 위치를 추가하는 컬렉션은 없습니다.",
"no_locations_to_recommendations": "발견 된 위치는 없습니다. \n권장 사항을 얻으려면 하나 이상의 위치를 ​​추가하십시오.",
"public_location": "공개 위치",
"share_location": "이 위치를 공유하십시오!",
"visit_calendar": "캘린더를 방문하십시오",
"wiki_location_desc": "위치 이름과 일치하는 Wikipedia 기사에서 발췌 한 내용을 가져옵니다.",
"will_be_marked_location": "위치가 저장되면 방문한대로 표시됩니다.",
"no_locations_found": "발견 된 위치는 없습니다"
},
"auth": {
"confirm_password": "비밀번호 확인",
@ -265,10 +269,10 @@
"registration_disabled": "현재 등록할 수 없습니다.",
"signup": "가입",
"username": "사용자 이름",
"no_public_adventures": "공개 모험이 발견되지 않았습니다",
"no_public_collections": "공개 컬렉션이 발견되지 않았습니다",
"user_adventures": "사용자 모험",
"user_collections": "사용자 수집"
"user_collections": "사용자 수집",
"no_public_locations": "공공 장소가 발견되지 않았습니다",
"user_locations": "사용자 위치"
},
"categories": {
"category_name": "카테고리 이름",
@ -277,9 +281,9 @@
"manage_categories": "카테고리 관리",
"no_categories_found": "카테고리가 없습니다.",
"select_category": "카테고리 선택",
"update_after_refresh": "페이지를 새로고침해야 모험 카드가 업데이트됩니다.",
"add_new_category": "새 카테고리를 추가하십시오",
"name_required": "카테고리 이름이 필요합니다"
"name_required": "카테고리 이름이 필요합니다",
"location_update_after_refresh": "페이지를 새로 고치면 위치 카드가 업데이트됩니다."
},
"checklist": {
"checklist_delete_error": "체크리스트 삭제 오류",
@ -381,13 +385,16 @@
"show_visited_regions": "방문한 지역 보기",
"view_details": "상세 보기",
"adventure_stats": "모험 통계",
"adventures_shown": "모험 쇼",
"completion": "완성",
"display_options": "디스플레이 옵션",
"map_controls": "맵 컨트롤",
"marker_placed_on_map": "마커가지도에 배치되었습니다",
"place_marker_desc": "지도를 클릭하여 마커를 배치하거나 위치없이 모험을 추가하십시오.",
"regions": "지역"
"regions": "지역",
"add_location": "새로운 위치를 추가하십시오",
"add_location_at_marker": "마커에 새 위치를 추가하십시오",
"location_map": "위치지도",
"locations_shown": "표시된 위치",
"place_marker_desc_location": "지도를 클릭하여 마커를 배치하십시오."
},
"navbar": {
"about": "Adventurelog 소개",
@ -448,21 +455,27 @@
"no_shared_adventures": "이 사용자는 아직 공개 모험을 공유하지 않았습니다.",
"no_shared_collections": "이 사용자는 아직 공개 컬렉션을 공유하지 않았습니다.",
"planned_trips": "계획된 여행",
"public_adventure_experiences": "공개 모험 경험",
"your_journey_at_a_glance": "당신의 모험 여행",
"travel_statistics": "여행 통계"
"travel_statistics": "여행 통계",
"public_location_experiences": "공개 위치 경험"
},
"recomendations": {
"recommendation": "추천",
"recommendations": "권장 사항",
"adventure_recommendations": "모험 추천",
"food": "음식",
"tourism": "관광 여행"
"tourism": "관광 여행",
"location_recommendations": "위치 권장 사항"
},
"search": {
"adventurelog_results": "Adventurelog 결과",
"online_results": "온라인 결과",
"public_adventures": "공개 모험"
"public_adventures": "공개 모험",
"cities": "도시",
"countries": "국가",
"found": "설립하다",
"result": "결과",
"results": "결과",
"try_searching_desc": "모험, 컬렉션, 국가, 지역, 도시 또는 사용자를 검색하십시오."
},
"settings": {
"about_this_background": "이 배경에 대해",
@ -694,17 +707,19 @@
"google_maps_integration_desc": "Google지도 계정을 연결하여 고품질 위치 검색 결과 및 권장 사항을 얻으십시오."
},
"calendar": {
"all_categories": "모든 카테고리",
"all_day_event": "하루 종일 이벤트",
"calendar_overview": "캘린더 개요",
"categories": "카테고리",
"day": "낮",
"events_scheduled": "예약 된 이벤트",
"filter_by_category": "카테고리 별 필터",
"filtered_results": "필터링 된 결과",
"month": "월",
"today": "오늘",
"total_events": "총 이벤트",
"week": "주"
},
"locations": {
"location": "위치",
"locations": "위치",
"my_locations": "내 위치"
}
}

View file

@ -15,7 +15,6 @@
"activities": {},
"add_to_collection": "Toevoegen aan collectie",
"adventure": "Avontuur",
"adventure_delete_confirm": "Weet je zeker dat je dit avontuur wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
"archive": "Archiveer",
"archived": "Gearchiveerd",
"archived_collection_message": "Collectie succesvol gearchiveerd!",
@ -26,10 +25,6 @@
"clear": "Leegmaken",
"collection": "Collectie",
"collection_adventures": "Inclusief collectie-avonturen",
"collection_link_error": "Fout bij het koppelen van dit avontuur aan de collectie",
"collection_link_success": "Avontuur succesvol gekoppeld aan collectie!",
"collection_remove_error": "Fout bij verwijderen van dit avontuur uit de collectie",
"collection_remove_success": "Avontuur is succesvol uit de collectie verwijderd!",
"count_txt": "resultaten die overeenkomen met uw zoekopdracht",
"create_new": "Maak nieuw...",
"date": "Datum",
@ -46,8 +41,6 @@
"my_collections": "Mijn collecties",
"name": "Naam",
"no_image_found": "Geen afbeelding gevonden",
"not_found": "Avontuur niet gevonden",
"not_found_desc": "Het avontuur waar je naar op zoek was, kon niet worden gevonden. \nProbeer een ander avontuur of kom later nog eens terug.",
"open_details": "Details openen",
"open_filters": "Filters openen",
"order_by": "Sorteer op",
@ -64,9 +57,7 @@
"updated": "Gewijzigd",
"visit": "Bezoek",
"visits": "Bezoeken",
"adventure_delete_success": "Avontuur succesvol verwijderd!",
"dates": "Datums",
"delete_adventure": "Avontuur verwijderen",
"duration": "Duur",
"image_removed_error": "Fout bij het verwijderen van de afbeelding",
"image_removed_success": "Afbeelding succesvol verwijderd!",
@ -81,10 +72,6 @@
"activity_types": "Activiteitstypen",
"add": "Toevoegen",
"add_notes": "Voeg opmerkingen toe",
"adventure_create_error": "Kan geen avontuur aanmaken",
"adventure_created": "Avontuur aangemaakt",
"adventure_update_error": "Kan avontuur niet wijzigen",
"adventure_updated": "Avontuur gewijzigd",
"basic_information": "Basisinformatie",
"category": "Categorie",
"clear_map": "Kaart leegmaken",
@ -100,23 +87,19 @@
"location": "Locatie",
"location_information": "Informatie over de locatie",
"my_images": "Mijn afbeeldingen",
"new_adventure": "Nieuw avontuur",
"no_description_found": "Geen beschrijving gevonden",
"no_images": "Geen afbeeldingen",
"no_location": "Voer een locatie in",
"no_results": "Geen resultaten gevonden",
"public_adventure": "Openbaar avontuur",
"remove": "Verwijderen",
"save_next": "Opslaan & Volgende",
"search_for_location": "Zoek een locatie",
"search_results": "Zoekresultaten",
"see_adventures": "Zie Avonturen",
"share_adventure": "Deel dit avontuur!",
"start_date": "Startdatum",
"upload_image": "Afbeelding uploaden",
"url": "URL",
"warning": "Waarschuwing",
"wiki_desc": "Haalt een fragment uit een Wikipedia-artikel dat overeenkomt met de naam van het avontuur.",
"wikipedia": "Wikipedia",
"adventure_not_found": "Er zijn geen avonturen om weer te geven. \nVoeg er een paar toe via de plusknop rechtsonder of probeer de filters te wijzigen!",
"all": "Alle",
@ -124,7 +107,6 @@
"mark_visited": "Markeer als bezocht",
"my_adventures": "Mijn avonturen",
"no_adventures_found": "Geen avonturen gevonden",
"no_collections_found": "Er zijn geen collecties gevonden waar dit avontuur aan kan worden toegevoegd.",
"no_linkable_adventures": "Er zijn geen avonturen gevonden die aan deze collectie kunnen worden gekoppeld.",
"not_visited": "Niet bezocht",
"regions_updated": "regio's bijgewerkt",
@ -181,10 +163,7 @@
"to": "Naar",
"transportation_delete_confirm": "Weet u zeker dat u dit transport wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
"ending_airport": "Luchthaven van aankomst",
"will_be_marked": "wordt gemarkeerd als bezocht zodra het avontuur is opgeslagen.",
"cities_updated": "steden bijgewerkt",
"create_adventure": "Creëer avontuur",
"no_adventures_to_recommendations": "Geen avonturen gevonden. \nVoeg ten minste één avontuur toe om aanbevelingen te krijgen.",
"finding_recommendations": "Ontdek verborgen juweeltjes voor je volgende avontuur",
"attachment": "Bijlage",
"attachment_delete_success": "Bijlage succesvol verwijderd!",
@ -246,7 +225,32 @@
"name_location": "naam, locatie",
"collection_contents": "Collectie-inhoud",
"check_in": "Inchecken",
"check_out": "Uitchecken"
"check_out": "Uitchecken",
"collection_link_location_error": "Foutkoppelingslocatie naar verzameling",
"collection_link_location_success": "Locatie gekoppeld aan het succesvol verzamelen!",
"collection_locations": "Neem verzamellocaties op",
"collection_remove_location_error": "Fout het verwijderen van locatie uit het verzamelen",
"collection_remove_location_success": "Locatie verwijderd uit de collectie succesvol!",
"create_location": "Locatie maken",
"delete_location": "Verwijder locatie",
"edit_location": "Locatie bewerken",
"location_create_error": "Kan locatie niet maken",
"location_created": "Locatie gemaakt",
"location_delete_confirm": "Weet u zeker dat u deze locatie wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
"location_delete_success": "Locatie verwijderd met succes!",
"location_not_found": "Locatie niet gevonden",
"location_not_found_desc": "De locatie waarnaar u op zoek was, kon niet worden gevonden. \nProbeer een andere locatie of kom later terug.",
"location_update_error": "De locatie niet bijwerken",
"location_updated": "Locatie bijgewerkt",
"new_location": "Nieuwe locatie",
"no_collections_to_add_location": "Geen collecties gevonden om deze locatie toe te voegen aan.",
"no_locations_to_recommendations": "Geen locaties gevonden. \nVoeg minstens één locatie toe om aanbevelingen te krijgen.",
"public_location": "Openbare locatie",
"share_location": "Deel deze locatie!",
"visit_calendar": "Bezoek de agenda",
"wiki_location_desc": "Haalt fragment uit het Wikipedia -artikel dat overeenkomt met de naam van de locatie.",
"will_be_marked_location": "wordt gemarkeerd als bezocht zodra de locatie is opgeslagen.",
"no_locations_found": "Geen locaties gevonden"
},
"home": {
"desc_1": "Ontdek, plan en verken met gemak",
@ -317,10 +321,10 @@
"public_tooltip": "Met een openbaar profiel kunnen gebruikers collecties met u delen en uw profiel bekijken op de gebruikerspagina.",
"new_password": "Nieuw wachtwoord",
"or_3rd_party": "Of log in met een service van derden",
"no_public_adventures": "Geen openbare avonturen gevonden",
"no_public_collections": "Geen openbare collecties gevonden",
"user_adventures": "Gebruikersavonturen",
"user_collections": "Gebruikerscollecties"
"user_collections": "Gebruikerscollecties",
"no_public_locations": "Geen openbare locaties gevonden",
"user_locations": "Gebruikerslocaties"
},
"users": {
"no_users_found": "Er zijn geen gebruikers gevonden met openbare profielen."
@ -568,7 +572,13 @@
"search": {
"adventurelog_results": "AdventureLog resultaten",
"online_results": "Online resultaten",
"public_adventures": "Openbare avonturen"
"public_adventures": "Openbare avonturen",
"cities": "Steden",
"countries": "Landen",
"found": "gevonden",
"result": "Resultaat",
"results": "Resultaat",
"try_searching_desc": "Probeer op zoek naar avonturen, collecties, landen, regio's, steden of gebruikers."
},
"map": {
"add_adventure": "Voeg nieuw avontuur toe",
@ -579,13 +589,16 @@
"show_visited_regions": "Toon bezochte regio's",
"view_details": "Details bekijken",
"adventure_stats": "Avontuurstatistieken",
"adventures_shown": "Avonturen getoond",
"completion": "Voltooiing",
"display_options": "Displayopties",
"map_controls": "Kaartbesturing",
"marker_placed_on_map": "Marker geplaatst op kaart",
"place_marker_desc": "Klik op de kaart om een marker te plaatsen of voeg een avontuur toe zonder locatie.",
"regions": "Gebieden"
"regions": "Gebieden",
"add_location": "Voeg een nieuwe locatie toe",
"add_location_at_marker": "Voeg een nieuwe locatie toe bij Marker",
"location_map": "Locatiekaart",
"locations_shown": "Getoonde locaties",
"place_marker_desc_location": "Klik op de kaart om een marker te plaatsen."
},
"languages": {},
"share": {
@ -611,9 +624,9 @@
"no_shared_adventures": "Deze gebruiker heeft nog geen openbare avonturen gedeeld.",
"no_shared_collections": "Deze gebruiker heeft nog geen openbare collecties gedeeld.",
"planned_trips": "Geplande reizen",
"public_adventure_experiences": "Publieke avontuurlijke ervaringen",
"travel_statistics": "Reisstatistieken",
"your_journey_at_a_glance": "Je avontuurlijke reis in één oogopslag"
"your_journey_at_a_glance": "Je avontuurlijke reis in één oogopslag",
"public_location_experiences": "Openbare locatie -ervaringen"
},
"categories": {
"category_name": "Categorienaam",
@ -622,9 +635,9 @@
"manage_categories": "Beheer categorieën",
"no_categories_found": "Geen categorieën gevonden.",
"select_category": "Selecteer een categorie",
"update_after_refresh": "De avonturenkaarten worden bijgewerkt zodra u de pagina vernieuwt.",
"add_new_category": "Voeg een nieuwe categorie toe",
"name_required": "Categorienaam is vereist"
"name_required": "Categorienaam is vereist",
"location_update_after_refresh": "De locatiekaarten worden bijgewerkt zodra u de pagina vernieuwt."
},
"dashboard": {
"add_some": "Waarom begint u niet met het plannen van uw volgende avontuur? \nJe kunt een nieuw avontuur toevoegen door op de onderstaande knop te klikken.",
@ -670,9 +683,9 @@
"recomendations": {
"recommendation": "Aanbeveling",
"recommendations": "Aanbevelingen",
"adventure_recommendations": "Avontuuraanbevelingen",
"food": "Eten",
"tourism": "Toerisme"
"food": "Voedsel",
"tourism": "Toerisme",
"location_recommendations": "Locatieaanbevelingen"
},
"lodging": {
"apartment": "Appartement",
@ -695,17 +708,19 @@
"google_maps_integration_desc": "Sluit uw Google Maps-account aan om zoekresultaten en aanbevelingen van hoge kwaliteit te krijgen."
},
"calendar": {
"all_categories": "Alle categorieën",
"all_day_event": "Evenement dat de hele dag duurt",
"all_day_event": "De hele dag evenement",
"calendar_overview": "Kalenderoverzicht",
"categories": "Categorieën",
"day": "Dag",
"events_scheduled": "geplande evenementen",
"filter_by_category": "Filter per categorie",
"filtered_results": "Gefilterde resultaten",
"month": "Maand",
"today": "Vandaag",
"total_events": "Totale gebeurtenissen",
"week": "Week"
},
"locations": {
"location": "Locatie",
"locations": "Locaties",
"my_locations": "Mijn locaties"
}
}

View file

@ -64,12 +64,7 @@
"start_your_journey": "Start reisen"
},
"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.",
@ -83,8 +78,6 @@
"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",
"collection": "Samling",
"longitude": "Lengdegrad",
@ -109,7 +102,6 @@
"rating": "Vurdering",
"my_images": "Mine bilder",
"no_images": "Ingen bilder",
"share_adventure": "Del dette eventyret!",
"copy_link": "Kopier lenke",
"image": "Bilde",
"upload_image": "Last opp bilde",
@ -131,12 +123,10 @@
"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",
"generate_desc": "Generer beskrivelse",
"public_adventure": "Offentlig eventyr",
"location_information": "Plasseringsinformasjon",
"link": "Lenke",
"links": "Lenker",
@ -157,15 +147,12 @@
"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!",
"cancel": "Avbryt",
"delete_collection": "Slett samling",
"delete_adventure": "Slett eventyr",
"adventure_delete_success": "Eventyret ble slettet!",
"visited": "Besøkt",
"planned": "Planlagt",
"duration": "Varighet",
@ -183,17 +170,10 @@
"image_fetch_failed": "Kunne ikke hente bilde",
"no_location": "Vennligst angi et sted",
"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",
@ -233,7 +213,6 @@
"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",
"emoji_picker": "Emoji-velger",
@ -298,7 +277,32 @@
"name_location": "Navn, plassering",
"collection_contents": "Samlingsinnhold",
"check_in": "Sjekk inn",
"check_out": "Sjekk ut"
"check_out": "Sjekk ut",
"collection_link_location_error": "Feil koblingssted til samling",
"collection_link_location_success": "Plassering knyttet til samlingen vellykket!",
"collection_locations": "Inkluderer samlingssteder",
"collection_remove_location_error": "Feil å fjerne plasseringen fra samlingen",
"collection_remove_location_success": "Plassering fjernet fra samlingen med hell!",
"create_location": "Skape sted",
"delete_location": "Slett plassering",
"edit_location": "Rediger plassering",
"location_create_error": "Kunne ikke skape sted",
"location_created": "Plassering opprettet",
"location_delete_confirm": "Er du sikker på at du vil slette dette stedet? \nDenne handlingen kan ikke angres.",
"location_delete_success": "Plassering slettet vellykket!",
"location_not_found": "Plasseringen ikke funnet",
"location_not_found_desc": "Plasseringen du lette etter ble ikke funnet. \nPrøv et annet sted eller sjekk tilbake senere.",
"location_update_error": "Kunne ikke oppdatere plasseringen",
"location_updated": "Plassering oppdatert",
"new_location": "Ny beliggenhet",
"no_collections_to_add_location": "Ingen samlinger funnet å legge dette stedet til.",
"no_locations_to_recommendations": "Ingen steder funnet. \nLegg til minst ett sted for å få anbefalinger.",
"public_location": "Offentlig beliggenhet",
"share_location": "Del dette stedet!",
"visit_calendar": "Besøk kalenderen",
"wiki_location_desc": "Trekker utdrag fra Wikipedia -artikkelen som samsvarer med navnet på stedet.",
"will_be_marked_location": "vil bli merket som besøkt når stedet er lagret.",
"no_locations_found": "Ingen steder funnet"
},
"worldtravel": {
"country_list": "Liste over land",
@ -373,10 +377,10 @@
"public_tooltip": "Med en offentlig profil kan brukere dele samlinger med deg og se profilen din på brukersiden.",
"new_password": "Nytt passord (6+ tegn)",
"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"
"user_collections": "Brukerens samlinger",
"no_public_locations": "Ingen offentlige steder funnet",
"user_locations": "Brukerplasser"
},
"users": {
"no_users_found": "Ingen brukere med offentlig profil funnet."
@ -585,7 +589,13 @@
"search": {
"adventurelog_results": "AdventureLog-resultater",
"public_adventures": "Offentlige eventyr",
"online_results": "Nettresultater"
"online_results": "Nettresultater",
"cities": "Byer",
"countries": "Land",
"found": "funnet",
"result": "Resultat",
"results": "Resultater",
"try_searching_desc": "Prøv å søke etter eventyr, samlinger, land, regioner, byer eller brukere."
},
"map": {
"view_details": "Vis detaljer",
@ -596,13 +606,16 @@
"clear_marker": "Fjern markør",
"add_adventure": "Legg til nytt eventyr",
"adventure_stats": "Eventyrstatistikk",
"adventures_shown": "Eventyr vist",
"completion": "Fullføring",
"display_options": "Vis alternativer",
"map_controls": "Kartkontroller",
"marker_placed_on_map": "Markør plassert på kart",
"place_marker_desc": "Klikk på kartet for å plassere en markør, eller legg til et eventyr uten beliggenhet.",
"regions": "Regioner"
"regions": "Regioner",
"add_location": "Legg til nytt sted",
"add_location_at_marker": "Legg til nytt sted på markør",
"location_map": "Stedskart",
"locations_shown": "steder vist",
"place_marker_desc_location": "Klikk på kartet for å plassere en markør."
},
"share": {
"shared": "Delt",
@ -628,20 +641,20 @@
"no_shared_adventures": "Denne brukeren har ikke delt noen offentlige eventyr ennå.",
"no_shared_collections": "Denne brukeren har ikke delt noen offentlige samlinger ennå.",
"planned_trips": "Planlagte turer",
"public_adventure_experiences": "Offentlige eventyropplevelser",
"travel_statistics": "Reisestatistikk",
"your_journey_at_a_glance": "Din eventyrreise på et øyeblikk"
"your_journey_at_a_glance": "Din eventyrreise på et øyeblikk",
"public_location_experiences": "Offentlige beliggenhetsopplevelser"
},
"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",
"add_new_category": "Legg til ny kategori",
"name_required": "Kategorinavnet er påkrevd"
"name_required": "Kategorinavnet er påkrevd",
"location_update_after_refresh": "Stedskortene vil bli oppdatert når du oppdaterer siden."
},
"dashboard": {
"welcome_back": "Velkommen tilbake",
@ -687,25 +700,27 @@
"recomendations": {
"recommendation": "Anbefaling",
"recommendations": "Anbefalinger",
"adventure_recommendations": "Eventyranbefalinger",
"food": "Mat",
"tourism": "Turisme"
"tourism": "Turisme",
"location_recommendations": "Stedsanbefalinger"
},
"google_maps": {
"google_maps_integration_desc": "Koble til Google Maps-kontoen din for å få søkeresultater og anbefalinger av høy kvalitet."
},
"calendar": {
"all_categories": "Alle kategorier",
"all_day_event": "Hele dagens arrangement",
"calendar_overview": "Kalenderoversikt",
"categories": "Kategorier",
"day": "Dag",
"events_scheduled": "hendelser planlagt",
"filter_by_category": "Filter etter kategori",
"filtered_results": "Filtrerte resultater",
"month": "Måned",
"today": "I dag",
"total_events": "Total hendelser",
"week": "Uke"
},
"locations": {
"location": "Sted",
"locations": "Lokasjoner",
"my_locations": "Mine lokasjoner"
}
}

View file

@ -64,19 +64,12 @@
"start_your_journey": "Rozpocznij swoją podróż"
},
"adventures": {
"collection_remove_success": "Podróż została pomyślnie usunięta z kolekcji!",
"collection_remove_error": "Błąd podczas usuwania podróży z kolekcji",
"collection_link_success": "Podróż została pomyślnie dodana do kolekcji!",
"no_image_found": "Nie znaleziono obrazu",
"collection_link_error": "Błąd podczas dodawania podróży do kolekcji",
"adventure_delete_confirm": "Czy na pewno chcesz usunąć tę podróż? Ta operacja jest nieodwracalna.",
"open_details": "Otwórz szczegóły",
"edit_adventure": "Edytuj podróż",
"remove_from_collection": "Usuń z kolekcji",
"add_to_collection": "Dodaj do kolekcji",
"delete": "Usuń",
"not_found": "Podróż nie znaleziona",
"not_found_desc": "Podróży, której szukasz, nie można znaleźć. Spróbuj poszukać innej podróży lub sprawdź później.",
"homepage": "Strona główna",
"collection": "Kolekcja",
"longitude": "Długość geograficzna",
@ -101,7 +94,6 @@
"rating": "Ocena",
"my_images": "Moje obrazy",
"no_images": "Brak obrazów",
"share_adventure": "Podziel się tą podróżą!",
"copy_link": "Kopiuj link",
"image": "Obraz",
"upload_image": "Prześlij obraz",
@ -122,9 +114,7 @@
"clear_map": "Wyczyść mapę",
"search_results": "Wyniki wyszukiwania",
"no_results": "Nie znaleziono wyników",
"wiki_desc": "Pobiera fragment artykułu z Wikipedii pasującego do nazwy podróży.",
"generate_desc": "Generuj opis",
"public_adventure": "Publiczna podróż",
"location_information": "Informacje o lokalizacji",
"link": "Link",
"links": "Linki",
@ -145,15 +135,12 @@
"edit_collection": "Edytuj kolekcję",
"unarchive": "Przywróć z archiwum",
"archive": "Archiwizuj",
"no_collections_found": "Nie znaleziono kolekcji, do których można dodać tę podróż.",
"not_visited": "Nie odwiedzone",
"archived_collection_message": "Kolekcja została pomyślnie zarchiwizowana!",
"unarchived_collection_message": "Kolekcja została pomyślnie przywrócona z archiwum!",
"delete_collection_success": "Kolekcja została pomyślnie usunięta!",
"cancel": "Anuluj",
"delete_collection": "Usuń kolekcję",
"delete_adventure": "Usuń wyprawę",
"adventure_delete_success": "Podróż została pomyślnie usunięta!",
"visited": "Odwiedzona",
"planned": "Planowana",
"duration": "Czas trwania",
@ -171,13 +158,8 @@
"image_fetch_failed": "Nie udało się pobrać obrazu",
"no_location": "Proszę podać lokalizację",
"no_description_found": "Nie znaleziono opisu",
"adventure_created": "Podróż została utworzona",
"adventure_create_error": "Nie udało się stworzyć podróży",
"adventure_updated": "Podróż została zaktualizowana",
"adventure_update_error": "Nie udało się zaktualizować podróży",
"set_to_pin": "Ustaw jako przypiętą",
"category_fetch_error": "Błąd podczas pobierania kategorii",
"new_adventure": "Nowa podróż",
"basic_information": "Podstawowe informacje",
"adventure_not_found": "Brak podróży do wyświetlenia. Dodaj je za pomocą przycisku plus w prawym dolnym rogu lub spróbuj zmienić filtry!",
"no_adventures_found": "Brak podróży",
@ -233,10 +215,7 @@
"starting_airport": "Początkowe lotnisko",
"to": "Do",
"transportation_delete_confirm": "Czy na pewno chcesz usunąć ten transport? \nTej akcji nie można cofnąć.",
"will_be_marked": "zostanie oznaczona jako odwiedzona po zapisaniu przygody.",
"cities_updated": "miasta zaktualizowane",
"create_adventure": "Stwórz przygodę",
"no_adventures_to_recommendations": "Nie znaleziono żadnych przygód. \nDodaj co najmniej jedną przygodę, aby uzyskać rekomendacje.",
"finding_recommendations": "Odkrywanie ukrytych klejnotów na następną przygodę",
"attachment": "Załącznik",
"attachment_delete_success": "Załącznik został pomyślnie usunięty!",
@ -298,7 +277,32 @@
"delete_collection_warning": "Czy na pewno chcesz usunąć tę kolekcję? \nTego działania nie można cofnąć.",
"collection_contents": "Zawartość kolekcji",
"check_in": "Zameldować się",
"check_out": "Wymeldować się"
"check_out": "Wymeldować się",
"collection_link_location_error": "Błąd łączący lokalizację z kolekcją",
"collection_link_location_success": "Lokalizacja powiązana z kolekcją pomyślnie!",
"collection_locations": "Obejmują lokalizacje kolekcji",
"collection_remove_location_error": "Lokalizacja usuwania błędów z kolekcji",
"collection_remove_location_success": "Lokalizacja pomyślnie usunięta z kolekcji!",
"create_location": "Utwórz lokalizację",
"delete_location": "Usuń lokalizację",
"edit_location": "Edytuj lokalizację",
"location_create_error": "Nie udało się utworzyć lokalizacji",
"location_created": "Utworzona lokalizacja",
"location_delete_confirm": "Czy na pewno chcesz usunąć tę lokalizację? \nTego działania nie można cofnąć.",
"location_delete_success": "Lokalizacja pomyślnie usunięta!",
"location_not_found": "Nie znaleziono lokalizacji",
"location_not_found_desc": "Nie można było znaleźć lokalizacji. \nWypróbuj inną lokalizację lub sprawdź później.",
"location_update_error": "Nie udało się zaktualizować lokalizacji",
"location_updated": "Zaktualizowana lokalizacja",
"new_location": "Nowa lokalizacja",
"no_collections_to_add_location": "Brak kolekcji dodawania tej lokalizacji do.",
"no_locations_to_recommendations": "Nie znaleziono żadnych lokalizacji. \nDodaj co najmniej jedną lokalizację, aby uzyskać zalecenia.",
"public_location": "Lokalizacja publiczna",
"share_location": "Udostępnij tę lokalizację!",
"visit_calendar": "Odwiedź kalendarz",
"wiki_location_desc": "Wyciąga fragment artykułu Wikipedii pasujący do nazwy lokalizacji.",
"will_be_marked_location": "zostanie oznaczone jako odwiedzone po zapisaniu lokalizacji.",
"no_locations_found": "Nie znaleziono żadnych lokalizacji"
},
"worldtravel": {
"country_list": "Lista krajów",
@ -373,10 +377,10 @@
"public_tooltip": "Dzięki publicznemu profilowi użytkownicy mogą dzielić się z Tobą kolekcjami i oglądać Twój profil na stronie użytkowników.",
"new_password": "Nowe hasło",
"or_3rd_party": "Lub zaloguj się za pomocą usługi strony trzeciej",
"no_public_adventures": "Nie znaleziono publicznych przygód",
"no_public_collections": "Nie znaleziono publicznych kolekcji",
"user_adventures": "Przygody użytkowników",
"user_collections": "Kolekcje użytkowników"
"user_collections": "Kolekcje użytkowników",
"no_public_locations": "Nie znaleziono żadnych lokalizacji publicznych",
"user_locations": "Lokalizacje użytkowników"
},
"users": {
"no_users_found": "Nie znaleziono użytkowników z publicznymi profilami."
@ -568,7 +572,13 @@
"search": {
"adventurelog_results": "Wyniki AdventureLog",
"public_adventures": "Publiczne podróże",
"online_results": "Wyniki online"
"online_results": "Wyniki online",
"cities": "Miasta",
"countries": "Kraje",
"found": "znaleziony",
"result": "Wynik",
"results": "Wyniki",
"try_searching_desc": "Spróbuj szukać przygód, kolekcji, krajów, regionów, miast lub użytkowników."
},
"map": {
"view_details": "Zobacz szczegóły",
@ -579,13 +589,16 @@
"clear_marker": "Usuń znacznik",
"add_adventure": "Dodaj nową podróż",
"adventure_stats": "Statystyki przygodowe",
"adventures_shown": "Pokazane przygody",
"completion": "Ukończenie",
"display_options": "Opcje wyświetlania",
"map_controls": "Sterowanie mapą",
"marker_placed_on_map": "Marker umieszczony na mapie",
"place_marker_desc": "Kliknij mapę, aby umieścić znacznik lub dodać przygodę bez lokalizacji.",
"regions": "Regiony"
"regions": "Regiony",
"add_location": "Dodaj nową lokalizację",
"add_location_at_marker": "Dodaj nową lokalizację na znaczniku",
"location_map": "Mapa lokalizacji",
"locations_shown": "Pokazane lokalizacje",
"place_marker_desc_location": "Kliknij mapę, aby umieścić znacznik."
},
"share": {
"shared": "Udostępnione",
@ -611,20 +624,20 @@
"no_shared_adventures": "Ten użytkownik nie podzielił się jeszcze żadnymi publicznymi przygodami.",
"no_shared_collections": "Ten użytkownik nie udostępnił jeszcze żadnych publicznych kolekcji.",
"planned_trips": "Planowane wycieczki",
"public_adventure_experiences": "Public Adventure Doświadczenia",
"travel_statistics": "Statystyka podróży",
"your_journey_at_a_glance": "Twoja przygodowa podróż na pierwszy rzut oka"
"your_journey_at_a_glance": "Twoja przygodowa podróż na pierwszy rzut oka",
"public_location_experiences": "Doświadczenia lokalizacji publicznej"
},
"categories": {
"manage_categories": "Zarządzaj kategoriami",
"no_categories_found": "Brak kategorii.",
"edit_category": "Edytuj kategorię",
"icon": "Ikona",
"update_after_refresh": "Karty podróży zostaną zaktualizowane po odświeżeniu strony.",
"select_category": "Wybierz kategorię",
"category_name": "Nazwa kategorii",
"add_new_category": "Dodaj nową kategorię",
"name_required": "Nazwa kategorii jest wymagana"
"name_required": "Nazwa kategorii jest wymagana",
"location_update_after_refresh": "Karty lokalizacji zostaną zaktualizowane po odświeżeniu strony."
},
"dashboard": {
"add_some": "Dlaczego nie zacząć planować kolejnej przygody? \nMożesz dodać nową przygodę, klikając przycisk poniżej.",
@ -670,9 +683,9 @@
"recomendations": {
"recommendation": "Zalecenie",
"recommendations": "Zalecenia",
"adventure_recommendations": "Zalecenia przygodowe",
"food": "Żywność",
"tourism": "Turystyka"
"tourism": "Turystyka",
"location_recommendations": "Zalecenia dotyczące lokalizacji"
},
"lodging": {
"apartment": "Apartament",
@ -695,17 +708,19 @@
"google_maps_integration_desc": "Połącz swoje konto Google Maps, aby uzyskać wysokiej jakości wyniki wyszukiwania i zalecenia dotyczące lokalizacji."
},
"calendar": {
"all_categories": "Wszystkie kategorie",
"all_day_event": "Wydarzenie przez cały dzień",
"calendar_overview": "Przegląd kalendarza",
"categories": "Kategorie",
"day": "Dzień",
"events_scheduled": "Zaplanowane wydarzenia",
"filter_by_category": "Filtr według kategorii",
"filtered_results": "Przefiltrowane wyniki",
"month": "Miesiąc",
"today": "Dzisiaj",
"total_events": "Całkowite zdarzenia",
"week": "Tydzień"
},
"locations": {
"location": "Lokalizacja",
"locations": "Lokalizacje",
"my_locations": "Moje lokalizacje"
}
}

View file

@ -64,9 +64,6 @@
"start_your_journey": "Начните свое путешествие"
},
"adventures": {
"collection_remove_success": "Приключение успешно удалено из коллекции!",
"collection_remove_error": "Ошибка удаления приключения из коллекции",
"collection_link_success": "Приключение успешно связано с коллекцией!",
"invalid_date_range": "Недопустимый диапазон дат",
"timezone": "Часовой пояс",
"no_visits": "Нет посещений",
@ -75,8 +72,6 @@
"departure_date": "Дата отправления",
"arrival_date": "Дата прибытия",
"no_image_found": "Изображение не найдено",
"collection_link_error": "Ошибка связывания приключения с коллекцией",
"adventure_delete_confirm": "Вы уверены, что хотите удалить это приключение? Это действие нельзя отменить.",
"checklist_delete_confirm": "Вы уверены, что хотите удалить этот контрольный список? Это действие нельзя отменить.",
"note_delete_confirm": "Вы уверены, что хотите удалить эту заметку? Это действие нельзя отменить.",
"transportation_delete_confirm": "Вы уверены, что хотите удалить этот транспорт? Это действие нельзя отменить.",
@ -90,8 +85,6 @@
"remove_from_collection": "Убрать из коллекции",
"add_to_collection": "Добавить в коллекцию",
"delete": "Удалить",
"not_found": "Приключение не найдено",
"not_found_desc": "Приключение, которое вы искали, не найдено. Попробуйте другое приключение или проверьте позже.",
"homepage": "Главная страница",
"collection": "Коллекция",
"longitude": "Долгота",
@ -120,7 +113,6 @@
"my_images": "Мои изображения",
"no_images": "Нет изображений",
"distance": "Расстояние",
"share_adventure": "Поделиться этим приключением!",
"copy_link": "Копировать ссылку",
"sun_times": "Время солнца",
"sunrise": "Восход",
@ -146,12 +138,10 @@
"search_results": "Результаты поиска",
"collection_no_start_end_date": "Добавление дат начала и окончания коллекции разблокирует функции планирования маршрута на странице коллекции.",
"no_results": "Результаты не найдены",
"wiki_desc": "Извлекает отрывок из статьи Википедии, соответствующей названию приключения.",
"attachments": "Вложения",
"attachment": "Вложение",
"images": "Изображения",
"generate_desc": "Сгенерировать описание",
"public_adventure": "Публичное приключение",
"location_information": "Информация о местоположении",
"link": "Ссылка",
"links": "Ссылки",
@ -172,15 +162,12 @@
"edit_collection": "Редактировать коллекцию",
"unarchive": "Разархивировать",
"archive": "Архивировать",
"no_collections_found": "Не найдено коллекций для добавления этого приключения.",
"not_visited": "Не посещено",
"archived_collection_message": "Коллекция успешно архивирована!",
"unarchived_collection_message": "Коллекция успешно разархивирована!",
"delete_collection_success": "Коллекция успешно удалена!",
"cancel": "Отмена",
"delete_collection": "Удалить коллекцию",
"delete_adventure": "Удалить приключение",
"adventure_delete_success": "Приключение успешно удалено!",
"visited": "Посещено",
"planned": "Запланировано",
"duration": "Продолжительность",
@ -198,17 +185,10 @@
"image_fetch_failed": "Не удалось получить изображение",
"no_location": "Пожалуйста, введите местоположение",
"no_description_found": "Описание не найдено",
"adventure_created": "Приключение создано",
"adventure_create_error": "Не удалось создать приключение",
"lodging": "Жильё",
"create_adventure": "Создать приключение",
"adventure_updated": "Приключение обновлено",
"adventure_update_error": "Не удалось обновить приключение",
"set_to_pin": "Установить как булавку",
"category_fetch_error": "Ошибка получения категорий",
"new_adventure": "Новое приключение",
"basic_information": "Основная информация",
"no_adventures_to_recommendations": "Приключения не найдены. Добавьте хотя бы одно приключение, чтобы получить рекомендации.",
"display_name": "Отображаемое имя",
"adventure_not_found": "Нет приключений для отображения. Добавьте их, используя кнопку плюс в правом нижнем углу, или попробуйте изменить фильтры!",
"no_adventures_found": "Приключения не найдены",
@ -250,7 +230,6 @@
"no_location_found": "Местоположение не найдено",
"from": "От",
"to": "До",
"will_be_marked": "будет отмечено как посещённое после сохранения приключения.",
"start": "Начало",
"end": "Конец",
"emoji_picker": "Выбор эмодзи",
@ -298,7 +277,32 @@
"name_location": "имя, местоположение",
"collection_contents": "Содержание коллекции",
"check_in": "Регистрироваться",
"check_out": "Проверить"
"check_out": "Проверить",
"collection_link_location_error": "Ошибка связывания местоположения с сбором",
"collection_link_location_success": "Местоположение, связанное с коллекцией успешно!",
"collection_locations": "Включите места для сбора",
"collection_remove_location_error": "Ошибка удаления местоположения из сбора",
"collection_remove_location_success": "Место удалено из коллекции успешно!",
"create_location": "Создать местоположение",
"delete_location": "Удалить местоположение",
"edit_location": "Редактировать местоположение",
"location_create_error": "Не удалось создать местоположение",
"location_created": "Место создано",
"location_delete_confirm": "Вы уверены, что хотите удалить это место? \nЭто действие не может быть отменено.",
"location_delete_success": "Место удалено успешно!",
"location_not_found": "Местоположение не найдено",
"location_not_found_desc": "Место, которое вы искали, не было найдено. \nПожалуйста, попробуйте другое место или проверьте позже.",
"location_update_error": "Не удалось обновить местоположение",
"location_updated": "Место обновлено",
"new_location": "Новое место",
"no_collections_to_add_location": "Коллекции не обнаружили, чтобы добавить это место.",
"no_locations_to_recommendations": "Никаких мест не найдено. \nДобавьте хотя бы одно место, чтобы получить рекомендации.",
"public_location": "Общественное местоположение",
"share_location": "Поделитесь этим расположением!",
"visit_calendar": "Посетите календарь",
"wiki_location_desc": "Вытягивает отрывок из статьи Википедии, соответствующей названию места.",
"will_be_marked_location": "будет отмечен по посещению после сохранения местоположения.",
"no_locations_found": "Никаких мест не найдено"
},
"worldtravel": {
"country_list": "Список стран",
@ -373,10 +377,10 @@
"public_tooltip": "С публичным профилем пользователи могут делиться с вами коллекциями и просматривать ваш профиль на странице пользователей.",
"new_password": "Новый пароль (6+ символов)",
"or_3rd_party": "Или войти через сторонний сервис",
"no_public_adventures": "Публичные приключения не найдены",
"no_public_collections": "Публичные коллекции не найдены",
"user_adventures": "Приключения пользователя",
"user_collections": "Коллекции пользователя"
"user_collections": "Коллекции пользователя",
"no_public_locations": "Общественных мест не найдено",
"user_locations": "Пользовательские местоположения"
},
"users": {
"no_users_found": "Пользователи с публичными профилями не найдены."
@ -585,7 +589,13 @@
"search": {
"adventurelog_results": "Результаты AdventureLog",
"public_adventures": "Публичные приключения",
"online_results": "Онлайн результаты"
"online_results": "Онлайн результаты",
"cities": "Города",
"countries": "Страны",
"found": "найденный",
"result": "Результат",
"results": "Результаты",
"try_searching_desc": "Попробуйте искать приключения, коллекции, страны, регионы, города или пользователей."
},
"map": {
"view_details": "Подробности",
@ -596,13 +606,16 @@
"clear_marker": "Очистить маркер",
"add_adventure": "Добавить новое приключение",
"adventure_stats": "Приключенческая статистика",
"adventures_shown": "Приключения показаны",
"completion": "Завершение",
"display_options": "Параметры отображения",
"map_controls": "Карта управления",
"marker_placed_on_map": "Маркер размещен на карте",
"place_marker_desc": "Нажмите на карту, чтобы разместить маркер, или добавить приключение без местоположения.",
"regions": "Регионы"
"regions": "Регионы",
"add_location": "Добавить новое место",
"add_location_at_marker": "Добавить новое место в маркере",
"location_map": "Карта местоположения",
"locations_shown": "Места показаны",
"place_marker_desc_location": "Нажмите на карту, чтобы разместить маркер."
},
"share": {
"shared": "Поделено",
@ -628,20 +641,20 @@
"no_shared_adventures": "Этот пользователь еще не поделился публичными приключениями.",
"no_shared_collections": "Этот пользователь еще не поделился публичными коллекциями.",
"planned_trips": "Запланированные поездки",
"public_adventure_experiences": "Общественные приключения",
"travel_statistics": "Статистика путешествий",
"your_journey_at_a_glance": "Ваше приключенческое путешествие с первого взгляда"
"your_journey_at_a_glance": "Ваше приключенческое путешествие с первого взгляда",
"public_location_experiences": "Общественное местоположение"
},
"categories": {
"manage_categories": "Управление категориями",
"no_categories_found": "Категории не найдены.",
"edit_category": "Редактировать категорию",
"icon": "Иконка",
"update_after_refresh": "Карточки приключений будут обновлены после обновления страницы.",
"select_category": "Выбрать категорию",
"category_name": "Название категории",
"add_new_category": "Добавить новую категорию",
"name_required": "Требуется название категории"
"name_required": "Требуется название категории",
"location_update_after_refresh": "Карты местоположения будут обновлены после обновления страницы."
},
"dashboard": {
"welcome_back": "Добро пожаловать обратно",
@ -690,22 +703,24 @@
"recomendations": {
"recommendation": "Рекомендация",
"recommendations": "Рекомендации",
"adventure_recommendations": "Рекомендации приключений",
"food": "Еда",
"tourism": "Туризм"
"tourism": "Туризм",
"location_recommendations": "Рекомендации местоположения"
},
"calendar": {
"all_categories": "Все категории",
"all_day_event": "Событие на весь день",
"calendar_overview": "Обзор календаря",
"categories": "Категории",
"day": "День",
"events_scheduled": "События запланированы",
"filter_by_category": "Фильтр по категории",
"filtered_results": "Отфильтрованные результаты",
"month": "Месяц",
"today": "Сегодня",
"total_events": "Общее количество событий",
"week": "Неделя"
},
"locations": {
"location": "Расположение",
"locations": "Локации",
"my_locations": "Мои локации"
}
}

View file

@ -15,8 +15,6 @@
"activities": {},
"add_to_collection": "Lägg till i samlingen",
"adventure": "Äventyr",
"adventure_delete_confirm": "Är du säker på att du vill ta bort det här äventyret? \nDenna åtgärd kan inte ångras.",
"adventure_delete_success": "Äventyret har raderats!",
"archive": "Arkiv",
"archived": "Arkiverad",
"archived_collection_message": "Samlingen har arkiverats!",
@ -27,16 +25,11 @@
"clear": "Rensa",
"collection": "Samling",
"collection_adventures": "Inkludera samlingsäventyr",
"collection_link_error": "Det gick inte att länka äventyr till samling",
"collection_link_success": "Äventyr kopplat till samling framgångsrikt!",
"collection_remove_error": "Det gick inte att ta bort äventyr från samlingen",
"collection_remove_success": "Äventyret har tagits bort från samlingen!",
"count_txt": "resultat som matchar din sökning",
"create_new": "Skapa nytt...",
"date": "Datum",
"dates": "Datum",
"delete": "Radera",
"delete_adventure": "Ta bort äventyr",
"delete_collection": "Ta bort samling",
"delete_collection_success": "Samlingen har raderats!",
"descending": "Fallande",
@ -50,8 +43,6 @@
"my_collections": "Mina samlingar",
"name": "Namn",
"no_image_found": "Ingen bild hittades",
"not_found": "Äventyret hittades inte",
"not_found_desc": "Äventyret du letade efter kunde inte hittas. \nProva ett annat äventyr eller kom tillbaka senare.",
"open_details": "Öppna Detaljer",
"open_filters": "Öppna filter",
"order_by": "Sortera efter",
@ -81,10 +72,6 @@
"activity_types": "Aktivitetstyper",
"add": "Tillägga",
"add_notes": "Lägg till anteckningar",
"adventure_create_error": "Det gick inte att skapa äventyr",
"adventure_created": "Äventyr skapat",
"adventure_update_error": "Det gick inte att uppdatera äventyret",
"adventure_updated": "Äventyr uppdaterat",
"basic_information": "Grundläggande information",
"category": "Kategori",
"clear_map": "Rensa karta",
@ -100,30 +87,25 @@
"location": "Plats",
"location_information": "Platsinformation",
"my_images": "Mina bilder",
"new_adventure": "Nytt äventyr",
"no_description_found": "Ingen beskrivning hittades",
"no_images": "Inga bilder",
"no_location": "Vänligen ange en plats",
"no_results": "Inga resultat hittades",
"public_adventure": "Offentligt äventyr",
"remove": "Ta bort",
"save_next": "Spara",
"search_for_location": "Sök efter en plats",
"search_results": "Sökresultat",
"see_adventures": "Se äventyr",
"share_adventure": "Dela detta äventyr!",
"start_date": "Startdatum",
"upload_image": "Ladda upp bild",
"url": "URL",
"warning": "Varning",
"wiki_desc": "Hämtar utdrag från Wikipedia-artikeln som matchar äventyrets namn.",
"adventure_not_found": "Det finns inga äventyr att visa upp. \nLägg till några med hjälp av plusknappen längst ner till höger eller prova att byta filter!",
"all": "Alla",
"error_updating_regions": "Fel vid uppdatering av regioner",
"mark_visited": "Markera som besökt",
"my_adventures": "Mina äventyr",
"no_adventures_found": "Inga äventyr hittades",
"no_collections_found": "Inga samlingar hittades att lägga till detta äventyr till.",
"no_linkable_adventures": "Inga äventyr hittades som kan kopplas till denna samling.",
"not_visited": "Ej besökta",
"regions_updated": "regioner uppdaterade",
@ -181,10 +163,7 @@
"starting_airport": "Startar flygplats",
"to": "Till",
"transportation_delete_confirm": "Är du säker på att du vill ta bort denna transport? \nDenna åtgärd kan inte ångras.",
"will_be_marked": "kommer att markeras som besökt när äventyret har sparats.",
"cities_updated": "städer uppdaterade",
"create_adventure": "Skapa äventyr",
"no_adventures_to_recommendations": "Inga äventyr hittades. \nLägg till minst ett äventyr för att få rekommendationer.",
"finding_recommendations": "Upptäck dolda pärlor för ditt nästa äventyr",
"attachment": "Fastsättning",
"attachment_delete_success": "Bilagan har raderats!",
@ -246,7 +225,32 @@
"name_location": "namn, plats",
"collection_contents": "Insamlingsinnehåll",
"check_in": "Checka in",
"check_out": "Checka ut"
"check_out": "Checka ut",
"collection_link_location_error": "Fel som länkar plats till insamling",
"collection_link_location_success": "Plats kopplad till samling framgångsrikt!",
"collection_locations": "Inkludera insamlingsplatser",
"collection_remove_location_error": "Fel att ta bort platsen från samlingen",
"collection_remove_location_success": "Plats tas bort från samlingen framgångsrikt!",
"create_location": "Skapa plats",
"delete_location": "Radera plats",
"edit_location": "Redigera plats",
"location_create_error": "Det gick inte att skapa plats",
"location_created": "Plats skapad",
"location_delete_confirm": "Är du säker på att du vill ta bort den här platsen? \nDenna åtgärd kan inte ångras.",
"location_delete_success": "Plats raderas framgångsrikt!",
"location_not_found": "Plats hittades inte",
"location_not_found_desc": "Platsen du letade efter kunde inte hittas. \nFörsök med en annan plats eller kom tillbaka senare.",
"location_update_error": "Det gick inte att uppdatera platsen",
"location_updated": "Plats uppdaterad",
"new_location": "Ny plats",
"no_collections_to_add_location": "Inga samlingar som hittats för att lägga till den här platsen till.",
"no_locations_to_recommendations": "Inga platser hittades. \nLägg till minst en plats för att få rekommendationer.",
"public_location": "Allmän plats",
"share_location": "Dela den här platsen!",
"visit_calendar": "Besök kalendern",
"wiki_location_desc": "Drar utdrag från Wikipedia -artikeln som matchar namnet på platsen.",
"will_be_marked_location": "kommer att markeras som besöks när platsen har sparats.",
"no_locations_found": "Inga platser hittades"
},
"home": {
"desc_1": "Upptäck, planera och utforska med lätthet",
@ -373,10 +377,10 @@
"public_profile": "Offentlig profil",
"new_password": "Nytt lösenord",
"or_3rd_party": "Eller logga in med en tredjepartstjänst",
"no_public_adventures": "Inga offentliga äventyr hittades",
"no_public_collections": "Inga offentliga samlingar hittades",
"user_adventures": "Användaräventyr",
"user_collections": "Användarsamlingar"
"user_collections": "Användarsamlingar",
"no_public_locations": "Inga offentliga platser hittades",
"user_locations": "Användarplatser"
},
"users": {
"no_users_found": "Inga användare hittades med offentliga profiler."
@ -568,7 +572,13 @@
"search": {
"adventurelog_results": "AdventureLog-resultat",
"online_results": "Online resultat",
"public_adventures": "Offentliga äventyr"
"public_adventures": "Offentliga äventyr",
"cities": "Städer",
"countries": "Länder",
"found": "funnna",
"result": "Resultat",
"results": "Resultat",
"try_searching_desc": "Försök att söka efter äventyr, samlingar, länder, regioner, städer eller användare."
},
"map": {
"add_adventure": "Lägg till nytt äventyr",
@ -579,13 +589,16 @@
"show_visited_regions": "Visa besökta regioner",
"view_details": "Visa detaljer",
"adventure_stats": "Äventyrsstatistik",
"adventures_shown": "Äventyr visas",
"completion": "Komplettering",
"display_options": "Visningsalternativ",
"map_controls": "Kartkontroller",
"marker_placed_on_map": "Markör placerad på kartan",
"place_marker_desc": "Klicka på kartan för att placera en markör, eller lägg till ett äventyr utan plats.",
"regions": "Regioner"
"regions": "Regioner",
"add_location": "Lägg till en ny plats",
"add_location_at_marker": "Lägg till en ny plats på Marker",
"location_map": "Platskarta",
"locations_shown": "Visas",
"place_marker_desc_location": "Klicka på kartan för att placera en markör."
},
"languages": {},
"share": {
@ -611,9 +624,9 @@
"no_shared_adventures": "Den här användaren har inte delat några offentliga äventyr än.",
"no_shared_collections": "Den här användaren har inte delat några offentliga samlingar än.",
"planned_trips": "Planerade resor",
"public_adventure_experiences": "Allmänt äventyrsupplevelser",
"travel_statistics": "Resestatistik",
"your_journey_at_a_glance": "Din äventyrsresa med en överblick"
"your_journey_at_a_glance": "Din äventyrsresa med en överblick",
"public_location_experiences": "Allmän platsupplevelser"
},
"categories": {
"category_name": "Kategorinamn",
@ -622,9 +635,9 @@
"manage_categories": "Hantera kategorier",
"no_categories_found": "Inga kategorier hittades.",
"select_category": "Välj Kategori",
"update_after_refresh": "Äventyrskorten kommer att uppdateras när du uppdaterar sidan.",
"add_new_category": "Lägg till en ny kategori",
"name_required": "Kategorinamn krävs"
"name_required": "Kategorinamn krävs",
"location_update_after_refresh": "Platskorten kommer att uppdateras när du uppdaterar sidan."
},
"dashboard": {
"add_some": "Varför inte börja planera ditt nästa äventyr? \nDu kan lägga till ett nytt äventyr genom att klicka på knappen nedan.",
@ -670,9 +683,9 @@
"recomendations": {
"recommendation": "Rekommendation",
"recommendations": "Rekommendationer",
"adventure_recommendations": "Äventyrsrekommendationer",
"food": "Mat",
"tourism": "Turism"
"tourism": "Turism",
"location_recommendations": "Platsrekommendationer"
},
"lodging": {
"apartment": "Lägenhet",
@ -695,17 +708,19 @@
"google_maps_integration_desc": "Anslut ditt Google Maps-konto för att få sökresultat och rekommendationer av hög kvalitet."
},
"calendar": {
"all_categories": "Alla kategorier",
"all_day_event": "Hela dagen",
"calendar_overview": "Kalenderöversikt",
"categories": "Kategorier",
"day": "Dag",
"events_scheduled": "Händelser planerade",
"filter_by_category": "Filter efter kategori",
"filtered_results": "Filtrerade resultat",
"month": "Månad",
"today": "I dag",
"total_events": "Totala evenemang",
"week": "Vecka"
},
"locations": {
"location": "Plats",
"locations": "Plats",
"my_locations": "Mina platser"
}
}

View file

@ -64,12 +64,7 @@
"start_your_journey": "开始您的旅程"
},
"adventures": {
"collection_remove_success": "成功从合集中移除冒险!",
"collection_remove_error": "从合集中移除冒险时出错",
"collection_link_success": "成功将冒险链接到合集!",
"no_image_found": "未找到图片",
"collection_link_error": "链接冒险到合集时出错",
"adventure_delete_confirm": "您确定要删除此冒险吗?此操作无法撤销。",
"checklist_delete_confirm": "您确定要删除此检查清单吗?此操作无法撤销。",
"note_delete_confirm": "您确定要删除此笔记吗?此操作无法撤销。",
"transportation_delete_confirm": "您确定要删除此交通工具吗?此操作无法撤销。",
@ -83,8 +78,6 @@
"remove_from_collection": "从合集中移除",
"add_to_collection": "添加到合集",
"delete": "删除",
"not_found": "未找到冒险",
"not_found_desc": "未找到你要查找的冒险。请尝试其他冒险或稍后再试。",
"homepage": "主页",
"collection": "合集",
"longitude": "经度",
@ -109,7 +102,6 @@
"rating": "评分",
"my_images": "我的图片",
"no_images": "没有图片",
"share_adventure": "分享此冒险!",
"copy_link": "复制链接",
"image": "图片",
"upload_image": "上传图片",
@ -130,12 +122,10 @@
"clear_map": "清除地图",
"search_results": "搜索结果",
"no_results": "未找到结果",
"wiki_desc": "从与冒险名称匹配的维基百科文章中提取摘录。",
"attachments": "附件",
"attachment": "附件",
"images": "图片",
"generate_desc": "生成描述",
"public_adventure": "公开冒险",
"location_information": "位置信息",
"link": "链接",
"links": "链接",
@ -156,15 +146,12 @@
"edit_collection": "编辑合集",
"unarchive": "取消归档",
"archive": "归档",
"no_collections_found": "未找到可添加此冒险的合集。",
"not_visited": "未访问",
"archived_collection_message": "成功归档合集!",
"unarchived_collection_message": "成功取消归档合集!",
"delete_collection_success": "成功删除合集!",
"cancel": "取消",
"delete_collection": "删除合集",
"delete_adventure": "删除冒险",
"adventure_delete_success": "成功删除冒险!",
"visited": "已访问",
"planned": "计划中",
"duration": "持续时间",
@ -182,17 +169,10 @@
"image_fetch_failed": "获取图片失败",
"no_location": "请输入位置",
"no_description_found": "未找到描述",
"adventure_created": "冒险已创建",
"adventure_create_error": "创建冒险失败",
"lodging": "住宿",
"create_adventure": "创建冒险",
"adventure_updated": "冒险已更新",
"adventure_update_error": "更新冒险失败",
"set_to_pin": "设置为图钉",
"category_fetch_error": "获取类别时出错",
"new_adventure": "新冒险",
"basic_information": "基本信息",
"no_adventures_to_recommendations": "未找到冒险。添加至少一个冒险以获得推荐。",
"display_name": "显示名称",
"adventure_not_found": "没找到任何冒险。使用右下角的加号按钮添加一些,或尝试更改筛选条件!",
"no_adventures_found": "未找到冒险",
@ -232,7 +212,6 @@
"no_location_found": "未找到位置",
"from": "从",
"to": "到",
"will_be_marked": "将在冒险保存后标记为已访问。",
"start": "开始",
"end": "结束",
"emoji_picker": "表情符号选择器",
@ -298,7 +277,32 @@
"name_location": "名称,位置",
"collection_contents": "收集内容",
"check_in": "报到",
"check_out": "查看"
"check_out": "查看",
"collection_link_location_error": "链接位置到集合的错误",
"collection_link_location_success": "成功链接到收集的位置!",
"collection_locations": "包括收集位置",
"collection_remove_location_error": "从集合中删除位置的错误",
"collection_remove_location_success": "成功从收藏中删除的位置!",
"create_location": "创建位置",
"delete_location": "删除位置",
"edit_location": "编辑位置",
"location_create_error": "无法创建位置",
"location_created": "创建的位置",
"location_delete_confirm": "您确定要删除此位置吗?\n该动作不能撤消。",
"location_delete_success": "位置成功删除了!",
"location_not_found": "找不到位置",
"location_not_found_desc": "找不到您寻找的位置。\n请尝试其他位置或稍后再检查。",
"location_update_error": "无法更新位置",
"location_updated": "位置更新",
"new_location": "新位置",
"no_collections_to_add_location": "没有发现将此位置添加到。",
"no_locations_to_recommendations": "找不到位置。\n添加至少一个位置以获取建议。",
"public_location": "公共位置",
"share_location": "分享这个位置!",
"visit_calendar": "访问日历",
"wiki_location_desc": "从Wikipedia文章中提取摘录符合该位置的名称。",
"will_be_marked_location": "保存位置后,将被标记为访问。",
"no_locations_found": "找不到位置"
},
"auth": {
"forgot_password": "忘记密码?",
@ -317,10 +321,10 @@
"public_tooltip": "通过公开个人资料,用户可以与您共享合集,并在用户页面查看您的资料。",
"new_password": "新密码6个字符以上",
"or_3rd_party": "或使用第三方服务登录",
"no_public_adventures": "未找到公开冒险",
"no_public_collections": "未找到公开合集",
"user_adventures": "用户冒险",
"user_collections": "用户合集"
"user_collections": "用户合集",
"no_public_locations": "找不到公共场所",
"user_locations": "用户位置"
},
"worldtravel": {
"all": "全部",
@ -568,7 +572,13 @@
"search": {
"adventurelog_results": "AdventureLog 结果",
"online_results": "在线结果",
"public_adventures": "已公开的冒险"
"public_adventures": "已公开的冒险",
"cities": "城市",
"countries": "国家",
"found": "成立",
"result": "结果",
"results": "结果",
"try_searching_desc": "尝试搜索冒险,收藏,国家,地区,城市或用户。"
},
"map": {
"add_adventure": "添加新冒险",
@ -579,13 +589,16 @@
"show_visited_regions": "显示访问过的地区",
"view_details": "查看详情",
"adventure_stats": "冒险统计",
"adventures_shown": "展示的冒险",
"completion": "完成",
"display_options": "显示选项",
"map_controls": "地图控件",
"marker_placed_on_map": "放置在地图上的标记",
"place_marker_desc": "单击地图以放置标记,或在没有位置的情况下添加冒险。",
"regions": "地区"
"regions": "地区",
"add_location": "添加新位置",
"add_location_at_marker": "在标记处添加新位置",
"location_map": "位置图",
"locations_shown": "显示的位置",
"place_marker_desc_location": "单击地图以放置标记。"
},
"languages": {},
"share": {
@ -611,9 +624,9 @@
"no_shared_adventures": "该用户尚未分享任何公共冒险。",
"no_shared_collections": "该用户尚未共享任何公共收藏。",
"planned_trips": "计划的旅行",
"public_adventure_experiences": "公共冒险经验",
"travel_statistics": "旅行统计",
"your_journey_at_a_glance": "您一眼的冒险之旅"
"your_journey_at_a_glance": "您一眼的冒险之旅",
"public_location_experiences": "公共位置经验"
},
"categories": {
"category_name": "类别名称",
@ -622,9 +635,9 @@
"manage_categories": "管理类别",
"no_categories_found": "未找到类别。",
"select_category": "选择类别",
"update_after_refresh": "刷新页面后,冒险卡将更新。",
"add_new_category": "添加新类别",
"name_required": "需要类别名称"
"name_required": "需要类别名称",
"location_update_after_refresh": "刷新页面后,将更新位置卡。"
},
"dashboard": {
"add_some": "为什么不开始计划你的下一次冒险呢?\n您可以通过单击下面的按钮添加新的冒险。",
@ -670,9 +683,9 @@
"recomendations": {
"recommendation": "推荐",
"recommendations": "建议",
"adventure_recommendations": "冒险建议",
"food": "食物",
"tourism": "旅游"
"tourism": "旅游",
"location_recommendations": "位置建议"
},
"lodging": {
"campground": "露营地",
@ -695,17 +708,19 @@
"google_maps_integration_desc": "连接您的Google Maps帐户以获取高质量的位置搜索结果和建议。"
},
"calendar": {
"all_categories": "所有类别",
"all_day_event": "全天活动",
"calendar_overview": "日历概述",
"categories": "类别",
"day": "天",
"events_scheduled": "预定事件",
"filter_by_category": "按类别过滤",
"filtered_results": "过滤结果",
"month": "月",
"today": "今天",
"total_events": "总事件",
"week": "星期"
},
"locations": {
"location": "地点",
"locations": "位置",
"my_locations": "我的位置"
}
}

View file

@ -102,7 +102,7 @@
<div class="flex flex-col sm:flex-row gap-4 pt-4">
{#if data.user}
<button
on:click={() => goto('/adventures')}
on:click={() => goto('/locations')}
class="btn btn-primary btn-lg gap-3 shadow-lg hover:shadow-xl transition-all duration-300 group"
>
<PlayIcon class="w-5 h-5 group-hover:scale-110 transition-transform" />

View file

@ -8,7 +8,7 @@ export const POST: RequestHandler = async (event) => {
let allActivities: string[] = [];
let csrfToken = await fetchCSRFToken();
let sessionId = event.cookies.get('sessionid');
let res = await event.fetch(`${endpoint}/api/activity-types/types/`, {
let res = await event.fetch(`${endpoint}/api/tags/types/`, {
headers: {
'X-CSRFToken': csrfToken,
Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`

View file

@ -1,99 +1,5 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
import type { Adventure } from '$lib/types';
import type { Actions } from '@sveltejs/kit';
import { fetchCSRFToken } from '$lib/index.server';
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load = (async (event) => {
if (!event.locals.user) {
return redirect(302, '/login');
} else {
let count = 0;
let adventures: Adventure[] = [];
let typeString = event.url.searchParams.get('types');
// If no type is specified, default to 'all'
if (!typeString) {
typeString = 'all';
}
const include_collections = event.url.searchParams.get('include_collections') || 'false';
const order_by = event.url.searchParams.get('order_by') || 'updated_at';
const order_direction = event.url.searchParams.get('order_direction') || 'asc';
const page = event.url.searchParams.get('page') || '1';
const is_visited = event.url.searchParams.get('is_visited') || 'all';
let initialFetch = await event.fetch(
`${serverEndpoint}/api/adventures/filtered?types=${typeString}&order_by=${order_by}&order_direction=${order_direction}&include_collections=${include_collections}&page=${page}&is_visited=${is_visited}`,
{
headers: {
Cookie: `sessionid=${event.cookies.get('sessionid')}`
},
credentials: 'include'
}
);
if (!initialFetch.ok) {
let error_message = await initialFetch.json();
console.error(error_message);
console.error('Failed to fetch visited adventures');
return redirect(302, '/login');
} else {
let res = await initialFetch.json();
let visited = res.results as Adventure[];
count = res.count;
adventures = [...adventures, ...visited];
}
return {
props: {
adventures,
count
}
};
}
}) satisfies PageServerLoad;
export const actions: Actions = {
image: async (event) => {
let formData = await event.request.formData();
let csrfToken = await fetchCSRFToken();
let sessionId = event.cookies.get('sessionid');
let res = await fetch(`${serverEndpoint}/api/images/`, {
method: 'POST',
headers: {
Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`,
'X-CSRFToken': csrfToken,
Referer: event.url.origin // Include Referer header
},
body: formData
});
let data = await res.json();
return data;
},
attachment: async (event) => {
let formData = await event.request.formData();
let csrfToken = await fetchCSRFToken();
let sessionId = event.cookies.get('sessionid');
let res = await fetch(`${serverEndpoint}/api/attachments/`, {
method: 'POST',
headers: {
Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`,
'X-CSRFToken': csrfToken,
Referer: event.url.origin // Include Referer header
},
body: formData
});
let data = await res.json();
console.log(res);
console.log(data);
return data;
}
};
export const load = (async (_event) => {
return redirect(301, '/locations');
}) satisfies import('./$types').PageServerLoad;

View file

@ -1,76 +1,7 @@
import type { PageServerLoad } from './$types';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
import type { AdditionalAdventure, Adventure, Collection } from '$lib/types';
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
import { redirect } from '@sveltejs/kit';
export const load = (async (event) => {
const id = event.params as { id: string };
let request = await fetch(`${endpoint}/api/adventures/${id.id}/additional-info/`, {
headers: {
Cookie: `sessionid=${event.cookies.get('sessionid')}`
},
credentials: 'include'
});
if (!request.ok) {
console.error('Failed to fetch adventure ' + id.id);
return {
props: {
adventure: null
}
};
} else {
let adventure = (await request.json()) as AdditionalAdventure;
return {
props: {
adventure
}
};
}
return redirect(301, `/locations/${id.id}`);
}) satisfies PageServerLoad;
import { redirect, type Actions } from '@sveltejs/kit';
import { fetchCSRFToken } from '$lib/index.server';
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const actions: Actions = {
delete: async (event) => {
const id = event.params as { id: string };
const adventureId = id.id;
if (!event.locals.user) {
return redirect(302, '/login');
}
if (!adventureId) {
return {
status: 400,
error: new Error('Bad request')
};
}
let csrfToken = await fetchCSRFToken();
let res = await fetch(`${serverEndpoint}/api/adventures/${event.params.id}`, {
method: 'DELETE',
headers: {
Referer: event.url.origin, // Include Referer header
Cookie: `sessionid=${event.cookies.get('sessionid')};
csrftoken=${csrfToken}`,
'X-CSRFToken': csrfToken
},
credentials: 'include'
});
console.log(res);
if (!res.ok) {
return {
status: res.status,
error: new Error('Failed to delete adventure')
};
} else {
return {
status: 204
};
}
}
};

View file

@ -1,4 +1,4 @@
import type { Adventure } from '$lib/types';
import type { Location } from '$lib/types';
import type { PageServerLoad } from './$types';
import { formatDateInTimezone, formatAllDayDate } from '$lib/dateUtils';
import { isAllDay } from '$lib';
@ -8,12 +8,12 @@ const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load = (async (event) => {
let sessionId = event.cookies.get('sessionid');
let visitedFetch = await fetch(`${endpoint}/api/adventures/all/?include_collections=true`, {
let visitedFetch = await fetch(`${endpoint}/api/locations/all/?include_collections=true`, {
headers: {
Cookie: `sessionid=${sessionId}`
}
});
let adventures = (await visitedFetch.json()) as Adventure[];
let adventures = (await visitedFetch.json()) as Location[];
// Get user's local timezone as fallback
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;

View file

@ -26,7 +26,7 @@
return marked(markdown);
};
let adventures = data.props.adventures;
let locations = data.props.adventures;
let allDates = data.props.dates;
let filteredDates = [...allDates];
@ -174,7 +174,7 @@
</script>
<svelte:head>
<title>{$t('adventures.adventure_calendar')} - AdventureLog</title>
<title>{$t('adventures.visit_calendar')} - AdventureLog</title>
</svelte:head>
<div class="min-h-screen bg-gradient-to-br from-base-200 via-base-100 to-base-200">
@ -196,7 +196,7 @@
</div>
<div>
<h1 class="text-3xl font-bold text-primary bg-clip-text">
{$t('adventures.adventure_calendar')}
{$t('adventures.visit_calendar')}
</h1>
<p class="text-sm text-base-content/60">
{filteredDates.length}
@ -214,8 +214,8 @@
<div class="stat-value text-lg text-primary">{allDates.length}</div>
</div>
<div class="stat py-2 px-4">
<div class="stat-title text-xs">{$t('navbar.adventures')}</div>
<div class="stat-value text-lg text-secondary">{adventures.length}</div>
<div class="stat-title text-xs">{$t('locations.locations')}</div>
<div class="stat-value text-lg text-secondary">{locations.length}</div>
</div>
</div>
</div>
@ -229,7 +229,7 @@
/>
<input
type="text"
placeholder="Search adventures or locations..."
placeholder={$t('adventures.search_for_location')}
class="input input-bordered w-full pl-10 pr-10 bg-base-100/80"
bind:value={searchFilter}
/>
@ -298,8 +298,8 @@
<div class="grid grid-cols-2 gap-4">
<div class="stat p-0">
<div class="stat-title text-xs">{$t('navbar.adventures')}</div>
<div class="stat-value text-lg text-primary">{adventures.length}</div>
<div class="stat-title text-xs">{$t('locations.locations')}</div>
<div class="stat-value text-lg text-primary">{locations.length}</div>
</div>
</div>
@ -418,7 +418,7 @@
{#if selectedEvent.extendedProps.adventureId}
<a
href={`/adventures/${selectedEvent.extendedProps.adventureId}`}
href={`/locations/${selectedEvent.extendedProps.adventureId}`}
class="btn btn-neutral btn-block mt-4"
>
{$t('map.view_details')}

Some files were not shown because too many files have changed in this diff Show more