mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-06 21:55:18 +02:00
Hotel tracking, new profile system, UI improvements and lots of bug fixes
This commit is contained in:
commit
00e4ec64ae
87 changed files with 3941 additions and 1422 deletions
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
* @seanmorley15
|
46
.github/workflows/cdn-beta.yml
vendored
Normal file
46
.github/workflows/cdn-beta.yml
vendored
Normal file
|
@ -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
|
46
.github/workflows/cdn-latest.yml
vendored
Normal file
46
.github/workflows/cdn-latest.yml
vendored
Normal file
|
@ -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
|
43
.github/workflows/cdn-release.yml
vendored
Normal file
43
.github/workflows/cdn-release.yml
vendored
Normal file
|
@ -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
|
|
@ -1,91 +1,50 @@
|
||||||
# Contributing to AdventureLog
|
# Contributing to AdventureLog
|
||||||
|
|
||||||
When contributing to this repository, please first discuss the change you wish to make via issue,
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
## Pull Request Process
|
## 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.
|
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. Update the README.md with details of changes to the interface, this includes new environment
|
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.
|
||||||
variables, exposed ports, useful file locations and container parameters.
|
3. **Pull Request**: Submit a pull request with your changes. Make sure to reference the issue you opened in the description.
|
||||||
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.
|
|
||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
|
|
||||||
### Our Pledge
|
### Our Pledge
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
### Our Standards
|
### Our Standards
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment
|
In order to maintain a positive environment, we encourage the following behaviors:
|
||||||
include:
|
|
||||||
|
|
||||||
- Using welcoming and inclusive language
|
- **Inclusivity**: Use welcoming and inclusive language that fosters collaboration across all perspectives and experiences.
|
||||||
- Being respectful of differing viewpoints and experiences
|
- **Respect**: Respect differing opinions and engage with empathy, understanding that each person’s perspective is valuable.
|
||||||
- Gracefully accepting constructive criticism
|
- **Constructive Feedback**: Offer feedback that helps improve the project and allows contributors to grow from it.
|
||||||
- Focusing on what is best for the community
|
- **Adventure Spirit**: Bring the same sense of curiosity, discovery, and positivity that drives AdventureLog into all interactions with the community.
|
||||||
- Showing empathy towards other community members
|
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
- Personal attacks, trolling, or any form of harassment.
|
||||||
advances
|
- Insensitive or discriminatory language, including sexualized comments or imagery.
|
||||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
- Spamming or misusing project spaces for personal gain.
|
||||||
- Public or private harassment
|
- Publishing or using others’ private information without permission.
|
||||||
- Publishing others' private information, such as a physical or electronic
|
- Anything else that could be seen as disrespectful or unprofessional in a collaborative environment.
|
||||||
address, without explicit permission
|
|
||||||
- Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
### Our Responsibilities
|
### Our Responsibilities
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable
|
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.
|
||||||
behavior and are expected to take appropriate and fair corrective action in
|
|
||||||
response to any instances of unacceptable behavior.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
We strive to foster a community that balances open collaboration with respect for all contributors.
|
||||||
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.
|
|
||||||
|
|
||||||
### Scope
|
### Scope
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
### Enforcement
|
### Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
### Attribution
|
### Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], 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.
|
||||||
available at [http://contributor-covenant.org/version/1/4][version]
|
|
||||||
|
|
||||||
[homepage]: http://contributor-covenant.org
|
|
||||||
[version]: http://contributor-covenant.org/version/1/4/
|
|
||||||
|
|
|
@ -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)
|
- WorldTravel Dataset [dr5hn/countries-states-cities-database](https://github.com/dr5hn/countries-states-cities-database)
|
||||||
|
|
||||||
### Top Supporters 💖
|
### Top Supporters 💖
|
||||||
|
|
||||||
- [Veymax](https://x.com/veymax)
|
- [Veymax](https://x.com/veymax)
|
||||||
- [nebriv](https://github.com/nebriv)
|
- [nebriv](https://github.com/nebriv)
|
||||||
- [Victor Butler](https://x.com/victor_butler)
|
- [Victor Butler](https://x.com/victor_butler)
|
||||||
|
|
|
@ -25,10 +25,10 @@ EMAIL_BACKEND='console'
|
||||||
|
|
||||||
# ------------------- #
|
# ------------------- #
|
||||||
# For Developers to start a Demo Database
|
# 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'
|
# PGHOST='localhost'
|
||||||
# PGDATABASE='admin'
|
# PGDATABASE='adventurelog'
|
||||||
# PGUSER='admin'
|
# PGUSER='admin'
|
||||||
# PGPASSWORD='admin'
|
# PGPASSWORD='admin'
|
||||||
# ------------------- #
|
# ------------------- #
|
0
backend/server/achievements/__init__.py
Normal file
0
backend/server/achievements/__init__.py
Normal file
9
backend/server/achievements/admin.py
Normal file
9
backend/server/achievements/admin.py
Normal file
|
@ -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)
|
6
backend/server/achievements/apps.py
Normal file
6
backend/server/achievements/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AchievementsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'achievements'
|
0
backend/server/achievements/management/__init__.py
Normal file
0
backend/server/achievements/management/__init__.py
Normal file
|
@ -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}"))
|
34
backend/server/achievements/models.py
Normal file
34
backend/server/achievements/models.py
Normal file
|
@ -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}"
|
3
backend/server/achievements/tests.py
Normal file
3
backend/server/achievements/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
backend/server/achievements/views.py
Normal file
3
backend/server/achievements/views.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
|
@ -1,7 +1,7 @@
|
||||||
import os
|
import os
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment
|
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment, Lodging
|
||||||
from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
|
from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
|
||||||
from allauth.account.decorators import secure_admin_login
|
from allauth.account.decorators import secure_admin_login
|
||||||
|
|
||||||
|
@ -140,6 +140,7 @@ admin.site.register(Category, CategoryAdmin)
|
||||||
admin.site.register(City, CityAdmin)
|
admin.site.register(City, CityAdmin)
|
||||||
admin.site.register(VisitedCity)
|
admin.site.register(VisitedCity)
|
||||||
admin.site.register(Attachment)
|
admin.site.register(Attachment)
|
||||||
|
admin.site.register(Lodging)
|
||||||
|
|
||||||
admin.site.site_header = 'AdventureLog Admin'
|
admin.site.site_header = 'AdventureLog Admin'
|
||||||
admin.site.site_title = 'AdventureLog Admin Site'
|
admin.site.site_title = 'AdventureLog Admin Site'
|
||||||
|
|
39
backend/server/adventures/migrations/0022_hotel.py
Normal file
39
backend/server/adventures/migrations/0022_hotel.py
Normal file
|
@ -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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -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',
|
||||||
|
),
|
||||||
|
]
|
|
@ -36,6 +36,20 @@ ADVENTURE_TYPES = [
|
||||||
('other', 'Other')
|
('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 = [
|
TRANSPORTATION_TYPES = [
|
||||||
('car', 'Car'),
|
('car', 'Car'),
|
||||||
('plane', 'Plane'),
|
('plane', 'Plane'),
|
||||||
|
@ -319,3 +333,37 @@ class Category(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name + ' - ' + self.display_name + ' - ' + self.icon
|
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
|
|
@ -1,8 +1,9 @@
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import os
|
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 rest_framework import serializers
|
||||||
from main.utils import CustomModelSerializer
|
from main.utils import CustomModelSerializer
|
||||||
|
from users.serializers import CustomUserDetailsSerializer
|
||||||
|
|
||||||
|
|
||||||
class AdventureImageSerializer(CustomModelSerializer):
|
class AdventureImageSerializer(CustomModelSerializer):
|
||||||
|
@ -80,15 +81,16 @@ class AdventureSerializer(CustomModelSerializer):
|
||||||
attachments = AttachmentSerializer(many=True, read_only=True)
|
attachments = AttachmentSerializer(many=True, read_only=True)
|
||||||
category = CategorySerializer(read_only=False, required=False)
|
category = CategorySerializer(read_only=False, required=False)
|
||||||
is_visited = serializers.SerializerMethodField()
|
is_visited = serializers.SerializerMethodField()
|
||||||
|
user = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Adventure
|
model = Adventure
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location',
|
'id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location',
|
||||||
'is_public', 'collection', 'created_at', 'updated_at', 'images', 'link', 'longitude',
|
'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):
|
def validate_category(self, category_data):
|
||||||
if isinstance(category_data, Category):
|
if isinstance(category_data, Category):
|
||||||
|
@ -127,6 +129,10 @@ class AdventureSerializer(CustomModelSerializer):
|
||||||
)
|
)
|
||||||
return category
|
return category
|
||||||
|
|
||||||
|
def get_user(self, obj):
|
||||||
|
user = obj.user_id
|
||||||
|
return CustomUserDetailsSerializer(user).data
|
||||||
|
|
||||||
def get_is_visited(self, obj):
|
def get_is_visited(self, obj):
|
||||||
current_date = timezone.now().date()
|
current_date = timezone.now().date()
|
||||||
for visit in obj.visits.all():
|
for visit in obj.visits.all():
|
||||||
|
@ -197,6 +203,17 @@ class TransportationSerializer(CustomModelSerializer):
|
||||||
]
|
]
|
||||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
|
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 NoteSerializer(CustomModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -283,10 +300,11 @@ class CollectionSerializer(CustomModelSerializer):
|
||||||
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')
|
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')
|
||||||
notes = NoteSerializer(many=True, read_only=True, source='note_set')
|
notes = NoteSerializer(many=True, read_only=True, source='note_set')
|
||||||
checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set')
|
checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set')
|
||||||
|
lodging = LodgingSerializer(many=True, read_only=True, source='lodging_set')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Collection
|
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']
|
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
|
@ -297,4 +315,3 @@ class CollectionSerializer(CustomModelSerializer):
|
||||||
shared_uuids.append(str(user.uuid))
|
shared_uuids.append(str(user.uuid))
|
||||||
representation['shared_with'] = shared_uuids
|
representation['shared_with'] = shared_uuids
|
||||||
return representation
|
return representation
|
||||||
|
|
|
@ -18,6 +18,7 @@ router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-cale
|
||||||
router.register(r'overpass', OverpassViewSet, basename='overpass')
|
router.register(r'overpass', OverpassViewSet, basename='overpass')
|
||||||
router.register(r'search', GlobalSearchView, basename='search')
|
router.register(r'search', GlobalSearchView, basename='search')
|
||||||
router.register(r'attachments', AttachmentViewSet, basename='attachments')
|
router.register(r'attachments', AttachmentViewSet, basename='attachments')
|
||||||
|
router.register(r'lodging', LodgingViewSet, basename='lodging')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -13,3 +13,4 @@ from .stats_view import *
|
||||||
from .transportation_view import *
|
from .transportation_view import *
|
||||||
from .global_search_view import *
|
from .global_search_view import *
|
||||||
from .attachment_view import *
|
from .attachment_view import *
|
||||||
|
from .lodging_view import *
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.utils import timezone
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db.models import Q, Max
|
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 import viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from adventures.models import Adventure, Category, Transportation, Lodging
|
||||||
from adventures.models import Adventure, Category
|
|
||||||
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
||||||
from adventures.serializers import AdventureSerializer
|
from adventures.serializers import AdventureSerializer, TransportationSerializer, LodgingSerializer
|
||||||
from adventures.utils import pagination
|
from adventures.utils import pagination
|
||||||
|
|
||||||
class AdventureViewSet(viewsets.ModelViewSet):
|
class AdventureViewSet(viewsets.ModelViewSet):
|
||||||
|
@ -98,9 +98,22 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
||||||
user_id=request.user.id
|
user_id=request.user.id
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_visited.lower() in ['true', 'false']:
|
is_visited_param = request.query_params.get('is_visited')
|
||||||
is_visited_bool = is_visited.lower() == 'true'
|
if is_visited_param is not None:
|
||||||
queryset = queryset.filter(is_visited=is_visited_bool)
|
# 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)
|
queryset = self.apply_sorting(queryset)
|
||||||
return self.paginate_and_respond(queryset, request)
|
return self.paginate_and_respond(queryset, request)
|
||||||
|
@ -185,3 +198,49 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
return Response(serializer.data)
|
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)
|
||||||
|
|
84
backend/server/adventures/views/lodging_view.py
Normal file
84
backend/server/adventures/views/lodging_view.py
Normal file
|
@ -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)
|
|
@ -21,11 +21,15 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
|
||||||
country_code = None
|
country_code = None
|
||||||
city = None
|
city = None
|
||||||
visited_city = None
|
visited_city = None
|
||||||
|
location_name = None
|
||||||
|
|
||||||
# town = None
|
# town = None
|
||||||
# city = None
|
# city = None
|
||||||
# county = None
|
# county = None
|
||||||
|
|
||||||
|
if 'name' in data.keys():
|
||||||
|
location_name = data['name']
|
||||||
|
|
||||||
if 'address' in data.keys():
|
if 'address' in data.keys():
|
||||||
keys = data['address'].keys()
|
keys = data['address'].keys()
|
||||||
for key in keys:
|
for key in keys:
|
||||||
|
@ -58,7 +62,7 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
|
||||||
if visited_city:
|
if visited_city:
|
||||||
city_visited = True
|
city_visited = True
|
||||||
if region:
|
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"}
|
return {"error": "No region found"}
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
|
|
|
@ -2,29 +2,42 @@ from rest_framework import viewsets
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from worldtravel.models import City, Region, Country, VisitedCity, VisitedRegion
|
from worldtravel.models import City, Region, Country, VisitedCity, VisitedRegion
|
||||||
from adventures.models import Adventure, Collection
|
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):
|
class StatsViewSet(viewsets.ViewSet):
|
||||||
"""
|
"""
|
||||||
A simple ViewSet for listing the stats of a user.
|
A simple ViewSet for listing the stats of a user.
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
@action(detail=False, methods=['get'], url_path='counts/(?P<username>[^/.]+)')
|
||||||
|
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)
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
# remove the email address from the response
|
||||||
def counts(self, request):
|
user.email = None
|
||||||
|
|
||||||
|
# get the counts for the user
|
||||||
adventure_count = Adventure.objects.filter(
|
adventure_count = Adventure.objects.filter(
|
||||||
user_id=request.user.id).count()
|
user_id=user.id).count()
|
||||||
trips_count = Collection.objects.filter(
|
trips_count = Collection.objects.filter(
|
||||||
user_id=request.user.id).count()
|
user_id=user.id).count()
|
||||||
visited_city_count = VisitedCity.objects.filter(
|
visited_city_count = VisitedCity.objects.filter(
|
||||||
user_id=request.user.id).count()
|
user_id=user.id).count()
|
||||||
total_cities = City.objects.count()
|
total_cities = City.objects.count()
|
||||||
visited_region_count = VisitedRegion.objects.filter(
|
visited_region_count = VisitedRegion.objects.filter(
|
||||||
user_id=request.user.id).count()
|
user_id=user.id).count()
|
||||||
total_regions = Region.objects.count()
|
total_regions = Region.objects.count()
|
||||||
visited_country_count = VisitedRegion.objects.filter(
|
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()
|
total_countries = Country.objects.count()
|
||||||
return Response({
|
return Response({
|
||||||
'adventure_count': adventure_count,
|
'adventure_count': adventure_count,
|
||||||
|
|
|
@ -63,25 +63,31 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
||||||
return integration
|
return integration
|
||||||
|
|
||||||
query = request.query_params.get('query', '')
|
query = request.query_params.get('query', '')
|
||||||
|
date = request.query_params.get('date', '')
|
||||||
|
|
||||||
if not query:
|
if not query and not date:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
'message': 'Query is required.',
|
'message': 'Query or date is required.',
|
||||||
'error': True,
|
'error': True,
|
||||||
'code': 'immich.query_required'
|
'code': 'immich.query_required'
|
||||||
},
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
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
|
# 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:
|
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
|
'x-api-key': integration.api_key
|
||||||
},
|
},
|
||||||
json = {
|
json = arguments
|
||||||
'query': query
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
res = immich_fetch.json()
|
res = immich_fetch.json()
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
|
@ -219,10 +225,14 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'assets' in res:
|
if 'assets' in res:
|
||||||
return Response(
|
paginator = self.pagination_class()
|
||||||
res['assets'],
|
# for each item in the items, we need to add the image url to the item so we can display it in the frontend
|
||||||
status=status.HTTP_200_OK
|
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:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
|
|
|
@ -61,6 +61,7 @@ INSTALLED_APPS = (
|
||||||
'users',
|
'users',
|
||||||
'integrations',
|
'integrations',
|
||||||
'django.contrib.gis',
|
'django.contrib.gis',
|
||||||
|
# 'achievements', # Not done yet, will be added later in a future update
|
||||||
# 'widget_tweaks',
|
# 'widget_tweaks',
|
||||||
# 'slippers',
|
# 'slippers',
|
||||||
|
|
||||||
|
@ -128,23 +129,20 @@ USE_L10N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
unParsedFrontenedUrl = getenv('FRONTEND_URL', 'http://localhost:3000')
|
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')
|
SESSION_COOKIE_SECURE = FRONTEND_URL.startswith('https')
|
||||||
|
|
||||||
# Parse the FRONTEND_URL
|
hostname = urlparse(FRONTEND_URL).hostname
|
||||||
# Remove and ' from the URL
|
|
||||||
|
|
||||||
parsed_url = urlparse(FRONTEND_URL)
|
|
||||||
hostname = parsed_url.hostname
|
|
||||||
|
|
||||||
# Check if the hostname is an IP address
|
|
||||||
is_ip_address = hostname.replace('.', '').isdigit()
|
is_ip_address = hostname.replace('.', '').isdigit()
|
||||||
|
|
||||||
if is_ip_address:
|
# Check if the hostname is single-label (no dots)
|
||||||
# Do not set a domain for IP addresses
|
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
|
SESSION_COOKIE_DOMAIN = None
|
||||||
else:
|
else:
|
||||||
# Use publicsuffix2 to calculate the correct cookie domain
|
# Use publicsuffix2 to calculate the correct cookie domain
|
||||||
|
@ -155,6 +153,7 @@ else:
|
||||||
# Fallback to the hostname if parsing fails
|
# Fallback to the hostname if parsing fails
|
||||||
SESSION_COOKIE_DOMAIN = hostname
|
SESSION_COOKIE_DOMAIN = hostname
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/1.7/howto/static-files/
|
# 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
|
# https://github.com/dr5hn/countries-states-cities-database/tags
|
||||||
COUNTRY_REGION_JSON_VERSION = 'v2.5'
|
COUNTRY_REGION_JSON_VERSION = 'v2.5'
|
|
@ -22,7 +22,7 @@ urlpatterns = [
|
||||||
|
|
||||||
path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'),
|
path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'),
|
||||||
path('auth/users/', PublicUserListView.as_view(), name='public-user-list'),
|
path('auth/users/', PublicUserListView.as_view(), name='public-user-list'),
|
||||||
path('auth/user/<uuid:user_id>/', PublicUserDetailView.as_view(), name='public-user-detail'),
|
path('auth/user/<str:username>/', PublicUserDetailView.as_view(), name='public-user-detail'),
|
||||||
path('auth/update-user/', UpdateUserMetadataView.as_view(), name='update-user-metadata'),
|
path('auth/update-user/', UpdateUserMetadataView.as_view(), name='update-user-metadata'),
|
||||||
|
|
||||||
path('auth/user-metadata/', UserMetadataView.as_view(), name='user-metadata'),
|
path('auth/user-metadata/', UserMetadataView.as_view(), name='user-metadata'),
|
||||||
|
|
|
@ -118,5 +118,7 @@ class CustomUserDetailsSerializer(UserDetailsSerializer):
|
||||||
|
|
||||||
# Remove `pk` field from the response
|
# Remove `pk` field from the response
|
||||||
representation.pop('pk', None)
|
representation.pop('pk', None)
|
||||||
|
# Remove the email field
|
||||||
|
representation.pop('email', None)
|
||||||
|
|
||||||
return representation
|
return representation
|
||||||
|
|
|
@ -11,6 +11,8 @@ from django.shortcuts import get_object_or_404
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from .serializers import CustomUserDetailsSerializer as PublicUserSerializer
|
from .serializers import CustomUserDetailsSerializer as PublicUserSerializer
|
||||||
from allauth.socialaccount.models import SocialApp
|
from allauth.socialaccount.models import SocialApp
|
||||||
|
from adventures.serializers import AdventureSerializer, CollectionSerializer
|
||||||
|
from adventures.models import Adventure, Collection
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
@ -66,6 +68,9 @@ class PublicUserListView(APIView):
|
||||||
for user in users:
|
for user in users:
|
||||||
user.email = None
|
user.email = None
|
||||||
serializer = PublicUserSerializer(users, many=True)
|
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)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
class PublicUserDetailView(APIView):
|
class PublicUserDetailView(APIView):
|
||||||
|
@ -79,12 +84,29 @@ class PublicUserDetailView(APIView):
|
||||||
},
|
},
|
||||||
operation_description="Get public user information."
|
operation_description="Get public user information."
|
||||||
)
|
)
|
||||||
def get(self, request, user_id):
|
def get(self, request, username):
|
||||||
user = get_object_or_404(User, uuid=user_id, public_profile=True)
|
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
|
# remove the email address from the response
|
||||||
user.email = None
|
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):
|
class UserMetadataView(APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
1
cdn/.gitignore
vendored
Normal file
1
cdn/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
data/
|
36
cdn/Dockerfile
Normal file
36
cdn/Dockerfile
Normal file
|
@ -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"]
|
3
cdn/README.md
Normal file
3
cdn/README.md
Normal file
|
@ -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.
|
9
cdn/docker-compose.yml
Normal file
9
cdn/docker-compose.yml
Normal file
|
@ -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
|
9
cdn/entrypoint.sh
Normal file
9
cdn/entrypoint.sh
Normal file
|
@ -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;'
|
90
cdn/index.html
Normal file
90
cdn/index.html
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="https://adventurelog.app/adventurelog.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>AdventureLog CDN</title>
|
||||||
|
<link
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: linear-gradient(to right, #1e3c72, #2a5298);
|
||||||
|
color: white;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.hero-container {
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
.attribution-background {
|
||||||
|
background: rgba(42, 71, 105, 0.808);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="hero-container">
|
||||||
|
<div>
|
||||||
|
<h1 class="fw-bold">Welcome to the AdventureLog CDN</h1>
|
||||||
|
<p class="fs-5">
|
||||||
|
This is a content delivery network for the AdventureLog project. You
|
||||||
|
can browse the content by clicking the button below.
|
||||||
|
</p>
|
||||||
|
<a href="/data/" class="btn btn-light btn-lg fw-bold">Browse Content</a>
|
||||||
|
</div>
|
||||||
|
<div class="container mt-5">
|
||||||
|
<h2 class="fw-bold">About AdventureLog</h2>
|
||||||
|
<p class="fs-5">
|
||||||
|
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 <a href="https://seanmorley.com">Sean Morley</a> and is
|
||||||
|
open source. View it on GitHub here:
|
||||||
|
<a href="https://github.com/seanmorley15/AdventureLog">AdventureLog</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- Data Attributions -->
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="card shadow-sm border-0 attribution-background">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="fw-bold text-primary">Data Attributions</h2>
|
||||||
|
<p class="fs-5 text-white">
|
||||||
|
The data provided in this CDN is sourced from the following
|
||||||
|
repositories:
|
||||||
|
</p>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<a
|
||||||
|
href="https://flagpedia.net/"
|
||||||
|
class="text-decoration-none fw-semibold"
|
||||||
|
>
|
||||||
|
🌍 Flagpedia - Flags of the World
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<a
|
||||||
|
href="https://github.com/dr5hn/countries-states-cities-database/"
|
||||||
|
class="text-decoration-none fw-semibold"
|
||||||
|
>
|
||||||
|
🏙️ dr5hn/countries-states-cities-database - Country, Region,
|
||||||
|
and City Data
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
87
cdn/main.py
Normal file
87
cdn/main.py
Normal file
|
@ -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')
|
13
cdn/nginx.conf
Normal file
13
cdn/nginx.conf
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
events {}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location /data/ {
|
||||||
|
root /var/www/html;
|
||||||
|
autoindex on; # Enable directory listing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
cdn/requirements.txt
Normal file
1
cdn/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
osm2geojson==0.2.5
|
|
@ -178,6 +178,8 @@ export default defineConfig({
|
||||||
{ icon: "github", link: "https://github.com/seanmorley15/AdventureLog" },
|
{ icon: "github", link: "https://github.com/seanmorley15/AdventureLog" },
|
||||||
{ icon: "discord", link: "https://discord.gg/wRbQ9Egr8C" },
|
{ icon: "discord", link: "https://discord.gg/wRbQ9Egr8C" },
|
||||||
{ icon: "buymeacoffee", link: "https://buymeacoffee.com/seanmorley15" },
|
{ icon: "buymeacoffee", link: "https://buymeacoffee.com/seanmorley15" },
|
||||||
|
{ icon: "x", link: "https://x.com/AdventureLogApp" },
|
||||||
|
{ icon: "mastodon", link: "https://mastodon.social/@adventurelog" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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.
|
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
|
## Troubleshooting
|
||||||
|
|
||||||
### 404 error when logging in.
|
### 404 error when logging in.
|
||||||
|
|
|
@ -35,10 +35,15 @@ This configuration is done in the [Admin Panel](../../guides/admin_panel.md). Yo
|
||||||
- Settings: can be left blank
|
- 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.
|
- 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
|
#### What it Should Look Like
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
4. Save the configuration.
|
|
||||||
|
|
||||||
Users should now be able to log in to AdventureLog using their GitHub account, and link it to existing accounts.
|
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
"@event-calendar/day-grid": "^3.7.1",
|
"@event-calendar/day-grid": "^3.7.1",
|
||||||
"@event-calendar/time-grid": "^3.7.1",
|
"@event-calendar/time-grid": "^3.7.1",
|
||||||
"@iconify-json/mdi": "^1.1.67",
|
"@iconify-json/mdi": "^1.1.67",
|
||||||
"@sveltejs/adapter-auto": "^3.2.2",
|
|
||||||
"@sveltejs/adapter-node": "^5.2.0",
|
"@sveltejs/adapter-node": "^5.2.0",
|
||||||
"@sveltejs/adapter-vercel": "^5.4.1",
|
"@sveltejs/adapter-vercel": "^5.4.1",
|
||||||
"@sveltejs/kit": "^2.8.3",
|
"@sveltejs/kit": "^2.8.3",
|
||||||
|
@ -47,7 +46,6 @@
|
||||||
"psl": "^1.15.0",
|
"psl": "^1.15.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"svelte-i18n": "^4.0.1",
|
"svelte-i18n": "^4.0.1",
|
||||||
"svelte-maplibre": "^0.9.8",
|
"svelte-maplibre": "^0.9.8"
|
||||||
"tsparticles": "^3.7.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
398
frontend/pnpm-lock.yaml
generated
398
frontend/pnpm-lock.yaml
generated
|
@ -35,9 +35,6 @@ importers:
|
||||||
svelte-maplibre:
|
svelte-maplibre:
|
||||||
specifier: ^0.9.8
|
specifier: ^0.9.8
|
||||||
version: 0.9.8(svelte@4.2.19)
|
version: 0.9.8(svelte@4.2.19)
|
||||||
tsparticles:
|
|
||||||
specifier: ^3.7.1
|
|
||||||
version: 3.7.1
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@event-calendar/core':
|
'@event-calendar/core':
|
||||||
specifier: ^3.7.1
|
specifier: ^3.7.1
|
||||||
|
@ -51,9 +48,6 @@ importers:
|
||||||
'@iconify-json/mdi':
|
'@iconify-json/mdi':
|
||||||
specifier: ^1.1.67
|
specifier: ^1.1.67
|
||||||
version: 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':
|
'@sveltejs/adapter-node':
|
||||||
specifier: ^5.2.0
|
specifier: ^5.2.0
|
||||||
version: 5.2.0(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))
|
version: 5.2.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]
|
cpu: [x64]
|
||||||
os: [win32]
|
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':
|
'@sveltejs/adapter-node@5.2.0':
|
||||||
resolution: {integrity: sha512-HVZoei2078XSyPmvdTHE03VXDUD0ytTvMuMHMQP0j6zX4nPDpCcKrgvU7baEblMeCCMdM/shQvstFxOJPQKlUQ==}
|
resolution: {integrity: sha512-HVZoei2078XSyPmvdTHE03VXDUD0ytTvMuMHMQP0j6zX4nPDpCcKrgvU7baEblMeCCMdM/shQvstFxOJPQKlUQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -785,147 +774,6 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
tailwindcss: '>=3.0.0 || insiders'
|
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':
|
'@types/cookie@0.6.0':
|
||||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||||
|
|
||||||
|
@ -2234,9 +2082,6 @@ packages:
|
||||||
tslib@2.6.3:
|
tslib@2.6.3:
|
||||||
resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
|
resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
|
||||||
|
|
||||||
tsparticles@3.7.1:
|
|
||||||
resolution: {integrity: sha512-NNkOYIo01eHpDuaJxDCGgcLEMZKEJTCN/XPVCLg7VxgEWN19rjXpDnDguISxadS8GSFPws7hpGgbeDDAm3MX+Q==}
|
|
||||||
|
|
||||||
type-detect@4.0.8:
|
type-detect@4.0.8:
|
||||||
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
|
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -2875,11 +2720,6 @@ snapshots:
|
||||||
'@rollup/rollup-win32-x64-msvc@4.31.0':
|
'@rollup/rollup-win32-x64-msvc@4.31.0':
|
||||||
optional: true
|
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)))':
|
'@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:
|
dependencies:
|
||||||
'@rollup/plugin-commonjs': 26.0.1(rollup@4.24.0)
|
'@rollup/plugin-commonjs': 26.0.1(rollup@4.24.0)
|
||||||
|
@ -2946,228 +2786,6 @@ snapshots:
|
||||||
postcss-selector-parser: 6.0.10
|
postcss-selector-parser: 6.0.10
|
||||||
tailwindcss: 3.4.4
|
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/cookie@0.6.0': {}
|
||||||
|
|
||||||
'@types/estree@1.0.6': {}
|
'@types/estree@1.0.6': {}
|
||||||
|
@ -4539,22 +4157,6 @@ snapshots:
|
||||||
|
|
||||||
tslib@2.6.3: {}
|
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-detect@4.0.8: {}
|
||||||
|
|
||||||
type@2.7.3: {}
|
type@2.7.3: {}
|
||||||
|
|
|
@ -18,9 +18,7 @@
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log(res);
|
|
||||||
let data = await res.json();
|
let data = await res.json();
|
||||||
console.log('ACTIVITIES' + data.activities);
|
|
||||||
if (data && data.activities) {
|
if (data && data.activities) {
|
||||||
allActivities = data.activities;
|
allActivities = data.activities;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +1,37 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import type {
|
import type { Adventure, Attachment, Category, Collection } from '$lib/types';
|
||||||
Adventure,
|
|
||||||
Attachment,
|
|
||||||
Category,
|
|
||||||
Collection,
|
|
||||||
OpenStreetMapPlace,
|
|
||||||
Point,
|
|
||||||
ReverseGeocode
|
|
||||||
} from '$lib/types';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { enhance } from '$app/forms';
|
|
||||||
import { addToast } from '$lib/toasts';
|
import { addToast } from '$lib/toasts';
|
||||||
import { deserialize } from '$app/forms';
|
import { deserialize } from '$app/forms';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
export let longitude: number | null = null;
|
|
||||||
export let latitude: number | null = null;
|
|
||||||
export let collection: Collection | null = null;
|
export let collection: Collection | null = null;
|
||||||
|
|
||||||
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let query: string = '';
|
|
||||||
let places: OpenStreetMapPlace[] = [];
|
|
||||||
let images: { id: string; image: string; is_primary: boolean }[] = [];
|
let images: { id: string; image: string; is_primary: boolean }[] = [];
|
||||||
let warningMessage: string = '';
|
let warningMessage: string = '';
|
||||||
let constrainDates: boolean = false;
|
let constrainDates: boolean = false;
|
||||||
|
|
||||||
let categories: Category[] = [];
|
let categories: Category[] = [];
|
||||||
|
|
||||||
|
export let initialLatLng: { lat: number; lng: number } | null = null; // Used to pass the location from the map selection to the modal
|
||||||
|
|
||||||
|
let fileInput: HTMLInputElement;
|
||||||
|
let immichIntegration: boolean = false;
|
||||||
|
|
||||||
import ActivityComplete from './ActivityComplete.svelte';
|
import ActivityComplete from './ActivityComplete.svelte';
|
||||||
import { appVersion } from '$lib/config';
|
|
||||||
import CategoryDropdown from './CategoryDropdown.svelte';
|
import CategoryDropdown from './CategoryDropdown.svelte';
|
||||||
import { findFirstValue } from '$lib';
|
import { findFirstValue } from '$lib';
|
||||||
import MarkdownEditor from './MarkdownEditor.svelte';
|
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||||
import ImmichSelect from './ImmichSelect.svelte';
|
import ImmichSelect from './ImmichSelect.svelte';
|
||||||
|
|
||||||
import Star from '~icons/mdi/star';
|
import Star from '~icons/mdi/star';
|
||||||
import Crown from '~icons/mdi/crown';
|
import Crown from '~icons/mdi/crown';
|
||||||
import AttachmentCard from './AttachmentCard.svelte';
|
import AttachmentCard from './AttachmentCard.svelte';
|
||||||
|
import LocationDropdown from './LocationDropdown.svelte';
|
||||||
|
let modal: HTMLDialogElement;
|
||||||
|
|
||||||
let wikiError: string = '';
|
let wikiError: string = '';
|
||||||
|
|
||||||
let noPlaces: boolean = false;
|
|
||||||
|
|
||||||
let is_custom_location: boolean = false;
|
|
||||||
|
|
||||||
let reverseGeocodePlace: ReverseGeocode | null = null;
|
|
||||||
|
|
||||||
let adventure: Adventure = {
|
let adventure: Adventure = {
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
|
@ -101,38 +86,33 @@
|
||||||
attachments: adventureToEdit?.attachments || []
|
attachments: adventureToEdit?.attachments || []
|
||||||
};
|
};
|
||||||
|
|
||||||
let markers: Point[] = [];
|
onMount(async () => {
|
||||||
|
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||||
|
modal.showModal();
|
||||||
|
let categoryFetch = await fetch('/api/categories/categories');
|
||||||
|
if (categoryFetch.ok) {
|
||||||
|
categories = await categoryFetch.json();
|
||||||
|
} else {
|
||||||
|
addToast('error', $t('adventures.category_fetch_error'));
|
||||||
|
}
|
||||||
|
// Check for Immich Integration
|
||||||
|
let res = await fetch('/api/integrations');
|
||||||
|
if (!res.ok) {
|
||||||
|
addToast('error', $t('immich.integration_fetch_error'));
|
||||||
|
} else {
|
||||||
|
let data = await res.json();
|
||||||
|
if (data.immich) {
|
||||||
|
immichIntegration = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let url: string = '';
|
let url: string = '';
|
||||||
let imageError: string = '';
|
let imageError: string = '';
|
||||||
let wikiImageError: string = '';
|
let wikiImageError: string = '';
|
||||||
|
let triggerMarkVisted: boolean = false;
|
||||||
let old_display_name: string = '';
|
|
||||||
|
|
||||||
images = adventure.images || [];
|
images = adventure.images || [];
|
||||||
|
|
||||||
if (longitude && latitude) {
|
|
||||||
adventure.latitude = latitude;
|
|
||||||
adventure.longitude = longitude;
|
|
||||||
reverseGeocode(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$: {
|
|
||||||
is_custom_location = adventure.location != reverseGeocodePlace?.display_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (adventure.longitude && adventure.latitude) {
|
|
||||||
markers = [];
|
|
||||||
markers = [
|
|
||||||
{
|
|
||||||
lngLat: { lng: adventure.longitude, lat: adventure.latitude },
|
|
||||||
location: adventure.location || '',
|
|
||||||
name: adventure.name,
|
|
||||||
activity_type: ''
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (!adventure.rating) {
|
if (!adventure.rating) {
|
||||||
adventure.rating = NaN;
|
adventure.rating = NaN;
|
||||||
|
@ -179,14 +159,11 @@
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
if (input.files && input.files.length) {
|
if (input.files && input.files.length) {
|
||||||
selectedFile = input.files[0];
|
selectedFile = input.files[0];
|
||||||
console.log('Selected file:', selectedFile);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadAttachment(event: Event) {
|
async function uploadAttachment(event: Event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
console.log('UPLOAD');
|
|
||||||
console.log(selectedFile);
|
|
||||||
|
|
||||||
if (!selectedFile) {
|
if (!selectedFile) {
|
||||||
console.error('No files selected');
|
console.error('No files selected');
|
||||||
|
@ -194,23 +171,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = selectedFile;
|
const file = selectedFile;
|
||||||
console.log(file);
|
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('adventure', adventure.id);
|
formData.append('adventure', adventure.id);
|
||||||
formData.append('name', attachmentName);
|
formData.append('name', attachmentName);
|
||||||
|
|
||||||
console.log(formData);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/adventures?/attachment', {
|
const res = await fetch('/adventures?/attachment', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(res);
|
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const newData = deserialize(await res.text()) as { data: Attachment };
|
const newData = deserialize(await res.text()) as { data: Attachment };
|
||||||
adventure.attachments = [...adventure.attachments, newData.data];
|
adventure.attachments = [...adventure.attachments, newData.data];
|
||||||
|
@ -230,11 +202,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearMap() {
|
|
||||||
console.log('CLEAR');
|
|
||||||
markers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
let imageSearch: string = adventure.name || '';
|
let imageSearch: string = adventure.name || '';
|
||||||
|
|
||||||
async function removeImage(id: string) {
|
async function removeImage(id: string) {
|
||||||
|
@ -244,7 +211,6 @@
|
||||||
if (res.status === 204) {
|
if (res.status === 204) {
|
||||||
images = images.filter((image) => image.id !== id);
|
images = images.filter((image) => image.id !== id);
|
||||||
adventure.images = images;
|
adventure.images = images;
|
||||||
console.log(images);
|
|
||||||
addToast('success', $t('adventures.image_removed_success'));
|
addToast('success', $t('adventures.image_removed_success'));
|
||||||
} else {
|
} else {
|
||||||
addToast('error', $t('adventures.image_removed_error'));
|
addToast('error', $t('adventures.image_removed_error'));
|
||||||
|
@ -258,51 +224,6 @@
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
let willBeMarkedVisited: boolean = false;
|
|
||||||
|
|
||||||
$: {
|
|
||||||
willBeMarkedVisited = false; // Reset before evaluating
|
|
||||||
|
|
||||||
const today = new Date(); // Cache today's date to avoid redundant calculations
|
|
||||||
|
|
||||||
for (const visit of adventure.visits) {
|
|
||||||
const startDate = new Date(visit.start_date);
|
|
||||||
const endDate = visit.end_date ? new Date(visit.end_date) : null;
|
|
||||||
|
|
||||||
// If the visit has both a start date and an end date, check if it started by today
|
|
||||||
if (startDate && endDate && startDate <= today) {
|
|
||||||
willBeMarkedVisited = true;
|
|
||||||
break; // Exit the loop since we've determined the result
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the visit has a start date but no end date, check if it started by today
|
|
||||||
if (startDate && !endDate && startDate <= today) {
|
|
||||||
willBeMarkedVisited = true;
|
|
||||||
break; // Exit the loop since we've determined the result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('WMBV:', willBeMarkedVisited);
|
|
||||||
}
|
|
||||||
|
|
||||||
let previousCoords: { lat: number; lng: number } | null = null;
|
|
||||||
|
|
||||||
$: if (markers.length > 0) {
|
|
||||||
const newLat = Math.round(markers[0].lngLat.lat * 1e6) / 1e6;
|
|
||||||
const newLng = Math.round(markers[0].lngLat.lng * 1e6) / 1e6;
|
|
||||||
|
|
||||||
if (!previousCoords || previousCoords.lat !== newLat || previousCoords.lng !== newLng) {
|
|
||||||
adventure.latitude = newLat;
|
|
||||||
adventure.longitude = newLng;
|
|
||||||
previousCoords = { lat: newLat, lng: newLng };
|
|
||||||
reverseGeocode();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!adventure.name) {
|
|
||||||
adventure.name = markers[0].name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function makePrimaryImage(image_id: string) {
|
async function makePrimaryImage(image_id: string) {
|
||||||
let res = await fetch(`/api/images/${image_id}/toggle_primary`, {
|
let res = await fetch(`/api/images/${image_id}/toggle_primary`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
|
@ -342,9 +263,7 @@
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let newData = deserialize(await res.text()) as { data: { id: string; image: string } };
|
let newData = deserialize(await res.text()) as { data: { id: string; image: string } };
|
||||||
console.log(newData);
|
|
||||||
let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false };
|
let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false };
|
||||||
console.log(newImage);
|
|
||||||
images = [...images, newImage];
|
images = [...images, newImage];
|
||||||
adventure.images = images;
|
adventure.images = images;
|
||||||
addToast('success', $t('adventures.image_upload_success'));
|
addToast('success', $t('adventures.image_upload_success'));
|
||||||
|
@ -395,9 +314,7 @@
|
||||||
});
|
});
|
||||||
if (res2.ok) {
|
if (res2.ok) {
|
||||||
let newData = deserialize(await res2.text()) as { data: { id: string; image: string } };
|
let newData = deserialize(await res2.text()) as { data: { id: string; image: string } };
|
||||||
console.log(newData);
|
|
||||||
let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false };
|
let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false };
|
||||||
console.log(newImage);
|
|
||||||
images = [...images, newImage];
|
images = [...images, newImage];
|
||||||
adventure.images = images;
|
adventure.images = images;
|
||||||
addToast('success', $t('adventures.image_upload_success'));
|
addToast('success', $t('adventures.image_upload_success'));
|
||||||
|
@ -407,28 +324,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function geocode(e: Event | null) {
|
|
||||||
if (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
if (!query) {
|
|
||||||
alert($t('adventures.no_location'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let res = await fetch(`https://nominatim.openstreetmap.org/search?q=${query}&format=jsonv2`, {
|
|
||||||
headers: {
|
|
||||||
'User-Agent': `AdventureLog / ${appVersion} `
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log(res);
|
|
||||||
let data = (await res.json()) as OpenStreetMapPlace[];
|
|
||||||
places = data;
|
|
||||||
if (data.length === 0) {
|
|
||||||
noPlaces = true;
|
|
||||||
} else {
|
|
||||||
noPlaces = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_start_date: string = '';
|
let new_start_date: string = '';
|
||||||
let new_end_date: string = '';
|
let new_end_date: string = '';
|
||||||
|
@ -459,93 +354,6 @@
|
||||||
new_notes = '';
|
new_notes = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function markVisited() {
|
|
||||||
console.log(reverseGeocodePlace);
|
|
||||||
if (reverseGeocodePlace) {
|
|
||||||
if (!reverseGeocodePlace.region_visited && reverseGeocodePlace.region_id) {
|
|
||||||
let region_res = await fetch(`/api/visitedregion`, {
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ region: reverseGeocodePlace.region_id })
|
|
||||||
});
|
|
||||||
if (region_res.ok) {
|
|
||||||
reverseGeocodePlace.region_visited = true;
|
|
||||||
addToast('success', `Visit to ${reverseGeocodePlace.region} marked`);
|
|
||||||
} else {
|
|
||||||
addToast('error', `Failed to mark visit to ${reverseGeocodePlace.region}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!reverseGeocodePlace.city_visited && reverseGeocodePlace.city_id != null) {
|
|
||||||
let city_res = await fetch(`/api/visitedcity`, {
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ city: reverseGeocodePlace.city_id })
|
|
||||||
});
|
|
||||||
if (city_res.ok) {
|
|
||||||
reverseGeocodePlace.city_visited = true;
|
|
||||||
addToast('success', `Visit to ${reverseGeocodePlace.city} marked`);
|
|
||||||
} else {
|
|
||||||
addToast('error', `Failed to mark visit to ${reverseGeocodePlace.city}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reverseGeocode(force_update: boolean = false) {
|
|
||||||
let res = await fetch(
|
|
||||||
`/api/reverse-geocode/reverse_geocode/?lat=${adventure.latitude}&lon=${adventure.longitude}`
|
|
||||||
);
|
|
||||||
let data = await res.json();
|
|
||||||
if (data.error) {
|
|
||||||
console.log(data.error);
|
|
||||||
reverseGeocodePlace = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
reverseGeocodePlace = data;
|
|
||||||
|
|
||||||
console.log(reverseGeocodePlace);
|
|
||||||
console.log(is_custom_location);
|
|
||||||
|
|
||||||
if (
|
|
||||||
reverseGeocodePlace &&
|
|
||||||
reverseGeocodePlace.display_name &&
|
|
||||||
(!is_custom_location || force_update)
|
|
||||||
) {
|
|
||||||
old_display_name = reverseGeocodePlace.display_name;
|
|
||||||
adventure.location = reverseGeocodePlace.display_name;
|
|
||||||
}
|
|
||||||
console.log(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileInput: HTMLInputElement;
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
let modal: HTMLDialogElement;
|
|
||||||
|
|
||||||
let immichIntegration: boolean = false;
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
|
||||||
modal.showModal();
|
|
||||||
console.log('open');
|
|
||||||
let categoryFetch = await fetch('/api/categories/categories');
|
|
||||||
if (categoryFetch.ok) {
|
|
||||||
categories = await categoryFetch.json();
|
|
||||||
} else {
|
|
||||||
addToast('error', $t('adventures.category_fetch_error'));
|
|
||||||
}
|
|
||||||
// Check for Immich Integration
|
|
||||||
let res = await fetch('/api/integrations');
|
|
||||||
if (!res.ok) {
|
|
||||||
addToast('error', $t('immich.integration_fetch_error'));
|
|
||||||
} else {
|
|
||||||
let data = await res.json();
|
|
||||||
if (data.immich) {
|
|
||||||
immichIntegration = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
dispatch('close');
|
dispatch('close');
|
||||||
}
|
}
|
||||||
|
@ -567,42 +375,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addMarker(e: CustomEvent<any>) {
|
|
||||||
markers = [];
|
|
||||||
markers = [
|
|
||||||
...markers,
|
|
||||||
{
|
|
||||||
lngLat: e.detail.lngLat,
|
|
||||||
name: '',
|
|
||||||
location: '',
|
|
||||||
activity_type: ''
|
|
||||||
}
|
|
||||||
];
|
|
||||||
console.log(markers);
|
|
||||||
}
|
|
||||||
|
|
||||||
function imageSubmit() {
|
|
||||||
return async ({ result }: any) => {
|
|
||||||
if (result.type === 'success') {
|
|
||||||
if (result.data.id && result.data.image) {
|
|
||||||
adventure.images = [...adventure.images, result.data];
|
|
||||||
images = [...images, result.data];
|
|
||||||
addToast('success', $t('adventures.image_upload_success'));
|
|
||||||
|
|
||||||
fileInput.value = '';
|
|
||||||
console.log(adventure);
|
|
||||||
} else {
|
|
||||||
addToast('error', result.data.error || $t('adventures.image_upload_error'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit(event: Event) {
|
async function handleSubmit(event: Event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
console.log(adventure);
|
triggerMarkVisted = true;
|
||||||
|
|
||||||
if (adventure.id === '') {
|
if (adventure.id === '') {
|
||||||
console.log(categories);
|
|
||||||
if (adventure.category?.display_name == '') {
|
if (adventure.category?.display_name == '') {
|
||||||
if (categories.some((category) => category.name === 'general')) {
|
if (categories.some((category) => category.name === 'general')) {
|
||||||
adventure.category = categories.find(
|
adventure.category = categories.find(
|
||||||
|
@ -655,12 +432,6 @@
|
||||||
addToast('error', $t('adventures.adventure_update_error'));
|
addToast('error', $t('adventures.adventure_update_error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
adventure.is_visited &&
|
|
||||||
(!reverseGeocodePlace?.region_visited || !reverseGeocodePlace?.city_visited)
|
|
||||||
) {
|
|
||||||
markVisited();
|
|
||||||
}
|
|
||||||
imageSearch = adventure.name;
|
imageSearch = adventure.name;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -811,150 +582,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="collapse collapse-plus bg-base-200 mb-4">
|
<LocationDropdown bind:item={adventure} bind:triggerMarkVisted {initialLatLng} />
|
||||||
<input type="checkbox" />
|
|
||||||
<div class="collapse-title text-xl font-medium">
|
|
||||||
{$t('adventures.location_information')}
|
|
||||||
</div>
|
|
||||||
<div class="collapse-content">
|
|
||||||
<!-- <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> -->
|
|
||||||
<div>
|
|
||||||
<label for="latitude">{$t('adventures.location')}</label><br />
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="location"
|
|
||||||
name="location"
|
|
||||||
bind:value={adventure.location}
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
/>
|
|
||||||
{#if is_custom_location}
|
|
||||||
<button
|
|
||||||
class="btn btn-primary ml-2"
|
|
||||||
type="button"
|
|
||||||
on:click={() => (adventure.location = reverseGeocodePlace?.display_name)}
|
|
||||||
>{$t('adventures.set_to_pin')}</button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<form on:submit={geocode} class="mt-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={$t('adventures.search_for_location')}
|
|
||||||
class="input input-bordered w-full max-w-xs mb-2"
|
|
||||||
id="search"
|
|
||||||
name="search"
|
|
||||||
bind:value={query}
|
|
||||||
/>
|
|
||||||
<button class="btn btn-neutral -mt-1" type="submit">{$t('navbar.search')}</button>
|
|
||||||
<button class="btn btn-neutral -mt-1" type="button" on:click={clearMap}
|
|
||||||
>{$t('adventures.clear_map')}</button
|
|
||||||
>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{#if places.length > 0}
|
|
||||||
<div class="mt-4 max-w-full">
|
|
||||||
<h3 class="font-bold text-lg mb-4">{$t('adventures.search_results')}</h3>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap">
|
|
||||||
{#each places as place}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-neutral mb-2 mr-2 max-w-full break-words whitespace-normal text-left"
|
|
||||||
on:click={() => {
|
|
||||||
markers = [
|
|
||||||
{
|
|
||||||
lngLat: { lng: Number(place.lon), lat: Number(place.lat) },
|
|
||||||
location: place.display_name,
|
|
||||||
name: place.name,
|
|
||||||
activity_type: place.type
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{place.display_name}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if noPlaces}
|
|
||||||
<p class="text-error text-lg">{$t('adventures.no_results')}</p>
|
|
||||||
{/if}
|
|
||||||
<!-- </div> -->
|
|
||||||
<div>
|
|
||||||
<MapLibre
|
|
||||||
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
|
||||||
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full rounded-lg"
|
|
||||||
standardControls
|
|
||||||
>
|
|
||||||
<!-- MapEvents gives you access to map events even from other components inside the map,
|
|
||||||
where you might not have access to the top-level `MapLibre` component. In this case
|
|
||||||
it would also work to just use on:click on the MapLibre component itself. -->
|
|
||||||
<MapEvents on:click={addMarker} />
|
|
||||||
|
|
||||||
{#each markers as marker}
|
|
||||||
<DefaultMarker lngLat={marker.lngLat} />
|
|
||||||
{/each}
|
|
||||||
</MapLibre>
|
|
||||||
{#if reverseGeocodePlace}
|
|
||||||
<div class="mt-2">
|
|
||||||
<p>
|
|
||||||
{reverseGeocodePlace.city
|
|
||||||
? reverseGeocodePlace.city + ', '
|
|
||||||
: ''}{reverseGeocodePlace.region},
|
|
||||||
{reverseGeocodePlace.country}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{reverseGeocodePlace.region}:
|
|
||||||
{reverseGeocodePlace.region_visited
|
|
||||||
? $t('adventures.visited')
|
|
||||||
: $t('adventures.not_visited')}
|
|
||||||
</p>
|
|
||||||
{#if reverseGeocodePlace.city}
|
|
||||||
<p>
|
|
||||||
{reverseGeocodePlace.city}:
|
|
||||||
{reverseGeocodePlace.city_visited
|
|
||||||
? $t('adventures.visited')
|
|
||||||
: $t('adventures.not_visited')}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if !reverseGeocodePlace.region_visited || (!reverseGeocodePlace.city_visited && !willBeMarkedVisited)}
|
|
||||||
<button type="button" class="btn btn-neutral" on:click={markVisited}>
|
|
||||||
{$t('adventures.mark_visited')}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
{#if (willBeMarkedVisited && !reverseGeocodePlace.region_visited && reverseGeocodePlace.region_id) || (!reverseGeocodePlace.city_visited && willBeMarkedVisited && reverseGeocodePlace.city_id)}
|
|
||||||
<div role="alert" class="alert alert-info mt-2">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
class="h-6 w-6 shrink-0 stroke-current"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<span
|
|
||||||
>{reverseGeocodePlace.city
|
|
||||||
? reverseGeocodePlace.city + ', '
|
|
||||||
: ''}{reverseGeocodePlace.region},
|
|
||||||
{reverseGeocodePlace.country}
|
|
||||||
{$t('adventures.will_be_marked')}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="collapse collapse-plus bg-base-200 mb-4 overflow-visible">
|
<div class="collapse collapse-plus bg-base-200 mb-4 overflow-visible">
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
|
@ -1257,6 +885,7 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
||||||
|
|
||||||
{#if immichIntegration}
|
{#if immichIntegration}
|
||||||
<ImmichSelect
|
<ImmichSelect
|
||||||
|
{adventure}
|
||||||
on:fetchImage={(e) => {
|
on:fetchImage={(e) => {
|
||||||
url = e.detail;
|
url = e.detail;
|
||||||
fetchImage();
|
fetchImage();
|
||||||
|
@ -1314,7 +943,7 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if adventure.is_public && adventure.id}
|
{#if adventure.is_public && adventure.id}
|
||||||
<div class="bg-neutral p-4 mt-2 rounded-md shadow-sm">
|
<div class="bg-neutral p-4 mt-2 rounded-md shadow-sm text-neutral-content">
|
||||||
<p class=" font-semibold">{$t('adventures.share_adventure')}</p>
|
<p class=" font-semibold">{$t('adventures.share_adventure')}</p>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<p class="text-card-foreground font-mono">
|
<p class="text-card-foreground font-mono">
|
||||||
|
|
|
@ -34,9 +34,14 @@
|
||||||
? `${user.first_name} ${user.last_name}`
|
? `${user.first_name} ${user.last_name}`
|
||||||
: user.username}
|
: user.username}
|
||||||
</p>
|
</p>
|
||||||
<li><button on:click={() => goto('/profile')}>{$t('navbar.profile')}</button></li>
|
<li>
|
||||||
|
<button on:click={() => goto(`/profile/${user.username}`)}>{$t('navbar.profile')}</button>
|
||||||
|
</li>
|
||||||
<li><button on:click={() => goto('/adventures')}>{$t('navbar.my_adventures')}</button></li>
|
<li><button on:click={() => goto('/adventures')}>{$t('navbar.my_adventures')}</button></li>
|
||||||
<li><button on:click={() => goto('/shared')}>{$t('navbar.shared_with_me')}</button></li>
|
<li><button on:click={() => goto('/shared')}>{$t('navbar.shared_with_me')}</button></li>
|
||||||
|
{#if user.is_staff}
|
||||||
|
<li><button on:click={() => goto('/admin')}>{$t('navbar.admin_panel')}</button></li>
|
||||||
|
{/if}
|
||||||
<li><button on:click={() => goto('/settings')}>{$t('navbar.settings')}</button></li>
|
<li><button on:click={() => goto('/settings')}>{$t('navbar.settings')}</button></li>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<li><button formaction="/?/logout">{$t('navbar.logout')}</button></li>
|
<li><button formaction="/?/logout">{$t('navbar.logout')}</button></li>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { continentCodeToString, getFlag } from '$lib';
|
|
||||||
import type { Country } from '$lib/types';
|
import type { Country } from '$lib/types';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
const dispatch = createEventDispatcher();
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
import MapMarkerStar from '~icons/mdi/map-marker-star';
|
import MapMarkerStar from '~icons/mdi/map-marker-star';
|
||||||
|
|
||||||
|
@ -37,15 +36,15 @@
|
||||||
Visited {country.num_visits} Region{country.num_visits > 1 ? 's' : ''}
|
Visited {country.num_visits} Region{country.num_visits > 1 ? 's' : ''}
|
||||||
</div>
|
</div>
|
||||||
{:else if country.num_visits > 0 && country.num_visits === country.num_regions}
|
{:else if country.num_visits > 0 && country.num_visits === country.num_regions}
|
||||||
<div class="badge badge-success">Completed</div>
|
<div class="badge badge-success">{$t('adventures.visited')}</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="badge badge-error">Not Visited</div>
|
<div class="badge badge-error">{$t('adventures.not_visited')}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions justify-end">
|
<div class="card-actions justify-end">
|
||||||
<!-- <button class="btn btn-info" on:click={moreInfo}>More Info</button> -->
|
<!-- <button class="btn btn-info" on:click={moreInfo}>More Info</button> -->
|
||||||
<button class="btn btn-primary" on:click={nav}>Open</button>
|
<button class="btn btn-primary" on:click={nav}>{$t('notes.open')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,32 +1,81 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let immichSearchValue: string = '';
|
|
||||||
let searchOrSelect: string = 'search';
|
|
||||||
let immichError: string = '';
|
|
||||||
let immichNext: string = '';
|
|
||||||
let immichPage: number = 1;
|
|
||||||
|
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import ImmichLogo from '$lib/assets/immich.svg';
|
||||||
|
import type { Adventure, ImmichAlbum } from '$lib/types';
|
||||||
|
import { debounce } from '$lib';
|
||||||
|
|
||||||
|
let immichImages: any[] = [];
|
||||||
|
let immichSearchValue: string = '';
|
||||||
|
let searchCategory: 'search' | 'date' | 'album' = 'date';
|
||||||
|
let immichError: string = '';
|
||||||
|
let immichNextURL: string = '';
|
||||||
|
let loading = false;
|
||||||
|
|
||||||
|
export let adventure: Adventure | null = null;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let albums: ImmichAlbum[] = [];
|
let albums: ImmichAlbum[] = [];
|
||||||
let currentAlbum: string = '';
|
let currentAlbum: string = '';
|
||||||
|
|
||||||
|
let selectedDate: string =
|
||||||
|
(adventure as Adventure | null)?.visits
|
||||||
|
.map((v) => new Date(v.end_date || v.start_date))
|
||||||
|
.sort((a, b) => +b - +a)[0]
|
||||||
|
?.toISOString()
|
||||||
|
?.split('T')[0] || '';
|
||||||
|
if (!selectedDate) {
|
||||||
|
selectedDate = new Date().toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (currentAlbum) {
|
if (currentAlbum) {
|
||||||
immichImages = [];
|
immichImages = [];
|
||||||
fetchAlbumAssets(currentAlbum);
|
fetchAlbumAssets(currentAlbum);
|
||||||
|
} else if (searchCategory === 'date' && selectedDate) {
|
||||||
|
searchImmich();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMoreImmich() {
|
||||||
|
// The next URL returned by our API is a absolute url to API, but we need to use the relative path, to use the frontend api proxy.
|
||||||
|
const url = new URL(immichNextURL);
|
||||||
|
immichNextURL = url.pathname + url.search;
|
||||||
|
return fetchAssets(immichNextURL, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAssets(url: string, usingNext = false) {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
let res = await fetch(url);
|
||||||
|
immichError = '';
|
||||||
|
if (!res.ok) {
|
||||||
|
let data = await res.json();
|
||||||
|
let errorMessage = data.message;
|
||||||
|
console.error('Error in handling fetchAsstes', errorMessage);
|
||||||
|
immichError = $t(data.code);
|
||||||
} else {
|
} else {
|
||||||
immichImages = [];
|
let data = await res.json();
|
||||||
|
if (data.results && data.results.length > 0) {
|
||||||
|
if (usingNext) {
|
||||||
|
immichImages = [...immichImages, ...data.results];
|
||||||
|
} else {
|
||||||
|
immichImages = data.results;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
immichError = $t('immich.no_items_found');
|
||||||
|
}
|
||||||
|
|
||||||
|
immichNextURL = data.next || '';
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAlbumAssets(album_id: string) {
|
async function fetchAlbumAssets(album_id: string) {
|
||||||
let res = await fetch(`/api/integrations/immich/albums/${album_id}`);
|
return fetchAssets(`/api/integrations/immich/albums/${album_id}`);
|
||||||
if (res.ok) {
|
|
||||||
let data = await res.json();
|
|
||||||
immichNext = '';
|
|
||||||
immichImages = data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
@ -37,66 +86,23 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let immichImages: any[] = [];
|
function buildQueryParams() {
|
||||||
import { t } from 'svelte-i18n';
|
let params = new URLSearchParams();
|
||||||
import ImmichLogo from '$lib/assets/immich.svg';
|
if (immichSearchValue && searchCategory === 'search') {
|
||||||
import type { ImmichAlbum } from '$lib/types';
|
params.append('query', immichSearchValue);
|
||||||
|
} else if (selectedDate && searchCategory === 'date') {
|
||||||
async function searchImmich() {
|
params.append('date', selectedDate);
|
||||||
let res = await fetch(`/api/integrations/immich/search/?query=${immichSearchValue}`);
|
|
||||||
if (!res.ok) {
|
|
||||||
let data = await res.json();
|
|
||||||
let errorMessage = data.message;
|
|
||||||
console.log(errorMessage);
|
|
||||||
immichError = $t(data.code);
|
|
||||||
} else {
|
|
||||||
let data = await res.json();
|
|
||||||
console.log(data);
|
|
||||||
immichError = '';
|
|
||||||
if (data.results && data.results.length > 0) {
|
|
||||||
immichImages = data.results;
|
|
||||||
} else {
|
|
||||||
immichError = $t('immich.no_items_found');
|
|
||||||
}
|
|
||||||
if (data.next) {
|
|
||||||
immichNext =
|
|
||||||
'/api/integrations/immich/search?query=' +
|
|
||||||
immichSearchValue +
|
|
||||||
'&page=' +
|
|
||||||
(immichPage + 1);
|
|
||||||
} else {
|
|
||||||
immichNext = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return params.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMoreImmich() {
|
const searchImmich = debounce(() => {
|
||||||
let res = await fetch(immichNext);
|
_searchImmich();
|
||||||
if (!res.ok) {
|
}, 500); // Debounce the search function to avoid multiple requests on every key press
|
||||||
let data = await res.json();
|
|
||||||
let errorMessage = data.message;
|
async function _searchImmich() {
|
||||||
console.log(errorMessage);
|
immichImages = [];
|
||||||
immichError = $t(data.code);
|
return fetchAssets(`/api/integrations/immich/search/?${buildQueryParams()}`);
|
||||||
} else {
|
|
||||||
let data = await res.json();
|
|
||||||
console.log(data);
|
|
||||||
immichError = '';
|
|
||||||
if (data.results && data.results.length > 0) {
|
|
||||||
immichImages = [...immichImages, ...data.results];
|
|
||||||
} else {
|
|
||||||
immichError = $t('immich.no_items_found');
|
|
||||||
}
|
|
||||||
if (data.next) {
|
|
||||||
immichNext =
|
|
||||||
'/api/integrations/immich/search?query=' +
|
|
||||||
immichSearchValue +
|
|
||||||
'&page=' +
|
|
||||||
(immichPage + 1);
|
|
||||||
immichPage++;
|
|
||||||
} else {
|
|
||||||
immichNext = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -111,20 +117,27 @@
|
||||||
on:click={() => (currentAlbum = '')}
|
on:click={() => (currentAlbum = '')}
|
||||||
type="radio"
|
type="radio"
|
||||||
class="join-item btn"
|
class="join-item btn"
|
||||||
bind:group={searchOrSelect}
|
bind:group={searchCategory}
|
||||||
value="search"
|
value="search"
|
||||||
aria-label="Search"
|
aria-label="Search"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
class="join-item btn"
|
class="join-item btn"
|
||||||
bind:group={searchOrSelect}
|
bind:group={searchCategory}
|
||||||
value="select"
|
value="date"
|
||||||
|
aria-label="Show by date"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="join-item btn"
|
||||||
|
bind:group={searchCategory}
|
||||||
|
value="album"
|
||||||
aria-label="Select Album"
|
aria-label="Select Album"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{#if searchOrSelect === 'search'}
|
{#if searchCategory === 'search'}
|
||||||
<form on:submit|preventDefault={searchImmich}>
|
<form on:submit|preventDefault={searchImmich}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -134,7 +147,13 @@
|
||||||
/>
|
/>
|
||||||
<button type="submit" class="btn btn-neutral mt-2">Search</button>
|
<button type="submit" class="btn btn-neutral mt-2">Search</button>
|
||||||
</form>
|
</form>
|
||||||
{:else}
|
{:else if searchCategory === 'date'}
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
bind:value={selectedDate}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-2"
|
||||||
|
/>
|
||||||
|
{:else if searchCategory === 'album'}
|
||||||
<select class="select select-bordered w-full max-w-xs mt-2" bind:value={currentAlbum}>
|
<select class="select select-bordered w-full max-w-xs mt-2" bind:value={currentAlbum}>
|
||||||
<option value="" disabled selected>Select an Album</option>
|
<option value="" disabled selected>Select an Album</option>
|
||||||
{#each albums as album}
|
{#each albums as album}
|
||||||
|
@ -147,14 +166,25 @@
|
||||||
|
|
||||||
<p class="text-red-500">{immichError}</p>
|
<p class="text-red-500">{immichError}</p>
|
||||||
<div class="flex flex-wrap gap-4 mr-4 mt-2">
|
<div class="flex flex-wrap gap-4 mr-4 mt-2">
|
||||||
|
{#if loading}
|
||||||
|
<div
|
||||||
|
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[100] w-24 h-24"
|
||||||
|
>
|
||||||
|
<span class="loading loading-spinner w-24 h-24"></span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#each immichImages as image}
|
{#each immichImages as image}
|
||||||
<div class="flex flex-col items-center gap-2">
|
<div class="flex flex-col items-center gap-2" class:blur-sm={loading}>
|
||||||
<!-- svelte-ignore a11y-img-redundant-alt -->
|
<!-- svelte-ignore a11y-img-redundant-alt -->
|
||||||
<img
|
<img
|
||||||
src={`/immich/${image.id}`}
|
src={`/immich/${image.id}`}
|
||||||
alt="Image from Immich"
|
alt="Image from Immich"
|
||||||
class="h-24 w-24 object-cover rounded-md"
|
class="h-24 w-24 object-cover rounded-md"
|
||||||
/>
|
/>
|
||||||
|
<h4>
|
||||||
|
{image.fileCreatedAt?.split('T')[0] || 'Unknown'}
|
||||||
|
</h4>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary"
|
||||||
|
@ -168,7 +198,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{#if immichNext}
|
{#if immichNextURL}
|
||||||
<button class="btn btn-neutral" on:click={loadMoreImmich}>{$t('immich.load_more')}</button>
|
<button class="btn btn-neutral" on:click={loadMoreImmich}>{$t('immich.load_more')}</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
354
frontend/src/lib/components/LocationDropdown.svelte
Normal file
354
frontend/src/lib/components/LocationDropdown.svelte
Normal file
|
@ -0,0 +1,354 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { appVersion } from '$lib/config';
|
||||||
|
import { addToast } from '$lib/toasts';
|
||||||
|
import type { Adventure, Lodging, OpenStreetMapPlace, Point, ReverseGeocode } from '$lib/types';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
|
||||||
|
|
||||||
|
export let item: Adventure | Lodging;
|
||||||
|
export let triggerMarkVisted: boolean = false;
|
||||||
|
|
||||||
|
export let initialLatLng: { lat: number; lng: number } | null = null; // Used to pass the location from the map selection to the modal
|
||||||
|
|
||||||
|
let reverseGeocodePlace: ReverseGeocode | null = null;
|
||||||
|
let markers: Point[] = [];
|
||||||
|
|
||||||
|
let query: string = '';
|
||||||
|
let is_custom_location: boolean = false;
|
||||||
|
let willBeMarkedVisited: boolean = false;
|
||||||
|
let previousCoords: { lat: number; lng: number } | null = null;
|
||||||
|
let old_display_name: string = '';
|
||||||
|
let places: OpenStreetMapPlace[] = [];
|
||||||
|
let noPlaces: boolean = false;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (initialLatLng) {
|
||||||
|
markers = [
|
||||||
|
{
|
||||||
|
lngLat: { lng: initialLatLng.lng, lat: initialLatLng.lat },
|
||||||
|
name: '',
|
||||||
|
location: '',
|
||||||
|
activity_type: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
item.latitude = initialLatLng.lat;
|
||||||
|
item.longitude = initialLatLng.lng;
|
||||||
|
reverseGeocode();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$: if (markers.length > 0) {
|
||||||
|
const newLat = Math.round(markers[0].lngLat.lat * 1e6) / 1e6;
|
||||||
|
const newLng = Math.round(markers[0].lngLat.lng * 1e6) / 1e6;
|
||||||
|
|
||||||
|
if (!previousCoords || previousCoords.lat !== newLat || previousCoords.lng !== newLng) {
|
||||||
|
item.latitude = newLat;
|
||||||
|
item.longitude = newLng;
|
||||||
|
previousCoords = { lat: newLat, lng: newLng };
|
||||||
|
reverseGeocode();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.name) {
|
||||||
|
item.name = markers[0].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (triggerMarkVisted && willBeMarkedVisited) {
|
||||||
|
markVisited();
|
||||||
|
triggerMarkVisted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
is_custom_location = Boolean(
|
||||||
|
item.location != reverseGeocodePlace?.display_name && item.location
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.longitude && item.latitude) {
|
||||||
|
markers = [];
|
||||||
|
markers = [
|
||||||
|
{
|
||||||
|
lngLat: { lng: item.longitude, lat: item.latitude },
|
||||||
|
location: item.location || '',
|
||||||
|
name: item.name,
|
||||||
|
activity_type: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if ('visits' in item) {
|
||||||
|
willBeMarkedVisited = false; // Reset before evaluating
|
||||||
|
|
||||||
|
const today = new Date(); // Cache today's date to avoid redundant calculations
|
||||||
|
|
||||||
|
for (const visit of item.visits) {
|
||||||
|
const startDate = new Date(visit.start_date);
|
||||||
|
const endDate = visit.end_date ? new Date(visit.end_date) : null;
|
||||||
|
|
||||||
|
// If the visit has both a start date and an end date, check if it started by today
|
||||||
|
if (startDate && endDate && startDate <= today) {
|
||||||
|
willBeMarkedVisited = true;
|
||||||
|
break; // Exit the loop since we've determined the result
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the visit has a start date but no end date, check if it started by today
|
||||||
|
if (startDate && !endDate && startDate <= today) {
|
||||||
|
willBeMarkedVisited = true;
|
||||||
|
break; // Exit the loop since we've determined the result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markVisited() {
|
||||||
|
console.log(reverseGeocodePlace);
|
||||||
|
if (reverseGeocodePlace) {
|
||||||
|
if (!reverseGeocodePlace.region_visited && reverseGeocodePlace.region_id) {
|
||||||
|
let region_res = await fetch(`/api/visitedregion`, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ region: reverseGeocodePlace.region_id })
|
||||||
|
});
|
||||||
|
if (region_res.ok) {
|
||||||
|
reverseGeocodePlace.region_visited = true;
|
||||||
|
addToast('success', `Visit to ${reverseGeocodePlace.region} marked`);
|
||||||
|
} else {
|
||||||
|
addToast('error', `Failed to mark visit to ${reverseGeocodePlace.region}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!reverseGeocodePlace.city_visited && reverseGeocodePlace.city_id != null) {
|
||||||
|
let city_res = await fetch(`/api/visitedcity`, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ city: reverseGeocodePlace.city_id })
|
||||||
|
});
|
||||||
|
if (city_res.ok) {
|
||||||
|
reverseGeocodePlace.city_visited = true;
|
||||||
|
addToast('success', `Visit to ${reverseGeocodePlace.city} marked`);
|
||||||
|
} else {
|
||||||
|
addToast('error', `Failed to mark visit to ${reverseGeocodePlace.city}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addMarker(e: CustomEvent<any>) {
|
||||||
|
markers = [];
|
||||||
|
markers = [
|
||||||
|
...markers,
|
||||||
|
{
|
||||||
|
lngLat: e.detail.lngLat,
|
||||||
|
name: '',
|
||||||
|
location: '',
|
||||||
|
activity_type: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
console.log(markers);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function geocode(e: Event | null) {
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
if (!query) {
|
||||||
|
alert($t('adventures.no_location'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let res = await fetch(`https://nominatim.openstreetmap.org/search?q=${query}&format=jsonv2`, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': `AdventureLog / ${appVersion} `
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(res);
|
||||||
|
let data = (await res.json()) as OpenStreetMapPlace[];
|
||||||
|
places = data;
|
||||||
|
if (data.length === 0) {
|
||||||
|
noPlaces = true;
|
||||||
|
} else {
|
||||||
|
noPlaces = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reverseGeocode(force_update: boolean = false) {
|
||||||
|
let res = await fetch(
|
||||||
|
`/api/reverse-geocode/reverse_geocode/?lat=${item.latitude}&lon=${item.longitude}`
|
||||||
|
);
|
||||||
|
let data = await res.json();
|
||||||
|
if (data.error) {
|
||||||
|
console.log(data.error);
|
||||||
|
reverseGeocodePlace = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reverseGeocodePlace = data;
|
||||||
|
|
||||||
|
console.log(reverseGeocodePlace);
|
||||||
|
console.log(is_custom_location);
|
||||||
|
|
||||||
|
if (
|
||||||
|
reverseGeocodePlace &&
|
||||||
|
reverseGeocodePlace.display_name &&
|
||||||
|
(!is_custom_location || force_update)
|
||||||
|
) {
|
||||||
|
old_display_name = reverseGeocodePlace.display_name;
|
||||||
|
item.location = reverseGeocodePlace.display_name;
|
||||||
|
if (reverseGeocodePlace.location_name) {
|
||||||
|
item.name = reverseGeocodePlace.location_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMap() {
|
||||||
|
console.log('CLEAR');
|
||||||
|
markers = [];
|
||||||
|
item.latitude = null;
|
||||||
|
item.longitude = null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||||
|
<input type="checkbox" />
|
||||||
|
<div class="collapse-title text-xl font-medium">
|
||||||
|
{$t('adventures.location_information')}
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<!-- <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> -->
|
||||||
|
<div>
|
||||||
|
<label for="latitude">{$t('adventures.location')}</label><br />
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="location"
|
||||||
|
name="location"
|
||||||
|
bind:value={item.location}
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
/>
|
||||||
|
{#if is_custom_location}
|
||||||
|
<button
|
||||||
|
class="btn btn-primary ml-2"
|
||||||
|
type="button"
|
||||||
|
on:click={() => (item.location = reverseGeocodePlace?.display_name)}
|
||||||
|
>{$t('adventures.set_to_pin')}</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<form on:submit={geocode} class="mt-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={$t('adventures.search_for_location')}
|
||||||
|
class="input input-bordered w-full max-w-xs mb-2"
|
||||||
|
id="search"
|
||||||
|
name="search"
|
||||||
|
bind:value={query}
|
||||||
|
/>
|
||||||
|
<button class="btn btn-neutral -mt-1" type="submit">{$t('navbar.search')}</button>
|
||||||
|
<button class="btn btn-neutral -mt-1" type="button" on:click={clearMap}
|
||||||
|
>{$t('adventures.clear_map')}</button
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{#if places.length > 0}
|
||||||
|
<div class="mt-4 max-w-full">
|
||||||
|
<h3 class="font-bold text-lg mb-4">{$t('adventures.search_results')}</h3>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
{#each places as place}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-neutral mb-2 mr-2 max-w-full break-words whitespace-normal text-left"
|
||||||
|
on:click={() => {
|
||||||
|
markers = [
|
||||||
|
{
|
||||||
|
lngLat: { lng: Number(place.lon), lat: Number(place.lat) },
|
||||||
|
location: place.display_name,
|
||||||
|
name: place.name,
|
||||||
|
activity_type: place.type
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{place.display_name}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if noPlaces}
|
||||||
|
<p class="text-error text-lg">{$t('adventures.no_results')}</p>
|
||||||
|
{/if}
|
||||||
|
<!-- </div> -->
|
||||||
|
<div>
|
||||||
|
<MapLibre
|
||||||
|
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
||||||
|
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full rounded-lg"
|
||||||
|
standardControls
|
||||||
|
zoom={item.latitude && item.longitude ? 12 : 1}
|
||||||
|
center={{ lng: item.longitude || 0, lat: item.latitude || 0 }}
|
||||||
|
>
|
||||||
|
<!-- MapEvents gives you access to map events even from other components inside the map,
|
||||||
|
where you might not have access to the top-level `MapLibre` component. In this case
|
||||||
|
it would also work to just use on:click on the MapLibre component itself. -->
|
||||||
|
<MapEvents on:click={addMarker} />
|
||||||
|
|
||||||
|
{#each markers as marker}
|
||||||
|
<DefaultMarker lngLat={marker.lngLat} />
|
||||||
|
{/each}
|
||||||
|
</MapLibre>
|
||||||
|
{#if reverseGeocodePlace}
|
||||||
|
<div class="mt-2 p-4 bg-neutral rounded-lg shadow-md">
|
||||||
|
<h3 class="text-lg font-bold mb-2">{$t('adventures.location_details')}</h3>
|
||||||
|
<p class="mb-1">
|
||||||
|
<span class="font-semibold">{$t('adventures.display_name')}:</span>
|
||||||
|
{reverseGeocodePlace.city
|
||||||
|
? reverseGeocodePlace.city + ', '
|
||||||
|
: ''}{reverseGeocodePlace.region}, {reverseGeocodePlace.country}
|
||||||
|
</p>
|
||||||
|
<p class="mb-1">
|
||||||
|
<span class="font-semibold">{$t('adventures.region')}:</span>
|
||||||
|
{reverseGeocodePlace.region}
|
||||||
|
{reverseGeocodePlace.region_visited ? '✅' : '❌'}
|
||||||
|
</p>
|
||||||
|
{#if reverseGeocodePlace.city}
|
||||||
|
<p class="mb-1">
|
||||||
|
<span class="font-semibold">{$t('adventures.city')}:</span>
|
||||||
|
{reverseGeocodePlace.city}
|
||||||
|
{reverseGeocodePlace.city_visited ? '✅' : '❌'}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if !reverseGeocodePlace.region_visited || (!reverseGeocodePlace.city_visited && !willBeMarkedVisited)}
|
||||||
|
<button type="button" class="btn btn-primary mt-2" on:click={markVisited}>
|
||||||
|
{$t('adventures.mark_visited')}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if (willBeMarkedVisited && !reverseGeocodePlace.region_visited && reverseGeocodePlace.region_id) || (!reverseGeocodePlace.city_visited && willBeMarkedVisited && reverseGeocodePlace.city_id)}
|
||||||
|
<div role="alert" class="alert alert-info mt-2 flex items-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="h-6 w-6 shrink-0 stroke-current mr-2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
{reverseGeocodePlace.city
|
||||||
|
? reverseGeocodePlace.city + ', '
|
||||||
|
: ''}{reverseGeocodePlace.region}, {reverseGeocodePlace.country}
|
||||||
|
{$t('adventures.will_be_marked')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
174
frontend/src/lib/components/LodgingCard.svelte
Normal file
174
frontend/src/lib/components/LodgingCard.svelte
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import TrashCanOutline from '~icons/mdi/trash-can-outline';
|
||||||
|
import FileDocumentEdit from '~icons/mdi/file-document-edit';
|
||||||
|
import type { Collection, Lodging, User } from '$lib/types';
|
||||||
|
import { addToast } from '$lib/toasts';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import DeleteWarning from './DeleteWarning.svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let lodging: Lodging;
|
||||||
|
export let user: User | null = null;
|
||||||
|
export let collection: Collection | null = null;
|
||||||
|
|
||||||
|
let isWarningModalOpen: boolean = false;
|
||||||
|
|
||||||
|
function editTransportation() {
|
||||||
|
dispatch('edit', lodging);
|
||||||
|
}
|
||||||
|
|
||||||
|
let unlinked: boolean = false;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (collection?.start_date && collection.end_date) {
|
||||||
|
// Parse transportation dates
|
||||||
|
let transportationStartDate = lodging.check_in
|
||||||
|
? new Date(lodging.check_in.split('T')[0]) // Ensure proper date parsing
|
||||||
|
: null;
|
||||||
|
let transportationEndDate = lodging.check_out
|
||||||
|
? new Date(lodging.check_out.split('T')[0])
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Parse collection dates
|
||||||
|
let collectionStartDate = new Date(collection.start_date);
|
||||||
|
let collectionEndDate = new Date(collection.end_date);
|
||||||
|
|
||||||
|
// Check if the collection range is outside the transportation range
|
||||||
|
const startOutsideRange =
|
||||||
|
transportationStartDate &&
|
||||||
|
collectionStartDate < transportationStartDate &&
|
||||||
|
collectionEndDate < transportationStartDate;
|
||||||
|
|
||||||
|
const endOutsideRange =
|
||||||
|
transportationEndDate &&
|
||||||
|
collectionStartDate > transportationEndDate &&
|
||||||
|
collectionEndDate > transportationEndDate;
|
||||||
|
|
||||||
|
unlinked = !!(
|
||||||
|
startOutsideRange ||
|
||||||
|
endOutsideRange ||
|
||||||
|
(!transportationStartDate && !transportationEndDate)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTransportation() {
|
||||||
|
let res = await fetch(`/api/lodging/${lodging.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
console.log($t('transportation.transportation_delete_error'));
|
||||||
|
} else {
|
||||||
|
addToast('info', $t('transportation.transportation_deleted'));
|
||||||
|
isWarningModalOpen = false;
|
||||||
|
dispatch('delete', lodging.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isWarningModalOpen}
|
||||||
|
<DeleteWarning
|
||||||
|
title={$t('adventures.delete_lodging')}
|
||||||
|
button_text="Delete"
|
||||||
|
description={$t('adventures.lodging_delete_confirm')}
|
||||||
|
is_warning={false}
|
||||||
|
on:close={() => (isWarningModalOpen = false)}
|
||||||
|
on:confirm={deleteTransportation}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl"
|
||||||
|
>
|
||||||
|
<div class="card-body space-y-4">
|
||||||
|
<!-- Title and Type -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="card-title text-lg font-semibold truncate">{lodging.name}</h2>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="badge badge-secondary">
|
||||||
|
{$t(`lodging.${lodging.type}`)}
|
||||||
|
</div>
|
||||||
|
<!-- {#if hotel.type == 'plane' && hotel.flight_number}
|
||||||
|
<div class="badge badge-neutral-200">{hotel.flight_number}</div>
|
||||||
|
{/if} -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if unlinked}
|
||||||
|
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Location -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#if lodging.location}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium text-sm">{$t('adventures.location')}:</span>
|
||||||
|
<p>{lodging.location}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if lodging.check_in && lodging.check_out}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium text-sm">{$t('adventures.dates')}:</span>
|
||||||
|
<p>
|
||||||
|
{new Date(lodging.check_in).toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric'
|
||||||
|
})}
|
||||||
|
-
|
||||||
|
{new Date(lodging.check_out).toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric'
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))}
|
||||||
|
{#if lodging.reservation_number}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium text-sm">{$t('adventures.reservation_number')}:</span>
|
||||||
|
<p>{lodging.reservation_number}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if lodging.price}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium text-sm">{$t('adventures.price')}:</span>
|
||||||
|
<p>{lodging.price}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))}
|
||||||
|
<div class="card-actions justify-end">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-sm flex items-center gap-1"
|
||||||
|
on:click={editTransportation}
|
||||||
|
title="Edit"
|
||||||
|
>
|
||||||
|
<FileDocumentEdit class="w-5 h-5" />
|
||||||
|
<span>{$t('transportation.edit')}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
on:click={() => (isWarningModalOpen = true)}
|
||||||
|
class="btn btn-secondary btn-sm flex items-center gap-1"
|
||||||
|
title="Delete"
|
||||||
|
>
|
||||||
|
<TrashCanOutline class="w-5 h-5" />
|
||||||
|
<span>{$t('adventures.delete')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
428
frontend/src/lib/components/LodgingModal.svelte
Normal file
428
frontend/src/lib/components/LodgingModal.svelte
Normal file
|
@ -0,0 +1,428 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
import { addToast } from '$lib/toasts';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||||
|
import type { Collection, Lodging } from '$lib/types';
|
||||||
|
import LocationDropdown from './LocationDropdown.svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let collection: Collection;
|
||||||
|
export let lodgingToEdit: Lodging | null = null;
|
||||||
|
|
||||||
|
let modal: HTMLDialogElement;
|
||||||
|
let constrainDates: boolean = false;
|
||||||
|
let lodging: Lodging = { ...initializeLodging(lodgingToEdit) };
|
||||||
|
let fullStartDate: string = '';
|
||||||
|
let fullEndDate: string = '';
|
||||||
|
|
||||||
|
// Format date as local datetime
|
||||||
|
// Convert an ISO date to a datetime-local value in local time.
|
||||||
|
function toLocalDatetime(value: string | null): string {
|
||||||
|
if (!value) return '';
|
||||||
|
const date = new Date(value);
|
||||||
|
// Adjust the time by subtracting the timezone offset.
|
||||||
|
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
|
||||||
|
// Return format YYYY-MM-DDTHH:mm
|
||||||
|
return date.toISOString().slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
type LodgingType = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LODGING_TYPES: LodgingType[] = [
|
||||||
|
{ value: 'hotel', label: 'Hotel' },
|
||||||
|
{ value: 'hostel', label: 'Hostel' },
|
||||||
|
{ value: 'resort', label: 'Resort' },
|
||||||
|
{ value: 'bnb', label: 'Bed & Breakfast' },
|
||||||
|
{ value: 'campground', label: 'Campground' },
|
||||||
|
{ value: 'cabin', label: 'Cabin' },
|
||||||
|
{ value: 'apartment', label: 'Apartment' },
|
||||||
|
{ value: 'house', label: 'House' },
|
||||||
|
{ value: 'villa', label: 'Villa' },
|
||||||
|
{ value: 'motel', label: 'Motel' },
|
||||||
|
{ value: 'other', label: 'Other' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Initialize hotel with values from hotelToEdit or default values
|
||||||
|
function initializeLodging(hotelToEdit: Lodging | null): Lodging {
|
||||||
|
return {
|
||||||
|
id: hotelToEdit?.id || '',
|
||||||
|
user_id: hotelToEdit?.user_id || '',
|
||||||
|
name: hotelToEdit?.name || '',
|
||||||
|
type: hotelToEdit?.type || 'other',
|
||||||
|
description: hotelToEdit?.description || '',
|
||||||
|
rating: hotelToEdit?.rating || NaN,
|
||||||
|
link: hotelToEdit?.link || '',
|
||||||
|
check_in: hotelToEdit?.check_in ? toLocalDatetime(hotelToEdit.check_in) : null,
|
||||||
|
check_out: hotelToEdit?.check_out ? toLocalDatetime(hotelToEdit.check_out) : null,
|
||||||
|
reservation_number: hotelToEdit?.reservation_number || '',
|
||||||
|
price: hotelToEdit?.price || null,
|
||||||
|
latitude: hotelToEdit?.latitude || null,
|
||||||
|
longitude: hotelToEdit?.longitude || null,
|
||||||
|
location: hotelToEdit?.location || '',
|
||||||
|
is_public: hotelToEdit?.is_public || false,
|
||||||
|
collection: hotelToEdit?.collection || collection.id,
|
||||||
|
created_at: hotelToEdit?.created_at || '',
|
||||||
|
updated_at: hotelToEdit?.updated_at || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set full start and end dates from collection
|
||||||
|
if (collection.start_date && collection.end_date) {
|
||||||
|
fullStartDate = `${collection.start_date}T00:00`;
|
||||||
|
fullEndDate = `${collection.end_date}T23:59`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle rating change
|
||||||
|
$: {
|
||||||
|
if (!lodging.rating) {
|
||||||
|
lodging.rating = NaN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show modal on mount
|
||||||
|
onMount(() => {
|
||||||
|
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||||
|
if (modal) modal.showModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
function close() {
|
||||||
|
dispatch('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal on escape key press
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle form submission (save hotel)
|
||||||
|
async function handleSubmit(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (lodging.check_in && !lodging.check_out) {
|
||||||
|
const checkInDate = new Date(lodging.check_in);
|
||||||
|
checkInDate.setDate(checkInDate.getDate() + 1);
|
||||||
|
lodging.check_out = checkInDate.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lodging.check_in && lodging.check_out && lodging.check_in > lodging.check_out) {
|
||||||
|
addToast('error', $t('adventures.start_before_end_error'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only convert to UTC if the time is still in local format.
|
||||||
|
if (lodging.check_in && !lodging.check_in.includes('Z')) {
|
||||||
|
// new Date(lodging.check_in) interprets the input as local time.
|
||||||
|
lodging.check_in = new Date(lodging.check_in).toISOString();
|
||||||
|
}
|
||||||
|
if (lodging.check_out && !lodging.check_out.includes('Z')) {
|
||||||
|
lodging.check_out = new Date(lodging.check_out).toISOString();
|
||||||
|
}
|
||||||
|
console.log(lodging.check_in, lodging.check_out);
|
||||||
|
|
||||||
|
// Create or update lodging...
|
||||||
|
const url = lodging.id === '' ? '/api/lodging' : `/api/lodging/${lodging.id}`;
|
||||||
|
const method = lodging.id === '' ? 'POST' : 'PATCH';
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(lodging)
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.id) {
|
||||||
|
lodging = data as Lodging;
|
||||||
|
const toastMessage =
|
||||||
|
lodging.id === '' ? 'adventures.adventure_created' : 'adventures.adventure_updated';
|
||||||
|
addToast('success', $t(toastMessage));
|
||||||
|
dispatch('save', lodging);
|
||||||
|
} else {
|
||||||
|
const errorMessage =
|
||||||
|
lodging.id === ''
|
||||||
|
? 'adventures.adventure_create_error'
|
||||||
|
: 'adventures.adventure_update_error';
|
||||||
|
addToast('error', $t(errorMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<dialog id="my_modal_1" class="modal">
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
|
<div class="modal-box w-11/12 max-w-3xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||||
|
<h3 class="font-bold text-2xl">
|
||||||
|
{lodgingToEdit ? $t('lodging.edit_lodging') : $t('lodging.new_lodging')}
|
||||||
|
</h3>
|
||||||
|
<div class="modal-action items-center">
|
||||||
|
<form method="post" style="width: 100%;" on:submit={handleSubmit}>
|
||||||
|
<!-- Basic Information Section -->
|
||||||
|
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">
|
||||||
|
{$t('adventures.basic_information')}
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<!-- Name -->
|
||||||
|
<div>
|
||||||
|
<label for="name">
|
||||||
|
{$t('adventures.name')}<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
bind:value={lodging.name}
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="type">
|
||||||
|
{$t('transportation.type')}<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
class="select select-bordered w-full max-w-xs"
|
||||||
|
name="type"
|
||||||
|
id="type"
|
||||||
|
bind:value={lodging.type}
|
||||||
|
>
|
||||||
|
<option disabled selected>{$t('transportation.type')}</option>
|
||||||
|
<option value="hotel">{$t('lodging.hotel')}</option>
|
||||||
|
<option value="hostel">{$t('lodging.hostel')}</option>
|
||||||
|
<option value="resort">{$t('lodging.resort')}</option>
|
||||||
|
<option value="bnb">{$t('lodging.bnb')}</option>
|
||||||
|
<option value="campground">{$t('lodging.campground')}</option>
|
||||||
|
<option value="cabin">{$t('lodging.cabin')}</option>
|
||||||
|
<option value="apartment">{$t('lodging.apartment')}</option>
|
||||||
|
<option value="house">{$t('lodging.house')}</option>
|
||||||
|
<option value="villa">{$t('lodging.villa')}</option>
|
||||||
|
<option value="motel">{$t('lodging.motel')}</option>
|
||||||
|
<option value="other">{$t('lodging.other')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Description -->
|
||||||
|
<div>
|
||||||
|
<label for="description">{$t('adventures.description')}</label><br />
|
||||||
|
<MarkdownEditor bind:text={lodging.description} editor_height={'h-32'} />
|
||||||
|
</div>
|
||||||
|
<!-- Rating -->
|
||||||
|
<div>
|
||||||
|
<label for="rating">{$t('adventures.rating')}</label><br />
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="5"
|
||||||
|
hidden
|
||||||
|
bind:value={lodging.rating}
|
||||||
|
id="rating"
|
||||||
|
name="rating"
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
<div class="rating -ml-3 mt-1">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="rating-2"
|
||||||
|
class="rating-hidden"
|
||||||
|
checked={Number.isNaN(lodging.rating)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="rating-2"
|
||||||
|
class="mask mask-star-2 bg-orange-400"
|
||||||
|
on:click={() => (lodging.rating = 1)}
|
||||||
|
checked={lodging.rating === 1}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="rating-2"
|
||||||
|
class="mask mask-star-2 bg-orange-400"
|
||||||
|
on:click={() => (lodging.rating = 2)}
|
||||||
|
checked={lodging.rating === 2}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="rating-2"
|
||||||
|
class="mask mask-star-2 bg-orange-400"
|
||||||
|
on:click={() => (lodging.rating = 3)}
|
||||||
|
checked={lodging.rating === 3}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="rating-2"
|
||||||
|
class="mask mask-star-2 bg-orange-400"
|
||||||
|
on:click={() => (lodging.rating = 4)}
|
||||||
|
checked={lodging.rating === 4}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="rating-2"
|
||||||
|
class="mask mask-star-2 bg-orange-400"
|
||||||
|
on:click={() => (lodging.rating = 5)}
|
||||||
|
checked={lodging.rating === 5}
|
||||||
|
/>
|
||||||
|
{#if lodging.rating}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-error ml-2"
|
||||||
|
on:click={() => (lodging.rating = NaN)}
|
||||||
|
>
|
||||||
|
{$t('adventures.remove')}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Link -->
|
||||||
|
<div>
|
||||||
|
<label for="link">{$t('adventures.link')}</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="link"
|
||||||
|
name="link"
|
||||||
|
bind:value={lodging.link}
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">
|
||||||
|
{$t('adventures.lodging_information')}
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<!-- Reservation Number -->
|
||||||
|
<div>
|
||||||
|
<label for="date">
|
||||||
|
{$t('lodging.reservation_number')}
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="reservation_number"
|
||||||
|
name="reservation_number"
|
||||||
|
bind:value={lodging.reservation_number}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Price -->
|
||||||
|
<div>
|
||||||
|
<label for="price">
|
||||||
|
{$t('adventures.price')}
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="price"
|
||||||
|
name="price"
|
||||||
|
bind:value={lodging.price}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">
|
||||||
|
{$t('adventures.date_information')}
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<!-- Check In -->
|
||||||
|
<div>
|
||||||
|
<label for="date">
|
||||||
|
{$t('lodging.check_in')}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{#if collection && collection.start_date && collection.end_date}<label
|
||||||
|
class="label cursor-pointer flex items-start space-x-2"
|
||||||
|
>
|
||||||
|
<span class="label-text">{$t('adventures.date_constrain')}</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-primary"
|
||||||
|
id="constrain_dates"
|
||||||
|
name="constrain_dates"
|
||||||
|
on:change={() => (constrainDates = !constrainDates)}
|
||||||
|
/></label
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
id="date"
|
||||||
|
name="date"
|
||||||
|
bind:value={lodging.check_in}
|
||||||
|
min={constrainDates ? fullStartDate : ''}
|
||||||
|
max={constrainDates ? fullEndDate : ''}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- End Date -->
|
||||||
|
<div>
|
||||||
|
<label for="end_date">
|
||||||
|
{$t('lodging.check_out')}
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
id="end_date"
|
||||||
|
name="end_date"
|
||||||
|
min={constrainDates ? lodging.check_in : ''}
|
||||||
|
max={constrainDates ? fullEndDate : ''}
|
||||||
|
bind:value={lodging.check_out}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div role="alert" class="alert shadow-lg bg-neutral mt-4">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="stroke-info h-6 w-6 shrink-0"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
{$t('lodging.current_timezone')}:
|
||||||
|
{(() => {
|
||||||
|
const tz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
const [continent, city] = tz.split('/');
|
||||||
|
return `${continent} (${city.replace('_', ' ')})`;
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Location Information -->
|
||||||
|
<LocationDropdown bind:item={lodging} />
|
||||||
|
|
||||||
|
<!-- Form Actions -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{$t('notes.save')}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn" on:click={close}>
|
||||||
|
{$t('about.close')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
|
@ -8,6 +8,11 @@
|
||||||
import Calendar from '~icons/mdi/calendar';
|
import Calendar from '~icons/mdi/calendar';
|
||||||
import AboutModal from './AboutModal.svelte';
|
import AboutModal from './AboutModal.svelte';
|
||||||
import AccountMultiple from '~icons/mdi/account-multiple';
|
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 Avatar from './Avatar.svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { t, locale, locales } from 'svelte-i18n';
|
import { t, locale, locales } from 'svelte-i18n';
|
||||||
|
@ -17,7 +22,11 @@
|
||||||
|
|
||||||
// Event listener for focusing input
|
// Event listener for focusing input
|
||||||
function handleKeydown(event: KeyboardEvent) {
|
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
|
event.preventDefault(); // Prevent browser's search shortcut
|
||||||
if (inputElement) {
|
if (inputElement) {
|
||||||
inputElement.focus();
|
inputElement.focus();
|
||||||
|
@ -44,7 +53,8 @@
|
||||||
nl: 'Nederlands',
|
nl: 'Nederlands',
|
||||||
sv: 'Svenska',
|
sv: 'Svenska',
|
||||||
zh: '中文',
|
zh: '中文',
|
||||||
pl: 'Polski'
|
pl: 'Polski',
|
||||||
|
ko: '한국어'
|
||||||
};
|
};
|
||||||
|
|
||||||
let query: string = '';
|
let query: string = '';
|
||||||
|
@ -110,6 +120,7 @@
|
||||||
>
|
>
|
||||||
{#if data.user}
|
{#if data.user}
|
||||||
<li>
|
<li>
|
||||||
|
<MapMarker />
|
||||||
<button on:click={() => goto('/adventures')}>{$t('navbar.adventures')}</button>
|
<button on:click={() => goto('/adventures')}>{$t('navbar.adventures')}</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -167,36 +178,52 @@
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-ghost text-xl" href="/"
|
<a class="btn btn-ghost text-2xl font-bold tracking-normal" href="/">
|
||||||
>AdventureLog <img src="/favicon.png" alt="Map Logo" class="w-10" /></a
|
AdventureLog <img src="/favicon.png" alt="Map Logo" class="w-10" />
|
||||||
>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-center hidden lg:flex">
|
<div class="navbar-center hidden lg:flex">
|
||||||
<ul class="menu menu-horizontal px-1 gap-2">
|
<ul class="menu menu-horizontal px-1 gap-2">
|
||||||
{#if data.user}
|
{#if data.user}
|
||||||
<li>
|
<li>
|
||||||
<button class="btn btn-neutral" on:click={() => goto('/adventures')}
|
<button
|
||||||
>{$t('navbar.adventures')}</button
|
class="btn btn-neutral flex items-center gap-1"
|
||||||
|
on:click={() => goto('/adventures')}
|
||||||
|
>
|
||||||
|
<MapMarker class="w-5 h-5" />
|
||||||
|
<span>{$t('navbar.adventures')}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="btn btn-neutral flex items-center gap-1"
|
||||||
|
on:click={() => goto('/collections')}
|
||||||
|
>
|
||||||
|
<FormatListBulletedSquare class="w-5 h-5" />
|
||||||
|
{$t('navbar.collections')}</button
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button class="btn btn-neutral" on:click={() => goto('/collections')}
|
<button
|
||||||
>{$t('navbar.collections')}</button
|
class="btn btn-neutral flex items-center gap-1"
|
||||||
|
on:click={() => goto('/worldtravel')}
|
||||||
|
>
|
||||||
|
<Earth class="w-5 h-5" />
|
||||||
|
{$t('navbar.worldtravel')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button class="btn btn-neutral flex items-center gap-1" on:click={() => goto('/map')}>
|
||||||
|
<Map class="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button class="btn btn-neutral flex items-center gap-1" on:click={() => goto('/calendar')}
|
||||||
|
><Calendar /></button
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button class="btn btn-neutral" on:click={() => goto('/worldtravel')}
|
<button class="btn btn-neutral flex items-center gap-1" on:click={() => goto('/users')}
|
||||||
>{$t('navbar.worldtravel')}</button
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button class="btn btn-neutral" on:click={() => goto('/map')}>{$t('navbar.map')}</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button class="btn btn-neutral" on:click={() => goto('/calendar')}><Calendar /></button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button class="btn btn-neutral" on:click={() => goto('/users')}
|
|
||||||
><AccountMultiple /></button
|
><AccountMultiple /></button
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
@ -225,9 +252,9 @@
|
||||||
bind:this={inputElement}
|
bind:this={inputElement}
|
||||||
/><kbd class="kbd">/</kbd>
|
/><kbd class="kbd">/</kbd>
|
||||||
</label>
|
</label>
|
||||||
<button on:click={searchGo} type="submit" class="btn btn-neutral"
|
<button on:click={searchGo} type="submit" class="btn btn-neutral flex items-center gap-1">
|
||||||
>{$t('navbar.search')}</button
|
<Magnify class="w-5 h-5" />
|
||||||
>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -94,6 +94,7 @@
|
||||||
'User-Agent': `AdventureLog / ${appVersion} `
|
'User-Agent': `AdventureLog / ${appVersion} `
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
console.log(query);
|
||||||
let data = await res.json();
|
let data = await res.json();
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
@ -243,6 +244,20 @@
|
||||||
{$t('adventures.basic_information')}
|
{$t('adventures.basic_information')}
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
|
<!-- Name -->
|
||||||
|
<div>
|
||||||
|
<label for="name">
|
||||||
|
{$t('adventures.name')}<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
bind:value={transportation.name}
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<!-- Type selection -->
|
<!-- Type selection -->
|
||||||
<div>
|
<div>
|
||||||
<label for="type">
|
<label for="type">
|
||||||
|
@ -267,20 +282,6 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Name -->
|
|
||||||
<div>
|
|
||||||
<label for="name">
|
|
||||||
{$t('adventures.name')}<span class="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
bind:value={transportation.name}
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<div>
|
<div>
|
||||||
<label for="description">{$t('adventures.description')}</label><br />
|
<label for="description">{$t('adventures.description')}</label><br />
|
||||||
|
@ -464,7 +465,7 @@
|
||||||
bind:value={starting_airport}
|
bind:value={starting_airport}
|
||||||
name="starting_airport"
|
name="starting_airport"
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered w-full"
|
||||||
placeholder="Enter starting airport code (e.g., JFK)"
|
placeholder={$t('transportation.starting_airport_desc')}
|
||||||
/>
|
/>
|
||||||
<label for="ending_airport" class="label">
|
<label for="ending_airport" class="label">
|
||||||
<span class="label-text">{$t('adventures.ending_airport')}</span>
|
<span class="label-text">{$t('adventures.ending_airport')}</span>
|
||||||
|
@ -475,10 +476,10 @@
|
||||||
bind:value={ending_airport}
|
bind:value={ending_airport}
|
||||||
name="ending_airport"
|
name="ending_airport"
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered w-full"
|
||||||
placeholder="Enter ending airport code (e.g., LAX)"
|
placeholder={$t('transportation.ending_airport_desc')}
|
||||||
/>
|
/>
|
||||||
<button type="button" class="btn btn-primary mt-2" on:click={geocode}>
|
<button type="button" class="btn btn-primary mt-2" on:click={geocode}>
|
||||||
Fetch Location Information
|
{$t('transportation.fetch_location_information')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
<!-- Card Actions -->
|
<!-- Card Actions -->
|
||||||
<div class="card-actions justify-center mt-6">
|
<div class="card-actions justify-center mt-6">
|
||||||
{#if !sharing}
|
{#if !sharing}
|
||||||
<button class="btn btn-primary" on:click={() => goto(`/user/${user.uuid}`)}>
|
<button class="btn btn-primary" on:click={() => goto(`/profile/${user.username}`)}>
|
||||||
View Profile
|
View Profile
|
||||||
</button>
|
</button>
|
||||||
{:else if shared_with && !shared_with.includes(user.uuid)}
|
{:else if shared_with && !shared_with.includes(user.uuid)}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type {
|
||||||
Background,
|
Background,
|
||||||
Checklist,
|
Checklist,
|
||||||
Collection,
|
Collection,
|
||||||
|
Lodging,
|
||||||
Note,
|
Note,
|
||||||
Transportation,
|
Transportation,
|
||||||
User
|
User
|
||||||
|
@ -149,6 +150,50 @@ export function groupTransportationsByDate(
|
||||||
return groupedTransportations;
|
return groupedTransportations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function groupLodgingByDate(
|
||||||
|
transportations: Lodging[],
|
||||||
|
startDate: Date,
|
||||||
|
numberOfDays: number
|
||||||
|
): Record<string, Lodging[]> {
|
||||||
|
const groupedTransportations: Record<string, Lodging[]> = {};
|
||||||
|
|
||||||
|
// 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(
|
export function groupNotesByDate(
|
||||||
notes: Note[],
|
notes: Note[],
|
||||||
startDate: Date,
|
startDate: Date,
|
||||||
|
@ -464,3 +509,13 @@ export function osmTagToEmoji(tag: string) {
|
||||||
return '📍'; // Default placeholder emoji for unknown tags
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,11 @@
|
||||||
"url": "backgrounds/adventurelog_showcase_5.webp",
|
"url": "backgrounds/adventurelog_showcase_5.webp",
|
||||||
"author": "Sean Morley",
|
"author": "Sean Morley",
|
||||||
"location": "Hoboken, New Jersey, USA"
|
"location": "Hoboken, New Jersey, USA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "backgrounds/adventurelog_showcase_6.webp",
|
||||||
|
"author": "Sean Morley",
|
||||||
|
"location": "Smugglers' Notch Resort, Vermont, USA"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
export type User = {
|
export type User = {
|
||||||
pk: number;
|
pk: number;
|
||||||
username: string;
|
username: string;
|
||||||
email: string | null;
|
|
||||||
first_name: string | null;
|
first_name: string | null;
|
||||||
last_name: string | null;
|
last_name: string | null;
|
||||||
date_joined: string | null;
|
date_joined: string | null;
|
||||||
|
@ -41,6 +40,7 @@ export type Adventure = {
|
||||||
is_visited?: boolean;
|
is_visited?: boolean;
|
||||||
category: Category | null;
|
category: Category | null;
|
||||||
attachments: Attachment[];
|
attachments: Attachment[];
|
||||||
|
user?: User | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Country = {
|
export type Country = {
|
||||||
|
@ -113,6 +113,7 @@ export type Collection = {
|
||||||
end_date: string | null;
|
end_date: string | null;
|
||||||
transportations?: Transportation[];
|
transportations?: Transportation[];
|
||||||
notes?: Note[];
|
notes?: Note[];
|
||||||
|
lodging?: Lodging[];
|
||||||
checklists?: Checklist[];
|
checklists?: Checklist[];
|
||||||
is_archived?: boolean;
|
is_archived?: boolean;
|
||||||
shared_with: string[] | undefined;
|
shared_with: string[] | undefined;
|
||||||
|
@ -209,6 +210,7 @@ export type ReverseGeocode = {
|
||||||
display_name: string;
|
display_name: string;
|
||||||
city: string;
|
city: string;
|
||||||
city_id: string;
|
city_id: string;
|
||||||
|
location_name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Category = {
|
export type Category = {
|
||||||
|
@ -262,3 +264,24 @@ export type Attachment = {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
name: 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
|
||||||
|
};
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"about": "Um",
|
"about": "Über",
|
||||||
"close": "Schließen",
|
"close": "Schließen",
|
||||||
"license": "Lizenziert unter der GPL-3.0-Lizenz.",
|
"license": "Lizenziert unter der GPL-3.0-Lizenz.",
|
||||||
"message": "Hergestellt mit ❤️ in den Vereinigten Staaten.",
|
"message": "Hergestellt mit ❤️ in den Vereinigten Staaten.",
|
||||||
"nominatim_1": "Standortsuche und Geokodierung werden bereitgestellt von",
|
"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",
|
"oss_attributions": "Open-Source-Zuschreibungen",
|
||||||
"other_attributions": "Weitere Hinweise finden Sie in der README-Datei.",
|
"other_attributions": "Weitere Hinweise finden Sie in der README-Datei.",
|
||||||
"source_code": "Quellcode"
|
"source_code": "Quellcode"
|
||||||
|
@ -55,12 +55,12 @@
|
||||||
"archived_collection_message": "Sammlung erfolgreich archiviert!",
|
"archived_collection_message": "Sammlung erfolgreich archiviert!",
|
||||||
"archived_collections": "Archivierte Sammlungen",
|
"archived_collections": "Archivierte Sammlungen",
|
||||||
"ascending": "Aufsteigend",
|
"ascending": "Aufsteigend",
|
||||||
"cancel": "Stornieren",
|
"cancel": "Abbrechen",
|
||||||
"category_filter": "Kategoriefilter",
|
"category_filter": "Kategoriefilter",
|
||||||
"clear": "Klar",
|
"clear": "zurücksetzen",
|
||||||
"close_filters": "Filter schließen",
|
"close_filters": "Filter schließen",
|
||||||
"collection": "Sammlung",
|
"collection": "Sammlung",
|
||||||
"collection_adventures": "Schließen Sie Sammlungsabenteuer ein",
|
"collection_adventures": "Sammlungsabenteuer berücksichtigen",
|
||||||
"count_txt": "Ergebnisse, die Ihrer Suche entsprechen",
|
"count_txt": "Ergebnisse, die Ihrer Suche entsprechen",
|
||||||
"date": "Datum",
|
"date": "Datum",
|
||||||
"dates": "Termine",
|
"dates": "Termine",
|
||||||
|
@ -85,13 +85,13 @@
|
||||||
"not_found": "Abenteuer nicht gefunden",
|
"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.",
|
"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",
|
"open_filters": "Öffnen Sie Filter",
|
||||||
"order_by": "Bestellen nach",
|
"order_by": "Sortieren nach",
|
||||||
"order_direction": "Bestellrichtung",
|
"order_direction": "Sortierreihenfolge",
|
||||||
"planned": "Geplant",
|
"planned": "Geplant",
|
||||||
"private": "Privat",
|
"private": "Privat",
|
||||||
"public": "Öffentlich",
|
"public": "Öffentlich",
|
||||||
"rating": "Bewertung",
|
"rating": "Bewertung",
|
||||||
"share": "Aktie",
|
"share": "Teilen",
|
||||||
"sort": "Sortieren",
|
"sort": "Sortieren",
|
||||||
"sources": "Quellen",
|
"sources": "Quellen",
|
||||||
"start_before_end_error": "Das Startdatum muss vor dem Enddatum liegen",
|
"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_an_activity": "Fügen Sie eine Aktivität hinzu",
|
||||||
"add_notes": "Notizen hinzufügen",
|
"add_notes": "Notizen hinzufügen",
|
||||||
"adventure_create_error": "Das Abenteuer konnte nicht erstellt werden",
|
"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_update_error": "Das Abenteuer konnte nicht aktualisiert werden",
|
||||||
"adventure_updated": "Abenteuer aktualisiert",
|
"adventure_updated": "Abenteuer aktualisiert",
|
||||||
"basic_information": "Grundlegende Informationen",
|
"basic_information": "Grundlegende Informationen",
|
||||||
"category": "Kategorie",
|
"category": "Kategorie",
|
||||||
"clear_map": "Klare Karte",
|
"clear_map": "Karte leeren",
|
||||||
"copy_link": "Link kopieren",
|
"copy_link": "Link kopieren",
|
||||||
"create_new": "Neu erstellen...",
|
"create_new": "Neu erstellen...",
|
||||||
"date_constrain": "Auf Abholtermine beschränken",
|
"date_constrain": "Auf Abholtermine beschränken",
|
||||||
|
@ -137,7 +137,7 @@
|
||||||
"no_start_date": "Bitte geben Sie ein Startdatum ein",
|
"no_start_date": "Bitte geben Sie ein Startdatum ein",
|
||||||
"public_adventure": "Öffentliches Abenteuer",
|
"public_adventure": "Öffentliches Abenteuer",
|
||||||
"remove": "Entfernen",
|
"remove": "Entfernen",
|
||||||
"save_next": "Speichern",
|
"save_next": "Speichern & weiter",
|
||||||
"search_for_location": "Suchen Sie nach einem Ort",
|
"search_for_location": "Suchen Sie nach einem Ort",
|
||||||
"search_results": "Suchergebnisse",
|
"search_results": "Suchergebnisse",
|
||||||
"see_adventures": "Siehe Abenteuer",
|
"see_adventures": "Siehe Abenteuer",
|
||||||
|
@ -154,7 +154,7 @@
|
||||||
"all": "Alle",
|
"all": "Alle",
|
||||||
"error_updating_regions": "Fehler beim Aktualisieren der Regionen",
|
"error_updating_regions": "Fehler beim Aktualisieren der Regionen",
|
||||||
"mark_region_as_visited": "Region {region}, {country} als besucht markieren?",
|
"mark_region_as_visited": "Region {region}, {country} als besucht markieren?",
|
||||||
"mark_visited": "Mark besucht",
|
"mark_visited": "als besucht markieren",
|
||||||
"my_adventures": "Meine Abenteuer",
|
"my_adventures": "Meine Abenteuer",
|
||||||
"no_adventures_found": "Keine Abenteuer gefunden",
|
"no_adventures_found": "Keine Abenteuer gefunden",
|
||||||
"no_collections_found": "Es wurden keine Sammlungen gefunden, zu denen dieses Abenteuer hinzugefügt werden kann.",
|
"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": "Besuchte Regionen aktualisieren",
|
||||||
"update_visited_regions_disclaimer": "Dies kann je nach Anzahl der Abenteuer, die Sie besucht haben, eine Weile dauern.",
|
"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": "Ü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...",
|
"add_new": "Neu hinzufügen...",
|
||||||
"checklist": "Checkliste",
|
"checklist": "Checkliste",
|
||||||
"checklists": "Checklisten",
|
"checklists": "Checklisten",
|
||||||
|
@ -185,7 +185,7 @@
|
||||||
"visit_link": "Besuchen Sie den Link",
|
"visit_link": "Besuchen Sie den Link",
|
||||||
"collection_archived": "Diese Sammlung wurde archiviert.",
|
"collection_archived": "Diese Sammlung wurde archiviert.",
|
||||||
"day": "Tag",
|
"day": "Tag",
|
||||||
"add_a_tag": "Fügen Sie ein Tag hinzu",
|
"add_a_tag": "Fügen Sie einen Tag hinzu",
|
||||||
"tags": "Schlagworte",
|
"tags": "Schlagworte",
|
||||||
"set_to_pin": "Auf „Anpinnen“ setzen",
|
"set_to_pin": "Auf „Anpinnen“ setzen",
|
||||||
"category_fetch_error": "Fehler beim Abrufen der Kategorien",
|
"category_fetch_error": "Fehler beim Abrufen der Kategorien",
|
||||||
|
@ -196,7 +196,7 @@
|
||||||
"hide": "Verstecken",
|
"hide": "Verstecken",
|
||||||
"show": "Zeigen",
|
"show": "Zeigen",
|
||||||
"download_calendar": "Kalender herunterladen",
|
"download_calendar": "Kalender herunterladen",
|
||||||
"md_instructions": "Schreiben Sie hier Ihren Abschlag...",
|
"md_instructions": "Schreiben Sie hier Ihren Markdowntext...",
|
||||||
"preview": "Vorschau",
|
"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.",
|
"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",
|
"clear_location": "Standort löschen",
|
||||||
|
@ -207,14 +207,14 @@
|
||||||
"end": "Ende",
|
"end": "Ende",
|
||||||
"ending_airport": "Endflughafen",
|
"ending_airport": "Endflughafen",
|
||||||
"flight_information": "Fluginformationen",
|
"flight_information": "Fluginformationen",
|
||||||
"from": "Aus",
|
"from": "Von",
|
||||||
"no_location_found": "Kein Standort gefunden",
|
"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.",
|
"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",
|
"out_of_range": "Nicht im Datumsbereich der Reiseroute",
|
||||||
"show_region_labels": "Regionsbeschriftungen anzeigen",
|
"show_region_labels": "Regionsbeschriftungen anzeigen",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"starting_airport": "Startflughafen",
|
"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.",
|
"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",
|
"show_map": "Karte anzeigen",
|
||||||
"will_be_marked": "wird als besucht markiert, sobald das Abenteuer gespeichert ist.",
|
"will_be_marked": "wird als besucht markiert, sobald das Abenteuer gespeichert ist.",
|
||||||
|
@ -234,7 +234,19 @@
|
||||||
"images": "Bilder",
|
"images": "Bilder",
|
||||||
"primary": "Primär",
|
"primary": "Primär",
|
||||||
"upload": "Hochladen",
|
"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": {
|
"home": {
|
||||||
"desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit",
|
"desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit",
|
||||||
|
@ -254,7 +266,7 @@
|
||||||
"about": "Über AdventureLog",
|
"about": "Über AdventureLog",
|
||||||
"adventures": "Abenteuer",
|
"adventures": "Abenteuer",
|
||||||
"collections": "Sammlungen",
|
"collections": "Sammlungen",
|
||||||
"discord": "Zwietracht",
|
"discord": "Discord",
|
||||||
"documentation": "Dokumentation",
|
"documentation": "Dokumentation",
|
||||||
"greeting": "Hallo",
|
"greeting": "Hallo",
|
||||||
"logout": "Abmelden",
|
"logout": "Abmelden",
|
||||||
|
@ -269,7 +281,7 @@
|
||||||
"aqua": "Aqua",
|
"aqua": "Aqua",
|
||||||
"dark": "Dunkel",
|
"dark": "Dunkel",
|
||||||
"forest": "Wald",
|
"forest": "Wald",
|
||||||
"light": "Licht",
|
"light": "Hell",
|
||||||
"night": "Nacht",
|
"night": "Nacht",
|
||||||
"aestheticDark": "Ästhetisches Dunkel",
|
"aestheticDark": "Ästhetisches Dunkel",
|
||||||
"aestheticLight": "Ästhetisches Licht",
|
"aestheticLight": "Ästhetisches Licht",
|
||||||
|
@ -281,7 +293,8 @@
|
||||||
"tag": "Etikett",
|
"tag": "Etikett",
|
||||||
"language_selection": "Sprache",
|
"language_selection": "Sprache",
|
||||||
"support": "Unterstützung",
|
"support": "Unterstützung",
|
||||||
"calendar": "Kalender"
|
"calendar": "Kalender",
|
||||||
|
"admin_panel": "Admin -Panel"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"confirm_password": "Passwort bestätigen",
|
"confirm_password": "Passwort bestätigen",
|
||||||
|
@ -302,7 +315,11 @@
|
||||||
"both_passwords_required": "Beide Passwörter sind erforderlich",
|
"both_passwords_required": "Beide Passwörter sind erforderlich",
|
||||||
"new_password": "Neues Passwort",
|
"new_password": "Neues Passwort",
|
||||||
"reset_failed": "Passwort konnte nicht zurückgesetzt werden",
|
"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": {
|
"users": {
|
||||||
"no_users_found": "Keine Benutzer mit öffentlichen Profilen gefunden."
|
"no_users_found": "Keine Benutzer mit öffentlichen Profilen gefunden."
|
||||||
|
@ -317,7 +334,7 @@
|
||||||
"not_visited": "Nicht besucht",
|
"not_visited": "Nicht besucht",
|
||||||
"num_countries": "Länder gefunden",
|
"num_countries": "Länder gefunden",
|
||||||
"partially_visited": "Teilweise besucht",
|
"partially_visited": "Teilweise besucht",
|
||||||
"all_visited": "Sie haben alle Regionen in besucht",
|
"all_visited": "Sie haben alle Regionen besucht in",
|
||||||
"cities": "Städte",
|
"cities": "Städte",
|
||||||
"failed_to_mark_visit": "Der Besuch konnte nicht markiert werden",
|
"failed_to_mark_visit": "Der Besuch konnte nicht markiert werden",
|
||||||
"failed_to_remove_visit": "Der Besuch von konnte nicht entfernt werden",
|
"failed_to_remove_visit": "Der Besuch von konnte nicht entfernt werden",
|
||||||
|
@ -352,7 +369,7 @@
|
||||||
"password_is_required": "Passwort ist erforderlich",
|
"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!",
|
"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",
|
"reset_password": "Passwort zurücksetzen",
|
||||||
"submit": "Einreichen",
|
"submit": "Absenden",
|
||||||
"token_required": "Zum Zurücksetzen des Passworts sind Token und UID erforderlich.",
|
"token_required": "Zum Zurücksetzen des Passworts sind Token und UID erforderlich.",
|
||||||
"about_this_background": "Über diesen Hintergrund",
|
"about_this_background": "Über diesen Hintergrund",
|
||||||
"join_discord": "Treten Sie dem Discord bei",
|
"join_discord": "Treten Sie dem Discord bei",
|
||||||
|
@ -379,7 +396,7 @@
|
||||||
"generic_error": "Bei der Bearbeitung Ihrer Anfrage ist ein Fehler aufgetreten.",
|
"generic_error": "Bei der Bearbeitung Ihrer Anfrage ist ein Fehler aufgetreten.",
|
||||||
"invalid_code": "Ungültiger MFA-Code",
|
"invalid_code": "Ungültiger MFA-Code",
|
||||||
"invalid_credentials": "Ungültiger Benutzername oder Passwort",
|
"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_disabled": "Multi-Faktor-Authentifizierung erfolgreich deaktiviert!",
|
||||||
"mfa_enabled": "Multi-Faktor-Authentifizierung erfolgreich aktiviert!",
|
"mfa_enabled": "Multi-Faktor-Authentifizierung erfolgreich aktiviert!",
|
||||||
"mfa_not_enabled": "MFA ist nicht aktiviert",
|
"mfa_not_enabled": "MFA ist nicht aktiviert",
|
||||||
|
@ -481,7 +498,10 @@
|
||||||
"provide_start_date": "Bitte geben Sie ein Startdatum an",
|
"provide_start_date": "Bitte geben Sie ein Startdatum an",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"to_location": "Zum Standort",
|
"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": {
|
"search": {
|
||||||
"adventurelog_results": "AdventureLog-Ergebnisse",
|
"adventurelog_results": "AdventureLog-Ergebnisse",
|
||||||
|
@ -531,7 +551,7 @@
|
||||||
"countries_visited": "Besuchte Länder",
|
"countries_visited": "Besuchte Länder",
|
||||||
"no_recent_adventures": "Keine aktuellen Abenteuer?",
|
"no_recent_adventures": "Keine aktuellen Abenteuer?",
|
||||||
"recent_adventures": "Aktuelle Abenteuer",
|
"recent_adventures": "Aktuelle Abenteuer",
|
||||||
"total_adventures": "Totale Abenteuer",
|
"total_adventures": "Abenteuer insgesamt",
|
||||||
"total_visited_regions": "Insgesamt besuchte Regionen",
|
"total_visited_regions": "Insgesamt besuchte Regionen",
|
||||||
"welcome_back": "Willkommen zurück",
|
"welcome_back": "Willkommen zurück",
|
||||||
"total_visited_cities": "Insgesamt besuchte Städte"
|
"total_visited_cities": "Insgesamt besuchte Städte"
|
||||||
|
@ -567,5 +587,35 @@
|
||||||
"phone": "Telefon",
|
"phone": "Telefon",
|
||||||
"recommendation": "Empfehlung",
|
"recommendation": "Empfehlung",
|
||||||
"website": "Webseite"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"support": "Support",
|
"support": "Support",
|
||||||
"calendar": "Calendar",
|
"calendar": "Calendar",
|
||||||
"theme_selection": "Theme Selection",
|
"theme_selection": "Theme Selection",
|
||||||
|
"admin_panel": "Admin Panel",
|
||||||
"themes": {
|
"themes": {
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
|
@ -67,9 +68,11 @@
|
||||||
"checklist_delete_confirm": "Are you sure you want to delete this checklist? This action cannot be undone.",
|
"checklist_delete_confirm": "Are you sure you want to delete this checklist? This action cannot be undone.",
|
||||||
"note_delete_confirm": "Are you sure you want to delete this note? This action cannot be undone.",
|
"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.",
|
"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_checklist": "Delete Checklist",
|
||||||
"delete_note": "Delete Note",
|
"delete_note": "Delete Note",
|
||||||
"delete_transportation": "Delete Transportation",
|
"delete_transportation": "Delete Transportation",
|
||||||
|
"delete_lodging": "Delete Lodging",
|
||||||
"open_details": "Open Details",
|
"open_details": "Open Details",
|
||||||
"edit_adventure": "Edit Adventure",
|
"edit_adventure": "Edit Adventure",
|
||||||
"remove_from_collection": "Remove from Collection",
|
"remove_from_collection": "Remove from Collection",
|
||||||
|
@ -165,6 +168,7 @@
|
||||||
"delete_collection_success": "Collection deleted successfully!",
|
"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.",
|
"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",
|
"cancel": "Cancel",
|
||||||
|
"of": "of",
|
||||||
"delete_collection": "Delete Collection",
|
"delete_collection": "Delete Collection",
|
||||||
"delete_adventure": "Delete Adventure",
|
"delete_adventure": "Delete Adventure",
|
||||||
"adventure_delete_success": "Adventure deleted successfully!",
|
"adventure_delete_success": "Adventure deleted successfully!",
|
||||||
|
@ -190,6 +194,7 @@
|
||||||
"no_description_found": "No description found",
|
"no_description_found": "No description found",
|
||||||
"adventure_created": "Adventure created",
|
"adventure_created": "Adventure created",
|
||||||
"adventure_create_error": "Failed to create adventure",
|
"adventure_create_error": "Failed to create adventure",
|
||||||
|
"lodging": "Lodging",
|
||||||
"create_adventure": "Create Adventure",
|
"create_adventure": "Create Adventure",
|
||||||
"adventure_updated": "Adventure updated",
|
"adventure_updated": "Adventure updated",
|
||||||
"adventure_update_error": "Failed to update adventure",
|
"adventure_update_error": "Failed to update adventure",
|
||||||
|
@ -198,6 +203,7 @@
|
||||||
"new_adventure": "New Adventure",
|
"new_adventure": "New Adventure",
|
||||||
"basic_information": "Basic Information",
|
"basic_information": "Basic Information",
|
||||||
"no_adventures_to_recommendations": "No adventures found. Add at leat one adventure to get recommendations.",
|
"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!",
|
"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",
|
"no_adventures_found": "No adventures found",
|
||||||
"mark_region_as_visited": "Mark region {region}, {country} as visited?",
|
"mark_region_as_visited": "Mark region {region}, {country} as visited?",
|
||||||
|
@ -248,6 +254,9 @@
|
||||||
"out_of_range": "Not in itinerary date range",
|
"out_of_range": "Not in itinerary date range",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"finding_recommendations": "Discovering hidden gems for your next adventure",
|
"finding_recommendations": "Discovering hidden gems for your next adventure",
|
||||||
|
"location_details": "Location Details",
|
||||||
|
"city": "City",
|
||||||
|
"region": "Region",
|
||||||
"md_instructions": "Write your markdown here...",
|
"md_instructions": "Write your markdown here...",
|
||||||
"days": "days",
|
"days": "days",
|
||||||
"attachment_upload_success": "Attachment uploaded successfully!",
|
"attachment_upload_success": "Attachment uploaded successfully!",
|
||||||
|
@ -257,6 +266,7 @@
|
||||||
"attachment_update_success": "Attachment updated successfully!",
|
"attachment_update_success": "Attachment updated successfully!",
|
||||||
"attachment_name": "Attachment Name",
|
"attachment_name": "Attachment Name",
|
||||||
"gpx_tip": "Upload GPX files to attachments to view them on the map!",
|
"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",
|
"attachment_update_error": "Error updating attachment",
|
||||||
"activities": {
|
"activities": {
|
||||||
"general": "General 🌍",
|
"general": "General 🌍",
|
||||||
|
@ -281,7 +291,10 @@
|
||||||
"spiritual_journeys": "Spiritual Journeys 🧘♀️",
|
"spiritual_journeys": "Spiritual Journeys 🧘♀️",
|
||||||
"volunteer_work": "Volunteer Work 🤝",
|
"volunteer_work": "Volunteer Work 🤝",
|
||||||
"other": "Other"
|
"other": "Other"
|
||||||
}
|
},
|
||||||
|
"lodging_information": "Lodging Information",
|
||||||
|
"price": "Price",
|
||||||
|
"reservation_number": "Reservation Number"
|
||||||
},
|
},
|
||||||
"worldtravel": {
|
"worldtravel": {
|
||||||
"country_list": "Country List",
|
"country_list": "Country List",
|
||||||
|
@ -326,7 +339,11 @@
|
||||||
"new_password": "New Password (6+ characters)",
|
"new_password": "New Password (6+ characters)",
|
||||||
"both_passwords_required": "Both passwords are required",
|
"both_passwords_required": "Both passwords are required",
|
||||||
"reset_failed": "Failed to reset password",
|
"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": {
|
"users": {
|
||||||
"no_users_found": "No users found with public profiles."
|
"no_users_found": "No users found with public profiles."
|
||||||
|
@ -467,6 +484,9 @@
|
||||||
"flight_number": "Flight Number",
|
"flight_number": "Flight Number",
|
||||||
"from_location": "From Location",
|
"from_location": "From Location",
|
||||||
"to_location": "To 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",
|
"edit": "Edit",
|
||||||
"modes": {
|
"modes": {
|
||||||
"car": "Car",
|
"car": "Car",
|
||||||
|
@ -483,6 +503,36 @@
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"date_and_time": "Date & Time"
|
"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": {
|
"search": {
|
||||||
"adventurelog_results": "AdventureLog Results",
|
"adventurelog_results": "AdventureLog Results",
|
||||||
"public_adventures": "Public Adventures",
|
"public_adventures": "Public Adventures",
|
||||||
|
|
|
@ -30,7 +30,8 @@
|
||||||
"tag": "Etiqueta",
|
"tag": "Etiqueta",
|
||||||
"language_selection": "Idioma",
|
"language_selection": "Idioma",
|
||||||
"support": "Apoyo",
|
"support": "Apoyo",
|
||||||
"calendar": "Calendario"
|
"calendar": "Calendario",
|
||||||
|
"admin_panel": "Panel de administración"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"about": "Acerca de",
|
"about": "Acerca de",
|
||||||
|
@ -281,7 +282,19 @@
|
||||||
"primary": "Primario",
|
"primary": "Primario",
|
||||||
"upload": "Subir",
|
"upload": "Subir",
|
||||||
"view_attachment": "Ver archivo adjunto",
|
"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": {
|
"worldtravel": {
|
||||||
"all": "Todo",
|
"all": "Todo",
|
||||||
|
@ -326,7 +339,11 @@
|
||||||
"both_passwords_required": "Se requieren ambas contraseñas",
|
"both_passwords_required": "Se requieren ambas contraseñas",
|
||||||
"new_password": "Nueva contraseña",
|
"new_password": "Nueva contraseña",
|
||||||
"reset_failed": "No se pudo restablecer la 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": {
|
"users": {
|
||||||
"no_users_found": "No se encontraron usuarios con perfiles públicos."
|
"no_users_found": "No se encontraron usuarios con perfiles públicos."
|
||||||
|
@ -481,7 +498,10 @@
|
||||||
"flight_number": "Número de vuelo",
|
"flight_number": "Número de vuelo",
|
||||||
"from_location": "Desde la ubicación",
|
"from_location": "Desde la ubicación",
|
||||||
"transportation_added": "¡Transporte agregado exitosamente!",
|
"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": {
|
"search": {
|
||||||
"adventurelog_results": "Resultados del registro de aventuras",
|
"adventurelog_results": "Resultados del registro de aventuras",
|
||||||
|
@ -567,5 +587,35 @@
|
||||||
"phone": "Teléfono",
|
"phone": "Teléfono",
|
||||||
"recommendation": "Recomendación",
|
"recommendation": "Recomendación",
|
||||||
"website": "Sitio web"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -234,7 +234,19 @@
|
||||||
"images": "Images",
|
"images": "Images",
|
||||||
"primary": "Primaire",
|
"primary": "Primaire",
|
||||||
"upload": "Télécharger",
|
"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": {
|
"home": {
|
||||||
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
|
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
|
||||||
|
@ -281,7 +293,8 @@
|
||||||
"tag": "Étiqueter",
|
"tag": "Étiqueter",
|
||||||
"language_selection": "Langue",
|
"language_selection": "Langue",
|
||||||
"support": "Soutien",
|
"support": "Soutien",
|
||||||
"calendar": "Calendrier"
|
"calendar": "Calendrier",
|
||||||
|
"admin_panel": "Panneau d'administration"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"confirm_password": "Confirmez le mot de passe",
|
"confirm_password": "Confirmez le mot de passe",
|
||||||
|
@ -302,7 +315,11 @@
|
||||||
"both_passwords_required": "Les deux mots de passe sont requis",
|
"both_passwords_required": "Les deux mots de passe sont requis",
|
||||||
"new_password": "Nouveau mot de passe",
|
"new_password": "Nouveau mot de passe",
|
||||||
"reset_failed": "Échec de la réinitialisation du 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": {
|
"users": {
|
||||||
"no_users_found": "Aucun utilisateur trouvé avec des profils publics."
|
"no_users_found": "Aucun utilisateur trouvé avec des profils publics."
|
||||||
|
@ -481,7 +498,10 @@
|
||||||
"transportation_added": "Transport ajouté avec succès !",
|
"transportation_added": "Transport ajouté avec succès !",
|
||||||
"transportation_delete_error": "Erreur lors de la suppression du transport",
|
"transportation_delete_error": "Erreur lors de la suppression du transport",
|
||||||
"transportation_deleted": "Transport supprimé avec succès !",
|
"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": {
|
"search": {
|
||||||
"adventurelog_results": "Résultats du journal d'aventure",
|
"adventurelog_results": "Résultats du journal d'aventure",
|
||||||
|
@ -567,5 +587,35 @@
|
||||||
"phone": "Téléphone",
|
"phone": "Téléphone",
|
||||||
"recommendation": "Recommandation",
|
"recommendation": "Recommandation",
|
||||||
"website": "Site web"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -234,7 +234,19 @@
|
||||||
"images": "Immagini",
|
"images": "Immagini",
|
||||||
"primary": "Primario",
|
"primary": "Primario",
|
||||||
"upload": "Caricamento",
|
"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": {
|
"home": {
|
||||||
"desc_1": "Scopri, pianifica ed esplora con facilità",
|
"desc_1": "Scopri, pianifica ed esplora con facilità",
|
||||||
|
@ -281,7 +293,8 @@
|
||||||
"tag": "Etichetta",
|
"tag": "Etichetta",
|
||||||
"language_selection": "Lingua",
|
"language_selection": "Lingua",
|
||||||
"support": "Supporto",
|
"support": "Supporto",
|
||||||
"calendar": "Calendario"
|
"calendar": "Calendario",
|
||||||
|
"admin_panel": "Pannello di amministrazione"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"confirm_password": "Conferma password",
|
"confirm_password": "Conferma password",
|
||||||
|
@ -302,7 +315,11 @@
|
||||||
"both_passwords_required": "Sono necessarie entrambe le password",
|
"both_passwords_required": "Sono necessarie entrambe le password",
|
||||||
"new_password": "Nuova parola d'ordine",
|
"new_password": "Nuova parola d'ordine",
|
||||||
"reset_failed": "Impossibile reimpostare la password",
|
"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": {
|
"users": {
|
||||||
"no_users_found": "Nessun utente trovato con profili pubblici."
|
"no_users_found": "Nessun utente trovato con profili pubblici."
|
||||||
|
@ -481,7 +498,10 @@
|
||||||
"transportation_delete_error": "Errore durante l'eliminazione del trasporto",
|
"transportation_delete_error": "Errore durante l'eliminazione del trasporto",
|
||||||
"transportation_deleted": "Trasporto eliminato con successo!",
|
"transportation_deleted": "Trasporto eliminato con successo!",
|
||||||
"transportation_edit_success": "Trasporti modificati 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": {
|
"search": {
|
||||||
"adventurelog_results": "Risultati di AdventureLog",
|
"adventurelog_results": "Risultati di AdventureLog",
|
||||||
|
@ -567,5 +587,35 @@
|
||||||
"phone": "Telefono",
|
"phone": "Telefono",
|
||||||
"recommendation": "Raccomandazione",
|
"recommendation": "Raccomandazione",
|
||||||
"website": "Sito web"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
620
frontend/src/locales/ko.json
Normal file
620
frontend/src/locales/ko.json
Normal file
|
@ -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": "체크 아웃"
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,9 +51,9 @@
|
||||||
"close_filters": "Sluit filters",
|
"close_filters": "Sluit filters",
|
||||||
"collection": "Collectie",
|
"collection": "Collectie",
|
||||||
"collection_adventures": "Inclusief collectie-avonturen",
|
"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_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!",
|
"collection_remove_success": "Avontuur is succesvol uit de collectie verwijderd!",
|
||||||
"count_txt": "resultaten die overeenkomen met uw zoekopdracht",
|
"count_txt": "resultaten die overeenkomen met uw zoekopdracht",
|
||||||
"create_new": "Maak nieuwe...",
|
"create_new": "Maak nieuwe...",
|
||||||
|
@ -94,9 +94,9 @@
|
||||||
"dates": "Datums",
|
"dates": "Datums",
|
||||||
"delete_adventure": "Avontuur verwijderen",
|
"delete_adventure": "Avontuur verwijderen",
|
||||||
"duration": "Duur",
|
"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_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!",
|
"image_upload_success": "Afbeelding succesvol geüpload!",
|
||||||
"no_image_url": "Er is geen afbeelding gevonden op die URL.",
|
"no_image_url": "Er is geen afbeelding gevonden op die URL.",
|
||||||
"planned": "Gepland",
|
"planned": "Gepland",
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
"category": "Categorie",
|
"category": "Categorie",
|
||||||
"clear_map": "Kaart leegmaken",
|
"clear_map": "Kaart leegmaken",
|
||||||
"copy_link": "Kopieer link",
|
"copy_link": "Kopieer link",
|
||||||
"date_constrain": "Beperk u tot ophaaldata",
|
"date_constrain": "Beperk tot de datums van de collectie",
|
||||||
"description": "Beschrijving",
|
"description": "Beschrijving",
|
||||||
"end_date": "Einddatum",
|
"end_date": "Einddatum",
|
||||||
"fetch_image": "Afbeelding ophalen",
|
"fetch_image": "Afbeelding ophalen",
|
||||||
|
@ -126,7 +126,7 @@
|
||||||
"image_fetch_failed": "Kan afbeelding niet ophalen",
|
"image_fetch_failed": "Kan afbeelding niet ophalen",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
"location": "Locatie",
|
"location": "Locatie",
|
||||||
"location_information": "Locatie-informatie",
|
"location_information": "Informatie over de locatie",
|
||||||
"my_images": "Mijn afbeeldingen",
|
"my_images": "Mijn afbeeldingen",
|
||||||
"my_visits": "Mijn bezoeken",
|
"my_visits": "Mijn bezoeken",
|
||||||
"new_adventure": "Nieuw avontuur",
|
"new_adventure": "Nieuw avontuur",
|
||||||
|
@ -152,8 +152,8 @@
|
||||||
"wikipedia": "Wikipedia",
|
"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!",
|
"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",
|
"all": "Alle",
|
||||||
"error_updating_regions": "Fout bij wijzigen van regio's",
|
"error_updating_regions": "Fout bij het wijzigen van regio's",
|
||||||
"mark_visited": "Markeer bezocht",
|
"mark_visited": "Markeer als bezocht",
|
||||||
"my_adventures": "Mijn avonturen",
|
"my_adventures": "Mijn avonturen",
|
||||||
"no_adventures_found": "Geen avonturen gevonden",
|
"no_adventures_found": "Geen avonturen gevonden",
|
||||||
"no_collections_found": "Er zijn geen collecties gevonden waar dit avontuur aan kan worden toegevoegd.",
|
"no_collections_found": "Er zijn geen collecties gevonden waar dit avontuur aan kan worden toegevoegd.",
|
||||||
|
@ -162,7 +162,7 @@
|
||||||
"regions_updated": "regio's bijgewerkt",
|
"regions_updated": "regio's bijgewerkt",
|
||||||
"update_visited_regions": "Werk bezochte regio's bij",
|
"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.",
|
"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.",
|
"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...",
|
"add_new": "Nieuw toevoegen...",
|
||||||
"checklist": "Controlelijst",
|
"checklist": "Controlelijst",
|
||||||
|
@ -194,9 +194,9 @@
|
||||||
"adventure_calendar": "Avonturenkalender",
|
"adventure_calendar": "Avonturenkalender",
|
||||||
"emoji_picker": "Emoji-kiezer",
|
"emoji_picker": "Emoji-kiezer",
|
||||||
"hide": "Verbergen",
|
"hide": "Verbergen",
|
||||||
"show": "Show",
|
"show": "Toon",
|
||||||
"download_calendar": "Agenda downloaden",
|
"download_calendar": "Agenda downloaden",
|
||||||
"md_instructions": "Schrijf hier uw korting...",
|
"md_instructions": "Schrijf hier in markdown...",
|
||||||
"preview": "Voorbeeld",
|
"preview": "Voorbeeld",
|
||||||
"checklist_delete_confirm": "Weet u zeker dat u deze checklist wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
|
"checklist_delete_confirm": "Weet u zeker dat u deze checklist wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
|
||||||
"clear_location": "Locatie wissen",
|
"clear_location": "Locatie wissen",
|
||||||
|
@ -212,10 +212,10 @@
|
||||||
"out_of_range": "Niet binnen het datumbereik van het reisplan",
|
"out_of_range": "Niet binnen het datumbereik van het reisplan",
|
||||||
"show_region_labels": "Toon regiolabels",
|
"show_region_labels": "Toon regiolabels",
|
||||||
"start": "Begin",
|
"start": "Begin",
|
||||||
"starting_airport": "Startende luchthaven",
|
"starting_airport": "Luchthaven van vertrek",
|
||||||
"to": "Naar",
|
"to": "Naar",
|
||||||
"transportation_delete_confirm": "Weet u zeker dat u dit transport wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
|
"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",
|
"show_map": "Toon kaart",
|
||||||
"will_be_marked": "wordt gemarkeerd als bezocht zodra het avontuur is opgeslagen.",
|
"will_be_marked": "wordt gemarkeerd als bezocht zodra het avontuur is opgeslagen.",
|
||||||
"cities_updated": "steden bijgewerkt",
|
"cities_updated": "steden bijgewerkt",
|
||||||
|
@ -225,16 +225,28 @@
|
||||||
"attachment": "Bijlage",
|
"attachment": "Bijlage",
|
||||||
"attachment_delete_success": "Bijlage succesvol verwijderd!",
|
"attachment_delete_success": "Bijlage succesvol verwijderd!",
|
||||||
"attachment_name": "Naam van bijlage",
|
"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_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!",
|
"attachment_upload_success": "Bijlage succesvol geüpload!",
|
||||||
"attachments": "Bijlagen",
|
"attachments": "Bijlagen",
|
||||||
"gpx_tip": "Upload GPX-bestanden naar bijlagen om ze op de kaart te bekijken!",
|
"gpx_tip": "Upload GPX-bestanden naar bijlagen om ze op de kaart te bekijken!",
|
||||||
"images": "Afbeeldingen",
|
"images": "Afbeeldingen",
|
||||||
"primary": "Primair",
|
"primary": "Primair",
|
||||||
"upload": "Uploaden",
|
"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": {
|
"home": {
|
||||||
"desc_1": "Ontdek, plan en verken met gemak",
|
"desc_1": "Ontdek, plan en verken met gemak",
|
||||||
|
@ -254,7 +266,7 @@
|
||||||
"about": "Over AdventureLog",
|
"about": "Over AdventureLog",
|
||||||
"adventures": "Avonturen",
|
"adventures": "Avonturen",
|
||||||
"collections": "Collecties",
|
"collections": "Collecties",
|
||||||
"discord": "discord",
|
"discord": "Discord",
|
||||||
"documentation": "Documentatie",
|
"documentation": "Documentatie",
|
||||||
"greeting": "Hoi",
|
"greeting": "Hoi",
|
||||||
"logout": "Uitloggen",
|
"logout": "Uitloggen",
|
||||||
|
@ -281,7 +293,8 @@
|
||||||
"tag": "Label",
|
"tag": "Label",
|
||||||
"language_selection": "Taal",
|
"language_selection": "Taal",
|
||||||
"support": "Steun",
|
"support": "Steun",
|
||||||
"calendar": "Kalender"
|
"calendar": "Kalender",
|
||||||
|
"admin_panel": "Admin -paneel"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"confirm_password": "Bevestig wachtwoord",
|
"confirm_password": "Bevestig wachtwoord",
|
||||||
|
@ -302,7 +315,11 @@
|
||||||
"both_passwords_required": "Beide wachtwoorden zijn vereist",
|
"both_passwords_required": "Beide wachtwoorden zijn vereist",
|
||||||
"new_password": "Nieuw wachtwoord",
|
"new_password": "Nieuw wachtwoord",
|
||||||
"reset_failed": "Kan het wachtwoord niet opnieuw instellen",
|
"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": {
|
"users": {
|
||||||
"no_users_found": "Er zijn geen gebruikers gevonden met openbare profielen."
|
"no_users_found": "Er zijn geen gebruikers gevonden met openbare profielen."
|
||||||
|
@ -325,8 +342,8 @@
|
||||||
"no_cities_found": "Geen steden gevonden",
|
"no_cities_found": "Geen steden gevonden",
|
||||||
"region_failed_visited": "Kan de regio niet als bezocht markeren",
|
"region_failed_visited": "Kan de regio niet als bezocht markeren",
|
||||||
"region_stats": "Regiostatistieken",
|
"region_stats": "Regiostatistieken",
|
||||||
"regions_in": "Regio's binnen",
|
"regions_in": "Regio's in",
|
||||||
"removed": "VERWIJDERD",
|
"removed": "verwijderd",
|
||||||
"view_cities": "Steden bekijken",
|
"view_cities": "Steden bekijken",
|
||||||
"visit_remove_failed": "Kan bezoek niet verwijderen",
|
"visit_remove_failed": "Kan bezoek niet verwijderen",
|
||||||
"visit_to": "Bezoek aan"
|
"visit_to": "Bezoek aan"
|
||||||
|
@ -358,8 +375,8 @@
|
||||||
"join_discord": "Sluit je aan bij Discord",
|
"join_discord": "Sluit je aan bij Discord",
|
||||||
"join_discord_desc": "om uw eigen foto's te delen. \nPlaats ze in de",
|
"join_discord_desc": "om uw eigen foto's te delen. \nPlaats ze in de",
|
||||||
"photo_by": "Foto door",
|
"photo_by": "Foto door",
|
||||||
"change_password_error": "Kan wachtwoord niet wijzigen. \nOngeldig huidig wachtwoord of ongeldig nieuw wachtwoord.",
|
"change_password_error": "Kan wachtwoord niet wijzigen. \nOngeldig huidig wachtwoord of ongeldig nieuw wachtwoord.",
|
||||||
"current_password": "Huidig wachtwoord",
|
"current_password": "Huidig wachtwoord",
|
||||||
"password_change_lopout_warning": "Na het wijzigen van uw wachtwoord wordt u uitgelogd.",
|
"password_change_lopout_warning": "Na het wijzigen van uw wachtwoord wordt u uitgelogd.",
|
||||||
"authenticator_code": "Authenticatiecode",
|
"authenticator_code": "Authenticatiecode",
|
||||||
"copy": "Kopiëren",
|
"copy": "Kopiëren",
|
||||||
|
@ -369,7 +386,7 @@
|
||||||
"email_removed": "E-mail succesvol verwijderd!",
|
"email_removed": "E-mail succesvol verwijderd!",
|
||||||
"email_removed_error": "Fout bij verwijderen van e-mail",
|
"email_removed_error": "Fout bij verwijderen van e-mail",
|
||||||
"email_set_primary": "E-mailadres is succesvol ingesteld als primair!",
|
"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": "E-mail succesvol geverifieerd!",
|
||||||
"email_verified_erorr_desc": "Uw e-mailadres kan niet worden geverifieerd. \nProbeer het opnieuw.",
|
"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",
|
"email_verified_error": "Fout bij het verifiëren van e-mailadres",
|
||||||
|
@ -403,8 +420,8 @@
|
||||||
"username_taken": "Deze gebruikersnaam is al in gebruik.",
|
"username_taken": "Deze gebruikersnaam is al in gebruik.",
|
||||||
"administration_settings": "Beheerinstellingen",
|
"administration_settings": "Beheerinstellingen",
|
||||||
"documentation_link": "Documentatielink",
|
"documentation_link": "Documentatielink",
|
||||||
"launch_account_connections": "Start Accountverbindingen",
|
"launch_account_connections": "Start accountverbindingen",
|
||||||
"launch_administration_panel": "Start het Beheerpaneel",
|
"launch_administration_panel": "Start het beheerpaneel",
|
||||||
"no_verified_email_warning": "U moet een geverifieerd e-mailadres hebben om tweefactorauthenticatie in te schakelen.",
|
"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": "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.",
|
"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": {
|
"checklist": {
|
||||||
"add_item": "Artikel toevoegen",
|
"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_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.",
|
"checklist_public": "Deze controlelijst is openbaar omdat deze zich in een openbare collectie bevindt.",
|
||||||
"editing_checklist": "Controlelijst bewerken",
|
"editing_checklist": "Controlelijst bewerken",
|
||||||
"failed_to_save": "Kan controlelijst niet opslaan",
|
"failed_to_save": "Kan controlelijst niet opslaan",
|
||||||
|
@ -427,7 +444,7 @@
|
||||||
"new_item": "Nieuw artikel",
|
"new_item": "Nieuw artikel",
|
||||||
"save": "Opslaan",
|
"save": "Opslaan",
|
||||||
"checklist_viewer": "Controlelijstviewer",
|
"checklist_viewer": "Controlelijstviewer",
|
||||||
"new_checklist": "Nieuwe checklist"
|
"new_checklist": "Nieuwe controlelijst"
|
||||||
},
|
},
|
||||||
"collection": {
|
"collection": {
|
||||||
"collection_created": "Collectie succesvol aangemaakt!",
|
"collection_created": "Collectie succesvol aangemaakt!",
|
||||||
|
@ -435,7 +452,7 @@
|
||||||
"create": "Aanmaken",
|
"create": "Aanmaken",
|
||||||
"edit_collection": "Collectie bewerken",
|
"edit_collection": "Collectie bewerken",
|
||||||
"error_creating_collection": "Fout bij aanmaken collectie",
|
"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",
|
"new_collection": "Nieuwe collectie",
|
||||||
"public_collection": "Openbare collectie"
|
"public_collection": "Openbare collectie"
|
||||||
},
|
},
|
||||||
|
@ -446,22 +463,22 @@
|
||||||
"failed_to_save": "Kan opmerking niet opslaan",
|
"failed_to_save": "Kan opmerking niet opslaan",
|
||||||
"note_delete_error": "Fout bij verwijderen van opmerking",
|
"note_delete_error": "Fout bij verwijderen van opmerking",
|
||||||
"note_deleted": "Opmerking succesvol verwijderd!",
|
"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.",
|
"note_public": "Deze opmerking is openbaar omdat deze zich in een openbare collectie bevindt.",
|
||||||
"open": "Open",
|
"open": "Open",
|
||||||
"save": "Opslaan",
|
"save": "Opslaan",
|
||||||
"invalid_url": "Ongeldige URL",
|
"invalid_url": "Ongeldige URL",
|
||||||
"note_viewer": "Notitieviewer"
|
"note_viewer": "Bekijk notities"
|
||||||
},
|
},
|
||||||
"transportation": {
|
"transportation": {
|
||||||
"date_and_time": "Datum",
|
"date_and_time": "Datum",
|
||||||
"date_time": "Startdatum",
|
"date_time": "Startdatum",
|
||||||
"edit": "Bewerking",
|
"edit": "Bewerk",
|
||||||
"edit_transportation": "Vervoer bewerken",
|
"edit_transportation": "Vervoer bewerken",
|
||||||
"end_date_time": "Einddatum",
|
"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",
|
"flight_number": "Vluchtnummer",
|
||||||
"from_location": "Van locatie",
|
"from_location": "Vertreklocatie",
|
||||||
"modes": {
|
"modes": {
|
||||||
"bike": "Fiets",
|
"bike": "Fiets",
|
||||||
"boat": "Boot",
|
"boat": "Boot",
|
||||||
|
@ -470,9 +487,9 @@
|
||||||
"car": "Auto",
|
"car": "Auto",
|
||||||
"other": "Ander",
|
"other": "Ander",
|
||||||
"plane": "Vliegtuig",
|
"plane": "Vliegtuig",
|
||||||
"walking": "Lopen"
|
"walking": "Wandelen"
|
||||||
},
|
},
|
||||||
"to_location": "Naar locatie",
|
"to_location": "Aankomstlocatie",
|
||||||
"transportation_edit_success": "Vervoer succesvol bewerkt!",
|
"transportation_edit_success": "Vervoer succesvol bewerkt!",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"new_transportation": "Nieuw vervoer",
|
"new_transportation": "Nieuw vervoer",
|
||||||
|
@ -481,7 +498,10 @@
|
||||||
"transport_type": "Vervoerstype",
|
"transport_type": "Vervoerstype",
|
||||||
"transportation_added": "Vervoer succesvol toegevoegd!",
|
"transportation_added": "Vervoer succesvol toegevoegd!",
|
||||||
"transportation_delete_error": "Fout bij verwijderen vervoer",
|
"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": {
|
"search": {
|
||||||
"adventurelog_results": "AdventureLog-resultaten",
|
"adventurelog_results": "AdventureLog-resultaten",
|
||||||
|
@ -490,7 +510,7 @@
|
||||||
},
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"add_adventure": "Voeg nieuw avontuur toe",
|
"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",
|
"adventure_map": "Avonturenkaart",
|
||||||
"clear_marker": "Verwijder markeerpunt",
|
"clear_marker": "Verwijder markeerpunt",
|
||||||
"map_options": "Kaartopties",
|
"map_options": "Kaartopties",
|
||||||
|
@ -520,10 +540,10 @@
|
||||||
"categories": {
|
"categories": {
|
||||||
"category_name": "Categorienaam",
|
"category_name": "Categorienaam",
|
||||||
"edit_category": "Categorie bewerken",
|
"edit_category": "Categorie bewerken",
|
||||||
"icon": "Ikoon",
|
"icon": "Icoon",
|
||||||
"manage_categories": "Beheer categorieën",
|
"manage_categories": "Beheer categorieën",
|
||||||
"no_categories_found": "Geen categorieën gevonden.",
|
"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."
|
"update_after_refresh": "De avonturenkaarten worden bijgewerkt zodra u de pagina vernieuwt."
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
|
@ -538,7 +558,7 @@
|
||||||
},
|
},
|
||||||
"immich": {
|
"immich": {
|
||||||
"api_key": "Immich API-sleutel",
|
"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",
|
"disable": "Uitzetten",
|
||||||
"enable_immich": "Schakel Immich in",
|
"enable_immich": "Schakel Immich in",
|
||||||
"imageid_required": "Afbeeldings-ID is vereist",
|
"imageid_required": "Afbeeldings-ID is vereist",
|
||||||
|
@ -559,7 +579,7 @@
|
||||||
"server_url": "Immich-server-URL",
|
"server_url": "Immich-server-URL",
|
||||||
"update_integration": "Integratie bijwerken",
|
"update_integration": "Integratie bijwerken",
|
||||||
"documentation": "Immich-integratiedocumentatie",
|
"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": {
|
"recomendations": {
|
||||||
"address": "Adres",
|
"address": "Adres",
|
||||||
|
@ -567,5 +587,35 @@
|
||||||
"phone": "Telefoon",
|
"phone": "Telefoon",
|
||||||
"recommendation": "Aanbeveling",
|
"recommendation": "Aanbeveling",
|
||||||
"website": "Website"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,8 @@
|
||||||
"aqua": "Aqua",
|
"aqua": "Aqua",
|
||||||
"northernLights": "Zorza Polarna"
|
"northernLights": "Zorza Polarna"
|
||||||
},
|
},
|
||||||
"calendar": "Kalendarz"
|
"calendar": "Kalendarz",
|
||||||
|
"admin_panel": "Panel administracyjny"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"about": "O aplikacji",
|
"about": "O aplikacji",
|
||||||
|
@ -281,7 +282,19 @@
|
||||||
"images": "Obrazy",
|
"images": "Obrazy",
|
||||||
"primary": "Podstawowy",
|
"primary": "Podstawowy",
|
||||||
"upload": "Wgrywać",
|
"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": {
|
"worldtravel": {
|
||||||
"country_list": "Lista krajów",
|
"country_list": "Lista krajów",
|
||||||
|
@ -326,7 +339,11 @@
|
||||||
"both_passwords_required": "Obydwa hasła są wymagane",
|
"both_passwords_required": "Obydwa hasła są wymagane",
|
||||||
"new_password": "Nowe hasło",
|
"new_password": "Nowe hasło",
|
||||||
"reset_failed": "Nie udało się zresetować hasła",
|
"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": {
|
"users": {
|
||||||
"no_users_found": "Nie znaleziono użytkowników z publicznymi profilami."
|
"no_users_found": "Nie znaleziono użytkowników z publicznymi profilami."
|
||||||
|
@ -481,7 +498,10 @@
|
||||||
"transportation_edit_success": "Transport edytowany pomyślnie!",
|
"transportation_edit_success": "Transport edytowany pomyślnie!",
|
||||||
"edit_transportation": "Edytuj transport",
|
"edit_transportation": "Edytuj transport",
|
||||||
"start": "Początek",
|
"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": {
|
"search": {
|
||||||
"adventurelog_results": "Wyniki AdventureLog",
|
"adventurelog_results": "Wyniki AdventureLog",
|
||||||
|
@ -567,5 +587,35 @@
|
||||||
"phone": "Telefon",
|
"phone": "Telefon",
|
||||||
"recommendation": "Zalecenie",
|
"recommendation": "Zalecenie",
|
||||||
"website": "Strona internetowa"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -234,7 +234,19 @@
|
||||||
"images": "Bilder",
|
"images": "Bilder",
|
||||||
"primary": "Primär",
|
"primary": "Primär",
|
||||||
"upload": "Ladda upp",
|
"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": {
|
"home": {
|
||||||
"desc_1": "Upptäck, planera och utforska med lätthet",
|
"desc_1": "Upptäck, planera och utforska med lätthet",
|
||||||
|
@ -281,7 +293,8 @@
|
||||||
"tag": "Tagg",
|
"tag": "Tagg",
|
||||||
"language_selection": "Språk",
|
"language_selection": "Språk",
|
||||||
"support": "Support",
|
"support": "Support",
|
||||||
"calendar": "Kalender"
|
"calendar": "Kalender",
|
||||||
|
"admin_panel": "Administratör"
|
||||||
},
|
},
|
||||||
"worldtravel": {
|
"worldtravel": {
|
||||||
"all": "Alla",
|
"all": "Alla",
|
||||||
|
@ -326,7 +339,11 @@
|
||||||
"both_passwords_required": "Båda lösenorden krävs",
|
"both_passwords_required": "Båda lösenorden krävs",
|
||||||
"new_password": "Nytt lösenord",
|
"new_password": "Nytt lösenord",
|
||||||
"reset_failed": "Det gick inte att återställa lösenordet",
|
"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": {
|
"users": {
|
||||||
"no_users_found": "Inga användare hittades med offentliga profiler."
|
"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_delete_error": "Det gick inte att ta bort transport",
|
||||||
"transportation_deleted": "Transporten har raderats!",
|
"transportation_deleted": "Transporten har raderats!",
|
||||||
"transportation_edit_success": "Transporten har redigerats!",
|
"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": {
|
"search": {
|
||||||
"adventurelog_results": "AdventureLog-resultat",
|
"adventurelog_results": "AdventureLog-resultat",
|
||||||
|
@ -567,5 +587,35 @@
|
||||||
"phone": "Telefon",
|
"phone": "Telefon",
|
||||||
"recommendation": "Rekommendation",
|
"recommendation": "Rekommendation",
|
||||||
"website": "Webbplats"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -234,7 +234,19 @@
|
||||||
"images": "图片",
|
"images": "图片",
|
||||||
"primary": "基本的",
|
"primary": "基本的",
|
||||||
"upload": "上传",
|
"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": {
|
"home": {
|
||||||
"desc_1": "轻松发现、规划和探索",
|
"desc_1": "轻松发现、规划和探索",
|
||||||
|
@ -281,7 +293,8 @@
|
||||||
"tag": "标签",
|
"tag": "标签",
|
||||||
"language_selection": "语言",
|
"language_selection": "语言",
|
||||||
"support": "支持",
|
"support": "支持",
|
||||||
"calendar": "日历"
|
"calendar": "日历",
|
||||||
|
"admin_panel": "管理面板"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"forgot_password": "忘记密码?",
|
"forgot_password": "忘记密码?",
|
||||||
|
@ -302,7 +315,11 @@
|
||||||
"both_passwords_required": "两个密码都需要",
|
"both_passwords_required": "两个密码都需要",
|
||||||
"new_password": "新密码",
|
"new_password": "新密码",
|
||||||
"reset_failed": "重置密码失败",
|
"reset_failed": "重置密码失败",
|
||||||
"or_3rd_party": "或者使用第三方服务登录"
|
"or_3rd_party": "或者使用第三方服务登录",
|
||||||
|
"no_public_adventures": "找不到公共冒险",
|
||||||
|
"no_public_collections": "找不到公共收藏",
|
||||||
|
"user_adventures": "用户冒险",
|
||||||
|
"user_collections": "用户收集"
|
||||||
},
|
},
|
||||||
"worldtravel": {
|
"worldtravel": {
|
||||||
"all": "全部",
|
"all": "全部",
|
||||||
|
@ -481,7 +498,10 @@
|
||||||
"transportation_delete_error": "删除交通时出错",
|
"transportation_delete_error": "删除交通时出错",
|
||||||
"transportation_deleted": "交通删除成功!",
|
"transportation_deleted": "交通删除成功!",
|
||||||
"transportation_edit_success": "交通编辑成功!",
|
"transportation_edit_success": "交通编辑成功!",
|
||||||
"type": "类型"
|
"type": "类型",
|
||||||
|
"ending_airport_desc": "输入结束机场代码(例如LAX)",
|
||||||
|
"fetch_location_information": "获取位置信息",
|
||||||
|
"starting_airport_desc": "输入启动机场代码(例如肯尼迪国际机构)"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"adventurelog_results": "冒险日志结果",
|
"adventurelog_results": "冒险日志结果",
|
||||||
|
@ -567,5 +587,35 @@
|
||||||
"phone": "电话",
|
"phone": "电话",
|
||||||
"recommendation": "推荐",
|
"recommendation": "推荐",
|
||||||
"website": "网站"
|
"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": "当前时区"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,9 @@
|
||||||
register('nl', () => import('../locales/nl.json'));
|
register('nl', () => import('../locales/nl.json'));
|
||||||
register('sv', () => import('../locales/sv.json'));
|
register('sv', () => import('../locales/sv.json'));
|
||||||
register('pl', () => import('../locales/pl.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) {
|
if (browser) {
|
||||||
init({
|
init({
|
||||||
|
|
|
@ -2,7 +2,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||||
import { redirect, type Actions } from '@sveltejs/kit';
|
import { redirect, type Actions } from '@sveltejs/kit';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import psl from 'psl';
|
import psl from 'psl';
|
||||||
import { themes } from '$lib';
|
import { getRandomBackground, themes } from '$lib';
|
||||||
import { fetchCSRFToken } from '$lib/index.server';
|
import { fetchCSRFToken } from '$lib/index.server';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
@ -11,6 +11,13 @@ const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
export const load = (async (event) => {
|
export const load = (async (event) => {
|
||||||
if (event.locals.user) {
|
if (event.locals.user) {
|
||||||
return redirect(302, '/dashboard');
|
return redirect(302, '/dashboard');
|
||||||
|
} else {
|
||||||
|
const background = getRandomBackground();
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
background
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
|
@ -51,8 +58,10 @@ export const actions: Actions = {
|
||||||
|
|
||||||
// Check if hostname is an IP address
|
// Check if hostname is an IP address
|
||||||
const isIPAddress = /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname);
|
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);
|
const parsed = psl.parse(hostname);
|
||||||
|
|
||||||
if (parsed && parsed.domain) {
|
if (parsed && parsed.domain) {
|
||||||
|
|
|
@ -1,45 +1,47 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import AdventureOverlook from '$lib/assets/AdventureOverlook.webp';
|
|
||||||
import MapWithPins from '$lib/assets/MapWithPins.webp';
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
import MapWithPins from '$lib/assets/MapWithPins.webp';
|
||||||
|
import type { Background } from '$lib/types.js';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
|
let background: Background = data.props?.background ?? { url: '' };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="flex items-center justify-center w-full py-12 md:py-24 lg:py-32">
|
<!-- Hero Section -->
|
||||||
<div class="container px-4 md:px-6">
|
<section class="flex items-center justify-center w-full py-20 bg-gray-50 dark:bg-gray-800">
|
||||||
<div class="grid gap-6 lg:grid-cols-[1fr_550px] lg:gap-12 xl:grid-cols-[1fr_650px]">
|
<div class="container mx-auto px-4 flex flex-col-reverse md:flex-row items-center gap-8">
|
||||||
<div class="flex flex-col justify-center space-y-4">
|
<!-- Text Content -->
|
||||||
<div class="space-y-2">
|
<div class="w-full md:w-1/2 space-y-6">
|
||||||
{#if data.user}
|
{#if data.user}
|
||||||
{#if data.user.first_name && data.user.first_name !== null}
|
{#if data.user.first_name && data.user.first_name !== null}
|
||||||
<h1
|
<h1
|
||||||
class="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pb-4"
|
class="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||||
>
|
>
|
||||||
{data.user.first_name.charAt(0).toUpperCase() + data.user.first_name.slice(1)},
|
{data.user.first_name.charAt(0).toUpperCase() + data.user.first_name.slice(1)}, {$t(
|
||||||
{$t('home.hero_1')}
|
'home.hero_1'
|
||||||
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
{:else}
|
{:else}
|
||||||
<h1
|
<h1
|
||||||
class="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pb-4"
|
class="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||||
>
|
>
|
||||||
{$t('home.hero_1')}
|
{$t('home.hero_1')}
|
||||||
</h1>
|
</h1>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<h1
|
<h1
|
||||||
class="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pb-4"
|
class="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||||
>
|
>
|
||||||
{$t('home.hero_1')}
|
{$t('home.hero_1')}
|
||||||
</h1>
|
</h1>
|
||||||
{/if}
|
{/if}
|
||||||
<p class="max-w-[600px] text-gray-500 md:text-xl dark:text-gray-400">
|
<p class="text-xl text-gray-600 dark:text-gray-300 max-w-xl">
|
||||||
{$t('home.hero_2')}
|
{$t('home.hero_2')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<div class="flex flex-col sm:flex-row gap-4">
|
||||||
<div class="flex flex-col gap-2 min-[400px]:flex-row">
|
|
||||||
{#if data.user}
|
{#if data.user}
|
||||||
<button on:click={() => goto('/adventures')} class="btn btn-primary">
|
<button on:click={() => goto('/adventures')} class="btn btn-primary">
|
||||||
{$t('home.go_to')}
|
{$t('home.go_to')}
|
||||||
|
@ -48,79 +50,68 @@
|
||||||
<button on:click={() => goto('/login')} class="btn btn-primary">
|
<button on:click={() => goto('/login')} class="btn btn-primary">
|
||||||
{$t('auth.login')}
|
{$t('auth.login')}
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => goto('/signup')} class="btn btn-neutral">
|
<button on:click={() => goto('/signup')} class="btn btn-secondary">
|
||||||
{$t('auth.signup')}
|
{$t('auth.signup')}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Image -->
|
||||||
|
<div class="w-full md:w-1/2">
|
||||||
<img
|
<img
|
||||||
src={AdventureOverlook}
|
src={background.url}
|
||||||
width="550"
|
alt={background.location}
|
||||||
height="550"
|
class="rounded-lg shadow-lg object-cover w-full h-full"
|
||||||
alt="Hero"
|
|
||||||
class="mx-auto aspect-video overflow-hidden rounded-xl object-cover sm:w-full lg:order-last"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section
|
|
||||||
class="flex items-center justify-center w-full py-12 md:py-24 lg:py-32 bg-gray-100 dark:bg-gray-800"
|
<!-- Features Section -->
|
||||||
>
|
<section id="features" class="py-16 bg-white dark:bg-gray-900">
|
||||||
<div class="container px-4 md:px-6">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex flex-col items-center justify-center space-y-4 text-center">
|
<div class="text-center mb-12">
|
||||||
<div class="space-y-2">
|
<div class="inline-block text-neutral-content bg-neutral px-4 py-2 rounded-full">
|
||||||
<div
|
|
||||||
class="inline-block rounded-lg bg-gray-100 px-3 py-1 text-md dark:bg-gray-800 dark:text-gray-400"
|
|
||||||
>
|
|
||||||
{$t('home.key_features')}
|
{$t('home.key_features')}
|
||||||
</div>
|
</div>
|
||||||
<h2
|
<h2
|
||||||
class="text-3xl font-bold tracking-tighter sm:text-5xl bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
class="mt-4 text-3xl md:text-4xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||||
>
|
>
|
||||||
{$t('home.desc_1')}
|
{$t('home.desc_1')}
|
||||||
</h2>
|
</h2>
|
||||||
<p
|
<p class="mt-4 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto text-lg">
|
||||||
class="max-w-[900px] text-gray-500 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed dark:text-gray-400"
|
|
||||||
>
|
|
||||||
{$t('home.desc_2')}
|
{$t('home.desc_2')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
||||||
<div class="mx-auto grid max-w-5xl items-center gap-6 py-12 lg:grid-cols-2 lg:gap-12">
|
<!-- Image for Features -->
|
||||||
<!-- svelte-ignore a11y-img-redundant-alt -->
|
<div class="order-1 md:order-2">
|
||||||
<img
|
<img
|
||||||
src={MapWithPins}
|
src={MapWithPins}
|
||||||
width="550"
|
alt="World map with pins"
|
||||||
height="310"
|
class="rounded-lg shadow-lg object-cover"
|
||||||
alt="Image"
|
|
||||||
class="mx-auto aspect-video overflow-hidden rounded-xl object-cover object-center sm:w-full lg:order-last"
|
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col justify-center space-y-4">
|
</div>
|
||||||
<ul class="grid gap-6">
|
<!-- Feature List -->
|
||||||
<li>
|
<div class="order-2 md:order-1">
|
||||||
<div class="grid gap-1">
|
<ul class="space-y-6">
|
||||||
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_1')}</h3>
|
<li class="space-y-2">
|
||||||
<p class="text-gray-500 dark:text-gray-400">
|
<h3 class="text-xl font-semibold dark:text-gray-300">{$t('home.feature_1')}</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
{$t('home.feature_1_desc')}
|
{$t('home.feature_1_desc')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li class="space-y-2">
|
||||||
<div class="grid gap-1">
|
<h3 class="text-xl font-semibold dark:text-gray-300">{$t('home.feature_2')}</h3>
|
||||||
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_2')}</h3>
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
<p class="text-gray-500 dark:text-gray-400">
|
|
||||||
{$t('home.feature_2_desc')}
|
{$t('home.feature_2_desc')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li class="space-y-2">
|
||||||
<div class="grid gap-1">
|
<h3 class="text-xl font-semibold dark:text-gray-300">{$t('home.feature_3')}</h3>
|
||||||
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_3')}</h3>
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
<p class="text-gray-500 dark:text-gray-400">
|
|
||||||
{$t('home.feature_3_desc')}
|
{$t('home.feature_3_desc')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
17
frontend/src/routes/admin/+page.server.ts
Normal file
17
frontend/src/routes/admin/+page.server.ts
Normal file
|
@ -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/');
|
||||||
|
};
|
|
@ -261,7 +261,7 @@
|
||||||
<CategoryFilterDropdown bind:types={typeString} />
|
<CategoryFilterDropdown bind:types={typeString} />
|
||||||
<button
|
<button
|
||||||
on:click={() => (is_category_modal_open = true)}
|
on:click={() => (is_category_modal_open = true)}
|
||||||
class="btn btn-neutral btn-sm min-w-full">Manage Categories</button
|
class="btn btn-neutral btn-sm min-w-full">{$t('categories.manage_categories')}</button
|
||||||
>
|
>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<h3 class="text-center font-bold text-lg mb-4">{$t('adventures.sort')}</h3>
|
<h3 class="text-center font-bold text-lg mb-4">{$t('adventures.sort')}</h3>
|
||||||
|
|
|
@ -185,7 +185,10 @@
|
||||||
alt={adventure.name}
|
alt={adventure.name}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<div class="flex justify-center w-full py-2 gap-2">
|
<!-- Scrollable button container -->
|
||||||
|
<div
|
||||||
|
class="flex w-full py-2 gap-2 overflow-x-auto whitespace-nowrap scrollbar-hide justify-start"
|
||||||
|
>
|
||||||
{#each adventure.images as _, i}
|
{#each adventure.images as _, i}
|
||||||
<button
|
<button
|
||||||
on:click={() => goToSlide(i)}
|
on:click={() => goToSlide(i)}
|
||||||
|
@ -197,6 +200,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="grid gap-4">
|
<div class="grid gap-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
@ -221,6 +225,43 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
|
{#if adventure.user}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{#if adventure.user.profile_pic}
|
||||||
|
<div class="avatar">
|
||||||
|
<div class="w-8 rounded-full">
|
||||||
|
<img src={adventure.user.profile_pic} alt={adventure.user.username} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="avatar placeholder">
|
||||||
|
<div class="bg-neutral text-neutral-content w-8 rounded-full">
|
||||||
|
<span class="text-lg"
|
||||||
|
>{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)
|
||||||
|
: ''}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if adventure.user.public_profile}
|
||||||
|
<a href={`/profile/${adventure.user.username}`} class="text-base font-medium">
|
||||||
|
{adventure.user.first_name || adventure.user.username}{' '}
|
||||||
|
{adventure.user.last_name}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<span class="text-base font-medium">
|
||||||
|
{adventure.user.first_name || adventure.user.username}{' '}
|
||||||
|
{adventure.user.last_name}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@ -241,6 +282,7 @@
|
||||||
>{adventure.is_public ? 'Public' : 'Private'}</span
|
>{adventure.is_public ? 'Public' : 'Private'}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if adventure.location}
|
{#if adventure.location}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<svg
|
<svg
|
||||||
|
|
|
@ -33,6 +33,6 @@
|
||||||
<!-- download calendar -->
|
<!-- download calendar -->
|
||||||
<div class="flex items-center justify-center mt-4">
|
<div class="flex items-center justify-center mt-4">
|
||||||
<a href={icsCalendarDataUrl} download="adventures.ics" class="btn btn-primary"
|
<a href={icsCalendarDataUrl} download="adventures.ics" class="btn btn-primary"
|
||||||
>Download Calendar</a
|
>{$t('adventures.download_calendar')}</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Adventure, Checklist, Collection, Note, Transportation } from '$lib/types';
|
import type { Adventure, Checklist, Collection, Lodging, Note, Transportation } from '$lib/types';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { marked } from 'marked'; // Import the markdown parser
|
import { marked } from 'marked'; // Import the markdown parser
|
||||||
|
@ -27,7 +27,8 @@
|
||||||
groupNotesByDate,
|
groupNotesByDate,
|
||||||
groupTransportationsByDate,
|
groupTransportationsByDate,
|
||||||
groupChecklistsByDate,
|
groupChecklistsByDate,
|
||||||
osmTagToEmoji
|
osmTagToEmoji,
|
||||||
|
groupLodgingByDate
|
||||||
} from '$lib';
|
} from '$lib';
|
||||||
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
|
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
|
||||||
import ChecklistModal from '$lib/components/ChecklistModal.svelte';
|
import ChecklistModal from '$lib/components/ChecklistModal.svelte';
|
||||||
|
@ -35,6 +36,8 @@
|
||||||
import TransportationModal from '$lib/components/TransportationModal.svelte';
|
import TransportationModal from '$lib/components/TransportationModal.svelte';
|
||||||
import CardCarousel from '$lib/components/CardCarousel.svelte';
|
import CardCarousel from '$lib/components/CardCarousel.svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import LodgingModal from '$lib/components/LodgingModal.svelte';
|
||||||
|
import LodgingCard from '$lib/components/LodgingCard.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
@ -103,6 +106,19 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lodging) {
|
||||||
|
dates = dates.concat(
|
||||||
|
lodging
|
||||||
|
.filter((i) => i.check_in)
|
||||||
|
.map((lodging) => ({
|
||||||
|
id: lodging.id,
|
||||||
|
start: lodging.check_in || '', // Ensure it's a string
|
||||||
|
end: lodging.check_out || lodging.check_in || '', // Ensure it's a string
|
||||||
|
title: lodging.name
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Update `options.events` when `dates` changes
|
// Update `options.events` when `dates` changes
|
||||||
options = { ...options, events: dates };
|
options = { ...options, events: dates };
|
||||||
}
|
}
|
||||||
|
@ -115,6 +131,7 @@
|
||||||
let numAdventures: number = 0;
|
let numAdventures: number = 0;
|
||||||
|
|
||||||
let transportations: Transportation[] = [];
|
let transportations: Transportation[] = [];
|
||||||
|
let lodging: Lodging[] = [];
|
||||||
let notes: Note[] = [];
|
let notes: Note[] = [];
|
||||||
let checklists: Checklist[] = [];
|
let checklists: Checklist[] = [];
|
||||||
|
|
||||||
|
@ -174,6 +191,9 @@
|
||||||
if (collection.transportations) {
|
if (collection.transportations) {
|
||||||
transportations = collection.transportations;
|
transportations = collection.transportations;
|
||||||
}
|
}
|
||||||
|
if (collection.lodging) {
|
||||||
|
lodging = collection.lodging;
|
||||||
|
}
|
||||||
if (collection.notes) {
|
if (collection.notes) {
|
||||||
notes = collection.notes;
|
notes = collection.notes;
|
||||||
}
|
}
|
||||||
|
@ -243,6 +263,8 @@
|
||||||
|
|
||||||
let adventureToEdit: Adventure | null = null;
|
let adventureToEdit: Adventure | null = null;
|
||||||
let transportationToEdit: Transportation | null = null;
|
let transportationToEdit: Transportation | null = null;
|
||||||
|
let isShowingLodgingModal: boolean = false;
|
||||||
|
let lodgingToEdit: Lodging | null = null;
|
||||||
let isAdventureModalOpen: boolean = false;
|
let isAdventureModalOpen: boolean = false;
|
||||||
let isNoteModalOpen: boolean = false;
|
let isNoteModalOpen: boolean = false;
|
||||||
let noteToEdit: Note | null;
|
let noteToEdit: Note | null;
|
||||||
|
@ -260,6 +282,11 @@
|
||||||
isShowingTransportationModal = true;
|
isShowingTransportationModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function editLodging(event: CustomEvent<Lodging>) {
|
||||||
|
lodgingToEdit = event.detail;
|
||||||
|
isShowingLodgingModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
function saveOrCreateAdventure(event: CustomEvent<Adventure>) {
|
function saveOrCreateAdventure(event: CustomEvent<Adventure>) {
|
||||||
if (adventures.find((adventure) => adventure.id === event.detail.id)) {
|
if (adventures.find((adventure) => adventure.id === event.detail.id)) {
|
||||||
adventures = adventures.map((adventure) => {
|
adventures = adventures.map((adventure) => {
|
||||||
|
@ -355,6 +382,22 @@
|
||||||
}
|
}
|
||||||
isShowingTransportationModal = false;
|
isShowingTransportationModal = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveOrCreateLodging(event: CustomEvent<Lodging>) {
|
||||||
|
if (lodging.find((lodging) => lodging.id === event.detail.id)) {
|
||||||
|
// Update existing hotel
|
||||||
|
lodging = lodging.map((lodging) => {
|
||||||
|
if (lodging.id === event.detail.id) {
|
||||||
|
return event.detail;
|
||||||
|
}
|
||||||
|
return lodging;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create new lodging
|
||||||
|
lodging = [event.detail, ...lodging];
|
||||||
|
}
|
||||||
|
isShowingLodgingModal = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isShowingLinkModal}
|
{#if isShowingLinkModal}
|
||||||
|
@ -376,6 +419,15 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if isShowingLodgingModal}
|
||||||
|
<LodgingModal
|
||||||
|
{lodgingToEdit}
|
||||||
|
on:close={() => (isShowingLodgingModal = false)}
|
||||||
|
on:save={saveOrCreateLodging}
|
||||||
|
{collection}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if isAdventureModalOpen}
|
{#if isAdventureModalOpen}
|
||||||
<AdventureModal
|
<AdventureModal
|
||||||
{adventureToEdit}
|
{adventureToEdit}
|
||||||
|
@ -501,6 +553,16 @@
|
||||||
>
|
>
|
||||||
{$t('adventures.checklist')}</button
|
{$t('adventures.checklist')}</button
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
on:click={() => {
|
||||||
|
isShowingLodgingModal = true;
|
||||||
|
newType = '';
|
||||||
|
lodgingToEdit = null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$t('adventures.lodging')}</button
|
||||||
|
>
|
||||||
|
|
||||||
<!-- <button
|
<!-- <button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
|
@ -542,7 +604,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if collection && !collection.start_date && adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0}
|
{#if collection && !collection.start_date && adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0 && lodging.length == 0}
|
||||||
<NotFound error={undefined} />
|
<NotFound error={undefined} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -654,6 +716,63 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if lodging.length > 0}
|
||||||
|
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.lodging')}</h1>
|
||||||
|
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||||
|
{#each lodging as hotel}
|
||||||
|
<LodgingCard
|
||||||
|
lodging={hotel}
|
||||||
|
user={data?.user}
|
||||||
|
on:delete={(event) => {
|
||||||
|
lodging = lodging.filter((t) => t.id != event.detail);
|
||||||
|
}}
|
||||||
|
on:edit={editLodging}
|
||||||
|
{collection}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if notes.length > 0}
|
||||||
|
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.notes')}</h1>
|
||||||
|
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||||
|
{#each notes as note}
|
||||||
|
<NoteCard
|
||||||
|
{note}
|
||||||
|
user={data.user || null}
|
||||||
|
on:edit={(event) => {
|
||||||
|
noteToEdit = event.detail;
|
||||||
|
isNoteModalOpen = true;
|
||||||
|
}}
|
||||||
|
on:delete={(event) => {
|
||||||
|
notes = notes.filter((n) => n.id != event.detail);
|
||||||
|
}}
|
||||||
|
{collection}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if checklists.length > 0}
|
||||||
|
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.checklists')}</h1>
|
||||||
|
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||||
|
{#each checklists as checklist}
|
||||||
|
<ChecklistCard
|
||||||
|
{checklist}
|
||||||
|
user={data.user || null}
|
||||||
|
on:delete={(event) => {
|
||||||
|
checklists = checklists.filter((n) => n.id != event.detail);
|
||||||
|
}}
|
||||||
|
on:edit={(event) => {
|
||||||
|
checklistToEdit = event.detail;
|
||||||
|
isShowingChecklistModal = true;
|
||||||
|
}}
|
||||||
|
{collection}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if notes.length > 0}
|
{#if notes.length > 0}
|
||||||
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.notes')}</h1>
|
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.notes')}</h1>
|
||||||
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||||
|
@ -695,7 +814,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- if none found -->
|
<!-- if none found -->
|
||||||
{#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}
|
||||||
<NotFound error={undefined} />
|
<NotFound error={undefined} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -743,6 +862,10 @@
|
||||||
new Date(collection.start_date),
|
new Date(collection.start_date),
|
||||||
numberOfDays
|
numberOfDays
|
||||||
)[dateString] || []}
|
)[dateString] || []}
|
||||||
|
{@const dayLodging =
|
||||||
|
groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays)[
|
||||||
|
dateString
|
||||||
|
] || []}
|
||||||
{@const dayNotes =
|
{@const dayNotes =
|
||||||
groupNotesByDate(notes, new Date(collection.start_date), numberOfDays)[dateString] ||
|
groupNotesByDate(notes, new Date(collection.start_date), numberOfDays)[dateString] ||
|
||||||
[]}
|
[]}
|
||||||
|
@ -804,6 +927,18 @@
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if dayLodging.length > 0}
|
||||||
|
{#each dayLodging as hotel}
|
||||||
|
<LodgingCard
|
||||||
|
lodging={hotel}
|
||||||
|
user={data?.user}
|
||||||
|
on:delete={(event) => {
|
||||||
|
lodging = lodging.filter((t) => t.id != event.detail);
|
||||||
|
}}
|
||||||
|
on:edit={editLodging}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
{#if dayChecklists.length > 0}
|
{#if dayChecklists.length > 0}
|
||||||
{#each dayChecklists as checklist}
|
{#each dayChecklists as checklist}
|
||||||
<ChecklistCard
|
<ChecklistCard
|
||||||
|
@ -821,7 +956,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#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}
|
||||||
<p class="text-center text-lg mt-2 italic">{$t('adventures.nothing_planned')}</p>
|
<p class="text-center text-lg mt-2 italic">{$t('adventures.nothing_planned')}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -950,7 +1085,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if currentView == 'recommendations'}
|
{#if currentView == 'recommendations' && data.user}
|
||||||
<div class="card bg-base-200 shadow-xl my-8 mx-auto w-10/12">
|
<div class="card bg-base-200 shadow-xl my-8 mx-auto w-10/12">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-3xl justify-center mb-4">Adventure Recommendations</h2>
|
<h2 class="card-title text-3xl justify-center mb-4">Adventure Recommendations</h2>
|
||||||
|
|
|
@ -20,11 +20,14 @@ export const load = (async (event) => {
|
||||||
|
|
||||||
let stats = null;
|
let stats = null;
|
||||||
|
|
||||||
let res = await event.fetch(`${serverEndpoint}/api/stats/counts/`, {
|
let res = await event.fetch(
|
||||||
|
`${serverEndpoint}/api/stats/counts/${event.locals.user.username}`,
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
console.error('Failed to fetch user stats');
|
console.error('Failed to fetch user stats');
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -120,8 +120,10 @@ function handleSuccessfulLogin(event: RequestEvent<RouteParams, '/login'>, respo
|
||||||
|
|
||||||
// Check if hostname is an IP address
|
// Check if hostname is an IP address
|
||||||
const isIPAddress = /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname);
|
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);
|
const parsed = psl.parse(hostname);
|
||||||
|
|
||||||
if (parsed && parsed.domain) {
|
if (parsed && parsed.domain) {
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
let createModalOpen: boolean = false;
|
let createModalOpen: boolean = false;
|
||||||
let showGeo: boolean = false;
|
let showGeo: boolean = false;
|
||||||
|
|
||||||
|
export let initialLatLng: { lat: number; lng: number } | null = null;
|
||||||
|
|
||||||
let visitedRegions: VisitedRegion[] = data.props.visitedRegions;
|
let visitedRegions: VisitedRegion[] = data.props.visitedRegions;
|
||||||
let adventures: Adventure[] = data.props.adventures;
|
let adventures: Adventure[] = data.props.adventures;
|
||||||
|
|
||||||
|
@ -49,6 +51,11 @@
|
||||||
newLatitude = e.detail.lngLat.lat;
|
newLatitude = e.detail.lngLat.lat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function newAdventure() {
|
||||||
|
initialLatLng = { lat: newLatitude, lng: newLongitude } as { lat: number; lng: number };
|
||||||
|
createModalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
function createNewAdventure(event: CustomEvent) {
|
function createNewAdventure(event: CustomEvent) {
|
||||||
adventures = [...adventures, event.detail];
|
adventures = [...adventures, event.detail];
|
||||||
newMarker = null;
|
newMarker = null;
|
||||||
|
@ -86,7 +93,7 @@
|
||||||
/>
|
/>
|
||||||
<div class="divider divider-horizontal"></div>
|
<div class="divider divider-horizontal"></div>
|
||||||
{#if newMarker}
|
{#if newMarker}
|
||||||
<button type="button" class="btn btn-primary mb-2" on:click={() => (createModalOpen = true)}
|
<button type="button" class="btn btn-primary mb-2" on:click={newAdventure}
|
||||||
>{$t('map.add_adventure_at_marker')}</button
|
>{$t('map.add_adventure_at_marker')}</button
|
||||||
>
|
>
|
||||||
<button type="button" class="btn btn-neutral mb-2" on:click={() => (newMarker = null)}
|
<button type="button" class="btn btn-neutral mb-2" on:click={() => (newMarker = null)}
|
||||||
|
@ -105,14 +112,13 @@
|
||||||
<AdventureModal
|
<AdventureModal
|
||||||
on:close={() => (createModalOpen = false)}
|
on:close={() => (createModalOpen = false)}
|
||||||
on:save={createNewAdventure}
|
on:save={createNewAdventure}
|
||||||
latitude={newLatitude}
|
{initialLatLng}
|
||||||
longitude={newLongitude}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<MapLibre
|
<MapLibre
|
||||||
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
||||||
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full"
|
class="mx-auto aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12 rounded-lg"
|
||||||
standardControls
|
standardControls
|
||||||
>
|
>
|
||||||
{#each filteredAdventures as adventure}
|
{#each filteredAdventures as adventure}
|
||||||
|
@ -197,7 +203,7 @@
|
||||||
</MapLibre>
|
</MapLibre>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Travel Map</title>
|
<title>Adventure Map</title>
|
||||||
<meta name="description" content="View your travels on a map." />
|
<meta name="description" content="View your travels on a map." />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { redirect } from '@sveltejs/kit';
|
|
||||||
import type { PageServerLoad, RequestEvent } from '../$types';
|
|
||||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async (event: RequestEvent) => {
|
|
||||||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
|
||||||
if (!event.locals.user || !event.cookies.get('sessionid')) {
|
|
||||||
return redirect(302, '/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
let sessionId = event.cookies.get('sessionid');
|
|
||||||
let stats = null;
|
|
||||||
|
|
||||||
let res = await event.fetch(`${endpoint}/api/stats/counts/`, {
|
|
||||||
headers: {
|
|
||||||
Cookie: `sessionid=${sessionId}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
console.error('Failed to fetch user stats');
|
|
||||||
} else {
|
|
||||||
stats = await res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: event.locals.user,
|
|
||||||
stats
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,112 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
export let data;
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
|
|
||||||
let stats: {
|
|
||||||
visited_country_count: number;
|
|
||||||
total_regions: number;
|
|
||||||
trips_count: number;
|
|
||||||
adventure_count: number;
|
|
||||||
visited_region_count: number;
|
|
||||||
total_countries: number;
|
|
||||||
visited_city_count: number;
|
|
||||||
total_cities: number;
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
stats = data.stats || null;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section class="min-h-screen bg-base-100 py-8 px-4">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<!-- Profile Picture -->
|
|
||||||
{#if data.user.profile_pic}
|
|
||||||
<div class="avatar">
|
|
||||||
<div
|
|
||||||
class="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 shadow-md"
|
|
||||||
>
|
|
||||||
<img src={data.user.profile_pic} alt="Profile" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- User Name -->
|
|
||||||
{#if data.user && data.user.first_name && data.user.last_name}
|
|
||||||
<h1 class="text-4xl font-bold text-primary mt-4">
|
|
||||||
{data.user.first_name}
|
|
||||||
{data.user.last_name}
|
|
||||||
</h1>
|
|
||||||
{/if}
|
|
||||||
<p class="text-lg text-base-content mt-2">{data.user.username}</p>
|
|
||||||
|
|
||||||
<!-- Member Since -->
|
|
||||||
{#if data.user && data.user.date_joined}
|
|
||||||
<div class="mt-4 flex items-center text-center text-base-content">
|
|
||||||
<p class="text-lg font-medium">{$t('profile.member_since')}</p>
|
|
||||||
<div class="flex items-center ml-2">
|
|
||||||
<iconify-icon icon="mdi:calendar" class="text-2xl text-primary"></iconify-icon>
|
|
||||||
<p class="ml-2 text-lg">
|
|
||||||
{new Date(data.user.date_joined).toLocaleDateString(undefined, { timeZone: 'UTC' })}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stats Section -->
|
|
||||||
{#if stats}
|
|
||||||
<div class="divider my-8"></div>
|
|
||||||
|
|
||||||
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
|
|
||||||
{$t('profile.user_stats')}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<div class="stats stats-vertical lg:stats-horizontal shadow bg-base-200">
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-title">{$t('navbar.adventures')}</div>
|
|
||||||
<div class="stat-value text-center">{stats.adventure_count}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-title">{$t('navbar.collections')}</div>
|
|
||||||
<div class="stat-value text-center">{stats.trips_count}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-title">{$t('profile.visited_countries')}</div>
|
|
||||||
<div class="stat-value text-center">
|
|
||||||
{Math.round((stats.visited_country_count / stats.total_countries) * 100)}%
|
|
||||||
</div>
|
|
||||||
<div class="stat-desc text-center">
|
|
||||||
{stats.visited_country_count}/{stats.total_countries}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-title">{$t('profile.visited_regions')}</div>
|
|
||||||
<div class="stat-value text-center">
|
|
||||||
{Math.round((stats.visited_region_count / stats.total_regions) * 100)}%
|
|
||||||
</div>
|
|
||||||
<div class="stat-desc text-center">
|
|
||||||
{stats.visited_region_count}/{stats.total_regions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-title">{$t('profile.visited_cities')}</div>
|
|
||||||
<div class="stat-value text-center">
|
|
||||||
{Math.round((stats.visited_city_count / stats.total_cities) * 100)}%
|
|
||||||
</div>
|
|
||||||
<div class="stat-desc text-center">
|
|
||||||
{stats.visited_city_count}/{stats.total_cities}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>Profile | AdventureLog</title>
|
|
||||||
<meta name="description" content="{data.user.first_name}'s profile on AdventureLog." />
|
|
||||||
</svelte:head>
|
|
49
frontend/src/routes/profile/[uuid]/+page.server.ts
Normal file
49
frontend/src/routes/profile/[uuid]/+page.server.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import { redirect, error } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad, RequestEvent } from '../../$types';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async (event: RequestEvent) => {
|
||||||
|
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
let username = event.params.uuid as string;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
return error(404, 'Not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// let sessionId = event.cookies.get('sessionid');
|
||||||
|
let stats = null;
|
||||||
|
|
||||||
|
let res = await event.fetch(`${endpoint}/api/stats/counts/${username}`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error('Failed to fetch user stats');
|
||||||
|
} else {
|
||||||
|
stats = await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
let userData = await event.fetch(`${endpoint}/auth/user/${username}/`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!userData.ok) {
|
||||||
|
return error(404, 'Not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = await userData.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: data.user,
|
||||||
|
adventures: data.adventures,
|
||||||
|
collections: data.collections,
|
||||||
|
stats: stats
|
||||||
|
};
|
||||||
|
};
|
184
frontend/src/routes/profile/[uuid]/+page.svelte
Normal file
184
frontend/src/routes/profile/[uuid]/+page.svelte
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let data;
|
||||||
|
import AdventureCard from '$lib/components/AdventureCard.svelte';
|
||||||
|
import CollectionCard from '$lib/components/CollectionCard.svelte';
|
||||||
|
import type { Adventure, Collection, User } from '$lib/types.js';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
let stats: {
|
||||||
|
visited_country_count: number;
|
||||||
|
total_regions: number;
|
||||||
|
trips_count: number;
|
||||||
|
adventure_count: number;
|
||||||
|
visited_region_count: number;
|
||||||
|
total_countries: number;
|
||||||
|
visited_city_count: number;
|
||||||
|
total_cities: number;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
const user: User = data.user;
|
||||||
|
const adventures: Adventure[] = data.adventures;
|
||||||
|
const collections: Collection[] = data.collections;
|
||||||
|
stats = data.stats || null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="min-h-screen bg-base-100 py-8 px-4">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<!-- Profile Picture -->
|
||||||
|
{#if user.profile_pic}
|
||||||
|
<div class="avatar">
|
||||||
|
<div
|
||||||
|
class="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 shadow-md"
|
||||||
|
>
|
||||||
|
<img src={user.profile_pic} alt="Profile" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- show first last initial -->
|
||||||
|
<div class="avatar">
|
||||||
|
<div
|
||||||
|
class="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 shadow-md"
|
||||||
|
>
|
||||||
|
{#if user.first_name && user.last_name}
|
||||||
|
<img
|
||||||
|
src={`https://eu.ui-avatars.com/api/?name=${user.first_name}+${user.last_name}&size=250`}
|
||||||
|
alt="Profile"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<img
|
||||||
|
src={`https://eu.ui-avatars.com/api/?name=${user.username}&size=250`}
|
||||||
|
alt="Profile"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- User Name -->
|
||||||
|
{#if user && user.first_name && user.last_name}
|
||||||
|
<h1 class="text-4xl font-bold text-primary mt-4">
|
||||||
|
{user.first_name}
|
||||||
|
{user.last_name}
|
||||||
|
</h1>
|
||||||
|
{/if}
|
||||||
|
<p class="text-lg text-base-content mt-2">{user.username}</p>
|
||||||
|
|
||||||
|
<!-- Member Since -->
|
||||||
|
{#if user && user.date_joined}
|
||||||
|
<div class="mt-4 flex items-center text-center text-base-content">
|
||||||
|
<p class="text-lg font-medium">{$t('profile.member_since')}</p>
|
||||||
|
<div class="flex items-center ml-2">
|
||||||
|
<iconify-icon icon="mdi:calendar" class="text-2xl text-primary"></iconify-icon>
|
||||||
|
<p class="ml-2 text-lg">
|
||||||
|
{new Date(user.date_joined).toLocaleDateString(undefined, { timeZone: 'UTC' })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Section -->
|
||||||
|
{#if stats}
|
||||||
|
<div class="divider my-8"></div>
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
|
||||||
|
{$t('profile.user_stats')}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="stats stats-vertical lg:stats-horizontal shadow bg-base-200">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-title">{$t('navbar.adventures')}</div>
|
||||||
|
<div class="stat-value text-center">{stats.adventure_count}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-title">{$t('navbar.collections')}</div>
|
||||||
|
<div class="stat-value text-center">{stats.trips_count}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-title">{$t('profile.visited_countries')}</div>
|
||||||
|
<div class="stat-value text-center">
|
||||||
|
{stats.visited_country_count}
|
||||||
|
</div>
|
||||||
|
<div class="stat-desc text-center">
|
||||||
|
{Math.round((stats.visited_country_count / stats.total_countries) * 100)}% {$t(
|
||||||
|
'adventures.of'
|
||||||
|
)}
|
||||||
|
{stats.total_countries}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-title">{$t('profile.visited_regions')}</div>
|
||||||
|
<div class="stat-value text-center">
|
||||||
|
{stats.visited_region_count}
|
||||||
|
</div>
|
||||||
|
<div class="stat-desc text-center">
|
||||||
|
{Math.round((stats.visited_region_count / stats.total_regions) * 100)}% {$t(
|
||||||
|
'adventures.of'
|
||||||
|
)}
|
||||||
|
{stats.total_regions}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-title">{$t('profile.visited_cities')}</div>
|
||||||
|
<div class="stat-value text-center">
|
||||||
|
{stats.visited_city_count}
|
||||||
|
</div>
|
||||||
|
<div class="stat-desc text-center">
|
||||||
|
{Math.round((stats.visited_city_count / stats.total_cities) * 100)}% {$t(
|
||||||
|
'adventures.of'
|
||||||
|
)}
|
||||||
|
{stats.total_cities}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Adventures Section -->
|
||||||
|
<div class="divider my-8"></div>
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
|
||||||
|
{$t('auth.user_adventures')}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{#if adventures && adventures.length === 0}
|
||||||
|
<p class="text-lg text-center text-base-content">
|
||||||
|
{$t('auth.no_public_adventures')}
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||||
|
{#each adventures as adventure}
|
||||||
|
<AdventureCard {adventure} user={null} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Collections Section -->
|
||||||
|
<div class="divider my-8"></div>
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
|
||||||
|
{$t('auth.user_collections')}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{#if collections && collections.length === 0}
|
||||||
|
<p class="text-lg text-center text-base-content">
|
||||||
|
{$t('auth.no_public_collections')}
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||||
|
{#each collections as collection}
|
||||||
|
<CollectionCard {collection} type={''} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{user.first_name || user.username}'s Profile | AdventureLog</title>
|
||||||
|
<meta name="description" content="User Profile" />
|
||||||
|
</svelte:head>
|
|
@ -458,7 +458,7 @@
|
||||||
<!-- Social Auth Settings -->
|
<!-- Social Auth Settings -->
|
||||||
<section class="space-y-8">
|
<section class="space-y-8">
|
||||||
<h2 class="text-2xl font-semibold text-center mt-8">{$t('settings.social_oidc_auth')}</h2>
|
<h2 class="text-2xl font-semibold text-center mt-8">{$t('settings.social_oidc_auth')}</h2>
|
||||||
<div class="bg-neutral p-6 rounded-lg shadow-md text-center">
|
<div class="bg-neutral p-6 rounded-lg shadow-md text-center text-neutral-content">
|
||||||
<p>
|
<p>
|
||||||
{$t('settings.social_auth_desc')}
|
{$t('settings.social_auth_desc')}
|
||||||
</p>
|
</p>
|
||||||
|
|
BIN
frontend/static/backgrounds/adventurelog_showcase_6.webp
Normal file
BIN
frontend/static/backgrounds/adventurelog_showcase_6.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 760 KiB |
Loading…
Add table
Add a link
Reference in a new issue