mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-05 05:05:17 +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
|
||||
|
||||
When contributing to this repository, please first discuss the change you wish to make via issue,
|
||||
email, or any other method with the owners of this repository before making a change.
|
||||
|
||||
Please note we have a code of conduct, please follow it in all your interactions with the project.
|
||||
We’re excited to have you contribute to AdventureLog! To ensure that this community remains welcoming and productive for all users and developers, please follow this simple Code of Conduct.
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. Please make sure you create an issue first for your change so you can link any pull requests to this issue. There should be a clear relationship between pull requests and issues.
|
||||
2. Update the README.md with details of changes to the interface, this includes new environment
|
||||
variables, exposed ports, useful file locations and container parameters.
|
||||
3. Increase the version numbers in any examples files and the README.md to the new version that this
|
||||
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
|
||||
4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
|
||||
do not have permission to do that, you may request the second reviewer to merge it for you.
|
||||
1. **Open an Issue First**: Discuss any changes or features you plan to implement by opening an issue. This helps to clarify your idea and ensures there’s a shared understanding.
|
||||
2. **Document Changes**: If your changes impact the user interface, add new environment variables, or introduce new container configurations, make sure to update the documentation accordingly. The documentation is located in the `documentation` folder.
|
||||
3. **Pull Request**: Submit a pull request with your changes. Make sure to reference the issue you opened in the description.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
### Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||
nationality, personal appearance, race, religion, or sexual identity and
|
||||
orientation.
|
||||
At AdventureLog, we are committed to creating a community that fosters adventure, exploration, and innovation. We encourage diverse participation and strive to maintain a space where everyone feels welcome to contribute, regardless of their background or experience level. We ask that you contribute with respect and kindness, making sure to prioritize collaboration and mutual growth.
|
||||
|
||||
### Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
In order to maintain a positive environment, we encourage the following behaviors:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
- **Inclusivity**: Use welcoming and inclusive language that fosters collaboration across all perspectives and experiences.
|
||||
- **Respect**: Respect differing opinions and engage with empathy, understanding that each person’s perspective is valuable.
|
||||
- **Constructive Feedback**: Offer feedback that helps improve the project and allows contributors to grow from it.
|
||||
- **Adventure Spirit**: Bring the same sense of curiosity, discovery, and positivity that drives AdventureLog into all interactions with the community.
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
- Personal attacks, trolling, or any form of harassment.
|
||||
- Insensitive or discriminatory language, including sexualized comments or imagery.
|
||||
- Spamming or misusing project spaces for personal gain.
|
||||
- Publishing or using others’ private information without permission.
|
||||
- Anything else that could be seen as disrespectful or unprofessional in a collaborative environment.
|
||||
|
||||
### Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
As maintainers of AdventureLog, we are committed to enforcing this Code of Conduct and taking corrective action when necessary. This may involve moderating comments, pulling code, or banning users who engage in harmful behaviors.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
We strive to foster a community that balances open collaboration with respect for all contributors.
|
||||
|
||||
### Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
This Code of Conduct applies in all spaces related to AdventureLog. This includes our GitHub repository, discussions, documentation, social media accounts, and events—both online and in person.
|
||||
|
||||
### Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
If you experience or witness unacceptable behavior, please report it to the project team at `contact@adventurelog.app`. All reports will be confidential and handled swiftly. The maintainers will investigate the issue and take appropriate action as needed.
|
||||
|
||||
### Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
This Code of Conduct is inspired by the [Contributor Covenant](http://contributor-covenant.org), version 1.4, and adapted to fit the unique spirit of AdventureLog.
|
||||
|
|
|
@ -144,6 +144,7 @@ Hi! I'm Sean, the creator of AdventureLog. I'm a college student and software de
|
|||
- WorldTravel Dataset [dr5hn/countries-states-cities-database](https://github.com/dr5hn/countries-states-cities-database)
|
||||
|
||||
### Top Supporters 💖
|
||||
|
||||
- [Veymax](https://x.com/veymax)
|
||||
- [nebriv](https://github.com/nebriv)
|
||||
- [Victor Butler](https://x.com/victor_butler)
|
||||
|
|
|
@ -25,10 +25,10 @@ EMAIL_BACKEND='console'
|
|||
|
||||
# ------------------- #
|
||||
# For Developers to start a Demo Database
|
||||
# docker run --name postgres-admin -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=admin -p 5432:5432 -d postgis/postgis:15-3.3
|
||||
# docker run --name adventurelog-development -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=adventurelog -p 5432:5432 -d postgis/postgis:15-3.3
|
||||
|
||||
# PGHOST='localhost'
|
||||
# PGDATABASE='admin'
|
||||
# PGDATABASE='adventurelog'
|
||||
# PGUSER='admin'
|
||||
# PGPASSWORD='admin'
|
||||
# ------------------- #
|
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,8 +1,8 @@
|
|||
import os
|
||||
from django.contrib import admin
|
||||
from django.utils.html import mark_safe
|
||||
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment
|
||||
from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
|
||||
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment, Lodging
|
||||
from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
|
||||
from allauth.account.decorators import secure_admin_login
|
||||
|
||||
admin.autodiscover()
|
||||
|
@ -140,6 +140,7 @@ admin.site.register(Category, CategoryAdmin)
|
|||
admin.site.register(City, CityAdmin)
|
||||
admin.site.register(VisitedCity)
|
||||
admin.site.register(Attachment)
|
||||
admin.site.register(Lodging)
|
||||
|
||||
admin.site.site_header = 'AdventureLog Admin'
|
||||
admin.site.site_title = 'AdventureLog Admin Site'
|
||||
|
|
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')
|
||||
]
|
||||
|
||||
LODGING_TYPES = [
|
||||
('hotel', 'Hotel'),
|
||||
('hostel', 'Hostel'),
|
||||
('resort', 'Resort'),
|
||||
('bnb', 'Bed & Breakfast'),
|
||||
('campground', 'Campground'),
|
||||
('cabin', 'Cabin'),
|
||||
('apartment', 'Apartment'),
|
||||
('house', 'House'),
|
||||
('villa', 'Villa'),
|
||||
('motel', 'Motel'),
|
||||
('other', 'Other')
|
||||
]
|
||||
|
||||
TRANSPORTATION_TYPES = [
|
||||
('car', 'Car'),
|
||||
('plane', 'Plane'),
|
||||
|
@ -318,4 +332,38 @@ class Category(models.Model):
|
|||
|
||||
|
||||
def __str__(self):
|
||||
return self.name + ' - ' + self.display_name + ' - ' + self.icon
|
||||
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
|
||||
import os
|
||||
from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, Attachment
|
||||
from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, Attachment, Lodging
|
||||
from rest_framework import serializers
|
||||
from main.utils import CustomModelSerializer
|
||||
from users.serializers import CustomUserDetailsSerializer
|
||||
|
||||
|
||||
class AdventureImageSerializer(CustomModelSerializer):
|
||||
|
@ -80,15 +81,16 @@ class AdventureSerializer(CustomModelSerializer):
|
|||
attachments = AttachmentSerializer(many=True, read_only=True)
|
||||
category = CategorySerializer(read_only=False, required=False)
|
||||
is_visited = serializers.SerializerMethodField()
|
||||
user = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Adventure
|
||||
fields = [
|
||||
'id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location',
|
||||
'is_public', 'collection', 'created_at', 'updated_at', 'images', 'link', 'longitude',
|
||||
'latitude', 'visits', 'is_visited', 'category', 'attachments'
|
||||
'latitude', 'visits', 'is_visited', 'category', 'attachments', 'user'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'is_visited']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'is_visited', 'user']
|
||||
|
||||
def validate_category(self, category_data):
|
||||
if isinstance(category_data, Category):
|
||||
|
@ -126,7 +128,11 @@ class AdventureSerializer(CustomModelSerializer):
|
|||
}
|
||||
)
|
||||
return category
|
||||
|
||||
|
||||
def get_user(self, obj):
|
||||
user = obj.user_id
|
||||
return CustomUserDetailsSerializer(user).data
|
||||
|
||||
def get_is_visited(self, obj):
|
||||
current_date = timezone.now().date()
|
||||
for visit in obj.visits.all():
|
||||
|
@ -197,6 +203,17 @@ class TransportationSerializer(CustomModelSerializer):
|
|||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
|
||||
|
||||
class LodgingSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Lodging
|
||||
fields = [
|
||||
'id', 'user_id', 'name', 'description', 'rating', 'link', 'check_in', 'check_out',
|
||||
'reservation_number', 'price', 'latitude', 'longitude', 'location', 'is_public',
|
||||
'collection', 'created_at', 'updated_at', 'type'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
|
||||
|
||||
class NoteSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
|
@ -283,10 +300,11 @@ class CollectionSerializer(CustomModelSerializer):
|
|||
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')
|
||||
notes = NoteSerializer(many=True, read_only=True, source='note_set')
|
||||
checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set')
|
||||
lodging = LodgingSerializer(many=True, read_only=True, source='lodging_set')
|
||||
|
||||
class Meta:
|
||||
model = Collection
|
||||
fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link']
|
||||
fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link', 'lodging']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
|
||||
|
||||
def to_representation(self, instance):
|
||||
|
@ -296,5 +314,4 @@ class CollectionSerializer(CustomModelSerializer):
|
|||
for user in instance.shared_with.all():
|
||||
shared_uuids.append(str(user.uuid))
|
||||
representation['shared_with'] = shared_uuids
|
||||
return representation
|
||||
|
||||
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'search', GlobalSearchView, basename='search')
|
||||
router.register(r'attachments', AttachmentViewSet, basename='attachments')
|
||||
router.register(r'lodging', LodgingViewSet, basename='lodging')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
@ -12,4 +12,5 @@ from .reverse_geocode_view import *
|
|||
from .stats_view import *
|
||||
from .transportation_view import *
|
||||
from .global_search_view import *
|
||||
from .attachment_view import *
|
||||
from .attachment_view import *
|
||||
from .lodging_view import *
|
|
@ -1,3 +1,4 @@
|
|||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q, Max
|
||||
|
@ -5,10 +6,9 @@ from django.db.models.functions import Lower
|
|||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from adventures.models import Adventure, Category
|
||||
from adventures.models import Adventure, Category, Transportation, Lodging
|
||||
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
||||
from adventures.serializers import AdventureSerializer
|
||||
from adventures.serializers import AdventureSerializer, TransportationSerializer, LodgingSerializer
|
||||
from adventures.utils import pagination
|
||||
|
||||
class AdventureViewSet(viewsets.ModelViewSet):
|
||||
|
@ -98,9 +98,22 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
user_id=request.user.id
|
||||
)
|
||||
|
||||
if is_visited.lower() in ['true', 'false']:
|
||||
is_visited_bool = is_visited.lower() == 'true'
|
||||
queryset = queryset.filter(is_visited=is_visited_bool)
|
||||
is_visited_param = request.query_params.get('is_visited')
|
||||
if is_visited_param is not None:
|
||||
# Convert is_visited_param to a boolean
|
||||
if is_visited_param.lower() == 'true':
|
||||
is_visited_bool = True
|
||||
elif is_visited_param.lower() == 'false':
|
||||
is_visited_bool = False
|
||||
else:
|
||||
is_visited_bool = None
|
||||
|
||||
# Filter logic: "visited" means at least one visit with start_date <= today
|
||||
now = timezone.now().date()
|
||||
if is_visited_bool is True:
|
||||
queryset = queryset.filter(visits__start_date__lte=now).distinct()
|
||||
elif is_visited_bool is False:
|
||||
queryset = queryset.exclude(visits__start_date__lte=now).distinct()
|
||||
|
||||
queryset = self.apply_sorting(queryset)
|
||||
return self.paginate_and_respond(queryset, request)
|
||||
|
@ -185,3 +198,49 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
# @action(detail=True, methods=['post'])
|
||||
# def convert(self, request, pk=None):
|
||||
# """
|
||||
# Convert an Adventure instance into a Transportation or Lodging instance.
|
||||
# Expects a JSON body with "target_type": "transportation" or "lodging".
|
||||
# """
|
||||
# adventure = self.get_object()
|
||||
# target_type = request.data.get("target_type", "").lower()
|
||||
|
||||
# if target_type not in ["transportation", "lodging"]:
|
||||
# return Response(
|
||||
# {"error": "Invalid target type. Must be 'transportation' or 'lodging'."},
|
||||
# status=400
|
||||
# )
|
||||
# if not adventure.collection:
|
||||
# return Response(
|
||||
# {"error": "Adventure must be part of a collection to be converted."},
|
||||
# status=400
|
||||
# )
|
||||
|
||||
# # Define the overlapping fields that both the Adventure and target models share.
|
||||
# overlapping_fields = ["name", "description", "is_public", 'collection']
|
||||
|
||||
# # Gather the overlapping data from the adventure instance.
|
||||
# conversion_data = {}
|
||||
# for field in overlapping_fields:
|
||||
# if hasattr(adventure, field):
|
||||
# conversion_data[field] = getattr(adventure, field)
|
||||
|
||||
# # Make sure to include the user reference
|
||||
# conversion_data["user_id"] = adventure.user_id
|
||||
|
||||
# # Convert the adventure instance within an atomic transaction.
|
||||
# with transaction.atomic():
|
||||
# if target_type == "transportation":
|
||||
# new_instance = Transportation.objects.create(**conversion_data)
|
||||
# serializer = TransportationSerializer(new_instance)
|
||||
# else: # target_type == "lodging"
|
||||
# new_instance = Lodging.objects.create(**conversion_data)
|
||||
# serializer = LodgingSerializer(new_instance)
|
||||
|
||||
# # Optionally, delete the original adventure to avoid duplicates.
|
||||
# adventure.delete()
|
||||
|
||||
# return Response(serializer.data)
|
||||
|
|
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,10 +21,14 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
|
|||
country_code = None
|
||||
city = None
|
||||
visited_city = None
|
||||
location_name = None
|
||||
|
||||
# town = None
|
||||
# city = None
|
||||
# county = None
|
||||
|
||||
if 'name' in data.keys():
|
||||
location_name = data['name']
|
||||
|
||||
if 'address' in data.keys():
|
||||
keys = data['address'].keys()
|
||||
|
@ -58,7 +62,7 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
|
|||
if visited_city:
|
||||
city_visited = True
|
||||
if region:
|
||||
return {"region_id": iso_code, "region": region.name, "country": region.country.name, "region_visited": region_visited, "display_name": display_name, "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited}
|
||||
return {"region_id": iso_code, "region": region.name, "country": region.country.name, "region_visited": region_visited, "display_name": display_name, "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited, 'location_name': location_name}
|
||||
return {"error": "No region found"}
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
|
|
|
@ -2,29 +2,42 @@ from rest_framework import viewsets
|
|||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from django.shortcuts import get_object_or_404
|
||||
from worldtravel.models import City, Region, Country, VisitedCity, VisitedRegion
|
||||
from adventures.models import Adventure, Collection
|
||||
from users.serializers import CustomUserDetailsSerializer as PublicUserSerializer
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class StatsViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
A simple ViewSet for listing the stats of a user.
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
@action(detail=False, methods=['get'], url_path='counts/(?P<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)
|
||||
|
||||
# remove the email address from the response
|
||||
user.email = None
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def counts(self, request):
|
||||
# get the counts for the user
|
||||
adventure_count = Adventure.objects.filter(
|
||||
user_id=request.user.id).count()
|
||||
user_id=user.id).count()
|
||||
trips_count = Collection.objects.filter(
|
||||
user_id=request.user.id).count()
|
||||
user_id=user.id).count()
|
||||
visited_city_count = VisitedCity.objects.filter(
|
||||
user_id=request.user.id).count()
|
||||
user_id=user.id).count()
|
||||
total_cities = City.objects.count()
|
||||
visited_region_count = VisitedRegion.objects.filter(
|
||||
user_id=request.user.id).count()
|
||||
user_id=user.id).count()
|
||||
total_regions = Region.objects.count()
|
||||
visited_country_count = VisitedRegion.objects.filter(
|
||||
user_id=request.user.id).values('region__country').distinct().count()
|
||||
user_id=user.id).values('region__country').distinct().count()
|
||||
total_countries = Country.objects.count()
|
||||
return Response({
|
||||
'adventure_count': adventure_count,
|
||||
|
|
|
@ -63,25 +63,31 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
|||
return integration
|
||||
|
||||
query = request.query_params.get('query', '')
|
||||
date = request.query_params.get('date', '')
|
||||
|
||||
if not query:
|
||||
if not query and not date:
|
||||
return Response(
|
||||
{
|
||||
'message': 'Query is required.',
|
||||
'message': 'Query or date is required.',
|
||||
'error': True,
|
||||
'code': 'immich.query_required'
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
arguments = {}
|
||||
if query:
|
||||
arguments['query'] = query
|
||||
if date:
|
||||
arguments['takenBefore'] = date
|
||||
|
||||
# check so if the server is down, it does not tweak out like a madman and crash the server with a 500 error code
|
||||
try:
|
||||
immich_fetch = requests.post(f'{integration.server_url}/search/smart', headers={
|
||||
url = f'{integration.server_url}/search/{"smart" if query else "metadata"}'
|
||||
immich_fetch = requests.post(url, headers={
|
||||
'x-api-key': integration.api_key
|
||||
},
|
||||
json = {
|
||||
'query': query
|
||||
}
|
||||
json = arguments
|
||||
)
|
||||
res = immich_fetch.json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
|
@ -219,10 +225,14 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
|||
)
|
||||
|
||||
if 'assets' in res:
|
||||
return Response(
|
||||
res['assets'],
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
paginator = self.pagination_class()
|
||||
# for each item in the items, we need to add the image url to the item so we can display it in the frontend
|
||||
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')
|
||||
public_url = public_url.replace("'", "")
|
||||
for item in res['assets']:
|
||||
item['image_url'] = f'{public_url}/api/integrations/immich/get/{item["id"]}'
|
||||
result_page = paginator.paginate_queryset(res['assets'], request)
|
||||
return paginator.get_paginated_response(result_page)
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
|
|
|
@ -61,6 +61,7 @@ INSTALLED_APPS = (
|
|||
'users',
|
||||
'integrations',
|
||||
'django.contrib.gis',
|
||||
# 'achievements', # Not done yet, will be added later in a future update
|
||||
# 'widget_tweaks',
|
||||
# 'slippers',
|
||||
|
||||
|
@ -128,23 +129,20 @@ USE_L10N = True
|
|||
USE_TZ = True
|
||||
|
||||
unParsedFrontenedUrl = getenv('FRONTEND_URL', 'http://localhost:3000')
|
||||
FRONTEND_URL = unParsedFrontenedUrl.replace("'", "").replace('"', '')
|
||||
FRONTEND_URL = unParsedFrontenedUrl.translate(str.maketrans('', '', '\'"'))
|
||||
|
||||
SESSION_COOKIE_SAMESITE = None
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
|
||||
SESSION_COOKIE_SECURE = FRONTEND_URL.startswith('https')
|
||||
|
||||
# Parse the FRONTEND_URL
|
||||
# Remove and ' from the URL
|
||||
|
||||
parsed_url = urlparse(FRONTEND_URL)
|
||||
hostname = parsed_url.hostname
|
||||
|
||||
# Check if the hostname is an IP address
|
||||
hostname = urlparse(FRONTEND_URL).hostname
|
||||
is_ip_address = hostname.replace('.', '').isdigit()
|
||||
|
||||
if is_ip_address:
|
||||
# Do not set a domain for IP addresses
|
||||
# Check if the hostname is single-label (no dots)
|
||||
is_single_label = '.' not in hostname
|
||||
|
||||
if is_ip_address or is_single_label:
|
||||
# Do not set a domain for IP addresses or single-label hostnames
|
||||
SESSION_COOKIE_DOMAIN = None
|
||||
else:
|
||||
# Use publicsuffix2 to calculate the correct cookie domain
|
||||
|
@ -155,6 +153,7 @@ else:
|
|||
# Fallback to the hostname if parsing fails
|
||||
SESSION_COOKIE_DOMAIN = hostname
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.7/howto/static-files/
|
||||
|
||||
|
@ -301,5 +300,8 @@ LOGGING = {
|
|||
},
|
||||
},
|
||||
}
|
||||
|
||||
# ADVENTURELOG_CDN_URL = getenv('ADVENTURELOG_CDN_URL', 'https://cdn.adventurelog.app')
|
||||
|
||||
# https://github.com/dr5hn/countries-states-cities-database/tags
|
||||
COUNTRY_REGION_JSON_VERSION = 'v2.5'
|
|
@ -22,7 +22,7 @@ urlpatterns = [
|
|||
|
||||
path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'),
|
||||
path('auth/users/', PublicUserListView.as_view(), name='public-user-list'),
|
||||
path('auth/user/<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/user-metadata/', UserMetadataView.as_view(), name='user-metadata'),
|
||||
|
|
|
@ -118,5 +118,7 @@ class CustomUserDetailsSerializer(UserDetailsSerializer):
|
|||
|
||||
# Remove `pk` field from the response
|
||||
representation.pop('pk', None)
|
||||
# Remove the email field
|
||||
representation.pop('email', None)
|
||||
|
||||
return representation
|
||||
|
|
|
@ -11,6 +11,8 @@ from django.shortcuts import get_object_or_404
|
|||
from django.contrib.auth import get_user_model
|
||||
from .serializers import CustomUserDetailsSerializer as PublicUserSerializer
|
||||
from allauth.socialaccount.models import SocialApp
|
||||
from adventures.serializers import AdventureSerializer, CollectionSerializer
|
||||
from adventures.models import Adventure, Collection
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -66,6 +68,9 @@ class PublicUserListView(APIView):
|
|||
for user in users:
|
||||
user.email = None
|
||||
serializer = PublicUserSerializer(users, many=True)
|
||||
# for every user, remove the field has_password
|
||||
for user in serializer.data:
|
||||
user.pop('has_password', None)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
class PublicUserDetailView(APIView):
|
||||
|
@ -79,12 +84,29 @@ class PublicUserDetailView(APIView):
|
|||
},
|
||||
operation_description="Get public user information."
|
||||
)
|
||||
def get(self, request, user_id):
|
||||
user = get_object_or_404(User, uuid=user_id, public_profile=True)
|
||||
def get(self, request, username):
|
||||
if request.user.username == username:
|
||||
user = get_object_or_404(User, username=username)
|
||||
else:
|
||||
user = get_object_or_404(User, username=username, public_profile=True)
|
||||
serializer = PublicUserSerializer(user)
|
||||
# for every user, remove the field has_password
|
||||
serializer.data.pop('has_password', None)
|
||||
|
||||
# remove the email address from the response
|
||||
user.email = None
|
||||
serializer = PublicUserSerializer(user)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
# Get the users adventures and collections to include in the response
|
||||
adventures = Adventure.objects.filter(user_id=user, is_public=True)
|
||||
collections = Collection.objects.filter(user_id=user, is_public=True)
|
||||
adventure_serializer = AdventureSerializer(adventures, many=True)
|
||||
collection_serializer = CollectionSerializer(collections, many=True)
|
||||
|
||||
return Response({
|
||||
'user': serializer.data,
|
||||
'adventures': adventure_serializer.data,
|
||||
'collections': collection_serializer.data
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
class UserMetadataView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
|
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: "discord", link: "https://discord.gg/wRbQ9Egr8C" },
|
||||
{ icon: "buymeacoffee", link: "https://buymeacoffee.com/seanmorley15" },
|
||||
{ icon: "x", link: "https://x.com/AdventureLogApp" },
|
||||
{ icon: "mastodon", link: "https://mastodon.social/@adventurelog" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -55,6 +55,10 @@ This configuration is done in the [Admin Panel](../../guides/admin_panel.md). Yo
|
|||
|
||||
Ensure that the Authentik server is running and accessible by AdventureLog. Users should now be able to log in to AdventureLog using their Authentik account.
|
||||
|
||||
## Linking to Existing Account
|
||||
|
||||
If a user has an existing AdventureLog account and wants to link it to their Authentik account, they can do so by logging in to their AdventureLog account and navigating to the `Settings` page. There is a button that says `Launch Account Connections`, click that and then choose the provider to link to the existing account.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 404 error when logging in.
|
||||
|
|
|
@ -35,10 +35,15 @@ This configuration is done in the [Admin Panel](../../guides/admin_panel.md). Yo
|
|||
- Settings: can be left blank
|
||||
- Sites: move over the sites you want to enable Authentik on, usually `example.com` and `www.example.com` unless you renamed your sites.
|
||||
|
||||
4. Save the configuration.
|
||||
|
||||
Users should now be able to log in to AdventureLog using their GitHub account, and link it to existing accounts.
|
||||
|
||||
## Linking to Existing Account
|
||||
|
||||
If a user has an existing AdventureLog account and wants to link it to their Github account, they can do so by logging in to their AdventureLog account and navigating to the `Settings` page. There is a button that says `Launch Account Connections`, click that and then choose the provider to link to the existing account.
|
||||
|
||||
#### What it Should Look Like
|
||||
|
||||

|
||||
|
||||
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/time-grid": "^3.7.1",
|
||||
"@iconify-json/mdi": "^1.1.67",
|
||||
"@sveltejs/adapter-auto": "^3.2.2",
|
||||
"@sveltejs/adapter-node": "^5.2.0",
|
||||
"@sveltejs/adapter-vercel": "^5.4.1",
|
||||
"@sveltejs/kit": "^2.8.3",
|
||||
|
@ -47,7 +46,6 @@
|
|||
"psl": "^1.15.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"svelte-i18n": "^4.0.1",
|
||||
"svelte-maplibre": "^0.9.8",
|
||||
"tsparticles": "^3.7.1"
|
||||
"svelte-maplibre": "^0.9.8"
|
||||
}
|
||||
}
|
||||
|
|
398
frontend/pnpm-lock.yaml
generated
398
frontend/pnpm-lock.yaml
generated
|
@ -35,9 +35,6 @@ importers:
|
|||
svelte-maplibre:
|
||||
specifier: ^0.9.8
|
||||
version: 0.9.8(svelte@4.2.19)
|
||||
tsparticles:
|
||||
specifier: ^3.7.1
|
||||
version: 3.7.1
|
||||
devDependencies:
|
||||
'@event-calendar/core':
|
||||
specifier: ^3.7.1
|
||||
|
@ -51,9 +48,6 @@ importers:
|
|||
'@iconify-json/mdi':
|
||||
specifier: ^1.1.67
|
||||
version: 1.1.67
|
||||
'@sveltejs/adapter-auto':
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))
|
||||
'@sveltejs/adapter-node':
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))
|
||||
|
@ -741,11 +735,6 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@sveltejs/adapter-auto@3.2.2':
|
||||
resolution: {integrity: sha512-Mso5xPCA8zgcKrv+QioVlqMZkyUQ5MjDJiEPuG/Z7cV/5tmwV7LmcVWk5tZ+H0NCOV1x12AsoSpt/CwFwuVXMA==}
|
||||
peerDependencies:
|
||||
'@sveltejs/kit': ^2.0.0
|
||||
|
||||
'@sveltejs/adapter-node@5.2.0':
|
||||
resolution: {integrity: sha512-HVZoei2078XSyPmvdTHE03VXDUD0ytTvMuMHMQP0j6zX4nPDpCcKrgvU7baEblMeCCMdM/shQvstFxOJPQKlUQ==}
|
||||
peerDependencies:
|
||||
|
@ -785,147 +774,6 @@ packages:
|
|||
peerDependencies:
|
||||
tailwindcss: '>=3.0.0 || insiders'
|
||||
|
||||
'@tsparticles/basic@3.7.1':
|
||||
resolution: {integrity: sha512-oJMJ3qzYUROYaOEsaFXkVynxT2OTWBXbQ9MNc1bJi/bVc1VOU44VN7X/KmiZjD+w1U+Qalk6BeVvDRwpFshblw==}
|
||||
|
||||
'@tsparticles/engine@3.7.1':
|
||||
resolution: {integrity: sha512-GYzBgq/oOE9YJdOL1++MoawWmYg4AvVct6CIrJGx84ZRb3U2owYmLsRGabYl0qX1CWWOvUG569043RJmyp/vQA==}
|
||||
|
||||
'@tsparticles/interaction-external-attract@3.7.1':
|
||||
resolution: {integrity: sha512-cpnMsFJ7ZJNKccpQvskKvSs1ofknByHE6FGqbEb17ij7HqvbECQOCOVKHPFnYipHe14cXor/Cd+nVisRcTASoQ==}
|
||||
|
||||
'@tsparticles/interaction-external-bounce@3.7.1':
|
||||
resolution: {integrity: sha512-npvU9Qt6WDonjezHqi+hWM44ga2Oh5yXdr8eSoJpvuHZrCP7rIdRSz5XseHouO1bMS9DbXk86sx4qwrhB5w58w==}
|
||||
|
||||
'@tsparticles/interaction-external-bubble@3.7.1':
|
||||
resolution: {integrity: sha512-WdbYL46lMfuf2g5kfVB1hhhxRBtEXDvnwz8PJwLKurSThL/27bqsqysyXsMzXtLByXUneGhtJTj4D5I5RYdgjA==}
|
||||
|
||||
'@tsparticles/interaction-external-connect@3.7.1':
|
||||
resolution: {integrity: sha512-hqx0ANIbjLIz/nxmk0LvqANBiNLLmVybbCA7N+xDHtEORvpKjNlKEvMz6Razocl6vRjoHZ/olSwcxIG84dh/cg==}
|
||||
|
||||
'@tsparticles/interaction-external-grab@3.7.1':
|
||||
resolution: {integrity: sha512-JMYpFW+7YvkpK5MYlt4Ec3Gwb5ZxS7RLVL8IRUSd5yJOw25husPTYg+FQywxrt5WhKe+tPsCAYo+uGIbTTHi9w==}
|
||||
|
||||
'@tsparticles/interaction-external-pause@3.7.1':
|
||||
resolution: {integrity: sha512-Kkp+7sCe24hawH0XvS1V6UCCuHfMvpLK7oseqSam9Gt4SyGrFvaqIXxkjXhRhn9MysJyKFPBV4/dtBM1HR9p6A==}
|
||||
|
||||
'@tsparticles/interaction-external-push@3.7.1':
|
||||
resolution: {integrity: sha512-4VoaR5jvXgQdB7irtq4uSZYr5c+D6TBTVEnLVpBfJhUs6jhw6mgN5g7yp5izIYkK0AlcO431MHn8dvJacvRLDw==}
|
||||
|
||||
'@tsparticles/interaction-external-remove@3.7.1':
|
||||
resolution: {integrity: sha512-FRBW7U7zD5MkO6/b7e8iSMk/UTtRLY2XiIVFZNsKri3Re3yPpvZzzd5tl2YlYGQlg1Xc+K8SJYMQQA3PtgQ/Tg==}
|
||||
|
||||
'@tsparticles/interaction-external-repulse@3.7.1':
|
||||
resolution: {integrity: sha512-mwM06dVmg2FEvHMQsPOfRBQWACbjf3qnelODkqI9DSVxQ0B8DESP4BYNXyraFGYv00YiPzRv5Xy/uejHdbsQUA==}
|
||||
|
||||
'@tsparticles/interaction-external-slow@3.7.1':
|
||||
resolution: {integrity: sha512-CfCAs3kUQC3pLOj0dbzn5AolQyBHgjxORLdfnYBhepvFV1BXB+4ytChRfXBzjykBPI6U+rCnw5Fk/vVjAroSFA==}
|
||||
|
||||
'@tsparticles/interaction-external-trail@3.7.1':
|
||||
resolution: {integrity: sha512-M7lNQUWP15m8YIDP/JZcZAXaVJLqdwpBs0Uv9F6dU6jsnNXwwHFVFZ+1icrnlbgl9k/Ehhodbdo5weE7GHhQhQ==}
|
||||
|
||||
'@tsparticles/interaction-particles-attract@3.7.1':
|
||||
resolution: {integrity: sha512-UABbBORKaiItAT8vR0t4ye2H3VE6/Ah4zcylBlnq0Jd5yDkyP4rnkwhicaY6y4Zlfwyx+0PWdAC4f/ziFAyObg==}
|
||||
|
||||
'@tsparticles/interaction-particles-collisions@3.7.1':
|
||||
resolution: {integrity: sha512-0GY9++Gn2KXViyeifXWkH7a2UO5+uRwyS1rDeTN8eleyiq2j9zQf4xztZEIft8T0hTetq2rkWxQ92j2kev6NVA==}
|
||||
|
||||
'@tsparticles/interaction-particles-links@3.7.1':
|
||||
resolution: {integrity: sha512-BxCXAAOBNmEvlyOQzwprryW8YdtMIop2v4kgSCff5MCtDwYWoQIfzaQlWbBAkD9ey6BoF8iMjhBUaY1MnDecTA==}
|
||||
|
||||
'@tsparticles/move-base@3.7.1':
|
||||
resolution: {integrity: sha512-LPtMHwJHhzwfRIcSAk814fY9NcRiICwaEbapaJSYyP1DwscSXqOWoyAEWwzV9hMgAcPdsED6nGeg8RCXGm58lw==}
|
||||
|
||||
'@tsparticles/move-parallax@3.7.1':
|
||||
resolution: {integrity: sha512-B40azo6EJyMdI+kmIxpqWDaObPwODTYLDCikzkZ73n5tS6OhFUlkz81Scfo+g1iGTdryKFygUKhVGcG1EFuA5g==}
|
||||
|
||||
'@tsparticles/plugin-absorbers@3.7.1':
|
||||
resolution: {integrity: sha512-3s+fILLV1tdKOq/bXwfoxFVbzkWwXpdWTC2C0QIP6BFwDSQqV5txluiLEf7SCf8C5etQ6dstEnOgVbdzK7+eWA==}
|
||||
|
||||
'@tsparticles/plugin-easing-quad@3.7.1':
|
||||
resolution: {integrity: sha512-nSwKCRe6C/noCi3dyZlm1GiQGask0aXdWDuS36b82iwzwQ01cBTXeXR25mLr4fsfMLFfYAZXyBxEMMpw3rkSiw==}
|
||||
|
||||
'@tsparticles/plugin-emitters-shape-circle@3.7.1':
|
||||
resolution: {integrity: sha512-eBwktnGROkiyCvtrSwdPpoRbIjQgV/Odq//0dw8D+qUdnox6dNzzhJjz8L2LAA2kQZBqtdBqV2kcx3w5ZdqoEQ==}
|
||||
|
||||
'@tsparticles/plugin-emitters-shape-square@3.7.1':
|
||||
resolution: {integrity: sha512-nvGBsRLrkiz6Q38TJRl8Y/eu9i1ChQ9oorQydLBok+iZ6MefuOj39iYsAOkD1w9yRVrFWKHG6CR1mmJUniz/HA==}
|
||||
|
||||
'@tsparticles/plugin-emitters@3.7.1':
|
||||
resolution: {integrity: sha512-WV5Uwxp/Ckqv5kZynTj6mj13jYbQCArNLFv8ks+zjdlteoyT5EhQl4rg+TalaySCb1zCd6Fu2Scp35l3JJgbnw==}
|
||||
|
||||
'@tsparticles/plugin-hex-color@3.7.1':
|
||||
resolution: {integrity: sha512-7xu3MV8EdNNejjYyEmrq5fCDdYAcqz/9VatLpnwtwR5Q5t2qI0tD4CrcGaFfC/rTAVJacfiJe02UV/hlj03KKA==}
|
||||
|
||||
'@tsparticles/plugin-hsl-color@3.7.1':
|
||||
resolution: {integrity: sha512-zzAI1CuoCMBJhgeYZ5Rq42nGbPg35ZzIs11eQegjsWG5Msm5QKSj60qPzERnoUcCc4HCKtIWP7rYMz6h3xpoEg==}
|
||||
|
||||
'@tsparticles/plugin-rgb-color@3.7.1':
|
||||
resolution: {integrity: sha512-taEraTpCYR6jpjflqBL95tN0zFU8JrAChXTt8mxVn7gddxoNMHI/LGymEPRCweLukwV6GQyAGOkeGEdWDPtYTA==}
|
||||
|
||||
'@tsparticles/shape-circle@3.7.1':
|
||||
resolution: {integrity: sha512-kmOWaUuFwuTtcCFYjuyJbdA5qDqWdGsharLalYnIczkLu2c1I8jJo/OmGePKhWn62ocu7mqKMomfElkIcw2AsA==}
|
||||
|
||||
'@tsparticles/shape-emoji@3.7.1':
|
||||
resolution: {integrity: sha512-mX18c/xhYVljS/r5Xbowzclw+1YwhtWoQFOOfkmjjZppA+RjgcwSKLvH6E20PaH1yVTjBOfSF+3POKpwsULzTg==}
|
||||
|
||||
'@tsparticles/shape-image@3.7.1':
|
||||
resolution: {integrity: sha512-eDzfkQhqLY6fb9QH2Vo9TGfdJBFFpYnojhxQxc7IdzIwOFMD3JK4B52RVl9oowR+rNE8dNp6P2L+eMAF4yld0g==}
|
||||
|
||||
'@tsparticles/shape-line@3.7.1':
|
||||
resolution: {integrity: sha512-lMPYApUpg7avxmYPfHHr4dQepZSNn/g0Q1/g2+lnTi8ZtUBiCZ2WMVy9R3GOzyozbnzigLQ6AJRnOpsUZV7H4g==}
|
||||
|
||||
'@tsparticles/shape-polygon@3.7.1':
|
||||
resolution: {integrity: sha512-5FrRfpYC3qnvV2nXBLE4Q0v+SMNWJO8xgzh6MBFwfptvqH4EOrqc/58eS5x0jlf+evwf9LjPgeGkOTcwaHHcYQ==}
|
||||
|
||||
'@tsparticles/shape-square@3.7.1':
|
||||
resolution: {integrity: sha512-7VCqbRwinjBZ+Ryme27rOtl+jKrET8qDthqZLrAoj3WONBqyt+R9q6SXAJ9WodqEX68IBvcluqbFY5qDZm8iAQ==}
|
||||
|
||||
'@tsparticles/shape-star@3.7.1':
|
||||
resolution: {integrity: sha512-3G4oipioyWKLEQYT11Sx3k6AObu3dbv/A5LRqGGTQm5IR6UACa+INwykZYI0a+MdJJMb83E0e4Fn3hlZbi0/8w==}
|
||||
|
||||
'@tsparticles/shape-text@3.7.1':
|
||||
resolution: {integrity: sha512-aU1V9O8uQQBlL0jGFh9Q0b5vQ1Ji6Oo5ptyyj5yJ5uP/ZU00L0Vhk4DNyLXpaU0+H6OBoPpCqnvEsZBB9/HmCQ==}
|
||||
|
||||
'@tsparticles/slim@3.7.1':
|
||||
resolution: {integrity: sha512-OtJEhud2KleX7OxiG2r/VYriHNIwTpFm3sPFy4EOJzAD0EW7KZoKXGpGn5gwGI1NWeB0jso92yNTrTC2ZTW0qw==}
|
||||
|
||||
'@tsparticles/updater-color@3.7.1':
|
||||
resolution: {integrity: sha512-QimV3yn17dcdJx7PpTwLtw9BhkQ0q8qFF035OdcZpnynBPAO/hg0zvSMpMGoeuDVFH02wWBy4h2/BYCv6wh6Sw==}
|
||||
|
||||
'@tsparticles/updater-destroy@3.7.1':
|
||||
resolution: {integrity: sha512-krXNoMDKyeyE/ZjQh3LVjrLYivFefQOQ9i+B7RpMe7x4h+iRgpB6npTCqidGQ82+hZ8G6xfQ9ToduebWwK4JGg==}
|
||||
|
||||
'@tsparticles/updater-life@3.7.1':
|
||||
resolution: {integrity: sha512-NY5gUrgO5AsARNC0usP9PKahXf7JCxbP/H1vzTfA0SJw4veANfWTldOvhIlcm2CHVP5P1b827p0hWsBHECwz7A==}
|
||||
|
||||
'@tsparticles/updater-opacity@3.7.1':
|
||||
resolution: {integrity: sha512-YcyviCooTv7SAKw7sxd84CfJqZ7dYPSdYZzCpedV6TKIObRiwLqXlyLXQGJ3YltghKQSCSolmVy8woWBCDm1qA==}
|
||||
|
||||
'@tsparticles/updater-out-modes@3.7.1':
|
||||
resolution: {integrity: sha512-Cb5sWquRtUYLSiFpmBjjYKRdpNV52diCo9+qMtK1oVlldDBhUwqO+1TQjdlaA2yl5DURlY9ZfOHXvY+IT7CHCw==}
|
||||
|
||||
'@tsparticles/updater-roll@3.7.1':
|
||||
resolution: {integrity: sha512-gHLRqpTGVGPJBEAIPUiYVembIn5bcaTXXxsUJEM/IN+GIOvj2uZZGZ4r2aFTA6WugqEbJsJdblDSvMfouyz7Ug==}
|
||||
|
||||
'@tsparticles/updater-rotate@3.7.1':
|
||||
resolution: {integrity: sha512-toVHwl+h6SvtA8dyxSA2kMH2QdDA71vehuAa+HoRqf1y06h5kxyYiMKZFHCqDJ6lFfRPs47MjrC9dD2bDz14MQ==}
|
||||
|
||||
'@tsparticles/updater-size@3.7.1':
|
||||
resolution: {integrity: sha512-+Y0H0PnDJVIsJ+zHTyubYu1jtRFmVnY1dAv3VCjScIDw6bcpL/ol+HrtHTGIX0WbMyUfjCyALfAoaXi/Wm8VcQ==}
|
||||
|
||||
'@tsparticles/updater-stroke-color@3.7.1':
|
||||
resolution: {integrity: sha512-VHhQkCNuxjx/Hy7A+g0Yijb24T0+wQ3jNsF/yfrR9dEdZWSBiimZLvV1bilPdAeEtieAJTAZo2VNhcD1snF0iQ==}
|
||||
|
||||
'@tsparticles/updater-tilt@3.7.1':
|
||||
resolution: {integrity: sha512-pSOXoXPre1VPKC5nC5GW0L9jw63w1dVdsDdggEau7MP9xO7trko9L/KyayBX12Y4Ief1ca12Incxxr67hw7GGA==}
|
||||
|
||||
'@tsparticles/updater-twinkle@3.7.1':
|
||||
resolution: {integrity: sha512-maRTqPbeZcxBK6s1ry+ih71qSVaitfP1KTrAKR38v26GMwyO6z+zYV2bu9WTRt21FRFAoxlMLWxNu21GtQoXDA==}
|
||||
|
||||
'@tsparticles/updater-wobble@3.7.1':
|
||||
resolution: {integrity: sha512-YIlNg4L0w4egQJhPLpgcvcfv9+X621+cQsrdN9sSmajxhhwtEQvQUvFUzGTcvpjVi+GcBNp0t4sCKEzoP8iaYw==}
|
||||
|
||||
'@types/cookie@0.6.0':
|
||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||
|
||||
|
@ -2234,9 +2082,6 @@ packages:
|
|||
tslib@2.6.3:
|
||||
resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
|
||||
|
||||
tsparticles@3.7.1:
|
||||
resolution: {integrity: sha512-NNkOYIo01eHpDuaJxDCGgcLEMZKEJTCN/XPVCLg7VxgEWN19rjXpDnDguISxadS8GSFPws7hpGgbeDDAm3MX+Q==}
|
||||
|
||||
type-detect@4.0.8:
|
||||
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
|
||||
engines: {node: '>=4'}
|
||||
|
@ -2875,11 +2720,6 @@ snapshots:
|
|||
'@rollup/rollup-win32-x64-msvc@4.31.0':
|
||||
optional: true
|
||||
|
||||
'@sveltejs/adapter-auto@3.2.2(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))':
|
||||
dependencies:
|
||||
'@sveltejs/kit': 2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4))
|
||||
import-meta-resolve: 4.1.0
|
||||
|
||||
'@sveltejs/adapter-node@5.2.0(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))':
|
||||
dependencies:
|
||||
'@rollup/plugin-commonjs': 26.0.1(rollup@4.24.0)
|
||||
|
@ -2946,228 +2786,6 @@ snapshots:
|
|||
postcss-selector-parser: 6.0.10
|
||||
tailwindcss: 3.4.4
|
||||
|
||||
'@tsparticles/basic@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
'@tsparticles/move-base': 3.7.1
|
||||
'@tsparticles/plugin-hex-color': 3.7.1
|
||||
'@tsparticles/plugin-hsl-color': 3.7.1
|
||||
'@tsparticles/plugin-rgb-color': 3.7.1
|
||||
'@tsparticles/shape-circle': 3.7.1
|
||||
'@tsparticles/updater-color': 3.7.1
|
||||
'@tsparticles/updater-opacity': 3.7.1
|
||||
'@tsparticles/updater-out-modes': 3.7.1
|
||||
'@tsparticles/updater-size': 3.7.1
|
||||
|
||||
'@tsparticles/engine@3.7.1': {}
|
||||
|
||||
'@tsparticles/interaction-external-attract@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/interaction-external-bounce@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/interaction-external-bubble@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/interaction-external-connect@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/interaction-external-grab@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/interaction-external-pause@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/interaction-external-push@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/interaction-external-remove@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/interaction-external-repulse@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/interaction-external-slow@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/interaction-external-trail@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/interaction-particles-attract@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/interaction-particles-collisions@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/interaction-particles-links@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/move-base@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/move-parallax@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/plugin-absorbers@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/plugin-easing-quad@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/plugin-emitters-shape-circle@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
'@tsparticles/plugin-emitters': 3.7.1
|
||||
|
||||
'@tsparticles/plugin-emitters-shape-square@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
'@tsparticles/plugin-emitters': 3.7.1
|
||||
|
||||
'@tsparticles/plugin-emitters@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/plugin-hex-color@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/plugin-hsl-color@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/plugin-rgb-color@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/shape-circle@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/shape-emoji@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/shape-image@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/shape-line@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/shape-polygon@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/shape-square@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/shape-star@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/shape-text@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/slim@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/basic': 3.7.1
|
||||
'@tsparticles/engine': 3.7.1
|
||||
'@tsparticles/interaction-external-attract': 3.7.1
|
||||
'@tsparticles/interaction-external-bounce': 3.7.1
|
||||
'@tsparticles/interaction-external-bubble': 3.7.1
|
||||
'@tsparticles/interaction-external-connect': 3.7.1
|
||||
'@tsparticles/interaction-external-grab': 3.7.1
|
||||
'@tsparticles/interaction-external-pause': 3.7.1
|
||||
'@tsparticles/interaction-external-push': 3.7.1
|
||||
'@tsparticles/interaction-external-remove': 3.7.1
|
||||
'@tsparticles/interaction-external-repulse': 3.7.1
|
||||
'@tsparticles/interaction-external-slow': 3.7.1
|
||||
'@tsparticles/interaction-particles-attract': 3.7.1
|
||||
'@tsparticles/interaction-particles-collisions': 3.7.1
|
||||
'@tsparticles/interaction-particles-links': 3.7.1
|
||||
'@tsparticles/move-parallax': 3.7.1
|
||||
'@tsparticles/plugin-easing-quad': 3.7.1
|
||||
'@tsparticles/shape-emoji': 3.7.1
|
||||
'@tsparticles/shape-image': 3.7.1
|
||||
'@tsparticles/shape-line': 3.7.1
|
||||
'@tsparticles/shape-polygon': 3.7.1
|
||||
'@tsparticles/shape-square': 3.7.1
|
||||
'@tsparticles/shape-star': 3.7.1
|
||||
'@tsparticles/updater-life': 3.7.1
|
||||
'@tsparticles/updater-rotate': 3.7.1
|
||||
'@tsparticles/updater-stroke-color': 3.7.1
|
||||
|
||||
'@tsparticles/updater-color@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/updater-destroy@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/updater-life@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/updater-opacity@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/updater-out-modes@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/updater-roll@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/updater-rotate@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/updater-size@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/updater-stroke-color@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/updater-tilt@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/updater-twinkle@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@tsparticles/updater-wobble@3.7.1':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
|
||||
'@types/cookie@0.6.0': {}
|
||||
|
||||
'@types/estree@1.0.6': {}
|
||||
|
@ -4539,22 +4157,6 @@ snapshots:
|
|||
|
||||
tslib@2.6.3: {}
|
||||
|
||||
tsparticles@3.7.1:
|
||||
dependencies:
|
||||
'@tsparticles/engine': 3.7.1
|
||||
'@tsparticles/interaction-external-trail': 3.7.1
|
||||
'@tsparticles/plugin-absorbers': 3.7.1
|
||||
'@tsparticles/plugin-emitters': 3.7.1
|
||||
'@tsparticles/plugin-emitters-shape-circle': 3.7.1
|
||||
'@tsparticles/plugin-emitters-shape-square': 3.7.1
|
||||
'@tsparticles/shape-text': 3.7.1
|
||||
'@tsparticles/slim': 3.7.1
|
||||
'@tsparticles/updater-destroy': 3.7.1
|
||||
'@tsparticles/updater-roll': 3.7.1
|
||||
'@tsparticles/updater-tilt': 3.7.1
|
||||
'@tsparticles/updater-twinkle': 3.7.1
|
||||
'@tsparticles/updater-wobble': 3.7.1
|
||||
|
||||
type-detect@4.0.8: {}
|
||||
|
||||
type@2.7.3: {}
|
||||
|
|
|
@ -18,9 +18,7 @@
|
|||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
console.log(res);
|
||||
let data = await res.json();
|
||||
console.log('ACTIVITIES' + data.activities);
|
||||
if (data && data.activities) {
|
||||
allActivities = data.activities;
|
||||
}
|
||||
|
|
|
@ -1,52 +1,37 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type {
|
||||
Adventure,
|
||||
Attachment,
|
||||
Category,
|
||||
Collection,
|
||||
OpenStreetMapPlace,
|
||||
Point,
|
||||
ReverseGeocode
|
||||
} from '$lib/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import type { Adventure, Attachment, Category, Collection } from '$lib/types';
|
||||
import { addToast } from '$lib/toasts';
|
||||
import { deserialize } from '$app/forms';
|
||||
import { t } from 'svelte-i18n';
|
||||
export let longitude: number | null = null;
|
||||
export let latitude: number | 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 warningMessage: string = '';
|
||||
let constrainDates: boolean = false;
|
||||
|
||||
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 { appVersion } from '$lib/config';
|
||||
import CategoryDropdown from './CategoryDropdown.svelte';
|
||||
import { findFirstValue } from '$lib';
|
||||
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||
import ImmichSelect from './ImmichSelect.svelte';
|
||||
|
||||
import Star from '~icons/mdi/star';
|
||||
import Crown from '~icons/mdi/crown';
|
||||
import AttachmentCard from './AttachmentCard.svelte';
|
||||
import LocationDropdown from './LocationDropdown.svelte';
|
||||
let modal: HTMLDialogElement;
|
||||
|
||||
let wikiError: string = '';
|
||||
|
||||
let noPlaces: boolean = false;
|
||||
|
||||
let is_custom_location: boolean = false;
|
||||
|
||||
let reverseGeocodePlace: ReverseGeocode | null = null;
|
||||
|
||||
let adventure: Adventure = {
|
||||
id: '',
|
||||
name: '',
|
||||
|
@ -101,38 +86,33 @@
|
|||
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 imageError: string = '';
|
||||
let wikiImageError: string = '';
|
||||
|
||||
let old_display_name: string = '';
|
||||
let triggerMarkVisted: boolean = false;
|
||||
|
||||
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) {
|
||||
adventure.rating = NaN;
|
||||
|
@ -179,14 +159,11 @@
|
|||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length) {
|
||||
selectedFile = input.files[0];
|
||||
console.log('Selected file:', selectedFile);
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadAttachment(event: Event) {
|
||||
event.preventDefault();
|
||||
console.log('UPLOAD');
|
||||
console.log(selectedFile);
|
||||
|
||||
if (!selectedFile) {
|
||||
console.error('No files selected');
|
||||
|
@ -194,23 +171,18 @@
|
|||
}
|
||||
|
||||
const file = selectedFile;
|
||||
console.log(file);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('adventure', adventure.id);
|
||||
formData.append('name', attachmentName);
|
||||
|
||||
console.log(formData);
|
||||
|
||||
try {
|
||||
const res = await fetch('/adventures?/attachment', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
console.log(res);
|
||||
|
||||
if (res.ok) {
|
||||
const newData = deserialize(await res.text()) as { data: Attachment };
|
||||
adventure.attachments = [...adventure.attachments, newData.data];
|
||||
|
@ -230,11 +202,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
function clearMap() {
|
||||
console.log('CLEAR');
|
||||
markers = [];
|
||||
}
|
||||
|
||||
let imageSearch: string = adventure.name || '';
|
||||
|
||||
async function removeImage(id: string) {
|
||||
|
@ -244,7 +211,6 @@
|
|||
if (res.status === 204) {
|
||||
images = images.filter((image) => image.id !== id);
|
||||
adventure.images = images;
|
||||
console.log(images);
|
||||
addToast('success', $t('adventures.image_removed_success'));
|
||||
} else {
|
||||
addToast('error', $t('adventures.image_removed_error'));
|
||||
|
@ -258,51 +224,6 @@
|
|||
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) {
|
||||
let res = await fetch(`/api/images/${image_id}/toggle_primary`, {
|
||||
method: 'POST'
|
||||
|
@ -342,9 +263,7 @@
|
|||
});
|
||||
if (res.ok) {
|
||||
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 };
|
||||
console.log(newImage);
|
||||
images = [...images, newImage];
|
||||
adventure.images = images;
|
||||
addToast('success', $t('adventures.image_upload_success'));
|
||||
|
@ -395,9 +314,7 @@
|
|||
});
|
||||
if (res2.ok) {
|
||||
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 };
|
||||
console.log(newImage);
|
||||
images = [...images, newImage];
|
||||
adventure.images = images;
|
||||
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_end_date: string = '';
|
||||
|
@ -459,93 +354,6 @@
|
|||
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() {
|
||||
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) {
|
||||
event.preventDefault();
|
||||
console.log(adventure);
|
||||
triggerMarkVisted = true;
|
||||
|
||||
if (adventure.id === '') {
|
||||
console.log(categories);
|
||||
if (adventure.category?.display_name == '') {
|
||||
if (categories.some((category) => category.name === 'general')) {
|
||||
adventure.category = categories.find(
|
||||
|
@ -655,12 +432,6 @@
|
|||
addToast('error', $t('adventures.adventure_update_error'));
|
||||
}
|
||||
}
|
||||
if (
|
||||
adventure.is_visited &&
|
||||
(!reverseGeocodePlace?.region_visited || !reverseGeocodePlace?.city_visited)
|
||||
) {
|
||||
markVisited();
|
||||
}
|
||||
imageSearch = adventure.name;
|
||||
}
|
||||
</script>
|
||||
|
@ -811,150 +582,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
{$t('adventures.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>
|
||||
<LocationDropdown bind:item={adventure} bind:triggerMarkVisted {initialLatLng} />
|
||||
|
||||
<div class="collapse collapse-plus bg-base-200 mb-4 overflow-visible">
|
||||
<input type="checkbox" />
|
||||
|
@ -1257,6 +885,7 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
|||
|
||||
{#if immichIntegration}
|
||||
<ImmichSelect
|
||||
{adventure}
|
||||
on:fetchImage={(e) => {
|
||||
url = e.detail;
|
||||
fetchImage();
|
||||
|
@ -1314,7 +943,7 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
|||
{/if}
|
||||
|
||||
{#if adventure.is_public && adventure.id}
|
||||
<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>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-card-foreground font-mono">
|
||||
|
|
|
@ -34,9 +34,14 @@
|
|||
? `${user.first_name} ${user.last_name}`
|
||||
: user.username}
|
||||
</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('/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>
|
||||
<form method="post">
|
||||
<li><button formaction="/?/logout">{$t('navbar.logout')}</button></li>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { continentCodeToString, getFlag } from '$lib';
|
||||
import type { Country } from '$lib/types';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
import MapMarkerStar from '~icons/mdi/map-marker-star';
|
||||
|
||||
|
@ -37,15 +36,15 @@
|
|||
Visited {country.num_visits} Region{country.num_visits > 1 ? 's' : ''}
|
||||
</div>
|
||||
{: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}
|
||||
<div class="badge badge-error">Not Visited</div>
|
||||
<div class="badge badge-error">{$t('adventures.not_visited')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end">
|
||||
<!-- <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>
|
||||
|
|
|
@ -1,32 +1,81 @@
|
|||
<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 { 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();
|
||||
|
||||
let albums: ImmichAlbum[] = [];
|
||||
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) {
|
||||
immichImages = [];
|
||||
fetchAlbumAssets(currentAlbum);
|
||||
} else {
|
||||
immichImages = [];
|
||||
} 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 {
|
||||
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) {
|
||||
let res = await fetch(`/api/integrations/immich/albums/${album_id}`);
|
||||
if (res.ok) {
|
||||
let data = await res.json();
|
||||
immichNext = '';
|
||||
immichImages = data;
|
||||
}
|
||||
return fetchAssets(`/api/integrations/immich/albums/${album_id}`);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
|
@ -37,66 +86,23 @@
|
|||
}
|
||||
});
|
||||
|
||||
let immichImages: any[] = [];
|
||||
import { t } from 'svelte-i18n';
|
||||
import ImmichLogo from '$lib/assets/immich.svg';
|
||||
import type { ImmichAlbum } from '$lib/types';
|
||||
|
||||
async function searchImmich() {
|
||||
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 = '';
|
||||
}
|
||||
function buildQueryParams() {
|
||||
let params = new URLSearchParams();
|
||||
if (immichSearchValue && searchCategory === 'search') {
|
||||
params.append('query', immichSearchValue);
|
||||
} else if (selectedDate && searchCategory === 'date') {
|
||||
params.append('date', selectedDate);
|
||||
}
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
async function loadMoreImmich() {
|
||||
let res = await fetch(immichNext);
|
||||
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 = [...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 = '';
|
||||
}
|
||||
}
|
||||
const searchImmich = debounce(() => {
|
||||
_searchImmich();
|
||||
}, 500); // Debounce the search function to avoid multiple requests on every key press
|
||||
|
||||
async function _searchImmich() {
|
||||
immichImages = [];
|
||||
return fetchAssets(`/api/integrations/immich/search/?${buildQueryParams()}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -111,20 +117,27 @@
|
|||
on:click={() => (currentAlbum = '')}
|
||||
type="radio"
|
||||
class="join-item btn"
|
||||
bind:group={searchOrSelect}
|
||||
bind:group={searchCategory}
|
||||
value="search"
|
||||
aria-label="Search"
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
class="join-item btn"
|
||||
bind:group={searchOrSelect}
|
||||
value="select"
|
||||
bind:group={searchCategory}
|
||||
value="date"
|
||||
aria-label="Show by date"
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
class="join-item btn"
|
||||
bind:group={searchCategory}
|
||||
value="album"
|
||||
aria-label="Select Album"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{#if searchOrSelect === 'search'}
|
||||
{#if searchCategory === 'search'}
|
||||
<form on:submit|preventDefault={searchImmich}>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -134,7 +147,13 @@
|
|||
/>
|
||||
<button type="submit" class="btn btn-neutral mt-2">Search</button>
|
||||
</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}>
|
||||
<option value="" disabled selected>Select an Album</option>
|
||||
{#each albums as album}
|
||||
|
@ -147,14 +166,25 @@
|
|||
|
||||
<p class="text-red-500">{immichError}</p>
|
||||
<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}
|
||||
<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 -->
|
||||
<img
|
||||
src={`/immich/${image.id}`}
|
||||
alt="Image from Immich"
|
||||
class="h-24 w-24 object-cover rounded-md"
|
||||
/>
|
||||
<h4>
|
||||
{image.fileCreatedAt?.split('T')[0] || 'Unknown'}
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
|
@ -168,7 +198,7 @@
|
|||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{#if immichNext}
|
||||
{#if immichNextURL}
|
||||
<button class="btn btn-neutral" on:click={loadMoreImmich}>{$t('immich.load_more')}</button>
|
||||
{/if}
|
||||
</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 AboutModal from './AboutModal.svelte';
|
||||
import AccountMultiple from '~icons/mdi/account-multiple';
|
||||
import MapMarker from '~icons/mdi/map-marker';
|
||||
import FormatListBulletedSquare from '~icons/mdi/format-list-bulleted-square';
|
||||
import Earth from '~icons/mdi/earth';
|
||||
import Magnify from '~icons/mdi/magnify';
|
||||
import Map from '~icons/mdi/map';
|
||||
import Avatar from './Avatar.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { t, locale, locales } from 'svelte-i18n';
|
||||
|
@ -17,7 +22,11 @@
|
|||
|
||||
// Event listener for focusing input
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === '/' && document.activeElement !== inputElement) {
|
||||
// Ignore any keypresses in an input/textarea field, so we don't interfere with typing.
|
||||
if (
|
||||
event.key === '/' &&
|
||||
!['INPUT', 'TEXTAREA'].includes((event.target as HTMLElement)?.tagName)
|
||||
) {
|
||||
event.preventDefault(); // Prevent browser's search shortcut
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
|
@ -44,7 +53,8 @@
|
|||
nl: 'Nederlands',
|
||||
sv: 'Svenska',
|
||||
zh: '中文',
|
||||
pl: 'Polski'
|
||||
pl: 'Polski',
|
||||
ko: '한국어'
|
||||
};
|
||||
|
||||
let query: string = '';
|
||||
|
@ -110,6 +120,7 @@
|
|||
>
|
||||
{#if data.user}
|
||||
<li>
|
||||
<MapMarker />
|
||||
<button on:click={() => goto('/adventures')}>{$t('navbar.adventures')}</button>
|
||||
</li>
|
||||
<li>
|
||||
|
@ -167,36 +178,52 @@
|
|||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
<a class="btn btn-ghost text-xl" href="/"
|
||||
>AdventureLog <img src="/favicon.png" alt="Map Logo" class="w-10" /></a
|
||||
>
|
||||
<a class="btn btn-ghost text-2xl font-bold tracking-normal" href="/">
|
||||
AdventureLog <img src="/favicon.png" alt="Map Logo" class="w-10" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-center hidden lg:flex">
|
||||
<ul class="menu menu-horizontal px-1 gap-2">
|
||||
{#if data.user}
|
||||
<li>
|
||||
<button class="btn btn-neutral" on:click={() => goto('/adventures')}
|
||||
>{$t('navbar.adventures')}</button
|
||||
<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>
|
||||
<button class="btn btn-neutral" on:click={() => goto('/collections')}
|
||||
>{$t('navbar.collections')}</button
|
||||
<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>
|
||||
<button class="btn btn-neutral" on:click={() => goto('/worldtravel')}
|
||||
>{$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')}
|
||||
<button class="btn btn-neutral flex items-center gap-1" on:click={() => goto('/users')}
|
||||
><AccountMultiple /></button
|
||||
>
|
||||
</li>
|
||||
|
@ -225,9 +252,9 @@
|
|||
bind:this={inputElement}
|
||||
/><kbd class="kbd">/</kbd>
|
||||
</label>
|
||||
<button on:click={searchGo} type="submit" class="btn btn-neutral"
|
||||
>{$t('navbar.search')}</button
|
||||
>
|
||||
<button on:click={searchGo} type="submit" class="btn btn-neutral flex items-center gap-1">
|
||||
<Magnify class="w-5 h-5" />
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
</ul>
|
||||
|
|
|
@ -94,6 +94,7 @@
|
|||
'User-Agent': `AdventureLog / ${appVersion} `
|
||||
}
|
||||
});
|
||||
console.log(query);
|
||||
let data = await res.json();
|
||||
return data;
|
||||
};
|
||||
|
@ -243,6 +244,20 @@
|
|||
{$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={transportation.name}
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<!-- Type selection -->
|
||||
<div>
|
||||
<label for="type">
|
||||
|
@ -267,20 +282,6 @@
|
|||
</select>
|
||||
</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 -->
|
||||
<div>
|
||||
<label for="description">{$t('adventures.description')}</label><br />
|
||||
|
@ -464,7 +465,7 @@
|
|||
bind:value={starting_airport}
|
||||
name="starting_airport"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Enter starting airport code (e.g., JFK)"
|
||||
placeholder={$t('transportation.starting_airport_desc')}
|
||||
/>
|
||||
<label for="ending_airport" class="label">
|
||||
<span class="label-text">{$t('adventures.ending_airport')}</span>
|
||||
|
@ -475,10 +476,10 @@
|
|||
bind:value={ending_airport}
|
||||
name="ending_airport"
|
||||
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}>
|
||||
Fetch Location Information
|
||||
{$t('transportation.fetch_location_information')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
<!-- Card Actions -->
|
||||
<div class="card-actions justify-center mt-6">
|
||||
{#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
|
||||
</button>
|
||||
{:else if shared_with && !shared_with.includes(user.uuid)}
|
||||
|
|
|
@ -5,6 +5,7 @@ import type {
|
|||
Background,
|
||||
Checklist,
|
||||
Collection,
|
||||
Lodging,
|
||||
Note,
|
||||
Transportation,
|
||||
User
|
||||
|
@ -149,6 +150,50 @@ export function groupTransportationsByDate(
|
|||
return groupedTransportations;
|
||||
}
|
||||
|
||||
export function groupLodgingByDate(
|
||||
transportations: Lodging[],
|
||||
startDate: Date,
|
||||
numberOfDays: number
|
||||
): Record<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(
|
||||
notes: Note[],
|
||||
startDate: Date,
|
||||
|
@ -464,3 +509,13 @@ export function osmTagToEmoji(tag: string) {
|
|||
return '📍'; // Default placeholder emoji for unknown tags
|
||||
}
|
||||
}
|
||||
|
||||
export function debounce(func: Function, timeout: number) {
|
||||
let timer: number | NodeJS.Timeout;
|
||||
return (...args: any) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
func(...args);
|
||||
}, timeout);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -24,6 +24,11 @@
|
|||
"url": "backgrounds/adventurelog_showcase_5.webp",
|
||||
"author": "Sean Morley",
|
||||
"location": "Hoboken, New Jersey, USA"
|
||||
},
|
||||
{
|
||||
"url": "backgrounds/adventurelog_showcase_6.webp",
|
||||
"author": "Sean Morley",
|
||||
"location": "Smugglers' Notch Resort, Vermont, USA"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
export type User = {
|
||||
pk: number;
|
||||
username: string;
|
||||
email: string | null;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
date_joined: string | null;
|
||||
|
@ -41,6 +40,7 @@ export type Adventure = {
|
|||
is_visited?: boolean;
|
||||
category: Category | null;
|
||||
attachments: Attachment[];
|
||||
user?: User | null;
|
||||
};
|
||||
|
||||
export type Country = {
|
||||
|
@ -113,6 +113,7 @@ export type Collection = {
|
|||
end_date: string | null;
|
||||
transportations?: Transportation[];
|
||||
notes?: Note[];
|
||||
lodging?: Lodging[];
|
||||
checklists?: Checklist[];
|
||||
is_archived?: boolean;
|
||||
shared_with: string[] | undefined;
|
||||
|
@ -209,6 +210,7 @@ export type ReverseGeocode = {
|
|||
display_name: string;
|
||||
city: string;
|
||||
city_id: string;
|
||||
location_name: string;
|
||||
};
|
||||
|
||||
export type Category = {
|
||||
|
@ -262,3 +264,24 @@ export type Attachment = {
|
|||
user_id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Lodging = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
description: string | null;
|
||||
rating: number | null;
|
||||
link: string | null;
|
||||
check_in: string | null; // ISO 8601 date string
|
||||
check_out: string | null; // ISO 8601 date string
|
||||
reservation_number: string | null;
|
||||
price: number | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
location: string | null;
|
||||
is_public: boolean;
|
||||
collection: string | null;
|
||||
created_at: string; // ISO 8601 date string
|
||||
updated_at: string; // ISO 8601 date string
|
||||
};
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"about": {
|
||||
"about": "Um",
|
||||
"about": "Über",
|
||||
"close": "Schließen",
|
||||
"license": "Lizenziert unter der GPL-3.0-Lizenz.",
|
||||
"message": "Hergestellt mit ❤️ in den Vereinigten Staaten.",
|
||||
"nominatim_1": "Standortsuche und Geokodierung werden bereitgestellt von",
|
||||
"nominatim_2": "Ihre Daten werden unter der ODbL-Lizenz lizenziert.",
|
||||
"nominatim_2": "Deren Daten sind unter der ODbL-Lizenz lizenziert.",
|
||||
"oss_attributions": "Open-Source-Zuschreibungen",
|
||||
"other_attributions": "Weitere Hinweise finden Sie in der README-Datei.",
|
||||
"source_code": "Quellcode"
|
||||
|
@ -55,12 +55,12 @@
|
|||
"archived_collection_message": "Sammlung erfolgreich archiviert!",
|
||||
"archived_collections": "Archivierte Sammlungen",
|
||||
"ascending": "Aufsteigend",
|
||||
"cancel": "Stornieren",
|
||||
"cancel": "Abbrechen",
|
||||
"category_filter": "Kategoriefilter",
|
||||
"clear": "Klar",
|
||||
"clear": "zurücksetzen",
|
||||
"close_filters": "Filter schließen",
|
||||
"collection": "Sammlung",
|
||||
"collection_adventures": "Schließen Sie Sammlungsabenteuer ein",
|
||||
"collection_adventures": "Sammlungsabenteuer berücksichtigen",
|
||||
"count_txt": "Ergebnisse, die Ihrer Suche entsprechen",
|
||||
"date": "Datum",
|
||||
"dates": "Termine",
|
||||
|
@ -85,13 +85,13 @@
|
|||
"not_found": "Abenteuer nicht gefunden",
|
||||
"not_found_desc": "Das von Ihnen gesuchte Abenteuer konnte nicht gefunden werden. \nBitte probieren Sie ein anderes Abenteuer aus oder schauen Sie später noch einmal vorbei.",
|
||||
"open_filters": "Öffnen Sie Filter",
|
||||
"order_by": "Bestellen nach",
|
||||
"order_direction": "Bestellrichtung",
|
||||
"order_by": "Sortieren nach",
|
||||
"order_direction": "Sortierreihenfolge",
|
||||
"planned": "Geplant",
|
||||
"private": "Privat",
|
||||
"public": "Öffentlich",
|
||||
"rating": "Bewertung",
|
||||
"share": "Aktie",
|
||||
"share": "Teilen",
|
||||
"sort": "Sortieren",
|
||||
"sources": "Quellen",
|
||||
"start_before_end_error": "Das Startdatum muss vor dem Enddatum liegen",
|
||||
|
@ -109,12 +109,12 @@
|
|||
"add_an_activity": "Fügen Sie eine Aktivität hinzu",
|
||||
"add_notes": "Notizen hinzufügen",
|
||||
"adventure_create_error": "Das Abenteuer konnte nicht erstellt werden",
|
||||
"adventure_created": "Abenteuer geschaffen",
|
||||
"adventure_created": "Abenteuer erstellt",
|
||||
"adventure_update_error": "Das Abenteuer konnte nicht aktualisiert werden",
|
||||
"adventure_updated": "Abenteuer aktualisiert",
|
||||
"basic_information": "Grundlegende Informationen",
|
||||
"category": "Kategorie",
|
||||
"clear_map": "Klare Karte",
|
||||
"clear_map": "Karte leeren",
|
||||
"copy_link": "Link kopieren",
|
||||
"create_new": "Neu erstellen...",
|
||||
"date_constrain": "Auf Abholtermine beschränken",
|
||||
|
@ -137,7 +137,7 @@
|
|||
"no_start_date": "Bitte geben Sie ein Startdatum ein",
|
||||
"public_adventure": "Öffentliches Abenteuer",
|
||||
"remove": "Entfernen",
|
||||
"save_next": "Speichern",
|
||||
"save_next": "Speichern & weiter",
|
||||
"search_for_location": "Suchen Sie nach einem Ort",
|
||||
"search_results": "Suchergebnisse",
|
||||
"see_adventures": "Siehe Abenteuer",
|
||||
|
@ -154,7 +154,7 @@
|
|||
"all": "Alle",
|
||||
"error_updating_regions": "Fehler beim Aktualisieren der Regionen",
|
||||
"mark_region_as_visited": "Region {region}, {country} als besucht markieren?",
|
||||
"mark_visited": "Mark besucht",
|
||||
"mark_visited": "als besucht markieren",
|
||||
"my_adventures": "Meine Abenteuer",
|
||||
"no_adventures_found": "Keine Abenteuer gefunden",
|
||||
"no_collections_found": "Es wurden keine Sammlungen gefunden, zu denen dieses Abenteuer hinzugefügt werden kann.",
|
||||
|
@ -164,7 +164,7 @@
|
|||
"update_visited_regions": "Besuchte Regionen aktualisieren",
|
||||
"update_visited_regions_disclaimer": "Dies kann je nach Anzahl der Abenteuer, die Sie besucht haben, eine Weile dauern.",
|
||||
"visited_region_check": "Überprüfung der besuchten Region",
|
||||
"visited_region_check_desc": "Wenn Sie diese Option auswählen, überprüft der Server alle von Ihnen besuchten Abenteuer und markiert die Regionen, in denen sie sich befinden, als im Rahmen von Weltreisen besucht.",
|
||||
"visited_region_check_desc": "Wenn Sie diese Option auswählen, überprüft der Server alle von Ihnen besuchten Abenteuer und markiert die Regionen, in denen sie sich befinden, im Bereich Weltreisen als besucht.",
|
||||
"add_new": "Neu hinzufügen...",
|
||||
"checklist": "Checkliste",
|
||||
"checklists": "Checklisten",
|
||||
|
@ -185,7 +185,7 @@
|
|||
"visit_link": "Besuchen Sie den Link",
|
||||
"collection_archived": "Diese Sammlung wurde archiviert.",
|
||||
"day": "Tag",
|
||||
"add_a_tag": "Fügen Sie ein Tag hinzu",
|
||||
"add_a_tag": "Fügen Sie einen Tag hinzu",
|
||||
"tags": "Schlagworte",
|
||||
"set_to_pin": "Auf „Anpinnen“ setzen",
|
||||
"category_fetch_error": "Fehler beim Abrufen der Kategorien",
|
||||
|
@ -196,7 +196,7 @@
|
|||
"hide": "Verstecken",
|
||||
"show": "Zeigen",
|
||||
"download_calendar": "Kalender herunterladen",
|
||||
"md_instructions": "Schreiben Sie hier Ihren Abschlag...",
|
||||
"md_instructions": "Schreiben Sie hier Ihren Markdowntext...",
|
||||
"preview": "Vorschau",
|
||||
"checklist_delete_confirm": "Sind Sie sicher, dass Sie diese Checkliste löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"clear_location": "Standort löschen",
|
||||
|
@ -207,14 +207,14 @@
|
|||
"end": "Ende",
|
||||
"ending_airport": "Endflughafen",
|
||||
"flight_information": "Fluginformationen",
|
||||
"from": "Aus",
|
||||
"from": "Von",
|
||||
"no_location_found": "Kein Standort gefunden",
|
||||
"note_delete_confirm": "Sind Sie sicher, dass Sie diese Notiz löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"out_of_range": "Nicht im Datumsbereich der Reiseroute",
|
||||
"show_region_labels": "Regionsbeschriftungen anzeigen",
|
||||
"start": "Start",
|
||||
"starting_airport": "Startflughafen",
|
||||
"to": "Zu",
|
||||
"to": "Nach",
|
||||
"transportation_delete_confirm": "Sind Sie sicher, dass Sie diesen Transport löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"show_map": "Karte anzeigen",
|
||||
"will_be_marked": "wird als besucht markiert, sobald das Abenteuer gespeichert ist.",
|
||||
|
@ -234,7 +234,19 @@
|
|||
"images": "Bilder",
|
||||
"primary": "Primär",
|
||||
"upload": "Hochladen",
|
||||
"view_attachment": "Anhang anzeigen"
|
||||
"view_attachment": "Anhang anzeigen",
|
||||
"of": "von",
|
||||
"city": "Stadt",
|
||||
"display_name": "Anzeigename",
|
||||
"location_details": "Standortdetails",
|
||||
"lodging": "Unterkunft",
|
||||
"region": "Region",
|
||||
"delete_lodging": "Unterkunft löschen",
|
||||
"lodging_delete_confirm": "Sind Sie sicher, dass Sie diesen Standort löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"lodging_information": "Unterkunftsinformationen",
|
||||
"price": "Preis",
|
||||
"reservation_number": "Reservierungsnummer",
|
||||
"welcome_map_info": "Öffentliche Abenteuer auf diesem Server"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit",
|
||||
|
@ -254,7 +266,7 @@
|
|||
"about": "Über AdventureLog",
|
||||
"adventures": "Abenteuer",
|
||||
"collections": "Sammlungen",
|
||||
"discord": "Zwietracht",
|
||||
"discord": "Discord",
|
||||
"documentation": "Dokumentation",
|
||||
"greeting": "Hallo",
|
||||
"logout": "Abmelden",
|
||||
|
@ -269,7 +281,7 @@
|
|||
"aqua": "Aqua",
|
||||
"dark": "Dunkel",
|
||||
"forest": "Wald",
|
||||
"light": "Licht",
|
||||
"light": "Hell",
|
||||
"night": "Nacht",
|
||||
"aestheticDark": "Ästhetisches Dunkel",
|
||||
"aestheticLight": "Ästhetisches Licht",
|
||||
|
@ -281,7 +293,8 @@
|
|||
"tag": "Etikett",
|
||||
"language_selection": "Sprache",
|
||||
"support": "Unterstützung",
|
||||
"calendar": "Kalender"
|
||||
"calendar": "Kalender",
|
||||
"admin_panel": "Admin -Panel"
|
||||
},
|
||||
"auth": {
|
||||
"confirm_password": "Passwort bestätigen",
|
||||
|
@ -302,7 +315,11 @@
|
|||
"both_passwords_required": "Beide Passwörter sind erforderlich",
|
||||
"new_password": "Neues Passwort",
|
||||
"reset_failed": "Passwort konnte nicht zurückgesetzt werden",
|
||||
"or_3rd_party": "Oder melden Sie sich bei einem Drittanbieter an"
|
||||
"or_3rd_party": "Oder melden Sie sich bei einem Drittanbieter an",
|
||||
"no_public_adventures": "Keine öffentlichen Abenteuer gefunden",
|
||||
"no_public_collections": "Keine öffentlichen Sammlungen gefunden",
|
||||
"user_adventures": "Benutzerabenteuer",
|
||||
"user_collections": "Benutzersammlungen"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "Keine Benutzer mit öffentlichen Profilen gefunden."
|
||||
|
@ -317,7 +334,7 @@
|
|||
"not_visited": "Nicht besucht",
|
||||
"num_countries": "Länder gefunden",
|
||||
"partially_visited": "Teilweise besucht",
|
||||
"all_visited": "Sie haben alle Regionen in besucht",
|
||||
"all_visited": "Sie haben alle Regionen besucht in",
|
||||
"cities": "Städte",
|
||||
"failed_to_mark_visit": "Der Besuch konnte nicht markiert werden",
|
||||
"failed_to_remove_visit": "Der Besuch von konnte nicht entfernt werden",
|
||||
|
@ -352,7 +369,7 @@
|
|||
"password_is_required": "Passwort ist erforderlich",
|
||||
"possible_reset": "Wenn die von Ihnen angegebene E-Mail-Adresse mit einem Konto verknüpft ist, erhalten Sie eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts!",
|
||||
"reset_password": "Passwort zurücksetzen",
|
||||
"submit": "Einreichen",
|
||||
"submit": "Absenden",
|
||||
"token_required": "Zum Zurücksetzen des Passworts sind Token und UID erforderlich.",
|
||||
"about_this_background": "Über diesen Hintergrund",
|
||||
"join_discord": "Treten Sie dem Discord bei",
|
||||
|
@ -379,7 +396,7 @@
|
|||
"generic_error": "Bei der Bearbeitung Ihrer Anfrage ist ein Fehler aufgetreten.",
|
||||
"invalid_code": "Ungültiger MFA-Code",
|
||||
"invalid_credentials": "Ungültiger Benutzername oder Passwort",
|
||||
"make_primary": "Machen Sie primär",
|
||||
"make_primary": "Zum bevorzugten machen",
|
||||
"mfa_disabled": "Multi-Faktor-Authentifizierung erfolgreich deaktiviert!",
|
||||
"mfa_enabled": "Multi-Faktor-Authentifizierung erfolgreich aktiviert!",
|
||||
"mfa_not_enabled": "MFA ist nicht aktiviert",
|
||||
|
@ -481,7 +498,10 @@
|
|||
"provide_start_date": "Bitte geben Sie ein Startdatum an",
|
||||
"start": "Start",
|
||||
"to_location": "Zum Standort",
|
||||
"transport_type": "Transporttyp"
|
||||
"transport_type": "Transporttyp",
|
||||
"ending_airport_desc": "Geben Sie den Ending Airport Code ein (z. B. lax)",
|
||||
"fetch_location_information": "Standortinformationen abrufen",
|
||||
"starting_airport_desc": "Geben Sie den Start -Flughafencode ein (z. B. JFK)"
|
||||
},
|
||||
"search": {
|
||||
"adventurelog_results": "AdventureLog-Ergebnisse",
|
||||
|
@ -531,7 +551,7 @@
|
|||
"countries_visited": "Besuchte Länder",
|
||||
"no_recent_adventures": "Keine aktuellen Abenteuer?",
|
||||
"recent_adventures": "Aktuelle Abenteuer",
|
||||
"total_adventures": "Totale Abenteuer",
|
||||
"total_adventures": "Abenteuer insgesamt",
|
||||
"total_visited_regions": "Insgesamt besuchte Regionen",
|
||||
"welcome_back": "Willkommen zurück",
|
||||
"total_visited_cities": "Insgesamt besuchte Städte"
|
||||
|
@ -567,5 +587,35 @@
|
|||
"phone": "Telefon",
|
||||
"recommendation": "Empfehlung",
|
||||
"website": "Webseite"
|
||||
},
|
||||
"lodging": {
|
||||
"apartment": "Wohnung",
|
||||
"bnb": "Übernachtung mit Frühstück",
|
||||
"cabin": "Kabine",
|
||||
"campground": "Campingplatz",
|
||||
"check_in": "Einchecken",
|
||||
"check_out": "Kasse",
|
||||
"date_and_time": "Datum",
|
||||
"edit": "Bearbeiten",
|
||||
"edit_lodging": "Unterkunft bearbeiten",
|
||||
"error_editing_lodging": "Fehlerbearbeitung",
|
||||
"hostel": "Herberge",
|
||||
"hotel": "Hotel",
|
||||
"house": "Haus",
|
||||
"lodging_added": "Unterkunft erfolgreich hinzugefügt!",
|
||||
"lodging_delete_error": "Fehler beim Löschen von Unterkünften",
|
||||
"lodging_deleted": "Unterkunft erfolgreich gelöscht!",
|
||||
"lodging_edit_success": "Unterbringung erfolgreich bearbeitet!",
|
||||
"lodging_type": "Unterkunftstyp",
|
||||
"motel": "Motel",
|
||||
"new_lodging": "Neue Unterkunft",
|
||||
"other": "Andere",
|
||||
"provide_start_date": "Bitte geben Sie einen Startdatum an",
|
||||
"reservation_number": "Reservierungsnummer",
|
||||
"resort": "Resort",
|
||||
"start": "Start",
|
||||
"type": "Typ",
|
||||
"villa": "Villa",
|
||||
"current_timezone": "Aktuelle Zeitzone"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"support": "Support",
|
||||
"calendar": "Calendar",
|
||||
"theme_selection": "Theme Selection",
|
||||
"admin_panel": "Admin Panel",
|
||||
"themes": {
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
|
@ -67,9 +68,11 @@
|
|||
"checklist_delete_confirm": "Are you sure you want to delete this checklist? This action cannot be undone.",
|
||||
"note_delete_confirm": "Are you sure you want to delete this note? This action cannot be undone.",
|
||||
"transportation_delete_confirm": "Are you sure you want to delete this transportation? This action cannot be undone.",
|
||||
"lodging_delete_confirm": "Are you sure you want to delete this lodging location? This action cannot be undone.",
|
||||
"delete_checklist": "Delete Checklist",
|
||||
"delete_note": "Delete Note",
|
||||
"delete_transportation": "Delete Transportation",
|
||||
"delete_lodging": "Delete Lodging",
|
||||
"open_details": "Open Details",
|
||||
"edit_adventure": "Edit Adventure",
|
||||
"remove_from_collection": "Remove from Collection",
|
||||
|
@ -165,6 +168,7 @@
|
|||
"delete_collection_success": "Collection deleted successfully!",
|
||||
"delete_collection_warning": "Are you sure you want to delete this collection? This will also delete all of the linked adventures. This action cannot be undone.",
|
||||
"cancel": "Cancel",
|
||||
"of": "of",
|
||||
"delete_collection": "Delete Collection",
|
||||
"delete_adventure": "Delete Adventure",
|
||||
"adventure_delete_success": "Adventure deleted successfully!",
|
||||
|
@ -190,6 +194,7 @@
|
|||
"no_description_found": "No description found",
|
||||
"adventure_created": "Adventure created",
|
||||
"adventure_create_error": "Failed to create adventure",
|
||||
"lodging": "Lodging",
|
||||
"create_adventure": "Create Adventure",
|
||||
"adventure_updated": "Adventure updated",
|
||||
"adventure_update_error": "Failed to update adventure",
|
||||
|
@ -198,6 +203,7 @@
|
|||
"new_adventure": "New Adventure",
|
||||
"basic_information": "Basic Information",
|
||||
"no_adventures_to_recommendations": "No adventures found. Add at leat one adventure to get recommendations.",
|
||||
"display_name": "Display Name",
|
||||
"adventure_not_found": "There are no adventures to display. Add some using the plus button at the bottom right or try changing filters!",
|
||||
"no_adventures_found": "No adventures found",
|
||||
"mark_region_as_visited": "Mark region {region}, {country} as visited?",
|
||||
|
@ -248,6 +254,9 @@
|
|||
"out_of_range": "Not in itinerary date range",
|
||||
"preview": "Preview",
|
||||
"finding_recommendations": "Discovering hidden gems for your next adventure",
|
||||
"location_details": "Location Details",
|
||||
"city": "City",
|
||||
"region": "Region",
|
||||
"md_instructions": "Write your markdown here...",
|
||||
"days": "days",
|
||||
"attachment_upload_success": "Attachment uploaded successfully!",
|
||||
|
@ -257,6 +266,7 @@
|
|||
"attachment_update_success": "Attachment updated successfully!",
|
||||
"attachment_name": "Attachment Name",
|
||||
"gpx_tip": "Upload GPX files to attachments to view them on the map!",
|
||||
"welcome_map_info": "Public adventures on this server",
|
||||
"attachment_update_error": "Error updating attachment",
|
||||
"activities": {
|
||||
"general": "General 🌍",
|
||||
|
@ -281,7 +291,10 @@
|
|||
"spiritual_journeys": "Spiritual Journeys 🧘♀️",
|
||||
"volunteer_work": "Volunteer Work 🤝",
|
||||
"other": "Other"
|
||||
}
|
||||
},
|
||||
"lodging_information": "Lodging Information",
|
||||
"price": "Price",
|
||||
"reservation_number": "Reservation Number"
|
||||
},
|
||||
"worldtravel": {
|
||||
"country_list": "Country List",
|
||||
|
@ -326,7 +339,11 @@
|
|||
"new_password": "New Password (6+ characters)",
|
||||
"both_passwords_required": "Both passwords are required",
|
||||
"reset_failed": "Failed to reset password",
|
||||
"or_3rd_party": "Or login with a third-party service"
|
||||
"or_3rd_party": "Or login with a third-party service",
|
||||
"no_public_adventures": "No public adventures found",
|
||||
"no_public_collections": "No public collections found",
|
||||
"user_adventures": "User Adventures",
|
||||
"user_collections": "User Collections"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "No users found with public profiles."
|
||||
|
@ -467,6 +484,9 @@
|
|||
"flight_number": "Flight Number",
|
||||
"from_location": "From Location",
|
||||
"to_location": "To Location",
|
||||
"fetch_location_information": "Fetch Location Information",
|
||||
"starting_airport_desc": "Enter starting airport code (e.g., JFK)",
|
||||
"ending_airport_desc": "Enter ending airport code (e.g., LAX)",
|
||||
"edit": "Edit",
|
||||
"modes": {
|
||||
"car": "Car",
|
||||
|
@ -483,6 +503,36 @@
|
|||
"start": "Start",
|
||||
"date_and_time": "Date & Time"
|
||||
},
|
||||
"lodging": {
|
||||
"lodging_deleted": "Lodging deleted successfully!",
|
||||
"lodging_delete_error": "Error deleting lodging",
|
||||
"provide_start_date": "Please provide a start date",
|
||||
"lodging_type": "Lodging Type",
|
||||
"type": "Type",
|
||||
"lodging_added": "Lodging added successfully!",
|
||||
"error_editing_lodging": "Error editing lodging",
|
||||
"new_lodging": "New Lodging",
|
||||
"check_in": "Check In",
|
||||
"check_out": "Check Out",
|
||||
"edit": "Edit",
|
||||
"lodging_edit_success": "Lodging edited successfully!",
|
||||
"edit_lodging": "Edit Lodging",
|
||||
"start": "Start",
|
||||
"date_and_time": "Date & Time",
|
||||
"hotel": "Hotel",
|
||||
"hostel": "Hostel",
|
||||
"resort": "Resort",
|
||||
"bnb": "Bed and Breakfast",
|
||||
"campground": "Campground",
|
||||
"cabin": "Cabin",
|
||||
"apartment": "Apartment",
|
||||
"house": "House",
|
||||
"villa": "Villa",
|
||||
"motel": "Motel",
|
||||
"other": "Other",
|
||||
"reservation_number": "Reservation Number",
|
||||
"current_timezone": "Current Timezone"
|
||||
},
|
||||
"search": {
|
||||
"adventurelog_results": "AdventureLog Results",
|
||||
"public_adventures": "Public Adventures",
|
||||
|
|
|
@ -30,7 +30,8 @@
|
|||
"tag": "Etiqueta",
|
||||
"language_selection": "Idioma",
|
||||
"support": "Apoyo",
|
||||
"calendar": "Calendario"
|
||||
"calendar": "Calendario",
|
||||
"admin_panel": "Panel de administración"
|
||||
},
|
||||
"about": {
|
||||
"about": "Acerca de",
|
||||
|
@ -281,7 +282,19 @@
|
|||
"primary": "Primario",
|
||||
"upload": "Subir",
|
||||
"view_attachment": "Ver archivo adjunto",
|
||||
"attachment_name": "Nombre del archivo adjunto"
|
||||
"attachment_name": "Nombre del archivo adjunto",
|
||||
"of": "de",
|
||||
"city": "Ciudad",
|
||||
"delete_lodging": "Eliminar alojamiento",
|
||||
"display_name": "Nombre para mostrar",
|
||||
"location_details": "Detalles de la ubicación",
|
||||
"lodging": "Alojamiento",
|
||||
"lodging_delete_confirm": "¿Estás seguro de que quieres eliminar este lugar de alojamiento? \nEsta acción no se puede deshacer.",
|
||||
"lodging_information": "Información de alojamiento",
|
||||
"price": "Precio",
|
||||
"region": "Región",
|
||||
"reservation_number": "Número de reserva",
|
||||
"welcome_map_info": "Aventuras públicas en este servidor"
|
||||
},
|
||||
"worldtravel": {
|
||||
"all": "Todo",
|
||||
|
@ -326,7 +339,11 @@
|
|||
"both_passwords_required": "Se requieren ambas contraseñas",
|
||||
"new_password": "Nueva contraseña",
|
||||
"reset_failed": "No se pudo restablecer la contraseña",
|
||||
"or_3rd_party": "O inicie sesión con un servicio de terceros"
|
||||
"or_3rd_party": "O inicie sesión con un servicio de terceros",
|
||||
"no_public_adventures": "No se encontraron aventuras públicas",
|
||||
"no_public_collections": "No se encontraron colecciones públicas",
|
||||
"user_adventures": "Aventuras de usuario",
|
||||
"user_collections": "Colecciones de usuarios"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "No se encontraron usuarios con perfiles públicos."
|
||||
|
@ -481,7 +498,10 @@
|
|||
"flight_number": "Número de vuelo",
|
||||
"from_location": "Desde la ubicación",
|
||||
"transportation_added": "¡Transporte agregado exitosamente!",
|
||||
"transportation_delete_error": "Error al eliminar el transporte"
|
||||
"transportation_delete_error": "Error al eliminar el transporte",
|
||||
"ending_airport_desc": "Ingrese el código de aeropuerto final (por ejemplo, LAX)",
|
||||
"fetch_location_information": "Información de ubicación para obtener",
|
||||
"starting_airport_desc": "Ingrese el código de aeropuerto inicial (por ejemplo, JFK)"
|
||||
},
|
||||
"search": {
|
||||
"adventurelog_results": "Resultados del registro de aventuras",
|
||||
|
@ -567,5 +587,35 @@
|
|||
"phone": "Teléfono",
|
||||
"recommendation": "Recomendación",
|
||||
"website": "Sitio web"
|
||||
},
|
||||
"lodging": {
|
||||
"apartment": "Departamento",
|
||||
"bnb": "Cama y desayuno",
|
||||
"cabin": "Cabina",
|
||||
"campground": "Terreno de camping",
|
||||
"check_in": "Registrarse",
|
||||
"check_out": "Verificar",
|
||||
"date_and_time": "Fecha",
|
||||
"edit": "Editar",
|
||||
"error_editing_lodging": "Error de edición de alojamiento",
|
||||
"hostel": "Albergue",
|
||||
"hotel": "Hotel",
|
||||
"house": "Casa",
|
||||
"lodging_added": "¡Alojamiento agregado con éxito!",
|
||||
"lodging_delete_error": "Error de eliminación de alojamiento",
|
||||
"lodging_deleted": "¡Alojamiento eliminado con éxito!",
|
||||
"lodging_edit_success": "¡Alojamiento editado con éxito!",
|
||||
"lodging_type": "Tipo de alojamiento",
|
||||
"motel": "Motel",
|
||||
"new_lodging": "Nuevo alojamiento",
|
||||
"other": "Otro",
|
||||
"provide_start_date": "Proporcione una fecha de inicio",
|
||||
"reservation_number": "Número de reserva",
|
||||
"resort": "Complejo",
|
||||
"start": "Comenzar",
|
||||
"type": "Tipo",
|
||||
"villa": "Villa",
|
||||
"edit_lodging": "Editar alojamiento",
|
||||
"current_timezone": "Zona horaria"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -234,7 +234,19 @@
|
|||
"images": "Images",
|
||||
"primary": "Primaire",
|
||||
"upload": "Télécharger",
|
||||
"view_attachment": "Voir la pièce jointe"
|
||||
"view_attachment": "Voir la pièce jointe",
|
||||
"of": "de",
|
||||
"city": "Ville",
|
||||
"delete_lodging": "Supprimer l'hébergement",
|
||||
"display_name": "Nom d'affichage",
|
||||
"location_details": "Détails de l'emplacement",
|
||||
"lodging": "Hébergement",
|
||||
"lodging_delete_confirm": "Êtes-vous sûr de vouloir supprimer cet emplacement d'hébergement? \nCette action ne peut pas être annulée.",
|
||||
"lodging_information": "Informations sur l'hébergement",
|
||||
"price": "Prix",
|
||||
"region": "Région",
|
||||
"reservation_number": "Numéro de réservation",
|
||||
"welcome_map_info": "Aventures publiques sur ce serveur"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
|
||||
|
@ -281,7 +293,8 @@
|
|||
"tag": "Étiqueter",
|
||||
"language_selection": "Langue",
|
||||
"support": "Soutien",
|
||||
"calendar": "Calendrier"
|
||||
"calendar": "Calendrier",
|
||||
"admin_panel": "Panneau d'administration"
|
||||
},
|
||||
"auth": {
|
||||
"confirm_password": "Confirmez le mot de passe",
|
||||
|
@ -302,7 +315,11 @@
|
|||
"both_passwords_required": "Les deux mots de passe sont requis",
|
||||
"new_password": "Nouveau mot de passe",
|
||||
"reset_failed": "Échec de la réinitialisation du mot de passe",
|
||||
"or_3rd_party": "Ou connectez-vous avec un service tiers"
|
||||
"or_3rd_party": "Ou connectez-vous avec un service tiers",
|
||||
"no_public_adventures": "Aucune aventure publique trouvée",
|
||||
"no_public_collections": "Aucune collection publique trouvée",
|
||||
"user_adventures": "Aventures utilisateur",
|
||||
"user_collections": "Collections d'utilisateurs"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "Aucun utilisateur trouvé avec des profils publics."
|
||||
|
@ -481,7 +498,10 @@
|
|||
"transportation_added": "Transport ajouté avec succès !",
|
||||
"transportation_delete_error": "Erreur lors de la suppression du transport",
|
||||
"transportation_deleted": "Transport supprimé avec succès !",
|
||||
"transportation_edit_success": "Transport modifié avec succès !"
|
||||
"transportation_edit_success": "Transport modifié avec succès !",
|
||||
"ending_airport_desc": "Entrez la fin du code aéroportuaire (par exemple, laxiste)",
|
||||
"fetch_location_information": "Récupérer les informations de localisation",
|
||||
"starting_airport_desc": "Entrez le code aéroport de démarrage (par exemple, JFK)"
|
||||
},
|
||||
"search": {
|
||||
"adventurelog_results": "Résultats du journal d'aventure",
|
||||
|
@ -567,5 +587,35 @@
|
|||
"phone": "Téléphone",
|
||||
"recommendation": "Recommandation",
|
||||
"website": "Site web"
|
||||
},
|
||||
"lodging": {
|
||||
"apartment": "Appartement",
|
||||
"bnb": "Bed and petit-déjeuner",
|
||||
"cabin": "Cabine",
|
||||
"campground": "Camping",
|
||||
"check_in": "Enregistrement",
|
||||
"check_out": "Vérifier",
|
||||
"date_and_time": "Date",
|
||||
"edit": "Modifier",
|
||||
"edit_lodging": "Modifier l'hébergement",
|
||||
"error_editing_lodging": "Édition d'erreurs Hébergement",
|
||||
"hostel": "Auberge",
|
||||
"hotel": "Hôtel",
|
||||
"house": "Maison",
|
||||
"lodging_added": "L'hébergement a ajouté avec succès!",
|
||||
"lodging_delete_error": "Erreur de suppression de l'hébergement",
|
||||
"lodging_deleted": "L'hébergement est supprimé avec succès!",
|
||||
"lodging_edit_success": "L'hébergement édité avec succès!",
|
||||
"lodging_type": "Type d'hébergement",
|
||||
"motel": "Motel",
|
||||
"new_lodging": "Nouveau logement",
|
||||
"other": "Autre",
|
||||
"provide_start_date": "Veuillez fournir une date de début",
|
||||
"reservation_number": "Numéro de réservation",
|
||||
"resort": "Station balnéaire",
|
||||
"start": "Commencer",
|
||||
"type": "Taper",
|
||||
"villa": "Villa",
|
||||
"current_timezone": "Fuseau horaire actuel"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -234,7 +234,19 @@
|
|||
"images": "Immagini",
|
||||
"primary": "Primario",
|
||||
"upload": "Caricamento",
|
||||
"view_attachment": "Visualizza allegato"
|
||||
"view_attachment": "Visualizza allegato",
|
||||
"of": "Di",
|
||||
"city": "Città",
|
||||
"delete_lodging": "Elimina alloggio",
|
||||
"display_name": "Nome da visualizzare",
|
||||
"location_details": "Dettagli della posizione",
|
||||
"lodging": "Alloggio",
|
||||
"lodging_delete_confirm": "Sei sicuro di voler eliminare questa posizione di alloggio? \nQuesta azione non può essere annullata.",
|
||||
"lodging_information": "Informazioni di alloggio",
|
||||
"price": "Prezzo",
|
||||
"region": "Regione",
|
||||
"welcome_map_info": "Avventure pubbliche su questo server",
|
||||
"reservation_number": "Numero di prenotazione"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Scopri, pianifica ed esplora con facilità",
|
||||
|
@ -281,7 +293,8 @@
|
|||
"tag": "Etichetta",
|
||||
"language_selection": "Lingua",
|
||||
"support": "Supporto",
|
||||
"calendar": "Calendario"
|
||||
"calendar": "Calendario",
|
||||
"admin_panel": "Pannello di amministrazione"
|
||||
},
|
||||
"auth": {
|
||||
"confirm_password": "Conferma password",
|
||||
|
@ -302,7 +315,11 @@
|
|||
"both_passwords_required": "Sono necessarie entrambe le password",
|
||||
"new_password": "Nuova parola d'ordine",
|
||||
"reset_failed": "Impossibile reimpostare la password",
|
||||
"or_3rd_party": "Oppure accedi con un servizio di terze parti"
|
||||
"or_3rd_party": "Oppure accedi con un servizio di terze parti",
|
||||
"no_public_adventures": "Nessuna avventura pubblica trovata",
|
||||
"no_public_collections": "Nessuna collezione pubblica trovata",
|
||||
"user_adventures": "Avventure utente",
|
||||
"user_collections": "Collezioni utente"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "Nessun utente trovato con profili pubblici."
|
||||
|
@ -481,7 +498,10 @@
|
|||
"transportation_delete_error": "Errore durante l'eliminazione del trasporto",
|
||||
"transportation_deleted": "Trasporto eliminato con successo!",
|
||||
"transportation_edit_success": "Trasporti modificati con successo!",
|
||||
"type": "Tipo"
|
||||
"type": "Tipo",
|
||||
"ending_airport_desc": "Immettere il codice aeroportuale finale (ad es. LAX)",
|
||||
"fetch_location_information": "Informazioni sulla posizione di recupero",
|
||||
"starting_airport_desc": "Immettere il codice dell'aeroporto di partenza (ad es. JFK)"
|
||||
},
|
||||
"search": {
|
||||
"adventurelog_results": "Risultati di AdventureLog",
|
||||
|
@ -567,5 +587,35 @@
|
|||
"phone": "Telefono",
|
||||
"recommendation": "Raccomandazione",
|
||||
"website": "Sito web"
|
||||
},
|
||||
"lodging": {
|
||||
"apartment": "Appartamento",
|
||||
"bnb": "Bed and Breakfast",
|
||||
"cabin": "Cabina",
|
||||
"campground": "Campeggio",
|
||||
"check_in": "Check -in",
|
||||
"check_out": "Guardare",
|
||||
"date_and_time": "Data",
|
||||
"edit": "Modificare",
|
||||
"edit_lodging": "Modifica alloggio",
|
||||
"error_editing_lodging": "Alloggio di modifica degli errori",
|
||||
"hostel": "Ostello",
|
||||
"hotel": "Hotel",
|
||||
"house": "Casa",
|
||||
"lodging_added": "Alloggio aggiunto con successo!",
|
||||
"new_lodging": "Nuovo alloggio",
|
||||
"other": "Altro",
|
||||
"provide_start_date": "Si prega di fornire una data di inizio",
|
||||
"reservation_number": "Numero di prenotazione",
|
||||
"resort": "Ricorrere",
|
||||
"start": "Inizio",
|
||||
"type": "Tipo",
|
||||
"villa": "Villa",
|
||||
"lodging_delete_error": "Errore di eliminazione dell'alloggio",
|
||||
"lodging_deleted": "Alloggio eliminato con successo!",
|
||||
"lodging_edit_success": "Alloggio modificato con successo!",
|
||||
"lodging_type": "Tipo di alloggio",
|
||||
"motel": "Motel",
|
||||
"current_timezone": "Fuso orario attuale"
|
||||
}
|
||||
}
|
||||
|
|
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",
|
||||
"collection": "Collectie",
|
||||
"collection_adventures": "Inclusief collectie-avonturen",
|
||||
"collection_link_error": "Fout bij het koppelen van avontuur aan collectie",
|
||||
"collection_link_error": "Fout bij het koppelen van dit avontuur aan de collectie",
|
||||
"collection_link_success": "Avontuur succesvol gekoppeld aan collectie!",
|
||||
"collection_remove_error": "Fout bij verwijderen van avontuur uit collectie",
|
||||
"collection_remove_error": "Fout bij verwijderen van dit avontuur uit de collectie",
|
||||
"collection_remove_success": "Avontuur is succesvol uit de collectie verwijderd!",
|
||||
"count_txt": "resultaten die overeenkomen met uw zoekopdracht",
|
||||
"create_new": "Maak nieuwe...",
|
||||
|
@ -94,9 +94,9 @@
|
|||
"dates": "Datums",
|
||||
"delete_adventure": "Avontuur verwijderen",
|
||||
"duration": "Duur",
|
||||
"image_removed_error": "Fout bij verwijderen van afbeelding",
|
||||
"image_removed_error": "Fout bij het verwijderen van de afbeelding",
|
||||
"image_removed_success": "Afbeelding succesvol verwijderd!",
|
||||
"image_upload_error": "Fout bij het uploaden van afbeelding",
|
||||
"image_upload_error": "Fout bij het uploaden van de afbeelding",
|
||||
"image_upload_success": "Afbeelding succesvol geüpload!",
|
||||
"no_image_url": "Er is geen afbeelding gevonden op die URL.",
|
||||
"planned": "Gepland",
|
||||
|
@ -117,7 +117,7 @@
|
|||
"category": "Categorie",
|
||||
"clear_map": "Kaart leegmaken",
|
||||
"copy_link": "Kopieer link",
|
||||
"date_constrain": "Beperk u tot ophaaldata",
|
||||
"date_constrain": "Beperk tot de datums van de collectie",
|
||||
"description": "Beschrijving",
|
||||
"end_date": "Einddatum",
|
||||
"fetch_image": "Afbeelding ophalen",
|
||||
|
@ -126,7 +126,7 @@
|
|||
"image_fetch_failed": "Kan afbeelding niet ophalen",
|
||||
"link": "Link",
|
||||
"location": "Locatie",
|
||||
"location_information": "Locatie-informatie",
|
||||
"location_information": "Informatie over de locatie",
|
||||
"my_images": "Mijn afbeeldingen",
|
||||
"my_visits": "Mijn bezoeken",
|
||||
"new_adventure": "Nieuw avontuur",
|
||||
|
@ -152,8 +152,8 @@
|
|||
"wikipedia": "Wikipedia",
|
||||
"adventure_not_found": "Er zijn geen avonturen om weer te geven. \nVoeg er een paar toe via de plusknop rechtsonder of probeer de filters te wijzigen!",
|
||||
"all": "Alle",
|
||||
"error_updating_regions": "Fout bij wijzigen van regio's",
|
||||
"mark_visited": "Markeer bezocht",
|
||||
"error_updating_regions": "Fout bij het wijzigen van regio's",
|
||||
"mark_visited": "Markeer als bezocht",
|
||||
"my_adventures": "Mijn avonturen",
|
||||
"no_adventures_found": "Geen avonturen gevonden",
|
||||
"no_collections_found": "Er zijn geen collecties gevonden waar dit avontuur aan kan worden toegevoegd.",
|
||||
|
@ -162,7 +162,7 @@
|
|||
"regions_updated": "regio's bijgewerkt",
|
||||
"update_visited_regions": "Werk bezochte regio's bij",
|
||||
"update_visited_regions_disclaimer": "Dit kan even duren, afhankelijk van het aantal avondturen dat je hebt beleefd.",
|
||||
"visited_region_check": "Check bezochte regio's",
|
||||
"visited_region_check": "Controleer bezochte regio's",
|
||||
"visited_region_check_desc": "Door dit te selecteren, controleert de server alle avonturen die je beleefde en markeert hun regio's als bezocht in de wereldreizen.",
|
||||
"add_new": "Nieuw toevoegen...",
|
||||
"checklist": "Controlelijst",
|
||||
|
@ -194,9 +194,9 @@
|
|||
"adventure_calendar": "Avonturenkalender",
|
||||
"emoji_picker": "Emoji-kiezer",
|
||||
"hide": "Verbergen",
|
||||
"show": "Show",
|
||||
"show": "Toon",
|
||||
"download_calendar": "Agenda downloaden",
|
||||
"md_instructions": "Schrijf hier uw korting...",
|
||||
"md_instructions": "Schrijf hier in markdown...",
|
||||
"preview": "Voorbeeld",
|
||||
"checklist_delete_confirm": "Weet u zeker dat u deze checklist wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
|
||||
"clear_location": "Locatie wissen",
|
||||
|
@ -212,10 +212,10 @@
|
|||
"out_of_range": "Niet binnen het datumbereik van het reisplan",
|
||||
"show_region_labels": "Toon regiolabels",
|
||||
"start": "Begin",
|
||||
"starting_airport": "Startende luchthaven",
|
||||
"starting_airport": "Luchthaven van vertrek",
|
||||
"to": "Naar",
|
||||
"transportation_delete_confirm": "Weet u zeker dat u dit transport wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
|
||||
"ending_airport": "Einde luchthaven",
|
||||
"ending_airport": "Luchthaven van aankomst",
|
||||
"show_map": "Toon kaart",
|
||||
"will_be_marked": "wordt gemarkeerd als bezocht zodra het avontuur is opgeslagen.",
|
||||
"cities_updated": "steden bijgewerkt",
|
||||
|
@ -225,16 +225,28 @@
|
|||
"attachment": "Bijlage",
|
||||
"attachment_delete_success": "Bijlage succesvol verwijderd!",
|
||||
"attachment_name": "Naam van bijlage",
|
||||
"attachment_update_error": "Fout bij bijwerken van bijlage",
|
||||
"attachment_update_error": "Fout bij het bijwerken van de bijlage",
|
||||
"attachment_update_success": "Bijlage succesvol bijgewerkt!",
|
||||
"attachment_upload_error": "Fout bij het uploaden van bijlage",
|
||||
"attachment_upload_error": "Fout bij het uploaden van de bijlage",
|
||||
"attachment_upload_success": "Bijlage succesvol geüpload!",
|
||||
"attachments": "Bijlagen",
|
||||
"gpx_tip": "Upload GPX-bestanden naar bijlagen om ze op de kaart te bekijken!",
|
||||
"images": "Afbeeldingen",
|
||||
"primary": "Primair",
|
||||
"upload": "Uploaden",
|
||||
"view_attachment": "Bijlage bekijken"
|
||||
"view_attachment": "Bijlage bekijken",
|
||||
"of": "van",
|
||||
"city": "Stad",
|
||||
"delete_lodging": "Verwijder accommodatie",
|
||||
"display_name": "Weergavenaam",
|
||||
"location_details": "Locatiegegevens",
|
||||
"lodging": "Onderdak",
|
||||
"reservation_number": "Reserveringsnummer",
|
||||
"welcome_map_info": "Publieke avonturen op deze server",
|
||||
"lodging_delete_confirm": "Weet u zeker dat u deze accommodatielocatie wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
|
||||
"lodging_information": "Informatie overliggen",
|
||||
"price": "Prijs",
|
||||
"region": "Regio"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Ontdek, plan en verken met gemak",
|
||||
|
@ -254,7 +266,7 @@
|
|||
"about": "Over AdventureLog",
|
||||
"adventures": "Avonturen",
|
||||
"collections": "Collecties",
|
||||
"discord": "discord",
|
||||
"discord": "Discord",
|
||||
"documentation": "Documentatie",
|
||||
"greeting": "Hoi",
|
||||
"logout": "Uitloggen",
|
||||
|
@ -281,7 +293,8 @@
|
|||
"tag": "Label",
|
||||
"language_selection": "Taal",
|
||||
"support": "Steun",
|
||||
"calendar": "Kalender"
|
||||
"calendar": "Kalender",
|
||||
"admin_panel": "Admin -paneel"
|
||||
},
|
||||
"auth": {
|
||||
"confirm_password": "Bevestig wachtwoord",
|
||||
|
@ -302,7 +315,11 @@
|
|||
"both_passwords_required": "Beide wachtwoorden zijn vereist",
|
||||
"new_password": "Nieuw wachtwoord",
|
||||
"reset_failed": "Kan het wachtwoord niet opnieuw instellen",
|
||||
"or_3rd_party": "Of log in met een service van derden"
|
||||
"or_3rd_party": "Of log in met een service van derden",
|
||||
"no_public_adventures": "Geen openbare avonturen gevonden",
|
||||
"no_public_collections": "Geen openbare collecties gevonden",
|
||||
"user_adventures": "Gebruikersavonturen",
|
||||
"user_collections": "Gebruikerscollecties"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "Er zijn geen gebruikers gevonden met openbare profielen."
|
||||
|
@ -325,8 +342,8 @@
|
|||
"no_cities_found": "Geen steden gevonden",
|
||||
"region_failed_visited": "Kan de regio niet als bezocht markeren",
|
||||
"region_stats": "Regiostatistieken",
|
||||
"regions_in": "Regio's binnen",
|
||||
"removed": "VERWIJDERD",
|
||||
"regions_in": "Regio's in",
|
||||
"removed": "verwijderd",
|
||||
"view_cities": "Steden bekijken",
|
||||
"visit_remove_failed": "Kan bezoek niet verwijderen",
|
||||
"visit_to": "Bezoek aan"
|
||||
|
@ -358,8 +375,8 @@
|
|||
"join_discord": "Sluit je aan bij Discord",
|
||||
"join_discord_desc": "om uw eigen foto's te delen. \nPlaats ze in de",
|
||||
"photo_by": "Foto door",
|
||||
"change_password_error": "Kan wachtwoord niet wijzigen. \nOngeldig huidig wachtwoord of ongeldig nieuw wachtwoord.",
|
||||
"current_password": "Huidig wachtwoord",
|
||||
"change_password_error": "Kan wachtwoord niet wijzigen. \nOngeldig huidig wachtwoord of ongeldig nieuw wachtwoord.",
|
||||
"current_password": "Huidig wachtwoord",
|
||||
"password_change_lopout_warning": "Na het wijzigen van uw wachtwoord wordt u uitgelogd.",
|
||||
"authenticator_code": "Authenticatiecode",
|
||||
"copy": "Kopiëren",
|
||||
|
@ -369,7 +386,7 @@
|
|||
"email_removed": "E-mail succesvol verwijderd!",
|
||||
"email_removed_error": "Fout bij verwijderen van e-mail",
|
||||
"email_set_primary": "E-mailadres is succesvol ingesteld als primair!",
|
||||
"email_set_primary_error": "Fout bij het instellen van e-mail als primair",
|
||||
"email_set_primary_error": "Fout bij het instellen van dit e-mail als primair",
|
||||
"email_verified": "E-mail succesvol geverifieerd!",
|
||||
"email_verified_erorr_desc": "Uw e-mailadres kan niet worden geverifieerd. \nProbeer het opnieuw.",
|
||||
"email_verified_error": "Fout bij het verifiëren van e-mailadres",
|
||||
|
@ -403,8 +420,8 @@
|
|||
"username_taken": "Deze gebruikersnaam is al in gebruik.",
|
||||
"administration_settings": "Beheerinstellingen",
|
||||
"documentation_link": "Documentatielink",
|
||||
"launch_account_connections": "Start Accountverbindingen",
|
||||
"launch_administration_panel": "Start het Beheerpaneel",
|
||||
"launch_account_connections": "Start accountverbindingen",
|
||||
"launch_administration_panel": "Start het beheerpaneel",
|
||||
"no_verified_email_warning": "U moet een geverifieerd e-mailadres hebben om tweefactorauthenticatie in te schakelen.",
|
||||
"social_auth_desc": "Schakel sociale en OIDC-authenticatieproviders in of uit voor uw account. \nMet deze verbindingen kunt u inloggen met zelfgehoste authenticatie-identiteitsproviders zoals Authentik of externe providers zoals GitHub.",
|
||||
"social_auth_desc_2": "Deze instellingen worden beheerd op de AdventureLog-server en moeten handmatig worden ingeschakeld door de beheerder.",
|
||||
|
@ -414,9 +431,9 @@
|
|||
},
|
||||
"checklist": {
|
||||
"add_item": "Artikel toevoegen",
|
||||
"checklist_delete_error": "Fout bij verwijderen van controlelijst",
|
||||
"checklist_delete_error": "Fout bij het verwijderen van de controlelijst",
|
||||
"checklist_deleted": "Controlelijst succesvol verwijderd!",
|
||||
"checklist_editor": "Controlelijst-editor",
|
||||
"checklist_editor": "Controlelijsten bewerken",
|
||||
"checklist_public": "Deze controlelijst is openbaar omdat deze zich in een openbare collectie bevindt.",
|
||||
"editing_checklist": "Controlelijst bewerken",
|
||||
"failed_to_save": "Kan controlelijst niet opslaan",
|
||||
|
@ -427,7 +444,7 @@
|
|||
"new_item": "Nieuw artikel",
|
||||
"save": "Opslaan",
|
||||
"checklist_viewer": "Controlelijstviewer",
|
||||
"new_checklist": "Nieuwe checklist"
|
||||
"new_checklist": "Nieuwe controlelijst"
|
||||
},
|
||||
"collection": {
|
||||
"collection_created": "Collectie succesvol aangemaakt!",
|
||||
|
@ -435,7 +452,7 @@
|
|||
"create": "Aanmaken",
|
||||
"edit_collection": "Collectie bewerken",
|
||||
"error_creating_collection": "Fout bij aanmaken collectie",
|
||||
"error_editing_collection": "Fout bij bewerken collectie",
|
||||
"error_editing_collection": "Fout bij het bewerken van de collectie",
|
||||
"new_collection": "Nieuwe collectie",
|
||||
"public_collection": "Openbare collectie"
|
||||
},
|
||||
|
@ -446,22 +463,22 @@
|
|||
"failed_to_save": "Kan opmerking niet opslaan",
|
||||
"note_delete_error": "Fout bij verwijderen van opmerking",
|
||||
"note_deleted": "Opmerking succesvol verwijderd!",
|
||||
"note_editor": "Opmerking-editor",
|
||||
"note_editor": "Opmerkingen bewerken",
|
||||
"note_public": "Deze opmerking is openbaar omdat deze zich in een openbare collectie bevindt.",
|
||||
"open": "Open",
|
||||
"save": "Opslaan",
|
||||
"invalid_url": "Ongeldige URL",
|
||||
"note_viewer": "Notitieviewer"
|
||||
"note_viewer": "Bekijk notities"
|
||||
},
|
||||
"transportation": {
|
||||
"date_and_time": "Datum",
|
||||
"date_time": "Startdatum",
|
||||
"edit": "Bewerking",
|
||||
"edit": "Bewerk",
|
||||
"edit_transportation": "Vervoer bewerken",
|
||||
"end_date_time": "Einddatum",
|
||||
"error_editing_transportation": "Fout bij bewerken van vervoer",
|
||||
"error_editing_transportation": "Fout bij het bewerken van het vervoer",
|
||||
"flight_number": "Vluchtnummer",
|
||||
"from_location": "Van locatie",
|
||||
"from_location": "Vertreklocatie",
|
||||
"modes": {
|
||||
"bike": "Fiets",
|
||||
"boat": "Boot",
|
||||
|
@ -470,9 +487,9 @@
|
|||
"car": "Auto",
|
||||
"other": "Ander",
|
||||
"plane": "Vliegtuig",
|
||||
"walking": "Lopen"
|
||||
"walking": "Wandelen"
|
||||
},
|
||||
"to_location": "Naar locatie",
|
||||
"to_location": "Aankomstlocatie",
|
||||
"transportation_edit_success": "Vervoer succesvol bewerkt!",
|
||||
"type": "Type",
|
||||
"new_transportation": "Nieuw vervoer",
|
||||
|
@ -481,7 +498,10 @@
|
|||
"transport_type": "Vervoerstype",
|
||||
"transportation_added": "Vervoer succesvol toegevoegd!",
|
||||
"transportation_delete_error": "Fout bij verwijderen vervoer",
|
||||
"transportation_deleted": "Vervoer succesvol verwijderd!"
|
||||
"transportation_deleted": "Vervoer succesvol verwijderd!",
|
||||
"ending_airport_desc": "Voer eindigende luchthavencode in (bijv. LAX)",
|
||||
"fetch_location_information": "Locatie -informatie ophalen",
|
||||
"starting_airport_desc": "Voer de startende luchthavencode in (bijv. JFK)"
|
||||
},
|
||||
"search": {
|
||||
"adventurelog_results": "AdventureLog-resultaten",
|
||||
|
@ -490,7 +510,7 @@
|
|||
},
|
||||
"map": {
|
||||
"add_adventure": "Voeg nieuw avontuur toe",
|
||||
"add_adventure_at_marker": "Voeg een nieuw avontuur toe bij markeerpunt",
|
||||
"add_adventure_at_marker": "Voeg een nieuw avontuur toe bij het markeerpunt",
|
||||
"adventure_map": "Avonturenkaart",
|
||||
"clear_marker": "Verwijder markeerpunt",
|
||||
"map_options": "Kaartopties",
|
||||
|
@ -520,10 +540,10 @@
|
|||
"categories": {
|
||||
"category_name": "Categorienaam",
|
||||
"edit_category": "Categorie bewerken",
|
||||
"icon": "Ikoon",
|
||||
"icon": "Icoon",
|
||||
"manage_categories": "Beheer categorieën",
|
||||
"no_categories_found": "Geen categorieën gevonden.",
|
||||
"select_category": "Selecteer categorie",
|
||||
"select_category": "Selecteer een categorie",
|
||||
"update_after_refresh": "De avonturenkaarten worden bijgewerkt zodra u de pagina vernieuwt."
|
||||
},
|
||||
"dashboard": {
|
||||
|
@ -538,7 +558,7 @@
|
|||
},
|
||||
"immich": {
|
||||
"api_key": "Immich API-sleutel",
|
||||
"api_note": "Let op: dit moet de URL naar de Immich API-server zijn, dus deze eindigt waarschijnlijk op /api, tenzij je een aangepaste configuratie hebt.",
|
||||
"api_note": "Let op: dit moet de URL naar de Immich API-server zijn, deze eindigt dus waarschijnlijk op /api, tenzij je een aangepaste configuratie hebt.",
|
||||
"disable": "Uitzetten",
|
||||
"enable_immich": "Schakel Immich in",
|
||||
"imageid_required": "Afbeeldings-ID is vereist",
|
||||
|
@ -559,7 +579,7 @@
|
|||
"server_url": "Immich-server-URL",
|
||||
"update_integration": "Integratie bijwerken",
|
||||
"documentation": "Immich-integratiedocumentatie",
|
||||
"localhost_note": "Opmerking: localhost zal hoogstwaarschijnlijk niet werken tenzij u de docker-netwerken dienovereenkomstig hebt ingesteld. \nHet wordt aanbevolen om het IP-adres van de server of de domeinnaam te gebruiken."
|
||||
"localhost_note": "Opmerking: localhost zal hoogstwaarschijnlijk niet werken tenzij dit bewust zo geconfigureerd is in het docker-netwerk. \nHet is aanbevolen om het IP-adres van de server of de domeinnaam te gebruiken."
|
||||
},
|
||||
"recomendations": {
|
||||
"address": "Adres",
|
||||
|
@ -567,5 +587,35 @@
|
|||
"phone": "Telefoon",
|
||||
"recommendation": "Aanbeveling",
|
||||
"website": "Website"
|
||||
},
|
||||
"lodging": {
|
||||
"apartment": "Appartement",
|
||||
"bnb": "Bed and breakfast",
|
||||
"cabin": "Cabine",
|
||||
"campground": "Camping",
|
||||
"check_in": "Inchecken",
|
||||
"check_out": "Uitchecken",
|
||||
"date_and_time": "Datum",
|
||||
"edit": "Bewerking",
|
||||
"edit_lodging": "Bewerkingen bewerken",
|
||||
"error_editing_lodging": "Foutbewerkingsbewerkingen",
|
||||
"hostel": "Hostel",
|
||||
"hotel": "Hotel",
|
||||
"house": "Huis",
|
||||
"lodging_added": "Lodging met succes toegevoegd!",
|
||||
"lodging_delete_error": "Fout bij het verwijderen van accommodatie",
|
||||
"lodging_deleted": "Met succes verwijderd!",
|
||||
"lodging_edit_success": "Lodging met succes bewerkt!",
|
||||
"lodging_type": "Lodging type",
|
||||
"motel": "Motel",
|
||||
"new_lodging": "Nieuwe accommodatie",
|
||||
"other": "Ander",
|
||||
"provide_start_date": "Geef een startdatum op",
|
||||
"reservation_number": "Reserveringsnummer",
|
||||
"resort": "Toevlucht",
|
||||
"start": "Begin",
|
||||
"type": "Type",
|
||||
"villa": "Villa",
|
||||
"current_timezone": "Huidige tijdzone"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,8 @@
|
|||
"aqua": "Aqua",
|
||||
"northernLights": "Zorza Polarna"
|
||||
},
|
||||
"calendar": "Kalendarz"
|
||||
"calendar": "Kalendarz",
|
||||
"admin_panel": "Panel administracyjny"
|
||||
},
|
||||
"about": {
|
||||
"about": "O aplikacji",
|
||||
|
@ -281,7 +282,19 @@
|
|||
"images": "Obrazy",
|
||||
"primary": "Podstawowy",
|
||||
"upload": "Wgrywać",
|
||||
"view_attachment": "Zobacz załącznik"
|
||||
"view_attachment": "Zobacz załącznik",
|
||||
"of": "z",
|
||||
"city": "Miasto",
|
||||
"delete_lodging": "Usunąć zakwaterowanie",
|
||||
"display_name": "Nazwa wyświetlania",
|
||||
"location_details": "Szczegóły lokalizacji",
|
||||
"lodging": "Kwatera",
|
||||
"lodging_delete_confirm": "Czy na pewno chcesz usunąć tę lokalizację zakwaterowania? \nTego działania nie można cofnąć.",
|
||||
"lodging_information": "Informacje o zakwaterowaniu",
|
||||
"price": "Cena",
|
||||
"region": "Region",
|
||||
"reservation_number": "Numer rezerwacji",
|
||||
"welcome_map_info": "Publiczne przygody na tym serwerze"
|
||||
},
|
||||
"worldtravel": {
|
||||
"country_list": "Lista krajów",
|
||||
|
@ -326,7 +339,11 @@
|
|||
"both_passwords_required": "Obydwa hasła są wymagane",
|
||||
"new_password": "Nowe hasło",
|
||||
"reset_failed": "Nie udało się zresetować hasła",
|
||||
"or_3rd_party": "Lub zaloguj się za pomocą usługi strony trzeciej"
|
||||
"or_3rd_party": "Lub zaloguj się za pomocą usługi strony trzeciej",
|
||||
"no_public_adventures": "Nie znaleziono publicznych przygód",
|
||||
"no_public_collections": "Nie znaleziono publicznych kolekcji",
|
||||
"user_adventures": "Przygody użytkowników",
|
||||
"user_collections": "Kolekcje użytkowników"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "Nie znaleziono użytkowników z publicznymi profilami."
|
||||
|
@ -481,7 +498,10 @@
|
|||
"transportation_edit_success": "Transport edytowany pomyślnie!",
|
||||
"edit_transportation": "Edytuj transport",
|
||||
"start": "Początek",
|
||||
"date_and_time": "Data i godzina"
|
||||
"date_and_time": "Data i godzina",
|
||||
"ending_airport_desc": "Wprowadź końcowe kod lotniska (np. LAX)",
|
||||
"fetch_location_information": "Pobierać informacje o lokalizacji",
|
||||
"starting_airport_desc": "Wprowadź początkowy kod lotniska (np. JFK)"
|
||||
},
|
||||
"search": {
|
||||
"adventurelog_results": "Wyniki AdventureLog",
|
||||
|
@ -567,5 +587,35 @@
|
|||
"phone": "Telefon",
|
||||
"recommendation": "Zalecenie",
|
||||
"website": "Strona internetowa"
|
||||
},
|
||||
"lodging": {
|
||||
"apartment": "Apartament",
|
||||
"bnb": "Nocleg i śniadanie",
|
||||
"cabin": "Kabina",
|
||||
"campground": "Obozowisko",
|
||||
"lodging_added": "Lodowanie dodane pomyślnie!",
|
||||
"lodging_delete_error": "Usuwanie błędów",
|
||||
"lodging_deleted": "Z powodzeniem usunięto!",
|
||||
"lodging_edit_success": "Złożone zredagowane zredagowane!",
|
||||
"lodging_type": "Typ składowania",
|
||||
"motel": "Motel",
|
||||
"start": "Start",
|
||||
"type": "Typ",
|
||||
"villa": "Willa",
|
||||
"check_in": "Zameldować się",
|
||||
"check_out": "Wymeldować się",
|
||||
"date_and_time": "Data",
|
||||
"edit": "Redagować",
|
||||
"edit_lodging": "Edytuj zakwaterowanie",
|
||||
"error_editing_lodging": "Edycja błędów",
|
||||
"hostel": "Schronisko",
|
||||
"hotel": "Hotel",
|
||||
"house": "Dom",
|
||||
"new_lodging": "Nowe zakwaterowanie",
|
||||
"other": "Inny",
|
||||
"provide_start_date": "Proszę podać datę rozpoczęcia",
|
||||
"reservation_number": "Numer rezerwacji",
|
||||
"resort": "Uciec",
|
||||
"current_timezone": "Obecna strefa czasowa"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -234,7 +234,19 @@
|
|||
"images": "Bilder",
|
||||
"primary": "Primär",
|
||||
"upload": "Ladda upp",
|
||||
"view_attachment": "Visa bilaga"
|
||||
"view_attachment": "Visa bilaga",
|
||||
"of": "av",
|
||||
"city": "Stad",
|
||||
"delete_lodging": "Ta bort logi",
|
||||
"display_name": "Visningsnamn",
|
||||
"location_details": "Platsinformation",
|
||||
"lodging": "Logi",
|
||||
"welcome_map_info": "Offentliga äventyr på denna server",
|
||||
"lodging_delete_confirm": "Är du säker på att du vill ta bort den här logiplatsen? \nDenna åtgärd kan inte ångras.",
|
||||
"lodging_information": "Logi information",
|
||||
"price": "Pris",
|
||||
"region": "Område",
|
||||
"reservation_number": "Bokningsnummer"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Upptäck, planera och utforska med lätthet",
|
||||
|
@ -281,7 +293,8 @@
|
|||
"tag": "Tagg",
|
||||
"language_selection": "Språk",
|
||||
"support": "Support",
|
||||
"calendar": "Kalender"
|
||||
"calendar": "Kalender",
|
||||
"admin_panel": "Administratör"
|
||||
},
|
||||
"worldtravel": {
|
||||
"all": "Alla",
|
||||
|
@ -326,7 +339,11 @@
|
|||
"both_passwords_required": "Båda lösenorden krävs",
|
||||
"new_password": "Nytt lösenord",
|
||||
"reset_failed": "Det gick inte att återställa lösenordet",
|
||||
"or_3rd_party": "Eller logga in med en tredjepartstjänst"
|
||||
"or_3rd_party": "Eller logga in med en tredjepartstjänst",
|
||||
"no_public_adventures": "Inga offentliga äventyr hittades",
|
||||
"no_public_collections": "Inga offentliga samlingar hittades",
|
||||
"user_adventures": "Användaräventyr",
|
||||
"user_collections": "Användarsamlingar"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "Inga användare hittades med offentliga profiler."
|
||||
|
@ -481,7 +498,10 @@
|
|||
"transportation_delete_error": "Det gick inte att ta bort transport",
|
||||
"transportation_deleted": "Transporten har raderats!",
|
||||
"transportation_edit_success": "Transporten har redigerats!",
|
||||
"type": "Typ"
|
||||
"type": "Typ",
|
||||
"ending_airport_desc": "Ange slut på flygplatskoden (t.ex. LAX)",
|
||||
"fetch_location_information": "Hämta platsinformation",
|
||||
"starting_airport_desc": "Ange start av flygplatskoden (t.ex. JFK)"
|
||||
},
|
||||
"search": {
|
||||
"adventurelog_results": "AdventureLog-resultat",
|
||||
|
@ -567,5 +587,35 @@
|
|||
"phone": "Telefon",
|
||||
"recommendation": "Rekommendation",
|
||||
"website": "Webbplats"
|
||||
},
|
||||
"lodging": {
|
||||
"apartment": "Lägenhet",
|
||||
"bnb": "Säng och frukost",
|
||||
"cabin": "Stuga",
|
||||
"campground": "Campingplats",
|
||||
"error_editing_lodging": "Felredigeringsbefäl",
|
||||
"hostel": "Vandrarhem",
|
||||
"hotel": "Hotell",
|
||||
"house": "Hus",
|
||||
"lodging_added": "Logi tillagd framgångsrikt!",
|
||||
"lodging_delete_error": "Felladering logi",
|
||||
"lodging_deleted": "Logga raderas framgångsrikt!",
|
||||
"lodging_edit_success": "LOGGE Redigerad framgångsrikt!",
|
||||
"lodging_type": "Logi",
|
||||
"motel": "Motell",
|
||||
"new_lodging": "Inställning",
|
||||
"other": "Andra",
|
||||
"provide_start_date": "Vänligen ange ett startdatum",
|
||||
"reservation_number": "Bokningsnummer",
|
||||
"resort": "Tillflykt",
|
||||
"start": "Start",
|
||||
"type": "Typ",
|
||||
"villa": "Villa",
|
||||
"check_in": "Checka in",
|
||||
"check_out": "Checka ut",
|
||||
"date_and_time": "Datum",
|
||||
"edit": "Redigera",
|
||||
"edit_lodging": "Redigera logi",
|
||||
"current_timezone": "Nuvarande tidszon"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -234,7 +234,19 @@
|
|||
"images": "图片",
|
||||
"primary": "基本的",
|
||||
"upload": "上传",
|
||||
"view_attachment": "查看附件"
|
||||
"view_attachment": "查看附件",
|
||||
"of": "的",
|
||||
"city": "城市",
|
||||
"delete_lodging": "删除住宿",
|
||||
"display_name": "显示名称",
|
||||
"location_details": "位置详细信息",
|
||||
"lodging": "住宿",
|
||||
"lodging_delete_confirm": "您确定要删除此住宿地点吗?\n该动作不能撤消。",
|
||||
"lodging_information": "住宿信息",
|
||||
"price": "价格",
|
||||
"region": "地区",
|
||||
"reservation_number": "预订号",
|
||||
"welcome_map_info": "该服务器上的公共冒险"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "轻松发现、规划和探索",
|
||||
|
@ -281,7 +293,8 @@
|
|||
"tag": "标签",
|
||||
"language_selection": "语言",
|
||||
"support": "支持",
|
||||
"calendar": "日历"
|
||||
"calendar": "日历",
|
||||
"admin_panel": "管理面板"
|
||||
},
|
||||
"auth": {
|
||||
"forgot_password": "忘记密码?",
|
||||
|
@ -302,7 +315,11 @@
|
|||
"both_passwords_required": "两个密码都需要",
|
||||
"new_password": "新密码",
|
||||
"reset_failed": "重置密码失败",
|
||||
"or_3rd_party": "或者使用第三方服务登录"
|
||||
"or_3rd_party": "或者使用第三方服务登录",
|
||||
"no_public_adventures": "找不到公共冒险",
|
||||
"no_public_collections": "找不到公共收藏",
|
||||
"user_adventures": "用户冒险",
|
||||
"user_collections": "用户收集"
|
||||
},
|
||||
"worldtravel": {
|
||||
"all": "全部",
|
||||
|
@ -481,7 +498,10 @@
|
|||
"transportation_delete_error": "删除交通时出错",
|
||||
"transportation_deleted": "交通删除成功!",
|
||||
"transportation_edit_success": "交通编辑成功!",
|
||||
"type": "类型"
|
||||
"type": "类型",
|
||||
"ending_airport_desc": "输入结束机场代码(例如LAX)",
|
||||
"fetch_location_information": "获取位置信息",
|
||||
"starting_airport_desc": "输入启动机场代码(例如肯尼迪国际机构)"
|
||||
},
|
||||
"search": {
|
||||
"adventurelog_results": "冒险日志结果",
|
||||
|
@ -567,5 +587,35 @@
|
|||
"phone": "电话",
|
||||
"recommendation": "推荐",
|
||||
"website": "网站"
|
||||
},
|
||||
"lodging": {
|
||||
"campground": "营地",
|
||||
"check_in": "报到",
|
||||
"check_out": "查看",
|
||||
"date_and_time": "日期",
|
||||
"edit": "编辑",
|
||||
"edit_lodging": "编辑住宿",
|
||||
"error_editing_lodging": "错误编辑住宿",
|
||||
"hostel": "旅馆",
|
||||
"hotel": "酒店",
|
||||
"house": "房子",
|
||||
"lodging_added": "住宿成功增加了!",
|
||||
"lodging_delete_error": "错误删除住宿",
|
||||
"lodging_deleted": "住宿成功删除了!",
|
||||
"lodging_edit_success": "住宿成功编辑了!",
|
||||
"lodging_type": "住宿类型",
|
||||
"motel": "汽车旅馆",
|
||||
"start": "开始",
|
||||
"type": "类型",
|
||||
"villa": "别墅",
|
||||
"apartment": "公寓",
|
||||
"bnb": "床和早餐",
|
||||
"cabin": "舱",
|
||||
"new_lodging": "新住宿",
|
||||
"other": "其他",
|
||||
"provide_start_date": "请提供开始日期",
|
||||
"reservation_number": "预订号",
|
||||
"resort": "采取",
|
||||
"current_timezone": "当前时区"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,9 @@
|
|||
register('nl', () => import('../locales/nl.json'));
|
||||
register('sv', () => import('../locales/sv.json'));
|
||||
register('pl', () => import('../locales/pl.json'));
|
||||
register('ko', () => import('../locales/ko.json'));
|
||||
|
||||
let locales = ['en', 'es', 'fr', 'de', 'it', 'zh', 'nl', 'sv', 'pl'];
|
||||
let locales = ['en', 'es', 'fr', 'de', 'it', 'zh', 'nl', 'sv', 'pl', 'ko'];
|
||||
|
||||
if (browser) {
|
||||
init({
|
||||
|
|
|
@ -2,7 +2,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
|||
import { redirect, type Actions } from '@sveltejs/kit';
|
||||
// @ts-ignore
|
||||
import psl from 'psl';
|
||||
import { themes } from '$lib';
|
||||
import { getRandomBackground, themes } from '$lib';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
|
@ -11,6 +11,13 @@ const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
|||
export const load = (async (event) => {
|
||||
if (event.locals.user) {
|
||||
return redirect(302, '/dashboard');
|
||||
} else {
|
||||
const background = getRandomBackground();
|
||||
return {
|
||||
props: {
|
||||
background
|
||||
}
|
||||
};
|
||||
}
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
|
@ -51,8 +58,10 @@ export const actions: Actions = {
|
|||
|
||||
// Check if hostname is an IP address
|
||||
const isIPAddress = /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname);
|
||||
const isLocalhost = hostname === 'localhost';
|
||||
const isSingleLabel = hostname.split('.').length === 1;
|
||||
|
||||
if (!isIPAddress) {
|
||||
if (!isIPAddress && !isSingleLabel && !isLocalhost) {
|
||||
const parsed = psl.parse(hostname);
|
||||
|
||||
if (parsed && parsed.domain) {
|
||||
|
|
|
@ -1,126 +1,117 @@
|
|||
<script lang="ts">
|
||||
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 MapWithPins from '$lib/assets/MapWithPins.webp';
|
||||
import type { Background } from '$lib/types.js';
|
||||
|
||||
export let data;
|
||||
|
||||
let background: Background = data.props?.background ?? { url: '' };
|
||||
</script>
|
||||
|
||||
<section class="flex items-center justify-center w-full py-12 md:py-24 lg:py-32">
|
||||
<div class="container px-4 md:px-6">
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr_550px] lg:gap-12 xl:grid-cols-[1fr_650px]">
|
||||
<div class="flex flex-col justify-center space-y-4">
|
||||
<div class="space-y-2">
|
||||
{#if data.user}
|
||||
{#if data.user.first_name && data.user.first_name !== null}
|
||||
<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"
|
||||
>
|
||||
{data.user.first_name.charAt(0).toUpperCase() + data.user.first_name.slice(1)},
|
||||
{$t('home.hero_1')}
|
||||
</h1>
|
||||
{:else}
|
||||
<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"
|
||||
>
|
||||
{$t('home.hero_1')}
|
||||
</h1>
|
||||
{/if}
|
||||
{:else}
|
||||
<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"
|
||||
>
|
||||
{$t('home.hero_1')}
|
||||
</h1>
|
||||
{/if}
|
||||
<p class="max-w-[600px] text-gray-500 md:text-xl dark:text-gray-400">
|
||||
{$t('home.hero_2')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 min-[400px]:flex-row">
|
||||
{#if data.user}
|
||||
<button on:click={() => goto('/adventures')} class="btn btn-primary">
|
||||
{$t('home.go_to')}
|
||||
</button>
|
||||
{:else}
|
||||
<button on:click={() => goto('/login')} class="btn btn-primary">
|
||||
{$t('auth.login')}
|
||||
</button>
|
||||
<button on:click={() => goto('/signup')} class="btn btn-neutral">
|
||||
{$t('auth.signup')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Hero Section -->
|
||||
<section class="flex items-center justify-center w-full py-20 bg-gray-50 dark:bg-gray-800">
|
||||
<div class="container mx-auto px-4 flex flex-col-reverse md:flex-row items-center gap-8">
|
||||
<!-- Text Content -->
|
||||
<div class="w-full md:w-1/2 space-y-6">
|
||||
{#if data.user}
|
||||
{#if data.user.first_name && data.user.first_name !== null}
|
||||
<h1
|
||||
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)}, {$t(
|
||||
'home.hero_1'
|
||||
)}
|
||||
</h1>
|
||||
{:else}
|
||||
<h1
|
||||
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')}
|
||||
</h1>
|
||||
{/if}
|
||||
{:else}
|
||||
<h1
|
||||
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')}
|
||||
</h1>
|
||||
{/if}
|
||||
<p class="text-xl text-gray-600 dark:text-gray-300 max-w-xl">
|
||||
{$t('home.hero_2')}
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
{#if data.user}
|
||||
<button on:click={() => goto('/adventures')} class="btn btn-primary">
|
||||
{$t('home.go_to')}
|
||||
</button>
|
||||
{:else}
|
||||
<button on:click={() => goto('/login')} class="btn btn-primary">
|
||||
{$t('auth.login')}
|
||||
</button>
|
||||
<button on:click={() => goto('/signup')} class="btn btn-secondary">
|
||||
{$t('auth.signup')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Image -->
|
||||
<div class="w-full md:w-1/2">
|
||||
<img
|
||||
src={AdventureOverlook}
|
||||
width="550"
|
||||
height="550"
|
||||
alt="Hero"
|
||||
class="mx-auto aspect-video overflow-hidden rounded-xl object-cover sm:w-full lg:order-last"
|
||||
src={background.url}
|
||||
alt={background.location}
|
||||
class="rounded-lg shadow-lg object-cover w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
<div class="container px-4 md:px-6">
|
||||
<div class="flex flex-col items-center justify-center space-y-4 text-center">
|
||||
<div class="space-y-2">
|
||||
<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')}
|
||||
</div>
|
||||
<h2
|
||||
class="text-3xl font-bold tracking-tighter sm:text-5xl bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||
>
|
||||
{$t('home.desc_1')}
|
||||
</h2>
|
||||
<p
|
||||
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')}
|
||||
</p>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section id="features" class="py-16 bg-white dark:bg-gray-900">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="text-center mb-12">
|
||||
<div class="inline-block text-neutral-content bg-neutral px-4 py-2 rounded-full">
|
||||
{$t('home.key_features')}
|
||||
</div>
|
||||
<h2
|
||||
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')}
|
||||
</h2>
|
||||
<p class="mt-4 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto text-lg">
|
||||
{$t('home.desc_2')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mx-auto grid max-w-5xl items-center gap-6 py-12 lg:grid-cols-2 lg:gap-12">
|
||||
<!-- svelte-ignore a11y-img-redundant-alt -->
|
||||
<img
|
||||
src={MapWithPins}
|
||||
width="550"
|
||||
height="310"
|
||||
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">
|
||||
<ul class="grid gap-6">
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_1')}</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
{$t('home.feature_1_desc')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
||||
<!-- Image for Features -->
|
||||
<div class="order-1 md:order-2">
|
||||
<img
|
||||
src={MapWithPins}
|
||||
alt="World map with pins"
|
||||
class="rounded-lg shadow-lg object-cover"
|
||||
/>
|
||||
</div>
|
||||
<!-- Feature List -->
|
||||
<div class="order-2 md:order-1">
|
||||
<ul class="space-y-6">
|
||||
<li class="space-y-2">
|
||||
<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')}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_2')}</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
{$t('home.feature_2_desc')}
|
||||
</p>
|
||||
</div>
|
||||
<li class="space-y-2">
|
||||
<h3 class="text-xl font-semibold dark:text-gray-300">{$t('home.feature_2')}</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
{$t('home.feature_2_desc')}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_3')}</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
{$t('home.feature_3_desc')}
|
||||
</p>
|
||||
</div>
|
||||
<li class="space-y-2">
|
||||
<h3 class="text-xl font-semibold dark:text-gray-300">{$t('home.feature_3')}</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
{$t('home.feature_3_desc')}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</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} />
|
||||
<button
|
||||
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>
|
||||
<h3 class="text-center font-bold text-lg mb-4">{$t('adventures.sort')}</h3>
|
||||
|
|
|
@ -185,7 +185,10 @@
|
|||
alt={adventure.name}
|
||||
/>
|
||||
</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}
|
||||
<button
|
||||
on:click={() => goToSlide(i)}
|
||||
|
@ -197,6 +200,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
|
@ -221,6 +225,43 @@
|
|||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
@ -241,6 +282,7 @@
|
|||
>{adventure.is_public ? 'Public' : 'Private'}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if adventure.location}
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
|
|
|
@ -33,6 +33,6 @@
|
|||
<!-- download calendar -->
|
||||
<div class="flex items-center justify-center mt-4">
|
||||
<a href={icsCalendarDataUrl} download="adventures.ics" class="btn btn-primary"
|
||||
>Download Calendar</a
|
||||
>{$t('adventures.download_calendar')}</a
|
||||
>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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 type { PageData } from './$types';
|
||||
import { marked } from 'marked'; // Import the markdown parser
|
||||
|
@ -27,7 +27,8 @@
|
|||
groupNotesByDate,
|
||||
groupTransportationsByDate,
|
||||
groupChecklistsByDate,
|
||||
osmTagToEmoji
|
||||
osmTagToEmoji,
|
||||
groupLodgingByDate
|
||||
} from '$lib';
|
||||
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
|
||||
import ChecklistModal from '$lib/components/ChecklistModal.svelte';
|
||||
|
@ -35,6 +36,8 @@
|
|||
import TransportationModal from '$lib/components/TransportationModal.svelte';
|
||||
import CardCarousel from '$lib/components/CardCarousel.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import LodgingModal from '$lib/components/LodgingModal.svelte';
|
||||
import LodgingCard from '$lib/components/LodgingCard.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
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
|
||||
options = { ...options, events: dates };
|
||||
}
|
||||
|
@ -115,6 +131,7 @@
|
|||
let numAdventures: number = 0;
|
||||
|
||||
let transportations: Transportation[] = [];
|
||||
let lodging: Lodging[] = [];
|
||||
let notes: Note[] = [];
|
||||
let checklists: Checklist[] = [];
|
||||
|
||||
|
@ -174,6 +191,9 @@
|
|||
if (collection.transportations) {
|
||||
transportations = collection.transportations;
|
||||
}
|
||||
if (collection.lodging) {
|
||||
lodging = collection.lodging;
|
||||
}
|
||||
if (collection.notes) {
|
||||
notes = collection.notes;
|
||||
}
|
||||
|
@ -243,6 +263,8 @@
|
|||
|
||||
let adventureToEdit: Adventure | null = null;
|
||||
let transportationToEdit: Transportation | null = null;
|
||||
let isShowingLodgingModal: boolean = false;
|
||||
let lodgingToEdit: Lodging | null = null;
|
||||
let isAdventureModalOpen: boolean = false;
|
||||
let isNoteModalOpen: boolean = false;
|
||||
let noteToEdit: Note | null;
|
||||
|
@ -260,6 +282,11 @@
|
|||
isShowingTransportationModal = true;
|
||||
}
|
||||
|
||||
function editLodging(event: CustomEvent<Lodging>) {
|
||||
lodgingToEdit = event.detail;
|
||||
isShowingLodgingModal = true;
|
||||
}
|
||||
|
||||
function saveOrCreateAdventure(event: CustomEvent<Adventure>) {
|
||||
if (adventures.find((adventure) => adventure.id === event.detail.id)) {
|
||||
adventures = adventures.map((adventure) => {
|
||||
|
@ -355,6 +382,22 @@
|
|||
}
|
||||
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>
|
||||
|
||||
{#if isShowingLinkModal}
|
||||
|
@ -376,6 +419,15 @@
|
|||
/>
|
||||
{/if}
|
||||
|
||||
{#if isShowingLodgingModal}
|
||||
<LodgingModal
|
||||
{lodgingToEdit}
|
||||
on:close={() => (isShowingLodgingModal = false)}
|
||||
on:save={saveOrCreateLodging}
|
||||
{collection}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isAdventureModalOpen}
|
||||
<AdventureModal
|
||||
{adventureToEdit}
|
||||
|
@ -501,6 +553,16 @@
|
|||
>
|
||||
{$t('adventures.checklist')}</button
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click={() => {
|
||||
isShowingLodgingModal = true;
|
||||
newType = '';
|
||||
lodgingToEdit = null;
|
||||
}}
|
||||
>
|
||||
{$t('adventures.lodging')}</button
|
||||
>
|
||||
|
||||
<!-- <button
|
||||
class="btn btn-primary"
|
||||
|
@ -542,7 +604,7 @@
|
|||
</div>
|
||||
{/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} />
|
||||
{/if}
|
||||
|
||||
|
@ -654,6 +716,63 @@
|
|||
</div>
|
||||
{/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}
|
||||
<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">
|
||||
|
@ -695,7 +814,7 @@
|
|||
{/if}
|
||||
|
||||
<!-- 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} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -743,6 +862,10 @@
|
|||
new Date(collection.start_date),
|
||||
numberOfDays
|
||||
)[dateString] || []}
|
||||
{@const dayLodging =
|
||||
groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays)[
|
||||
dateString
|
||||
] || []}
|
||||
{@const dayNotes =
|
||||
groupNotesByDate(notes, new Date(collection.start_date), numberOfDays)[dateString] ||
|
||||
[]}
|
||||
|
@ -804,6 +927,18 @@
|
|||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if dayLodging.length > 0}
|
||||
{#each dayLodging as hotel}
|
||||
<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}
|
||||
{#each dayChecklists as checklist}
|
||||
<ChecklistCard
|
||||
|
@ -821,7 +956,7 @@
|
|||
{/if}
|
||||
</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>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -950,7 +1085,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{/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-body">
|
||||
<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 res = await event.fetch(`${serverEndpoint}/api/stats/counts/`, {
|
||||
headers: {
|
||||
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||
let res = await event.fetch(
|
||||
`${serverEndpoint}/api/stats/counts/${event.locals.user.username}`,
|
||||
{
|
||||
headers: {
|
||||
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
if (!res.ok) {
|
||||
console.error('Failed to fetch user stats');
|
||||
} else {
|
||||
|
|
|
@ -120,8 +120,10 @@ function handleSuccessfulLogin(event: RequestEvent<RouteParams, '/login'>, respo
|
|||
|
||||
// Check if hostname is an IP address
|
||||
const isIPAddress = /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname);
|
||||
const isLocalhost = hostname === 'localhost';
|
||||
const isSingleLabel = hostname.split('.').length === 1;
|
||||
|
||||
if (!isIPAddress) {
|
||||
if (!isIPAddress && !isSingleLabel && !isLocalhost) {
|
||||
const parsed = psl.parse(hostname);
|
||||
|
||||
if (parsed && parsed.domain) {
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
let createModalOpen: boolean = false;
|
||||
let showGeo: boolean = false;
|
||||
|
||||
export let initialLatLng: { lat: number; lng: number } | null = null;
|
||||
|
||||
let visitedRegions: VisitedRegion[] = data.props.visitedRegions;
|
||||
let adventures: Adventure[] = data.props.adventures;
|
||||
|
||||
|
@ -49,6 +51,11 @@
|
|||
newLatitude = e.detail.lngLat.lat;
|
||||
}
|
||||
|
||||
function newAdventure() {
|
||||
initialLatLng = { lat: newLatitude, lng: newLongitude } as { lat: number; lng: number };
|
||||
createModalOpen = true;
|
||||
}
|
||||
|
||||
function createNewAdventure(event: CustomEvent) {
|
||||
adventures = [...adventures, event.detail];
|
||||
newMarker = null;
|
||||
|
@ -86,7 +93,7 @@
|
|||
/>
|
||||
<div class="divider divider-horizontal"></div>
|
||||
{#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
|
||||
>
|
||||
<button type="button" class="btn btn-neutral mb-2" on:click={() => (newMarker = null)}
|
||||
|
@ -105,14 +112,13 @@
|
|||
<AdventureModal
|
||||
on:close={() => (createModalOpen = false)}
|
||||
on:save={createNewAdventure}
|
||||
latitude={newLatitude}
|
||||
longitude={newLongitude}
|
||||
{initialLatLng}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<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"
|
||||
class="mx-auto aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12 rounded-lg"
|
||||
standardControls
|
||||
>
|
||||
{#each filteredAdventures as adventure}
|
||||
|
@ -197,7 +203,7 @@
|
|||
</MapLibre>
|
||||
|
||||
<svelte:head>
|
||||
<title>Travel Map</title>
|
||||
<title>Adventure Map</title>
|
||||
<meta name="description" content="View your travels on a map." />
|
||||
</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 -->
|
||||
<section class="space-y-8">
|
||||
<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>
|
||||
{$t('settings.social_auth_desc')}
|
||||
</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