diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..ca0f2f6 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @seanmorley15 \ No newline at end of file diff --git a/.github/workflows/cdn-beta.yml b/.github/workflows/cdn-beta.yml new file mode 100644 index 0000000..d5c2c85 --- /dev/null +++ b/.github/workflows/cdn-beta.yml @@ -0,0 +1,46 @@ +name: Upload beta CDN image to GHCR and Docker Hub + +on: + push: + branches: + - development + paths: + - "cdn/**" + +env: + IMAGE_NAME: "adventurelog-cdn" + +jobs: + upload: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.ACCESS_TOKEN }} + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: set lower case owner name + run: | + echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} + env: + OWNER: "${{ github.repository_owner }}" + + - name: Build Docker images + run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:beta -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:beta ./cdn diff --git a/.github/workflows/cdn-latest.yml b/.github/workflows/cdn-latest.yml new file mode 100644 index 0000000..376ede9 --- /dev/null +++ b/.github/workflows/cdn-latest.yml @@ -0,0 +1,46 @@ +name: Upload latest CDN image to GHCR and Docker Hub + +on: + push: + branches: + - main + paths: + - "cdn/**" + +env: + IMAGE_NAME: "adventurelog-cdn" + +jobs: + upload: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.ACCESS_TOKEN }} + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: set lower case owner name + run: | + echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} + env: + OWNER: "${{ github.repository_owner }}" + + - name: Build Docker images + run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:latest -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:latest ./cdn diff --git a/.github/workflows/cdn-release.yml b/.github/workflows/cdn-release.yml new file mode 100644 index 0000000..2bba9af --- /dev/null +++ b/.github/workflows/cdn-release.yml @@ -0,0 +1,43 @@ +name: Upload the tagged release CDN image to GHCR and Docker Hub + +on: + release: + types: [released] + +env: + IMAGE_NAME: "adventurelog-cdn" + +jobs: + upload: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.ACCESS_TOKEN }} + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: set lower case owner name + run: | + echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} + env: + OWNER: "${{ github.repository_owner }}" + + - name: Build Docker images + run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:${{ github.event.release.tag_name }} -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:${{ github.event.release.tag_name }} ./cdn diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c980b88..7b49b3d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,91 +1,50 @@ # Contributing to AdventureLog -When contributing to this repository, please first discuss the change you wish to make via issue, -email, or any other method with the owners of this repository before making a change. - -Please note we have a code of conduct, please follow it in all your interactions with the project. +We’re excited to have you contribute to AdventureLog! To ensure that this community remains welcoming and productive for all users and developers, please follow this simple Code of Conduct. ## Pull Request Process -1. Please make sure you create an issue first for your change so you can link any pull requests to this issue. There should be a clear relationship between pull requests and issues. -2. Update the README.md with details of changes to the interface, this includes new environment - variables, exposed ports, useful file locations and container parameters. -3. Increase the version numbers in any examples files and the README.md to the new version that this - Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). -4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you - do not have permission to do that, you may request the second reviewer to merge it for you. +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 there’s 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. ## Code of Conduct ### Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. +At AdventureLog, we are committed to creating a community that fosters adventure, exploration, and innovation. We encourage diverse participation and strive to maintain a space where everyone feels welcome to contribute, regardless of their background or experience level. We ask that you contribute with respect and kindness, making sure to prioritize collaboration and mutual growth. ### Our Standards -Examples of behavior that contributes to creating a positive environment -include: +In order to maintain a positive environment, we encourage the following behaviors: -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members +- **Inclusivity**: Use welcoming and inclusive language that fosters collaboration across all perspectives and experiences. +- **Respect**: Respect differing opinions and engage with empathy, understanding that each person’s perspective is valuable. +- **Constructive Feedback**: Offer feedback that helps improve the project and allows contributors to grow from it. +- **Adventure Spirit**: Bring the same sense of curiosity, discovery, and positivity that drives AdventureLog into all interactions with the community. -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -- The use of sexualized language or imagery and unwelcome sexual attention or - advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic - address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting +- Personal attacks, trolling, or any form of harassment. +- Insensitive or discriminatory language, including sexualized comments or imagery. +- Spamming or misusing project spaces for personal gain. +- Publishing or using others’ private information without permission. +- Anything else that could be seen as disrespectful or unprofessional in a collaborative environment. ### Our Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +As maintainers of AdventureLog, we are committed to enforcing this Code of Conduct and taking corrective action when necessary. This may involve moderating comments, pulling code, or banning users who engage in harmful behaviors. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +We strive to foster a community that balances open collaboration with respect for all contributors. ### Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies in all spaces related to AdventureLog. This includes our GitHub repository, discussions, documentation, social media accounts, and events—both online and in person. ### Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at [INSERT EMAIL ADDRESS]. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +If you experience or witness unacceptable behavior, please report it to the project team at `contact@adventurelog.app`. All reports will be confidential and handled swiftly. The maintainers will investigate the issue and take appropriate action as needed. ### Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +This Code of Conduct is inspired by the [Contributor Covenant](http://contributor-covenant.org), version 1.4, and adapted to fit the unique spirit of AdventureLog. diff --git a/README.md b/README.md index 4df60b0..3b48223 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ Hi! I'm Sean, the creator of AdventureLog. I'm a college student and software de - WorldTravel Dataset [dr5hn/countries-states-cities-database](https://github.com/dr5hn/countries-states-cities-database) ### Top Supporters 💖 + - [Veymax](https://x.com/veymax) - [nebriv](https://github.com/nebriv) - [Victor Butler](https://x.com/victor_butler) diff --git a/backend/server/.env.example b/backend/server/.env.example index 4c1f9ad..598aeb7 100644 --- a/backend/server/.env.example +++ b/backend/server/.env.example @@ -25,10 +25,10 @@ EMAIL_BACKEND='console' # ------------------- # # For Developers to start a Demo Database -# docker run --name postgres-admin -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=admin -p 5432:5432 -d postgis/postgis:15-3.3 +# docker run --name adventurelog-development -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=adventurelog -p 5432:5432 -d postgis/postgis:15-3.3 # PGHOST='localhost' -# PGDATABASE='admin' +# PGDATABASE='adventurelog' # PGUSER='admin' # PGPASSWORD='admin' # ------------------- # \ No newline at end of file diff --git a/backend/server/achievements/__init__.py b/backend/server/achievements/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/server/achievements/admin.py b/backend/server/achievements/admin.py new file mode 100644 index 0000000..af087ce --- /dev/null +++ b/backend/server/achievements/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from allauth.account.decorators import secure_admin_login +from achievements.models import Achievement, UserAchievement + +admin.autodiscover() +admin.site.login = secure_admin_login(admin.site.login) + +admin.site.register(Achievement) +admin.site.register(UserAchievement) \ No newline at end of file diff --git a/backend/server/achievements/apps.py b/backend/server/achievements/apps.py new file mode 100644 index 0000000..2a635e2 --- /dev/null +++ b/backend/server/achievements/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AchievementsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'achievements' diff --git a/backend/server/achievements/management/__init__.py b/backend/server/achievements/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/server/achievements/management/commands/__init__.py b/backend/server/achievements/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/server/achievements/management/commands/achievement-seed.py b/backend/server/achievements/management/commands/achievement-seed.py new file mode 100644 index 0000000..7713e88 --- /dev/null +++ b/backend/server/achievements/management/commands/achievement-seed.py @@ -0,0 +1,66 @@ +import json +from django.core.management.base import BaseCommand +from achievements.models import Achievement + +US_STATE_CODES = [ + 'US-AL', 'US-AK', 'US-AZ', 'US-AR', 'US-CA', 'US-CO', 'US-CT', 'US-DE', + 'US-FL', 'US-GA', 'US-HI', 'US-ID', 'US-IL', 'US-IN', 'US-IA', 'US-KS', + 'US-KY', 'US-LA', 'US-ME', 'US-MD', 'US-MA', 'US-MI', 'US-MN', 'US-MS', + 'US-MO', 'US-MT', 'US-NE', 'US-NV', 'US-NH', 'US-NJ', 'US-NM', 'US-NY', + 'US-NC', 'US-ND', 'US-OH', 'US-OK', 'US-OR', 'US-PA', 'US-RI', 'US-SC', + 'US-SD', 'US-TN', 'US-TX', 'US-UT', 'US-VT', 'US-VA', 'US-WA', 'US-WV', + 'US-WI', 'US-WY' +] + +ACHIEVEMENTS = [ + { + "name": "First Adventure", + "key": "achievements.first_adventure", + "type": "adventure_count", + "description": "Log your first adventure!", + "condition": {"type": "adventure_count", "value": 1}, + }, + { + "name": "Explorer", + "key": "achievements.explorer", + "type": "adventure_count", + "description": "Log 10 adventures.", + "condition": {"type": "adventure_count", "value": 10}, + }, + { + "name": "Globetrotter", + "key": "achievements.globetrotter", + "type": "country_count", + "description": "Visit 5 different countries.", + "condition": {"type": "country_count", "value": 5}, + }, + { + "name": "American Dream", + "key": "achievements.american_dream", + "type": "country_count", + "description": "Visit all 50 states in the USA.", + "condition": {"type": "country_count", "items": US_STATE_CODES}, + } +] + + + + +class Command(BaseCommand): + help = "Seeds the database with predefined achievements" + + def handle(self, *args, **kwargs): + for achievement_data in ACHIEVEMENTS: + achievement, created = Achievement.objects.update_or_create( + name=achievement_data["name"], + defaults={ + "description": achievement_data["description"], + "condition": json.dumps(achievement_data["condition"]), + "type": achievement_data["type"], + "key": achievement_data["key"], + }, + ) + if created: + self.stdout.write(self.style.SUCCESS(f"✅ Created: {achievement.name}")) + else: + self.stdout.write(self.style.WARNING(f"🔄 Updated: {achievement.name}")) diff --git a/backend/server/achievements/models.py b/backend/server/achievements/models.py new file mode 100644 index 0000000..c23bb31 --- /dev/null +++ b/backend/server/achievements/models.py @@ -0,0 +1,34 @@ +import uuid +from django.db import models +from django.contrib.auth import get_user_model + +User = get_user_model() + +VALID_ACHIEVEMENT_TYPES = [ + "adventure_count", + "country_count", +] + +class Achievement(models.Model): + """Stores all possible achievements""" + name = models.CharField(max_length=255, unique=True) + key = models.CharField(max_length=255, unique=True, default='achievements.other') # Used for frontend lookups, e.g. "achievements.first_adventure" + type = models.CharField(max_length=255, choices=[(tag, tag) for tag in VALID_ACHIEVEMENT_TYPES], default='adventure_count') # adventure_count, country_count, etc. + description = models.TextField() + icon = models.ImageField(upload_to="achievements/", null=True, blank=True) + condition = models.JSONField() # Stores rules like {"type": "adventure_count", "value": 10} + + def __str__(self): + return self.name + +class UserAchievement(models.Model): + """Tracks which achievements a user has earned""" + user = models.ForeignKey(User, on_delete=models.CASCADE) + achievement = models.ForeignKey(Achievement, on_delete=models.CASCADE) + earned_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ("user", "achievement") # Prevent duplicates + + def __str__(self): + return f"{self.user.username} - {self.achievement.name}" diff --git a/backend/server/achievements/tests.py b/backend/server/achievements/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/server/achievements/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/server/achievements/views.py b/backend/server/achievements/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/server/achievements/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index a1a1101..67b3d2e 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -1,8 +1,8 @@ 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 -from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity +from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment, Lodging +from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity from allauth.account.decorators import secure_admin_login admin.autodiscover() @@ -140,6 +140,7 @@ admin.site.register(Category, CategoryAdmin) admin.site.register(City, CityAdmin) admin.site.register(VisitedCity) admin.site.register(Attachment) +admin.site.register(Lodging) admin.site.site_header = 'AdventureLog Admin' admin.site.site_title = 'AdventureLog Admin Site' diff --git a/backend/server/adventures/migrations/0022_hotel.py b/backend/server/adventures/migrations/0022_hotel.py new file mode 100644 index 0000000..56a6097 --- /dev/null +++ b/backend/server/adventures/migrations/0022_hotel.py @@ -0,0 +1,39 @@ +# Generated by Django 5.0.8 on 2025-02-02 15:36 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0021_alter_attachment_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Hotel', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('name', models.CharField(max_length=200)), + ('description', models.TextField(blank=True, null=True)), + ('rating', models.FloatField(blank=True, null=True)), + ('link', models.URLField(blank=True, max_length=2083, null=True)), + ('check_in', models.DateTimeField(blank=True, null=True)), + ('check_out', models.DateTimeField(blank=True, null=True)), + ('reservation_number', models.CharField(blank=True, max_length=100, null=True)), + ('price', models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True)), + ('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), + ('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), + ('location', models.CharField(blank=True, max_length=200, null=True)), + ('is_public', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='adventures.collection')), + ('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/server/adventures/migrations/0023_lodging_delete_hotel.py b/backend/server/adventures/migrations/0023_lodging_delete_hotel.py new file mode 100644 index 0000000..44e502c --- /dev/null +++ b/backend/server/adventures/migrations/0023_lodging_delete_hotel.py @@ -0,0 +1,43 @@ +# Generated by Django 5.0.8 on 2025-02-08 01:50 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0022_hotel'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Lodging', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('name', models.CharField(max_length=200)), + ('type', models.CharField(choices=[('hotel', 'Hotel'), ('hostel', 'Hostel'), ('resort', 'Resort'), ('bnb', 'Bed & Breakfast'), ('campground', 'Campground'), ('cabin', 'Cabin'), ('apartment', 'Apartment'), ('house', 'House'), ('villa', 'Villa'), ('motel', 'Motel'), ('other', 'Other')], default='other', max_length=100)), + ('description', models.TextField(blank=True, null=True)), + ('rating', models.FloatField(blank=True, null=True)), + ('link', models.URLField(blank=True, max_length=2083, null=True)), + ('check_in', models.DateTimeField(blank=True, null=True)), + ('check_out', models.DateTimeField(blank=True, null=True)), + ('reservation_number', models.CharField(blank=True, max_length=100, null=True)), + ('price', models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True)), + ('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), + ('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), + ('location', models.CharField(blank=True, max_length=200, null=True)), + ('is_public', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='adventures.collection')), + ('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.DeleteModel( + name='Hotel', + ), + ] diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index af0d7b9..ad52e89 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -36,6 +36,20 @@ ADVENTURE_TYPES = [ ('other', 'Other') ] +LODGING_TYPES = [ + ('hotel', 'Hotel'), + ('hostel', 'Hostel'), + ('resort', 'Resort'), + ('bnb', 'Bed & Breakfast'), + ('campground', 'Campground'), + ('cabin', 'Cabin'), + ('apartment', 'Apartment'), + ('house', 'House'), + ('villa', 'Villa'), + ('motel', 'Motel'), + ('other', 'Other') +] + TRANSPORTATION_TYPES = [ ('car', 'Car'), ('plane', 'Plane'), @@ -318,4 +332,38 @@ class Category(models.Model): def __str__(self): - return self.name + ' - ' + self.display_name + ' - ' + self.icon \ No newline at end of file + return self.name + ' - ' + self.display_name + ' - ' + self.icon + +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) + name = models.CharField(max_length=200) + type = models.CharField(max_length=100, choices=LODGING_TYPES, default='other') + description = models.TextField(blank=True, null=True) + rating = models.FloatField(blank=True, null=True) + link = models.URLField(blank=True, null=True, max_length=2083) + check_in = models.DateTimeField(blank=True, null=True) + check_out = models.DateTimeField(blank=True, null=True) + reservation_number = models.CharField(max_length=100, blank=True, null=True) + price = models.DecimalField(max_digits=9, decimal_places=2, blank=True, null=True) + latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + location = models.CharField(max_length=200, blank=True, null=True) + is_public = models.BooleanField(default=False) + collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def clean(self): + if self.date and self.end_date and self.date > self.end_date: + raise ValidationError('The start date must be before the end date. Start date: ' + str(self.date) + ' End date: ' + str(self.end_date)) + + if self.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) + + def __str__(self): + return self.name \ No newline at end of file diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index a7c0bd2..97dd633 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -1,8 +1,9 @@ from django.utils import timezone import os -from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, Attachment +from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, Attachment, Lodging from rest_framework import serializers from main.utils import CustomModelSerializer +from users.serializers import CustomUserDetailsSerializer class AdventureImageSerializer(CustomModelSerializer): @@ -80,15 +81,16 @@ class AdventureSerializer(CustomModelSerializer): attachments = AttachmentSerializer(many=True, read_only=True) category = CategorySerializer(read_only=False, required=False) is_visited = serializers.SerializerMethodField() + user = serializers.SerializerMethodField() class Meta: model = Adventure fields = [ 'id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location', 'is_public', 'collection', 'created_at', 'updated_at', 'images', 'link', 'longitude', - 'latitude', 'visits', 'is_visited', 'category', 'attachments' + 'latitude', 'visits', 'is_visited', 'category', 'attachments', 'user' ] - read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'is_visited'] + read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'is_visited', 'user'] def validate_category(self, category_data): if isinstance(category_data, Category): @@ -126,7 +128,11 @@ class AdventureSerializer(CustomModelSerializer): } ) return category - + + def get_user(self, obj): + user = obj.user_id + return CustomUserDetailsSerializer(user).data + def get_is_visited(self, obj): current_date = timezone.now().date() for visit in obj.visits.all(): @@ -197,6 +203,17 @@ class TransportationSerializer(CustomModelSerializer): ] read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] +class LodgingSerializer(CustomModelSerializer): + + class Meta: + model = Lodging + fields = [ + 'id', 'user_id', 'name', 'description', 'rating', 'link', 'check_in', 'check_out', + 'reservation_number', 'price', 'latitude', 'longitude', 'location', 'is_public', + 'collection', 'created_at', 'updated_at', 'type' + ] + read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] + class NoteSerializer(CustomModelSerializer): class Meta: @@ -283,10 +300,11 @@ class CollectionSerializer(CustomModelSerializer): 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') + lodging = LodgingSerializer(many=True, read_only=True, source='lodging_set') 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'] + 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'] def to_representation(self, instance): @@ -296,5 +314,4 @@ class CollectionSerializer(CustomModelSerializer): for user in instance.shared_with.all(): shared_uuids.append(str(user.uuid)) representation['shared_with'] = shared_uuids - return representation - \ No newline at end of file + return representation \ No newline at end of file diff --git a/backend/server/adventures/urls.py b/backend/server/adventures/urls.py index 16ab3b5..1a98273 100644 --- a/backend/server/adventures/urls.py +++ b/backend/server/adventures/urls.py @@ -18,6 +18,7 @@ router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-cale router.register(r'overpass', OverpassViewSet, basename='overpass') router.register(r'search', GlobalSearchView, basename='search') router.register(r'attachments', AttachmentViewSet, basename='attachments') +router.register(r'lodging', LodgingViewSet, basename='lodging') urlpatterns = [ diff --git a/backend/server/adventures/views/__init__.py b/backend/server/adventures/views/__init__.py index 171df52..8f531f7 100644 --- a/backend/server/adventures/views/__init__.py +++ b/backend/server/adventures/views/__init__.py @@ -12,4 +12,5 @@ from .reverse_geocode_view import * from .stats_view import * from .transportation_view import * from .global_search_view import * -from .attachment_view import * \ No newline at end of file +from .attachment_view import * +from .lodging_view import * \ No newline at end of file diff --git a/backend/server/adventures/views/adventure_view.py b/backend/server/adventures/views/adventure_view.py index 5249e00..0dd52c7 100644 --- a/backend/server/adventures/views/adventure_view.py +++ b/backend/server/adventures/views/adventure_view.py @@ -1,3 +1,4 @@ +from django.utils import timezone from django.db import transaction from django.core.exceptions import PermissionDenied from django.db.models import Q, Max @@ -5,10 +6,9 @@ from django.db.models.functions import Lower from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response - -from adventures.models import Adventure, Category +from adventures.models import Adventure, Category, Transportation, Lodging from adventures.permissions import IsOwnerOrSharedWithFullAccess -from adventures.serializers import AdventureSerializer +from adventures.serializers import AdventureSerializer, TransportationSerializer, LodgingSerializer from adventures.utils import pagination class AdventureViewSet(viewsets.ModelViewSet): @@ -98,9 +98,22 @@ class AdventureViewSet(viewsets.ModelViewSet): user_id=request.user.id ) - if is_visited.lower() in ['true', 'false']: - is_visited_bool = is_visited.lower() == 'true' - queryset = queryset.filter(is_visited=is_visited_bool) + is_visited_param = request.query_params.get('is_visited') + if is_visited_param is not None: + # Convert is_visited_param to a boolean + if is_visited_param.lower() == 'true': + is_visited_bool = True + elif is_visited_param.lower() == 'false': + is_visited_bool = False + else: + is_visited_bool = None + + # Filter logic: "visited" means at least one visit with start_date <= today + now = timezone.now().date() + if is_visited_bool is True: + queryset = queryset.filter(visits__start_date__lte=now).distinct() + elif is_visited_bool is False: + queryset = queryset.exclude(visits__start_date__lte=now).distinct() queryset = self.apply_sorting(queryset) return self.paginate_and_respond(queryset, request) @@ -185,3 +198,49 @@ class AdventureViewSet(viewsets.ModelViewSet): serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) + + # @action(detail=True, methods=['post']) + # def convert(self, request, pk=None): + # """ + # Convert an Adventure instance into a Transportation or Lodging instance. + # Expects a JSON body with "target_type": "transportation" or "lodging". + # """ + # adventure = self.get_object() + # target_type = request.data.get("target_type", "").lower() + + # if target_type not in ["transportation", "lodging"]: + # return Response( + # {"error": "Invalid target type. Must be 'transportation' or 'lodging'."}, + # status=400 + # ) + # if not adventure.collection: + # return Response( + # {"error": "Adventure must be part of a collection to be converted."}, + # status=400 + # ) + + # # Define the overlapping fields that both the Adventure and target models share. + # overlapping_fields = ["name", "description", "is_public", 'collection'] + + # # Gather the overlapping data from the adventure instance. + # conversion_data = {} + # for field in overlapping_fields: + # if hasattr(adventure, field): + # conversion_data[field] = getattr(adventure, field) + + # # Make sure to include the user reference + # conversion_data["user_id"] = adventure.user_id + + # # Convert the adventure instance within an atomic transaction. + # with transaction.atomic(): + # if target_type == "transportation": + # new_instance = Transportation.objects.create(**conversion_data) + # serializer = TransportationSerializer(new_instance) + # else: # target_type == "lodging" + # new_instance = Lodging.objects.create(**conversion_data) + # serializer = LodgingSerializer(new_instance) + + # # Optionally, delete the original adventure to avoid duplicates. + # adventure.delete() + + # return Response(serializer.data) diff --git a/backend/server/adventures/views/lodging_view.py b/backend/server/adventures/views/lodging_view.py new file mode 100644 index 0000000..16114ba --- /dev/null +++ b/backend/server/adventures/views/lodging_view.py @@ -0,0 +1,84 @@ +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 Lodging +from adventures.serializers import LodgingSerializer +from rest_framework.exceptions import PermissionDenied +from adventures.permissions import IsOwnerOrSharedWithFullAccess +from rest_framework.permissions import IsAuthenticated + +class LodgingViewSet(viewsets.ModelViewSet): + queryset = Lodging.objects.all() + serializer_class = LodgingSerializer + permission_classes = [IsOwnerOrSharedWithFullAccess] + + def list(self, request, *args, **kwargs): + if not request.user.is_authenticated: + return Response(status=status.HTTP_403_FORBIDDEN) + queryset = Lodging.objects.filter( + Q(user_id=request.user.id) + ) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + 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 + return Lodging.objects.filter( + Q(is_public=True) | Q(user_id=user.id) | Q(collection__shared_with=user.id) + ).distinct().order_by('-updated_at') + # For other actions, include user's own adventures and shared adventures + return Lodging.objects.filter( + Q(user_id=user.id) | Q(collection__shared_with=user.id) + ).distinct().order_by('-updated_at') + + def partial_update(self, request, *args, **kwargs): + # Retrieve the current object + instance = self.get_object() + user = request.user + + # Partially update the instance with the request data + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + + # Retrieve the collection from the validated data + new_collection = serializer.validated_data.get('collection') + + 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: + 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: + raise PermissionDenied("You cannot remove the collection as you are not the owner.") + + # Perform the update + self.perform_update(serializer) + + # Return the updated instance + return Response(serializer.data) + + def perform_update(self, serializer): + serializer.save() + + # when creating an adventure, make sure the user is the owner of the collection or shared with the collection + def perform_create(self, serializer): + # Retrieve the collection from the validated data + collection = serializer.validated_data.get('collection') + + # Check if a collection is provided + 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(): + # 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) + return + + # Save the adventure with the current user as the owner + serializer.save(user_id=self.request.user) \ No newline at end of file diff --git a/backend/server/adventures/views/reverse_geocode_view.py b/backend/server/adventures/views/reverse_geocode_view.py index dbbd026..4dc1d6b 100644 --- a/backend/server/adventures/views/reverse_geocode_view.py +++ b/backend/server/adventures/views/reverse_geocode_view.py @@ -21,10 +21,14 @@ class ReverseGeocodeViewSet(viewsets.ViewSet): country_code = None city = None visited_city = None + location_name = None # town = None # city = None # county = None + + if 'name' in data.keys(): + location_name = data['name'] if 'address' in data.keys(): keys = data['address'].keys() @@ -58,7 +62,7 @@ class ReverseGeocodeViewSet(viewsets.ViewSet): if visited_city: city_visited = True if region: - return {"region_id": iso_code, "region": region.name, "country": region.country.name, "region_visited": region_visited, "display_name": display_name, "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited} + return {"region_id": iso_code, "region": region.name, "country": region.country.name, "region_visited": region_visited, "display_name": display_name, "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited, 'location_name': location_name} return {"error": "No region found"} @action(detail=False, methods=['get']) diff --git a/backend/server/adventures/views/stats_view.py b/backend/server/adventures/views/stats_view.py index 23733a5..37d9f07 100644 --- a/backend/server/adventures/views/stats_view.py +++ b/backend/server/adventures/views/stats_view.py @@ -2,29 +2,42 @@ 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 django.contrib.auth import get_user_model + +User = get_user_model() class StatsViewSet(viewsets.ViewSet): """ A simple ViewSet for listing the stats of a user. """ - permission_classes = [IsAuthenticated] + @action(detail=False, methods=['get'], url_path='counts/(?P[^/.]+)') + def counts(self, request, username): + if request.user.username == username: + user = get_object_or_404(User, username=username) + else: + user = get_object_or_404(User, username=username, public_profile=True) + serializer = PublicUserSerializer(user) + + # remove the email address from the response + user.email = None - @action(detail=False, methods=['get']) - def counts(self, request): + # get the counts for the user adventure_count = Adventure.objects.filter( - user_id=request.user.id).count() + user_id=user.id).count() trips_count = Collection.objects.filter( - user_id=request.user.id).count() + user_id=user.id).count() visited_city_count = VisitedCity.objects.filter( - user_id=request.user.id).count() + user_id=user.id).count() total_cities = City.objects.count() visited_region_count = VisitedRegion.objects.filter( - user_id=request.user.id).count() + user_id=user.id).count() total_regions = Region.objects.count() visited_country_count = VisitedRegion.objects.filter( - user_id=request.user.id).values('region__country').distinct().count() + user_id=user.id).values('region__country').distinct().count() total_countries = Country.objects.count() return Response({ 'adventure_count': adventure_count, diff --git a/backend/server/integrations/views.py b/backend/server/integrations/views.py index 935e28c..dbf383a 100644 --- a/backend/server/integrations/views.py +++ b/backend/server/integrations/views.py @@ -63,25 +63,31 @@ class ImmichIntegrationView(viewsets.ViewSet): return integration query = request.query_params.get('query', '') + date = request.query_params.get('date', '') - if not query: + if not query and not date: return Response( { - 'message': 'Query is required.', + 'message': 'Query or date is required.', 'error': True, 'code': 'immich.query_required' }, status=status.HTTP_400_BAD_REQUEST ) + arguments = {} + if query: + arguments['query'] = query + if date: + arguments['takenBefore'] = date + # check so if the server is down, it does not tweak out like a madman and crash the server with a 500 error code try: - immich_fetch = requests.post(f'{integration.server_url}/search/smart', headers={ + url = f'{integration.server_url}/search/{"smart" if query else "metadata"}' + immich_fetch = requests.post(url, headers={ 'x-api-key': integration.api_key }, - json = { - 'query': query - } + json = arguments ) res = immich_fetch.json() except requests.exceptions.ConnectionError: @@ -219,10 +225,14 @@ class ImmichIntegrationView(viewsets.ViewSet): ) if 'assets' in res: - return Response( - res['assets'], - status=status.HTTP_200_OK - ) + paginator = self.pagination_class() + # for each item in the items, we need to add the image url to the item so we can display it in the frontend + public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/') + public_url = public_url.replace("'", "") + for item in res['assets']: + item['image_url'] = f'{public_url}/api/integrations/immich/get/{item["id"]}' + result_page = paginator.paginate_queryset(res['assets'], request) + return paginator.get_paginated_response(result_page) else: return Response( { diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index c83c711..dd099a1 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -61,6 +61,7 @@ INSTALLED_APPS = ( 'users', 'integrations', 'django.contrib.gis', + # 'achievements', # Not done yet, will be added later in a future update # 'widget_tweaks', # 'slippers', @@ -128,23 +129,20 @@ USE_L10N = True USE_TZ = True unParsedFrontenedUrl = getenv('FRONTEND_URL', 'http://localhost:3000') -FRONTEND_URL = unParsedFrontenedUrl.replace("'", "").replace('"', '') +FRONTEND_URL = unParsedFrontenedUrl.translate(str.maketrans('', '', '\'"')) -SESSION_COOKIE_SAMESITE = None +SESSION_COOKIE_SAMESITE = 'Lax' SESSION_COOKIE_SECURE = FRONTEND_URL.startswith('https') -# Parse the FRONTEND_URL -# Remove and ' from the URL - -parsed_url = urlparse(FRONTEND_URL) -hostname = parsed_url.hostname - -# Check if the hostname is an IP address +hostname = urlparse(FRONTEND_URL).hostname is_ip_address = hostname.replace('.', '').isdigit() -if is_ip_address: - # Do not set a domain for IP addresses +# Check if the hostname is single-label (no dots) +is_single_label = '.' not in hostname + +if is_ip_address or is_single_label: + # Do not set a domain for IP addresses or single-label hostnames SESSION_COOKIE_DOMAIN = None else: # Use publicsuffix2 to calculate the correct cookie domain @@ -155,6 +153,7 @@ else: # Fallback to the hostname if parsing fails SESSION_COOKIE_DOMAIN = hostname + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.7/howto/static-files/ @@ -301,5 +300,8 @@ LOGGING = { }, }, } + +# ADVENTURELOG_CDN_URL = getenv('ADVENTURELOG_CDN_URL', 'https://cdn.adventurelog.app') + # https://github.com/dr5hn/countries-states-cities-database/tags COUNTRY_REGION_JSON_VERSION = 'v2.5' \ No newline at end of file diff --git a/backend/server/main/urls.py b/backend/server/main/urls.py index b7bb2a1..60ce08e 100644 --- a/backend/server/main/urls.py +++ b/backend/server/main/urls.py @@ -22,7 +22,7 @@ urlpatterns = [ path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'), path('auth/users/', PublicUserListView.as_view(), name='public-user-list'), - path('auth/user//', PublicUserDetailView.as_view(), name='public-user-detail'), + path('auth/user//', PublicUserDetailView.as_view(), name='public-user-detail'), path('auth/update-user/', UpdateUserMetadataView.as_view(), name='update-user-metadata'), path('auth/user-metadata/', UserMetadataView.as_view(), name='user-metadata'), diff --git a/backend/server/users/serializers.py b/backend/server/users/serializers.py index 0e0828f..81bacd6 100644 --- a/backend/server/users/serializers.py +++ b/backend/server/users/serializers.py @@ -118,5 +118,7 @@ class CustomUserDetailsSerializer(UserDetailsSerializer): # Remove `pk` field from the response representation.pop('pk', None) + # Remove the email field + representation.pop('email', None) return representation diff --git a/backend/server/users/views.py b/backend/server/users/views.py index b03760e..428c5b8 100644 --- a/backend/server/users/views.py +++ b/backend/server/users/views.py @@ -11,6 +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 User = get_user_model() @@ -66,6 +68,9 @@ class PublicUserListView(APIView): for user in users: user.email = None serializer = PublicUserSerializer(users, many=True) + # for every user, remove the field has_password + for user in serializer.data: + user.pop('has_password', None) return Response(serializer.data, status=status.HTTP_200_OK) class PublicUserDetailView(APIView): @@ -79,12 +84,29 @@ class PublicUserDetailView(APIView): }, operation_description="Get public user information." ) - def get(self, request, user_id): - user = get_object_or_404(User, uuid=user_id, public_profile=True) + def get(self, request, username): + if request.user.username == username: + user = get_object_or_404(User, username=username) + else: + user = get_object_or_404(User, username=username, public_profile=True) + serializer = PublicUserSerializer(user) + # for every user, remove the field has_password + serializer.data.pop('has_password', None) + # remove the email address from the response user.email = None - serializer = PublicUserSerializer(user) - return Response(serializer.data, status=status.HTTP_200_OK) + + # 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) + collection_serializer = CollectionSerializer(collections, many=True) + + return Response({ + 'user': serializer.data, + 'adventures': adventure_serializer.data, + 'collections': collection_serializer.data + }, status=status.HTTP_200_OK) class UserMetadataView(APIView): permission_classes = [IsAuthenticated] diff --git a/cdn/.gitignore b/cdn/.gitignore new file mode 100644 index 0000000..adbb97d --- /dev/null +++ b/cdn/.gitignore @@ -0,0 +1 @@ +data/ \ No newline at end of file diff --git a/cdn/Dockerfile b/cdn/Dockerfile new file mode 100644 index 0000000..f47e79a --- /dev/null +++ b/cdn/Dockerfile @@ -0,0 +1,36 @@ +# Use an official Python image as a base +FROM python:3.11-slim + +# Set the working directory +WORKDIR /app + +# Install required Python packages +RUN pip install --no-cache-dir requests osm2geojson + +# Copy the script into the container +COPY main.py /app/main.py + +# Run the script to generate the data folder and GeoJSON files (this runs inside the container) +RUN python -u /app/main.py + +# Install Nginx +RUN apt update && apt install -y nginx && rm -rf /var/lib/apt/lists/* + +# Copy the entire generated data folder to the Nginx serving directory +RUN mkdir -p /var/www/html/data && cp -r /app/data/* /var/www/html/data/ + +# Copy Nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Copy the index.html file to the Nginx serving directory +COPY index.html /usr/share/nginx/html/index.html + +# Expose port 80 for Nginx +EXPOSE 80 + +# Copy the entrypoint script into the container +COPY entrypoint.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh + +# Set the entrypoint script as the default command +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/cdn/README.md b/cdn/README.md new file mode 100644 index 0000000..f935c14 --- /dev/null +++ b/cdn/README.md @@ -0,0 +1,3 @@ +This folder contains the scripts to generate AdventureLOG CDN files. + +Special thanks to [@larsl-net](https://github.com/larsl-net) for the GeoJSON generation script. diff --git a/cdn/docker-compose.yml b/cdn/docker-compose.yml new file mode 100644 index 0000000..5699dd8 --- /dev/null +++ b/cdn/docker-compose.yml @@ -0,0 +1,9 @@ +services: + cdn: + build: . + container_name: adventurelog-cdn + ports: + - "8080:80" + restart: unless-stopped + volumes: + - ./data:/app/data # Ensures new data files persist diff --git a/cdn/entrypoint.sh b/cdn/entrypoint.sh new file mode 100644 index 0000000..fffe244 --- /dev/null +++ b/cdn/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Any setup tasks or checks can go here (if needed) +echo "AdventureLog CDN has started!" +echo "Refer to the documentation for information about connecting your AdventureLog instance to this CDN." +echo "Thanks to our data providers for making this possible! You can find them on the CDN site." + +# Start Nginx in the foreground (as the main process) +nginx -g 'daemon off;' diff --git a/cdn/index.html b/cdn/index.html new file mode 100644 index 0000000..7b78153 --- /dev/null +++ b/cdn/index.html @@ -0,0 +1,90 @@ + + + + + + + AdventureLog CDN + + + + +
+
+

Welcome to the AdventureLog CDN

+

+ This is a content delivery network for the AdventureLog project. You + can browse the content by clicking the button below. +

+ Browse Content +
+
+

About AdventureLog

+

+ AdventureLog is a project that aims to provide a platform for users to + log their adventures and share them with the world. The project is + developed by Sean Morley and is + open source. View it on GitHub here: + AdventureLog. +

+
+ +
+
+
+

Data Attributions

+

+ The data provided in this CDN is sourced from the following + repositories: +

+ +
+
+
+
+ + diff --git a/cdn/main.py b/cdn/main.py new file mode 100644 index 0000000..2c052fa --- /dev/null +++ b/cdn/main.py @@ -0,0 +1,87 @@ +import requests +import json +import os + +# The version of the CDN, this should be updated when the CDN data is updated so the client can check if it has the latest version +ADVENTURELOG_CDN_VERSION = 'v0.0.1' + +# https://github.com/dr5hn/countries-states-cities-database/tags +COUNTRY_REGION_JSON_VERSION = 'v2.5' # Test on past and latest versions to ensure that the data schema is consistent before updating + +def makeDataDir(): + """ + Creates the data directory if it doesn't exist + """ + path = os.path.join(os.path.dirname(__file__), 'data') + if not os.path.exists(path): + os.makedirs(path) + +def saveCdnVersion(): + """ + Saves the CDN version to a JSON file so the client can check if it has the latest version + """ + path = os.path.join(os.path.dirname(__file__), 'data', 'version.json') + with open(path, 'w') as f: + json.dump({'version': ADVENTURELOG_CDN_VERSION}, f) + print('CDN Version saved') + +def downloadCountriesStateCities(): + """ + Downloads the countries, states and cities data from the countries-states-cities-database repository + """ + res = requests.get(f'https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/{COUNTRY_REGION_JSON_VERSION}/json/countries%2Bstates%2Bcities.json') + + path = os.path.join(os.path.dirname(__file__), 'data', f'countries_states_cities.json') + + with open(path, 'w') as f: + f.write(res.text) + print('Countries, states and cities data downloaded successfully') + +def saveCountryFlag(country_code, name): + """ + Downloads the flag of a country and saves it in the data/flags directory + """ + # For standards, use the lowercase country_code + country_code = country_code.lower() + # Save the flag in the data/flags directory + flags_dir = os.path.join(os.path.dirname(__file__), 'data', 'flags') + + # Check if the flags directory exists, if not, create it + if not os.path.exists(flags_dir): + os.makedirs(flags_dir) + + # Check if the flag already exists in the media folder + flag_path = os.path.join(flags_dir, f'{country_code}.png') + if os.path.exists(flag_path): + # remove the flag if it already exists + os.remove(flag_path) + print(f'Flag for {country_code} ({name}) removed') + + res = requests.get(f'https://flagcdn.com/h240/{country_code}.png'.lower()) + if res.status_code == 200: + with open(flag_path, 'wb') as f: + f.write(res.content) + print(f'Flag for {country_code} downloaded') + else: + print(f'Error downloading flag for {country_code} ({name})') + +def saveCountryFlags(): + """ + Downloads the flags of all countries and saves them in the data/flags directory + """ + # Load the countries data + with open(os.path.join(os.path.dirname(__file__), 'data', f'countries_states_cities.json')) as f: + data = json.load(f) + + for country in data: + country_code = country['iso2'] + name = country['name'] + saveCountryFlag(country_code, name) + +# Run the functions +print('Starting CDN update') +makeDataDir() +saveCdnVersion() +downloadCountriesStateCities() +saveCountryFlags() +print('CDN update complete') \ No newline at end of file diff --git a/cdn/nginx.conf b/cdn/nginx.conf new file mode 100644 index 0000000..44a0df9 --- /dev/null +++ b/cdn/nginx.conf @@ -0,0 +1,13 @@ +events {} + +http { + server { + listen 80; + server_name _; + + location /data/ { + root /var/www/html; + autoindex on; # Enable directory listing + } + } +} diff --git a/cdn/requirements.txt b/cdn/requirements.txt new file mode 100644 index 0000000..1f11fab --- /dev/null +++ b/cdn/requirements.txt @@ -0,0 +1 @@ +osm2geojson==0.2.5 \ No newline at end of file diff --git a/documentation/.vitepress/config.mts b/documentation/.vitepress/config.mts index 5638acf..dbd9299 100644 --- a/documentation/.vitepress/config.mts +++ b/documentation/.vitepress/config.mts @@ -178,6 +178,8 @@ export default defineConfig({ { icon: "github", link: "https://github.com/seanmorley15/AdventureLog" }, { icon: "discord", link: "https://discord.gg/wRbQ9Egr8C" }, { icon: "buymeacoffee", link: "https://buymeacoffee.com/seanmorley15" }, + { icon: "x", link: "https://x.com/AdventureLogApp" }, + { icon: "mastodon", link: "https://mastodon.social/@adventurelog" }, ], }, }); diff --git a/documentation/docs/configuration/social_auth/authentik.md b/documentation/docs/configuration/social_auth/authentik.md index 1c13f50..ff57ac3 100644 --- a/documentation/docs/configuration/social_auth/authentik.md +++ b/documentation/docs/configuration/social_auth/authentik.md @@ -55,6 +55,10 @@ This configuration is done in the [Admin Panel](../../guides/admin_panel.md). Yo Ensure that the Authentik server is running and accessible by AdventureLog. Users should now be able to log in to AdventureLog using their Authentik account. +## Linking to Existing Account + +If a user has an existing AdventureLog account and wants to link it to their Authentik account, they can do so by logging in to their AdventureLog account and navigating to the `Settings` page. There is a button that says `Launch Account Connections`, click that and then choose the provider to link to the existing account. + ## Troubleshooting ### 404 error when logging in. diff --git a/documentation/docs/configuration/social_auth/github.md b/documentation/docs/configuration/social_auth/github.md index cbfe41b..51efb74 100644 --- a/documentation/docs/configuration/social_auth/github.md +++ b/documentation/docs/configuration/social_auth/github.md @@ -35,10 +35,15 @@ This configuration is done in the [Admin Panel](../../guides/admin_panel.md). Yo - Settings: can be left blank - Sites: move over the sites you want to enable Authentik on, usually `example.com` and `www.example.com` unless you renamed your sites. +4. Save the configuration. + +Users should now be able to log in to AdventureLog using their GitHub account, and link it to existing accounts. + +## Linking to Existing Account + +If a user has an existing AdventureLog account and wants to link it to their Github account, they can do so by logging in to their AdventureLog account and navigating to the `Settings` page. There is a button that says `Launch Account Connections`, click that and then choose the provider to link to the existing account. + #### What it Should Look Like ![Authentik Social Auth Configuration](/github_settings.png) -4. Save the configuration. - -Users should now be able to log in to AdventureLog using their GitHub account, and link it to existing accounts. diff --git a/frontend/package.json b/frontend/package.json index b80fe8d..ae71362 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,6 @@ "@event-calendar/day-grid": "^3.7.1", "@event-calendar/time-grid": "^3.7.1", "@iconify-json/mdi": "^1.1.67", - "@sveltejs/adapter-auto": "^3.2.2", "@sveltejs/adapter-node": "^5.2.0", "@sveltejs/adapter-vercel": "^5.4.1", "@sveltejs/kit": "^2.8.3", @@ -47,7 +46,6 @@ "psl": "^1.15.0", "qrcode": "^1.5.4", "svelte-i18n": "^4.0.1", - "svelte-maplibre": "^0.9.8", - "tsparticles": "^3.7.1" + "svelte-maplibre": "^0.9.8" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index ea075fa..edde452 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: svelte-maplibre: specifier: ^0.9.8 version: 0.9.8(svelte@4.2.19) - tsparticles: - specifier: ^3.7.1 - version: 3.7.1 devDependencies: '@event-calendar/core': specifier: ^3.7.1 @@ -51,9 +48,6 @@ importers: '@iconify-json/mdi': specifier: ^1.1.67 version: 1.1.67 - '@sveltejs/adapter-auto': - specifier: ^3.2.2 - version: 3.2.2(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4))) '@sveltejs/adapter-node': specifier: ^5.2.0 version: 5.2.0(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4))) @@ -741,11 +735,6 @@ packages: cpu: [x64] os: [win32] - '@sveltejs/adapter-auto@3.2.2': - resolution: {integrity: sha512-Mso5xPCA8zgcKrv+QioVlqMZkyUQ5MjDJiEPuG/Z7cV/5tmwV7LmcVWk5tZ+H0NCOV1x12AsoSpt/CwFwuVXMA==} - peerDependencies: - '@sveltejs/kit': ^2.0.0 - '@sveltejs/adapter-node@5.2.0': resolution: {integrity: sha512-HVZoei2078XSyPmvdTHE03VXDUD0ytTvMuMHMQP0j6zX4nPDpCcKrgvU7baEblMeCCMdM/shQvstFxOJPQKlUQ==} peerDependencies: @@ -785,147 +774,6 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders' - '@tsparticles/basic@3.7.1': - resolution: {integrity: sha512-oJMJ3qzYUROYaOEsaFXkVynxT2OTWBXbQ9MNc1bJi/bVc1VOU44VN7X/KmiZjD+w1U+Qalk6BeVvDRwpFshblw==} - - '@tsparticles/engine@3.7.1': - resolution: {integrity: sha512-GYzBgq/oOE9YJdOL1++MoawWmYg4AvVct6CIrJGx84ZRb3U2owYmLsRGabYl0qX1CWWOvUG569043RJmyp/vQA==} - - '@tsparticles/interaction-external-attract@3.7.1': - resolution: {integrity: sha512-cpnMsFJ7ZJNKccpQvskKvSs1ofknByHE6FGqbEb17ij7HqvbECQOCOVKHPFnYipHe14cXor/Cd+nVisRcTASoQ==} - - '@tsparticles/interaction-external-bounce@3.7.1': - resolution: {integrity: sha512-npvU9Qt6WDonjezHqi+hWM44ga2Oh5yXdr8eSoJpvuHZrCP7rIdRSz5XseHouO1bMS9DbXk86sx4qwrhB5w58w==} - - '@tsparticles/interaction-external-bubble@3.7.1': - resolution: {integrity: sha512-WdbYL46lMfuf2g5kfVB1hhhxRBtEXDvnwz8PJwLKurSThL/27bqsqysyXsMzXtLByXUneGhtJTj4D5I5RYdgjA==} - - '@tsparticles/interaction-external-connect@3.7.1': - resolution: {integrity: sha512-hqx0ANIbjLIz/nxmk0LvqANBiNLLmVybbCA7N+xDHtEORvpKjNlKEvMz6Razocl6vRjoHZ/olSwcxIG84dh/cg==} - - '@tsparticles/interaction-external-grab@3.7.1': - resolution: {integrity: sha512-JMYpFW+7YvkpK5MYlt4Ec3Gwb5ZxS7RLVL8IRUSd5yJOw25husPTYg+FQywxrt5WhKe+tPsCAYo+uGIbTTHi9w==} - - '@tsparticles/interaction-external-pause@3.7.1': - resolution: {integrity: sha512-Kkp+7sCe24hawH0XvS1V6UCCuHfMvpLK7oseqSam9Gt4SyGrFvaqIXxkjXhRhn9MysJyKFPBV4/dtBM1HR9p6A==} - - '@tsparticles/interaction-external-push@3.7.1': - resolution: {integrity: sha512-4VoaR5jvXgQdB7irtq4uSZYr5c+D6TBTVEnLVpBfJhUs6jhw6mgN5g7yp5izIYkK0AlcO431MHn8dvJacvRLDw==} - - '@tsparticles/interaction-external-remove@3.7.1': - resolution: {integrity: sha512-FRBW7U7zD5MkO6/b7e8iSMk/UTtRLY2XiIVFZNsKri3Re3yPpvZzzd5tl2YlYGQlg1Xc+K8SJYMQQA3PtgQ/Tg==} - - '@tsparticles/interaction-external-repulse@3.7.1': - resolution: {integrity: sha512-mwM06dVmg2FEvHMQsPOfRBQWACbjf3qnelODkqI9DSVxQ0B8DESP4BYNXyraFGYv00YiPzRv5Xy/uejHdbsQUA==} - - '@tsparticles/interaction-external-slow@3.7.1': - resolution: {integrity: sha512-CfCAs3kUQC3pLOj0dbzn5AolQyBHgjxORLdfnYBhepvFV1BXB+4ytChRfXBzjykBPI6U+rCnw5Fk/vVjAroSFA==} - - '@tsparticles/interaction-external-trail@3.7.1': - resolution: {integrity: sha512-M7lNQUWP15m8YIDP/JZcZAXaVJLqdwpBs0Uv9F6dU6jsnNXwwHFVFZ+1icrnlbgl9k/Ehhodbdo5weE7GHhQhQ==} - - '@tsparticles/interaction-particles-attract@3.7.1': - resolution: {integrity: sha512-UABbBORKaiItAT8vR0t4ye2H3VE6/Ah4zcylBlnq0Jd5yDkyP4rnkwhicaY6y4Zlfwyx+0PWdAC4f/ziFAyObg==} - - '@tsparticles/interaction-particles-collisions@3.7.1': - resolution: {integrity: sha512-0GY9++Gn2KXViyeifXWkH7a2UO5+uRwyS1rDeTN8eleyiq2j9zQf4xztZEIft8T0hTetq2rkWxQ92j2kev6NVA==} - - '@tsparticles/interaction-particles-links@3.7.1': - resolution: {integrity: sha512-BxCXAAOBNmEvlyOQzwprryW8YdtMIop2v4kgSCff5MCtDwYWoQIfzaQlWbBAkD9ey6BoF8iMjhBUaY1MnDecTA==} - - '@tsparticles/move-base@3.7.1': - resolution: {integrity: sha512-LPtMHwJHhzwfRIcSAk814fY9NcRiICwaEbapaJSYyP1DwscSXqOWoyAEWwzV9hMgAcPdsED6nGeg8RCXGm58lw==} - - '@tsparticles/move-parallax@3.7.1': - resolution: {integrity: sha512-B40azo6EJyMdI+kmIxpqWDaObPwODTYLDCikzkZ73n5tS6OhFUlkz81Scfo+g1iGTdryKFygUKhVGcG1EFuA5g==} - - '@tsparticles/plugin-absorbers@3.7.1': - resolution: {integrity: sha512-3s+fILLV1tdKOq/bXwfoxFVbzkWwXpdWTC2C0QIP6BFwDSQqV5txluiLEf7SCf8C5etQ6dstEnOgVbdzK7+eWA==} - - '@tsparticles/plugin-easing-quad@3.7.1': - resolution: {integrity: sha512-nSwKCRe6C/noCi3dyZlm1GiQGask0aXdWDuS36b82iwzwQ01cBTXeXR25mLr4fsfMLFfYAZXyBxEMMpw3rkSiw==} - - '@tsparticles/plugin-emitters-shape-circle@3.7.1': - resolution: {integrity: sha512-eBwktnGROkiyCvtrSwdPpoRbIjQgV/Odq//0dw8D+qUdnox6dNzzhJjz8L2LAA2kQZBqtdBqV2kcx3w5ZdqoEQ==} - - '@tsparticles/plugin-emitters-shape-square@3.7.1': - resolution: {integrity: sha512-nvGBsRLrkiz6Q38TJRl8Y/eu9i1ChQ9oorQydLBok+iZ6MefuOj39iYsAOkD1w9yRVrFWKHG6CR1mmJUniz/HA==} - - '@tsparticles/plugin-emitters@3.7.1': - resolution: {integrity: sha512-WV5Uwxp/Ckqv5kZynTj6mj13jYbQCArNLFv8ks+zjdlteoyT5EhQl4rg+TalaySCb1zCd6Fu2Scp35l3JJgbnw==} - - '@tsparticles/plugin-hex-color@3.7.1': - resolution: {integrity: sha512-7xu3MV8EdNNejjYyEmrq5fCDdYAcqz/9VatLpnwtwR5Q5t2qI0tD4CrcGaFfC/rTAVJacfiJe02UV/hlj03KKA==} - - '@tsparticles/plugin-hsl-color@3.7.1': - resolution: {integrity: sha512-zzAI1CuoCMBJhgeYZ5Rq42nGbPg35ZzIs11eQegjsWG5Msm5QKSj60qPzERnoUcCc4HCKtIWP7rYMz6h3xpoEg==} - - '@tsparticles/plugin-rgb-color@3.7.1': - resolution: {integrity: sha512-taEraTpCYR6jpjflqBL95tN0zFU8JrAChXTt8mxVn7gddxoNMHI/LGymEPRCweLukwV6GQyAGOkeGEdWDPtYTA==} - - '@tsparticles/shape-circle@3.7.1': - resolution: {integrity: sha512-kmOWaUuFwuTtcCFYjuyJbdA5qDqWdGsharLalYnIczkLu2c1I8jJo/OmGePKhWn62ocu7mqKMomfElkIcw2AsA==} - - '@tsparticles/shape-emoji@3.7.1': - resolution: {integrity: sha512-mX18c/xhYVljS/r5Xbowzclw+1YwhtWoQFOOfkmjjZppA+RjgcwSKLvH6E20PaH1yVTjBOfSF+3POKpwsULzTg==} - - '@tsparticles/shape-image@3.7.1': - resolution: {integrity: sha512-eDzfkQhqLY6fb9QH2Vo9TGfdJBFFpYnojhxQxc7IdzIwOFMD3JK4B52RVl9oowR+rNE8dNp6P2L+eMAF4yld0g==} - - '@tsparticles/shape-line@3.7.1': - resolution: {integrity: sha512-lMPYApUpg7avxmYPfHHr4dQepZSNn/g0Q1/g2+lnTi8ZtUBiCZ2WMVy9R3GOzyozbnzigLQ6AJRnOpsUZV7H4g==} - - '@tsparticles/shape-polygon@3.7.1': - resolution: {integrity: sha512-5FrRfpYC3qnvV2nXBLE4Q0v+SMNWJO8xgzh6MBFwfptvqH4EOrqc/58eS5x0jlf+evwf9LjPgeGkOTcwaHHcYQ==} - - '@tsparticles/shape-square@3.7.1': - resolution: {integrity: sha512-7VCqbRwinjBZ+Ryme27rOtl+jKrET8qDthqZLrAoj3WONBqyt+R9q6SXAJ9WodqEX68IBvcluqbFY5qDZm8iAQ==} - - '@tsparticles/shape-star@3.7.1': - resolution: {integrity: sha512-3G4oipioyWKLEQYT11Sx3k6AObu3dbv/A5LRqGGTQm5IR6UACa+INwykZYI0a+MdJJMb83E0e4Fn3hlZbi0/8w==} - - '@tsparticles/shape-text@3.7.1': - resolution: {integrity: sha512-aU1V9O8uQQBlL0jGFh9Q0b5vQ1Ji6Oo5ptyyj5yJ5uP/ZU00L0Vhk4DNyLXpaU0+H6OBoPpCqnvEsZBB9/HmCQ==} - - '@tsparticles/slim@3.7.1': - resolution: {integrity: sha512-OtJEhud2KleX7OxiG2r/VYriHNIwTpFm3sPFy4EOJzAD0EW7KZoKXGpGn5gwGI1NWeB0jso92yNTrTC2ZTW0qw==} - - '@tsparticles/updater-color@3.7.1': - resolution: {integrity: sha512-QimV3yn17dcdJx7PpTwLtw9BhkQ0q8qFF035OdcZpnynBPAO/hg0zvSMpMGoeuDVFH02wWBy4h2/BYCv6wh6Sw==} - - '@tsparticles/updater-destroy@3.7.1': - resolution: {integrity: sha512-krXNoMDKyeyE/ZjQh3LVjrLYivFefQOQ9i+B7RpMe7x4h+iRgpB6npTCqidGQ82+hZ8G6xfQ9ToduebWwK4JGg==} - - '@tsparticles/updater-life@3.7.1': - resolution: {integrity: sha512-NY5gUrgO5AsARNC0usP9PKahXf7JCxbP/H1vzTfA0SJw4veANfWTldOvhIlcm2CHVP5P1b827p0hWsBHECwz7A==} - - '@tsparticles/updater-opacity@3.7.1': - resolution: {integrity: sha512-YcyviCooTv7SAKw7sxd84CfJqZ7dYPSdYZzCpedV6TKIObRiwLqXlyLXQGJ3YltghKQSCSolmVy8woWBCDm1qA==} - - '@tsparticles/updater-out-modes@3.7.1': - resolution: {integrity: sha512-Cb5sWquRtUYLSiFpmBjjYKRdpNV52diCo9+qMtK1oVlldDBhUwqO+1TQjdlaA2yl5DURlY9ZfOHXvY+IT7CHCw==} - - '@tsparticles/updater-roll@3.7.1': - resolution: {integrity: sha512-gHLRqpTGVGPJBEAIPUiYVembIn5bcaTXXxsUJEM/IN+GIOvj2uZZGZ4r2aFTA6WugqEbJsJdblDSvMfouyz7Ug==} - - '@tsparticles/updater-rotate@3.7.1': - resolution: {integrity: sha512-toVHwl+h6SvtA8dyxSA2kMH2QdDA71vehuAa+HoRqf1y06h5kxyYiMKZFHCqDJ6lFfRPs47MjrC9dD2bDz14MQ==} - - '@tsparticles/updater-size@3.7.1': - resolution: {integrity: sha512-+Y0H0PnDJVIsJ+zHTyubYu1jtRFmVnY1dAv3VCjScIDw6bcpL/ol+HrtHTGIX0WbMyUfjCyALfAoaXi/Wm8VcQ==} - - '@tsparticles/updater-stroke-color@3.7.1': - resolution: {integrity: sha512-VHhQkCNuxjx/Hy7A+g0Yijb24T0+wQ3jNsF/yfrR9dEdZWSBiimZLvV1bilPdAeEtieAJTAZo2VNhcD1snF0iQ==} - - '@tsparticles/updater-tilt@3.7.1': - resolution: {integrity: sha512-pSOXoXPre1VPKC5nC5GW0L9jw63w1dVdsDdggEau7MP9xO7trko9L/KyayBX12Y4Ief1ca12Incxxr67hw7GGA==} - - '@tsparticles/updater-twinkle@3.7.1': - resolution: {integrity: sha512-maRTqPbeZcxBK6s1ry+ih71qSVaitfP1KTrAKR38v26GMwyO6z+zYV2bu9WTRt21FRFAoxlMLWxNu21GtQoXDA==} - - '@tsparticles/updater-wobble@3.7.1': - resolution: {integrity: sha512-YIlNg4L0w4egQJhPLpgcvcfv9+X621+cQsrdN9sSmajxhhwtEQvQUvFUzGTcvpjVi+GcBNp0t4sCKEzoP8iaYw==} - '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -2234,9 +2082,6 @@ packages: tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} - tsparticles@3.7.1: - resolution: {integrity: sha512-NNkOYIo01eHpDuaJxDCGgcLEMZKEJTCN/XPVCLg7VxgEWN19rjXpDnDguISxadS8GSFPws7hpGgbeDDAm3MX+Q==} - type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} @@ -2875,11 +2720,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.31.0': optional: true - '@sveltejs/adapter-auto@3.2.2(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))': - dependencies: - '@sveltejs/kit': 2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)) - import-meta-resolve: 4.1.0 - '@sveltejs/adapter-node@5.2.0(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))': dependencies: '@rollup/plugin-commonjs': 26.0.1(rollup@4.24.0) @@ -2946,228 +2786,6 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.4 - '@tsparticles/basic@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - '@tsparticles/move-base': 3.7.1 - '@tsparticles/plugin-hex-color': 3.7.1 - '@tsparticles/plugin-hsl-color': 3.7.1 - '@tsparticles/plugin-rgb-color': 3.7.1 - '@tsparticles/shape-circle': 3.7.1 - '@tsparticles/updater-color': 3.7.1 - '@tsparticles/updater-opacity': 3.7.1 - '@tsparticles/updater-out-modes': 3.7.1 - '@tsparticles/updater-size': 3.7.1 - - '@tsparticles/engine@3.7.1': {} - - '@tsparticles/interaction-external-attract@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/interaction-external-bounce@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/interaction-external-bubble@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/interaction-external-connect@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/interaction-external-grab@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/interaction-external-pause@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/interaction-external-push@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/interaction-external-remove@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/interaction-external-repulse@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/interaction-external-slow@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/interaction-external-trail@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/interaction-particles-attract@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/interaction-particles-collisions@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/interaction-particles-links@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/move-base@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/move-parallax@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/plugin-absorbers@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/plugin-easing-quad@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/plugin-emitters-shape-circle@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - '@tsparticles/plugin-emitters': 3.7.1 - - '@tsparticles/plugin-emitters-shape-square@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - '@tsparticles/plugin-emitters': 3.7.1 - - '@tsparticles/plugin-emitters@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/plugin-hex-color@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/plugin-hsl-color@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/plugin-rgb-color@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/shape-circle@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/shape-emoji@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/shape-image@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/shape-line@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/shape-polygon@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/shape-square@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/shape-star@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/shape-text@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/slim@3.7.1': - dependencies: - '@tsparticles/basic': 3.7.1 - '@tsparticles/engine': 3.7.1 - '@tsparticles/interaction-external-attract': 3.7.1 - '@tsparticles/interaction-external-bounce': 3.7.1 - '@tsparticles/interaction-external-bubble': 3.7.1 - '@tsparticles/interaction-external-connect': 3.7.1 - '@tsparticles/interaction-external-grab': 3.7.1 - '@tsparticles/interaction-external-pause': 3.7.1 - '@tsparticles/interaction-external-push': 3.7.1 - '@tsparticles/interaction-external-remove': 3.7.1 - '@tsparticles/interaction-external-repulse': 3.7.1 - '@tsparticles/interaction-external-slow': 3.7.1 - '@tsparticles/interaction-particles-attract': 3.7.1 - '@tsparticles/interaction-particles-collisions': 3.7.1 - '@tsparticles/interaction-particles-links': 3.7.1 - '@tsparticles/move-parallax': 3.7.1 - '@tsparticles/plugin-easing-quad': 3.7.1 - '@tsparticles/shape-emoji': 3.7.1 - '@tsparticles/shape-image': 3.7.1 - '@tsparticles/shape-line': 3.7.1 - '@tsparticles/shape-polygon': 3.7.1 - '@tsparticles/shape-square': 3.7.1 - '@tsparticles/shape-star': 3.7.1 - '@tsparticles/updater-life': 3.7.1 - '@tsparticles/updater-rotate': 3.7.1 - '@tsparticles/updater-stroke-color': 3.7.1 - - '@tsparticles/updater-color@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/updater-destroy@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/updater-life@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/updater-opacity@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/updater-out-modes@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/updater-roll@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/updater-rotate@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/updater-size@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/updater-stroke-color@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/updater-tilt@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/updater-twinkle@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - - '@tsparticles/updater-wobble@3.7.1': - dependencies: - '@tsparticles/engine': 3.7.1 - '@types/cookie@0.6.0': {} '@types/estree@1.0.6': {} @@ -4539,22 +4157,6 @@ snapshots: tslib@2.6.3: {} - tsparticles@3.7.1: - dependencies: - '@tsparticles/engine': 3.7.1 - '@tsparticles/interaction-external-trail': 3.7.1 - '@tsparticles/plugin-absorbers': 3.7.1 - '@tsparticles/plugin-emitters': 3.7.1 - '@tsparticles/plugin-emitters-shape-circle': 3.7.1 - '@tsparticles/plugin-emitters-shape-square': 3.7.1 - '@tsparticles/shape-text': 3.7.1 - '@tsparticles/slim': 3.7.1 - '@tsparticles/updater-destroy': 3.7.1 - '@tsparticles/updater-roll': 3.7.1 - '@tsparticles/updater-tilt': 3.7.1 - '@tsparticles/updater-twinkle': 3.7.1 - '@tsparticles/updater-wobble': 3.7.1 - type-detect@4.0.8: {} type@2.7.3: {} diff --git a/frontend/src/lib/components/ActivityComplete.svelte b/frontend/src/lib/components/ActivityComplete.svelte index ef39c52..099898c 100644 --- a/frontend/src/lib/components/ActivityComplete.svelte +++ b/frontend/src/lib/components/ActivityComplete.svelte @@ -18,9 +18,7 @@ 'Content-Type': 'application/json' } }); - console.log(res); let data = await res.json(); - console.log('ACTIVITIES' + data.activities); if (data && data.activities) { allActivities = data.activities; } diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index d6d0121..96648d7 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -1,52 +1,37 @@ @@ -811,150 +582,7 @@ -
- -
- {$t('adventures.location_information')} -
-
- -
-
-
- - {#if is_custom_location} - - {/if} -
-
- -
-
- - - -
-
- {#if places.length > 0} -
-

{$t('adventures.search_results')}

- -
- {#each places as place} - - {/each} -
-
- {:else if noPlaces} -

{$t('adventures.no_results')}

- {/if} - -
- - - - - {#each markers as marker} - - {/each} - - {#if reverseGeocodePlace} -
-

- {reverseGeocodePlace.city - ? reverseGeocodePlace.city + ', ' - : ''}{reverseGeocodePlace.region}, - {reverseGeocodePlace.country} -

-

- {reverseGeocodePlace.region}: - {reverseGeocodePlace.region_visited - ? $t('adventures.visited') - : $t('adventures.not_visited')} -

- {#if reverseGeocodePlace.city} -

- {reverseGeocodePlace.city}: - {reverseGeocodePlace.city_visited - ? $t('adventures.visited') - : $t('adventures.not_visited')} -

- {/if} -
- {#if !reverseGeocodePlace.region_visited || (!reverseGeocodePlace.city_visited && !willBeMarkedVisited)} - - {/if} - {#if (willBeMarkedVisited && !reverseGeocodePlace.region_visited && reverseGeocodePlace.region_id) || (!reverseGeocodePlace.city_visited && willBeMarkedVisited && reverseGeocodePlace.city_id)} - - {/if} - {/if} -
-
-
+
@@ -1257,6 +885,7 @@ it would also work to just use on:click on the MapLibre component itself. --> {#if immichIntegration} { url = e.detail; fetchImage(); @@ -1314,7 +943,7 @@ it would also work to just use on:click on the MapLibre component itself. --> {/if} {#if adventure.is_public && adventure.id} -
+

{$t('adventures.share_adventure')}

diff --git a/frontend/src/lib/components/Avatar.svelte b/frontend/src/lib/components/Avatar.svelte index 01eb068..dbf6b22 100644 --- a/frontend/src/lib/components/Avatar.svelte +++ b/frontend/src/lib/components/Avatar.svelte @@ -34,9 +34,14 @@ ? `${user.first_name} ${user.last_name}` : user.username}

-
  • +
  • + +
  • + {#if user.is_staff} +
  • + {/if}
  • diff --git a/frontend/src/lib/components/CountryCard.svelte b/frontend/src/lib/components/CountryCard.svelte index a2574cc..3894631 100644 --- a/frontend/src/lib/components/CountryCard.svelte +++ b/frontend/src/lib/components/CountryCard.svelte @@ -1,9 +1,8 @@ @@ -111,20 +117,27 @@ on:click={() => (currentAlbum = '')} type="radio" class="join-item btn" - bind:group={searchOrSelect} + bind:group={searchCategory} value="search" aria-label="Search" /> +
    - {#if searchOrSelect === 'search'} + {#if searchCategory === 'search'} - {:else} + {:else if searchCategory === 'date'} + + {:else if searchCategory === 'album'} +
    + {$t('adventures.location_information')} +
    +
    + +
    +
    +
    + + {#if is_custom_location} + + {/if} +
    +
    + +
    +
    + + + +
    +
    + {#if places.length > 0} +
    +

    {$t('adventures.search_results')}

    + +
    + {#each places as place} + + {/each} +
    +
    + {:else if noPlaces} +

    {$t('adventures.no_results')}

    + {/if} + +
    + + + + + {#each markers as marker} + + {/each} + + {#if reverseGeocodePlace} +
    +

    {$t('adventures.location_details')}

    +

    + {$t('adventures.display_name')}: + {reverseGeocodePlace.city + ? reverseGeocodePlace.city + ', ' + : ''}{reverseGeocodePlace.region}, {reverseGeocodePlace.country} +

    +

    + {$t('adventures.region')}: + {reverseGeocodePlace.region} + {reverseGeocodePlace.region_visited ? '✅' : '❌'} +

    + {#if reverseGeocodePlace.city} +

    + {$t('adventures.city')}: + {reverseGeocodePlace.city} + {reverseGeocodePlace.city_visited ? '✅' : '❌'} +

    + {/if} +
    + {#if !reverseGeocodePlace.region_visited || (!reverseGeocodePlace.city_visited && !willBeMarkedVisited)} + + {/if} + {#if (willBeMarkedVisited && !reverseGeocodePlace.region_visited && reverseGeocodePlace.region_id) || (!reverseGeocodePlace.city_visited && willBeMarkedVisited && reverseGeocodePlace.city_id)} + + {/if} + {/if} +
    +
    +
    diff --git a/frontend/src/lib/components/LodgingCard.svelte b/frontend/src/lib/components/LodgingCard.svelte new file mode 100644 index 0000000..c5d41ba --- /dev/null +++ b/frontend/src/lib/components/LodgingCard.svelte @@ -0,0 +1,174 @@ + + +{#if isWarningModalOpen} + (isWarningModalOpen = false)} + on:confirm={deleteTransportation} + /> +{/if} + +
    +
    + +
    +

    {lodging.name}

    +
    +
    + {$t(`lodging.${lodging.type}`)} +
    + +
    +
    + {#if unlinked} +
    {$t('adventures.out_of_range')}
    + {/if} + + +
    + {#if lodging.location} +
    + {$t('adventures.location')}: +

    {lodging.location}

    +
    + {/if} + + {#if lodging.check_in && lodging.check_out} +
    + {$t('adventures.dates')}: +

    + {new Date(lodging.check_in).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: 'numeric' + })} + - + {new Date(lodging.check_out).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: 'numeric' + })} +

    +
    + {/if} + {#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))} + {#if lodging.reservation_number} +
    + {$t('adventures.reservation_number')}: +

    {lodging.reservation_number}

    +
    + {/if} + {#if lodging.price} +
    + {$t('adventures.price')}: +

    {lodging.price}

    +
    + {/if} + {/if} +
    + + + {#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))} +
    + + +
    + {/if} +
    +
    diff --git a/frontend/src/lib/components/LodgingModal.svelte b/frontend/src/lib/components/LodgingModal.svelte new file mode 100644 index 0000000..d91155c --- /dev/null +++ b/frontend/src/lib/components/LodgingModal.svelte @@ -0,0 +1,428 @@ + + + + + + + diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte index 5e0d475..0e5e4f2 100644 --- a/frontend/src/lib/components/Navbar.svelte +++ b/frontend/src/lib/components/Navbar.svelte @@ -8,6 +8,11 @@ import Calendar from '~icons/mdi/calendar'; import AboutModal from './AboutModal.svelte'; import AccountMultiple from '~icons/mdi/account-multiple'; + import MapMarker from '~icons/mdi/map-marker'; + import FormatListBulletedSquare from '~icons/mdi/format-list-bulleted-square'; + import Earth from '~icons/mdi/earth'; + import Magnify from '~icons/mdi/magnify'; + import Map from '~icons/mdi/map'; import Avatar from './Avatar.svelte'; import { page } from '$app/stores'; import { t, locale, locales } from 'svelte-i18n'; @@ -17,7 +22,11 @@ // Event listener for focusing input function handleKeydown(event: KeyboardEvent) { - if (event.key === '/' && document.activeElement !== inputElement) { + // Ignore any keypresses in an input/textarea field, so we don't interfere with typing. + if ( + event.key === '/' && + !['INPUT', 'TEXTAREA'].includes((event.target as HTMLElement)?.tagName) + ) { event.preventDefault(); // Prevent browser's search shortcut if (inputElement) { inputElement.focus(); @@ -44,7 +53,8 @@ nl: 'Nederlands', sv: 'Svenska', zh: '中文', - pl: 'Polski' + pl: 'Polski', + ko: '한국어' }; let query: string = ''; @@ -110,6 +120,7 @@ > {#if data.user}
  • +
  • @@ -167,36 +178,52 @@ {/if}
  • - AdventureLog Map Logo + + AdventureLog Map Logo +
    + +
    + + +
    - -
    - - -

    @@ -464,7 +465,7 @@ bind:value={starting_airport} name="starting_airport" class="input input-bordered w-full" - placeholder="Enter starting airport code (e.g., JFK)" + placeholder={$t('transportation.starting_airport_desc')} />
    {/if} diff --git a/frontend/src/lib/components/UserCard.svelte b/frontend/src/lib/components/UserCard.svelte index aefa17e..3224241 100644 --- a/frontend/src/lib/components/UserCard.svelte +++ b/frontend/src/lib/components/UserCard.svelte @@ -50,7 +50,7 @@
    {#if !sharing} - {:else if shared_with && !shared_with.includes(user.uuid)} diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts index 17dcae4..df3e9e3 100644 --- a/frontend/src/lib/index.ts +++ b/frontend/src/lib/index.ts @@ -5,6 +5,7 @@ import type { Background, Checklist, Collection, + Lodging, Note, Transportation, User @@ -149,6 +150,50 @@ export function groupTransportationsByDate( return groupedTransportations; } +export function groupLodgingByDate( + transportations: Lodging[], + startDate: Date, + numberOfDays: number +): Record { + const groupedTransportations: Record = {}; + + // Initialize all days in the range + for (let i = 0; i < numberOfDays; i++) { + const currentDate = new Date(startDate); + currentDate.setUTCDate(startDate.getUTCDate() + i); + const dateString = currentDate.toISOString().split('T')[0]; + groupedTransportations[dateString] = []; + } + + transportations.forEach((transportation) => { + if (transportation.check_in) { + const transportationDate = new Date(transportation.check_in).toISOString().split('T')[0]; + if (transportation.check_out) { + const endDate = new Date(transportation.check_out).toISOString().split('T')[0]; + + // Loop through all days and include transportation if it falls within the range + for (let i = 0; i < numberOfDays; i++) { + const currentDate = new Date(startDate); + currentDate.setUTCDate(startDate.getUTCDate() + i); + const dateString = currentDate.toISOString().split('T')[0]; + + // Include the current day if it falls within the transportation date range + if (dateString >= transportationDate && dateString <= endDate) { + if (groupedTransportations[dateString]) { + groupedTransportations[dateString].push(transportation); + } + } + } + } else if (groupedTransportations[transportationDate]) { + // If there's no end date, add transportation to the start date only + groupedTransportations[transportationDate].push(transportation); + } + } + }); + + return groupedTransportations; +} + export function groupNotesByDate( notes: Note[], startDate: Date, @@ -464,3 +509,13 @@ export function osmTagToEmoji(tag: string) { return '📍'; // Default placeholder emoji for unknown tags } } + +export function debounce(func: Function, timeout: number) { + let timer: number | NodeJS.Timeout; + return (...args: any) => { + clearTimeout(timer); + timer = setTimeout(() => { + func(...args); + }, timeout); + }; +} diff --git a/frontend/src/lib/json/backgrounds.json b/frontend/src/lib/json/backgrounds.json index be43594..7b1b896 100644 --- a/frontend/src/lib/json/backgrounds.json +++ b/frontend/src/lib/json/backgrounds.json @@ -24,6 +24,11 @@ "url": "backgrounds/adventurelog_showcase_5.webp", "author": "Sean Morley", "location": "Hoboken, New Jersey, USA" + }, + { + "url": "backgrounds/adventurelog_showcase_6.webp", + "author": "Sean Morley", + "location": "Smugglers' Notch Resort, Vermont, USA" } ] } diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index c97c43d..15ada33 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -1,7 +1,6 @@ export type User = { pk: number; username: string; - email: string | null; first_name: string | null; last_name: string | null; date_joined: string | null; @@ -41,6 +40,7 @@ export type Adventure = { is_visited?: boolean; category: Category | null; attachments: Attachment[]; + user?: User | null; }; export type Country = { @@ -113,6 +113,7 @@ export type Collection = { end_date: string | null; transportations?: Transportation[]; notes?: Note[]; + lodging?: Lodging[]; checklists?: Checklist[]; is_archived?: boolean; shared_with: string[] | undefined; @@ -209,6 +210,7 @@ export type ReverseGeocode = { display_name: string; city: string; city_id: string; + location_name: string; }; export type Category = { @@ -262,3 +264,24 @@ export type Attachment = { user_id: string; name: string; }; + +export type Lodging = { + id: string; + user_id: string; + name: string; + type: string; + description: string | null; + rating: number | null; + link: string | null; + check_in: string | null; // ISO 8601 date string + check_out: string | null; // ISO 8601 date string + reservation_number: string | null; + price: number | null; + latitude: number | null; + longitude: number | null; + location: string | null; + is_public: boolean; + collection: string | null; + created_at: string; // ISO 8601 date string + updated_at: string; // ISO 8601 date string +}; diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 6115fe0..a5e57d1 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -1,11 +1,11 @@ { "about": { - "about": "Um", + "about": "Über", "close": "Schließen", "license": "Lizenziert unter der GPL-3.0-Lizenz.", "message": "Hergestellt mit ❤️ in den Vereinigten Staaten.", "nominatim_1": "Standortsuche und Geokodierung werden bereitgestellt von", - "nominatim_2": "Ihre Daten werden unter der ODbL-Lizenz lizenziert.", + "nominatim_2": "Deren Daten sind unter der ODbL-Lizenz lizenziert.", "oss_attributions": "Open-Source-Zuschreibungen", "other_attributions": "Weitere Hinweise finden Sie in der README-Datei.", "source_code": "Quellcode" @@ -55,12 +55,12 @@ "archived_collection_message": "Sammlung erfolgreich archiviert!", "archived_collections": "Archivierte Sammlungen", "ascending": "Aufsteigend", - "cancel": "Stornieren", + "cancel": "Abbrechen", "category_filter": "Kategoriefilter", - "clear": "Klar", + "clear": "zurücksetzen", "close_filters": "Filter schließen", "collection": "Sammlung", - "collection_adventures": "Schließen Sie Sammlungsabenteuer ein", + "collection_adventures": "Sammlungsabenteuer berücksichtigen", "count_txt": "Ergebnisse, die Ihrer Suche entsprechen", "date": "Datum", "dates": "Termine", @@ -85,13 +85,13 @@ "not_found": "Abenteuer nicht gefunden", "not_found_desc": "Das von Ihnen gesuchte Abenteuer konnte nicht gefunden werden. \nBitte probieren Sie ein anderes Abenteuer aus oder schauen Sie später noch einmal vorbei.", "open_filters": "Öffnen Sie Filter", - "order_by": "Bestellen nach", - "order_direction": "Bestellrichtung", + "order_by": "Sortieren nach", + "order_direction": "Sortierreihenfolge", "planned": "Geplant", "private": "Privat", "public": "Öffentlich", "rating": "Bewertung", - "share": "Aktie", + "share": "Teilen", "sort": "Sortieren", "sources": "Quellen", "start_before_end_error": "Das Startdatum muss vor dem Enddatum liegen", @@ -109,12 +109,12 @@ "add_an_activity": "Fügen Sie eine Aktivität hinzu", "add_notes": "Notizen hinzufügen", "adventure_create_error": "Das Abenteuer konnte nicht erstellt werden", - "adventure_created": "Abenteuer geschaffen", + "adventure_created": "Abenteuer erstellt", "adventure_update_error": "Das Abenteuer konnte nicht aktualisiert werden", "adventure_updated": "Abenteuer aktualisiert", "basic_information": "Grundlegende Informationen", "category": "Kategorie", - "clear_map": "Klare Karte", + "clear_map": "Karte leeren", "copy_link": "Link kopieren", "create_new": "Neu erstellen...", "date_constrain": "Auf Abholtermine beschränken", @@ -137,7 +137,7 @@ "no_start_date": "Bitte geben Sie ein Startdatum ein", "public_adventure": "Öffentliches Abenteuer", "remove": "Entfernen", - "save_next": "Speichern", + "save_next": "Speichern & weiter", "search_for_location": "Suchen Sie nach einem Ort", "search_results": "Suchergebnisse", "see_adventures": "Siehe Abenteuer", @@ -154,7 +154,7 @@ "all": "Alle", "error_updating_regions": "Fehler beim Aktualisieren der Regionen", "mark_region_as_visited": "Region {region}, {country} als besucht markieren?", - "mark_visited": "Mark besucht", + "mark_visited": "als besucht markieren", "my_adventures": "Meine Abenteuer", "no_adventures_found": "Keine Abenteuer gefunden", "no_collections_found": "Es wurden keine Sammlungen gefunden, zu denen dieses Abenteuer hinzugefügt werden kann.", @@ -164,7 +164,7 @@ "update_visited_regions": "Besuchte Regionen aktualisieren", "update_visited_regions_disclaimer": "Dies kann je nach Anzahl der Abenteuer, die Sie besucht haben, eine Weile dauern.", "visited_region_check": "Überprüfung der besuchten Region", - "visited_region_check_desc": "Wenn Sie diese Option auswählen, überprüft der Server alle von Ihnen besuchten Abenteuer und markiert die Regionen, in denen sie sich befinden, als im Rahmen von Weltreisen besucht.", + "visited_region_check_desc": "Wenn Sie diese Option auswählen, überprüft der Server alle von Ihnen besuchten Abenteuer und markiert die Regionen, in denen sie sich befinden, im Bereich Weltreisen als besucht.", "add_new": "Neu hinzufügen...", "checklist": "Checkliste", "checklists": "Checklisten", @@ -185,7 +185,7 @@ "visit_link": "Besuchen Sie den Link", "collection_archived": "Diese Sammlung wurde archiviert.", "day": "Tag", - "add_a_tag": "Fügen Sie ein Tag hinzu", + "add_a_tag": "Fügen Sie einen Tag hinzu", "tags": "Schlagworte", "set_to_pin": "Auf „Anpinnen“ setzen", "category_fetch_error": "Fehler beim Abrufen der Kategorien", @@ -196,7 +196,7 @@ "hide": "Verstecken", "show": "Zeigen", "download_calendar": "Kalender herunterladen", - "md_instructions": "Schreiben Sie hier Ihren Abschlag...", + "md_instructions": "Schreiben Sie hier Ihren Markdowntext...", "preview": "Vorschau", "checklist_delete_confirm": "Sind Sie sicher, dass Sie diese Checkliste löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.", "clear_location": "Standort löschen", @@ -207,14 +207,14 @@ "end": "Ende", "ending_airport": "Endflughafen", "flight_information": "Fluginformationen", - "from": "Aus", + "from": "Von", "no_location_found": "Kein Standort gefunden", "note_delete_confirm": "Sind Sie sicher, dass Sie diese Notiz löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.", "out_of_range": "Nicht im Datumsbereich der Reiseroute", "show_region_labels": "Regionsbeschriftungen anzeigen", "start": "Start", "starting_airport": "Startflughafen", - "to": "Zu", + "to": "Nach", "transportation_delete_confirm": "Sind Sie sicher, dass Sie diesen Transport löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.", "show_map": "Karte anzeigen", "will_be_marked": "wird als besucht markiert, sobald das Abenteuer gespeichert ist.", @@ -234,7 +234,19 @@ "images": "Bilder", "primary": "Primär", "upload": "Hochladen", - "view_attachment": "Anhang anzeigen" + "view_attachment": "Anhang anzeigen", + "of": "von", + "city": "Stadt", + "display_name": "Anzeigename", + "location_details": "Standortdetails", + "lodging": "Unterkunft", + "region": "Region", + "delete_lodging": "Unterkunft löschen", + "lodging_delete_confirm": "Sind Sie sicher, dass Sie diesen Standort löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.", + "lodging_information": "Unterkunftsinformationen", + "price": "Preis", + "reservation_number": "Reservierungsnummer", + "welcome_map_info": "Öffentliche Abenteuer auf diesem Server" }, "home": { "desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit", @@ -254,7 +266,7 @@ "about": "Über AdventureLog", "adventures": "Abenteuer", "collections": "Sammlungen", - "discord": "Zwietracht", + "discord": "Discord", "documentation": "Dokumentation", "greeting": "Hallo", "logout": "Abmelden", @@ -269,7 +281,7 @@ "aqua": "Aqua", "dark": "Dunkel", "forest": "Wald", - "light": "Licht", + "light": "Hell", "night": "Nacht", "aestheticDark": "Ästhetisches Dunkel", "aestheticLight": "Ästhetisches Licht", @@ -281,7 +293,8 @@ "tag": "Etikett", "language_selection": "Sprache", "support": "Unterstützung", - "calendar": "Kalender" + "calendar": "Kalender", + "admin_panel": "Admin -Panel" }, "auth": { "confirm_password": "Passwort bestätigen", @@ -302,7 +315,11 @@ "both_passwords_required": "Beide Passwörter sind erforderlich", "new_password": "Neues Passwort", "reset_failed": "Passwort konnte nicht zurückgesetzt werden", - "or_3rd_party": "Oder melden Sie sich bei einem Drittanbieter an" + "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" }, "users": { "no_users_found": "Keine Benutzer mit öffentlichen Profilen gefunden." @@ -317,7 +334,7 @@ "not_visited": "Nicht besucht", "num_countries": "Länder gefunden", "partially_visited": "Teilweise besucht", - "all_visited": "Sie haben alle Regionen in besucht", + "all_visited": "Sie haben alle Regionen besucht in", "cities": "Städte", "failed_to_mark_visit": "Der Besuch konnte nicht markiert werden", "failed_to_remove_visit": "Der Besuch von konnte nicht entfernt werden", @@ -352,7 +369,7 @@ "password_is_required": "Passwort ist erforderlich", "possible_reset": "Wenn die von Ihnen angegebene E-Mail-Adresse mit einem Konto verknüpft ist, erhalten Sie eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts!", "reset_password": "Passwort zurücksetzen", - "submit": "Einreichen", + "submit": "Absenden", "token_required": "Zum Zurücksetzen des Passworts sind Token und UID erforderlich.", "about_this_background": "Über diesen Hintergrund", "join_discord": "Treten Sie dem Discord bei", @@ -379,7 +396,7 @@ "generic_error": "Bei der Bearbeitung Ihrer Anfrage ist ein Fehler aufgetreten.", "invalid_code": "Ungültiger MFA-Code", "invalid_credentials": "Ungültiger Benutzername oder Passwort", - "make_primary": "Machen Sie primär", + "make_primary": "Zum bevorzugten machen", "mfa_disabled": "Multi-Faktor-Authentifizierung erfolgreich deaktiviert!", "mfa_enabled": "Multi-Faktor-Authentifizierung erfolgreich aktiviert!", "mfa_not_enabled": "MFA ist nicht aktiviert", @@ -481,7 +498,10 @@ "provide_start_date": "Bitte geben Sie ein Startdatum an", "start": "Start", "to_location": "Zum Standort", - "transport_type": "Transporttyp" + "transport_type": "Transporttyp", + "ending_airport_desc": "Geben Sie den Ending Airport Code ein (z. B. lax)", + "fetch_location_information": "Standortinformationen abrufen", + "starting_airport_desc": "Geben Sie den Start -Flughafencode ein (z. B. JFK)" }, "search": { "adventurelog_results": "AdventureLog-Ergebnisse", @@ -531,7 +551,7 @@ "countries_visited": "Besuchte Länder", "no_recent_adventures": "Keine aktuellen Abenteuer?", "recent_adventures": "Aktuelle Abenteuer", - "total_adventures": "Totale Abenteuer", + "total_adventures": "Abenteuer insgesamt", "total_visited_regions": "Insgesamt besuchte Regionen", "welcome_back": "Willkommen zurück", "total_visited_cities": "Insgesamt besuchte Städte" @@ -567,5 +587,35 @@ "phone": "Telefon", "recommendation": "Empfehlung", "website": "Webseite" + }, + "lodging": { + "apartment": "Wohnung", + "bnb": "Übernachtung mit Frühstück", + "cabin": "Kabine", + "campground": "Campingplatz", + "check_in": "Einchecken", + "check_out": "Kasse", + "date_and_time": "Datum", + "edit": "Bearbeiten", + "edit_lodging": "Unterkunft bearbeiten", + "error_editing_lodging": "Fehlerbearbeitung", + "hostel": "Herberge", + "hotel": "Hotel", + "house": "Haus", + "lodging_added": "Unterkunft erfolgreich hinzugefügt!", + "lodging_delete_error": "Fehler beim Löschen von Unterkünften", + "lodging_deleted": "Unterkunft erfolgreich gelöscht!", + "lodging_edit_success": "Unterbringung erfolgreich bearbeitet!", + "lodging_type": "Unterkunftstyp", + "motel": "Motel", + "new_lodging": "Neue Unterkunft", + "other": "Andere", + "provide_start_date": "Bitte geben Sie einen Startdatum an", + "reservation_number": "Reservierungsnummer", + "resort": "Resort", + "start": "Start", + "type": "Typ", + "villa": "Villa", + "current_timezone": "Aktuelle Zeitzone" } } diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 2b989e2..7b1eb46 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -21,6 +21,7 @@ "support": "Support", "calendar": "Calendar", "theme_selection": "Theme Selection", + "admin_panel": "Admin Panel", "themes": { "light": "Light", "dark": "Dark", @@ -67,9 +68,11 @@ "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.", + "lodging_delete_confirm": "Are you sure you want to delete this lodging location? This action cannot be undone.", "delete_checklist": "Delete Checklist", "delete_note": "Delete Note", "delete_transportation": "Delete Transportation", + "delete_lodging": "Delete Lodging", "open_details": "Open Details", "edit_adventure": "Edit Adventure", "remove_from_collection": "Remove from Collection", @@ -165,6 +168,7 @@ "delete_collection_success": "Collection deleted successfully!", "delete_collection_warning": "Are you sure you want to delete this collection? This will also delete all of the linked adventures. This action cannot be undone.", "cancel": "Cancel", + "of": "of", "delete_collection": "Delete Collection", "delete_adventure": "Delete Adventure", "adventure_delete_success": "Adventure deleted successfully!", @@ -190,6 +194,7 @@ "no_description_found": "No description found", "adventure_created": "Adventure created", "adventure_create_error": "Failed to create adventure", + "lodging": "Lodging", "create_adventure": "Create Adventure", "adventure_updated": "Adventure updated", "adventure_update_error": "Failed to update adventure", @@ -198,6 +203,7 @@ "new_adventure": "New Adventure", "basic_information": "Basic Information", "no_adventures_to_recommendations": "No adventures found. Add at leat one adventure 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!", "no_adventures_found": "No adventures found", "mark_region_as_visited": "Mark region {region}, {country} as visited?", @@ -248,6 +254,9 @@ "out_of_range": "Not in itinerary date range", "preview": "Preview", "finding_recommendations": "Discovering hidden gems for your next adventure", + "location_details": "Location Details", + "city": "City", + "region": "Region", "md_instructions": "Write your markdown here...", "days": "days", "attachment_upload_success": "Attachment uploaded successfully!", @@ -257,6 +266,7 @@ "attachment_update_success": "Attachment updated successfully!", "attachment_name": "Attachment Name", "gpx_tip": "Upload GPX files to attachments to view them on the map!", + "welcome_map_info": "Public adventures on this server", "attachment_update_error": "Error updating attachment", "activities": { "general": "General 🌍", @@ -281,7 +291,10 @@ "spiritual_journeys": "Spiritual Journeys 🧘‍♀️", "volunteer_work": "Volunteer Work 🤝", "other": "Other" - } + }, + "lodging_information": "Lodging Information", + "price": "Price", + "reservation_number": "Reservation Number" }, "worldtravel": { "country_list": "Country List", @@ -326,7 +339,11 @@ "new_password": "New Password (6+ characters)", "both_passwords_required": "Both passwords are required", "reset_failed": "Failed to reset password", - "or_3rd_party": "Or login with a third-party service" + "or_3rd_party": "Or login with a third-party service", + "no_public_adventures": "No public adventures found", + "no_public_collections": "No public collections found", + "user_adventures": "User Adventures", + "user_collections": "User Collections" }, "users": { "no_users_found": "No users found with public profiles." @@ -467,6 +484,9 @@ "flight_number": "Flight Number", "from_location": "From Location", "to_location": "To Location", + "fetch_location_information": "Fetch Location Information", + "starting_airport_desc": "Enter starting airport code (e.g., JFK)", + "ending_airport_desc": "Enter ending airport code (e.g., LAX)", "edit": "Edit", "modes": { "car": "Car", @@ -483,6 +503,36 @@ "start": "Start", "date_and_time": "Date & Time" }, + "lodging": { + "lodging_deleted": "Lodging deleted successfully!", + "lodging_delete_error": "Error deleting lodging", + "provide_start_date": "Please provide a start date", + "lodging_type": "Lodging Type", + "type": "Type", + "lodging_added": "Lodging added successfully!", + "error_editing_lodging": "Error editing lodging", + "new_lodging": "New Lodging", + "check_in": "Check In", + "check_out": "Check Out", + "edit": "Edit", + "lodging_edit_success": "Lodging edited successfully!", + "edit_lodging": "Edit Lodging", + "start": "Start", + "date_and_time": "Date & Time", + "hotel": "Hotel", + "hostel": "Hostel", + "resort": "Resort", + "bnb": "Bed and Breakfast", + "campground": "Campground", + "cabin": "Cabin", + "apartment": "Apartment", + "house": "House", + "villa": "Villa", + "motel": "Motel", + "other": "Other", + "reservation_number": "Reservation Number", + "current_timezone": "Current Timezone" + }, "search": { "adventurelog_results": "AdventureLog Results", "public_adventures": "Public Adventures", diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index 0f00856..e36d958 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -30,7 +30,8 @@ "tag": "Etiqueta", "language_selection": "Idioma", "support": "Apoyo", - "calendar": "Calendario" + "calendar": "Calendario", + "admin_panel": "Panel de administración" }, "about": { "about": "Acerca de", @@ -281,7 +282,19 @@ "primary": "Primario", "upload": "Subir", "view_attachment": "Ver archivo adjunto", - "attachment_name": "Nombre del archivo adjunto" + "attachment_name": "Nombre del archivo adjunto", + "of": "de", + "city": "Ciudad", + "delete_lodging": "Eliminar alojamiento", + "display_name": "Nombre para mostrar", + "location_details": "Detalles de la ubicación", + "lodging": "Alojamiento", + "lodging_delete_confirm": "¿Estás seguro de que quieres eliminar este lugar de alojamiento? \nEsta acción no se puede deshacer.", + "lodging_information": "Información de alojamiento", + "price": "Precio", + "region": "Región", + "reservation_number": "Número de reserva", + "welcome_map_info": "Aventuras públicas en este servidor" }, "worldtravel": { "all": "Todo", @@ -326,7 +339,11 @@ "both_passwords_required": "Se requieren ambas contraseñas", "new_password": "Nueva contraseña", "reset_failed": "No se pudo restablecer la contraseña", - "or_3rd_party": "O inicie sesión con un servicio de terceros" + "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" }, "users": { "no_users_found": "No se encontraron usuarios con perfiles públicos." @@ -481,7 +498,10 @@ "flight_number": "Número de vuelo", "from_location": "Desde la ubicación", "transportation_added": "¡Transporte agregado exitosamente!", - "transportation_delete_error": "Error al eliminar el transporte" + "transportation_delete_error": "Error al eliminar el transporte", + "ending_airport_desc": "Ingrese el código de aeropuerto final (por ejemplo, LAX)", + "fetch_location_information": "Información de ubicación para obtener", + "starting_airport_desc": "Ingrese el código de aeropuerto inicial (por ejemplo, JFK)" }, "search": { "adventurelog_results": "Resultados del registro de aventuras", @@ -567,5 +587,35 @@ "phone": "Teléfono", "recommendation": "Recomendación", "website": "Sitio web" + }, + "lodging": { + "apartment": "Departamento", + "bnb": "Cama y desayuno", + "cabin": "Cabina", + "campground": "Terreno de camping", + "check_in": "Registrarse", + "check_out": "Verificar", + "date_and_time": "Fecha", + "edit": "Editar", + "error_editing_lodging": "Error de edición de alojamiento", + "hostel": "Albergue", + "hotel": "Hotel", + "house": "Casa", + "lodging_added": "¡Alojamiento agregado con éxito!", + "lodging_delete_error": "Error de eliminación de alojamiento", + "lodging_deleted": "¡Alojamiento eliminado con éxito!", + "lodging_edit_success": "¡Alojamiento editado con éxito!", + "lodging_type": "Tipo de alojamiento", + "motel": "Motel", + "new_lodging": "Nuevo alojamiento", + "other": "Otro", + "provide_start_date": "Proporcione una fecha de inicio", + "reservation_number": "Número de reserva", + "resort": "Complejo", + "start": "Comenzar", + "type": "Tipo", + "villa": "Villa", + "edit_lodging": "Editar alojamiento", + "current_timezone": "Zona horaria" } } diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index e99f79d..79ddc6c 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -234,7 +234,19 @@ "images": "Images", "primary": "Primaire", "upload": "Télécharger", - "view_attachment": "Voir la pièce jointe" + "view_attachment": "Voir la pièce jointe", + "of": "de", + "city": "Ville", + "delete_lodging": "Supprimer l'hébergement", + "display_name": "Nom d'affichage", + "location_details": "Détails de l'emplacement", + "lodging": "Hébergement", + "lodging_delete_confirm": "Êtes-vous sûr de vouloir supprimer cet emplacement d'hébergement? \nCette action ne peut pas être annulée.", + "lodging_information": "Informations sur l'hébergement", + "price": "Prix", + "region": "Région", + "reservation_number": "Numéro de réservation", + "welcome_map_info": "Aventures publiques sur ce serveur" }, "home": { "desc_1": "Découvrez, planifiez et explorez en toute simplicité", @@ -281,7 +293,8 @@ "tag": "Étiqueter", "language_selection": "Langue", "support": "Soutien", - "calendar": "Calendrier" + "calendar": "Calendrier", + "admin_panel": "Panneau d'administration" }, "auth": { "confirm_password": "Confirmez le mot de passe", @@ -302,7 +315,11 @@ "both_passwords_required": "Les deux mots de passe sont requis", "new_password": "Nouveau mot de passe", "reset_failed": "Échec de la réinitialisation du mot de passe", - "or_3rd_party": "Ou connectez-vous avec un service tiers" + "or_3rd_party": "Ou connectez-vous avec un service tiers", + "no_public_adventures": "Aucune aventure publique trouvée", + "no_public_collections": "Aucune collection publique trouvée", + "user_adventures": "Aventures utilisateur", + "user_collections": "Collections d'utilisateurs" }, "users": { "no_users_found": "Aucun utilisateur trouvé avec des profils publics." @@ -481,7 +498,10 @@ "transportation_added": "Transport ajouté avec succès !", "transportation_delete_error": "Erreur lors de la suppression du transport", "transportation_deleted": "Transport supprimé avec succès !", - "transportation_edit_success": "Transport modifié avec succès !" + "transportation_edit_success": "Transport modifié avec succès !", + "ending_airport_desc": "Entrez la fin du code aéroportuaire (par exemple, laxiste)", + "fetch_location_information": "Récupérer les informations de localisation", + "starting_airport_desc": "Entrez le code aéroport de démarrage (par exemple, JFK)" }, "search": { "adventurelog_results": "Résultats du journal d'aventure", @@ -567,5 +587,35 @@ "phone": "Téléphone", "recommendation": "Recommandation", "website": "Site web" + }, + "lodging": { + "apartment": "Appartement", + "bnb": "Bed and petit-déjeuner", + "cabin": "Cabine", + "campground": "Camping", + "check_in": "Enregistrement", + "check_out": "Vérifier", + "date_and_time": "Date", + "edit": "Modifier", + "edit_lodging": "Modifier l'hébergement", + "error_editing_lodging": "Édition d'erreurs Hébergement", + "hostel": "Auberge", + "hotel": "Hôtel", + "house": "Maison", + "lodging_added": "L'hébergement a ajouté avec succès!", + "lodging_delete_error": "Erreur de suppression de l'hébergement", + "lodging_deleted": "L'hébergement est supprimé avec succès!", + "lodging_edit_success": "L'hébergement édité avec succès!", + "lodging_type": "Type d'hébergement", + "motel": "Motel", + "new_lodging": "Nouveau logement", + "other": "Autre", + "provide_start_date": "Veuillez fournir une date de début", + "reservation_number": "Numéro de réservation", + "resort": "Station balnéaire", + "start": "Commencer", + "type": "Taper", + "villa": "Villa", + "current_timezone": "Fuseau horaire actuel" } } diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json index 12877dc..3aadbd7 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -234,7 +234,19 @@ "images": "Immagini", "primary": "Primario", "upload": "Caricamento", - "view_attachment": "Visualizza allegato" + "view_attachment": "Visualizza allegato", + "of": "Di", + "city": "Città", + "delete_lodging": "Elimina alloggio", + "display_name": "Nome da visualizzare", + "location_details": "Dettagli della posizione", + "lodging": "Alloggio", + "lodging_delete_confirm": "Sei sicuro di voler eliminare questa posizione di alloggio? \nQuesta azione non può essere annullata.", + "lodging_information": "Informazioni di alloggio", + "price": "Prezzo", + "region": "Regione", + "welcome_map_info": "Avventure pubbliche su questo server", + "reservation_number": "Numero di prenotazione" }, "home": { "desc_1": "Scopri, pianifica ed esplora con facilità", @@ -281,7 +293,8 @@ "tag": "Etichetta", "language_selection": "Lingua", "support": "Supporto", - "calendar": "Calendario" + "calendar": "Calendario", + "admin_panel": "Pannello di amministrazione" }, "auth": { "confirm_password": "Conferma password", @@ -302,7 +315,11 @@ "both_passwords_required": "Sono necessarie entrambe le password", "new_password": "Nuova parola d'ordine", "reset_failed": "Impossibile reimpostare la password", - "or_3rd_party": "Oppure accedi con un servizio di terze parti" + "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" }, "users": { "no_users_found": "Nessun utente trovato con profili pubblici." @@ -481,7 +498,10 @@ "transportation_delete_error": "Errore durante l'eliminazione del trasporto", "transportation_deleted": "Trasporto eliminato con successo!", "transportation_edit_success": "Trasporti modificati con successo!", - "type": "Tipo" + "type": "Tipo", + "ending_airport_desc": "Immettere il codice aeroportuale finale (ad es. LAX)", + "fetch_location_information": "Informazioni sulla posizione di recupero", + "starting_airport_desc": "Immettere il codice dell'aeroporto di partenza (ad es. JFK)" }, "search": { "adventurelog_results": "Risultati di AdventureLog", @@ -567,5 +587,35 @@ "phone": "Telefono", "recommendation": "Raccomandazione", "website": "Sito web" + }, + "lodging": { + "apartment": "Appartamento", + "bnb": "Bed and Breakfast", + "cabin": "Cabina", + "campground": "Campeggio", + "check_in": "Check -in", + "check_out": "Guardare", + "date_and_time": "Data", + "edit": "Modificare", + "edit_lodging": "Modifica alloggio", + "error_editing_lodging": "Alloggio di modifica degli errori", + "hostel": "Ostello", + "hotel": "Hotel", + "house": "Casa", + "lodging_added": "Alloggio aggiunto con successo!", + "new_lodging": "Nuovo alloggio", + "other": "Altro", + "provide_start_date": "Si prega di fornire una data di inizio", + "reservation_number": "Numero di prenotazione", + "resort": "Ricorrere", + "start": "Inizio", + "type": "Tipo", + "villa": "Villa", + "lodging_delete_error": "Errore di eliminazione dell'alloggio", + "lodging_deleted": "Alloggio eliminato con successo!", + "lodging_edit_success": "Alloggio modificato con successo!", + "lodging_type": "Tipo di alloggio", + "motel": "Motel", + "current_timezone": "Fuso orario attuale" } } diff --git a/frontend/src/locales/ko.json b/frontend/src/locales/ko.json new file mode 100644 index 0000000..c1748d1 --- /dev/null +++ b/frontend/src/locales/ko.json @@ -0,0 +1,620 @@ +{ + "about": { + "about": "소개", + "close": "닫기", + "license": "GPL-3.0 라이선스 적용.", + "message": "미국에서 ❤️로 제작되었습니다.", + "nominatim_1": "위치 검색 및 지오코딩 제공", + "nominatim_2": "데이터는 ODbL 라이선스가 적용됩니다.", + "oss_attributions": "오픈 소스 속성", + "other_attributions": "추가 속성은 README 파일에서 찾을 수 있습니다.", + "source_code": "소스 코드" + }, + "adventures": { + "actions": "행동", + "activities": { + "activity": "활동 🏄", + "art_museums": "예술 & 박물관 🎨", + "attraction": "놀이동산 🎢", + "culture": "문화 🎭", + "dining": "식사 🍽️", + "event": "이벤트 🎉", + "festivals": "축제 🎪", + "fitness": "피트니스 🏋️", + "general": "일반 🌍", + "hiking": "하이킹 🥾", + "historical_sites": "사적지 🏛️", + "lodging": "숙박 🛌", + "music_concerts": "음악 🎶", + "nightlife": "야간문화 🌃", + "other": "기타", + "outdoor": "야외활동 🏞️", + "shopping": "쇼핑 🛍️", + "spiritual_journeys": "영적 여행 🧘‍♀️", + "transportation": "교통수단 🚗", + "volunteer_work": "자원 봉사 활동 🤝", + "water_sports": "수상 스포츠 🚤", + "wildlife": "야생 동물 🦒" + }, + "activity": "활동", + "activity_types": "활동 유형", + "add": "추가", + "add_a_tag": "태그를 추가하세요", + "add_an_activity": "활동을 추가하세요", + "add_new": "새로 추가...", + "add_notes": "메모 추가", + "add_to_collection": "컬렉션에 추가하세요", + "adventure": "모험", + "adventure_calendar": "모험 달력", + "adventure_create_error": "모험을 만들지 못했습니다", + "adventure_created": "모험 생성됨", + "adventure_delete_confirm": "이 모험을 삭제 하시겠습니까? 이 행동은 취소 할 수 없습니다.", + "adventure_delete_success": "모험이 성공적으로 삭제되었습니다!", + "adventure_details": "모험 정보", + "adventure_not_found": "표시할 모험이 없습니다. 오른쪽 하단의 플러스 버튼을 사용하여 추가하거나 필터를 변경하세요!", + "adventure_type": "모험 유형", + "adventure_update_error": "모험을 업데이트하지 못했습니다", + "adventure_updated": "모험 업데이트됨", + "all": "모두", + "archive": "보관", + "archived": "보관됨", + "archived_collection_message": "컬렉션이 성공적으로 보관되었습니다!", + "archived_collections": "보관된 컬렉션", + "ascending": "오름차순", + "attachment": "첨부 파일", + "attachment_delete_success": "첨부 파일이 성공적으로 삭제되었습니다!", + "attachment_name": "첨부 파일 이름", + "attachment_update_error": "첨부 파일 업데이트 실패", + "attachment_update_success": "첨부 파일이 성공적으로 업데이트되었습니다!", + "attachment_upload_error": "첨부 파일 업로드 실패", + "attachment_upload_success": "첨부 파일이 성공적으로 업로드되었습니다!", + "attachments": "첨부 파일", + "basic_information": "기본 정보", + "cancel": "취소", + "category": "카테고리", + "category_fetch_error": "카테고리 불러오기 실패", + "category_filter": "카테고리 필터", + "checklist": "체크리스트", + "checklist_delete_confirm": "이 체크리스트를 삭제 하시겠습니까? 이 행동은 취소할 수 없습니다.", + "clear_location": "장소 초기화", + "clear_map": "지도 초기화", + "close_filters": "필터 제거", + "collection": "컬렉션", + "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": "컬렉션 일자로 제한", + "date_information": "일자 정보", + "dates": "일자", + "day": "일", + "days": "일", + "delete": "삭제", + "delete_adventure": "모험 삭제", + "delete_checklist": "체크리스트 삭제", + "delete_collection": "컬렉션 삭제", + "delete_collection_success": "컬렉션이 성공적으로 삭제되었습니다!", + "delete_collection_warning": "이 컬렉션을 삭제 하시겠습니까? 링크된 모든 모험을 삭제합니다. 이 행동은 취소할 수 없습니다.", + "delete_note": "노트 삭제", + "delete_transportation": "교통수단 삭제", + "descending": "내림차순", + "description": "설명", + "download_calendar": "캘린더 다운로드", + "duration": "기간", + "edit_adventure": "모험 편집", + "edit_collection": "컬렉션 편집", + "emoji_picker": "이모티콘 선택", + "end": "끝", + "end_date": "종료일", + "ending_airport": "도착 공항", + "error_updating_regions": "지역 업데이트 오류", + "fetch_image": "이미지 가져오기", + "filter": "필터", + "finding_recommendations": "다음 모험을 위해 숨겨진 보물을 찾아보세요", + "flight_information": "항공편 정보", + "from": "출발", + "generate_desc": "설명을 적으세요", + "gpx_tip": "GPX 파일을 첨부 파일에 업로드하면 지도에서 볼 수 있습니다!", + "hide": "숨기기", + "homepage": "홈페이지", + "image": "이미지", + "image_fetch_failed": "이미지를 가져오지 못했습니다", + "image_removed_error": "이미지 삭제 오류", + "image_removed_success": "이미지가 성공적으로 삭제되었습니다!", + "image_upload_error": "이미지 업로드 오류", + "image_upload_success": "이미지가 성공적으로 업로드되었습니다!", + "images": "이미지", + "itineary_by_date": "날짜 순 일정표", + "keep_exploring": "계속 탐험하세요!", + "latitude": "위도", + "link": "링크", + "link_new": "새로운 링크...", + "linked_adventures": "링크된 모험", + "links": "링크", + "location": "위치", + "location_information": "위치 정보", + "longitude": "경도", + "mark_region_as_visited": "{country} {Region} 지역을 방문함으로 표시할까요?", + "mark_visited": "방문함으로 표시", + "md_instructions": "여기에 마크다운으로 작성할 수 있습니다...", + "my_adventures": "내 모험", + "my_collections": "내 컬렉션", + "my_images": "내 이미지", + "my_visits": "내 방문", + "name": "이름", + "new_adventure": "새로운 모험", + "no_adventures_found": "모험이 없습니다", + "no_adventures_to_recommendations": "모험이 없습니다. 장소를 추천받으려면 최소 하나 이상의 모험을 등록해야 합니다.", + "no_collections_found": "이 모험을 추가할 수 있는 컬렉션이 없습니다.", + "no_description_found": "설명이 없습니다", + "no_end_date": "종료 날짜를 입력하세요", + "no_image_found": "이미지가 없습니다", + "no_image_url": "해당 URL에 이미지가 없습니다.", + "no_images": "이미지가 없습니다", + "no_linkable_adventures": "이 컬렉션에 연결할 수 있는 모험이 없습니다.", + "no_location": "위치를 입력하세요", + "no_location_found": "위치가 없습니다", + "no_results": "결과가 없습니다", + "no_start_date": "시작 날짜를 입력하십시오", + "not_found": "모험이 없습니다", + "not_found_desc": "당신이 찾고 있던 모험을 찾을 수 없었습니다. 다른 모험을 찾아보거나 나중에 다시 해 보세요.", + "not_visited": "방문하지 않음", + "note": "노트", + "note_delete_confirm": "이 노트를 삭제 하시겠습니까? 이 행동은 취소 할 수 없습니다.", + "notes": "노트", + "nothing_planned": "이 날에 계획된 것이 없습니다. 즐거운 여행 되세요!", + "open_details": "상세 내용 보기", + "open_filters": "필터 열기", + "order_by": "정렬 기준", + "order_direction": "정렬 순서", + "out_of_range": "여정 날짜 범위 내에 없습니다", + "planned": "계획", + "preview": "미리보기", + "primary": "기본", + "private": "비공개", + "public": "공개", + "public_adventure": "공개 모험", + "rating": "평가", + "regions_updated": "지역이 업데이트되었습니다", + "remove": "제거", + "remove_from_collection": "컬렉션에서 제거", + "save_next": "저장 & 다음", + "search_for_location": "위치 검색", + "search_results": "검색 결과", + "see_adventures": "모험 보기", + "select_adventure_category": "모험 카테고리 선택", + "set_to_pin": "고정하기", + "share": "공유", + "share_adventure": "이 모험을 공유하세요!", + "show": "보기", + "show_map": "지도 보기", + "show_region_labels": "지역 레이블 표시", + "sort": "정렬", + "sources": "출처", + "start": "시작", + "start_before_end_error": "시작일은 종료일 이전이어야 합니다", + "start_date": "시작일", + "starting_airport": "출발 공항", + "tags": "태그", + "to": "도착", + "transportation": "교통수단", + "transportation_delete_confirm": "이 교통수단을 삭제 하시겠습니까? 이 행동은 취소 할 수 없습니다.", + "transportations": "교통수단", + "unarchive": "보관 취소", + "unarchived_collection_message": "컬렉션이 성공적으로 보관 취소되었습니다!", + "update_visited_regions": "방문 지역 업데이트", + "update_visited_regions_disclaimer": "방문한 모험의 수에 따라 시간이 걸릴 수 있습니다.", + "updated": "업데이트됨", + "upload": "업로드", + "upload_image": "이미지 업로드", + "upload_images_here": "여기에 이미지를 업로드하세요", + "url": "URL", + "view_attachment": "첨부 파일 보기", + "visit": "방문", + "visit_link": "링크 방문", + "visited": "방문함", + "visited_region_check": "방문 지역 확인", + "visited_region_check_desc": "이것을 선택하면 서버는 방문한 모든 모험을 확인하고 그 모험의 지역을 표시하여 세계 여행에서 방문 여부를 표시합니다.", + "visits": "방문", + "warning": "경고", + "wiki_desc": "모험의 이름과 일치하는 글을 위키백과에서 가져옵니다.", + "wiki_image_error": "위키백과 이미지 가져오기 오류", + "wikipedia": "위키백과", + "will_be_marked": "모험이 저장되면 방문했다고 표시합니다.", + "checklists": "체크리스트", + "cities_updated": "도시 업데이트됨", + "clear": "초기화", + "city": "도시", + "delete_lodging": "숙박을 삭제하십시오", + "display_name": "표시 이름", + "location_details": "위치 세부 사항", + "lodging": "하숙", + "lodging_delete_confirm": "이 숙소 위치를 삭제 하시겠습니까? \n이 조치는 취소 할 수 없습니다.", + "lodging_information": "숙박 정보", + "of": "~의", + "price": "가격", + "region": "지역", + "reservation_number": "예약 번호", + "welcome_map_info": "이 서버의 공개 모험" + }, + "auth": { + "both_passwords_required": "두 암호 모두 필요합니다", + "confirm_password": "비밀번호 확인", + "email": "이메일", + "email_required": "이메일이 필요합니다", + "first_name": "이름", + "forgot_password": "비밀번호를 잊으셨나요?", + "last_name": "성", + "login": "로그인", + "login_error": "제공한 자격 증명으로 로그인 할 수 없습니다.", + "new_password": "새 비밀번호 (6자 이상)", + "or_3rd_party": "또는 서드 파티 서비스로 로그인하세요", + "password": "비밀번호", + "profile_picture": "프로필 사진", + "public_profile": "공개 프로필", + "public_tooltip": "공개 프로필을 사용하면 당신에게 다른 사용자가 컬렉션을 공유할 수 있으며, 사용자 페이지에서 프로필이 노출됩니다.", + "registration_disabled": "현재 등록할 수 없습니다.", + "reset_failed": "비밀번호 재설정 실패", + "signup": "가입", + "username": "사용자 이름", + "no_public_adventures": "공개 모험이 발견되지 않았습니다", + "no_public_collections": "공개 컬렉션이 발견되지 않았습니다", + "user_adventures": "사용자 모험", + "user_collections": "사용자 수집" + }, + "categories": { + "category_name": "카테고리 이름", + "edit_category": "카테고리 편집", + "icon": "아이콘", + "manage_categories": "카테고리 관리", + "no_categories_found": "카테고리가 없습니다.", + "select_category": "카테고리 선택", + "update_after_refresh": "페이지를 새로고침해야 모험 카드가 업데이트됩니다." + }, + "checklist": { + "add_item": "항목 추가", + "checklist_delete_error": "체크리스트 삭제 오류", + "checklist_deleted": "체크리스트가 성공적으로 삭제되었습니다!", + "checklist_editor": "체크리스트 편집기", + "checklist_public": "이 체크리스트는 공개 컬렉션에 있기 때문에 공개되었습니다.", + "checklist_viewer": "체크리스트 뷰어", + "editing_checklist": "체크리스트 편집", + "failed_to_save": "체크리스트 저장 실패", + "item": "항목", + "item_already_exists": "항목이 이미 존재합니다", + "item_cannot_be_empty": "항목은 비어있을 수 없습니다", + "items": "항목", + "new_checklist": "새 체크리스트", + "new_item": "새 항목", + "save": "저장" + }, + "collection": { + "collection_created": "컬렉션이 성공적으로 생성되었습니다!", + "collection_edit_success": "컬렉션이 성공적으로 편집되었습니다!", + "create": "생성", + "edit_collection": "컬렉션 편집", + "error_creating_collection": "컬렉션 생성 오류", + "error_editing_collection": "컬렉션 편집 오류", + "new_collection": "새로운 컬렉션", + "public_collection": "공개 컬렉션" + }, + "dashboard": { + "add_some": "다음 모험을 계획해 보는게 어떨까요? 아래 버튼을 클릭하여 새로운 모험을 추가할 수 있습니다.", + "countries_visited": "방문한 국가", + "no_recent_adventures": "최근 모험이 없나요?", + "recent_adventures": "최근 모험", + "total_adventures": "모든 모험", + "total_visited_cities": "방문한 모든 도시", + "total_visited_regions": "방문한 모든 지역", + "welcome_back": "다시 오신 것을 환영합니다" + }, + "home": { + "desc_1": "쉽고 편리하게 발견하고, 계획하고, 탐험하세요", + "desc_2": "AdventureLog는 여러분의 여정을 더 편리하게 만들어 드립니다. 추억에 남을 다음 모험을 계획하고, 준비하고, 안내하는 데 필요한 모든 도구와 리소스를 제공할 수 있도록 설계되었습니다.", + "feature_1": "여행 로그", + "feature_1_desc": "나만의 여행 기록을 만들어 기록하고 친구나 가족들에게 경험을 공유하세요.", + "feature_2": "일정 계획", + "feature_2_desc": "손쉽게 맞춤형 일정을 만들고 하루 단위 일정표를 받아 보세요.", + "feature_3": "여행 지도", + "feature_3_desc": "세계 각지의 방문지를 인터랙티브 지도로 확인하고 새로운 여행지를 찾아 보세요.", + "go_to": "Adventurelog로 이동", + "hero_1": "세상에서 가장 짜릿한 모험을 발견하세요", + "hero_2": "Adventurelog로 다음 모험을 발견하고 계획해 보세요. 환상적인 여행지를 탐색하고, 나만의 일정을 만드세요. 어디서든 접속할 수 있습니다.", + "key_features": "주요 기능" + }, + "immich": { + "api_key": "Immich API 키", + "api_note": "참고 : 이것은 Immich API 서버의 URL이어야 합니다. 사용자 정의 구성을 하지 않았다면 끝이 /api 입니다.", + "disable": "사용 불가", + "documentation": "Immich 통합 문서", + "enable_immich": "Immich 활성화", + "imageid_required": "이미지 ID가 필요합니다", + "immich": "Immich", + "immich_desc": "Immich 계정을 Adventurelog와 통합하면 사진 라이브러리를 검색하고 모험 사진을 가져올 수 있습니다.", + "immich_disabled": "Immich 통합이 성공적으로 비활성화되었습니다!", + "immich_enabled": "Immich 통합이 성공적으로 활성화되었습니다!", + "immich_error": "Immich 통합 업데이트 오류", + "immich_integration": "Immich 통합", + "immich_updated": "Immich 설정이 성공적으로 업데이트되었습니다!", + "integration_enabled": "통합 활성화", + "integration_fetch_error": "Immich 통합에서 데이터 가져오기 오류", + "integration_missing": "백엔드에서 Immich 통합을 찾을 수 없습니다", + "load_more": "더 가져오기", + "localhost_note": "참고 : docker 네트워크를 적절하게 설정하지 않으면 localhost는 올바르게 작동하지 않을 수 있습니다. IP 주소나 서버의 도메인 주소를 사용하는 것이 좋습니다.", + "no_items_found": "항목 없음", + "query_required": "쿼리가 필요합니다", + "server_down": "현재 Immich 서버가 다운되었거나 도달할 수 없습니다", + "server_url": "Immich 서버 URL", + "update_integration": "통합 업데이트" + }, + "map": { + "add_adventure": "새로운 모험 추가", + "add_adventure_at_marker": "마커에 새로운 모험 추가", + "adventure_map": "모험 지도", + "clear_marker": "마커 초기화", + "map_options": "지도 옵션", + "show_visited_regions": "방문한 지역 보기", + "view_details": "상세 보기" + }, + "navbar": { + "about": "Adventurelog 소개", + "adventures": "모험", + "calendar": "달력", + "collections": "컬렉션", + "discord": "디스코드", + "documentation": "문서", + "greeting": "안녕하세요", + "language_selection": "언어", + "logout": "로그아웃", + "map": "지도", + "my_adventures": "내 모험", + "my_tags": "내 태그", + "profile": "프로필", + "search": "검색", + "settings": "설정", + "shared_with_me": "나와 공유함", + "support": "지원", + "tag": "태그", + "theme_selection": "테마 선택", + "themes": { + "aestheticDark": "Aesthetic Dark", + "aestheticLight": "Aesthetic Light", + "aqua": "Aqua", + "dark": "Dark", + "forest": "Forest", + "light": "Light", + "night": "Night", + "northernLights": "Northern Lights" + }, + "users": "사용자", + "worldtravel": "세계 여행", + "admin_panel": "관리자 패널" + }, + "notes": { + "add_a_link": "링크 추가", + "content": "콘텐츠", + "editing_note": "노트 편집", + "failed_to_save": "메모 저장 실패", + "invalid_url": "잘못된 URL", + "note_delete_error": "노트 삭제 오류", + "note_deleted": "노트가 성공적으로 삭제되었습니다!", + "note_editor": "노트 편집기", + "note_public": "이 노트는 공개 컬렉션에 있기 때문에 공개되었습니다.", + "note_viewer": "노트 뷰어", + "open": "열기", + "save": "저장" + }, + "profile": { + "member_since": "가입 시점", + "user_stats": "사용자 통계", + "visited_cities": "방문한 도시", + "visited_countries": "방문한 국가", + "visited_regions": "방문한 지역" + }, + "recomendations": { + "address": "주소", + "contact": "연락처", + "phone": "핸드폰", + "recommendation": "추천", + "website": "웹사이트" + }, + "search": { + "adventurelog_results": "Adventurelog 결과", + "online_results": "온라인 결과", + "public_adventures": "공개 모험" + }, + "settings": { + "about_this_background": "이 배경에 대해", + "account_settings": "사용자 계정 설정", + "add_email": "이메일 추가", + "add_email_blocked": "2단계 인증으로 보호된 계정에 이메일 주소를 추가할 수 없습니다.", + "administration_settings": "관리자 설정", + "authenticator_code": "인증 코드", + "change_password": "비밀번호 변경", + "change_password_error": "비밀번호를 변경할 수 없습니다. 현재 비밀번호 또는 새 비밀번호가 유효하지 않습니다.", + "confirm_new_password": "새 비밀번호 확인", + "copy": "복사", + "csrf_failed": "CSRF 토큰 가져오기 실패", + "current_email": "현재 이메일", + "current_password": "현재 비밀번호", + "disable_mfa": "MFA 비활성화", + "duplicate_email": "이 이메일 주소는 이미 사용 중입니다.", + "email_added": "이메일이 성공적으로 추가되었습니다!", + "email_added_error": "이메일 추가 오류", + "email_change": "이메일 변경", + "email_removed": "이메일이 성공적으로 제거되었습니다!", + "email_removed_error": "이메일 제거 오류", + "email_verified": "이메일을 성공적으로 인증했습니다!", + "email_verified_erorr_desc": "이메일이 인증되지 않았습니다. 다시 시도하세요.", + "email_verified_error": "이메일 인증 오류", + "email_verified_success": "이메일이 인증되었습니다. 이제 로그인 할 수 있습니다.", + "enable_mfa": "MFA 활성화", + "error_change_password": "비밀번호 변경 오류. 현재 비밀번호를 확인하고 다시 시도하세요.", + "generic_error": "요청 처리 중 오류가 발생했습니다.", + "invalid_code": "유효하지 않은 MFA 코드", + "invalid_credentials": "유효하지 않은 사용자 이름 또는 비밀번호", + "invalid_token": "유효하지 않거나 만료된 토큰입니다.", + "join_discord": "디스코드 참여", + "join_discord_desc": "#travel-share 채널에 자신의 사진을 공유하세요.", + "launch_account_connections": "계정 연결 시작", + "launch_administration_panel": "관리자 패널 열기", + "login_redir": "로그인 페이지로 리디렉션됩니다.", + "make_primary": "기본 이메일로 설정", + "mfa_disabled": "다중 인증이 성공적으로 비활성화되었습니다!", + "mfa_enabled": "다중 인증이 성공적으로 활성화되었습니다!", + "mfa_not_enabled": "MFA가 활성화되지 않았습니다", + "mfa_page_title": "다중 인증", + "mfa_required": "다중 인증이 필요합니다", + "missing_email": "이메일 주소를 입력하세요", + "new_email": "새 이메일", + "new_password": "새 비밀번호", + "no_emai_set": "이메일 세트가 없습니다", + "no_email_set": "이메일 세트가 없습니다", + "no_verified_email_warning": "2단계 인증을 활성화하려면 인증된 이메일 주소가 필요합니다.", + "not_verified": "인증되지 않음", + "password_change": "비밀번호 변경", + "password_change_lopout_warning": "비밀번호를 변경한 후 로그아웃됩니다.", + "password_does_not_match": "암호가 일치하지 않습니다", + "password_is_required": "비밀번호가 필요합니다", + "password_too_short": "비밀번호는 6 자 이상이어야 합니다", + "photo_by": "사진 제공", + "possible_reset": "입력한 이메일 주소가 계정과 일치하면, 비밀번호를 재설정하기 위한 이메일을 받게 됩니다!", + "primary": "기본", + "recovery_codes": "복구 코드", + "recovery_codes_desc": "이것은 복구 코드입니다. 안전하게 보관하세요. 재확인이 불가능합니다.", + "required": "필수 필드입니다", + "reset_password": "비밀번호 재설정", + "reset_session_error": "로그아웃 후 재접속하여 세션을 초기화하고 다시 시도하세요.", + "settings_page": "설정 페이지", + "social_auth_desc": "소셜 및 OIDC 인증 제공자를 활성화 또는 비활성화합니다. 활성화하면 Authentik 같은 자체 호스팅 인증 제공자나 Github 같은 서드 파티 인증 제공자로 로그인할 수 있습니다.", + "social_auth_desc_2": "관련 설정은 Adventurelog 서버에서 관리되며 관리자가 직접 활성화해야 합니다.", + "social_oidc_auth": "소셜 및 OIDC 인증", + "submit": "제출", + "token_required": "비밀번호를 재설정하려면 토큰 및 UID가 필요합니다.", + "update": "업데이트", + "update_error": "설정 업데이트 오류", + "update_success": "설정이 성공적으로 업데이트되었습니다!", + "username_taken": "이 사용자 이름은 이미 사용 중입니다.", + "verified": "인증됨", + "verify": "인증", + "verify_email_error": "이메일 인증 오류. 몇 분 후 다시 시도하세요.", + "verify_email_success": "인증 이메일이 성공적으로 발송되었습니다!", + "documentation_link": "문서화 링크", + "email_set_primary": "기본 이메일 세트로 설정했습니다!", + "email_set_primary_error": "기본 이메일 설정 오류", + "email_taken": "이 이메일 주소는 이미 사용 중입니다." + }, + "share": { + "go_to_settings": "설정으로 이동", + "no_shared_found": "공유받은 컬렉션이 없습니다.", + "no_users_shared": "공유한 사용자가 없습니다", + "not_shared_with": "공유하지 않음", + "set_public": "다른 사람에게 공유하려면, 프로필을 공개해야 합니다.", + "share_desc": "이 컬렉션을 다른 사용자와 공유하세요.", + "shared": "공유됨", + "shared_with": "공유한 사용자", + "unshared": "공유되지 않음", + "with": "함께 하는 사용자" + }, + "transportation": { + "date_and_time": "일시", + "date_time": "시작 일시", + "edit": "편집", + "edit_transportation": "교통수단 편집", + "end_date_time": "종료 일시", + "error_editing_transportation": "교통수단 편집 오류", + "flight_number": "항공편 번호", + "from_location": "출발지", + "modes": { + "bike": "자전거", + "boat": "보트", + "bus": "버스", + "car": "자동차", + "other": "기타", + "plane": "비행기", + "train": "기차", + "walking": "도보" + }, + "new_transportation": "새 교통수단", + "provide_start_date": "출발 일자를 입력하세요", + "start": "시작", + "to_location": "도착지", + "transport_type": "교통수단 유형", + "transportation_added": "교통수단이 성공적으로 추가되었습니다!", + "transportation_delete_error": "교통수단 삭제 오류", + "transportation_deleted": "교통수단이 성공적으로 삭제되었습니다!", + "transportation_edit_success": "교통수단이 성공적으로 편집되었습니다!", + "type": "유형", + "ending_airport_desc": "엔드 공항 코드 입력 (예 : LAX)", + "fetch_location_information": "위치 정보를 가져 오십시오", + "starting_airport_desc": "공항 시작 코드 입력 (예 : JFK)" + }, + "users": { + "no_users_found": "공개 프로필인 사용자가 없습니다." + }, + "worldtravel": { + "all": "모두", + "all_subregions": "모든 하위 지역", + "all_visited": "당신은 모든 지역을 방문했습니다", + "cities": "도시", + "clear_search": "검색 초기화", + "completely_visited": "모두 방문함", + "country_list": "국가 목록", + "failed_to_mark_visit": "방문함으로 표시 중 오류", + "failed_to_remove_visit": "방문함으로 표시 제거 중 오류", + "marked_visited": "방문으로 표시", + "no_cities_found": "도시가 없습니다", + "no_countries_found": "국가가 없습니다", + "not_visited": "방문하지 않음", + "num_countries": "개 국가 검색", + "partially_visited": "일부 방문함", + "region_failed_visited": "지역을 방문함으로 표시 중 실패", + "region_stats": "지역 통계", + "regions_in": "소속 지역", + "removed": "제거됨", + "view_cities": "도시 보기", + "visit_remove_failed": "방문 표시 제거 실패", + "visit_to": "방문함" + }, + "lodging": { + "apartment": "아파트", + "bnb": "숙박 및 아침 식사", + "cabin": "선실", + "campground": "캠프장", + "check_in": "체크인", + "current_timezone": "현재 시간대", + "date_and_time": "날짜", + "edit": "편집하다", + "edit_lodging": "숙박 편집", + "error_editing_lodging": "오류 편집 숙소", + "hostel": "숙박소", + "hotel": "호텔", + "house": "집", + "lodging_added": "숙박이 성공적으로 추가되었습니다!", + "lodging_delete_error": "오류 삭제 숙박", + "lodging_deleted": "숙박 시설이 성공적으로 삭제되었습니다!", + "lodging_edit_success": "숙박이 성공적으로 편집되었습니다!", + "lodging_type": "숙박 유형", + "motel": "모텔", + "new_lodging": "새로운 숙박", + "other": "다른", + "provide_start_date": "시작 날짜를 제공하십시오", + "reservation_number": "예약 번호", + "resort": "의지", + "start": "시작", + "type": "유형", + "villa": "별장", + "check_out": "체크 아웃" + } +} diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json index cdb1c5d..a74dfd7 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -51,9 +51,9 @@ "close_filters": "Sluit filters", "collection": "Collectie", "collection_adventures": "Inclusief collectie-avonturen", - "collection_link_error": "Fout bij het koppelen van avontuur aan collectie", + "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 avontuur uit 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 nieuwe...", @@ -94,9 +94,9 @@ "dates": "Datums", "delete_adventure": "Avontuur verwijderen", "duration": "Duur", - "image_removed_error": "Fout bij verwijderen van afbeelding", + "image_removed_error": "Fout bij het verwijderen van de afbeelding", "image_removed_success": "Afbeelding succesvol verwijderd!", - "image_upload_error": "Fout bij het uploaden van afbeelding", + "image_upload_error": "Fout bij het uploaden van de afbeelding", "image_upload_success": "Afbeelding succesvol geüpload!", "no_image_url": "Er is geen afbeelding gevonden op die URL.", "planned": "Gepland", @@ -117,7 +117,7 @@ "category": "Categorie", "clear_map": "Kaart leegmaken", "copy_link": "Kopieer link", - "date_constrain": "Beperk u tot ophaaldata", + "date_constrain": "Beperk tot de datums van de collectie", "description": "Beschrijving", "end_date": "Einddatum", "fetch_image": "Afbeelding ophalen", @@ -126,7 +126,7 @@ "image_fetch_failed": "Kan afbeelding niet ophalen", "link": "Link", "location": "Locatie", - "location_information": "Locatie-informatie", + "location_information": "Informatie over de locatie", "my_images": "Mijn afbeeldingen", "my_visits": "Mijn bezoeken", "new_adventure": "Nieuw avontuur", @@ -152,8 +152,8 @@ "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", - "error_updating_regions": "Fout bij wijzigen van regio's", - "mark_visited": "Markeer bezocht", + "error_updating_regions": "Fout bij het wijzigen van regio's", + "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.", @@ -162,7 +162,7 @@ "regions_updated": "regio's bijgewerkt", "update_visited_regions": "Werk bezochte regio's bij", "update_visited_regions_disclaimer": "Dit kan even duren, afhankelijk van het aantal avondturen dat je hebt beleefd.", - "visited_region_check": "Check bezochte regio's", + "visited_region_check": "Controleer bezochte regio's", "visited_region_check_desc": "Door dit te selecteren, controleert de server alle avonturen die je beleefde en markeert hun regio's als bezocht in de wereldreizen.", "add_new": "Nieuw toevoegen...", "checklist": "Controlelijst", @@ -194,9 +194,9 @@ "adventure_calendar": "Avonturenkalender", "emoji_picker": "Emoji-kiezer", "hide": "Verbergen", - "show": "Show", + "show": "Toon", "download_calendar": "Agenda downloaden", - "md_instructions": "Schrijf hier uw korting...", + "md_instructions": "Schrijf hier in markdown...", "preview": "Voorbeeld", "checklist_delete_confirm": "Weet u zeker dat u deze checklist wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.", "clear_location": "Locatie wissen", @@ -212,10 +212,10 @@ "out_of_range": "Niet binnen het datumbereik van het reisplan", "show_region_labels": "Toon regiolabels", "start": "Begin", - "starting_airport": "Startende luchthaven", + "starting_airport": "Luchthaven van vertrek", "to": "Naar", "transportation_delete_confirm": "Weet u zeker dat u dit transport wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.", - "ending_airport": "Einde luchthaven", + "ending_airport": "Luchthaven van aankomst", "show_map": "Toon kaart", "will_be_marked": "wordt gemarkeerd als bezocht zodra het avontuur is opgeslagen.", "cities_updated": "steden bijgewerkt", @@ -225,16 +225,28 @@ "attachment": "Bijlage", "attachment_delete_success": "Bijlage succesvol verwijderd!", "attachment_name": "Naam van bijlage", - "attachment_update_error": "Fout bij bijwerken van bijlage", + "attachment_update_error": "Fout bij het bijwerken van de bijlage", "attachment_update_success": "Bijlage succesvol bijgewerkt!", - "attachment_upload_error": "Fout bij het uploaden van bijlage", + "attachment_upload_error": "Fout bij het uploaden van de bijlage", "attachment_upload_success": "Bijlage succesvol geüpload!", "attachments": "Bijlagen", "gpx_tip": "Upload GPX-bestanden naar bijlagen om ze op de kaart te bekijken!", "images": "Afbeeldingen", "primary": "Primair", "upload": "Uploaden", - "view_attachment": "Bijlage bekijken" + "view_attachment": "Bijlage bekijken", + "of": "van", + "city": "Stad", + "delete_lodging": "Verwijder accommodatie", + "display_name": "Weergavenaam", + "location_details": "Locatiegegevens", + "lodging": "Onderdak", + "reservation_number": "Reserveringsnummer", + "welcome_map_info": "Publieke avonturen op deze server", + "lodging_delete_confirm": "Weet u zeker dat u deze accommodatielocatie wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.", + "lodging_information": "Informatie overliggen", + "price": "Prijs", + "region": "Regio" }, "home": { "desc_1": "Ontdek, plan en verken met gemak", @@ -254,7 +266,7 @@ "about": "Over AdventureLog", "adventures": "Avonturen", "collections": "Collecties", - "discord": "discord", + "discord": "Discord", "documentation": "Documentatie", "greeting": "Hoi", "logout": "Uitloggen", @@ -281,7 +293,8 @@ "tag": "Label", "language_selection": "Taal", "support": "Steun", - "calendar": "Kalender" + "calendar": "Kalender", + "admin_panel": "Admin -paneel" }, "auth": { "confirm_password": "Bevestig wachtwoord", @@ -302,7 +315,11 @@ "both_passwords_required": "Beide wachtwoorden zijn vereist", "new_password": "Nieuw wachtwoord", "reset_failed": "Kan het wachtwoord niet opnieuw instellen", - "or_3rd_party": "Of log in met een service van derden" + "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" }, "users": { "no_users_found": "Er zijn geen gebruikers gevonden met openbare profielen." @@ -325,8 +342,8 @@ "no_cities_found": "Geen steden gevonden", "region_failed_visited": "Kan de regio niet als bezocht markeren", "region_stats": "Regiostatistieken", - "regions_in": "Regio's binnen", - "removed": "VERWIJDERD", + "regions_in": "Regio's in", + "removed": "verwijderd", "view_cities": "Steden bekijken", "visit_remove_failed": "Kan bezoek niet verwijderen", "visit_to": "Bezoek aan" @@ -358,8 +375,8 @@ "join_discord": "Sluit je aan bij Discord", "join_discord_desc": "om uw eigen foto's te delen. \nPlaats ze in de", "photo_by": "Foto door", - "change_password_error": "Kan wachtwoord niet wijzigen. \nOngeldig huidig ​​wachtwoord of ongeldig nieuw wachtwoord.", - "current_password": "Huidig ​​wachtwoord", + "change_password_error": "Kan wachtwoord niet wijzigen. \nOngeldig huidig wachtwoord of ongeldig nieuw wachtwoord.", + "current_password": "Huidig wachtwoord", "password_change_lopout_warning": "Na het wijzigen van uw wachtwoord wordt u uitgelogd.", "authenticator_code": "Authenticatiecode", "copy": "Kopiëren", @@ -369,7 +386,7 @@ "email_removed": "E-mail succesvol verwijderd!", "email_removed_error": "Fout bij verwijderen van e-mail", "email_set_primary": "E-mailadres is succesvol ingesteld als primair!", - "email_set_primary_error": "Fout bij het instellen van e-mail als primair", + "email_set_primary_error": "Fout bij het instellen van dit e-mail als primair", "email_verified": "E-mail succesvol geverifieerd!", "email_verified_erorr_desc": "Uw e-mailadres kan niet worden geverifieerd. \nProbeer het opnieuw.", "email_verified_error": "Fout bij het verifiëren van e-mailadres", @@ -403,8 +420,8 @@ "username_taken": "Deze gebruikersnaam is al in gebruik.", "administration_settings": "Beheerinstellingen", "documentation_link": "Documentatielink", - "launch_account_connections": "Start Accountverbindingen", - "launch_administration_panel": "Start het Beheerpaneel", + "launch_account_connections": "Start accountverbindingen", + "launch_administration_panel": "Start het beheerpaneel", "no_verified_email_warning": "U moet een geverifieerd e-mailadres hebben om tweefactorauthenticatie in te schakelen.", "social_auth_desc": "Schakel sociale en OIDC-authenticatieproviders in of uit voor uw account. \nMet deze verbindingen kunt u inloggen met zelfgehoste authenticatie-identiteitsproviders zoals Authentik of externe providers zoals GitHub.", "social_auth_desc_2": "Deze instellingen worden beheerd op de AdventureLog-server en moeten handmatig worden ingeschakeld door de beheerder.", @@ -414,9 +431,9 @@ }, "checklist": { "add_item": "Artikel toevoegen", - "checklist_delete_error": "Fout bij verwijderen van controlelijst", + "checklist_delete_error": "Fout bij het verwijderen van de controlelijst", "checklist_deleted": "Controlelijst succesvol verwijderd!", - "checklist_editor": "Controlelijst-editor", + "checklist_editor": "Controlelijsten bewerken", "checklist_public": "Deze controlelijst is openbaar omdat deze zich in een openbare collectie bevindt.", "editing_checklist": "Controlelijst bewerken", "failed_to_save": "Kan controlelijst niet opslaan", @@ -427,7 +444,7 @@ "new_item": "Nieuw artikel", "save": "Opslaan", "checklist_viewer": "Controlelijstviewer", - "new_checklist": "Nieuwe checklist" + "new_checklist": "Nieuwe controlelijst" }, "collection": { "collection_created": "Collectie succesvol aangemaakt!", @@ -435,7 +452,7 @@ "create": "Aanmaken", "edit_collection": "Collectie bewerken", "error_creating_collection": "Fout bij aanmaken collectie", - "error_editing_collection": "Fout bij bewerken collectie", + "error_editing_collection": "Fout bij het bewerken van de collectie", "new_collection": "Nieuwe collectie", "public_collection": "Openbare collectie" }, @@ -446,22 +463,22 @@ "failed_to_save": "Kan opmerking niet opslaan", "note_delete_error": "Fout bij verwijderen van opmerking", "note_deleted": "Opmerking succesvol verwijderd!", - "note_editor": "Opmerking-editor", + "note_editor": "Opmerkingen bewerken", "note_public": "Deze opmerking is openbaar omdat deze zich in een openbare collectie bevindt.", "open": "Open", "save": "Opslaan", "invalid_url": "Ongeldige URL", - "note_viewer": "Notitieviewer" + "note_viewer": "Bekijk notities" }, "transportation": { "date_and_time": "Datum", "date_time": "Startdatum", - "edit": "Bewerking", + "edit": "Bewerk", "edit_transportation": "Vervoer bewerken", "end_date_time": "Einddatum", - "error_editing_transportation": "Fout bij bewerken van vervoer", + "error_editing_transportation": "Fout bij het bewerken van het vervoer", "flight_number": "Vluchtnummer", - "from_location": "Van locatie", + "from_location": "Vertreklocatie", "modes": { "bike": "Fiets", "boat": "Boot", @@ -470,9 +487,9 @@ "car": "Auto", "other": "Ander", "plane": "Vliegtuig", - "walking": "Lopen" + "walking": "Wandelen" }, - "to_location": "Naar locatie", + "to_location": "Aankomstlocatie", "transportation_edit_success": "Vervoer succesvol bewerkt!", "type": "Type", "new_transportation": "Nieuw vervoer", @@ -481,7 +498,10 @@ "transport_type": "Vervoerstype", "transportation_added": "Vervoer succesvol toegevoegd!", "transportation_delete_error": "Fout bij verwijderen vervoer", - "transportation_deleted": "Vervoer succesvol verwijderd!" + "transportation_deleted": "Vervoer succesvol verwijderd!", + "ending_airport_desc": "Voer eindigende luchthavencode in (bijv. LAX)", + "fetch_location_information": "Locatie -informatie ophalen", + "starting_airport_desc": "Voer de startende luchthavencode in (bijv. JFK)" }, "search": { "adventurelog_results": "AdventureLog-resultaten", @@ -490,7 +510,7 @@ }, "map": { "add_adventure": "Voeg nieuw avontuur toe", - "add_adventure_at_marker": "Voeg een nieuw avontuur toe bij markeerpunt", + "add_adventure_at_marker": "Voeg een nieuw avontuur toe bij het markeerpunt", "adventure_map": "Avonturenkaart", "clear_marker": "Verwijder markeerpunt", "map_options": "Kaartopties", @@ -520,10 +540,10 @@ "categories": { "category_name": "Categorienaam", "edit_category": "Categorie bewerken", - "icon": "Ikoon", + "icon": "Icoon", "manage_categories": "Beheer categorieën", "no_categories_found": "Geen categorieën gevonden.", - "select_category": "Selecteer categorie", + "select_category": "Selecteer een categorie", "update_after_refresh": "De avonturenkaarten worden bijgewerkt zodra u de pagina vernieuwt." }, "dashboard": { @@ -538,7 +558,7 @@ }, "immich": { "api_key": "Immich API-sleutel", - "api_note": "Let op: dit moet de URL naar de Immich API-server zijn, dus deze eindigt waarschijnlijk op /api, tenzij je een aangepaste configuratie hebt.", + "api_note": "Let op: dit moet de URL naar de Immich API-server zijn, deze eindigt dus waarschijnlijk op /api, tenzij je een aangepaste configuratie hebt.", "disable": "Uitzetten", "enable_immich": "Schakel Immich in", "imageid_required": "Afbeeldings-ID is vereist", @@ -559,7 +579,7 @@ "server_url": "Immich-server-URL", "update_integration": "Integratie bijwerken", "documentation": "Immich-integratiedocumentatie", - "localhost_note": "Opmerking: localhost zal hoogstwaarschijnlijk niet werken tenzij u de docker-netwerken dienovereenkomstig hebt ingesteld. \nHet wordt aanbevolen om het IP-adres van de server of de domeinnaam te gebruiken." + "localhost_note": "Opmerking: localhost zal hoogstwaarschijnlijk niet werken tenzij dit bewust zo geconfigureerd is in het docker-netwerk. \nHet is aanbevolen om het IP-adres van de server of de domeinnaam te gebruiken." }, "recomendations": { "address": "Adres", @@ -567,5 +587,35 @@ "phone": "Telefoon", "recommendation": "Aanbeveling", "website": "Website" + }, + "lodging": { + "apartment": "Appartement", + "bnb": "Bed and breakfast", + "cabin": "Cabine", + "campground": "Camping", + "check_in": "Inchecken", + "check_out": "Uitchecken", + "date_and_time": "Datum", + "edit": "Bewerking", + "edit_lodging": "Bewerkingen bewerken", + "error_editing_lodging": "Foutbewerkingsbewerkingen", + "hostel": "Hostel", + "hotel": "Hotel", + "house": "Huis", + "lodging_added": "Lodging met succes toegevoegd!", + "lodging_delete_error": "Fout bij het verwijderen van accommodatie", + "lodging_deleted": "Met succes verwijderd!", + "lodging_edit_success": "Lodging met succes bewerkt!", + "lodging_type": "Lodging type", + "motel": "Motel", + "new_lodging": "Nieuwe accommodatie", + "other": "Ander", + "provide_start_date": "Geef een startdatum op", + "reservation_number": "Reserveringsnummer", + "resort": "Toevlucht", + "start": "Begin", + "type": "Type", + "villa": "Villa", + "current_timezone": "Huidige tijdzone" } } diff --git a/frontend/src/locales/pl.json b/frontend/src/locales/pl.json index 31134d5..582164a 100644 --- a/frontend/src/locales/pl.json +++ b/frontend/src/locales/pl.json @@ -30,7 +30,8 @@ "aqua": "Aqua", "northernLights": "Zorza Polarna" }, - "calendar": "Kalendarz" + "calendar": "Kalendarz", + "admin_panel": "Panel administracyjny" }, "about": { "about": "O aplikacji", @@ -281,7 +282,19 @@ "images": "Obrazy", "primary": "Podstawowy", "upload": "Wgrywać", - "view_attachment": "Zobacz załącznik" + "view_attachment": "Zobacz załącznik", + "of": "z", + "city": "Miasto", + "delete_lodging": "Usunąć zakwaterowanie", + "display_name": "Nazwa wyświetlania", + "location_details": "Szczegóły lokalizacji", + "lodging": "Kwatera", + "lodging_delete_confirm": "Czy na pewno chcesz usunąć tę lokalizację zakwaterowania? \nTego działania nie można cofnąć.", + "lodging_information": "Informacje o zakwaterowaniu", + "price": "Cena", + "region": "Region", + "reservation_number": "Numer rezerwacji", + "welcome_map_info": "Publiczne przygody na tym serwerze" }, "worldtravel": { "country_list": "Lista krajów", @@ -326,7 +339,11 @@ "both_passwords_required": "Obydwa hasła są wymagane", "new_password": "Nowe hasło", "reset_failed": "Nie udało się zresetować hasła", - "or_3rd_party": "Lub zaloguj się za pomocą usługi strony trzeciej" + "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" }, "users": { "no_users_found": "Nie znaleziono użytkowników z publicznymi profilami." @@ -481,7 +498,10 @@ "transportation_edit_success": "Transport edytowany pomyślnie!", "edit_transportation": "Edytuj transport", "start": "Początek", - "date_and_time": "Data i godzina" + "date_and_time": "Data i godzina", + "ending_airport_desc": "Wprowadź końcowe kod lotniska (np. LAX)", + "fetch_location_information": "Pobierać informacje o lokalizacji", + "starting_airport_desc": "Wprowadź początkowy kod lotniska (np. JFK)" }, "search": { "adventurelog_results": "Wyniki AdventureLog", @@ -567,5 +587,35 @@ "phone": "Telefon", "recommendation": "Zalecenie", "website": "Strona internetowa" + }, + "lodging": { + "apartment": "Apartament", + "bnb": "Nocleg i śniadanie", + "cabin": "Kabina", + "campground": "Obozowisko", + "lodging_added": "Lodowanie dodane pomyślnie!", + "lodging_delete_error": "Usuwanie błędów", + "lodging_deleted": "Z powodzeniem usunięto!", + "lodging_edit_success": "Złożone zredagowane zredagowane!", + "lodging_type": "Typ składowania", + "motel": "Motel", + "start": "Start", + "type": "Typ", + "villa": "Willa", + "check_in": "Zameldować się", + "check_out": "Wymeldować się", + "date_and_time": "Data", + "edit": "Redagować", + "edit_lodging": "Edytuj zakwaterowanie", + "error_editing_lodging": "Edycja błędów", + "hostel": "Schronisko", + "hotel": "Hotel", + "house": "Dom", + "new_lodging": "Nowe zakwaterowanie", + "other": "Inny", + "provide_start_date": "Proszę podać datę rozpoczęcia", + "reservation_number": "Numer rezerwacji", + "resort": "Uciec", + "current_timezone": "Obecna strefa czasowa" } } diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index 919c6d9..6ee1e7b 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -234,7 +234,19 @@ "images": "Bilder", "primary": "Primär", "upload": "Ladda upp", - "view_attachment": "Visa bilaga" + "view_attachment": "Visa bilaga", + "of": "av", + "city": "Stad", + "delete_lodging": "Ta bort logi", + "display_name": "Visningsnamn", + "location_details": "Platsinformation", + "lodging": "Logi", + "welcome_map_info": "Offentliga äventyr på denna server", + "lodging_delete_confirm": "Är du säker på att du vill ta bort den här logiplatsen? \nDenna åtgärd kan inte ångras.", + "lodging_information": "Logi information", + "price": "Pris", + "region": "Område", + "reservation_number": "Bokningsnummer" }, "home": { "desc_1": "Upptäck, planera och utforska med lätthet", @@ -281,7 +293,8 @@ "tag": "Tagg", "language_selection": "Språk", "support": "Support", - "calendar": "Kalender" + "calendar": "Kalender", + "admin_panel": "Administratör" }, "worldtravel": { "all": "Alla", @@ -326,7 +339,11 @@ "both_passwords_required": "Båda lösenorden krävs", "new_password": "Nytt lösenord", "reset_failed": "Det gick inte att återställa lösenordet", - "or_3rd_party": "Eller logga in med en tredjepartstjänst" + "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" }, "users": { "no_users_found": "Inga användare hittades med offentliga profiler." @@ -481,7 +498,10 @@ "transportation_delete_error": "Det gick inte att ta bort transport", "transportation_deleted": "Transporten har raderats!", "transportation_edit_success": "Transporten har redigerats!", - "type": "Typ" + "type": "Typ", + "ending_airport_desc": "Ange slut på flygplatskoden (t.ex. LAX)", + "fetch_location_information": "Hämta platsinformation", + "starting_airport_desc": "Ange start av flygplatskoden (t.ex. JFK)" }, "search": { "adventurelog_results": "AdventureLog-resultat", @@ -567,5 +587,35 @@ "phone": "Telefon", "recommendation": "Rekommendation", "website": "Webbplats" + }, + "lodging": { + "apartment": "Lägenhet", + "bnb": "Säng och frukost", + "cabin": "Stuga", + "campground": "Campingplats", + "error_editing_lodging": "Felredigeringsbefäl", + "hostel": "Vandrarhem", + "hotel": "Hotell", + "house": "Hus", + "lodging_added": "Logi tillagd framgångsrikt!", + "lodging_delete_error": "Felladering logi", + "lodging_deleted": "Logga raderas framgångsrikt!", + "lodging_edit_success": "LOGGE Redigerad framgångsrikt!", + "lodging_type": "Logi", + "motel": "Motell", + "new_lodging": "Inställning", + "other": "Andra", + "provide_start_date": "Vänligen ange ett startdatum", + "reservation_number": "Bokningsnummer", + "resort": "Tillflykt", + "start": "Start", + "type": "Typ", + "villa": "Villa", + "check_in": "Checka in", + "check_out": "Checka ut", + "date_and_time": "Datum", + "edit": "Redigera", + "edit_lodging": "Redigera logi", + "current_timezone": "Nuvarande tidszon" } } diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 7270e3b..9a1f0d2 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -234,7 +234,19 @@ "images": "图片", "primary": "基本的", "upload": "上传", - "view_attachment": "查看附件" + "view_attachment": "查看附件", + "of": "的", + "city": "城市", + "delete_lodging": "删除住宿", + "display_name": "显示名称", + "location_details": "位置详细信息", + "lodging": "住宿", + "lodging_delete_confirm": "您确定要删除此住宿地点吗?\n该动作不能撤消。", + "lodging_information": "住宿信息", + "price": "价格", + "region": "地区", + "reservation_number": "预订号", + "welcome_map_info": "该服务器上的公共冒险" }, "home": { "desc_1": "轻松发现、规划和探索", @@ -281,7 +293,8 @@ "tag": "标签", "language_selection": "语言", "support": "支持", - "calendar": "日历" + "calendar": "日历", + "admin_panel": "管理面板" }, "auth": { "forgot_password": "忘记密码?", @@ -302,7 +315,11 @@ "both_passwords_required": "两个密码都需要", "new_password": "新密码", "reset_failed": "重置密码失败", - "or_3rd_party": "或者使用第三方服务登录" + "or_3rd_party": "或者使用第三方服务登录", + "no_public_adventures": "找不到公共冒险", + "no_public_collections": "找不到公共收藏", + "user_adventures": "用户冒险", + "user_collections": "用户收集" }, "worldtravel": { "all": "全部", @@ -481,7 +498,10 @@ "transportation_delete_error": "删除交通时出错", "transportation_deleted": "交通删除成功!", "transportation_edit_success": "交通编辑成功!", - "type": "类型" + "type": "类型", + "ending_airport_desc": "输入结束机场代码(例如LAX)", + "fetch_location_information": "获取位置信息", + "starting_airport_desc": "输入启动机场代码(例如肯尼迪国际机构)" }, "search": { "adventurelog_results": "冒险日志结果", @@ -567,5 +587,35 @@ "phone": "电话", "recommendation": "推荐", "website": "网站" + }, + "lodging": { + "campground": "营地", + "check_in": "报到", + "check_out": "查看", + "date_and_time": "日期", + "edit": "编辑", + "edit_lodging": "编辑住宿", + "error_editing_lodging": "错误编辑住宿", + "hostel": "旅馆", + "hotel": "酒店", + "house": "房子", + "lodging_added": "住宿成功增加了!", + "lodging_delete_error": "错误删除住宿", + "lodging_deleted": "住宿成功删除了!", + "lodging_edit_success": "住宿成功编辑了!", + "lodging_type": "住宿类型", + "motel": "汽车旅馆", + "start": "开始", + "type": "类型", + "villa": "别墅", + "apartment": "公寓", + "bnb": "床和早餐", + "cabin": "舱", + "new_lodging": "新住宿", + "other": "其他", + "provide_start_date": "请提供开始日期", + "reservation_number": "预订号", + "resort": "采取", + "current_timezone": "当前时区" } } diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 3010bca..dc904ea 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -14,8 +14,9 @@ register('nl', () => import('../locales/nl.json')); register('sv', () => import('../locales/sv.json')); register('pl', () => import('../locales/pl.json')); + register('ko', () => import('../locales/ko.json')); - let locales = ['en', 'es', 'fr', 'de', 'it', 'zh', 'nl', 'sv', 'pl']; + let locales = ['en', 'es', 'fr', 'de', 'it', 'zh', 'nl', 'sv', 'pl', 'ko']; if (browser) { init({ diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts index b8c71b5..e722dbf 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -2,7 +2,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; import { redirect, type Actions } from '@sveltejs/kit'; // @ts-ignore import psl from 'psl'; -import { themes } from '$lib'; +import { getRandomBackground, themes } from '$lib'; import { fetchCSRFToken } from '$lib/index.server'; import type { PageServerLoad } from './$types'; @@ -11,6 +11,13 @@ const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; export const load = (async (event) => { if (event.locals.user) { return redirect(302, '/dashboard'); + } else { + const background = getRandomBackground(); + return { + props: { + background + } + }; } }) satisfies PageServerLoad; @@ -51,8 +58,10 @@ export const actions: Actions = { // Check if hostname is an IP address const isIPAddress = /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname); + const isLocalhost = hostname === 'localhost'; + const isSingleLabel = hostname.split('.').length === 1; - if (!isIPAddress) { + if (!isIPAddress && !isSingleLabel && !isLocalhost) { const parsed = psl.parse(hostname); if (parsed && parsed.domain) { diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 9fadd74..b67404a 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,126 +1,117 @@ -
    -
    -
    -
    -
    - {#if data.user} - {#if data.user.first_name && data.user.first_name !== null} -

    - {data.user.first_name.charAt(0).toUpperCase() + data.user.first_name.slice(1)}, - {$t('home.hero_1')} -

    - {:else} -

    - {$t('home.hero_1')} -

    - {/if} - {:else} -

    - {$t('home.hero_1')} -

    - {/if} -

    - {$t('home.hero_2')} -

    -
    -
    - {#if data.user} - - {:else} - - - {/if} -
    + +
    +
    + +
    + {#if data.user} + {#if data.user.first_name && data.user.first_name !== null} +

    + {data.user.first_name.charAt(0).toUpperCase() + data.user.first_name.slice(1)}, {$t( + 'home.hero_1' + )} +

    + {:else} +

    + {$t('home.hero_1')} +

    + {/if} + {:else} +

    + {$t('home.hero_1')} +

    + {/if} +

    + {$t('home.hero_2')} +

    +
    + {#if data.user} + + {:else} + + + {/if}
    +
    + +
    Hero
    -
    -
    -
    -
    -
    - {$t('home.key_features')} -
    -

    - {$t('home.desc_1')} -

    -

    - {$t('home.desc_2')} -

    + + +
    +
    +
    +
    + {$t('home.key_features')}
    +

    + {$t('home.desc_1')} +

    +

    + {$t('home.desc_2')} +

    -
    - - Image -
    -
      -
    • -
      -

      {$t('home.feature_1')}

      -

      - {$t('home.feature_1_desc')} -

      -
      +
      + +
      + World map with pins +
      + +
      +
        +
      • +

        {$t('home.feature_1')}

        +

        + {$t('home.feature_1_desc')} +

      • -
      • -
        -

        {$t('home.feature_2')}

        -

        - {$t('home.feature_2_desc')} -

        -
        +
      • +

        {$t('home.feature_2')}

        +

        + {$t('home.feature_2_desc')} +

      • -
      • -
        -

        {$t('home.feature_3')}

        -

        - {$t('home.feature_3_desc')} -

        -
        +
      • +

        {$t('home.feature_3')}

        +

        + {$t('home.feature_3_desc')} +

      diff --git a/frontend/src/routes/admin/+page.server.ts b/frontend/src/routes/admin/+page.server.ts new file mode 100644 index 0000000..c32b3ba --- /dev/null +++ b/frontend/src/routes/admin/+page.server.ts @@ -0,0 +1,17 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from '../$types'; +const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; +const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; + +export const load: PageServerLoad = async (event) => { + let publicUrlFetch = await fetch(`${endpoint}/public-url/`); + let publicUrl = ''; + if (!publicUrlFetch.ok) { + return redirect(302, '/'); + } else { + let publicUrlJson = await publicUrlFetch.json(); + publicUrl = publicUrlJson.PUBLIC_URL; + } + + return redirect(302, publicUrl + '/admin/'); +}; diff --git a/frontend/src/routes/adventures/+page.svelte b/frontend/src/routes/adventures/+page.svelte index ea5e415..fa59fcc 100644 --- a/frontend/src/routes/adventures/+page.svelte +++ b/frontend/src/routes/adventures/+page.svelte @@ -261,7 +261,7 @@ {$t('categories.manage_categories')}

      {$t('adventures.sort')}

      diff --git a/frontend/src/routes/adventures/[id]/+page.svelte b/frontend/src/routes/adventures/[id]/+page.svelte index 35cd73c..d282083 100644 --- a/frontend/src/routes/adventures/[id]/+page.svelte +++ b/frontend/src/routes/adventures/[id]/+page.svelte @@ -185,7 +185,10 @@ alt={adventure.name} /> -
      + +
      {#each adventure.images as _, i}
      {/if} +
      @@ -221,6 +225,43 @@
      + {#if adventure.user} +
      + {#if adventure.user.profile_pic} +
      +
      + {adventure.user.username} +
      +
      + {:else} +
      +
      + {adventure.user.first_name + ? adventure.user.first_name.charAt(0) + : adventure.user.username.charAt(0)}{adventure.user.last_name + ? adventure.user.last_name.charAt(0) + : ''} +
      +
      + {/if} + +
      + {#if adventure.user.public_profile} + + {adventure.user.first_name || adventure.user.username}{' '} + {adventure.user.last_name} + + {:else} + + {adventure.user.first_name || adventure.user.username}{' '} + {adventure.user.last_name} + + {/if} +
      +
      + {/if}
      {adventure.is_public ? 'Public' : 'Private'}
      + {#if adventure.location}
      Download Calendar{$t('adventures.download_calendar')}
      diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 6f49525..8f007ca 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -1,5 +1,5 @@ {#if isShowingLinkModal} @@ -376,6 +419,15 @@ /> {/if} +{#if isShowingLodgingModal} + (isShowingLodgingModal = false)} + on:save={saveOrCreateLodging} + {collection} + /> +{/if} + {#if isAdventureModalOpen} {$t('adventures.checklist')} + - {#if adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0} + {#if adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0 && lodging.length == 0} {/if} {/if} @@ -743,6 +862,10 @@ new Date(collection.start_date), numberOfDays )[dateString] || []} + {@const dayLodging = + groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays)[ + dateString + ] || []} {@const dayNotes = groupNotesByDate(notes, new Date(collection.start_date), numberOfDays)[dateString] || []} @@ -804,6 +927,18 @@ /> {/each} {/if} + {#if dayLodging.length > 0} + {#each dayLodging as hotel} + { + lodging = lodging.filter((t) => t.id != event.detail); + }} + on:edit={editLodging} + /> + {/each} + {/if} {#if dayChecklists.length > 0} {#each dayChecklists as checklist} - {#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0} + {#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0 && dayLodging.length == 0}

      {$t('adventures.nothing_planned')}

      {/if}
      @@ -950,7 +1085,7 @@
      {/if} - {#if currentView == 'recommendations'} + {#if currentView == 'recommendations' && data.user}

      Adventure Recommendations

      diff --git a/frontend/src/routes/dashboard/+page.server.ts b/frontend/src/routes/dashboard/+page.server.ts index 5db5aef..a87a45a 100644 --- a/frontend/src/routes/dashboard/+page.server.ts +++ b/frontend/src/routes/dashboard/+page.server.ts @@ -20,11 +20,14 @@ export const load = (async (event) => { let stats = null; - let res = await event.fetch(`${serverEndpoint}/api/stats/counts/`, { - headers: { - Cookie: `sessionid=${event.cookies.get('sessionid')}` + let res = await event.fetch( + `${serverEndpoint}/api/stats/counts/${event.locals.user.username}`, + { + headers: { + Cookie: `sessionid=${event.cookies.get('sessionid')}` + } } - }); + ); if (!res.ok) { console.error('Failed to fetch user stats'); } else { diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index 68dcd88..1422605 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -120,8 +120,10 @@ function handleSuccessfulLogin(event: RequestEvent, respo // Check if hostname is an IP address const isIPAddress = /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname); + const isLocalhost = hostname === 'localhost'; + const isSingleLabel = hostname.split('.').length === 1; - if (!isIPAddress) { + if (!isIPAddress && !isSingleLabel && !isLocalhost) { const parsed = psl.parse(hostname); if (parsed && parsed.domain) { diff --git a/frontend/src/routes/map/+page.svelte b/frontend/src/routes/map/+page.svelte index f550760..97edc42 100644 --- a/frontend/src/routes/map/+page.svelte +++ b/frontend/src/routes/map/+page.svelte @@ -10,6 +10,8 @@ let createModalOpen: boolean = false; let showGeo: boolean = false; + export let initialLatLng: { lat: number; lng: number } | null = null; + let visitedRegions: VisitedRegion[] = data.props.visitedRegions; let adventures: Adventure[] = data.props.adventures; @@ -49,6 +51,11 @@ newLatitude = e.detail.lngLat.lat; } + function newAdventure() { + initialLatLng = { lat: newLatitude, lng: newLongitude } as { lat: number; lng: number }; + createModalOpen = true; + } + function createNewAdventure(event: CustomEvent) { adventures = [...adventures, event.detail]; newMarker = null; @@ -86,7 +93,7 @@ />
      {#if newMarker} -