mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-19 21:09:37 +02:00
Auth Migration, Calendar and Other Misc. Fixes
This commit is contained in:
commit
148568fca4
95 changed files with 3267 additions and 2379 deletions
25
README.md
25
README.md
|
@ -8,7 +8,6 @@
|
|||
- **[Demo](https://demo.adventurelog.app)**
|
||||
- **[Join the AdventureLog Community Discord Server](https://discord.gg/wRbQ9Egr8C)**
|
||||
|
||||
|
||||
# Table of Contents
|
||||
|
||||
- [Installation](#installation)
|
||||
|
@ -59,18 +58,18 @@ Here is a summary of the configuration options available in the `docker-compose.
|
|||
|
||||
### Backend Container (server)
|
||||
|
||||
| Name | Required | Description | Default Value |
|
||||
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
|
||||
| `PGHOST` | Yes | Databse host. | db |
|
||||
| `PGDATABASE` | Yes | Database. | database |
|
||||
| `PGUSER` | Yes | Database user. | adventure |
|
||||
| `PGPASSWORD` | Yes | Database password. | changeme123 |
|
||||
| `DJANGO_ADMIN_USERNAME` | Yes | Default username. | admin |
|
||||
| `DJANGO_ADMIN_PASSWORD` | Yes | Default password, change after inital login. | admin |
|
||||
| `DJANGO_ADMIN_EMAIL` | Yes | Default user's email. | admin@example.com |
|
||||
| `PUBLIC_URL` | Yes | This needs to match the outward port of the server and be accessible from where the app is used. It is used for the creation of image urls. | 'http://localhost:8016' |
|
||||
| `CSRF_TRUSTED_ORIGINS` | Yes | Need to be changed to the orgins where you use your backend server and frontend. These values are comma seperated. | http://localhost:8016 |
|
||||
| `FRONTEND_URL` | Yes | This is the publicly accessible url to the **frontend** container. This link should be accessible for all users. Used for email generation. | 'http://localhost:8015' |
|
||||
| Name | Required | Description | Default Value |
|
||||
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
|
||||
| `PGHOST` | Yes | Databse host. | db |
|
||||
| `PGDATABASE` | Yes | Database. | database |
|
||||
| `PGUSER` | Yes | Database user. | adventure |
|
||||
| `PGPASSWORD` | Yes | Database password. | changeme123 |
|
||||
| `DJANGO_ADMIN_USERNAME` | Yes | Default username. | admin |
|
||||
| `DJANGO_ADMIN_PASSWORD` | Yes | Default password, change after inital login. | admin |
|
||||
| `DJANGO_ADMIN_EMAIL` | Yes | Default user's email. | admin@example.com |
|
||||
| `PUBLIC_URL` | Yes | This needs to match the outward port of the server and be accessible from where the app is used. It is used for the creation of image urls. | http://localhost:8016 |
|
||||
| `CSRF_TRUSTED_ORIGINS` | Yes | Need to be changed to the orgins where you use your backend server and frontend. These values are comma seperated. | http://localhost:8016 |
|
||||
| `FRONTEND_URL` | Yes | This is the publicly accessible url to the **frontend** container. This link should be accessible for all users. Used for email generation. | http://localhost:8015 |
|
||||
|
||||
## Running the Containers
|
||||
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
# Preface
|
||||
|
||||
AdventureLog uses DjRestAuth, a Django REST Framework authentication backend for Django Rest Framework. DjRestAuth is licensed under the MIT License.
|
||||
|
||||
---
|
||||
|
||||
## The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 iMerica https://github.com/iMerica/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1,4 +0,0 @@
|
|||
# AdventureLog Django Backend
|
||||
A demo of a possible AdventureLog 2.0 version using Django as the backend with a REST API.
|
||||
|
||||
Based of django-rest-framework and dj-rest-auth.
|
|
@ -3,6 +3,11 @@ from django.contrib import admin
|
|||
from django.utils.html import mark_safe
|
||||
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category
|
||||
from worldtravel.models import Country, Region, VisitedRegion
|
||||
from allauth.account.decorators import secure_admin_login
|
||||
|
||||
admin.autodiscover()
|
||||
admin.site.login = secure_admin_login(admin.site.login)
|
||||
|
||||
|
||||
|
||||
class AdventureAdmin(admin.ModelAdmin):
|
||||
|
@ -54,9 +59,9 @@ from users.models import CustomUser
|
|||
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
model = CustomUser
|
||||
list_display = ['username', 'email', 'is_staff', 'is_active', 'image_display']
|
||||
list_display = ['username', 'is_staff', 'is_active', 'image_display']
|
||||
readonly_fields = ('uuid',)
|
||||
search_fields = ('username', 'email')
|
||||
search_fields = ('username',)
|
||||
fieldsets = UserAdmin.fieldsets + (
|
||||
(None, {'fields': ('profile_pic', 'uuid', 'public_profile')}),
|
||||
)
|
||||
|
|
|
@ -11,3 +11,13 @@ class AppVersionMiddleware:
|
|||
response['X-AdventureLog-Version'] = '1.0.0'
|
||||
|
||||
return response
|
||||
|
||||
# make a middlewra that prints all of the request cookies
|
||||
class PrintCookiesMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
print(request.COOKIES)
|
||||
response = self.get_response(request)
|
||||
return response
|
|
@ -1,6 +1,6 @@
|
|||
from django.urls import include, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet, AdventureImageViewSet, ReverseGeocodeViewSet, CategoryViewSet
|
||||
from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet, AdventureImageViewSet, ReverseGeocodeViewSet, CategoryViewSet, IcsCalendarGeneratorViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'adventures', AdventureViewSet, basename='adventures')
|
||||
|
@ -14,6 +14,7 @@ router.register(r'checklists', ChecklistViewSet, basename='checklists')
|
|||
router.register(r'images', AdventureImageViewSet, basename='images')
|
||||
router.register(r'reverse-geocode', ReverseGeocodeViewSet, basename='reverse-geocode')
|
||||
router.register(r'categories', CategoryViewSet, basename='categories')
|
||||
router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-calendar')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
@ -17,6 +17,9 @@ from rest_framework.pagination import PageNumberPagination
|
|||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import status
|
||||
from django.contrib.auth import get_user_model
|
||||
from icalendar import Calendar, Event, vText, vCalAddress
|
||||
from django.http import HttpResponse
|
||||
from datetime import datetime
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -73,6 +76,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
return queryset.order_by(ordering)
|
||||
|
||||
def get_queryset(self):
|
||||
print(self.request.user)
|
||||
# if the user is not authenticated return only public adventures for retrieve action
|
||||
if not self.request.user.is_authenticated:
|
||||
if self.action == 'retrieve':
|
||||
|
@ -1202,3 +1206,56 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
|
|||
new_region_count += 1
|
||||
new_regions[region.id] = region.name
|
||||
return Response({"new_regions": new_region_count, "regions": new_regions})
|
||||
|
||||
|
||||
from django.http import HttpResponse
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from icalendar import Calendar, Event, vText, vCalAddress
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class IcsCalendarGeneratorViewSet(viewsets.ViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def generate(self, request):
|
||||
adventures = Adventure.objects.filter(user_id=request.user)
|
||||
serializer = AdventureSerializer(adventures, many=True)
|
||||
user = request.user
|
||||
name = f"{user.first_name} {user.last_name}"
|
||||
print(serializer.data)
|
||||
|
||||
cal = Calendar()
|
||||
cal.add('prodid', '-//My Adventure Calendar//example.com//')
|
||||
cal.add('version', '2.0')
|
||||
|
||||
for adventure in serializer.data:
|
||||
if adventure['visits']:
|
||||
for visit in adventure['visits']:
|
||||
event = Event()
|
||||
event.add('summary', adventure['name'])
|
||||
start_date = datetime.strptime(visit['start_date'], '%Y-%m-%d').date()
|
||||
end_date = datetime.strptime(visit['end_date'], '%Y-%m-%d').date() + timedelta(days=1) if visit['end_date'] else start_date + timedelta(days=1)
|
||||
event.add('dtstart', start_date)
|
||||
event.add('dtend', end_date)
|
||||
event.add('dtstamp', datetime.now())
|
||||
event.add('transp', 'TRANSPARENT')
|
||||
event.add('class', 'PUBLIC')
|
||||
event.add('created', datetime.now())
|
||||
event.add('last-modified', datetime.now())
|
||||
event.add('description', adventure['description'])
|
||||
if adventure.get('location'):
|
||||
event.add('location', adventure['location'])
|
||||
if adventure.get('link'):
|
||||
event.add('url', adventure['link'])
|
||||
|
||||
organizer = vCalAddress(f'MAILTO:{user.email}')
|
||||
organizer.params['cn'] = vText(name)
|
||||
event.add('organizer', organizer)
|
||||
|
||||
cal.add_component(event)
|
||||
|
||||
response = HttpResponse(cal.to_ical(), content_type='text/calendar')
|
||||
response['Content-Disposition'] = 'attachment; filename=adventures.ics'
|
||||
return response
|
||||
|
|
|
@ -11,7 +11,6 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
|
|||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from datetime import timedelta
|
||||
from os import getenv
|
||||
from pathlib import Path
|
||||
# Load environment variables from .env file
|
||||
|
@ -35,8 +34,6 @@ DEBUG = getenv('DEBUG', 'True') == 'True'
|
|||
# ]
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
|
@ -47,19 +44,19 @@ INSTALLED_APPS = (
|
|||
'django.contrib.sites',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'dj_rest_auth',
|
||||
'allauth',
|
||||
'allauth.account',
|
||||
'dj_rest_auth.registration',
|
||||
'allauth.mfa',
|
||||
'allauth.headless',
|
||||
'allauth.socialaccount',
|
||||
'allauth.socialaccount.providers.facebook',
|
||||
# "widget_tweaks",
|
||||
# "slippers",
|
||||
'drf_yasg',
|
||||
'corsheaders',
|
||||
'adventures',
|
||||
'worldtravel',
|
||||
'users',
|
||||
'django.contrib.gis',
|
||||
|
||||
)
|
||||
|
||||
MIDDLEWARE = (
|
||||
|
@ -83,7 +80,6 @@ CACHES = {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
# For backwards compatibility for Django 1.8
|
||||
MIDDLEWARE_CLASSES = MIDDLEWARE
|
||||
|
||||
|
@ -91,11 +87,6 @@ ROOT_URLCONF = 'main.urls'
|
|||
|
||||
# WSGI_APPLICATION = 'demo.wsgi.application'
|
||||
|
||||
SIMPLE_JWT = {
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
|
||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=365),
|
||||
}
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
||||
|
||||
|
@ -114,6 +105,7 @@ DATABASES = {
|
|||
}
|
||||
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
||||
|
||||
|
@ -127,6 +119,8 @@ USE_L10N = True
|
|||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.7/howto/static-files/
|
||||
|
||||
|
@ -139,7 +133,15 @@ MEDIA_URL = '/media/'
|
|||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
STATICFILES_DIRS = [BASE_DIR / 'static']
|
||||
|
||||
# TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')]
|
||||
STORAGES = {
|
||||
"staticfiles": {
|
||||
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
},
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
|
@ -157,32 +159,34 @@ TEMPLATES = [
|
|||
},
|
||||
]
|
||||
|
||||
REST_AUTH = {
|
||||
'SESSION_LOGIN': True,
|
||||
'USE_JWT': True,
|
||||
'JWT_AUTH_COOKIE': 'auth',
|
||||
'JWT_AUTH_HTTPONLY': False,
|
||||
'REGISTER_SERIALIZER': 'users.serializers.RegisterSerializer',
|
||||
'USER_DETAILS_SERIALIZER': 'users.serializers.CustomUserDetailsSerializer',
|
||||
'PASSWORD_RESET_SERIALIZER': 'users.serializers.MyPasswordResetSerializer'
|
||||
}
|
||||
# Authentication settings
|
||||
|
||||
DISABLE_REGISTRATION = getenv('DISABLE_REGISTRATION', 'False') == 'True'
|
||||
DISABLE_REGISTRATION_MESSAGE = getenv('DISABLE_REGISTRATION_MESSAGE', 'Registration is disabled. Please contact the administrator if you need an account.')
|
||||
|
||||
STORAGES = {
|
||||
"staticfiles": {
|
||||
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
},
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
}
|
||||
}
|
||||
ALLAUTH_UI_THEME = "dark"
|
||||
SILENCED_SYSTEM_CHECKS = ["slippers.E001"]
|
||||
|
||||
AUTH_USER_MODEL = 'users.CustomUser'
|
||||
|
||||
ACCOUNT_ADAPTER = 'users.adapters.NoNewUsersAccountAdapter'
|
||||
|
||||
ACCOUNT_SIGNUP_FORM_CLASS = 'users.form_overrides.CustomSignupForm'
|
||||
|
||||
SESSION_SAVE_EVERY_REQUEST = True
|
||||
|
||||
FRONTEND_URL = getenv('FRONTEND_URL', 'http://localhost:3000')
|
||||
|
||||
HEADLESS_FRONTEND_URLS = {
|
||||
"account_confirm_email": f"{FRONTEND_URL}/user/verify-email/{{key}}",
|
||||
"account_reset_password": f"{FRONTEND_URL}/user/reset-password",
|
||||
"account_reset_password_from_key": f"{FRONTEND_URL}/user/reset-password/{{key}}",
|
||||
"account_signup": f"{FRONTEND_URL}/signup",
|
||||
# Fallback in case the state containing the `next` URL is lost and the handshake
|
||||
# with the third-party provider fails.
|
||||
"socialaccount_login_error": f"{FRONTEND_URL}/account/provider/callback",
|
||||
}
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
SITE_ID = 1
|
||||
ACCOUNT_EMAIL_REQUIRED = True
|
||||
|
@ -214,13 +218,8 @@ else:
|
|||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
'dj_rest_auth.jwt_auth.JWTCookieAuthentication'
|
||||
),
|
||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
|
||||
# 'DEFAULT_PERMISSION_CLASSES': [
|
||||
# 'rest_framework.permissions.IsAuthenticated',
|
||||
# ],
|
||||
}
|
||||
|
||||
SWAGGER_SETTINGS = {
|
||||
|
@ -228,12 +227,11 @@ SWAGGER_SETTINGS = {
|
|||
'LOGOUT_URL': 'logout',
|
||||
}
|
||||
|
||||
# For demo purposes only. Use a white list in the real world.
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()]
|
||||
|
||||
from os import getenv
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()]
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
LOGGING = {
|
||||
|
@ -260,6 +258,5 @@ LOGGING = {
|
|||
},
|
||||
},
|
||||
}
|
||||
|
||||
# https://github.com/dr5hn/countries-states-cities-database/tags
|
||||
COUNTRY_REGION_JSON_VERSION = 'v2.4'
|
|
@ -3,8 +3,7 @@ from django.contrib import admin
|
|||
from django.views.generic import RedirectView, TemplateView
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from adventures import urls as adventures
|
||||
from users.views import ChangeEmailView, IsRegistrationDisabled, PublicUserListView, PublicUserDetailView
|
||||
from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView
|
||||
from .views import get_csrf_token
|
||||
from drf_yasg.views import get_schema_view
|
||||
|
||||
|
@ -19,56 +18,27 @@ schema_view = get_schema_view(
|
|||
urlpatterns = [
|
||||
path('api/', include('adventures.urls')),
|
||||
path('api/', include('worldtravel.urls')),
|
||||
path("_allauth/", include("allauth.headless.urls")),
|
||||
|
||||
path('auth/change-email/', ChangeEmailView.as_view(), name='change_email'),
|
||||
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/update-user/', UpdateUserMetadataView.as_view(), name='update-user-metadata'),
|
||||
|
||||
path('auth/user-metadata/', UserMetadataView.as_view(), name='user-metadata'),
|
||||
|
||||
path('csrf/', get_csrf_token, name='get_csrf_token'),
|
||||
re_path(r'^$', TemplateView.as_view(
|
||||
template_name="home.html"), name='home'),
|
||||
re_path(r'^signup/$', TemplateView.as_view(template_name="signup.html"),
|
||||
name='signup'),
|
||||
re_path(r'^email-verification/$',
|
||||
TemplateView.as_view(template_name="email_verification.html"),
|
||||
name='email-verification'),
|
||||
re_path(r'^login/$', TemplateView.as_view(template_name="login.html"),
|
||||
name='login'),
|
||||
re_path(r'^logout/$', TemplateView.as_view(template_name="logout.html"),
|
||||
name='logout'),
|
||||
re_path(r'^password-reset/$',
|
||||
TemplateView.as_view(template_name="password_reset.html"),
|
||||
name='password-reset'),
|
||||
re_path(r'^password-reset/confirm/$',
|
||||
TemplateView.as_view(template_name="password_reset_confirm.html"),
|
||||
name='password-reset-confirm'),
|
||||
|
||||
re_path(r'^user-details/$',
|
||||
TemplateView.as_view(template_name="user_details.html"),
|
||||
name='user-details'),
|
||||
re_path(r'^password-change/$',
|
||||
TemplateView.as_view(template_name="password_change.html"),
|
||||
name='password-change'),
|
||||
re_path(r'^resend-email-verification/$',
|
||||
TemplateView.as_view(
|
||||
template_name="resend_email_verification.html"),
|
||||
name='resend-email-verification'),
|
||||
path('', TemplateView.as_view(template_name='home.html')),
|
||||
|
||||
|
||||
# this url is used to generate email content
|
||||
re_path(r'^password-reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,32})/$',
|
||||
TemplateView.as_view(template_name="password_reset_confirm.html"),
|
||||
name='password_reset_confirm'),
|
||||
|
||||
re_path(r'^auth/', include('dj_rest_auth.urls')),
|
||||
re_path(r'^auth/registration/',
|
||||
include('dj_rest_auth.registration.urls')),
|
||||
re_path(r'^account/', include('allauth.urls')),
|
||||
re_path(r'^admin/', admin.site.urls),
|
||||
re_path(r'^accounts/profile/$', RedirectView.as_view(url='/',
|
||||
permanent=True), name='profile-redirect'),
|
||||
re_path(r'^docs/$', schema_view.with_ui('swagger',
|
||||
cache_timeout=0), name='api_docs'),
|
||||
# path('auth/account-confirm-email/', VerifyEmailView.as_view(), name='account_email_verification_sent'),
|
||||
path("accounts/", include("allauth.urls")),
|
||||
|
||||
# Include the API endpoints:
|
||||
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
Django==5.0.8
|
||||
dj-rest-auth @ git+https://github.com/iMerica/dj-rest-auth.git@master
|
||||
djangorestframework>=3.15.2
|
||||
djangorestframework-simplejwt==5.3.1
|
||||
django-allauth==0.63.3
|
||||
drf-yasg==1.21.4
|
||||
django-cors-headers==4.4.0
|
||||
|
@ -14,3 +12,9 @@ django-resized
|
|||
django-geojson
|
||||
setuptools
|
||||
gunicorn==23.0.0
|
||||
qrcode==8.0
|
||||
# slippers==0.6.2
|
||||
# django-allauth-ui==1.5.1
|
||||
# django-widget-tweaks==1.5.0
|
||||
django-ical==1.9.2
|
||||
icalendar==6.1.0
|
|
@ -1,13 +0,0 @@
|
|||
{% extends "account/email/base_message.txt" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}{% autoescape off %}{% blocktrans %}You're receiving this email because you or someone else has requested a password reset for your user account.
|
||||
|
||||
It can be safely ignored if you did not request a password reset. Click the link below to reset your password.{% endblocktrans %}
|
||||
|
||||
{{ frontend_url }}/settings/forgot-password/confirm?token={{ temp_key }}&uid={{ user_pk }}
|
||||
|
||||
{% if username %}
|
||||
|
||||
|
||||
{% blocktrans %}In case you forgot, your username is {{ username }}.{% endblocktrans %}{% endif %}{% endautoescape %}{% endblock content %}
|
|
@ -4,8 +4,8 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Django-dj-rest-auth demo" />
|
||||
<meta name="author" content="iMerica, Inc." />
|
||||
<meta name="description" content="AdventureLog Server" />
|
||||
<meta name="author" content="Sean Morley" />
|
||||
|
||||
<title>AdventureLog API Server</title>
|
||||
|
||||
|
@ -31,39 +31,6 @@
|
|||
<body role="document">
|
||||
<div class="navbar navbar-inverse" role="navigation">
|
||||
<div class="container">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown"
|
||||
>API endpoints <span class="caret"></span
|
||||
></a>
|
||||
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<!-- these pages don't require user token -->
|
||||
<li><a href="{% url 'signup' %}">Signup</a></li>
|
||||
<li>
|
||||
<a href="{% url 'email-verification' %}">E-mail verification</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'resend-email-verification' %}"
|
||||
>Resend E-mail verification</a
|
||||
>
|
||||
</li>
|
||||
<li><a href="{% url 'login' %}">Login</a></li>
|
||||
<li><a href="{% url 'password-reset' %}">Password Reset</a></li>
|
||||
<li>
|
||||
<a href="{% url 'password-reset-confirm' %}"
|
||||
>Password Reset Confirm</a
|
||||
>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<!-- these pages require user token -->
|
||||
<li><a href="{% url 'user-details' %}">User details</a></li>
|
||||
<li><a href="{% url 'logout' %}">Logout</a></li>
|
||||
<li><a href="{% url 'password-change' %}">Password change</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="navbar-header">
|
||||
<button
|
||||
type="button"
|
||||
|
@ -80,20 +47,19 @@
|
|||
</div>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="active"><a href="/">Demo</a></li>
|
||||
<li class="active"><a href="/">Server Home</a></li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
href="http://dj-rest-auth.readthedocs.org/en/latest/"
|
||||
<a target="_blank" href="http://adventurelog.app"
|
||||
>Documentation</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" href="https://github.com/iMerica/dj-rest-auth"
|
||||
>Source code</a
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/seanmorley15/AdventureLog"
|
||||
>Source Code</a
|
||||
>
|
||||
</li>
|
||||
<li><a target="_blank" href="{% url 'api_docs' %}">API Docs</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!--/.nav-collapse -->
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h3>E-mail verification</h3><hr/>
|
||||
{% include "fragments/email_verification_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -4,7 +4,12 @@
|
|||
<h1>AdventureLog API Server</h1>
|
||||
<p>
|
||||
<a class="btn btn-primary btn-lg" href="/admin" role="button">Admin Site</a>
|
||||
<a class="btn btn-secondary btn-lg" href="/docs" role="button">API Docs</a>
|
||||
<a
|
||||
class="btn btn-secondary btn-lg"
|
||||
href="/accounts/password/change"
|
||||
role="button"
|
||||
>Account Managment</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h3>Login</h3><hr/>
|
||||
{% include "fragments/login_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,8 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h3>Logout</h3><hr/>
|
||||
{% include "fragments/logout_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,39 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<label for="token" class="col-sm-2 control-label">User Token</label>
|
||||
<div class="col-sm-4">
|
||||
<input name="token" type="text" class="form-control" id="token" placeholder="Token">
|
||||
<p class="help-block">Token received after login</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<h3>Update User Details</h3><hr/>
|
||||
{% include "fragments/password_change_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script type="text/javascript">
|
||||
$().ready(function(){
|
||||
$('form button[type=submit]').click(function(){
|
||||
var token = $('input[name=token]').val();
|
||||
var form = $('form');
|
||||
$.ajax({
|
||||
url: form.attr('action'),
|
||||
data: $('form').serialize(),
|
||||
type: "POST",
|
||||
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', 'Token '+token);}
|
||||
}).fail(function(data){error_response(data);})
|
||||
.done(function(data){susccess_response(data);});
|
||||
return false;
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,8 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h3>Password reset</h3><hr/>
|
||||
{% include "fragments/password_reset_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,26 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h3>Password reset confirmation</h3><hr/>
|
||||
{% include "fragments/password_reset_confirm_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block script %}
|
||||
<script type="text/javascript">
|
||||
var url_elements = window.location.pathname.split('/');
|
||||
if (url_elements.length == 6){
|
||||
var uid = url_elements[url_elements.length - 3];
|
||||
if (uid !== undefined){
|
||||
$('input[name=uid]').val(uid);
|
||||
}
|
||||
var token = url_elements[url_elements.length - 2];
|
||||
if (token !== undefined){
|
||||
$('input[name=token]').val(token);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,8 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h3>Resend E-mail verification</h3><hr/>
|
||||
{% include "fragments/resend_email_verification_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,8 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h3>Signup</h3><hr/>
|
||||
{% include "fragments/signup_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,58 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<h3>Retrieve User Details</h3><hr/>
|
||||
<div class="form-group">
|
||||
<label for="token" class="col-sm-2 control-label">User Token</label>
|
||||
<div class="col-sm-4">
|
||||
<input name="token" type="text" class="form-control" id="token" placeholder="Token">
|
||||
<p class="help-block">Token received after login</p>
|
||||
</div>
|
||||
<button id="get-user-details" class="btn btn-primary">GET user details</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<h3>Update User Details</h3><hr/>
|
||||
{% include "fragments/user_details_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script type="text/javascript">
|
||||
$().ready(function(){
|
||||
$('#get-user-details').click(function(){
|
||||
var token = $('input[name=token]').val();
|
||||
|
||||
$.ajax({
|
||||
url: "{% url 'rest_user_details' %}",
|
||||
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', 'Token '+token);},
|
||||
type: "GET",
|
||||
success: function(data) {
|
||||
$('input[name=username]').val(data.username);
|
||||
$('input[name=email]').val(data.email);
|
||||
$('input[name=first_name]').val(data.first_name);
|
||||
$('input[name=last_name]').val(data.last_name);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
$('form button[type=submit]').click(function(){
|
||||
var token = $('input[name=token]').val();
|
||||
var form = $('form');
|
||||
$.ajax({
|
||||
url: form.attr('action'),
|
||||
data: $('form').serialize(),
|
||||
type: "PUT",
|
||||
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', 'Token '+token);}
|
||||
}).fail(function(data){error_response(data);})
|
||||
.done(function(data){susccess_response(data);});
|
||||
return false;
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
10
backend/server/users/adapters.py
Normal file
10
backend/server/users/adapters.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from allauth.account.adapter import DefaultAccountAdapter
|
||||
from django.conf import settings
|
||||
|
||||
class NoNewUsersAccountAdapter(DefaultAccountAdapter):
|
||||
"""
|
||||
Disable new user registration.
|
||||
"""
|
||||
def is_open_for_signup(self, request):
|
||||
is_disabled = getattr(settings, 'DISABLE_REGISTRATION', False)
|
||||
return not is_disabled
|
|
@ -1,3 +1,13 @@
|
|||
from django.contrib import admin
|
||||
from allauth.account.decorators import secure_admin_login
|
||||
from django.contrib.sessions.models import Session
|
||||
|
||||
# Register your models here
|
||||
admin.autodiscover()
|
||||
admin.site.login = secure_admin_login(admin.site.login)
|
||||
|
||||
class SessionAdmin(admin.ModelAdmin):
|
||||
def _session_data(self, obj):
|
||||
return obj.get_decoded()
|
||||
list_display = ['session_key', '_session_data', 'expire_date']
|
||||
|
||||
admin.site.register(Session, SessionAdmin)
|
17
backend/server/users/form_overrides.py
Normal file
17
backend/server/users/form_overrides.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from django import forms
|
||||
|
||||
class CustomSignupForm(forms.Form):
|
||||
first_name = forms.CharField(max_length=30, required=True)
|
||||
last_name = forms.CharField(max_length=30, required=True)
|
||||
|
||||
def signup(self, request, user):
|
||||
# Delay the import to avoid circular import
|
||||
from allauth.account.forms import SignupForm
|
||||
|
||||
# No need to call super() from CustomSignupForm; use the SignupForm directly if needed
|
||||
user.first_name = self.cleaned_data['first_name']
|
||||
user.last_name = self.cleaned_data['last_name']
|
||||
|
||||
# Save the user instance
|
||||
user.save()
|
||||
return user
|
|
@ -1,55 +1,17 @@
|
|||
from allauth.account.utils import (filter_users_by_email, user_pk_to_url_str, user_username)
|
||||
from allauth.utils import build_absolute_uri
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.forms import default_token_generator
|
||||
from allauth.account import app_settings
|
||||
from django.conf import settings
|
||||
from django import forms
|
||||
|
||||
from allauth.account.forms import ResetPasswordForm as AllAuthPasswordResetForm
|
||||
class CustomSignupForm(forms.Form):
|
||||
first_name = forms.CharField(max_length=30, required=True)
|
||||
last_name = forms.CharField(max_length=30, required=True)
|
||||
|
||||
class CustomAllAuthPasswordResetForm(AllAuthPasswordResetForm):
|
||||
|
||||
def clean_email(self):
|
||||
"""
|
||||
Invalid email should not raise error, as this would leak users
|
||||
for unit test: test_password_reset_with_invalid_email
|
||||
"""
|
||||
email = self.cleaned_data["email"]
|
||||
email = get_adapter().clean_email(email)
|
||||
self.users = filter_users_by_email(email, is_active=True)
|
||||
return self.cleaned_data["email"]
|
||||
|
||||
def save(self, request, **kwargs):
|
||||
email = self.cleaned_data['email']
|
||||
token_generator = kwargs.get('token_generator', default_token_generator)
|
||||
|
||||
for user in self.users:
|
||||
temp_key = token_generator.make_token(user)
|
||||
|
||||
path = f"custom_password_reset_url/{user_pk_to_url_str(user)}/{temp_key}/"
|
||||
url = build_absolute_uri(request, path)
|
||||
|
||||
frontend_url = settings.FRONTEND_URL
|
||||
# remove ' from frontend_url
|
||||
frontend_url = frontend_url.replace("'", "")
|
||||
|
||||
#Values which are passed to password_reset_key_message.txt
|
||||
context = {
|
||||
"frontend_url": frontend_url,
|
||||
"user": user,
|
||||
"password_reset_url": url,
|
||||
"request": request,
|
||||
"path": path,
|
||||
"temp_key": temp_key,
|
||||
'user_pk': user_pk_to_url_str(user),
|
||||
}
|
||||
|
||||
if app_settings.AUTHENTICATION_METHOD != app_settings.AuthenticationMethod.EMAIL:
|
||||
context['username'] = user_username(user)
|
||||
get_adapter(request).send_mail(
|
||||
'account/email/password_reset_key', email, context
|
||||
)
|
||||
|
||||
return self.cleaned_data['email']
|
||||
def signup(self, request, user):
|
||||
# Delay the import to avoid circular import
|
||||
from allauth.account.forms import SignupForm
|
||||
|
||||
# No need to call super() from CustomSignupForm; use the SignupForm directly if needed
|
||||
user.first_name = self.cleaned_data['first_name']
|
||||
user.last_name = self.cleaned_data['last_name']
|
||||
|
||||
# Save the user instance
|
||||
user.save()
|
||||
return user
|
|
@ -1,26 +1,14 @@
|
|||
from rest_framework import serializers
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from adventures.models import Adventure, Collection
|
||||
from users.forms import CustomAllAuthPasswordResetForm
|
||||
from dj_rest_auth.serializers import PasswordResetSerializer
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from adventures.models import Collection
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
try:
|
||||
from allauth.account import app_settings as allauth_account_settings
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.utils import setup_user_email
|
||||
from allauth.socialaccount.models import EmailAddress
|
||||
from allauth.utils import get_username_max_length
|
||||
except ImportError:
|
||||
raise ImportError('allauth needs to be added to INSTALLED_APPS.')
|
||||
|
||||
class ChangeEmailSerializer(serializers.Serializer):
|
||||
new_email = serializers.EmailField(required=True)
|
||||
|
@ -32,104 +20,20 @@ class ChangeEmailSerializer(serializers.Serializer):
|
|||
return value
|
||||
|
||||
|
||||
class RegisterSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(
|
||||
max_length=get_username_max_length(),
|
||||
min_length=allauth_account_settings.USERNAME_MIN_LENGTH,
|
||||
required=allauth_account_settings.USERNAME_REQUIRED,
|
||||
)
|
||||
email = serializers.EmailField(required=allauth_account_settings.EMAIL_REQUIRED)
|
||||
password1 = serializers.CharField(write_only=True)
|
||||
password2 = serializers.CharField(write_only=True)
|
||||
first_name = serializers.CharField(required=False)
|
||||
last_name = serializers.CharField(required=False)
|
||||
|
||||
def validate_username(self, username):
|
||||
username = get_adapter().clean_username(username)
|
||||
return username
|
||||
|
||||
def validate_email(self, email):
|
||||
email = get_adapter().clean_email(email)
|
||||
if allauth_account_settings.UNIQUE_EMAIL:
|
||||
if email and EmailAddress.objects.is_verified(email):
|
||||
raise serializers.ValidationError(
|
||||
_('A user is already registered with this e-mail address.'),
|
||||
)
|
||||
return email
|
||||
|
||||
def validate_password1(self, password):
|
||||
return get_adapter().clean_password(password)
|
||||
|
||||
def validate(self, data):
|
||||
if data['password1'] != data['password2']:
|
||||
raise serializers.ValidationError(_("The two password fields didn't match."))
|
||||
|
||||
# check if a user with the same email already exists
|
||||
if User.objects.filter(email=data['email']).exists():
|
||||
raise serializers.ValidationError("This email is already in use.")
|
||||
|
||||
return data
|
||||
|
||||
def custom_signup(self, request, user):
|
||||
pass
|
||||
|
||||
def get_cleaned_data(self):
|
||||
return {
|
||||
'username': self.validated_data.get('username', ''),
|
||||
'password1': self.validated_data.get('password1', ''),
|
||||
'email': self.validated_data.get('email', ''),
|
||||
'first_name': self.validated_data.get('first_name', ''),
|
||||
'last_name': self.validated_data.get('last_name', ''),
|
||||
}
|
||||
|
||||
def save(self, request):
|
||||
# Check if registration is disabled
|
||||
if getattr(settings, 'DISABLE_REGISTRATION', False):
|
||||
raise PermissionDenied("Registration is currently disabled.")
|
||||
|
||||
# If registration is not disabled, proceed with the original logic
|
||||
adapter = get_adapter()
|
||||
user = adapter.new_user(request)
|
||||
self.cleaned_data = self.get_cleaned_data()
|
||||
user = adapter.save_user(request, user, self, commit=False)
|
||||
if "password1" in self.cleaned_data:
|
||||
try:
|
||||
adapter.clean_password(self.cleaned_data['password1'], user=user)
|
||||
except DjangoValidationError as exc:
|
||||
raise serializers.ValidationError(
|
||||
detail=serializers.as_serializer_error(exc)
|
||||
)
|
||||
user.save()
|
||||
self.custom_signup(request, user)
|
||||
setup_user_email(request, user, [])
|
||||
return user
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
UserModel = get_user_model()
|
||||
from dj_rest_auth.serializers import UserDetailsSerializer
|
||||
# from dj_rest_auth.serializers import UserDetailsSerializer
|
||||
from .models import CustomUser
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.conf import settings
|
||||
import os
|
||||
|
||||
# class AdventureSerializer(serializers.ModelSerializer):
|
||||
# image = serializers.SerializerMethodField()
|
||||
|
||||
# class Meta:
|
||||
# model = Adventure
|
||||
# fields = ['id', 'user_id', 'type', 'name', 'location', 'activity_types', 'description',
|
||||
# 'rating', 'link', 'image', 'date', 'trip_id', 'is_public', 'longitude', 'latitude']
|
||||
|
||||
# def get_image(self, obj):
|
||||
# if obj.image:
|
||||
# public_url = os.environ.get('PUBLIC_URL', '')
|
||||
# return f'{public_url}/media/{obj.image.name}'
|
||||
# return None
|
||||
|
||||
class UserDetailsSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
User model w/o password
|
||||
|
@ -163,9 +67,9 @@ class UserDetailsSerializer(serializers.ModelSerializer):
|
|||
if hasattr(UserModel, 'public_profile'):
|
||||
extra_fields.append('public_profile')
|
||||
|
||||
class Meta(UserDetailsSerializer.Meta):
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = UserDetailsSerializer.Meta.fields + ('profile_pic', 'uuid', 'public_profile')
|
||||
fields = ('profile_pic', 'uuid', 'public_profile', 'email', 'date_joined', 'is_staff', 'is_superuser', 'is_active', 'pk')
|
||||
|
||||
model = UserModel
|
||||
fields = ('pk', *extra_fields)
|
||||
|
@ -192,6 +96,7 @@ class CustomUserDetailsSerializer(UserDetailsSerializer):
|
|||
class Meta(UserDetailsSerializer.Meta):
|
||||
model = CustomUser
|
||||
fields = UserDetailsSerializer.Meta.fields + ('profile_pic', 'uuid', 'public_profile')
|
||||
read_only_fields = UserDetailsSerializer.Meta.read_only_fields + ('uuid',)
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
|
@ -203,13 +108,3 @@ class CustomUserDetailsSerializer(UserDetailsSerializer):
|
|||
representation['profile_pic'] = f"{public_url}/media/{instance.profile_pic.name}"
|
||||
del representation['pk'] # remove the pk field from the response
|
||||
return representation
|
||||
|
||||
class MyPasswordResetSerializer(PasswordResetSerializer):
|
||||
|
||||
def validate_email(self, value):
|
||||
# use the custom reset form
|
||||
self.reset_form = CustomAllAuthPasswordResetForm(data=self.initial_data)
|
||||
if not self.reset_form.is_valid():
|
||||
raise serializers.ValidationError(self.reset_form.errors)
|
||||
|
||||
return value
|
|
@ -83,3 +83,41 @@ class PublicUserDetailView(APIView):
|
|||
user.email = None
|
||||
serializer = PublicUserSerializer(user)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
class UserMetadataView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
responses={
|
||||
200: openapi.Response('User metadata'),
|
||||
400: 'Bad Request'
|
||||
},
|
||||
operation_description="Get user metadata."
|
||||
)
|
||||
def get(self, request):
|
||||
user = request.user
|
||||
serializer = PublicUserSerializer(user)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
class UpdateUserMetadataView(APIView):
|
||||
"""
|
||||
Update user metadata using fields from the PublicUserSerializer.
|
||||
Using patch opposed to put allows for partial updates, covers the case where it checks the username and says it's already taken. Duplicate uesrname values should not be included in the request to avoid this.
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
request_body=PublicUserSerializer,
|
||||
responses={
|
||||
200: openapi.Response('User metadata updated'),
|
||||
400: 'Bad Request'
|
||||
},
|
||||
operation_description="Update user metadata."
|
||||
)
|
||||
def patch(self, request):
|
||||
user = request.user
|
||||
serializer = PublicUserSerializer(user, data=request.data, partial=True, context={'request': request})
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
@ -1,3 +1,5 @@
|
|||
from django.contrib import admin
|
||||
from allauth.account.decorators import secure_admin_login
|
||||
|
||||
# Register your models here.
|
||||
admin.autodiscover()
|
||||
admin.site.login = secure_admin_login(admin.site.login)
|
|
@ -38,10 +38,10 @@ services:
|
|||
- DJANGO_ADMIN_USERNAME=admin
|
||||
- DJANGO_ADMIN_PASSWORD=admin
|
||||
- DJANGO_ADMIN_EMAIL=admin@example.com
|
||||
- PUBLIC_URL='http://localhost:8016' # Match the outward port, used for the creation of image urls
|
||||
- CSRF_TRUSTED_ORIGINS=http://localhost:8016 # Comma separated list of trusted origins for CSRF
|
||||
- PUBLIC_URL=http://localhost:8016 # Match the outward port, used for the creation of image urls
|
||||
- CSRF_TRUSTED_ORIGINS=http://localhost:8016,http://localhost:8015 # Comma separated list of trusted origins for CSRF
|
||||
- DEBUG=False
|
||||
- FRONTEND_URL='http://localhost:8015' # Used for email generation. This should be the url of the frontend
|
||||
- FRONTEND_URL=http://localhost:8015 # Used for email generation. This should be the url of the frontend
|
||||
ports:
|
||||
- "8016:80"
|
||||
depends_on:
|
||||
|
|
|
@ -31,18 +31,18 @@ Here is a summary of the configuration options available in the `docker-compose.
|
|||
|
||||
### Backend Container (server)
|
||||
|
||||
| Name | Required | Description | Default Value |
|
||||
| ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
|
||||
| `PGHOST` | Yes | Databse host. | db |
|
||||
| `PGDATABASE` | Yes | Database. | database |
|
||||
| `PGUSER` | Yes | Database user. | adventure |
|
||||
| `PGPASSWORD` | Yes | Database password. | changeme123 |
|
||||
| `DJANGO_ADMIN_USERNAME` | Yes | Default username. | admin |
|
||||
| `DJANGO_ADMIN_PASSWORD` | Yes | Default password, change after inital login. | admin |
|
||||
| `DJANGO_ADMIN_EMAIL` | Yes | Default user's email. | admin@example.com |
|
||||
| `PUBLIC_URL` | Yes | This needs to match the outward port of the server and be accessible from where the app is used. It is used for the creation of image urls. | 'http://localhost:8016' |
|
||||
| `CSRF_TRUSTED_ORIGINS` | Yes | Need to be changed to the orgins where you use your backend server and frontend. These values are comma seperated. | http://localhost:8016 |
|
||||
| `FRONTEND_URL` | Yes | This is the publically accessible url to the **frontend** container. This link should be accessable for all users. Used for email generation. | 'http://localhost:8015' |
|
||||
| Name | Required | Description | Default Value |
|
||||
| ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
|
||||
| `PGHOST` | Yes | Databse host. | db |
|
||||
| `PGDATABASE` | Yes | Database. | database |
|
||||
| `PGUSER` | Yes | Database user. | adventure |
|
||||
| `PGPASSWORD` | Yes | Database password. | changeme123 |
|
||||
| `DJANGO_ADMIN_USERNAME` | Yes | Default username. | admin |
|
||||
| `DJANGO_ADMIN_PASSWORD` | Yes | Default password, change after inital login. | admin |
|
||||
| `DJANGO_ADMIN_EMAIL` | Yes | Default user's email. | admin@example.com |
|
||||
| `PUBLIC_URL` | Yes | This needs to match the outward port of the server and be accessible from where the app is used. It is used for the creation of image urls. | http://localhost:8016 |
|
||||
| `CSRF_TRUSTED_ORIGINS` | Yes | Need to be changed to the orgins where you use your backend server and frontend. These values are comma seperated. | http://localhost:8016 |
|
||||
| `FRONTEND_URL` | Yes | This is the publically accessible url to the **frontend** container. This link should be accessable for all users. Used for email generation. | http://localhost:8015 |
|
||||
|
||||
## Running the Containers
|
||||
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
|
@ -12,14 +12,18 @@
|
|||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@event-calendar/core": "^3.7.1",
|
||||
"@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.5.17",
|
||||
"@sveltejs/kit": "^2.8.3",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"daisyui": "^4.12.6",
|
||||
"postcss": "^8.4.38",
|
||||
|
@ -36,6 +40,8 @@
|
|||
"type": "module",
|
||||
"dependencies": {
|
||||
"@lukulent/svelte-umami": "^0.0.3",
|
||||
"emoji-picker-element": "^1.26.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"svelte-i18n": "^4.0.1",
|
||||
"svelte-maplibre": "^0.9.8"
|
||||
}
|
||||
|
|
257
frontend/pnpm-lock.yaml
generated
257
frontend/pnpm-lock.yaml
generated
|
@ -11,6 +11,12 @@ importers:
|
|||
'@lukulent/svelte-umami':
|
||||
specifier: ^0.0.3
|
||||
version: 0.0.3(svelte@4.2.19)
|
||||
emoji-picker-element:
|
||||
specifier: ^1.26.0
|
||||
version: 1.26.0
|
||||
qrcode:
|
||||
specifier: ^1.5.4
|
||||
version: 1.5.4
|
||||
svelte-i18n:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1(svelte@4.2.19)
|
||||
|
@ -18,21 +24,30 @@ importers:
|
|||
specifier: ^0.9.8
|
||||
version: 0.9.8(svelte@4.2.19)
|
||||
devDependencies:
|
||||
'@event-calendar/core':
|
||||
specifier: ^3.7.1
|
||||
version: 3.7.1
|
||||
'@event-calendar/day-grid':
|
||||
specifier: ^3.7.1
|
||||
version: 3.7.1
|
||||
'@event-calendar/time-grid':
|
||||
specifier: ^3.7.1
|
||||
version: 3.7.1
|
||||
'@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.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))
|
||||
version: 3.2.2(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))
|
||||
'@sveltejs/adapter-node':
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))
|
||||
version: 5.2.0(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))
|
||||
'@sveltejs/adapter-vercel':
|
||||
specifier: ^5.4.1
|
||||
version: 5.4.1(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))
|
||||
version: 5.4.1(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))
|
||||
'@sveltejs/kit':
|
||||
specifier: ^2.5.17
|
||||
version: 2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
|
||||
specifier: ^2.8.3
|
||||
version: 2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
|
||||
'@sveltejs/vite-plugin-svelte':
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
|
||||
|
@ -42,6 +57,9 @@ importers:
|
|||
'@types/node':
|
||||
specifier: ^22.5.4
|
||||
version: 22.5.4
|
||||
'@types/qrcode':
|
||||
specifier: ^1.5.5
|
||||
version: 1.5.5
|
||||
autoprefixer:
|
||||
specifier: ^10.4.19
|
||||
version: 10.4.19(postcss@8.4.38)
|
||||
|
@ -374,6 +392,15 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@event-calendar/core@3.7.1':
|
||||
resolution: {integrity: sha512-S5D4arG7b47uhXmcT/rC7FT3UO9+KB+QhDuhfOzDgKCpAFlEBU1wt1UoHmPTbGy3J+yVMR+rmcresYUvM44+pA==}
|
||||
|
||||
'@event-calendar/day-grid@3.7.1':
|
||||
resolution: {integrity: sha512-kwmadkhUxtQDv+0azMkePrmilFp5dljWLHsluHl1uepfJa1yXlrvFy3GMFnYuPo2Gva0MV+HnU/GMqVG8vIcWw==}
|
||||
|
||||
'@event-calendar/time-grid@3.7.1':
|
||||
resolution: {integrity: sha512-kPC4+XhFcSoNSnYG0TSQeGylpvrbFF1g+cTcFFIW6qH3wPIeBBCo0fRuD4Tr5/q4ewZQ5lNrCkZXOpZxHJxOfw==}
|
||||
|
||||
'@formatjs/ecma402-abstract@2.2.1':
|
||||
resolution: {integrity: sha512-O4ywpkdJybrjFc9zyL8qK5aklleIAi5O4nYhBVJaOFtCkNrnU+lKFeJOFC48zpsZQmR8Aok2V79hGpHnzbmFpg==}
|
||||
|
||||
|
@ -615,12 +642,12 @@ packages:
|
|||
peerDependencies:
|
||||
'@sveltejs/kit': ^2.4.0
|
||||
|
||||
'@sveltejs/kit@2.5.17':
|
||||
resolution: {integrity: sha512-wiADwq7VreR3ctOyxilAZOfPz3Jiy2IIp2C8gfafhTdQaVuGIHllfqQm8dXZKADymKr3uShxzgLZFT+a+CM4kA==}
|
||||
'@sveltejs/kit@2.8.3':
|
||||
resolution: {integrity: sha512-DVBVwugfzzn0SxKA+eAmKqcZ7aHZROCHxH7/pyrOi+HLtQ721eEsctGb9MkhEuqj6q/9S/OFYdn37vdxzFPdvw==}
|
||||
engines: {node: '>=18.13'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@sveltejs/vite-plugin-svelte': ^3.0.0
|
||||
'@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1
|
||||
svelte: ^4.0.0 || ^5.0.0-next.0
|
||||
vite: ^5.0.3
|
||||
|
||||
|
@ -677,6 +704,9 @@ packages:
|
|||
'@types/pug@2.0.10':
|
||||
resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==}
|
||||
|
||||
'@types/qrcode@1.5.5':
|
||||
resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
|
||||
|
||||
'@types/resolve@1.20.2':
|
||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||
|
||||
|
@ -813,8 +843,12 @@ packages:
|
|||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
caniuse-lite@1.0.30001636:
|
||||
resolution: {integrity: sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==}
|
||||
camelcase@5.3.1:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
caniuse-lite@1.0.30001688:
|
||||
resolution: {integrity: sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==}
|
||||
|
||||
chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
|
@ -828,6 +862,9 @@ packages:
|
|||
resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
cliui@6.0.0:
|
||||
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||
|
||||
code-red@1.0.4:
|
||||
resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
|
||||
|
||||
|
@ -907,6 +944,10 @@ packages:
|
|||
supports-color:
|
||||
optional: true
|
||||
|
||||
decamelize@1.2.0:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
deepmerge@4.3.1:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -926,12 +967,15 @@ packages:
|
|||
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
devalue@5.0.0:
|
||||
resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==}
|
||||
devalue@5.1.1:
|
||||
resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==}
|
||||
|
||||
didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
|
||||
dijkstrajs@1.0.3:
|
||||
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||
|
||||
dlv@1.1.3:
|
||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||
|
||||
|
@ -944,6 +988,9 @@ packages:
|
|||
electron-to-chromium@1.4.810:
|
||||
resolution: {integrity: sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==}
|
||||
|
||||
emoji-picker-element@1.26.0:
|
||||
resolution: {integrity: sha512-IcffFc+LNymYScmMuxOJooZulOCOACGc1Xvj+s7XeKqpc+0EoZfWrV9o4rBjEiuM7XjsgcEjD+m5DHg0aIfnnA==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
|
@ -1032,6 +1079,10 @@ packages:
|
|||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
find-up@4.1.0:
|
||||
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
find-up@5.0.0:
|
||||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -1066,6 +1117,10 @@ packages:
|
|||
geojson-vt@4.0.2:
|
||||
resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==}
|
||||
|
||||
get-caller-file@2.0.5:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
|
||||
get-stream@6.0.1:
|
||||
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -1261,6 +1316,10 @@ packages:
|
|||
locate-character@3.0.0:
|
||||
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
||||
|
||||
locate-path@5.0.0:
|
||||
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
locate-path@6.0.0:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -1433,14 +1492,26 @@ packages:
|
|||
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
p-limit@2.3.0:
|
||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
p-limit@3.1.0:
|
||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
p-locate@4.1.0:
|
||||
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
p-locate@5.0.0:
|
||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
p-try@2.2.0:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
package-json-from-dist@1.0.0:
|
||||
resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==}
|
||||
|
||||
|
@ -1501,6 +1572,10 @@ packages:
|
|||
pmtiles@3.0.6:
|
||||
resolution: {integrity: sha512-IdeMETd5lBIDVTLul1HFl0Q7l4KLJjzdxgcp+sN7pYvbipaV7o/0u0HiV06kaFCD0IGEN8KtUHyFZpY30WMflw==}
|
||||
|
||||
pngjs@5.0.0:
|
||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
postcss-import@15.1.0:
|
||||
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
@ -1567,6 +1642,11 @@ packages:
|
|||
protocol-buffers-schema@3.6.0:
|
||||
resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==}
|
||||
|
||||
qrcode@1.5.4:
|
||||
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
|
@ -1584,6 +1664,13 @@ packages:
|
|||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-main-filename@2.0.0:
|
||||
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
|
@ -1668,9 +1755,9 @@ packages:
|
|||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
sirv@2.0.4:
|
||||
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
||||
engines: {node: '>= 10'}
|
||||
sirv@3.0.0:
|
||||
resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
sorcery@0.11.1:
|
||||
resolution: {integrity: sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==}
|
||||
|
@ -1974,6 +2061,9 @@ packages:
|
|||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
which-module@2.0.1:
|
||||
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||
|
||||
which@1.3.1:
|
||||
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
|
||||
hasBin: true
|
||||
|
@ -1986,6 +2076,10 @@ packages:
|
|||
wide-align@1.1.5:
|
||||
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -1997,6 +2091,9 @@ packages:
|
|||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
y18n@4.0.3:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
|
||||
yallist@4.0.0:
|
||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
|
||||
|
@ -2005,6 +2102,14 @@ packages:
|
|||
engines: {node: '>= 14'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
yargs@15.4.1:
|
||||
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -2167,6 +2272,20 @@ snapshots:
|
|||
'@esbuild/win32-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@event-calendar/core@3.7.1':
|
||||
dependencies:
|
||||
svelte: 4.2.19
|
||||
|
||||
'@event-calendar/day-grid@3.7.1':
|
||||
dependencies:
|
||||
'@event-calendar/core': 3.7.1
|
||||
svelte: 4.2.19
|
||||
|
||||
'@event-calendar/time-grid@3.7.1':
|
||||
dependencies:
|
||||
'@event-calendar/core': 3.7.1
|
||||
svelte: 4.2.19
|
||||
|
||||
'@formatjs/ecma402-abstract@2.2.1':
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 2.2.2
|
||||
|
@ -2398,34 +2517,34 @@ snapshots:
|
|||
'@rollup/rollup-win32-x64-msvc@4.24.0':
|
||||
optional: true
|
||||
|
||||
'@sveltejs/adapter-auto@3.2.2(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))':
|
||||
'@sveltejs/adapter-auto@3.2.2(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))':
|
||||
dependencies:
|
||||
'@sveltejs/kit': 2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
|
||||
'@sveltejs/kit': 2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
|
||||
import-meta-resolve: 4.1.0
|
||||
|
||||
'@sveltejs/adapter-node@5.2.0(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))':
|
||||
'@sveltejs/adapter-node@5.2.0(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))':
|
||||
dependencies:
|
||||
'@rollup/plugin-commonjs': 26.0.1(rollup@4.24.0)
|
||||
'@rollup/plugin-json': 6.1.0(rollup@4.24.0)
|
||||
'@rollup/plugin-node-resolve': 15.2.3(rollup@4.24.0)
|
||||
'@sveltejs/kit': 2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
|
||||
'@sveltejs/kit': 2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
|
||||
rollup: 4.24.0
|
||||
|
||||
'@sveltejs/adapter-vercel@5.4.1(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))':
|
||||
'@sveltejs/adapter-vercel@5.4.1(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))':
|
||||
dependencies:
|
||||
'@sveltejs/kit': 2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
|
||||
'@sveltejs/kit': 2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
|
||||
'@vercel/nft': 0.27.2
|
||||
esbuild: 0.21.5
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
'@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))':
|
||||
'@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))':
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
|
||||
'@types/cookie': 0.6.0
|
||||
cookie: 0.6.0
|
||||
devalue: 5.0.0
|
||||
devalue: 5.1.1
|
||||
esm-env: 1.0.0
|
||||
import-meta-resolve: 4.1.0
|
||||
kleur: 4.1.5
|
||||
|
@ -2433,7 +2552,7 @@ snapshots:
|
|||
mrmime: 2.0.0
|
||||
sade: 1.8.1
|
||||
set-cookie-parser: 2.6.0
|
||||
sirv: 2.0.4
|
||||
sirv: 3.0.0
|
||||
svelte: 4.2.19
|
||||
tiny-glob: 0.2.9
|
||||
vite: 5.3.6(@types/node@22.5.4)
|
||||
|
@ -2501,6 +2620,10 @@ snapshots:
|
|||
|
||||
'@types/pug@2.0.10': {}
|
||||
|
||||
'@types/qrcode@1.5.5':
|
||||
dependencies:
|
||||
'@types/node': 22.5.4
|
||||
|
||||
'@types/resolve@1.20.2': {}
|
||||
|
||||
'@types/supercluster@7.1.3':
|
||||
|
@ -2578,7 +2701,7 @@ snapshots:
|
|||
autoprefixer@10.4.19(postcss@8.4.38):
|
||||
dependencies:
|
||||
browserslist: 4.23.1
|
||||
caniuse-lite: 1.0.30001636
|
||||
caniuse-lite: 1.0.30001688
|
||||
fraction.js: 4.3.7
|
||||
normalize-range: 0.1.2
|
||||
picocolors: 1.0.1
|
||||
|
@ -2612,7 +2735,7 @@ snapshots:
|
|||
|
||||
browserslist@4.23.1:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001636
|
||||
caniuse-lite: 1.0.30001688
|
||||
electron-to-chromium: 1.4.810
|
||||
node-releases: 2.0.14
|
||||
update-browserslist-db: 1.0.16(browserslist@4.23.1)
|
||||
|
@ -2636,7 +2759,9 @@ snapshots:
|
|||
|
||||
camelcase-css@2.0.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001636: {}
|
||||
camelcase@5.3.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001688: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
dependencies:
|
||||
|
@ -2660,6 +2785,12 @@ snapshots:
|
|||
memoizee: 0.4.17
|
||||
timers-ext: 0.1.8
|
||||
|
||||
cliui@6.0.0:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 6.2.0
|
||||
|
||||
code-red@1.0.4:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
|
@ -2734,6 +2865,8 @@ snapshots:
|
|||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
delegates@1.0.0: {}
|
||||
|
@ -2744,10 +2877,12 @@ snapshots:
|
|||
|
||||
detect-libc@2.0.3: {}
|
||||
|
||||
devalue@5.0.0: {}
|
||||
devalue@5.1.1: {}
|
||||
|
||||
didyoumean@1.2.2: {}
|
||||
|
||||
dijkstrajs@1.0.3: {}
|
||||
|
||||
dlv@1.1.3: {}
|
||||
|
||||
earcut@2.2.4: {}
|
||||
|
@ -2756,6 +2891,8 @@ snapshots:
|
|||
|
||||
electron-to-chromium@1.4.810: {}
|
||||
|
||||
emoji-picker-element@1.26.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
@ -2908,6 +3045,11 @@ snapshots:
|
|||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
find-up@4.1.0:
|
||||
dependencies:
|
||||
locate-path: 5.0.0
|
||||
path-exists: 4.0.0
|
||||
|
||||
find-up@5.0.0:
|
||||
dependencies:
|
||||
locate-path: 6.0.0
|
||||
|
@ -2945,6 +3087,8 @@ snapshots:
|
|||
|
||||
geojson-vt@4.0.2: {}
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
get-stream@6.0.1: {}
|
||||
|
||||
get-value@2.0.6: {}
|
||||
|
@ -3116,6 +3260,10 @@ snapshots:
|
|||
|
||||
locate-character@3.0.0: {}
|
||||
|
||||
locate-path@5.0.0:
|
||||
dependencies:
|
||||
p-locate: 4.1.0
|
||||
|
||||
locate-path@6.0.0:
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
|
@ -3289,14 +3437,24 @@ snapshots:
|
|||
dependencies:
|
||||
mimic-fn: 2.1.0
|
||||
|
||||
p-limit@2.3.0:
|
||||
dependencies:
|
||||
p-try: 2.2.0
|
||||
|
||||
p-limit@3.1.0:
|
||||
dependencies:
|
||||
yocto-queue: 0.1.0
|
||||
|
||||
p-locate@4.1.0:
|
||||
dependencies:
|
||||
p-limit: 2.3.0
|
||||
|
||||
p-locate@5.0.0:
|
||||
dependencies:
|
||||
p-limit: 3.1.0
|
||||
|
||||
p-try@2.2.0: {}
|
||||
|
||||
package-json-from-dist@1.0.0: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
|
@ -3350,6 +3508,8 @@ snapshots:
|
|||
'@types/leaflet': 1.9.12
|
||||
fflate: 0.8.2
|
||||
|
||||
pngjs@5.0.0: {}
|
||||
|
||||
postcss-import@15.1.0(postcss@8.4.38):
|
||||
dependencies:
|
||||
postcss: 8.4.38
|
||||
|
@ -3409,6 +3569,12 @@ snapshots:
|
|||
|
||||
protocol-buffers-schema@3.6.0: {}
|
||||
|
||||
qrcode@1.5.4:
|
||||
dependencies:
|
||||
dijkstrajs: 1.0.3
|
||||
pngjs: 5.0.0
|
||||
yargs: 15.4.1
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
quickselect@2.0.0: {}
|
||||
|
@ -3427,6 +3593,10 @@ snapshots:
|
|||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
require-main-filename@2.0.0: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-from@5.0.0: {}
|
||||
|
@ -3517,7 +3687,7 @@ snapshots:
|
|||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
sirv@2.0.4:
|
||||
sirv@3.0.0:
|
||||
dependencies:
|
||||
'@polka/url': 1.0.0-next.25
|
||||
mrmime: 2.0.0
|
||||
|
@ -3823,6 +3993,8 @@ snapshots:
|
|||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
|
||||
which-module@2.0.1: {}
|
||||
|
||||
which@1.3.1:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
@ -3835,6 +4007,12 @@ snapshots:
|
|||
dependencies:
|
||||
string-width: 4.2.3
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
|
@ -3849,8 +4027,29 @@ snapshots:
|
|||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
y18n@4.0.3: {}
|
||||
|
||||
yallist@4.0.0: {}
|
||||
|
||||
yaml@2.4.5: {}
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
decamelize: 1.2.0
|
||||
|
||||
yargs@15.4.1:
|
||||
dependencies:
|
||||
cliui: 6.0.0
|
||||
decamelize: 1.2.0
|
||||
find-up: 4.1.0
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
require-main-filename: 2.0.0
|
||||
set-blocking: 2.0.0
|
||||
string-width: 4.2.3
|
||||
which-module: 2.0.1
|
||||
y18n: 4.0.3
|
||||
yargs-parser: 18.1.3
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
|
|
@ -1,95 +1,65 @@
|
|||
import type { Handle } from '@sveltejs/kit';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
import { fetchCSRFToken, tryRefreshToken } from '$lib/index.server';
|
||||
|
||||
export const authHook: Handle = async ({ event, resolve }) => {
|
||||
try {
|
||||
let authCookie = event.cookies.get('auth');
|
||||
let refreshCookie = event.cookies.get('refresh');
|
||||
let sessionid = event.cookies.get('sessionid');
|
||||
|
||||
if (!authCookie && !refreshCookie) {
|
||||
if (!sessionid) {
|
||||
event.locals.user = null;
|
||||
return await resolve(event);
|
||||
}
|
||||
|
||||
if (!authCookie && refreshCookie) {
|
||||
event.locals.user = null;
|
||||
const token = await tryRefreshToken(event.cookies.get('refresh') || '');
|
||||
if (token) {
|
||||
authCookie = token;
|
||||
event.cookies.set('auth', authCookie, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
return await resolve(event);
|
||||
}
|
||||
}
|
||||
|
||||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
let userFetch = await event.fetch(`${serverEndpoint}/auth/user/`, {
|
||||
const cookie = event.request.headers.get('cookie') || '';
|
||||
|
||||
let userFetch = await event.fetch(`${serverEndpoint}/auth/user-metadata/`, {
|
||||
headers: {
|
||||
Cookie: `${authCookie}`
|
||||
cookie
|
||||
}
|
||||
});
|
||||
|
||||
if (!userFetch.ok) {
|
||||
console.log('Refreshing token');
|
||||
const refreshCookie = event.cookies.get('refresh');
|
||||
|
||||
if (refreshCookie) {
|
||||
const csrfToken = await fetchCSRFToken();
|
||||
if (!csrfToken) {
|
||||
console.error('Failed to fetch CSRF token');
|
||||
event.locals.user = null;
|
||||
return await resolve(event);
|
||||
}
|
||||
|
||||
const refreshFetch = await event.fetch(`${serverEndpoint}/auth/token/refresh/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ refresh: refreshCookie })
|
||||
});
|
||||
|
||||
if (refreshFetch.ok) {
|
||||
const refresh = await refreshFetch.json();
|
||||
event.cookies.set('auth', 'auth=' + refresh.access, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
|
||||
path: '/'
|
||||
});
|
||||
|
||||
userFetch = await event.fetch(`${serverEndpoint}/auth/user/`, {
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
Cookie: `auth=${refresh.access}`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
event.locals.user = null;
|
||||
event.cookies.delete('sessionid', { path: '/' });
|
||||
return await resolve(event);
|
||||
}
|
||||
|
||||
if (userFetch.ok) {
|
||||
const user = await userFetch.json();
|
||||
event.locals.user = user;
|
||||
const setCookieHeader = userFetch.headers.get('Set-Cookie');
|
||||
|
||||
if (setCookieHeader) {
|
||||
// Regular expression to match sessionid cookie and its expiry
|
||||
const sessionIdRegex = /sessionid=([^;]+).*?expires=([^;]+)/;
|
||||
const match = setCookieHeader.match(sessionIdRegex);
|
||||
|
||||
if (match) {
|
||||
const sessionId = match[1];
|
||||
const expiryString = match[2];
|
||||
const expiryDate = new Date(expiryString);
|
||||
|
||||
// Set the sessionid cookie
|
||||
event.cookies.set('sessionid', sessionId, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: true,
|
||||
expires: expiryDate
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
event.locals.user = null;
|
||||
event.cookies.delete('auth', { path: '/' });
|
||||
event.cookies.delete('refresh', { path: '/' });
|
||||
event.cookies.delete('sessionid', { path: '/' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in authHook:', error);
|
||||
event.locals.user = null;
|
||||
event.cookies.delete('auth', { path: '/' });
|
||||
event.cookies.delete('refresh', { path: '/' });
|
||||
event.cookies.delete('sessionid', { path: '/' });
|
||||
}
|
||||
|
||||
return await resolve(event);
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
console.log(res);
|
||||
let data = await res.json();
|
||||
console.log('ACTIVITIES' + data.activities);
|
||||
if (data && data.activities) {
|
||||
|
|
|
@ -19,9 +19,10 @@
|
|||
import CardCarousel from './CardCarousel.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let type: string;
|
||||
export let type: string | null = null;
|
||||
export let user: User | null;
|
||||
export let collection: Collection | null = null;
|
||||
export let readOnly: boolean = false;
|
||||
|
||||
let isCollectionModalOpen: boolean = false;
|
||||
let isWarningModalOpen: boolean = false;
|
||||
|
@ -164,61 +165,67 @@
|
|||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
<div class="card-actions justify-end mt-2">
|
||||
<!-- action options dropdown -->
|
||||
{#if type != 'link'}
|
||||
{#if adventure.user_id == user?.uuid || (collection && user && collection.shared_with.includes(user.uuid))}
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-neutral-200">
|
||||
<DotsHorizontal class="w-6 h-6" />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
|
||||
>
|
||||
<button
|
||||
class="btn btn-neutral mb-2"
|
||||
on:click={() => goto(`/adventures/${adventure.id}`)}
|
||||
><Launch class="w-6 h-6" />{$t('adventures.open_details')}</button
|
||||
>
|
||||
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
|
||||
<FileDocumentEdit class="w-6 h-6" />
|
||||
{$t('adventures.edit_adventure')}
|
||||
</button>
|
||||
{#if !readOnly}
|
||||
<div class="card-actions justify-end mt-2">
|
||||
<!-- action options dropdown -->
|
||||
|
||||
<!-- remove from collection -->
|
||||
{#if adventure.collection && user?.uuid == adventure.user_id}
|
||||
<button class="btn btn-neutral mb-2" on:click={removeFromCollection}
|
||||
><LinkVariantRemove class="w-6 h-6" />{$t(
|
||||
'adventures.remove_from_collection'
|
||||
)}</button
|
||||
>
|
||||
{/if}
|
||||
{#if !adventure.collection}
|
||||
<button class="btn btn-neutral mb-2" on:click={() => (isCollectionModalOpen = true)}
|
||||
><Plus class="w-6 h-6" />{$t('adventures.add_to_collection')}</button
|
||||
>
|
||||
{/if}
|
||||
<button
|
||||
id="delete_adventure"
|
||||
data-umami-event="Delete Adventure"
|
||||
class="btn btn-warning"
|
||||
on:click={() => (isWarningModalOpen = true)}
|
||||
><TrashCan class="w-6 h-6" />{$t('adventures.delete')}</button
|
||||
{#if type != 'link'}
|
||||
{#if adventure.user_id == user?.uuid || (collection && user && collection.shared_with.includes(user.uuid))}
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-neutral-200">
|
||||
<DotsHorizontal class="w-6 h-6" />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
|
||||
>
|
||||
</ul>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="btn btn-neutral-200 mb-2"
|
||||
on:click={() => goto(`/adventures/${adventure.id}`)}><Launch class="w-6 h-6" /></button
|
||||
>
|
||||
<button
|
||||
class="btn btn-neutral mb-2"
|
||||
on:click={() => goto(`/adventures/${adventure.id}`)}
|
||||
><Launch class="w-6 h-6" />{$t('adventures.open_details')}</button
|
||||
>
|
||||
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
|
||||
<FileDocumentEdit class="w-6 h-6" />
|
||||
{$t('adventures.edit_adventure')}
|
||||
</button>
|
||||
|
||||
<!-- remove from collection -->
|
||||
{#if adventure.collection && user?.uuid == adventure.user_id}
|
||||
<button class="btn btn-neutral mb-2" on:click={removeFromCollection}
|
||||
><LinkVariantRemove class="w-6 h-6" />{$t(
|
||||
'adventures.remove_from_collection'
|
||||
)}</button
|
||||
>
|
||||
{/if}
|
||||
{#if !adventure.collection}
|
||||
<button
|
||||
class="btn btn-neutral mb-2"
|
||||
on:click={() => (isCollectionModalOpen = true)}
|
||||
><Plus class="w-6 h-6" />{$t('adventures.add_to_collection')}</button
|
||||
>
|
||||
{/if}
|
||||
<button
|
||||
id="delete_adventure"
|
||||
data-umami-event="Delete Adventure"
|
||||
class="btn btn-warning"
|
||||
on:click={() => (isWarningModalOpen = true)}
|
||||
><TrashCan class="w-6 h-6" />{$t('adventures.delete')}</button
|
||||
>
|
||||
</ul>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="btn btn-neutral-200 mb-2"
|
||||
on:click={() => goto(`/adventures/${adventure.id}`)}
|
||||
><Launch class="w-6 h-6" /></button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
{#if type == 'link'}
|
||||
<button class="btn btn-primary" on:click={link}><Link class="w-6 h-6" /></button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if type == 'link'}
|
||||
<button class="btn btn-primary" on:click={link}><Link class="w-6 h-6" /></button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { deserialize } from '$app/forms';
|
||||
import type { Adventure, User } from '$lib/types';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
@ -59,7 +58,7 @@
|
|||
{/if}
|
||||
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||
{#each adventures as adventure}
|
||||
<AdventureCard user={user ?? null} type="link" {adventure} on:link={add} />
|
||||
<AdventureCard {user} type="link" {adventure} on:link={add} />
|
||||
{/each}
|
||||
{#if adventures.length === 0 && !isLoading}
|
||||
<p class="text-center text-lg">
|
||||
|
|
|
@ -14,7 +14,12 @@
|
|||
num_adventures: 0
|
||||
};
|
||||
|
||||
let isOpen = false;
|
||||
let isOpen: boolean = false;
|
||||
let isEmojiPickerVisible: boolean = false;
|
||||
|
||||
function toggleEmojiPicker() {
|
||||
isEmojiPickerVisible = !isEmojiPickerVisible;
|
||||
}
|
||||
|
||||
function toggleDropdown() {
|
||||
isOpen = !isOpen;
|
||||
|
@ -31,6 +36,10 @@
|
|||
selectCategory(new_category);
|
||||
}
|
||||
|
||||
function handleEmojiSelect(event: CustomEvent) {
|
||||
new_category.icon = event.detail.unicode;
|
||||
}
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
let dropdownRef: HTMLDivElement;
|
||||
|
||||
|
@ -46,6 +55,9 @@
|
|||
document.removeEventListener('click', handleClickOutside);
|
||||
};
|
||||
});
|
||||
onMount(async () => {
|
||||
await import('emoji-picker-element');
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mt-2 relative" bind:this={dropdownRef}>
|
||||
|
@ -59,23 +71,36 @@
|
|||
<div class="absolute z-10 w-full mt-1 bg-base-300 rounded shadow-lg p-2">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={$t('categories.category_name')}
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
bind:value={new_category.display_name}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={$t('categories.icon')}
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
bind:value={new_category.icon}
|
||||
/>
|
||||
<button on:click={custom_category} type="button" class="btn btn-primary"
|
||||
>{$t('adventures.add')}</button
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={$t('categories.category_name')}
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
bind:value={new_category.display_name}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={$t('categories.icon')}
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
bind:value={new_category.icon}
|
||||
/>
|
||||
<button on:click={toggleEmojiPicker} type="button" class="btn btn-secondary">
|
||||
{isEmojiPickerVisible ? $t('adventures.hide') : $t('adventures.hide')}
|
||||
{$t('adventures.emoji_picker')}
|
||||
</button>
|
||||
<button on:click={custom_category} type="button" class="btn btn-primary">
|
||||
{$t('adventures.add')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if isEmojiPickerVisible}
|
||||
<div class="mt-2">
|
||||
<emoji-picker on:emoji-click={handleEmojiSelect}></emoji-picker>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
<!-- Sort the categories dynamically before rendering -->
|
||||
{#each categories
|
||||
|
|
|
@ -5,14 +5,10 @@
|
|||
import type { SubmitFunction } from '@sveltejs/kit';
|
||||
|
||||
import DotsHorizontal from '~icons/mdi/dots-horizontal';
|
||||
import WeatherSunny from '~icons/mdi/weather-sunny';
|
||||
import WeatherNight from '~icons/mdi/weather-night';
|
||||
import Forest from '~icons/mdi/forest';
|
||||
import Water from '~icons/mdi/water';
|
||||
import Calendar from '~icons/mdi/calendar';
|
||||
import AboutModal from './AboutModal.svelte';
|
||||
import AccountMultiple from '~icons/mdi/account-multiple';
|
||||
import Avatar from './Avatar.svelte';
|
||||
import PaletteOutline from '~icons/mdi/palette-outline';
|
||||
import { page } from '$app/stores';
|
||||
import { t, locale, locales } from 'svelte-i18n';
|
||||
import { themes } from '$lib';
|
||||
|
@ -91,6 +87,9 @@
|
|||
<li>
|
||||
<button on:click={() => goto('/map')}>{$t('navbar.map')}</button>
|
||||
</li>
|
||||
<li>
|
||||
<button on:click={() => goto('/calendar')}>{$t('navbar.calendar')}</button>
|
||||
</li>
|
||||
<li>
|
||||
<button on:click={() => goto('/users')}>{$t('navbar.users')}</button>
|
||||
</li>
|
||||
|
@ -157,6 +156,9 @@
|
|||
<li>
|
||||
<button class="btn btn-neutral" on:click={() => goto('/map')}>{$t('navbar.map')}</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-neutral" on:click={() => goto('/calendar')}><Calendar /></button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-neutral" on:click={() => goto('/users')}
|
||||
><AccountMultiple /></button
|
||||
|
|
|
@ -246,7 +246,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mr-4 mt-4">{$t('transportation.edit')}</button>
|
||||
<button type="submit" class="btn btn-primary mr-4 mt-4">{$t('collection.create')}</button>
|
||||
<!-- if there is a button in form, it will close the modal -->
|
||||
<button class="btn mt-4" on:click={close}>{$t('about.close')}</button>
|
||||
</form>
|
||||
|
|
184
frontend/src/lib/components/TOTPModal.svelte
Normal file
184
frontend/src/lib/components/TOTPModal.svelte
Normal file
|
@ -0,0 +1,184 @@
|
|||
<script lang="ts">
|
||||
import { addToast } from '$lib/toasts';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
import { onMount } from 'svelte';
|
||||
let modal: HTMLDialogElement;
|
||||
// @ts-ignore
|
||||
import QRCode from 'qrcode';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { User } from '$lib/types';
|
||||
export let user: User | null = null;
|
||||
let secret: string | null = null;
|
||||
let qrCodeDataUrl: string | null = null;
|
||||
let totpUrl: string | null = null;
|
||||
let first_code: string = '';
|
||||
let recovery_codes: string[] = [];
|
||||
export let is_enabled: boolean;
|
||||
let reauthError: boolean = false;
|
||||
|
||||
onMount(() => {
|
||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||
if (modal) {
|
||||
modal.showModal();
|
||||
}
|
||||
fetchSetupInfo();
|
||||
console.log(secret);
|
||||
});
|
||||
|
||||
async function generateQRCode(secret: string | null) {
|
||||
try {
|
||||
if (secret) {
|
||||
qrCodeDataUrl = await QRCode.toDataURL(secret);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating QR code:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSetupInfo() {
|
||||
const res = await fetch('/_allauth/browser/v1/account/authenticators/totp', {
|
||||
method: 'GET'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.status == 404) {
|
||||
secret = data.meta.secret;
|
||||
totpUrl = `otpauth://totp/AdventureLog:${user?.username}?secret=${secret}&issuer=AdventureLog`;
|
||||
generateQRCode(totpUrl);
|
||||
} else if (res.ok) {
|
||||
close();
|
||||
} else {
|
||||
addToast('error', $t('settings.generic_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function sendTotp() {
|
||||
const res = await fetch('/_allauth/browser/v1/account/authenticators/totp', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code: first_code
|
||||
}),
|
||||
credentials: 'include'
|
||||
});
|
||||
console.log(res);
|
||||
if (res.ok) {
|
||||
addToast('success', $t('settings.mfa_enabled'));
|
||||
is_enabled = true;
|
||||
getRecoveryCodes();
|
||||
} else {
|
||||
if (res.status == 401) {
|
||||
reauthError = true;
|
||||
}
|
||||
addToast('error', $t('settings.generic_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function getRecoveryCodes() {
|
||||
console.log('getting recovery codes');
|
||||
const res = await fetch('/_allauth/browser/v1/account/authenticators/recovery-codes', {
|
||||
method: 'GET'
|
||||
});
|
||||
if (res.ok) {
|
||||
let data = await res.json();
|
||||
recovery_codes = data.data.unused_codes;
|
||||
} else {
|
||||
addToast('error', $t('settings.generic_error'));
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
dispatch('close');
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(copyText: string | null) {
|
||||
if (copyText) {
|
||||
navigator.clipboard.writeText(copyText).then(
|
||||
() => {
|
||||
addToast('success', $t('adventures.copied_to_clipboard'));
|
||||
},
|
||||
() => {
|
||||
addToast('error', $t('adventures.copy_failed'));
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<dialog id="my_modal_1" class="modal">
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||
<h3 class="font-bold text-lg">{$t('settings.enable_mfa')}</h3>
|
||||
|
||||
{#if qrCodeDataUrl}
|
||||
<div class="mb-4 flex items-center justify-center mt-2">
|
||||
<img src={qrCodeDataUrl} alt="QR Code" class="w-64 h-64" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex items-center justify-center mb-6">
|
||||
{#if secret}
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={secret}
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
readonly
|
||||
/>
|
||||
<button class="btn btn-primary ml-2" on:click={() => copyToClipboard(secret)}
|
||||
>{$t('settings.copy')}</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder={$t('settings.authenticator_code')}
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
bind:value={first_code}
|
||||
/>
|
||||
|
||||
<div class="recovery-codes-container">
|
||||
{#if recovery_codes.length > 0}
|
||||
<h3 class="mt-4 text-center font-bold text-lg">{$t('settings.recovery_codes')}</h3>
|
||||
<p class="text-center text-lg mb-2">
|
||||
{$t('settings.recovery_codes_desc')}
|
||||
</p>
|
||||
<button
|
||||
class="btn btn-primary ml-2"
|
||||
on:click={() => copyToClipboard(recovery_codes.join(', '))}>{$t('settings.copy')}</button
|
||||
>
|
||||
{/if}
|
||||
<div class="recovery-codes-grid flex flex-wrap">
|
||||
{#each recovery_codes as code}
|
||||
<div
|
||||
class="recovery-code-item flex items-center justify-center m-2 w-full sm:w-1/2 md:w-1/3 lg:w-1/4"
|
||||
>
|
||||
<input type="text" value={code} class="input input-bordered w-full" readonly />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if reauthError}
|
||||
<div class="alert alert-error mt-4">
|
||||
{$t('settings.reset_session_error')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !is_enabled}
|
||||
<button class="btn btn-primary mt-4" on:click={sendTotp}>{$t('settings.enable_mfa')}</button>
|
||||
{/if}
|
||||
|
||||
<button class="btn btn-primary mt-4" on:click={close}>{$t('about.close')}</button>
|
||||
</div>
|
||||
</dialog>
|
|
@ -10,27 +10,3 @@ export const fetchCSRFToken = async () => {
|
|||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const tryRefreshToken = async (refreshToken: string) => {
|
||||
const csrfToken = await fetchCSRFToken();
|
||||
const refreshFetch = await fetch(`${serverEndpoint}/auth/token/refresh/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json' // Corrected header name
|
||||
},
|
||||
body: JSON.stringify({ refresh: refreshToken })
|
||||
});
|
||||
|
||||
if (refreshFetch.ok) {
|
||||
const refresh = await refreshFetch.json();
|
||||
const token = `auth=${refresh.access}`;
|
||||
return token;
|
||||
// event.cookies.set('auth', `auth=${refresh.access}`, {
|
||||
// httpOnly: true,
|
||||
// sameSite: 'lax',
|
||||
// expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
|
||||
// path: '/'
|
||||
// });
|
||||
}
|
||||
};
|
||||
|
|
|
@ -313,5 +313,6 @@ export let themes = [
|
|||
{ name: 'forest', label: 'Forest' },
|
||||
{ name: 'aqua', label: 'Aqua' },
|
||||
{ name: 'aestheticLight', label: 'Aesthetic Light' },
|
||||
{ name: 'aestheticDark', label: 'Aesthetic Dark' }
|
||||
{ name: 'aestheticDark', label: 'Aesthetic Dark' },
|
||||
{ name: 'northernLights', label: 'Northern Lights' }
|
||||
];
|
||||
|
|
|
@ -188,7 +188,14 @@
|
|||
"add_a_tag": "Fügen Sie ein Tag hinzu",
|
||||
"tags": "Schlagworte",
|
||||
"set_to_pin": "Auf „Anpinnen“ setzen",
|
||||
"category_fetch_error": "Fehler beim Abrufen der Kategorien"
|
||||
"category_fetch_error": "Fehler beim Abrufen der Kategorien",
|
||||
"copied_to_clipboard": "In die Zwischenablage kopiert!",
|
||||
"copy_failed": "Das Kopieren ist fehlgeschlagen",
|
||||
"adventure_calendar": "Abenteuerkalender",
|
||||
"emoji_picker": "Emoji-Picker",
|
||||
"hide": "Verstecken",
|
||||
"show": "Zeigen",
|
||||
"download_calendar": "Kalender herunterladen"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit",
|
||||
|
@ -226,14 +233,16 @@
|
|||
"light": "Licht",
|
||||
"night": "Nacht",
|
||||
"aestheticDark": "Ästhetisches Dunkel",
|
||||
"aestheticLight": "Ästhetisches Licht"
|
||||
"aestheticLight": "Ästhetisches Licht",
|
||||
"northernLights": "Nordlicht"
|
||||
},
|
||||
"users": "Benutzer",
|
||||
"worldtravel": "Weltreisen",
|
||||
"my_tags": "Meine Tags",
|
||||
"tag": "Etikett",
|
||||
"language_selection": "Sprache",
|
||||
"support": "Unterstützung"
|
||||
"support": "Unterstützung",
|
||||
"calendar": "Kalender"
|
||||
},
|
||||
"auth": {
|
||||
"confirm_password": "Passwort bestätigen",
|
||||
|
@ -249,7 +258,11 @@
|
|||
"username": "Benutzername",
|
||||
"profile_picture": "Profilbild",
|
||||
"public_profile": "Öffentliches Profil",
|
||||
"public_tooltip": "Mit einem öffentlichen Profil können Benutzer Sammlungen mit Ihnen teilen und Ihr Profil auf der Benutzerseite anzeigen."
|
||||
"public_tooltip": "Mit einem öffentlichen Profil können Benutzer Sammlungen mit Ihnen teilen und Ihr Profil auf der Benutzerseite anzeigen.",
|
||||
"email_required": "E-Mail ist erforderlich",
|
||||
"both_passwords_required": "Beide Passwörter sind erforderlich",
|
||||
"new_password": "Neues Passwort",
|
||||
"reset_failed": "Passwort konnte nicht zurückgesetzt werden"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "Keine Benutzer mit öffentlichen Profilen gefunden."
|
||||
|
@ -291,7 +304,50 @@
|
|||
"about_this_background": "Über diesen Hintergrund",
|
||||
"join_discord": "Treten Sie dem Discord bei",
|
||||
"join_discord_desc": "um Ihre eigenen Fotos zu teilen. \nVeröffentlichen Sie sie im",
|
||||
"photo_by": "Foto von"
|
||||
"photo_by": "Foto von",
|
||||
"change_password_error": "Passwort kann nicht geändert werden. \nUngültiges aktuelles Passwort oder ungültiges neues Passwort.",
|
||||
"current_password": "Aktuelles Passwort",
|
||||
"password_change_lopout_warning": "Nach der Passwortänderung werden Sie abgemeldet.",
|
||||
"authenticator_code": "Authentifikatorcode",
|
||||
"copy": "Kopie",
|
||||
"disable_mfa": "Deaktivieren Sie MFA",
|
||||
"email_added": "E-Mail erfolgreich hinzugefügt!",
|
||||
"email_added_error": "Fehler beim Hinzufügen der E-Mail",
|
||||
"email_removed": "E-Mail erfolgreich entfernt!",
|
||||
"email_removed_error": "Fehler beim Entfernen der E-Mail",
|
||||
"email_set_primary": "E-Mail erfolgreich als primäre E-Mail-Adresse festgelegt!",
|
||||
"email_set_primary_error": "Fehler beim Festlegen der E-Mail-Adresse als primär",
|
||||
"email_verified": "E-Mail erfolgreich bestätigt!",
|
||||
"email_verified_erorr_desc": "Ihre E-Mail-Adresse konnte nicht bestätigt werden. \nBitte versuchen Sie es erneut.",
|
||||
"email_verified_error": "Fehler bei der E-Mail-Bestätigung",
|
||||
"email_verified_success": "Ihre E-Mail-Adresse wurde bestätigt. \nSie können sich jetzt anmelden.",
|
||||
"enable_mfa": "Aktivieren Sie MFA",
|
||||
"error_change_password": "Fehler beim Ändern des Passworts. \nBitte überprüfen Sie Ihr aktuelles Passwort und versuchen Sie es erneut.",
|
||||
"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",
|
||||
"mfa_disabled": "Multi-Faktor-Authentifizierung erfolgreich deaktiviert!",
|
||||
"mfa_enabled": "Multi-Faktor-Authentifizierung erfolgreich aktiviert!",
|
||||
"mfa_not_enabled": "MFA ist nicht aktiviert",
|
||||
"mfa_page_title": "Multi-Faktor-Authentifizierung",
|
||||
"mfa_required": "Eine Multi-Faktor-Authentifizierung ist erforderlich",
|
||||
"no_emai_set": "Keine E-Mail-Adresse festgelegt",
|
||||
"not_verified": "Nicht verifiziert",
|
||||
"primary": "Primär",
|
||||
"recovery_codes": "Wiederherstellungscodes",
|
||||
"recovery_codes_desc": "Dies sind Ihre Wiederherstellungscodes. \nBewahren Sie sie sicher auf. \nSie werden sie nicht mehr sehen können.",
|
||||
"reset_session_error": "Bitte melden Sie sich ab und wieder an, um Ihre Sitzung zu aktualisieren, und versuchen Sie es erneut.",
|
||||
"verified": "Verifiziert",
|
||||
"verify": "Verifizieren",
|
||||
"verify_email_error": "Fehler bei der E-Mail-Bestätigung. \nVersuchen Sie es in ein paar Minuten noch einmal.",
|
||||
"verify_email_success": "E-Mail-Bestätigung erfolgreich gesendet!",
|
||||
"add_email_blocked": "Sie können keine E-Mail-Adresse zu einem Konto hinzufügen, das durch die Zwei-Faktor-Authentifizierung geschützt ist.",
|
||||
"required": "Dieses Feld ist erforderlich",
|
||||
"csrf_failed": "CSRF-Token konnte nicht abgerufen werden",
|
||||
"duplicate_email": "Diese E-Mail-Adresse wird bereits verwendet.",
|
||||
"email_taken": "Diese E-Mail-Adresse wird bereits verwendet.",
|
||||
"username_taken": "Dieser Benutzername wird bereits verwendet."
|
||||
},
|
||||
"checklist": {
|
||||
"add_item": "Artikel hinzufügen",
|
||||
|
@ -411,5 +467,14 @@
|
|||
"no_categories_found": "Keine Kategorien gefunden.",
|
||||
"select_category": "Kategorie auswählen",
|
||||
"update_after_refresh": "Die Abenteuerkarten werden aktualisiert, sobald Sie die Seite aktualisieren."
|
||||
},
|
||||
"dashboard": {
|
||||
"add_some": "Warum beginnen Sie nicht mit der Planung Ihres nächsten Abenteuers? \nSie können ein neues Abenteuer hinzufügen, indem Sie auf die Schaltfläche unten klicken.",
|
||||
"countries_visited": "Besuchte Länder",
|
||||
"no_recent_adventures": "Keine aktuellen Abenteuer?",
|
||||
"recent_adventures": "Aktuelle Abenteuer",
|
||||
"total_adventures": "Totale Abenteuer",
|
||||
"total_visited_regions": "Insgesamt besuchte Regionen",
|
||||
"welcome_back": "Willkommen zurück"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,415 +1,480 @@
|
|||
{
|
||||
"navbar": {
|
||||
"adventures": "Adventures",
|
||||
"collections": "Collections",
|
||||
"worldtravel": "World Travel",
|
||||
"map": "Map",
|
||||
"users": "Users",
|
||||
"search": "Search",
|
||||
"profile": "Profile",
|
||||
"greeting": "Hi",
|
||||
"my_adventures": "My Adventures",
|
||||
"my_tags": "My Tags",
|
||||
"tag": "Tag",
|
||||
"shared_with_me": "Shared With Me",
|
||||
"settings": "Settings",
|
||||
"logout": "Logout",
|
||||
"about": "About AdventureLog",
|
||||
"documentation": "Documentation",
|
||||
"discord": "Discord",
|
||||
"language_selection": "Language",
|
||||
"support": "Support",
|
||||
"theme_selection": "Theme Selection",
|
||||
"themes": {
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"night": "Night",
|
||||
"forest": "Forest",
|
||||
"aestheticLight": "Aesthetic Light",
|
||||
"aestheticDark": "Aesthetic Dark",
|
||||
"aqua": "Aqua"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"about": "About",
|
||||
"license": "Licensed under the GPL-3.0 License.",
|
||||
"source_code": "Source Code",
|
||||
"message": "Made with ❤️ in the United States.",
|
||||
"oss_attributions": "Open Source Attributions",
|
||||
"nominatim_1": "Location Search and Geocoding is provided by",
|
||||
"nominatim_2": "Their data is liscensed under the ODbL license.",
|
||||
"other_attributions": "Additional attributions can be found in the README file.",
|
||||
"close": "Close"
|
||||
},
|
||||
"home": {
|
||||
"hero_1": "Discover the World's Most Thrilling Adventures",
|
||||
"hero_2": "Discover and plan your next adventure with AdventureLog. Explore breathtaking destinations, create custom itineraries, and stay connected on the go.",
|
||||
"go_to": "Go To AdventureLog",
|
||||
"key_features": "Key Features",
|
||||
"desc_1": "Discover, Plan, and Explore with Ease",
|
||||
"desc_2": "AdventureLog is designed to simplify your journey, providing you with the tools and resources to plan, pack, and navigate your next unforgettable adventure.",
|
||||
"feature_1": "Travel Log",
|
||||
"feature_1_desc": "Keep track of your adventures with a personalized travel log and share your experiences with friends and family.",
|
||||
"feature_2": "Trip Planning",
|
||||
"feature_2_desc": "Easily create custom itineraries and get a day-by-day breakdown of your trip.",
|
||||
"feature_3": "Travel Map",
|
||||
"feature_3_desc": "View your travels throughout the world with an interactive map and explore new destinations."
|
||||
},
|
||||
"adventures": {
|
||||
"collection_remove_success": "Adventure removed from collection successfully!",
|
||||
"collection_remove_error": "Error removing adventure from collection",
|
||||
"collection_link_success": "Adventure linked to collection successfully!",
|
||||
"no_image_found": "No image found",
|
||||
"collection_link_error": "Error linking adventure to collection",
|
||||
"adventure_delete_confirm": "Are you sure you want to delete this adventure? This action cannot be undone.",
|
||||
"open_details": "Open Details",
|
||||
"edit_adventure": "Edit Adventure",
|
||||
"remove_from_collection": "Remove from Collection",
|
||||
"add_to_collection": "Add to Collection",
|
||||
"delete": "Delete",
|
||||
"not_found": "Adventure not found",
|
||||
"not_found_desc": "The adventure you were looking for could not be found. Please try a different adventure or check back later.",
|
||||
"homepage": "Homepage",
|
||||
"adventure_details": "Adventure Details",
|
||||
"collection": "Collection",
|
||||
"adventure_type": "Adventure Type",
|
||||
"longitude": "Longitude",
|
||||
"latitude": "Latitude",
|
||||
"visit": "Visit",
|
||||
"visits": "Visits",
|
||||
"create_new": "Create New...",
|
||||
"adventure": "Adventure",
|
||||
"count_txt": "results matching your search",
|
||||
"sort": "Sort",
|
||||
"order_by": "Order By",
|
||||
"order_direction": "Order Direction",
|
||||
"ascending": "Ascending",
|
||||
"descending": "Descending",
|
||||
"updated": "Updated",
|
||||
"name": "Name",
|
||||
"date": "Date",
|
||||
"activity_types": "Activity Types",
|
||||
"tags": "Tags",
|
||||
"add_a_tag": "Add a tag",
|
||||
"date_constrain": "Constrain to collection dates",
|
||||
"rating": "Rating",
|
||||
"my_images": "My Images",
|
||||
"add_an_activity": "Add an activity",
|
||||
"no_images": "No Images",
|
||||
"upload_images_here": "Upload images here",
|
||||
"share_adventure": "Share this Adventure!",
|
||||
"copy_link": "Copy Link",
|
||||
"image": "Image",
|
||||
"upload_image": "Upload Image",
|
||||
"url": "URL",
|
||||
"fetch_image": "Fetch Image",
|
||||
"wikipedia": "Wikipedia",
|
||||
"add_notes": "Add notes",
|
||||
"warning": "Warning",
|
||||
"my_adventures": "My Adventures",
|
||||
"no_linkable_adventures": "No adventures found that can be linked to this collection.",
|
||||
"add": "Add",
|
||||
"save_next": "Save & Next",
|
||||
"end_date": "End Date",
|
||||
"my_visits": "My Visits",
|
||||
"start_date": "Start Date",
|
||||
"remove": "Remove",
|
||||
"location": "Location",
|
||||
"search_for_location": "Search for a location",
|
||||
"clear_map": "Clear map",
|
||||
"search_results": "Searh results",
|
||||
"no_results": "No results found",
|
||||
"wiki_desc": "Pulls excerpt from Wikipedia article matching the name of the adventure.",
|
||||
"generate_desc": "Generate Description",
|
||||
"public_adventure": "Public Adventure",
|
||||
"location_information": "Location Information",
|
||||
"link": "Link",
|
||||
"links": "Links",
|
||||
"description": "Description",
|
||||
"sources": "Sources",
|
||||
"collection_adventures": "Include Collection Adventures",
|
||||
"filter": "Filter",
|
||||
"category_filter": "Category Filter",
|
||||
"category": "Category",
|
||||
"select_adventure_category": "Select Adventure Category",
|
||||
"clear": "Clear",
|
||||
"my_collections": "My Collections",
|
||||
"open_filters": "Open Filters",
|
||||
"close_filters": "Close Filters",
|
||||
"archived_collections": "Archived Collections",
|
||||
"share": "Share",
|
||||
"private": "Private",
|
||||
"public": "Public",
|
||||
"archived": "Archived",
|
||||
"edit_collection": "Edit Collection",
|
||||
"unarchive": "Unarchive",
|
||||
"archive": "Archive",
|
||||
"no_collections_found": "No collections found to add this adventure to.",
|
||||
"not_visited": "Not Visited",
|
||||
"archived_collection_message": "Collection archived successfully!",
|
||||
"unarchived_collection_message": "Collection unarchived successfully!",
|
||||
"delete_collection_success": "Collection deleted successfully!",
|
||||
"delete_collection_warning": "Are you sure you want to delete this collection? This will also delete all of the linked adventures. This action cannot be undone.",
|
||||
"cancel": "Cancel",
|
||||
"delete_collection": "Delete Collection",
|
||||
"delete_adventure": "Delete Adventure",
|
||||
"adventure_delete_success": "Adventure deleted successfully!",
|
||||
"visited": "Visited",
|
||||
"planned": "Planned",
|
||||
"duration": "Duration",
|
||||
"all": "All",
|
||||
"image_removed_success": "Image removed successfully!",
|
||||
"image_removed_error": "Error removing image",
|
||||
"no_image_url": "No image found at that URL.",
|
||||
"image_upload_success": "Image uploaded successfully!",
|
||||
"image_upload_error": "Error uploading image",
|
||||
"dates": "Dates",
|
||||
"wiki_image_error": "Error fetching image from Wikipedia",
|
||||
"start_before_end_error": "Start date must be before end date",
|
||||
"activity": "Activity",
|
||||
"actions": "Actions",
|
||||
"no_end_date": "Please enter an end date",
|
||||
"see_adventures": "See Adventures",
|
||||
"image_fetch_failed": "Failed to fetch image",
|
||||
"no_location": "Please enter a location",
|
||||
"no_start_date": "Please enter a start date",
|
||||
"no_description_found": "No description found",
|
||||
"adventure_created": "Adventure created",
|
||||
"adventure_create_error": "Failed to create adventure",
|
||||
"adventure_updated": "Adventure updated",
|
||||
"adventure_update_error": "Failed to update adventure",
|
||||
"set_to_pin": "Set to Pin",
|
||||
"category_fetch_error": "Error fetching categories",
|
||||
"new_adventure": "New Adventure",
|
||||
"basic_information": "Basic Information",
|
||||
"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?",
|
||||
"mark_visited": "Mark Visited",
|
||||
"error_updating_regions": "Error updating regions",
|
||||
"regions_updated": "regions updated",
|
||||
"visited_region_check": "Visited Region Check",
|
||||
"visited_region_check_desc": "By selecting this, the server will check all of your visited adventures and mark the regions they are located in as visited in world travel.",
|
||||
"update_visited_regions": "Update Visited Regions",
|
||||
"update_visited_regions_disclaimer": "This may take a while depending on the number of adventures you have visited.",
|
||||
"link_new": "Link New...",
|
||||
"add_new": "Add New...",
|
||||
"transportation": "Transportation",
|
||||
"note": "Note",
|
||||
"checklist": "Checklist",
|
||||
"collection_archived": "This collection has been archived.",
|
||||
"visit_link": "Visit Link",
|
||||
"collection_completed": "You've completed this collection!",
|
||||
"collection_stats": "Collection Stats",
|
||||
"keep_exploring": "Keep Exploring!",
|
||||
"linked_adventures": "Linked Adventures",
|
||||
"notes": "Notes",
|
||||
"checklists": "Checklists",
|
||||
"transportations": "Transportations",
|
||||
"day": "Day",
|
||||
"itineary_by_date": "Itinerary by Date",
|
||||
"nothing_planned": "Nothing planned for this day. Enjoy the journey!",
|
||||
"days": "days",
|
||||
"activities": {
|
||||
"general": "General 🌍",
|
||||
"outdoor": "Outdoor 🏞️",
|
||||
"lodging": "Lodging 🛌",
|
||||
"dining": "Dining 🍽️",
|
||||
"activity": "Activity 🏄",
|
||||
"attraction": "Attraction 🎢",
|
||||
"shopping": "Shopping 🛍️",
|
||||
"nightlife": "Nightlife 🌃",
|
||||
"event": "Event 🎉",
|
||||
"transportation": "Transportation 🚗",
|
||||
"culture": "Culture 🎭",
|
||||
"water_sports": "Water Sports 🚤",
|
||||
"hiking": "Hiking 🥾",
|
||||
"wildlife": "Wildlife 🦒",
|
||||
"historical_sites": "Historical Sites 🏛️",
|
||||
"music_concerts": "Music & Concerts 🎶",
|
||||
"fitness": "Fitness 🏋️",
|
||||
"art_museums": "Art & Museums 🎨",
|
||||
"festivals": "Festivals 🎪",
|
||||
"spiritual_journeys": "Spiritual Journeys 🧘♀️",
|
||||
"volunteer_work": "Volunteer Work 🤝",
|
||||
"other": "Other"
|
||||
}
|
||||
},
|
||||
"worldtravel": {
|
||||
"country_list": "Country List",
|
||||
"num_countries": "countries found",
|
||||
"all": "All",
|
||||
"partially_visited": "Partially Visited",
|
||||
"not_visited": "Not Visited",
|
||||
"completely_visited": "Completely Visited",
|
||||
"all_subregions": "All Subregions",
|
||||
"clear_search": "Clear Search",
|
||||
"no_countries_found": "No countries found"
|
||||
},
|
||||
"auth": {
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"forgot_password": "Forgot Password?",
|
||||
"signup": "Signup",
|
||||
"login_error": "Unable to login with the provided credentials.",
|
||||
"login": "Login",
|
||||
"email": "Email",
|
||||
"first_name": "First Name",
|
||||
"last_name": "Last Name",
|
||||
"confirm_password": "Confirm Password",
|
||||
"registration_disabled": "Registration is currently disabled.",
|
||||
"profile_picture": "Profile Picture",
|
||||
"public_profile": "Public Profile",
|
||||
"public_tooltip": "With a public profile, users can share collections with you and view your profile on the users page."
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "No users found with public profiles."
|
||||
},
|
||||
"settings": {
|
||||
"update_error": "Error updating settings",
|
||||
"update_success": "Settings updated successfully!",
|
||||
"settings_page": "Settings Page",
|
||||
"account_settings": "User Account Settings",
|
||||
"update": "Update",
|
||||
"password_change": "Change Password",
|
||||
"new_password": "New Password",
|
||||
"confirm_new_password": "Confirm New Password",
|
||||
"email_change": "Change Email",
|
||||
"current_email": "Current Email",
|
||||
"no_email_set": "No email set",
|
||||
"new_email": "New Email",
|
||||
"change_password": "Change Password",
|
||||
"login_redir": "You will then be redirected to the login page.",
|
||||
"token_required": "Token and UID are required for password reset.",
|
||||
"reset_password": "Reset Password",
|
||||
"possible_reset": "If the email address you provided is associated with an account, you will receive an email with instructions to reset your password!",
|
||||
"missing_email": "Please enter an email address",
|
||||
"submit": "Submit",
|
||||
"password_does_not_match": "Passwords do not match",
|
||||
"password_is_required": "Password is required",
|
||||
"invalid_token": "Token is invalid or has expired",
|
||||
"about_this_background": "About this background",
|
||||
"photo_by": "Photo by",
|
||||
"join_discord": "Join the Discord",
|
||||
"join_discord_desc": "to share your own photos. Post them in the #travel-share channel."
|
||||
},
|
||||
"collection": {
|
||||
"collection_created": "Collection created successfully!",
|
||||
"error_creating_collection": "Error creating collection",
|
||||
"new_collection": "New Collection",
|
||||
"create": "Create",
|
||||
"collection_edit_success": "Collection edited successfully!",
|
||||
"error_editing_collection": "Error editing collection",
|
||||
"edit_collection": "Edit Collection"
|
||||
},
|
||||
"notes": {
|
||||
"note_deleted": "Note deleted successfully!",
|
||||
"note_delete_error": "Error deleting note",
|
||||
"open": "Open",
|
||||
"failed_to_save": "Failed to save note",
|
||||
"note_editor": "Note Editor",
|
||||
"editing_note": "Editing note",
|
||||
"content": "Content",
|
||||
"save": "Save",
|
||||
"note_public": "This note is public because it is in a public collection.",
|
||||
"add_a_link": "Add a link",
|
||||
"invalid_url": "Invalid URL"
|
||||
},
|
||||
"checklist": {
|
||||
"checklist_deleted": "Checklist deleted successfully!",
|
||||
"checklist_delete_error": "Error deleting checklist",
|
||||
"failed_to_save": "Failed to save checklist",
|
||||
"checklist_editor": "Checklist Editor",
|
||||
"editing_checklist": "Editing checklist",
|
||||
"item": "Item",
|
||||
"items": "Items",
|
||||
"add_item": "Add Item",
|
||||
"new_item": "New Item",
|
||||
"save": "Save",
|
||||
"checklist_public": "This checklist is public because it is in a public collection.",
|
||||
"item_cannot_be_empty": "Item cannot be empty",
|
||||
"item_already_exists": "Item already exists"
|
||||
},
|
||||
"transportation": {
|
||||
"transportation_deleted": "Transportation deleted successfully!",
|
||||
"transportation_delete_error": "Error deleting transportation",
|
||||
"provide_start_date": "Please provide a start date",
|
||||
"transport_type": "Transport Type",
|
||||
"type": "Type",
|
||||
"transportation_added": "Transportation added successfully!",
|
||||
"error_editing_transportation": "Error editing transportation",
|
||||
"new_transportation": "New Transportation",
|
||||
"date_time": "Start Date & Time",
|
||||
"end_date_time": "End Date & Time",
|
||||
"flight_number": "Flight Number",
|
||||
"from_location": "From Location",
|
||||
"to_location": "To Location",
|
||||
"edit": "Edit",
|
||||
"modes": {
|
||||
"car": "Car",
|
||||
"plane": "Plane",
|
||||
"train": "Train",
|
||||
"bus": "Bus",
|
||||
"boat": "Boat",
|
||||
"bike": "Bike",
|
||||
"walking": "Walking",
|
||||
"other": "Other"
|
||||
},
|
||||
"transportation_edit_success": "Transportation edited successfully!",
|
||||
"edit_transportation": "Edit Transportation",
|
||||
"start": "Start",
|
||||
"date_and_time": "Date & Time"
|
||||
},
|
||||
"search": {
|
||||
"adventurelog_results": "AdventureLog Results",
|
||||
"public_adventures": "Public Adventures",
|
||||
"online_results": "Online Results"
|
||||
},
|
||||
"map": {
|
||||
"view_details": "View Details",
|
||||
"adventure_map": "Adventure Map",
|
||||
"map_options": "Map Options",
|
||||
"show_visited_regions": "Show Visited Regions",
|
||||
"add_adventure_at_marker": "Add New Adventure at Marker",
|
||||
"clear_marker": "Clear Marker",
|
||||
"add_adventure": "Add New Adventure"
|
||||
},
|
||||
"share": {
|
||||
"shared": "Shared",
|
||||
"with": "with",
|
||||
"unshared": "Unshared",
|
||||
"share_desc": "Share this collection with other users.",
|
||||
"shared_with": "Shared With",
|
||||
"no_users_shared": "No users shared with",
|
||||
"not_shared_with": "Not Shared With",
|
||||
"no_shared_found": "No collections found that are shared with you.",
|
||||
"set_public": "In order to allow users to share with you, you need your profile set to public.",
|
||||
"go_to_settings": "Go to settings"
|
||||
},
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"de": "German",
|
||||
"es": "Spanish",
|
||||
"fr": "French",
|
||||
"it": "Italian",
|
||||
"nl": "Dutch",
|
||||
"sv": "Swedish",
|
||||
"zh": "Chinese",
|
||||
"pl": "Polish"
|
||||
},
|
||||
"profile": {
|
||||
"member_since": "Member since",
|
||||
"user_stats": "User Stats",
|
||||
"visited_countries": "Visited Countries",
|
||||
"visited_regions": "Visited Regions"
|
||||
},
|
||||
"categories": {
|
||||
"manage_categories": "Manage Categories",
|
||||
"no_categories_found": "No categories found.",
|
||||
"edit_category": "Edit Category",
|
||||
"icon": "Icon",
|
||||
"update_after_refresh": "The adventure cards will be updated once you refresh the page.",
|
||||
"select_category": "Select Category",
|
||||
"category_name": "Category Name"
|
||||
}
|
||||
"navbar": {
|
||||
"adventures": "Adventures",
|
||||
"collections": "Collections",
|
||||
"worldtravel": "World Travel",
|
||||
"map": "Map",
|
||||
"users": "Users",
|
||||
"search": "Search",
|
||||
"profile": "Profile",
|
||||
"greeting": "Hi",
|
||||
"my_adventures": "My Adventures",
|
||||
"my_tags": "My Tags",
|
||||
"tag": "Tag",
|
||||
"shared_with_me": "Shared With Me",
|
||||
"settings": "Settings",
|
||||
"logout": "Logout",
|
||||
"about": "About AdventureLog",
|
||||
"documentation": "Documentation",
|
||||
"discord": "Discord",
|
||||
"language_selection": "Language",
|
||||
"support": "Support",
|
||||
"calendar": "Calendar",
|
||||
"theme_selection": "Theme Selection",
|
||||
"themes": {
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"night": "Night",
|
||||
"forest": "Forest",
|
||||
"aestheticLight": "Aesthetic Light",
|
||||
"aestheticDark": "Aesthetic Dark",
|
||||
"aqua": "Aqua",
|
||||
"northernLights": "Northern Lights"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"about": "About",
|
||||
"license": "Licensed under the GPL-3.0 License.",
|
||||
"source_code": "Source Code",
|
||||
"message": "Made with ❤️ in the United States.",
|
||||
"oss_attributions": "Open Source Attributions",
|
||||
"nominatim_1": "Location Search and Geocoding is provided by",
|
||||
"nominatim_2": "Their data is liscensed under the ODbL license.",
|
||||
"other_attributions": "Additional attributions can be found in the README file.",
|
||||
"close": "Close"
|
||||
},
|
||||
"home": {
|
||||
"hero_1": "Discover the World's Most Thrilling Adventures",
|
||||
"hero_2": "Discover and plan your next adventure with AdventureLog. Explore breathtaking destinations, create custom itineraries, and stay connected on the go.",
|
||||
"go_to": "Go To AdventureLog",
|
||||
"key_features": "Key Features",
|
||||
"desc_1": "Discover, Plan, and Explore with Ease",
|
||||
"desc_2": "AdventureLog is designed to simplify your journey, providing you with the tools and resources to plan, pack, and navigate your next unforgettable adventure.",
|
||||
"feature_1": "Travel Log",
|
||||
"feature_1_desc": "Keep track of your adventures with a personalized travel log and share your experiences with friends and family.",
|
||||
"feature_2": "Trip Planning",
|
||||
"feature_2_desc": "Easily create custom itineraries and get a day-by-day breakdown of your trip.",
|
||||
"feature_3": "Travel Map",
|
||||
"feature_3_desc": "View your travels throughout the world with an interactive map and explore new destinations."
|
||||
},
|
||||
"adventures": {
|
||||
"collection_remove_success": "Adventure removed from collection successfully!",
|
||||
"collection_remove_error": "Error removing adventure from collection",
|
||||
"collection_link_success": "Adventure linked to collection successfully!",
|
||||
"no_image_found": "No image found",
|
||||
"collection_link_error": "Error linking adventure to collection",
|
||||
"adventure_delete_confirm": "Are you sure you want to delete this adventure? This action cannot be undone.",
|
||||
"open_details": "Open Details",
|
||||
"edit_adventure": "Edit Adventure",
|
||||
"remove_from_collection": "Remove from Collection",
|
||||
"add_to_collection": "Add to Collection",
|
||||
"delete": "Delete",
|
||||
"not_found": "Adventure not found",
|
||||
"not_found_desc": "The adventure you were looking for could not be found. Please try a different adventure or check back later.",
|
||||
"homepage": "Homepage",
|
||||
"adventure_details": "Adventure Details",
|
||||
"collection": "Collection",
|
||||
"adventure_type": "Adventure Type",
|
||||
"longitude": "Longitude",
|
||||
"latitude": "Latitude",
|
||||
"visit": "Visit",
|
||||
"visits": "Visits",
|
||||
"create_new": "Create New...",
|
||||
"adventure": "Adventure",
|
||||
"count_txt": "results matching your search",
|
||||
"sort": "Sort",
|
||||
"order_by": "Order By",
|
||||
"order_direction": "Order Direction",
|
||||
"ascending": "Ascending",
|
||||
"descending": "Descending",
|
||||
"updated": "Updated",
|
||||
"name": "Name",
|
||||
"date": "Date",
|
||||
"activity_types": "Activity Types",
|
||||
"tags": "Tags",
|
||||
"add_a_tag": "Add a tag",
|
||||
"date_constrain": "Constrain to collection dates",
|
||||
"rating": "Rating",
|
||||
"my_images": "My Images",
|
||||
"add_an_activity": "Add an activity",
|
||||
"no_images": "No Images",
|
||||
"upload_images_here": "Upload images here",
|
||||
"share_adventure": "Share this Adventure!",
|
||||
"copy_link": "Copy Link",
|
||||
"image": "Image",
|
||||
"upload_image": "Upload Image",
|
||||
"url": "URL",
|
||||
"fetch_image": "Fetch Image",
|
||||
"wikipedia": "Wikipedia",
|
||||
"add_notes": "Add notes",
|
||||
"warning": "Warning",
|
||||
"my_adventures": "My Adventures",
|
||||
"no_linkable_adventures": "No adventures found that can be linked to this collection.",
|
||||
"add": "Add",
|
||||
"save_next": "Save & Next",
|
||||
"end_date": "End Date",
|
||||
"my_visits": "My Visits",
|
||||
"start_date": "Start Date",
|
||||
"remove": "Remove",
|
||||
"location": "Location",
|
||||
"search_for_location": "Search for a location",
|
||||
"clear_map": "Clear map",
|
||||
"search_results": "Searh results",
|
||||
"no_results": "No results found",
|
||||
"wiki_desc": "Pulls excerpt from Wikipedia article matching the name of the adventure.",
|
||||
"generate_desc": "Generate Description",
|
||||
"public_adventure": "Public Adventure",
|
||||
"location_information": "Location Information",
|
||||
"link": "Link",
|
||||
"links": "Links",
|
||||
"description": "Description",
|
||||
"sources": "Sources",
|
||||
"collection_adventures": "Include Collection Adventures",
|
||||
"filter": "Filter",
|
||||
"category_filter": "Category Filter",
|
||||
"category": "Category",
|
||||
"select_adventure_category": "Select Adventure Category",
|
||||
"clear": "Clear",
|
||||
"my_collections": "My Collections",
|
||||
"open_filters": "Open Filters",
|
||||
"close_filters": "Close Filters",
|
||||
"archived_collections": "Archived Collections",
|
||||
"share": "Share",
|
||||
"private": "Private",
|
||||
"public": "Public",
|
||||
"archived": "Archived",
|
||||
"edit_collection": "Edit Collection",
|
||||
"unarchive": "Unarchive",
|
||||
"archive": "Archive",
|
||||
"no_collections_found": "No collections found to add this adventure to.",
|
||||
"not_visited": "Not Visited",
|
||||
"archived_collection_message": "Collection archived successfully!",
|
||||
"unarchived_collection_message": "Collection unarchived successfully!",
|
||||
"delete_collection_success": "Collection deleted successfully!",
|
||||
"delete_collection_warning": "Are you sure you want to delete this collection? This will also delete all of the linked adventures. This action cannot be undone.",
|
||||
"cancel": "Cancel",
|
||||
"delete_collection": "Delete Collection",
|
||||
"delete_adventure": "Delete Adventure",
|
||||
"adventure_delete_success": "Adventure deleted successfully!",
|
||||
"visited": "Visited",
|
||||
"planned": "Planned",
|
||||
"duration": "Duration",
|
||||
"all": "All",
|
||||
"image_removed_success": "Image removed successfully!",
|
||||
"image_removed_error": "Error removing image",
|
||||
"no_image_url": "No image found at that URL.",
|
||||
"image_upload_success": "Image uploaded successfully!",
|
||||
"image_upload_error": "Error uploading image",
|
||||
"dates": "Dates",
|
||||
"wiki_image_error": "Error fetching image from Wikipedia",
|
||||
"start_before_end_error": "Start date must be before end date",
|
||||
"activity": "Activity",
|
||||
"actions": "Actions",
|
||||
"no_end_date": "Please enter an end date",
|
||||
"see_adventures": "See Adventures",
|
||||
"image_fetch_failed": "Failed to fetch image",
|
||||
"no_location": "Please enter a location",
|
||||
"no_start_date": "Please enter a start date",
|
||||
"no_description_found": "No description found",
|
||||
"adventure_created": "Adventure created",
|
||||
"adventure_create_error": "Failed to create adventure",
|
||||
"adventure_updated": "Adventure updated",
|
||||
"adventure_update_error": "Failed to update adventure",
|
||||
"set_to_pin": "Set to Pin",
|
||||
"category_fetch_error": "Error fetching categories",
|
||||
"new_adventure": "New Adventure",
|
||||
"basic_information": "Basic Information",
|
||||
"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?",
|
||||
"mark_visited": "Mark Visited",
|
||||
"error_updating_regions": "Error updating regions",
|
||||
"regions_updated": "regions updated",
|
||||
"visited_region_check": "Visited Region Check",
|
||||
"visited_region_check_desc": "By selecting this, the server will check all of your visited adventures and mark the regions they are located in as visited in world travel.",
|
||||
"update_visited_regions": "Update Visited Regions",
|
||||
"update_visited_regions_disclaimer": "This may take a while depending on the number of adventures you have visited.",
|
||||
"link_new": "Link New...",
|
||||
"add_new": "Add New...",
|
||||
"transportation": "Transportation",
|
||||
"note": "Note",
|
||||
"checklist": "Checklist",
|
||||
"collection_archived": "This collection has been archived.",
|
||||
"visit_link": "Visit Link",
|
||||
"collection_completed": "You've completed this collection!",
|
||||
"collection_stats": "Collection Stats",
|
||||
"keep_exploring": "Keep Exploring!",
|
||||
"linked_adventures": "Linked Adventures",
|
||||
"notes": "Notes",
|
||||
"checklists": "Checklists",
|
||||
"transportations": "Transportations",
|
||||
"adventure_calendar": "Adventure Calendar",
|
||||
"day": "Day",
|
||||
"itineary_by_date": "Itinerary by Date",
|
||||
"nothing_planned": "Nothing planned for this day. Enjoy the journey!",
|
||||
"copied_to_clipboard": "Copied to clipboard!",
|
||||
"copy_failed": "Copy failed",
|
||||
"show": "Show",
|
||||
"hide": "Hide",
|
||||
"emoji_picker": "Emoji Picker",
|
||||
"download_calendar": "Download Calendar",
|
||||
"days": "days",
|
||||
"activities": {
|
||||
"general": "General 🌍",
|
||||
"outdoor": "Outdoor 🏞️",
|
||||
"lodging": "Lodging 🛌",
|
||||
"dining": "Dining 🍽️",
|
||||
"activity": "Activity 🏄",
|
||||
"attraction": "Attraction 🎢",
|
||||
"shopping": "Shopping 🛍️",
|
||||
"nightlife": "Nightlife 🌃",
|
||||
"event": "Event 🎉",
|
||||
"transportation": "Transportation 🚗",
|
||||
"culture": "Culture 🎭",
|
||||
"water_sports": "Water Sports 🚤",
|
||||
"hiking": "Hiking 🥾",
|
||||
"wildlife": "Wildlife 🦒",
|
||||
"historical_sites": "Historical Sites 🏛️",
|
||||
"music_concerts": "Music & Concerts 🎶",
|
||||
"fitness": "Fitness 🏋️",
|
||||
"art_museums": "Art & Museums 🎨",
|
||||
"festivals": "Festivals 🎪",
|
||||
"spiritual_journeys": "Spiritual Journeys 🧘♀️",
|
||||
"volunteer_work": "Volunteer Work 🤝",
|
||||
"other": "Other"
|
||||
}
|
||||
},
|
||||
"worldtravel": {
|
||||
"country_list": "Country List",
|
||||
"num_countries": "countries found",
|
||||
"all": "All",
|
||||
"partially_visited": "Partially Visited",
|
||||
"not_visited": "Not Visited",
|
||||
"completely_visited": "Completely Visited",
|
||||
"all_subregions": "All Subregions",
|
||||
"clear_search": "Clear Search",
|
||||
"no_countries_found": "No countries found"
|
||||
},
|
||||
"auth": {
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"forgot_password": "Forgot Password?",
|
||||
"signup": "Signup",
|
||||
"login_error": "Unable to login with the provided credentials.",
|
||||
"login": "Login",
|
||||
"email": "Email",
|
||||
"first_name": "First Name",
|
||||
"last_name": "Last Name",
|
||||
"confirm_password": "Confirm Password",
|
||||
"registration_disabled": "Registration is currently disabled.",
|
||||
"profile_picture": "Profile Picture",
|
||||
"public_profile": "Public Profile",
|
||||
"public_tooltip": "With a public profile, users can share collections with you and view your profile on the users page.",
|
||||
"email_required": "Email is required",
|
||||
"new_password": "New Password",
|
||||
"both_passwords_required": "Both passwords are required",
|
||||
"reset_failed": "Failed to reset password"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "No users found with public profiles."
|
||||
},
|
||||
"settings": {
|
||||
"update_error": "Error updating settings",
|
||||
"update_success": "Settings updated successfully!",
|
||||
"settings_page": "Settings Page",
|
||||
"account_settings": "User Account Settings",
|
||||
"update": "Update",
|
||||
"password_change": "Change Password",
|
||||
"new_password": "New Password",
|
||||
"confirm_new_password": "Confirm New Password",
|
||||
"email_change": "Change Email",
|
||||
"current_email": "Current Email",
|
||||
"no_email_set": "No email set",
|
||||
"new_email": "New Email",
|
||||
"change_password": "Change Password",
|
||||
"login_redir": "You will then be redirected to the login page.",
|
||||
"token_required": "Token and UID are required for password reset.",
|
||||
"reset_password": "Reset Password",
|
||||
"possible_reset": "If the email address you provided is associated with an account, you will receive an email with instructions to reset your password!",
|
||||
"missing_email": "Please enter an email address",
|
||||
"submit": "Submit",
|
||||
"password_does_not_match": "Passwords do not match",
|
||||
"password_is_required": "Password is required",
|
||||
"invalid_token": "Token is invalid or has expired",
|
||||
"about_this_background": "About this background",
|
||||
"photo_by": "Photo by",
|
||||
"join_discord": "Join the Discord",
|
||||
"join_discord_desc": "to share your own photos. Post them in the #travel-share channel.",
|
||||
"current_password": "Current Password",
|
||||
"change_password_error": "Unable to change password. Invalid current password or invalid new password.",
|
||||
"password_change_lopout_warning": "You will be logged out after changing your password.",
|
||||
"generic_error": "An error occurred while processing your request.",
|
||||
"email_removed": "Email removed successfully!",
|
||||
"email_removed_error": "Error removing email",
|
||||
"verify_email_success": "Email verification sent successfully!",
|
||||
"verify_email_error": "Error verifying email. Try again in a few minutes.",
|
||||
"email_added": "Email added successfully!",
|
||||
"email_added_error": "Error adding email",
|
||||
"email_set_primary": "Email set as primary successfully!",
|
||||
"email_set_primary_error": "Error setting email as primary",
|
||||
"verified": "Verified",
|
||||
"primary": "Primary",
|
||||
"not_verified": "Not Verified",
|
||||
"make_primary": "Make Primary",
|
||||
"verify": "Verify",
|
||||
"no_emai_set": "No email set",
|
||||
"error_change_password": "Error changing password. Please check your current password and try again.",
|
||||
"mfa_disabled": "Multi-factor authentication disabled successfully!",
|
||||
"mfa_page_title": "Multi-factor Authentication",
|
||||
"enable_mfa": "Enable MFA",
|
||||
"disable_mfa": "Disable MFA",
|
||||
"mfa_not_enabled": "MFA is not enabled",
|
||||
"mfa_enabled": "Multi-factor authentication enabled successfully!",
|
||||
"copy": "Copy",
|
||||
"recovery_codes": "Recovery Codes",
|
||||
"recovery_codes_desc": "These are your recovery codes. Keep them safe. You will not be able to see them again.",
|
||||
"reset_session_error": "Please logout and back in to refresh your session and try again.",
|
||||
"authenticator_code": "Authenticator Code",
|
||||
"email_verified": "Email verified successfully!",
|
||||
"email_verified_success": "Your email has been verified. You can now log in.",
|
||||
"email_verified_error": "Error verifying email",
|
||||
"email_verified_erorr_desc": "Your email could not be verified. Please try again.",
|
||||
"invalid_code": "Invalid MFA code",
|
||||
"invalid_credentials": "Invalid username or password",
|
||||
"mfa_required": "Multi-factor authentication is required",
|
||||
"required": "This field is required",
|
||||
"add_email_blocked": "You cannot add an email address to an account protected by two-factor authentication.",
|
||||
"duplicate_email": "This email address is already in use.",
|
||||
"csrf_failed": "Failed to fetch CSRF token",
|
||||
"email_taken": "This email address is already in use.",
|
||||
"username_taken": "This username is already in use."
|
||||
},
|
||||
"collection": {
|
||||
"collection_created": "Collection created successfully!",
|
||||
"error_creating_collection": "Error creating collection",
|
||||
"new_collection": "New Collection",
|
||||
"create": "Create",
|
||||
"collection_edit_success": "Collection edited successfully!",
|
||||
"error_editing_collection": "Error editing collection",
|
||||
"edit_collection": "Edit Collection"
|
||||
},
|
||||
"notes": {
|
||||
"note_deleted": "Note deleted successfully!",
|
||||
"note_delete_error": "Error deleting note",
|
||||
"open": "Open",
|
||||
"failed_to_save": "Failed to save note",
|
||||
"note_editor": "Note Editor",
|
||||
"editing_note": "Editing note",
|
||||
"content": "Content",
|
||||
"save": "Save",
|
||||
"note_public": "This note is public because it is in a public collection.",
|
||||
"add_a_link": "Add a link",
|
||||
"invalid_url": "Invalid URL"
|
||||
},
|
||||
"checklist": {
|
||||
"checklist_deleted": "Checklist deleted successfully!",
|
||||
"checklist_delete_error": "Error deleting checklist",
|
||||
"failed_to_save": "Failed to save checklist",
|
||||
"checklist_editor": "Checklist Editor",
|
||||
"editing_checklist": "Editing checklist",
|
||||
"item": "Item",
|
||||
"items": "Items",
|
||||
"add_item": "Add Item",
|
||||
"new_item": "New Item",
|
||||
"save": "Save",
|
||||
"checklist_public": "This checklist is public because it is in a public collection.",
|
||||
"item_cannot_be_empty": "Item cannot be empty",
|
||||
"item_already_exists": "Item already exists"
|
||||
},
|
||||
"transportation": {
|
||||
"transportation_deleted": "Transportation deleted successfully!",
|
||||
"transportation_delete_error": "Error deleting transportation",
|
||||
"provide_start_date": "Please provide a start date",
|
||||
"transport_type": "Transport Type",
|
||||
"type": "Type",
|
||||
"transportation_added": "Transportation added successfully!",
|
||||
"error_editing_transportation": "Error editing transportation",
|
||||
"new_transportation": "New Transportation",
|
||||
"date_time": "Start Date & Time",
|
||||
"end_date_time": "End Date & Time",
|
||||
"flight_number": "Flight Number",
|
||||
"from_location": "From Location",
|
||||
"to_location": "To Location",
|
||||
"edit": "Edit",
|
||||
"modes": {
|
||||
"car": "Car",
|
||||
"plane": "Plane",
|
||||
"train": "Train",
|
||||
"bus": "Bus",
|
||||
"boat": "Boat",
|
||||
"bike": "Bike",
|
||||
"walking": "Walking",
|
||||
"other": "Other"
|
||||
},
|
||||
"transportation_edit_success": "Transportation edited successfully!",
|
||||
"edit_transportation": "Edit Transportation",
|
||||
"start": "Start",
|
||||
"date_and_time": "Date & Time"
|
||||
},
|
||||
"search": {
|
||||
"adventurelog_results": "AdventureLog Results",
|
||||
"public_adventures": "Public Adventures",
|
||||
"online_results": "Online Results"
|
||||
},
|
||||
"map": {
|
||||
"view_details": "View Details",
|
||||
"adventure_map": "Adventure Map",
|
||||
"map_options": "Map Options",
|
||||
"show_visited_regions": "Show Visited Regions",
|
||||
"add_adventure_at_marker": "Add New Adventure at Marker",
|
||||
"clear_marker": "Clear Marker",
|
||||
"add_adventure": "Add New Adventure"
|
||||
},
|
||||
"share": {
|
||||
"shared": "Shared",
|
||||
"with": "with",
|
||||
"unshared": "Unshared",
|
||||
"share_desc": "Share this collection with other users.",
|
||||
"shared_with": "Shared With",
|
||||
"no_users_shared": "No users shared with",
|
||||
"not_shared_with": "Not Shared With",
|
||||
"no_shared_found": "No collections found that are shared with you.",
|
||||
"set_public": "In order to allow users to share with you, you need your profile set to public.",
|
||||
"go_to_settings": "Go to settings"
|
||||
},
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"de": "German",
|
||||
"es": "Spanish",
|
||||
"fr": "French",
|
||||
"it": "Italian",
|
||||
"nl": "Dutch",
|
||||
"sv": "Swedish",
|
||||
"zh": "Chinese",
|
||||
"pl": "Polish"
|
||||
},
|
||||
"profile": {
|
||||
"member_since": "Member since",
|
||||
"user_stats": "User Stats",
|
||||
"visited_countries": "Visited Countries",
|
||||
"visited_regions": "Visited Regions"
|
||||
},
|
||||
"categories": {
|
||||
"manage_categories": "Manage Categories",
|
||||
"no_categories_found": "No categories found.",
|
||||
"edit_category": "Edit Category",
|
||||
"icon": "Icon",
|
||||
"update_after_refresh": "The adventure cards will be updated once you refresh the page.",
|
||||
"select_category": "Select Category",
|
||||
"category_name": "Category Name"
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome_back": "Welcome back",
|
||||
"countries_visited": "Countries Visited",
|
||||
"total_adventures": "Total Adventures",
|
||||
"total_visited_regions": "Total Visited Regions",
|
||||
"recent_adventures": "Recent Adventures",
|
||||
"no_recent_adventures": "No recent adventures?",
|
||||
"add_some": "Why not start planning your next adventure? You can add a new adventure by clicking the button below."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,12 +23,14 @@
|
|||
"forest": "Bosque",
|
||||
"aqua": "Aqua",
|
||||
"aestheticDark": "Estética Oscura",
|
||||
"aestheticLight": "Luz estetica"
|
||||
"aestheticLight": "Luz estetica",
|
||||
"northernLights": "Aurora boreal"
|
||||
},
|
||||
"my_tags": "Mis etiquetas",
|
||||
"tag": "Etiqueta",
|
||||
"language_selection": "Idioma",
|
||||
"support": "Apoyo"
|
||||
"support": "Apoyo",
|
||||
"calendar": "Calendario"
|
||||
},
|
||||
"about": {
|
||||
"about": "Acerca de",
|
||||
|
@ -233,7 +235,14 @@
|
|||
"add_a_tag": "Agregar una etiqueta",
|
||||
"tags": "Etiquetas",
|
||||
"set_to_pin": "Establecer en Fijar",
|
||||
"category_fetch_error": "Error al buscar categorías"
|
||||
"category_fetch_error": "Error al buscar categorías",
|
||||
"copied_to_clipboard": "¡Copiado al portapapeles!",
|
||||
"copy_failed": "Copia fallida",
|
||||
"adventure_calendar": "Calendario de aventuras",
|
||||
"emoji_picker": "Selector de emojis",
|
||||
"hide": "Esconder",
|
||||
"show": "Espectáculo",
|
||||
"download_calendar": "Descargar Calendario"
|
||||
},
|
||||
"worldtravel": {
|
||||
"all": "Todo",
|
||||
|
@ -260,7 +269,11 @@
|
|||
"registration_disabled": "El registro está actualmente deshabilitado.",
|
||||
"profile_picture": "Foto de perfil",
|
||||
"public_profile": "Perfil público",
|
||||
"public_tooltip": "Con un perfil público, los usuarios pueden compartir colecciones con usted y ver su perfil en la página de usuarios."
|
||||
"public_tooltip": "Con un perfil público, los usuarios pueden compartir colecciones con usted y ver su perfil en la página de usuarios.",
|
||||
"email_required": "Se requiere correo electrónico",
|
||||
"both_passwords_required": "Se requieren ambas contraseñas",
|
||||
"new_password": "Nueva contraseña",
|
||||
"reset_failed": "No se pudo restablecer la contraseña"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "No se encontraron usuarios con perfiles públicos."
|
||||
|
@ -291,7 +304,50 @@
|
|||
"about_this_background": "Sobre este trasfondo",
|
||||
"join_discord": "Únete a la discordia",
|
||||
"join_discord_desc": "para compartir tus propias fotos. \nPublicarlos en el",
|
||||
"photo_by": "Foto por"
|
||||
"photo_by": "Foto por",
|
||||
"change_password_error": "No se puede cambiar la contraseña. \nContraseña actual no válida o contraseña nueva no válida.",
|
||||
"current_password": "Contraseña actual",
|
||||
"password_change_lopout_warning": "Se cerrará su sesión después de cambiar su contraseña.",
|
||||
"generic_error": "Se produjo un error al procesar su solicitud.",
|
||||
"email_added": "¡Correo electrónico agregado exitosamente!",
|
||||
"email_added_error": "Error al agregar correo electrónico",
|
||||
"email_removed": "¡El correo electrónico se eliminó correctamente!",
|
||||
"email_removed_error": "Error al eliminar el correo electrónico",
|
||||
"email_set_primary": "¡El correo electrónico se configuró como principal correctamente!",
|
||||
"email_set_primary_error": "Error al configurar el correo electrónico como principal",
|
||||
"make_primary": "Hacer primario",
|
||||
"no_emai_set": "No hay correo electrónico configurado",
|
||||
"not_verified": "No verificado",
|
||||
"primary": "Primario",
|
||||
"verified": "Verificado",
|
||||
"verify": "Verificar",
|
||||
"verify_email_error": "Error al verificar el correo electrónico. \nInténtalo de nuevo en unos minutos.",
|
||||
"verify_email_success": "¡La verificación por correo electrónico se envió correctamente!",
|
||||
"error_change_password": "Error al cambiar la contraseña. \nPor favor verifique su contraseña actual e inténtelo nuevamente.",
|
||||
"disable_mfa": "Deshabilitar MFA",
|
||||
"enable_mfa": "Habilitar MFA",
|
||||
"mfa_disabled": "¡La autenticación multifactor se deshabilitó correctamente!",
|
||||
"mfa_not_enabled": "MFA no está habilitado",
|
||||
"mfa_page_title": "Autenticación multifactor",
|
||||
"copy": "Copiar",
|
||||
"mfa_enabled": "¡La autenticación multifactor se habilitó correctamente!",
|
||||
"recovery_codes": "Códigos de recuperación",
|
||||
"recovery_codes_desc": "Estos son tus códigos de recuperación. \nMantenlos a salvo. \nNo podrás volver a verlos.",
|
||||
"reset_session_error": "Por favor cierre sesión y vuelva a iniciarla para actualizar su sesión e inténtelo nuevamente.",
|
||||
"authenticator_code": "Código de autenticación",
|
||||
"email_verified": "¡Correo electrónico verificado exitosamente!",
|
||||
"email_verified_erorr_desc": "Su correo electrónico no pudo ser verificado. \nPor favor inténtalo de nuevo.",
|
||||
"email_verified_error": "Error al verificar el correo electrónico",
|
||||
"email_verified_success": "Su correo electrónico ha sido verificado. \nAhora puedes iniciar sesión.",
|
||||
"invalid_code": "Código MFA no válido",
|
||||
"invalid_credentials": "Nombre de usuario o contraseña no válidos",
|
||||
"mfa_required": "Se requiere autenticación multifactor",
|
||||
"add_email_blocked": "No puede agregar una dirección de correo electrónico a una cuenta protegida por autenticación de dos factores.",
|
||||
"required": "Este campo es obligatorio",
|
||||
"csrf_failed": "No se pudo recuperar el token CSRF",
|
||||
"duplicate_email": "Esta dirección de correo electrónico ya está en uso.",
|
||||
"email_taken": "Esta dirección de correo electrónico ya está en uso.",
|
||||
"username_taken": "Este nombre de usuario ya está en uso."
|
||||
},
|
||||
"checklist": {
|
||||
"add_item": "Agregar artículo",
|
||||
|
@ -411,5 +467,14 @@
|
|||
"no_categories_found": "No se encontraron categorías.",
|
||||
"select_category": "Seleccionar categoría",
|
||||
"update_after_refresh": "Las tarjetas de aventuras se actualizarán una vez que actualices la página."
|
||||
},
|
||||
"dashboard": {
|
||||
"add_some": "¿Por qué no empezar a planificar tu próxima aventura? \nPuedes agregar una nueva aventura haciendo clic en el botón de abajo.",
|
||||
"countries_visited": "Países visitados",
|
||||
"no_recent_adventures": "¿Sin aventuras recientes?",
|
||||
"recent_adventures": "Aventuras recientes",
|
||||
"total_adventures": "Aventuras totales",
|
||||
"total_visited_regions": "Total de regiones visitadas",
|
||||
"welcome_back": "Bienvenido de nuevo"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,7 +188,14 @@
|
|||
"add_a_tag": "Ajouter une balise",
|
||||
"tags": "Balises",
|
||||
"set_to_pin": "Définir sur Épingler",
|
||||
"category_fetch_error": "Erreur lors de la récupération des catégories"
|
||||
"category_fetch_error": "Erreur lors de la récupération des catégories",
|
||||
"copied_to_clipboard": "Copié dans le presse-papier !",
|
||||
"copy_failed": "Échec de la copie",
|
||||
"adventure_calendar": "Calendrier d'aventure",
|
||||
"emoji_picker": "Sélecteur d'émoticônes",
|
||||
"hide": "Cacher",
|
||||
"show": "Montrer",
|
||||
"download_calendar": "Télécharger le calendrier"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
|
||||
|
@ -226,14 +233,16 @@
|
|||
"aqua": "Aqua",
|
||||
"dark": "Sombre",
|
||||
"aestheticDark": "Esthétique sombre",
|
||||
"aestheticLight": "Lumière esthétique"
|
||||
"aestheticLight": "Lumière esthétique",
|
||||
"northernLights": "Aurores boréales"
|
||||
},
|
||||
"users": "Utilisateurs",
|
||||
"worldtravel": "Voyage dans le monde",
|
||||
"my_tags": "Mes balises",
|
||||
"tag": "Étiqueter",
|
||||
"language_selection": "Langue",
|
||||
"support": "Soutien"
|
||||
"support": "Soutien",
|
||||
"calendar": "Calendrier"
|
||||
},
|
||||
"auth": {
|
||||
"confirm_password": "Confirmez le mot de passe",
|
||||
|
@ -249,7 +258,11 @@
|
|||
"username": "Nom d'utilisateur",
|
||||
"profile_picture": "Photo de profil",
|
||||
"public_profile": "Profil public",
|
||||
"public_tooltip": "Avec un profil public, les utilisateurs peuvent partager des collections avec vous et afficher votre profil sur la page des utilisateurs."
|
||||
"public_tooltip": "Avec un profil public, les utilisateurs peuvent partager des collections avec vous et afficher votre profil sur la page des utilisateurs.",
|
||||
"email_required": "L'e-mail est requis",
|
||||
"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"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "Aucun utilisateur trouvé avec des profils publics."
|
||||
|
@ -291,7 +304,50 @@
|
|||
"about_this_background": "À propos de ce contexte",
|
||||
"join_discord": "Rejoignez le Discord",
|
||||
"join_discord_desc": "pour partager vos propres photos. \nPostez-les dans le",
|
||||
"photo_by": "Photo par"
|
||||
"photo_by": "Photo par",
|
||||
"change_password_error": "Impossible de changer le mot de passe. \nMot de passe actuel invalide ou nouveau mot de passe invalide.",
|
||||
"current_password": "Mot de passe actuel",
|
||||
"password_change_lopout_warning": "Vous serez déconnecté après avoir modifié votre mot de passe.",
|
||||
"authenticator_code": "Code d'authentification",
|
||||
"copy": "Copie",
|
||||
"disable_mfa": "Désactiver MFA",
|
||||
"email_added": "E-mail ajouté avec succès !",
|
||||
"email_added_error": "Erreur lors de l'ajout de l'e-mail",
|
||||
"email_removed": "E-mail supprimé avec succès !",
|
||||
"email_removed_error": "Erreur lors de la suppression de l'e-mail",
|
||||
"email_set_primary": "E-mail défini comme principal avec succès !",
|
||||
"email_set_primary_error": "Erreur lors de la définition de l'adresse e-mail comme adresse principale",
|
||||
"email_verified": "E-mail vérifié avec succès !",
|
||||
"email_verified_erorr_desc": "Votre email n'a pas pu être vérifié. \nVeuillez réessayer.",
|
||||
"email_verified_error": "Erreur lors de la vérification de l'e-mail",
|
||||
"email_verified_success": "Votre email a été vérifié. \nVous pouvez maintenant vous connecter.",
|
||||
"enable_mfa": "Activer l'authentification multifacteur",
|
||||
"error_change_password": "Erreur lors du changement de mot de passe. \nVeuillez vérifier votre mot de passe actuel et réessayer.",
|
||||
"generic_error": "Une erreur s'est produite lors du traitement de votre demande.",
|
||||
"invalid_code": "Code MFA invalide",
|
||||
"invalid_credentials": "Nom d'utilisateur ou mot de passe invalide",
|
||||
"make_primary": "Rendre primaire",
|
||||
"mfa_disabled": "Authentification multifacteur désactivée avec succès !",
|
||||
"mfa_enabled": "Authentification multifacteur activée avec succès !",
|
||||
"mfa_not_enabled": "MFA n'est pas activé",
|
||||
"mfa_page_title": "Authentification multifacteur",
|
||||
"mfa_required": "Une authentification multifacteur est requise",
|
||||
"no_emai_set": "Aucune adresse e-mail définie",
|
||||
"not_verified": "Non vérifié",
|
||||
"primary": "Primaire",
|
||||
"recovery_codes": "Codes de récupération",
|
||||
"recovery_codes_desc": "Ce sont vos codes de récupération. \nGardez-les en sécurité. \nVous ne pourrez plus les revoir.",
|
||||
"reset_session_error": "Veuillez vous déconnecter, puis vous reconnecter pour actualiser votre session et réessayer.",
|
||||
"verified": "Vérifié",
|
||||
"verify": "Vérifier",
|
||||
"verify_email_error": "Erreur lors de la vérification de l'e-mail. \nRéessayez dans quelques minutes.",
|
||||
"verify_email_success": "Vérification par e-mail envoyée avec succès !",
|
||||
"add_email_blocked": "Vous ne pouvez pas ajouter une adresse e-mail à un compte protégé par une authentification à deux facteurs.",
|
||||
"required": "Ce champ est obligatoire",
|
||||
"csrf_failed": "Échec de la récupération du jeton CSRF",
|
||||
"duplicate_email": "Cette adresse e-mail est déjà utilisée.",
|
||||
"email_taken": "Cette adresse e-mail est déjà utilisée.",
|
||||
"username_taken": "Ce nom d'utilisateur est déjà utilisé."
|
||||
},
|
||||
"checklist": {
|
||||
"add_item": "Ajouter un article",
|
||||
|
@ -411,5 +467,14 @@
|
|||
"no_categories_found": "Aucune catégorie trouvée.",
|
||||
"select_category": "Sélectionnez une catégorie",
|
||||
"update_after_refresh": "Les cartes d'aventure seront mises à jour une fois que vous aurez actualisé la page."
|
||||
},
|
||||
"dashboard": {
|
||||
"add_some": "Pourquoi ne pas commencer à planifier votre prochaine aventure ? \nVous pouvez ajouter une nouvelle aventure en cliquant sur le bouton ci-dessous.",
|
||||
"countries_visited": "Pays visités",
|
||||
"no_recent_adventures": "Pas d'aventures récentes ?",
|
||||
"recent_adventures": "Aventures récentes",
|
||||
"total_adventures": "Aventures totales",
|
||||
"total_visited_regions": "Total des régions visitées",
|
||||
"welcome_back": "Content de te revoir"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,7 +188,14 @@
|
|||
"add_a_tag": "Aggiungi un'etichetta",
|
||||
"tags": "Tag",
|
||||
"set_to_pin": "Imposta su Blocca",
|
||||
"category_fetch_error": "Errore durante il recupero delle categorie"
|
||||
"category_fetch_error": "Errore durante il recupero delle categorie",
|
||||
"copied_to_clipboard": "Copiato negli appunti!",
|
||||
"copy_failed": "Copia non riuscita",
|
||||
"adventure_calendar": "Calendario delle avventure",
|
||||
"emoji_picker": "Selettore di emoji",
|
||||
"hide": "Nascondere",
|
||||
"show": "Spettacolo",
|
||||
"download_calendar": "Scarica Calendario"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Scopri, pianifica ed esplora con facilità",
|
||||
|
@ -226,14 +233,16 @@
|
|||
"light": "Leggero",
|
||||
"night": "Notte",
|
||||
"aestheticDark": "Estetico scuro",
|
||||
"aestheticLight": "Luce estetica"
|
||||
"aestheticLight": "Luce estetica",
|
||||
"northernLights": "Aurora boreale"
|
||||
},
|
||||
"users": "Utenti",
|
||||
"worldtravel": "Viaggio nel mondo",
|
||||
"my_tags": "I miei tag",
|
||||
"tag": "Etichetta",
|
||||
"language_selection": "Lingua",
|
||||
"support": "Supporto"
|
||||
"support": "Supporto",
|
||||
"calendar": "Calendario"
|
||||
},
|
||||
"auth": {
|
||||
"confirm_password": "Conferma password",
|
||||
|
@ -249,7 +258,11 @@
|
|||
"username": "Nome utente",
|
||||
"profile_picture": "Immagine del profilo",
|
||||
"public_profile": "Profilo pubblico",
|
||||
"public_tooltip": "Con un profilo pubblico, gli utenti possono condividere raccolte con te e visualizzare il tuo profilo nella pagina degli utenti."
|
||||
"public_tooltip": "Con un profilo pubblico, gli utenti possono condividere raccolte con te e visualizzare il tuo profilo nella pagina degli utenti.",
|
||||
"email_required": "L'e-mail è obbligatoria",
|
||||
"both_passwords_required": "Sono necessarie entrambe le password",
|
||||
"new_password": "Nuova parola d'ordine",
|
||||
"reset_failed": "Impossibile reimpostare la password"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "Nessun utente trovato con profili pubblici."
|
||||
|
@ -291,7 +304,50 @@
|
|||
"about_this_background": "A proposito di questo contesto",
|
||||
"join_discord": "Unisciti alla Discordia",
|
||||
"join_discord_desc": "per condividere le tue foto. \nPubblicateli in",
|
||||
"photo_by": "Foto di"
|
||||
"photo_by": "Foto di",
|
||||
"change_password_error": "Impossibile modificare la password. \nPassword attuale non valida o nuova password non valida.",
|
||||
"current_password": "password attuale",
|
||||
"password_change_lopout_warning": "Verrai disconnesso dopo aver modificato la password.",
|
||||
"authenticator_code": "Codice Autenticatore",
|
||||
"copy": "Copia",
|
||||
"disable_mfa": "Disabilita MFA",
|
||||
"email_added": "Email aggiunta con successo!",
|
||||
"email_added_error": "Errore durante l'aggiunta dell'e-mail",
|
||||
"email_removed": "Email rimossa con successo!",
|
||||
"email_removed_error": "Errore durante la rimozione dell'e-mail",
|
||||
"email_set_primary": "Email impostata come primaria con successo!",
|
||||
"email_set_primary_error": "Errore durante l'impostazione dell'e-mail come principale",
|
||||
"email_verified": "Email verificata con successo!",
|
||||
"email_verified_erorr_desc": "Non è stato possibile verificare la tua email. \nPer favore riprova.",
|
||||
"email_verified_error": "Errore durante la verifica dell'e-mail",
|
||||
"email_verified_success": "La tua email è stata verificata. \nOra puoi accedere.",
|
||||
"enable_mfa": "Abilita MFA",
|
||||
"error_change_password": "Errore durante la modifica della password. \nControlla la tua password attuale e riprova.",
|
||||
"generic_error": "Si è verificato un errore durante l'elaborazione della tua richiesta.",
|
||||
"invalid_code": "Codice MFA non valido",
|
||||
"invalid_credentials": "Nome utente o password non validi",
|
||||
"make_primary": "Rendi primario",
|
||||
"mfa_disabled": "Autenticazione a più fattori disabilitata correttamente!",
|
||||
"mfa_enabled": "Autenticazione a più fattori abilitata correttamente!",
|
||||
"mfa_not_enabled": "L'MFA non è abilitata",
|
||||
"mfa_page_title": "Autenticazione a più fattori",
|
||||
"mfa_required": "È richiesta l'autenticazione a più fattori",
|
||||
"no_emai_set": "Nessuna e-mail impostata",
|
||||
"not_verified": "Non verificato",
|
||||
"primary": "Primario",
|
||||
"recovery_codes": "Codici di ripristino",
|
||||
"recovery_codes_desc": "Questi sono i tuoi codici di ripristino. \nTeneteli al sicuro. \nNon potrai vederli più.",
|
||||
"reset_session_error": "Esci, effettua nuovamente l'accesso per aggiornare la sessione e riprova.",
|
||||
"verified": "Verificato",
|
||||
"verify_email_success": "Verifica email inviata con successo!",
|
||||
"verify": "Verificare",
|
||||
"verify_email_error": "Errore durante la verifica dell'e-mail. \nRiprova tra qualche minuto.",
|
||||
"add_email_blocked": "Non è possibile aggiungere un indirizzo email a un account protetto dall'autenticazione a due fattori.",
|
||||
"required": "Questo campo è obbligatorio",
|
||||
"csrf_failed": "Impossibile recuperare il token CSRF",
|
||||
"duplicate_email": "Questo indirizzo email è già in uso.",
|
||||
"email_taken": "Questo indirizzo email è già in uso.",
|
||||
"username_taken": "Questo nome utente è già in uso."
|
||||
},
|
||||
"checklist": {
|
||||
"add_item": "Aggiungi articolo",
|
||||
|
@ -411,5 +467,14 @@
|
|||
"no_categories_found": "Nessuna categoria trovata.",
|
||||
"select_category": "Seleziona Categoria",
|
||||
"update_after_refresh": "Le carte avventura verranno aggiornate una volta aggiornata la pagina."
|
||||
},
|
||||
"dashboard": {
|
||||
"add_some": "Perché non iniziare a pianificare la tua prossima avventura? \nPuoi aggiungere una nuova avventura facendo clic sul pulsante in basso.",
|
||||
"countries_visited": "Paesi visitati",
|
||||
"no_recent_adventures": "Nessuna avventura recente?",
|
||||
"recent_adventures": "Avventure recenti",
|
||||
"total_adventures": "Avventure totali",
|
||||
"total_visited_regions": "Totale regioni visitate",
|
||||
"welcome_back": "Bentornato"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"about": {
|
||||
"about": "Over",
|
||||
"close": "Dichtbij",
|
||||
"close": "Sluiten",
|
||||
"license": "Gelicentieerd onder de GPL-3.0-licentie.",
|
||||
"message": "Gemaakt met ❤️ in de Verenigde Staten.",
|
||||
"message": "Met ❤️ gemaakt in de Verenigde Staten.",
|
||||
"nominatim_1": "Locatie zoeken en geocodering wordt verzorgd door",
|
||||
"nominatim_2": "Hun gegevens zijn in licentie gegeven onder de ODbL-licentie.",
|
||||
"oss_attributions": "Open source-attributies",
|
||||
"other_attributions": "Aanvullende toeschrijvingen zijn te vinden in het README-bestand.",
|
||||
"oss_attributions": "Open source gebruik",
|
||||
"other_attributions": "Aanvullende vermeldingen zijn te vinden in het README-bestand.",
|
||||
"source_code": "Broncode"
|
||||
},
|
||||
"adventures": {
|
||||
|
@ -33,38 +33,38 @@
|
|||
"transportation": "Vervoer 🚗",
|
||||
"volunteer_work": "Vrijwilligerswerk 🤝",
|
||||
"water_sports": "Watersport 🚤",
|
||||
"wildlife": "Dieren in het wild 🦒"
|
||||
"wildlife": "Wilde dieren 🦒"
|
||||
},
|
||||
"add_to_collection": "Toevoegen aan collectie",
|
||||
"adventure": "Avontuur",
|
||||
"adventure_delete_confirm": "Weet je zeker dat je dit avontuur wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
|
||||
"adventure_details": "Avontuurdetails",
|
||||
"adventure_type": "Avontuurtype",
|
||||
"archive": "Archief",
|
||||
"archive": "Archiveer",
|
||||
"archived": "Gearchiveerd",
|
||||
"archived_collection_message": "Collectie succesvol gearchiveerd!",
|
||||
"archived_collections": "Gearchiveerde collecties",
|
||||
"ascending": "Oplopend",
|
||||
"cancel": "Annuleren",
|
||||
"category_filter": "Categoriefilter",
|
||||
"clear": "Duidelijk",
|
||||
"close_filters": "Sluit Filters",
|
||||
"collection": "Verzameling",
|
||||
"clear": "Leeg maken",
|
||||
"close_filters": "Sluit filters",
|
||||
"collection": "Collectie",
|
||||
"collection_adventures": "Inclusief collectie-avonturen",
|
||||
"collection_link_error": "Fout bij het koppelen van avontuur aan collectie",
|
||||
"collection_link_success": "Avontuur succesvol gekoppeld aan collectie!",
|
||||
"collection_remove_error": "Fout bij verwijderen van avontuur uit verzameling",
|
||||
"collection_remove_error": "Fout bij verwijderen van avontuur uit collectie",
|
||||
"collection_remove_success": "Avontuur is succesvol uit de collectie verwijderd!",
|
||||
"count_txt": "resultaten die overeenkomen met uw zoekopdracht",
|
||||
"create_new": "Maak nieuwe...",
|
||||
"date": "Datum",
|
||||
"delete": "Verwijderen",
|
||||
"delete_collection": "Verzameling verwijderen",
|
||||
"delete_collection": "Collectie verwijderen",
|
||||
"delete_collection_success": "Collectie succesvol verwijderd!",
|
||||
"delete_collection_warning": "Weet u zeker dat u deze verzameling wilt verwijderen? \nHiermee worden ook alle gekoppelde avonturen verwijderd. \nDeze actie kan niet ongedaan worden gemaakt.",
|
||||
"delete_collection_warning": "Weet u zeker dat u deze collectie wilt verwijderen? \nHiermee worden ook alle gekoppelde avonturen verwijderd. \nDeze actie kan niet ongedaan worden gemaakt.",
|
||||
"descending": "Aflopend",
|
||||
"edit_adventure": "Avontuur bewerken",
|
||||
"edit_collection": "Verzameling bewerken",
|
||||
"edit_collection": "Collectie bewerken",
|
||||
"filter": "Filter",
|
||||
"homepage": "Startpagina",
|
||||
"latitude": "Breedte",
|
||||
|
@ -73,21 +73,21 @@
|
|||
"name": "Naam",
|
||||
"no_image_found": "Geen afbeelding gevonden",
|
||||
"not_found": "Avontuur niet gevonden",
|
||||
"not_found_desc": "Het avontuur waar je naar op zoek was, kon niet gevonden worden. \nProbeer een ander avontuur of kom later nog eens terug.",
|
||||
"not_found_desc": "Het avontuur waar je naar op zoek was, kon niet worden gevonden. \nProbeer een ander avontuur of kom later nog eens terug.",
|
||||
"open_details": "Details openen",
|
||||
"open_filters": "Filters openen",
|
||||
"order_by": "Bestel per",
|
||||
"order_direction": "Bestelrichting",
|
||||
"order_by": "Sorteer op",
|
||||
"order_direction": "Sorteervolgorde",
|
||||
"private": "Privé",
|
||||
"public": "Openbaar",
|
||||
"rating": "Beoordeling",
|
||||
"remove_from_collection": "Verwijderen uit collectie",
|
||||
"share": "Deel",
|
||||
"sort": "Soort",
|
||||
"sort": "Sorteer",
|
||||
"sources": "Bronnen",
|
||||
"unarchive": "Uit het archief halen",
|
||||
"unarchived_collection_message": "Collectie is succesvol gedearchiveerd!",
|
||||
"updated": "Bijgewerkt",
|
||||
"updated": "Gewijzigd",
|
||||
"visit": "Bezoek",
|
||||
"visits": "Bezoeken",
|
||||
"adventure_delete_success": "Avontuur succesvol verwijderd!",
|
||||
|
@ -108,14 +108,14 @@
|
|||
"activity_types": "Activiteitstypen",
|
||||
"add": "Toevoegen",
|
||||
"add_an_activity": "Voeg een activiteit toe",
|
||||
"add_notes": "Voeg notities toe",
|
||||
"adventure_create_error": "Kan geen avontuur creëren",
|
||||
"adventure_created": "Avontuur gecreëerd",
|
||||
"adventure_update_error": "Kan avontuur niet updaten",
|
||||
"adventure_updated": "Avontuur bijgewerkt",
|
||||
"add_notes": "Voeg opmerkingen toe",
|
||||
"adventure_create_error": "Kan geen avontuur aanmaken",
|
||||
"adventure_created": "Avontuur aangemaakt",
|
||||
"adventure_update_error": "Kan avontuur niet wijzigen",
|
||||
"adventure_updated": "Avontuur gewijzigd",
|
||||
"basic_information": "Basisinformatie",
|
||||
"category": "Categorie",
|
||||
"clear_map": "Duidelijke kaart",
|
||||
"clear_map": "Kaart leegmaken",
|
||||
"copy_link": "Kopieer link",
|
||||
"date_constrain": "Beperk u tot ophaaldata",
|
||||
"description": "Beschrijving",
|
||||
|
@ -137,7 +137,7 @@
|
|||
"no_start_date": "Voer een startdatum in",
|
||||
"public_adventure": "Openbaar avontuur",
|
||||
"remove": "Verwijderen",
|
||||
"save_next": "Redden",
|
||||
"save_next": "Opslaan & Volgende",
|
||||
"search_for_location": "Zoek een locatie",
|
||||
"search_results": "Zoekresultaten",
|
||||
"see_adventures": "Zie Avonturen",
|
||||
|
@ -152,27 +152,27 @@
|
|||
"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 updaten van regio's",
|
||||
"mark_visited": "Mark bezocht",
|
||||
"error_updating_regions": "Fout bij wijzigen van regio's",
|
||||
"mark_visited": "Markeer bezocht",
|
||||
"my_adventures": "Mijn avonturen",
|
||||
"no_adventures_found": "Geen avonturen gevonden",
|
||||
"no_collections_found": "Er zijn geen collecties gevonden waar dit avontuur aan kan worden toegevoegd.",
|
||||
"no_linkable_adventures": "Er zijn geen avonturen gevonden die aan deze collectie kunnen worden gekoppeld.",
|
||||
"not_visited": "Niet bezocht",
|
||||
"regions_updated": "regio's bijgewerkt",
|
||||
"update_visited_regions": "Update bezochte regio's",
|
||||
"update_visited_regions_disclaimer": "Dit kan even duren, afhankelijk van het aantal avonturen dat je hebt bezocht.",
|
||||
"visited_region_check": "Regiocheck bezocht",
|
||||
"visited_region_check_desc": "Door dit te selecteren, controleert de server al uw bezochte avonturen en markeert de regio's waarin ze zich bevinden als bezocht in de wereldreizen.",
|
||||
"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_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",
|
||||
"checklists": "Controlelijsten",
|
||||
"collection_archived": "Deze collectie is gearchiveerd.",
|
||||
"collection_completed": "Je hebt deze verzameling voltooid!",
|
||||
"collection_stats": "Verzamelstatistieken",
|
||||
"collection_completed": "Je hebt deze collectie voltooid!",
|
||||
"collection_stats": "Collectiestatistieken",
|
||||
"days": "dagen",
|
||||
"itineary_by_date": "Reisplan op datum",
|
||||
"keep_exploring": "Blijf verkennen!",
|
||||
"keep_exploring": "Blijf ontdekken!",
|
||||
"link_new": "Nieuwe link...",
|
||||
"linked_adventures": "Gekoppelde avonturen",
|
||||
"links": "Koppelingen",
|
||||
|
@ -182,13 +182,20 @@
|
|||
"notes": "Opmerkingen",
|
||||
"nothing_planned": "Niets gepland voor deze dag. \nGeniet van de reis!",
|
||||
"transportation": "Vervoer",
|
||||
"transportations": "Transporten",
|
||||
"transportations": "Vervoer",
|
||||
"visit_link": "Bezoek Link",
|
||||
"day": "Dag",
|
||||
"add_a_tag": "Voeg een label toe",
|
||||
"tags": "Labels",
|
||||
"set_to_pin": "Stel in op Vastzetten",
|
||||
"category_fetch_error": "Fout bij ophalen van categorieën"
|
||||
"set_to_pin": "Stel in op pin",
|
||||
"category_fetch_error": "Fout bij ophalen van categorieën",
|
||||
"copied_to_clipboard": "Gekopieerd naar klembord!",
|
||||
"copy_failed": "Kopiëren mislukt",
|
||||
"adventure_calendar": "Avonturenkalender",
|
||||
"emoji_picker": "Emoji-kiezer",
|
||||
"hide": "Verbergen",
|
||||
"show": "Show",
|
||||
"download_calendar": "Agenda downloaden"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Ontdek, plan en verken met gemak",
|
||||
|
@ -201,14 +208,14 @@
|
|||
"feature_3_desc": "Bekijk uw reizen over de hele wereld met een interactieve kaart en ontdek nieuwe bestemmingen.",
|
||||
"go_to": "Ga naar AdventureLog",
|
||||
"hero_2": "Ontdek en plan je volgende avontuur met AdventureLog. \nOntdek adembenemende bestemmingen, maak aangepaste reisroutes en blijf onderweg verbonden.",
|
||||
"hero_1": "Ontdek 's werelds meest opwindende avonturen",
|
||||
"hero_1": "ontdek 's werelds meest opwindende avonturen",
|
||||
"key_features": "Belangrijkste kenmerken"
|
||||
},
|
||||
"navbar": {
|
||||
"about": "Over AdventureLog",
|
||||
"adventures": "Avonturen",
|
||||
"collections": "Collecties",
|
||||
"discord": "Meningsverschil",
|
||||
"discord": "discord",
|
||||
"documentation": "Documentatie",
|
||||
"greeting": "Hoi",
|
||||
"logout": "Uitloggen",
|
||||
|
@ -218,7 +225,7 @@
|
|||
"search": "Zoekopdracht",
|
||||
"settings": "Instellingen",
|
||||
"shared_with_me": "Gedeeld met mij",
|
||||
"theme_selection": "Thema Selectie",
|
||||
"theme_selection": "Thema selectie",
|
||||
"themes": {
|
||||
"aqua": "Aqua",
|
||||
"dark": "Donker",
|
||||
|
@ -226,14 +233,16 @@
|
|||
"light": "Licht",
|
||||
"night": "Nacht",
|
||||
"aestheticDark": "Esthetisch donker",
|
||||
"aestheticLight": "Esthetisch licht"
|
||||
"aestheticLight": "Esthetisch licht",
|
||||
"northernLights": "Noorderlicht"
|
||||
},
|
||||
"users": "Gebruikers",
|
||||
"worldtravel": "Wereldreizen",
|
||||
"my_tags": "Mijn tags",
|
||||
"my_tags": "Mijn labels",
|
||||
"tag": "Label",
|
||||
"language_selection": "Taal",
|
||||
"support": "Steun"
|
||||
"support": "Steun",
|
||||
"calendar": "Kalender"
|
||||
},
|
||||
"auth": {
|
||||
"confirm_password": "Bevestig wachtwoord",
|
||||
|
@ -249,7 +258,11 @@
|
|||
"username": "Gebruikersnaam",
|
||||
"profile_picture": "Profielfoto",
|
||||
"public_profile": "Openbaar profiel",
|
||||
"public_tooltip": "Met een openbaar profiel kunnen gebruikers collecties met u delen en uw profiel bekijken op de gebruikerspagina."
|
||||
"public_tooltip": "Met een openbaar profiel kunnen gebruikers collecties met u delen en uw profiel bekijken op de gebruikerspagina.",
|
||||
"email_required": "E-mail is vereist",
|
||||
"both_passwords_required": "Beide wachtwoorden zijn vereist",
|
||||
"new_password": "Nieuw wachtwoord",
|
||||
"reset_failed": "Kan het wachtwoord niet opnieuw instellen"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "Er zijn geen gebruikers gevonden met openbare profielen."
|
||||
|
@ -266,18 +279,18 @@
|
|||
"partially_visited": "Gedeeltelijk bezocht"
|
||||
},
|
||||
"settings": {
|
||||
"account_settings": "Gebruikersaccountinstellingen",
|
||||
"account_settings": "Gebruikersaccount instellingen",
|
||||
"confirm_new_password": "Bevestig nieuw wachtwoord",
|
||||
"current_email": "Huidige e-mail",
|
||||
"current_email": "Huidig e-mailadres",
|
||||
"email_change": "Wijzig e-mailadres",
|
||||
"new_email": "Nieuwe e-mail",
|
||||
"new_email": "Nieuw e-mailadres",
|
||||
"new_password": "Nieuw wachtwoord",
|
||||
"no_email_set": "Geen e-mailadres ingesteld",
|
||||
"password_change": "Wachtwoord wijzigen",
|
||||
"settings_page": "Instellingenpagina",
|
||||
"update": "Update",
|
||||
"update_error": "Fout bij updaten van instellingen",
|
||||
"update_success": "Instellingen succesvol bijgewerkt!",
|
||||
"update": "Wijzig",
|
||||
"update_error": "Fout bij wijzigen van instellingen",
|
||||
"update_success": "Instellingen succesvol gewijzigd!",
|
||||
"change_password": "Wachtwoord wijzigen",
|
||||
"invalid_token": "Token is ongeldig of verlopen",
|
||||
"login_redir": "Vervolgens wordt u doorgestuurd naar de inlogpagina.",
|
||||
|
@ -289,54 +302,97 @@
|
|||
"submit": "Indienen",
|
||||
"token_required": "Token en UID zijn vereist voor het opnieuw instellen van het wachtwoord.",
|
||||
"about_this_background": "Over deze achtergrond",
|
||||
"join_discord": "Sluit je aan bij de onenigheid",
|
||||
"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"
|
||||
"photo_by": "Foto door",
|
||||
"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",
|
||||
"disable_mfa": "Schakel MFA uit",
|
||||
"email_added": "E-mailadres succesvol toegevoegd!",
|
||||
"email_added_error": "Fout bij het toevoegen van e-mailadres",
|
||||
"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_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",
|
||||
"email_verified_success": "Uw e-mailadres is geverifieerd. \nU kunt nu inloggen.",
|
||||
"enable_mfa": "Schakel MFA in",
|
||||
"error_change_password": "Fout bij wijzigen van wachtwoord. \nControleer uw huidige wachtwoord en probeer het opnieuw.",
|
||||
"generic_error": "Er is een fout opgetreden tijdens het verwerken van uw verzoek.",
|
||||
"invalid_code": "Ongeldige MFA-code",
|
||||
"invalid_credentials": "Ongeldige gebruikersnaam of wachtwoord",
|
||||
"make_primary": "Maak primair",
|
||||
"mfa_disabled": "Multi-factor authenticatie is succesvol uitgeschakeld!",
|
||||
"mfa_enabled": "Multi-factor authenticatie succesvol ingeschakeld!",
|
||||
"mfa_not_enabled": "MFA is niet ingeschakeld",
|
||||
"mfa_page_title": "Authenticatie met meerdere factoren",
|
||||
"mfa_required": "Multi-factor authenticatie is vereist",
|
||||
"no_emai_set": "Geen e-mailadres ingesteld",
|
||||
"not_verified": "Niet geverifieerd",
|
||||
"primary": "Primair",
|
||||
"recovery_codes": "Herstelcodes",
|
||||
"recovery_codes_desc": "Dit zijn uw herstelcodes. \nBewaar ze veilig. \nJe zult ze niet meer kunnen zien.",
|
||||
"reset_session_error": "Meld u af en weer aan om uw sessie te vernieuwen en het opnieuw te proberen.",
|
||||
"verified": "Geverifieerd",
|
||||
"verify": "Verifiëren",
|
||||
"verify_email_error": "Fout bij het verifiëren van e-mailadres. \nProbeer het over een paar minuten opnieuw.",
|
||||
"verify_email_success": "E-mailverificatie succesvol verzonden!",
|
||||
"add_email_blocked": "U kunt geen e-mailadres toevoegen aan een account dat is beveiligd met tweefactorauthenticatie.",
|
||||
"required": "Dit veld is verplicht",
|
||||
"csrf_failed": "Kan CSRF-token niet ophalen",
|
||||
"duplicate_email": "Dit e-mailadres is al in gebruik.",
|
||||
"email_taken": "Dit e-mailadres is al in gebruik.",
|
||||
"username_taken": "Deze gebruikersnaam is al in gebruik."
|
||||
},
|
||||
"checklist": {
|
||||
"add_item": "Artikel toevoegen",
|
||||
"checklist_delete_error": "Fout bij verwijderen van checklist",
|
||||
"checklist_delete_error": "Fout bij verwijderen van controlelijst",
|
||||
"checklist_deleted": "Controlelijst succesvol verwijderd!",
|
||||
"checklist_editor": "Controlelijst-editor",
|
||||
"checklist_public": "Deze checklist is openbaar omdat deze zich in een openbare collectie bevindt.",
|
||||
"checklist_public": "Deze controlelijst is openbaar omdat deze zich in een openbare collectie bevindt.",
|
||||
"editing_checklist": "Controlelijst bewerken",
|
||||
"failed_to_save": "Kan checklist niet opslaan",
|
||||
"item": "Item",
|
||||
"failed_to_save": "Kan controlelijst niet opslaan",
|
||||
"item": "Artikel",
|
||||
"item_already_exists": "Artikel bestaat al",
|
||||
"item_cannot_be_empty": "Artikel mag niet leeg zijn",
|
||||
"items": "Artikelen",
|
||||
"new_item": "Nieuw artikel",
|
||||
"save": "Redden"
|
||||
"save": "Opslaan"
|
||||
},
|
||||
"collection": {
|
||||
"collection_created": "Collectie succesvol aangemaakt!",
|
||||
"collection_edit_success": "Collectie succesvol bewerkt!",
|
||||
"create": "Creëren",
|
||||
"edit_collection": "Verzameling bewerken",
|
||||
"error_creating_collection": "Fout bij maken collectie",
|
||||
"create": "Aanmaken",
|
||||
"edit_collection": "Collectie bewerken",
|
||||
"error_creating_collection": "Fout bij aanmaken collectie",
|
||||
"error_editing_collection": "Fout bij bewerken collectie",
|
||||
"new_collection": "Nieuwe collectie"
|
||||
},
|
||||
"notes": {
|
||||
"add_a_link": "Voeg een link toe",
|
||||
"content": "Inhoud",
|
||||
"editing_note": "Notitie bewerken",
|
||||
"failed_to_save": "Kan notitie niet opslaan",
|
||||
"note_delete_error": "Fout bij verwijderen van notitie",
|
||||
"editing_note": "Opmerking bewerken",
|
||||
"failed_to_save": "Kan opmerking niet opslaan",
|
||||
"note_delete_error": "Fout bij verwijderen van opmerking",
|
||||
"note_deleted": "Opmerking succesvol verwijderd!",
|
||||
"note_editor": "Notitie-editor",
|
||||
"note_public": "Deze notitie is openbaar omdat deze zich in een openbare collectie bevindt.",
|
||||
"note_editor": "Opmerking-editor",
|
||||
"note_public": "Deze opmerking is openbaar omdat deze zich in een openbare collectie bevindt.",
|
||||
"open": "Open",
|
||||
"save": "Redden",
|
||||
"save": "Opslaan",
|
||||
"invalid_url": "Ongeldige URL"
|
||||
},
|
||||
"transportation": {
|
||||
"date_and_time": "Datum",
|
||||
"date_time": "Startdatum",
|
||||
"edit": "Bewerking",
|
||||
"edit_transportation": "Transport bewerken",
|
||||
"edit_transportation": "Vervoer bewerken",
|
||||
"end_date_time": "Einddatum",
|
||||
"error_editing_transportation": "Fout bij bewerken van transport",
|
||||
"error_editing_transportation": "Fout bij bewerken van vervoer",
|
||||
"flight_number": "Vluchtnummer",
|
||||
"from_location": "Van locatie",
|
||||
"modes": {
|
||||
|
@ -350,15 +406,15 @@
|
|||
"walking": "Lopen"
|
||||
},
|
||||
"to_location": "Naar locatie",
|
||||
"transportation_edit_success": "Transport succesvol bewerkt!",
|
||||
"transportation_edit_success": "Vervoer succesvol bewerkt!",
|
||||
"type": "Type",
|
||||
"new_transportation": "Nieuw transport",
|
||||
"new_transportation": "Nieuw vervoer",
|
||||
"provide_start_date": "Geef een startdatum op",
|
||||
"start": "Begin",
|
||||
"transport_type": "Transporttype",
|
||||
"transportation_added": "Transport succesvol toegevoegd!",
|
||||
"transportation_delete_error": "Fout bij verwijderen transport",
|
||||
"transportation_deleted": "Transport succesvol verwijderd!"
|
||||
"transport_type": "Vervoerstype",
|
||||
"transportation_added": "Vervoer succesvol toegevoegd!",
|
||||
"transportation_delete_error": "Fout bij verwijderen vervoer",
|
||||
"transportation_deleted": "Vervoer succesvol verwijderd!"
|
||||
},
|
||||
"search": {
|
||||
"adventurelog_results": "AdventureLog-resultaten",
|
||||
|
@ -367,9 +423,9 @@
|
|||
},
|
||||
"map": {
|
||||
"add_adventure": "Voeg nieuw avontuur toe",
|
||||
"add_adventure_at_marker": "Voeg een nieuw avontuur toe bij Marker",
|
||||
"add_adventure_at_marker": "Voeg een nieuw avontuur toe bij markeerpunt",
|
||||
"adventure_map": "Avonturenkaart",
|
||||
"clear_marker": "Duidelijke markering",
|
||||
"clear_marker": "Verwijder markeerpunt",
|
||||
"map_options": "Kaartopties",
|
||||
"show_visited_regions": "Toon bezochte regio's",
|
||||
"view_details": "Details bekijken"
|
||||
|
@ -388,7 +444,7 @@
|
|||
"share": {
|
||||
"no_users_shared": "Er zijn geen gebruikers gedeeld",
|
||||
"not_shared_with": "Niet gedeeld met",
|
||||
"share_desc": "Deel deze verzameling met andere gebruikers.",
|
||||
"share_desc": "Deel deze collectie met andere gebruikers.",
|
||||
"shared": "Gedeeld",
|
||||
"shared_with": "Gedeeld met",
|
||||
"unshared": "Niet gedeeld",
|
||||
|
@ -406,10 +462,19 @@
|
|||
"categories": {
|
||||
"category_name": "Categorienaam",
|
||||
"edit_category": "Categorie bewerken",
|
||||
"icon": "Icon",
|
||||
"icon": "Ikoon",
|
||||
"manage_categories": "Beheer categorieën",
|
||||
"no_categories_found": "Geen categorieën gevonden.",
|
||||
"select_category": "Selecteer Categorie",
|
||||
"select_category": "Selecteer categorie",
|
||||
"update_after_refresh": "De avonturenkaarten worden bijgewerkt zodra u de pagina vernieuwt."
|
||||
},
|
||||
"dashboard": {
|
||||
"add_some": "Waarom begint u niet met het plannen van uw volgende avontuur? \nJe kunt een nieuw avontuur toevoegen door op de onderstaande knop te klikken.",
|
||||
"countries_visited": "Bezochte landen",
|
||||
"no_recent_adventures": "Geen recente avonturen?",
|
||||
"recent_adventures": "Recente avonturen",
|
||||
"total_adventures": "Totale avonturen",
|
||||
"total_visited_regions": "Totaal bezochte regio's",
|
||||
"welcome_back": "Welkom terug"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,154 +1,156 @@
|
|||
{
|
||||
"navbar": {
|
||||
"adventures": "Podróże",
|
||||
"collections": "Kolekcje",
|
||||
"worldtravel": "Podróże po świecie",
|
||||
"map": "Mapa",
|
||||
"users": "Użytkownicy",
|
||||
"search": "Szukaj",
|
||||
"profile": "Profil",
|
||||
"greeting": "Cześć",
|
||||
"my_adventures": "Moje podróże",
|
||||
"my_tags": "Moje tagi",
|
||||
"tag": "Tag",
|
||||
"shared_with_me": "Udostępnione ze mną",
|
||||
"settings": "Ustawienia",
|
||||
"logout": "Wyloguj się",
|
||||
"about": "O AdventureLog",
|
||||
"documentation": "Dokumentacja",
|
||||
"discord": "Discord",
|
||||
"language_selection": "Wybór języka",
|
||||
"support": "Wsparcie",
|
||||
"theme_selection": "Wybór motywu",
|
||||
"themes": {
|
||||
"light": "Jasny",
|
||||
"dark": "Ciemny",
|
||||
"night": "Nocny",
|
||||
"forest": "Leśny",
|
||||
"aestheticLight": "Estetyczny Jasny",
|
||||
"aestheticDark": "Estetyczny Ciemny",
|
||||
"aqua": "Aqua"
|
||||
}
|
||||
"adventures": "Podróże",
|
||||
"collections": "Kolekcje",
|
||||
"worldtravel": "Podróże po świecie",
|
||||
"map": "Mapa",
|
||||
"users": "Użytkownicy",
|
||||
"search": "Szukaj",
|
||||
"profile": "Profil",
|
||||
"greeting": "Cześć",
|
||||
"my_adventures": "Moje podróże",
|
||||
"my_tags": "Moje tagi",
|
||||
"tag": "Tag",
|
||||
"shared_with_me": "Udostępnione ze mną",
|
||||
"settings": "Ustawienia",
|
||||
"logout": "Wyloguj się",
|
||||
"about": "O AdventureLog",
|
||||
"documentation": "Dokumentacja",
|
||||
"discord": "Discord",
|
||||
"language_selection": "Wybór języka",
|
||||
"support": "Wsparcie",
|
||||
"theme_selection": "Wybór motywu",
|
||||
"themes": {
|
||||
"light": "Jasny",
|
||||
"dark": "Ciemny",
|
||||
"night": "Nocny",
|
||||
"forest": "Leśny",
|
||||
"aestheticLight": "Estetyczny Jasny",
|
||||
"aestheticDark": "Estetyczny Ciemny",
|
||||
"aqua": "Aqua",
|
||||
"northernLights": "Zorza Polarna"
|
||||
},
|
||||
"calendar": "Kalendarz"
|
||||
},
|
||||
"about": {
|
||||
"about": "O aplikacji",
|
||||
"license": "Licencjonowane na licencji GPL-3.0.",
|
||||
"source_code": "Kod źródłowy",
|
||||
"message": "Stworzone z ❤️ w Stanach Zjednoczonych.",
|
||||
"oss_attributions": "Atrybucje Open Source",
|
||||
"nominatim_1": "Wyszukiwanie lokalizacji i geokodowanie zapewnia",
|
||||
"nominatim_2": "Ich dane są licencjonowane na licencji ODbL.",
|
||||
"other_attributions": "Dodatkowe atrybucje można znaleźć w pliku README.",
|
||||
"close": "Zamknij"
|
||||
"about": "O aplikacji",
|
||||
"license": "Licencjonowane na licencji GPL-3.0.",
|
||||
"source_code": "Kod źródłowy",
|
||||
"message": "Stworzone z ❤️ w Stanach Zjednoczonych.",
|
||||
"oss_attributions": "Atrybucje Open Source",
|
||||
"nominatim_1": "Wyszukiwanie lokalizacji i geokodowanie zapewnia",
|
||||
"nominatim_2": "Ich dane są licencjonowane na licencji ODbL.",
|
||||
"other_attributions": "Dodatkowe atrybucje można znaleźć w pliku README.",
|
||||
"close": "Zamknij"
|
||||
},
|
||||
"home": {
|
||||
"hero_1": "Odkryj najbardziej ekscytujące podróże na świecie",
|
||||
"hero_2": "Odkrywaj i planuj swoją kolejną podróż z AdventureLog. Poznaj zapierające dech w piersiach miejsca, twórz spersonalizowane plany podróży i bądź w kontakcie w trakcie podróży.",
|
||||
"go_to": "Przejdź do AdventureLog",
|
||||
"key_features": "Najważniejsze funkcje",
|
||||
"desc_1": "Odkrywaj, planuj i eksploruj z łatwością",
|
||||
"desc_2": "AdventureLog został zaprojektowany, aby uprościć Twoją podróż, oferując narzędzia i zasoby do planowania, pakowania i poruszania się po niezapomnianej podróży.",
|
||||
"feature_1": "Dziennik podróży",
|
||||
"feature_1_desc": "Śledź swoje podróże dzięki spersonalizowanemu dziennikowi podróży i dziel się swoimi doświadczeniami z przyjaciółmi i rodziną.",
|
||||
"feature_2": "Planowanie podróży",
|
||||
"feature_2_desc": "Łatwo twórz spersonalizowane plany podróży i uzyskaj szczegółowy rozkład swojego wyjazdu na każdy dzień.",
|
||||
"feature_3": "Mapa podróży",
|
||||
"feature_3_desc": "Zobacz swoje podróże na całym świecie dzięki interaktywnej mapie i odkrywaj nowe miejsca."
|
||||
"hero_1": "Odkryj najbardziej ekscytujące podróże na świecie",
|
||||
"hero_2": "Odkrywaj i planuj swoją kolejną podróż z AdventureLog. Poznaj zapierające dech w piersiach miejsca, twórz spersonalizowane plany podróży i bądź w kontakcie w trakcie podróży.",
|
||||
"go_to": "Przejdź do AdventureLog",
|
||||
"key_features": "Najważniejsze funkcje",
|
||||
"desc_1": "Odkrywaj, planuj i eksploruj z łatwością",
|
||||
"desc_2": "AdventureLog został zaprojektowany, aby uprościć Twoją podróż, oferując narzędzia i zasoby do planowania, pakowania i poruszania się po niezapomnianej podróży.",
|
||||
"feature_1": "Dziennik podróży",
|
||||
"feature_1_desc": "Śledź swoje podróże dzięki spersonalizowanemu dziennikowi podróży i dziel się swoimi doświadczeniami z przyjaciółmi i rodziną.",
|
||||
"feature_2": "Planowanie podróży",
|
||||
"feature_2_desc": "Łatwo twórz spersonalizowane plany podróży i uzyskaj szczegółowy rozkład swojego wyjazdu na każdy dzień.",
|
||||
"feature_3": "Mapa podróży",
|
||||
"feature_3_desc": "Zobacz swoje podróże na całym świecie dzięki interaktywnej mapie i odkrywaj nowe miejsca."
|
||||
},
|
||||
"adventures": {
|
||||
"collection_remove_success": "Podróż została pomyślnie usunięta z kolekcji!",
|
||||
"collection_remove_error": "Błąd podczas usuwania podróży z kolekcji",
|
||||
"collection_link_success": "Podróż została pomyślnie dodana do kolekcji!",
|
||||
"no_image_found": "Nie znaleziono obrazu",
|
||||
"collection_link_error": "Błąd podczas dodawania podróży do kolekcji",
|
||||
"adventure_delete_confirm": "Czy na pewno chcesz usunąć tę podróż? Ta operacja jest nieodwracalna.",
|
||||
"open_details": "Otwórz szczegóły",
|
||||
"edit_adventure": "Edytuj podróż",
|
||||
"remove_from_collection": "Usuń z kolekcji",
|
||||
"add_to_collection": "Dodaj do kolekcji",
|
||||
"delete": "Usuń",
|
||||
"not_found": "Podróż nie znaleziona",
|
||||
"not_found_desc": "Podróży, której szukasz, nie można znaleźć. Spróbuj poszukać innej podróży lub sprawdź później.",
|
||||
"homepage": "Strona główna",
|
||||
"adventure_details": "Szczegóły podróży",
|
||||
"collection": "Kolekcja",
|
||||
"adventure_type": "Typ podróży",
|
||||
"longitude": "Długość geograficzna",
|
||||
"latitude": "Szerokość geograficzna",
|
||||
"visit": "Odwiedź",
|
||||
"visits": "Odwiedziny",
|
||||
"create_new": "Utwórz nową...",
|
||||
"adventure": "Podróż",
|
||||
"count_txt": "wyniki pasujące do Twojego wyszukiwania",
|
||||
"sort": "Sortuj",
|
||||
"order_by": "Sortuj według",
|
||||
"order_direction": "Kierunek sortowania",
|
||||
"ascending": "Rosnąco",
|
||||
"descending": "Malejąco",
|
||||
"updated": "Zaktualizowano",
|
||||
"name": "Nazwa",
|
||||
"date": "Data",
|
||||
"activity_types": "Rodzaje aktywności",
|
||||
"tags": "Tagi",
|
||||
"add_a_tag": "Dodaj tag",
|
||||
"date_constrain": "Ogranicz do dat kolekcji",
|
||||
"rating": "Ocena",
|
||||
"my_images": "Moje obrazy",
|
||||
"add_an_activity": "Dodaj aktywność",
|
||||
"no_images": "Brak obrazów",
|
||||
"upload_images_here": "Prześlij obrazy tutaj",
|
||||
"share_adventure": "Podziel się tą podróżą!",
|
||||
"copy_link": "Kopiuj link",
|
||||
"image": "Obraz",
|
||||
"upload_image": "Prześlij obraz",
|
||||
"url": "URL",
|
||||
"fetch_image": "Pobierz obraz",
|
||||
"wikipedia": "Wikipedia",
|
||||
"add_notes": "Dodaj notatki",
|
||||
"warning": "Ostrzeżenie",
|
||||
"my_adventures": "Moje podróże",
|
||||
"no_linkable_adventures": "Nie znaleziono podróży, które można połączyć z tą kolekcją.",
|
||||
"add": "Dodaj",
|
||||
"save_next": "Zapisz i następny",
|
||||
"end_date": "Data zakończenia",
|
||||
"my_visits": "Moje wizyty",
|
||||
"start_date": "Data rozpoczęcia",
|
||||
"remove": "Usuń",
|
||||
"location": "Lokalizacja",
|
||||
"search_for_location": "Szukaj lokalizacji",
|
||||
"clear_map": "Wyczyść mapę",
|
||||
"search_results": "Wyniki wyszukiwania",
|
||||
"no_results": "Nie znaleziono wyników",
|
||||
"wiki_desc": "Pobiera fragment artykułu z Wikipedii pasującego do nazwy podróży.",
|
||||
"generate_desc": "Generuj opis",
|
||||
"public_adventure": "Publiczna podróż",
|
||||
"location_information": "Informacje o lokalizacji",
|
||||
"link": "Link",
|
||||
"links": "Linki",
|
||||
"description": "Opis",
|
||||
"sources": "Źródła",
|
||||
"collection_adventures": "Uwzględnij podróże z kolekcji",
|
||||
"filter": "Filtr",
|
||||
"category_filter": "Filtr kategorii",
|
||||
"category": "Kategoria",
|
||||
"select_adventure_category": "Wybierz kategorię podróży",
|
||||
"clear": "Wyczyść",
|
||||
"my_collections": "Moje kolekcje",
|
||||
"open_filters": "Otwórz filtry",
|
||||
"close_filters": "Zamknij filtry",
|
||||
"archived_collections": "Zarchiwizowane kolekcje",
|
||||
"share": "Podziel się",
|
||||
"private": "Prywatne",
|
||||
"public": "Publiczne",
|
||||
"archived": "Zarchiwizowane",
|
||||
"edit_collection": "Edytuj kolekcję",
|
||||
"unarchive": "Przywróć z archiwum",
|
||||
"archive": "Archiwizuj",
|
||||
"no_collections_found": "Nie znaleziono kolekcji, do których można dodać tę podróż.",
|
||||
"not_visited": "Nie odwiedzone",
|
||||
"archived_collection_message": "Kolekcja została pomyślnie zarchiwizowana!",
|
||||
"unarchived_collection_message": "Kolekcja została pomyślnie przywrócona z archiwum!",
|
||||
"delete_collection_success": "Kolekcja została pomyślnie usunięta!",
|
||||
"collection_remove_success": "Podróż została pomyślnie usunięta z kolekcji!",
|
||||
"collection_remove_error": "Błąd podczas usuwania podróży z kolekcji",
|
||||
"collection_link_success": "Podróż została pomyślnie dodana do kolekcji!",
|
||||
"no_image_found": "Nie znaleziono obrazu",
|
||||
"collection_link_error": "Błąd podczas dodawania podróży do kolekcji",
|
||||
"adventure_delete_confirm": "Czy na pewno chcesz usunąć tę podróż? Ta operacja jest nieodwracalna.",
|
||||
"open_details": "Otwórz szczegóły",
|
||||
"edit_adventure": "Edytuj podróż",
|
||||
"remove_from_collection": "Usuń z kolekcji",
|
||||
"add_to_collection": "Dodaj do kolekcji",
|
||||
"delete": "Usuń",
|
||||
"not_found": "Podróż nie znaleziona",
|
||||
"not_found_desc": "Podróży, której szukasz, nie można znaleźć. Spróbuj poszukać innej podróży lub sprawdź później.",
|
||||
"homepage": "Strona główna",
|
||||
"adventure_details": "Szczegóły podróży",
|
||||
"collection": "Kolekcja",
|
||||
"adventure_type": "Typ podróży",
|
||||
"longitude": "Długość geograficzna",
|
||||
"latitude": "Szerokość geograficzna",
|
||||
"visit": "Odwiedź",
|
||||
"visits": "Odwiedziny",
|
||||
"create_new": "Utwórz nową...",
|
||||
"adventure": "Podróż",
|
||||
"count_txt": "wyniki pasujące do Twojego wyszukiwania",
|
||||
"sort": "Sortuj",
|
||||
"order_by": "Sortuj według",
|
||||
"order_direction": "Kierunek sortowania",
|
||||
"ascending": "Rosnąco",
|
||||
"descending": "Malejąco",
|
||||
"updated": "Zaktualizowano",
|
||||
"name": "Nazwa",
|
||||
"date": "Data",
|
||||
"activity_types": "Rodzaje aktywności",
|
||||
"tags": "Tagi",
|
||||
"add_a_tag": "Dodaj tag",
|
||||
"date_constrain": "Ogranicz do dat kolekcji",
|
||||
"rating": "Ocena",
|
||||
"my_images": "Moje obrazy",
|
||||
"add_an_activity": "Dodaj aktywność",
|
||||
"no_images": "Brak obrazów",
|
||||
"upload_images_here": "Prześlij obrazy tutaj",
|
||||
"share_adventure": "Podziel się tą podróżą!",
|
||||
"copy_link": "Kopiuj link",
|
||||
"image": "Obraz",
|
||||
"upload_image": "Prześlij obraz",
|
||||
"url": "URL",
|
||||
"fetch_image": "Pobierz obraz",
|
||||
"wikipedia": "Wikipedia",
|
||||
"add_notes": "Dodaj notatki",
|
||||
"warning": "Ostrzeżenie",
|
||||
"my_adventures": "Moje podróże",
|
||||
"no_linkable_adventures": "Nie znaleziono podróży, które można połączyć z tą kolekcją.",
|
||||
"add": "Dodaj",
|
||||
"save_next": "Zapisz i następny",
|
||||
"end_date": "Data zakończenia",
|
||||
"my_visits": "Moje wizyty",
|
||||
"start_date": "Data rozpoczęcia",
|
||||
"remove": "Usuń",
|
||||
"location": "Lokalizacja",
|
||||
"search_for_location": "Szukaj lokalizacji",
|
||||
"clear_map": "Wyczyść mapę",
|
||||
"search_results": "Wyniki wyszukiwania",
|
||||
"no_results": "Nie znaleziono wyników",
|
||||
"wiki_desc": "Pobiera fragment artykułu z Wikipedii pasującego do nazwy podróży.",
|
||||
"generate_desc": "Generuj opis",
|
||||
"public_adventure": "Publiczna podróż",
|
||||
"location_information": "Informacje o lokalizacji",
|
||||
"link": "Link",
|
||||
"links": "Linki",
|
||||
"description": "Opis",
|
||||
"sources": "Źródła",
|
||||
"collection_adventures": "Uwzględnij podróże z kolekcji",
|
||||
"filter": "Filtr",
|
||||
"category_filter": "Filtr kategorii",
|
||||
"category": "Kategoria",
|
||||
"select_adventure_category": "Wybierz kategorię podróży",
|
||||
"clear": "Wyczyść",
|
||||
"my_collections": "Moje kolekcje",
|
||||
"open_filters": "Otwórz filtry",
|
||||
"close_filters": "Zamknij filtry",
|
||||
"archived_collections": "Zarchiwizowane kolekcje",
|
||||
"share": "Podziel się",
|
||||
"private": "Prywatne",
|
||||
"public": "Publiczne",
|
||||
"archived": "Zarchiwizowane",
|
||||
"edit_collection": "Edytuj kolekcję",
|
||||
"unarchive": "Przywróć z archiwum",
|
||||
"archive": "Archiwizuj",
|
||||
"no_collections_found": "Nie znaleziono kolekcji, do których można dodać tę podróż.",
|
||||
"not_visited": "Nie odwiedzone",
|
||||
"archived_collection_message": "Kolekcja została pomyślnie zarchiwizowana!",
|
||||
"unarchived_collection_message": "Kolekcja została pomyślnie przywrócona z archiwum!",
|
||||
"delete_collection_success": "Kolekcja została pomyślnie usunięta!",
|
||||
"delete_collection_warning": "Czy na pewno chcesz usunąć tę kolekcję? Spowoduje to również usunięcie wszystkich powiązanych podróży. Ta akcja jest nieodwracalna.",
|
||||
"cancel": "Anuluj",
|
||||
"delete_collection": "Usuń kolekcję",
|
||||
|
@ -211,32 +213,39 @@
|
|||
"nothing_planned": "Nic nie zaplanowane na ten dzień. Ciesz się podróżą!",
|
||||
"days": "dni",
|
||||
"activities": {
|
||||
"general": "Ogólne 🌍",
|
||||
"outdoor": "Na świeżym powietrzu 🏞️",
|
||||
"lodging": "Zakwaterowanie 🛌",
|
||||
"dining": "Posiłki 🍽️",
|
||||
"activity": "Aktywność 🏄",
|
||||
"attraction": "Atrakcje 🎢",
|
||||
"shopping": "Zakupy 🛍️",
|
||||
"nightlife": "Życie nocne 🌃",
|
||||
"event": "Wydarzenie 🎉",
|
||||
"transportation": "Transport 🚗",
|
||||
"culture": "Kultura 🎭",
|
||||
"water_sports": "Sporty wodne 🚤",
|
||||
"hiking": "Wędrówki 🥾",
|
||||
"wildlife": "Dzika przyroda 🦒",
|
||||
"historical_sites": "Miejsca historyczne 🏛️",
|
||||
"music_concerts": "Muzyka i koncerty 🎶",
|
||||
"fitness": "Fitness 🏋️",
|
||||
"art_museums": "Sztuka i muzea 🎨",
|
||||
"festivals": "Festiwale 🎪",
|
||||
"spiritual_journeys": "Podróże duchowe 🧘♀️",
|
||||
"volunteer_work": "Praca wolontariacka 🤝",
|
||||
"other": "Inne"
|
||||
}
|
||||
"general": "Ogólne 🌍",
|
||||
"outdoor": "Na świeżym powietrzu 🏞️",
|
||||
"lodging": "Zakwaterowanie 🛌",
|
||||
"dining": "Posiłki 🍽️",
|
||||
"activity": "Aktywność 🏄",
|
||||
"attraction": "Atrakcje 🎢",
|
||||
"shopping": "Zakupy 🛍️",
|
||||
"nightlife": "Życie nocne 🌃",
|
||||
"event": "Wydarzenie 🎉",
|
||||
"transportation": "Transport 🚗",
|
||||
"culture": "Kultura 🎭",
|
||||
"water_sports": "Sporty wodne 🚤",
|
||||
"hiking": "Wędrówki 🥾",
|
||||
"wildlife": "Dzika przyroda 🦒",
|
||||
"historical_sites": "Miejsca historyczne 🏛️",
|
||||
"music_concerts": "Muzyka i koncerty 🎶",
|
||||
"fitness": "Fitness 🏋️",
|
||||
"art_museums": "Sztuka i muzea 🎨",
|
||||
"festivals": "Festiwale 🎪",
|
||||
"spiritual_journeys": "Podróże duchowe 🧘♀️",
|
||||
"volunteer_work": "Praca wolontariacka 🤝",
|
||||
"other": "Inne"
|
||||
},
|
||||
"copied_to_clipboard": "Skopiowano do schowka!",
|
||||
"copy_failed": "Kopiowanie nie powiodło się",
|
||||
"adventure_calendar": "Kalendarz przygód",
|
||||
"emoji_picker": "Wybór emoji",
|
||||
"hide": "Ukrywać",
|
||||
"show": "Pokazywać",
|
||||
"download_calendar": "Pobierz Kalendarz"
|
||||
},
|
||||
"worldtravel": {
|
||||
"country_list": "Lista krajów",
|
||||
"country_list": "Lista krajów",
|
||||
"num_countries": "znalezione kraje",
|
||||
"all": "Wszystkie",
|
||||
"partially_visited": "Częściowo odwiedzone",
|
||||
|
@ -260,10 +269,14 @@
|
|||
"registration_disabled": "Rejestracja jest obecnie wyłączona.",
|
||||
"profile_picture": "Zdjęcie profilowe",
|
||||
"public_profile": "Publiczny profil",
|
||||
"public_tooltip": "Dzięki publicznemu profilowi użytkownicy mogą dzielić się z Tobą kolekcjami i oglądać Twój profil na stronie użytkowników."
|
||||
"public_tooltip": "Dzięki publicznemu profilowi użytkownicy mogą dzielić się z Tobą kolekcjami i oglądać Twój profil na stronie użytkowników.",
|
||||
"email_required": "Adres e-mail jest wymagany",
|
||||
"both_passwords_required": "Obydwa hasła są wymagane",
|
||||
"new_password": "Nowe hasło",
|
||||
"reset_failed": "Nie udało się zresetować hasła"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "Nie znaleziono użytkowników z publicznymi profilami."
|
||||
"no_users_found": "Nie znaleziono użytkowników z publicznymi profilami."
|
||||
},
|
||||
"settings": {
|
||||
"update_error": "Błąd podczas aktualizacji ustawień",
|
||||
|
@ -291,7 +304,50 @@
|
|||
"about_this_background": "O tym tle",
|
||||
"photo_by": "Zdjęcie autorstwa",
|
||||
"join_discord": "Dołącz do Discorda",
|
||||
"join_discord_desc": "aby podzielić się swoimi zdjęciami. Zamieść je w kanale #travel-share."
|
||||
"join_discord_desc": "aby podzielić się swoimi zdjęciami. Zamieść je w kanale #travel-share.",
|
||||
"change_password_error": "Nie można zmienić hasła. \nNieprawidłowe bieżące hasło lub nieprawidłowe nowe hasło.",
|
||||
"current_password": "Aktualne hasło",
|
||||
"password_change_lopout_warning": "Po zmianie hasła nastąpi wylogowanie.",
|
||||
"authenticator_code": "Kod uwierzytelniający",
|
||||
"copy": "Kopia",
|
||||
"disable_mfa": "Wyłącz usługę MFA",
|
||||
"email_added": "Adres e-mail został pomyślnie dodany!",
|
||||
"email_added_error": "Błąd podczas dodawania adresu e-mail",
|
||||
"email_removed": "E-mail został pomyślnie usunięty!",
|
||||
"email_removed_error": "Błąd podczas usuwania wiadomości e-mail",
|
||||
"email_set_primary": "Adres e-mail został pomyślnie ustawiony jako podstawowy!",
|
||||
"email_set_primary_error": "Błąd podczas ustawiania adresu e-mail jako głównego",
|
||||
"email_verified": "E-mail zweryfikowany pomyślnie!",
|
||||
"email_verified_erorr_desc": "Nie udało się zweryfikować Twojego adresu e-mail. \nSpróbuj ponownie.",
|
||||
"email_verified_error": "Błąd podczas weryfikacji adresu e-mail",
|
||||
"email_verified_success": "Twój e-mail został zweryfikowany. \nMożesz się teraz zalogować.",
|
||||
"enable_mfa": "Włącz usługę MFA",
|
||||
"error_change_password": "Błąd podczas zmiany hasła. \nSprawdź swoje aktualne hasło i spróbuj ponownie.",
|
||||
"generic_error": "Wystąpił błąd podczas przetwarzania Twojego żądania.",
|
||||
"invalid_code": "Nieprawidłowy kod MFA",
|
||||
"invalid_credentials": "Nieprawidłowa nazwa użytkownika lub hasło",
|
||||
"make_primary": "Ustaw jako podstawowy",
|
||||
"mfa_disabled": "Uwierzytelnianie wieloskładnikowe zostało pomyślnie wyłączone!",
|
||||
"mfa_enabled": "Uwierzytelnianie wieloskładnikowe zostało pomyślnie włączone!",
|
||||
"mfa_not_enabled": "Usługa MFA nie jest włączona",
|
||||
"mfa_page_title": "Uwierzytelnianie wieloskładnikowe",
|
||||
"mfa_required": "Wymagane jest uwierzytelnianie wieloskładnikowe",
|
||||
"no_emai_set": "Nie ustawiono adresu e-mail",
|
||||
"not_verified": "Nie zweryfikowano",
|
||||
"primary": "Podstawowy",
|
||||
"recovery_codes": "Kody odzyskiwania",
|
||||
"recovery_codes_desc": "To są Twoje kody odzyskiwania. \nZapewnij im bezpieczeństwo. \nNie będziesz mógł ich ponownie zobaczyć.",
|
||||
"reset_session_error": "Wyloguj się i zaloguj ponownie, aby odświeżyć sesję i spróbuj ponownie.",
|
||||
"verified": "Zweryfikowano",
|
||||
"verify": "Zweryfikować",
|
||||
"verify_email_error": "Błąd podczas weryfikacji adresu e-mail. \nSpróbuj ponownie za kilka minut.",
|
||||
"verify_email_success": "Weryfikacja e-mailowa została wysłana pomyślnie!",
|
||||
"add_email_blocked": "Nie można dodać adresu e-mail do konta chronionego uwierzytelnianiem dwuskładnikowym.",
|
||||
"required": "To pole jest wymagane",
|
||||
"csrf_failed": "Nie udało się pobrać tokena CSRF",
|
||||
"duplicate_email": "Ten adres e-mail jest już używany.",
|
||||
"email_taken": "Ten adres e-mail jest już używany.",
|
||||
"username_taken": "Ta nazwa użytkownika jest już używana."
|
||||
},
|
||||
"collection": {
|
||||
"collection_created": "Kolekcja została pomyślnie utworzona!",
|
||||
|
@ -316,7 +372,7 @@
|
|||
"invalid_url": "Nieprawidłowy URL"
|
||||
},
|
||||
"checklist": {
|
||||
"checklist_deleted": "Lista kontrolna została pomyślnie usunięta!",
|
||||
"checklist_deleted": "Lista kontrolna została pomyślnie usunięta!",
|
||||
"checklist_delete_error": "Błąd podczas usuwania listy kontrolnej",
|
||||
"failed_to_save": "Nie udało się zapisać listy kontrolnej",
|
||||
"checklist_editor": "Edytor listy kontrolnej",
|
||||
|
@ -346,14 +402,14 @@
|
|||
"to_location": "Miejsce docelowe",
|
||||
"edit": "Edytuj",
|
||||
"modes": {
|
||||
"car": "Samochód",
|
||||
"plane": "Samolot",
|
||||
"train": "Pociąg",
|
||||
"bus": "Autobus",
|
||||
"boat": "Łódź",
|
||||
"bike": "Rower",
|
||||
"walking": "Pieszo",
|
||||
"other": "Inne"
|
||||
"car": "Samochód",
|
||||
"plane": "Samolot",
|
||||
"train": "Pociąg",
|
||||
"bus": "Autobus",
|
||||
"boat": "Łódź",
|
||||
"bike": "Rower",
|
||||
"walking": "Pieszo",
|
||||
"other": "Inne"
|
||||
},
|
||||
"transportation_edit_success": "Transport edytowany pomyślnie!",
|
||||
"edit_transportation": "Edytuj transport",
|
||||
|
@ -380,36 +436,45 @@
|
|||
"unshared": "Nieudostępnione",
|
||||
"share_desc": "Udostępnij tę kolekcję innym użytkownikom.",
|
||||
"shared_with": "Współdzielone z",
|
||||
"no_users_shared": "Brak użytkowników, którym udostępniono",
|
||||
"no_users_shared": "Brak użytkowników, którym udostępniono",
|
||||
"not_shared_with": "Brak udostępnionych",
|
||||
"no_shared_found": "Brak kolekcji udostępnionych Tobie.",
|
||||
"set_public": "Aby umożliwić użytkownikom udostępnianie Tobie, musisz ustawić swój profil jako publiczny.",
|
||||
"set_public": "Aby umożliwić użytkownikom udostępnianie Tobie, musisz ustawić swój profil jako publiczny.",
|
||||
"go_to_settings": "Przejdź do ustawień"
|
||||
},
|
||||
"languages": {
|
||||
"en": "Angielski",
|
||||
"de": "Niemiecki",
|
||||
"es": "Hiszpański",
|
||||
"fr": "Francuski",
|
||||
"it": "Włoski",
|
||||
"nl": "Holenderski",
|
||||
"sv": "Szwedzki",
|
||||
"zh": "Chiński",
|
||||
"pl": "Polski"
|
||||
"en": "Angielski",
|
||||
"de": "Niemiecki",
|
||||
"es": "Hiszpański",
|
||||
"fr": "Francuski",
|
||||
"it": "Włoski",
|
||||
"nl": "Holenderski",
|
||||
"sv": "Szwedzki",
|
||||
"zh": "Chiński",
|
||||
"pl": "Polski"
|
||||
},
|
||||
"profile": {
|
||||
"member_since": "Użytkownik od",
|
||||
"user_stats": "Statystyki użytkownika",
|
||||
"visited_countries": "Odwiedzone kraje",
|
||||
"visited_regions": "Odwiedzone regiony"
|
||||
"member_since": "Użytkownik od",
|
||||
"user_stats": "Statystyki użytkownika",
|
||||
"visited_countries": "Odwiedzone kraje",
|
||||
"visited_regions": "Odwiedzone regiony"
|
||||
},
|
||||
"categories": {
|
||||
"manage_categories": "Zarządzaj kategoriami",
|
||||
"no_categories_found": "Brak kategorii.",
|
||||
"edit_category": "Edytuj kategorię",
|
||||
"icon": "Ikona",
|
||||
"update_after_refresh": "Karty podróży zostaną zaktualizowane po odświeżeniu strony.",
|
||||
"select_category": "Wybierz kategorię",
|
||||
"category_name": "Nazwa kategorii"
|
||||
"manage_categories": "Zarządzaj kategoriami",
|
||||
"no_categories_found": "Brak kategorii.",
|
||||
"edit_category": "Edytuj kategorię",
|
||||
"icon": "Ikona",
|
||||
"update_after_refresh": "Karty podróży zostaną zaktualizowane po odświeżeniu strony.",
|
||||
"select_category": "Wybierz kategorię",
|
||||
"category_name": "Nazwa kategorii"
|
||||
},
|
||||
"dashboard": {
|
||||
"add_some": "Dlaczego nie zacząć planować kolejnej przygody? \nMożesz dodać nową przygodę, klikając przycisk poniżej.",
|
||||
"countries_visited": "Odwiedzone kraje",
|
||||
"no_recent_adventures": "Brak nowych przygód?",
|
||||
"recent_adventures": "Ostatnie przygody",
|
||||
"total_adventures": "Totalne przygody",
|
||||
"total_visited_regions": "Łączna liczba odwiedzonych regionów",
|
||||
"welcome_back": "Witamy z powrotem"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,7 +188,14 @@
|
|||
"add_a_tag": "Lägg till en tagg",
|
||||
"tags": "Taggar",
|
||||
"set_to_pin": "Ställ in på Pin",
|
||||
"category_fetch_error": "Det gick inte att hämta kategorier"
|
||||
"category_fetch_error": "Det gick inte att hämta kategorier",
|
||||
"copied_to_clipboard": "Kopierat till urklipp!",
|
||||
"copy_failed": "Kopieringen misslyckades",
|
||||
"adventure_calendar": "Äventyrskalender",
|
||||
"emoji_picker": "Emoji-väljare",
|
||||
"hide": "Dölja",
|
||||
"show": "Visa",
|
||||
"download_calendar": "Ladda ner kalender"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Upptäck, planera och utforska med lätthet",
|
||||
|
@ -226,14 +233,16 @@
|
|||
"light": "Ljus",
|
||||
"night": "Natt",
|
||||
"aestheticDark": "Estetisk mörk",
|
||||
"aestheticLight": "Estetiskt ljus"
|
||||
"aestheticLight": "Estetiskt ljus",
|
||||
"northernLights": "Norrsken"
|
||||
},
|
||||
"users": "Användare",
|
||||
"worldtravel": "Världsresor",
|
||||
"my_tags": "Mina taggar",
|
||||
"tag": "Märka",
|
||||
"language_selection": "Språk",
|
||||
"support": "Stöd"
|
||||
"support": "Stöd",
|
||||
"calendar": "Kalender"
|
||||
},
|
||||
"worldtravel": {
|
||||
"all": "Alla",
|
||||
|
@ -260,7 +269,11 @@
|
|||
"username": "Användarnamn",
|
||||
"public_tooltip": "Med en offentlig profil kan användare dela samlingar med dig och se din profil på användarsidan.",
|
||||
"profile_picture": "Profilbild",
|
||||
"public_profile": "Offentlig profil"
|
||||
"public_profile": "Offentlig profil",
|
||||
"email_required": "E-post krävs",
|
||||
"both_passwords_required": "Båda lösenorden krävs",
|
||||
"new_password": "Nytt lösenord",
|
||||
"reset_failed": "Det gick inte att återställa lösenordet"
|
||||
},
|
||||
"users": {
|
||||
"no_users_found": "Inga användare hittades med offentliga profiler."
|
||||
|
@ -291,7 +304,50 @@
|
|||
"about_this_background": "Om denna bakgrund",
|
||||
"join_discord": "Gå med i Discord",
|
||||
"join_discord_desc": "för att dela dina egna foton. \nLägg upp dem i",
|
||||
"photo_by": "Foto av"
|
||||
"photo_by": "Foto av",
|
||||
"change_password_error": "Det går inte att ändra lösenord. \nOgiltigt nuvarande lösenord eller ogiltigt nytt lösenord.",
|
||||
"current_password": "Aktuellt lösenord",
|
||||
"password_change_lopout_warning": "Du kommer att loggas ut efter att du har ändrat ditt lösenord.",
|
||||
"authenticator_code": "Autentiseringskod",
|
||||
"copy": "Kopiera",
|
||||
"disable_mfa": "Inaktivera MFA",
|
||||
"email_added": "E-post har lagts till!",
|
||||
"email_added_error": "Det gick inte att lägga till e-post",
|
||||
"email_removed": "E-post har tagits bort!",
|
||||
"email_removed_error": "Det gick inte att ta bort e-post",
|
||||
"email_set_primary": "E-post har angetts som primärt!",
|
||||
"email_set_primary_error": "Det gick inte att ställa in e-post som primär",
|
||||
"email_verified": "E-post har verifierats!",
|
||||
"email_verified_erorr_desc": "Din e-postadress kunde inte verifieras. \nFörsök igen.",
|
||||
"email_verified_error": "Fel vid verifiering av e-post",
|
||||
"email_verified_success": "Din e-postadress har verifierats. \nDu kan nu logga in.",
|
||||
"enable_mfa": "Aktivera MFA",
|
||||
"error_change_password": "Fel vid byte av lösenord. \nKontrollera ditt nuvarande lösenord och försök igen.",
|
||||
"generic_error": "Ett fel uppstod när din begäran bearbetades.",
|
||||
"invalid_code": "Ogiltig MFA-kod",
|
||||
"invalid_credentials": "Ogiltigt användarnamn eller lösenord",
|
||||
"make_primary": "Gör Primär",
|
||||
"mfa_disabled": "Multifaktorautentisering har inaktiverats!",
|
||||
"mfa_enabled": "Multifaktorautentisering har aktiverats!",
|
||||
"mfa_not_enabled": "MFA är inte aktiverat",
|
||||
"mfa_page_title": "Multifaktorautentisering",
|
||||
"mfa_required": "Flerfaktorsautentisering krävs",
|
||||
"no_emai_set": "Ingen e-post inställd",
|
||||
"not_verified": "Ej verifierad",
|
||||
"primary": "Primär",
|
||||
"recovery_codes": "Återställningskoder",
|
||||
"recovery_codes_desc": "Det här är dina återställningskoder. \nHåll dem säkra. \nDu kommer inte att kunna se dem igen.",
|
||||
"reset_session_error": "Logga ut och in igen för att uppdatera din session och försök igen.",
|
||||
"verified": "Verifierad",
|
||||
"verify": "Kontrollera",
|
||||
"verify_email_error": "Fel vid verifiering av e-post. \nFörsök igen om några minuter.",
|
||||
"verify_email_success": "E-postverifiering har skickats!",
|
||||
"add_email_blocked": "Du kan inte lägga till en e-postadress till ett konto som skyddas av tvåfaktorsautentisering.",
|
||||
"required": "Detta fält är obligatoriskt",
|
||||
"csrf_failed": "Det gick inte att hämta CSRF-token",
|
||||
"duplicate_email": "Den här e-postadressen används redan.",
|
||||
"email_taken": "Den här e-postadressen används redan.",
|
||||
"username_taken": "Detta användarnamn används redan."
|
||||
},
|
||||
"checklist": {
|
||||
"add_item": "Lägg till objekt",
|
||||
|
@ -411,5 +467,14 @@
|
|||
"no_categories_found": "Inga kategorier hittades.",
|
||||
"select_category": "Välj Kategori",
|
||||
"update_after_refresh": "Äventyrskorten kommer att uppdateras när du uppdaterar sidan."
|
||||
},
|
||||
"dashboard": {
|
||||
"add_some": "Varför inte börja planera ditt nästa äventyr? \nDu kan lägga till ett nytt äventyr genom att klicka på knappen nedan.",
|
||||
"countries_visited": "Besökta länder",
|
||||
"no_recent_adventures": "Inga nya äventyr?",
|
||||
"recent_adventures": "Senaste äventyr",
|
||||
"total_adventures": "Totala äventyr",
|
||||
"total_visited_regions": "Totalt antal besökta regioner",
|
||||
"welcome_back": "Välkommen tillbaka"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,7 +188,14 @@
|
|||
"add_a_tag": "添加标签",
|
||||
"tags": "标签",
|
||||
"set_to_pin": "设置为固定",
|
||||
"category_fetch_error": "获取类别时出错"
|
||||
"category_fetch_error": "获取类别时出错",
|
||||
"copied_to_clipboard": "已复制到剪贴板!",
|
||||
"copy_failed": "复制失败",
|
||||
"adventure_calendar": "冒险日历",
|
||||
"emoji_picker": "表情符号选择器",
|
||||
"hide": "隐藏",
|
||||
"show": "展示",
|
||||
"download_calendar": "下载日历"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "轻松发现、规划和探索",
|
||||
|
@ -226,14 +233,16 @@
|
|||
"light": "光",
|
||||
"night": "夜晚",
|
||||
"aestheticDark": "审美黑暗",
|
||||
"aestheticLight": "美学之光"
|
||||
"aestheticLight": "美学之光",
|
||||
"northernLights": "北极光"
|
||||
},
|
||||
"users": "用户",
|
||||
"worldtravel": "环球旅行",
|
||||
"my_tags": "我的标签",
|
||||
"tag": "标签",
|
||||
"language_selection": "语言",
|
||||
"support": "支持"
|
||||
"support": "支持",
|
||||
"calendar": "日历"
|
||||
},
|
||||
"auth": {
|
||||
"forgot_password": "忘记密码?",
|
||||
|
@ -249,7 +258,11 @@
|
|||
"registration_disabled": "目前已禁用注册。",
|
||||
"profile_picture": "个人资料图片",
|
||||
"public_profile": "公开资料",
|
||||
"public_tooltip": "通过公开个人资料,用户可以与您共享收藏并在用户页面上查看您的个人资料。"
|
||||
"public_tooltip": "通过公开个人资料,用户可以与您共享收藏并在用户页面上查看您的个人资料。",
|
||||
"email_required": "电子邮件为必填项",
|
||||
"both_passwords_required": "两个密码都需要",
|
||||
"new_password": "新密码",
|
||||
"reset_failed": "重置密码失败"
|
||||
},
|
||||
"worldtravel": {
|
||||
"all": "全部",
|
||||
|
@ -291,7 +304,50 @@
|
|||
"about_this_background": "关于这个背景",
|
||||
"join_discord": "加入不和谐",
|
||||
"join_discord_desc": "分享您自己的照片。\n将它们张贴在",
|
||||
"photo_by": "摄影:"
|
||||
"photo_by": "摄影:",
|
||||
"change_password_error": "无法更改密码。\n当前密码无效或新密码无效。",
|
||||
"current_password": "当前密码",
|
||||
"password_change_lopout_warning": "更改密码后您将退出。",
|
||||
"authenticator_code": "验证码",
|
||||
"copy": "复制",
|
||||
"disable_mfa": "禁用 MFA",
|
||||
"email_added": "邮箱添加成功!",
|
||||
"email_added_error": "添加电子邮件时出错",
|
||||
"email_removed": "电子邮件删除成功!",
|
||||
"email_removed_error": "删除电子邮件时出错",
|
||||
"email_set_primary": "成功将电子邮件设置为主!",
|
||||
"email_set_primary_error": "将电子邮件设置为主要电子邮件时出错",
|
||||
"email_verified": "邮箱验证成功!",
|
||||
"email_verified_erorr_desc": "无法验证您的电子邮件。\n请再试一次。",
|
||||
"email_verified_error": "验证电子邮件时出错",
|
||||
"email_verified_success": "您的电子邮件已被验证。\n您现在可以登录了。",
|
||||
"enable_mfa": "启用 MFA",
|
||||
"error_change_password": "更改密码时出错。\n请检查您当前的密码,然后重试。",
|
||||
"generic_error": "处理您的请求时发生错误。",
|
||||
"invalid_code": "MFA 代码无效",
|
||||
"invalid_credentials": "用户名或密码无效",
|
||||
"make_primary": "设为主要",
|
||||
"mfa_disabled": "多重身份验证已成功禁用!",
|
||||
"mfa_enabled": "多重身份验证启用成功!",
|
||||
"mfa_not_enabled": "MFA 未启用",
|
||||
"mfa_page_title": "多重身份验证",
|
||||
"mfa_required": "需要多重身份验证",
|
||||
"no_emai_set": "没有设置电子邮件",
|
||||
"not_verified": "未验证",
|
||||
"primary": "基本的",
|
||||
"recovery_codes": "恢复代码",
|
||||
"recovery_codes_desc": "这些是您的恢复代码。\n确保他们的安全。\n你将无法再见到他们。",
|
||||
"reset_session_error": "请注销并重新登录以刷新您的会话,然后重试。",
|
||||
"verified": "已验证",
|
||||
"verify": "核实",
|
||||
"verify_email_error": "验证电子邮件时出错。\n几分钟后重试。",
|
||||
"verify_email_success": "邮箱验证发送成功!",
|
||||
"add_email_blocked": "您无法将电子邮件地址添加到受双因素身份验证保护的帐户。",
|
||||
"required": "此字段是必需的",
|
||||
"csrf_failed": "获取 CSRF 令牌失败",
|
||||
"duplicate_email": "该电子邮件地址已被使用。",
|
||||
"email_taken": "该电子邮件地址已被使用。",
|
||||
"username_taken": "该用户名已被使用。"
|
||||
},
|
||||
"checklist": {
|
||||
"add_item": "添加项目",
|
||||
|
@ -411,5 +467,14 @@
|
|||
"no_categories_found": "未找到类别。",
|
||||
"select_category": "选择类别",
|
||||
"update_after_refresh": "刷新页面后,冒险卡将更新。"
|
||||
},
|
||||
"dashboard": {
|
||||
"add_some": "为什么不开始计划你的下一次冒险呢?\n您可以通过单击下面的按钮添加新的冒险。",
|
||||
"countries_visited": "访问国家",
|
||||
"no_recent_adventures": "最近没有冒险吗?",
|
||||
"recent_adventures": "最近的冒险",
|
||||
"total_adventures": "全面冒险",
|
||||
"total_visited_regions": "总访问地区",
|
||||
"welcome_back": "欢迎回来"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
import { redirect, type Actions } from '@sveltejs/kit';
|
||||
import { themes } from '$lib';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
export const load = (async (event) => {
|
||||
if (event.locals.user) {
|
||||
return redirect(302, '/dashboard');
|
||||
}
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
export const actions: Actions = {
|
||||
setTheme: async ({ url, cookies }) => {
|
||||
const theme = url.searchParams.get('theme');
|
||||
|
@ -16,23 +24,24 @@ export const actions: Actions = {
|
|||
});
|
||||
}
|
||||
},
|
||||
logout: async ({ cookies }: { cookies: any }) => {
|
||||
const cookie = cookies.get('auth') || null;
|
||||
logout: async (event) => {
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
|
||||
if (!cookie) {
|
||||
if (!sessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`${serverEndpoint}/auth/logout/`, {
|
||||
method: 'POST',
|
||||
const res = await fetch(`${serverEndpoint}/_allauth/browser/v1/auth/session`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: cookies.get('auth')
|
||||
}
|
||||
Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
if (res.ok) {
|
||||
cookies.delete('auth', { path: '/', secure: false });
|
||||
cookies.delete('refresh', { path: '/', secure: false });
|
||||
if (res.status == 401) {
|
||||
return redirect(302, '/login');
|
||||
} else {
|
||||
return redirect(302, '/');
|
||||
|
|
94
frontend/src/routes/_allauth/[...path]/+server.ts
Normal file
94
frontend/src/routes/_allauth/[...path]/+server.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function GET(event) {
|
||||
const { url, params, request, fetch, cookies } = event;
|
||||
const searchParam = url.search ? `${url.search}&format=json` : '?format=json';
|
||||
return handleRequest(url, params, request, fetch, cookies, searchParam);
|
||||
}
|
||||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function POST({ url, params, request, fetch, cookies }) {
|
||||
const searchParam = url.search ? `${url.search}` : '';
|
||||
return handleRequest(url, params, request, fetch, cookies, searchParam, false);
|
||||
}
|
||||
|
||||
export async function PATCH({ url, params, request, fetch, cookies }) {
|
||||
const searchParam = url.search ? `${url.search}` : '';
|
||||
return handleRequest(url, params, request, fetch, cookies, searchParam, false);
|
||||
}
|
||||
|
||||
export async function PUT({ url, params, request, fetch, cookies }) {
|
||||
const searchParam = url.search ? `${url.search}` : '';
|
||||
return handleRequest(url, params, request, fetch, cookies, searchParam, false);
|
||||
}
|
||||
|
||||
export async function DELETE({ url, params, request, fetch, cookies }) {
|
||||
const searchParam = url.search ? `${url.search}` : '';
|
||||
return handleRequest(url, params, request, fetch, cookies, searchParam, false);
|
||||
}
|
||||
|
||||
async function handleRequest(
|
||||
url: any,
|
||||
params: any,
|
||||
request: any,
|
||||
fetch: any,
|
||||
cookies: any,
|
||||
searchParam: string,
|
||||
requreTrailingSlash: boolean | undefined = false
|
||||
) {
|
||||
const path = params.path;
|
||||
let targetUrl = `${endpoint}/_allauth/${path}`;
|
||||
|
||||
// Ensure the path ends with a trailing slash
|
||||
if (requreTrailingSlash && !targetUrl.endsWith('/')) {
|
||||
targetUrl += '/';
|
||||
}
|
||||
|
||||
// Append query parameters to the path correctly
|
||||
targetUrl += searchParam; // This will add ?format=json or &format=json to the URL
|
||||
|
||||
const headers = new Headers(request.headers);
|
||||
|
||||
const csrfToken = await fetchCSRFToken();
|
||||
if (!csrfToken) {
|
||||
return json({ error: 'CSRF token is missing or invalid' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl, {
|
||||
method: request.method,
|
||||
headers: {
|
||||
...Object.fromEntries(headers),
|
||||
'X-CSRFToken': csrfToken,
|
||||
Cookie: `csrftoken=${csrfToken}`
|
||||
},
|
||||
body:
|
||||
request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined,
|
||||
credentials: 'include' // This line ensures cookies are sent with the request
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: response.headers
|
||||
});
|
||||
}
|
||||
|
||||
const responseData = await response.text();
|
||||
// Create a new Headers object without the 'set-cookie' header
|
||||
const cleanHeaders = new Headers(response.headers);
|
||||
cleanHeaders.delete('set-cookie');
|
||||
|
||||
return new Response(responseData, {
|
||||
status: response.status,
|
||||
headers: cleanHeaders
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error forwarding request:', error);
|
||||
return json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { redirect, type Actions } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
|
@ -7,13 +8,16 @@ export const load = (async (event) => {
|
|||
if (!event.locals.user) {
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
let allActivities: string[] = [];
|
||||
let res = await fetch(`${endpoint}/api/activity-types/types/`, {
|
||||
let res = await event.fetch(`${endpoint}/api/activity-types/types/`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
}
|
||||
'X-CSRFToken': csrfToken,
|
||||
Cookie: `csrftoken=${csrfToken}`
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
console.log(res);
|
||||
let data = await res.json();
|
||||
if (data) {
|
||||
allActivities = data;
|
||||
|
@ -27,13 +31,16 @@ export const load = (async (event) => {
|
|||
|
||||
export const actions: Actions = {
|
||||
getActivities: async (event) => {
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
let allActivities: string[] = [];
|
||||
let res = await fetch(`${endpoint}/api/activity-types/types/`, {
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
Cookie: `csrftoken=${csrfToken}`
|
||||
}
|
||||
});
|
||||
console.log(res);
|
||||
let data = await res.json();
|
||||
if (data) {
|
||||
allActivities = data;
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '../data/$types';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
let allActivities: string[] = [];
|
||||
let res = await fetch(`${endpoint}/api/activity-types/types/`, {
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
let res = await event.fetch(`${endpoint}/api/activity-types/types/`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
}
|
||||
'X-CSRFToken': csrfToken,
|
||||
Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
let data = await res.json();
|
||||
if (data) {
|
||||
|
|
|
@ -4,8 +4,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
|||
import type { Adventure } from '$lib/types';
|
||||
|
||||
import type { Actions } from '@sveltejs/kit';
|
||||
import { fetchCSRFToken, tryRefreshToken } from '$lib/index.server';
|
||||
import { checkLink } from '$lib';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
|
||||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
|
@ -29,12 +28,13 @@ export const load = (async (event) => {
|
|||
const page = event.url.searchParams.get('page') || '1';
|
||||
const is_visited = event.url.searchParams.get('is_visited') || 'all';
|
||||
|
||||
let initialFetch = await fetch(
|
||||
let initialFetch = await event.fetch(
|
||||
`${serverEndpoint}/api/adventures/filtered?types=${typeString}&order_by=${order_by}&order_direction=${order_direction}&include_collections=${include_collections}&page=${page}&is_visited=${is_visited}`,
|
||||
{
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
}
|
||||
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||
},
|
||||
credentials: 'include'
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -61,371 +61,15 @@ export const load = (async (event) => {
|
|||
}) satisfies PageServerLoad;
|
||||
|
||||
export const actions: Actions = {
|
||||
create: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
|
||||
const type = formData.get('type') as string;
|
||||
const name = formData.get('name') as string;
|
||||
const location = formData.get('location') as string | null;
|
||||
let date = (formData.get('date') as string | null) ?? null;
|
||||
const description = formData.get('description') as string | null;
|
||||
const activity_types = formData.get('activity_types')
|
||||
? (formData.get('activity_types') as string).split(',')
|
||||
: null;
|
||||
const rating = formData.get('rating') ? Number(formData.get('rating')) : null;
|
||||
let link = formData.get('link') as string | null;
|
||||
let latitude = formData.get('latitude') as string | null;
|
||||
let longitude = formData.get('longitude') as string | null;
|
||||
let collection = formData.get('collection') as string | null;
|
||||
let is_public = formData.get('is_public') as string | null | boolean;
|
||||
|
||||
if (is_public) {
|
||||
is_public = true;
|
||||
} else {
|
||||
is_public = false;
|
||||
}
|
||||
|
||||
// check if latitude and longitude are valid
|
||||
if (latitude && longitude) {
|
||||
if (isNaN(Number(latitude)) || isNaN(Number(longitude))) {
|
||||
return {
|
||||
status: 400,
|
||||
body: { error: 'Invalid latitude or longitude' }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// round latitude and longitude to 6 decimal places
|
||||
if (latitude) {
|
||||
latitude = Number(latitude).toFixed(6);
|
||||
}
|
||||
if (longitude) {
|
||||
longitude = Number(longitude).toFixed(6);
|
||||
}
|
||||
|
||||
const image = formData.get('image') as File;
|
||||
|
||||
if (!type || !name) {
|
||||
return {
|
||||
status: 400,
|
||||
body: { error: 'Missing required fields' }
|
||||
};
|
||||
}
|
||||
|
||||
if (date == null || date == '') {
|
||||
date = null;
|
||||
}
|
||||
|
||||
if (link) {
|
||||
link = checkLink(link);
|
||||
}
|
||||
|
||||
const formDataToSend = new FormData();
|
||||
formDataToSend.append('type', type);
|
||||
formDataToSend.append('name', name);
|
||||
formDataToSend.append('location', location || '');
|
||||
formDataToSend.append('date', date || '');
|
||||
formDataToSend.append('description', description || '');
|
||||
formDataToSend.append('latitude', latitude || '');
|
||||
formDataToSend.append('longitude', longitude || '');
|
||||
formDataToSend.append('is_public', is_public.toString());
|
||||
|
||||
if (!isNaN(Number(collection))) {
|
||||
if (collection !== null) {
|
||||
formDataToSend.append('collection', collection);
|
||||
}
|
||||
}
|
||||
|
||||
if (activity_types) {
|
||||
// Filter out empty and duplicate activity types, then trim each activity type
|
||||
const cleanedActivityTypes = Array.from(
|
||||
new Set(
|
||||
activity_types
|
||||
.map((activity_type) => activity_type.trim())
|
||||
.filter((activity_type) => activity_type !== '' && activity_type !== ',')
|
||||
)
|
||||
);
|
||||
|
||||
// Append each cleaned activity type to formDataToSend
|
||||
cleanedActivityTypes.forEach((activity_type) => {
|
||||
formDataToSend.append('activity_types', activity_type);
|
||||
});
|
||||
}
|
||||
formDataToSend.append('rating', rating ? rating.toString() : '');
|
||||
formDataToSend.append('link', link || '');
|
||||
// formDataToSend.append('image', image);
|
||||
|
||||
// log each key-value pair in the FormData
|
||||
for (let pair of formDataToSend.entries()) {
|
||||
console.log(pair[0] + ', ' + pair[1]);
|
||||
}
|
||||
|
||||
let auth = event.cookies.get('auth');
|
||||
|
||||
if (!auth) {
|
||||
const refresh = event.cookies.get('refresh');
|
||||
if (!refresh) {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
let res = await tryRefreshToken(refresh);
|
||||
if (res) {
|
||||
auth = res;
|
||||
event.cookies.set('auth', auth, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth) {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
|
||||
const csrfToken = await fetchCSRFToken();
|
||||
|
||||
if (!csrfToken) {
|
||||
return {
|
||||
status: 500,
|
||||
body: { message: 'Failed to fetch CSRF token' }
|
||||
};
|
||||
}
|
||||
|
||||
const res = await fetch(`${serverEndpoint}/api/adventures/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
Cookie: auth
|
||||
},
|
||||
body: formDataToSend
|
||||
});
|
||||
|
||||
let new_id = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
const errorBody = await res.json();
|
||||
return {
|
||||
status: res.status,
|
||||
body: { error: errorBody }
|
||||
};
|
||||
}
|
||||
|
||||
let id = new_id.id;
|
||||
let user_id = new_id.user_id;
|
||||
let image_url = new_id.image;
|
||||
let link_url = new_id.link;
|
||||
|
||||
if (image && image.size > 0) {
|
||||
let imageForm = new FormData();
|
||||
imageForm.append('image', image);
|
||||
imageForm.append('adventure', id);
|
||||
let imageRes = await fetch(`${serverEndpoint}/api/images/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
},
|
||||
body: imageForm
|
||||
});
|
||||
let data = await imageRes.json();
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
return { id, user_id, image_url, link };
|
||||
},
|
||||
edit: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
|
||||
const adventureId = formData.get('adventureId') as string;
|
||||
const type = formData.get('type') as string;
|
||||
const name = formData.get('name') as string;
|
||||
const location = formData.get('location') as string | null;
|
||||
let date = (formData.get('date') as string | null) ?? null;
|
||||
const description = formData.get('description') as string | null;
|
||||
let activity_types = formData.get('activity_types')
|
||||
? (formData.get('activity_types') as string).split(',')
|
||||
: null;
|
||||
const rating = formData.get('rating') ? Number(formData.get('rating')) : null;
|
||||
let link = formData.get('link') as string | null;
|
||||
let latitude = formData.get('latitude') as string | null;
|
||||
let longitude = formData.get('longitude') as string | null;
|
||||
let is_public = formData.get('is_public') as string | null | boolean;
|
||||
|
||||
if (is_public) {
|
||||
is_public = true;
|
||||
} else {
|
||||
is_public = false;
|
||||
}
|
||||
|
||||
// check if latitude and longitude are valid
|
||||
if (latitude && longitude) {
|
||||
if (isNaN(Number(latitude)) || isNaN(Number(longitude))) {
|
||||
return {
|
||||
status: 400,
|
||||
body: { error: 'Invalid latitude or longitude' }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// round latitude and longitude to 6 decimal places
|
||||
if (latitude) {
|
||||
latitude = Number(latitude).toFixed(6);
|
||||
}
|
||||
if (longitude) {
|
||||
longitude = Number(longitude).toFixed(6);
|
||||
}
|
||||
|
||||
const image = formData.get('image') as File;
|
||||
|
||||
// console.log(activity_types);
|
||||
|
||||
if (!type || !name) {
|
||||
return {
|
||||
status: 400,
|
||||
body: { error: 'Missing required fields' }
|
||||
};
|
||||
}
|
||||
|
||||
if (date == null || date == '') {
|
||||
date = null;
|
||||
}
|
||||
|
||||
if (link) {
|
||||
link = checkLink(link);
|
||||
}
|
||||
|
||||
const formDataToSend = new FormData();
|
||||
formDataToSend.append('type', type);
|
||||
formDataToSend.append('name', name);
|
||||
formDataToSend.append('location', location || '');
|
||||
formDataToSend.append('date', date || '');
|
||||
formDataToSend.append('description', description || '');
|
||||
formDataToSend.append('latitude', latitude || '');
|
||||
formDataToSend.append('longitude', longitude || '');
|
||||
formDataToSend.append('is_public', is_public.toString());
|
||||
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
|
||||
if (activity_types) {
|
||||
// Filter out empty and duplicate activity types, then trim each activity type
|
||||
const cleanedActivityTypes = Array.from(
|
||||
new Set(
|
||||
activity_types
|
||||
.map((activity_type) => activity_type.trim())
|
||||
.filter((activity_type) => activity_type !== '' && activity_type !== ',')
|
||||
)
|
||||
);
|
||||
|
||||
// Append each cleaned activity type to formDataToSend
|
||||
cleanedActivityTypes.forEach((activity_type) => {
|
||||
formDataToSend.append('activity_types', activity_type);
|
||||
});
|
||||
} else {
|
||||
let res = await fetch(`${serverEndpoint}/api/adventures/${adventureId}/`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`,
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ activity_types: [] })
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errorBody = await res.json();
|
||||
return {
|
||||
status: res.status,
|
||||
body: { error: errorBody }
|
||||
};
|
||||
}
|
||||
}
|
||||
formDataToSend.append('rating', rating ? rating.toString() : '');
|
||||
formDataToSend.append('link', link || '');
|
||||
|
||||
if (image && image.size > 0) {
|
||||
formDataToSend.append('image', image);
|
||||
}
|
||||
|
||||
let auth = event.cookies.get('auth');
|
||||
|
||||
if (!auth) {
|
||||
const refresh = event.cookies.get('refresh');
|
||||
if (!refresh) {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
let res = await tryRefreshToken(refresh);
|
||||
if (res) {
|
||||
auth = res;
|
||||
event.cookies.set('auth', auth, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth) {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
|
||||
if (!csrfToken) {
|
||||
return {
|
||||
status: 500,
|
||||
body: { message: 'Failed to fetch CSRF token' }
|
||||
};
|
||||
}
|
||||
|
||||
const res = await fetch(`${serverEndpoint}/api/adventures/${adventureId}/`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
Cookie: auth
|
||||
},
|
||||
body: formDataToSend
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorBody = await res.json();
|
||||
return {
|
||||
status: res.status,
|
||||
body: { error: errorBody }
|
||||
};
|
||||
}
|
||||
|
||||
let adventure = await res.json();
|
||||
|
||||
let image_url = adventure.image;
|
||||
let link_url = adventure.link;
|
||||
return { image_url, link_url };
|
||||
},
|
||||
image: async (event) => {
|
||||
let formData = await event.request.formData();
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
let res = await fetch(`${serverEndpoint}/api/images/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`,
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
|
|
@ -226,7 +226,6 @@
|
|||
{#each adventures as adventure}
|
||||
<AdventureCard
|
||||
user={data.user}
|
||||
type={adventure.type}
|
||||
{adventure}
|
||||
on:delete={deleteAdventure}
|
||||
on:edit={editAdventure}
|
||||
|
|
|
@ -7,8 +7,9 @@ export const load = (async (event) => {
|
|||
const id = event.params as { id: string };
|
||||
let request = await fetch(`${endpoint}/api/adventures/${id.id}/`, {
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
}
|
||||
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!request.ok) {
|
||||
console.error('Failed to fetch adventure ' + id.id);
|
||||
|
@ -24,8 +25,9 @@ export const load = (async (event) => {
|
|||
if (adventure.collection) {
|
||||
let res2 = await fetch(`${endpoint}/api/collections/${adventure.collection}/`, {
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
}
|
||||
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
collection = await res2.json();
|
||||
}
|
||||
|
@ -39,8 +41,8 @@ export const load = (async (event) => {
|
|||
}
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
import type { Actions } from '@sveltejs/kit';
|
||||
import { tryRefreshToken } from '$lib/index.server';
|
||||
import { redirect, type Actions } from '@sveltejs/kit';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
|
||||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
|
@ -50,29 +52,7 @@ export const actions: Actions = {
|
|||
const adventureId = id.id;
|
||||
|
||||
if (!event.locals.user) {
|
||||
const refresh = event.cookies.get('refresh');
|
||||
let auth = event.cookies.get('auth');
|
||||
if (!refresh) {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
let res = await tryRefreshToken(refresh);
|
||||
if (res) {
|
||||
auth = res;
|
||||
event.cookies.set('auth', auth, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
if (!adventureId) {
|
||||
return {
|
||||
|
@ -81,12 +61,15 @@ export const actions: Actions = {
|
|||
};
|
||||
}
|
||||
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
|
||||
let res = await fetch(`${serverEndpoint}/api/adventures/${event.params.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
Cookie: `sessionid=${event.cookies.get('sessionid')}; csrftoken=${csrfToken}`,
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
console.log(res);
|
||||
if (!res.ok) {
|
||||
|
|
|
@ -1,69 +1,77 @@
|
|||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function GET({ url, params, request, fetch, cookies }) {
|
||||
// add the param format = json to the url or add additional if anothre param is already present
|
||||
if (url.search) {
|
||||
url.search = url.search + '&format=json';
|
||||
} else {
|
||||
url.search = '?format=json';
|
||||
}
|
||||
return handleRequest(url, params, request, fetch, cookies);
|
||||
export async function GET(event) {
|
||||
const { url, params, request, fetch, cookies } = event;
|
||||
const searchParam = url.search ? `${url.search}&format=json` : '?format=json';
|
||||
return handleRequest(url, params, request, fetch, cookies, searchParam);
|
||||
}
|
||||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function POST({ url, params, request, fetch, cookies }) {
|
||||
return handleRequest(url, params, request, fetch, cookies, true);
|
||||
const searchParam = url.search ? `${url.search}&format=json` : '?format=json';
|
||||
return handleRequest(url, params, request, fetch, cookies, searchParam, true);
|
||||
}
|
||||
|
||||
export async function PATCH({ url, params, request, fetch, cookies }) {
|
||||
return handleRequest(url, params, request, fetch, cookies, true);
|
||||
const searchParam = url.search ? `${url.search}&format=json` : '?format=json';
|
||||
return handleRequest(url, params, request, fetch, cookies, searchParam, true);
|
||||
}
|
||||
|
||||
export async function PUT({ url, params, request, fetch, cookies }) {
|
||||
return handleRequest(url, params, request, fetch, cookies, true);
|
||||
const searchParam = url.search ? `${url.search}&format=json` : '?format=json';
|
||||
return handleRequest(url, params, request, fetch, cookies, searchParam, true);
|
||||
}
|
||||
|
||||
export async function DELETE({ url, params, request, fetch, cookies }) {
|
||||
return handleRequest(url, params, request, fetch, cookies, true);
|
||||
const searchParam = url.search ? `${url.search}&format=json` : '?format=json';
|
||||
return handleRequest(url, params, request, fetch, cookies, searchParam, true);
|
||||
}
|
||||
|
||||
// Implement other HTTP methods as needed (PUT, DELETE, etc.)
|
||||
|
||||
async function handleRequest(
|
||||
url: any,
|
||||
params: any,
|
||||
request: any,
|
||||
fetch: any,
|
||||
cookies: any,
|
||||
searchParam: string,
|
||||
requreTrailingSlash: boolean | undefined = false
|
||||
) {
|
||||
const path = params.path;
|
||||
let targetUrl = `${endpoint}/api/${path}${url.search}`;
|
||||
let targetUrl = `${endpoint}/api/${path}`;
|
||||
|
||||
// Ensure the path ends with a trailing slash
|
||||
if (requreTrailingSlash && !targetUrl.endsWith('/')) {
|
||||
targetUrl += '/';
|
||||
}
|
||||
|
||||
// Append query parameters to the path correctly
|
||||
targetUrl += searchParam; // This will add ?format=json or &format=json to the URL
|
||||
|
||||
const headers = new Headers(request.headers);
|
||||
|
||||
const authCookie = cookies.get('auth');
|
||||
|
||||
if (authCookie) {
|
||||
headers.set('Cookie', `${authCookie}`);
|
||||
const csrfToken = await fetchCSRFToken();
|
||||
if (!csrfToken) {
|
||||
return json({ error: 'CSRF token is missing or invalid' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl, {
|
||||
method: request.method,
|
||||
headers: headers,
|
||||
body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined
|
||||
headers: {
|
||||
...Object.fromEntries(headers),
|
||||
'X-CSRFToken': csrfToken,
|
||||
Cookie: `csrftoken=${csrfToken}`
|
||||
},
|
||||
body:
|
||||
request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined,
|
||||
credentials: 'include' // This line ensures cookies are sent with the request
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
// For 204 No Content, return a response with no body
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: response.headers
|
||||
|
@ -71,10 +79,13 @@ async function handleRequest(
|
|||
}
|
||||
|
||||
const responseData = await response.text();
|
||||
// Create a new Headers object without the 'set-cookie' header
|
||||
const cleanHeaders = new Headers(response.headers);
|
||||
cleanHeaders.delete('set-cookie');
|
||||
|
||||
return new Response(responseData, {
|
||||
status: response.status,
|
||||
headers: response.headers
|
||||
headers: cleanHeaders
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error forwarding request:', error);
|
||||
|
|
47
frontend/src/routes/calendar/+page.server.ts
Normal file
47
frontend/src/routes/calendar/+page.server.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import type { Adventure } from '$lib/types';
|
||||
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 = (async (event) => {
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
let visitedFetch = await fetch(`${endpoint}/api/adventures/all/?include_collections=true`, {
|
||||
headers: {
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
let adventures = (await visitedFetch.json()) as Adventure[];
|
||||
|
||||
let dates: Array<{
|
||||
id: string;
|
||||
start: string;
|
||||
end: string;
|
||||
title: string;
|
||||
backgroundColor?: string;
|
||||
}> = [];
|
||||
adventures.forEach((adventure) => {
|
||||
adventure.visits.forEach((visit) => {
|
||||
dates.push({
|
||||
id: adventure.id,
|
||||
start: visit.start_date,
|
||||
end: visit.end_date || visit.start_date,
|
||||
title: adventure.name + (adventure.category?.icon ? ' ' + adventure.category.icon : '')
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let icsFetch = await fetch(`${endpoint}/api/ics-calendar/generate`, {
|
||||
headers: {
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
let ics_calendar = await icsFetch.text();
|
||||
|
||||
return {
|
||||
props: {
|
||||
adventures,
|
||||
dates,
|
||||
ics_calendar
|
||||
}
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
37
frontend/src/routes/calendar/+page.svelte
Normal file
37
frontend/src/routes/calendar/+page.svelte
Normal file
|
@ -0,0 +1,37 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
|
||||
// @ts-ignore
|
||||
import Calendar from '@event-calendar/core';
|
||||
// @ts-ignore
|
||||
import TimeGrid from '@event-calendar/time-grid';
|
||||
// @ts-ignore
|
||||
import DayGrid from '@event-calendar/day-grid';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let adventures = data.props.adventures;
|
||||
let dates = data.props.dates;
|
||||
|
||||
let icsCalendar = data.props.ics_calendar;
|
||||
// turn the ics calendar into a data URL
|
||||
let icsCalendarDataUrl = URL.createObjectURL(new Blob([icsCalendar], { type: 'text/calendar' }));
|
||||
|
||||
let plugins = [TimeGrid, DayGrid];
|
||||
let options = {
|
||||
view: 'dayGridMonth',
|
||||
events: [...dates]
|
||||
};
|
||||
</script>
|
||||
|
||||
<h1 class="text-center text-2xl font-bold">{$t('adventures.adventure_calendar')}</h1>
|
||||
|
||||
<Calendar {plugins} {options} />
|
||||
|
||||
<!-- download calendar -->
|
||||
<div class="flex items-center justify-center mt-4">
|
||||
<a href={icsCalendarDataUrl} download="adventures.ics" class="btn btn-primary"
|
||||
>Download Calendar</a
|
||||
>
|
||||
</div>
|
|
@ -4,7 +4,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
|||
import type { Adventure, Collection } from '$lib/types';
|
||||
|
||||
import type { Actions, RequestEvent } from '@sveltejs/kit';
|
||||
import { fetchCSRFToken, tryRefreshToken } from '$lib/index.server';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
import { checkLink } from '$lib';
|
||||
|
||||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
@ -17,10 +17,12 @@ export const load = (async (event) => {
|
|||
let previous = null;
|
||||
let count = 0;
|
||||
let adventures: Adventure[] = [];
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
let initialFetch = await fetch(`${serverEndpoint}/api/collections/?order_by=updated_at`, {
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
}
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!initialFetch.ok) {
|
||||
console.error('Failed to fetch visited adventures');
|
||||
|
@ -72,34 +74,9 @@ export const actions: Actions = {
|
|||
formDataToSend.append('start_date', start_date || '');
|
||||
formDataToSend.append('end_date', end_date || '');
|
||||
formDataToSend.append('link', link || '');
|
||||
let auth = event.cookies.get('auth');
|
||||
let sessionid = event.cookies.get('sessionid');
|
||||
|
||||
if (!auth) {
|
||||
const refresh = event.cookies.get('refresh');
|
||||
if (!refresh) {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
let res = await tryRefreshToken(refresh);
|
||||
if (res) {
|
||||
auth = res;
|
||||
event.cookies.set('auth', auth, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth) {
|
||||
if (!sessionid) {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
|
@ -119,7 +96,7 @@ export const actions: Actions = {
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
Cookie: auth
|
||||
Cookie: `sessionid=${sessionid}; csrftoken=${csrfToken}`
|
||||
},
|
||||
body: formDataToSend
|
||||
});
|
||||
|
@ -175,34 +152,9 @@ export const actions: Actions = {
|
|||
formDataToSend.append('end_date', end_date || '');
|
||||
formDataToSend.append('link', link || '');
|
||||
|
||||
let auth = event.cookies.get('auth');
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
|
||||
if (!auth) {
|
||||
const refresh = event.cookies.get('refresh');
|
||||
if (!refresh) {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
let res = await tryRefreshToken(refresh);
|
||||
if (res) {
|
||||
auth = res;
|
||||
event.cookies.set('auth', auth, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth) {
|
||||
if (!sessionId) {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
|
@ -222,9 +174,10 @@ export const actions: Actions = {
|
|||
method: 'PATCH',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
Cookie: auth
|
||||
Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`
|
||||
},
|
||||
body: formDataToSend
|
||||
body: formDataToSend,
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
@ -241,6 +194,10 @@ export const actions: Actions = {
|
|||
},
|
||||
get: async (event) => {
|
||||
if (!event.locals.user) {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
|
||||
const formData = await event.request.formData();
|
||||
|
@ -263,19 +220,20 @@ export const actions: Actions = {
|
|||
let previous = null;
|
||||
let count = 0;
|
||||
|
||||
let visitedFetch = await fetch(
|
||||
let collectionsFetch = await fetch(
|
||||
`${serverEndpoint}/api/collections/?order_by=${order_by}&order_direction=${order_direction}`,
|
||||
{
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
}
|
||||
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||
},
|
||||
credentials: 'include'
|
||||
}
|
||||
);
|
||||
if (!visitedFetch.ok) {
|
||||
if (!collectionsFetch.ok) {
|
||||
console.error('Failed to fetch visited adventures');
|
||||
return redirect(302, '/login');
|
||||
} else {
|
||||
let res = await visitedFetch.json();
|
||||
let res = await collectionsFetch.json();
|
||||
let visited = res.results as Adventure[];
|
||||
next = res.next;
|
||||
previous = res.previous;
|
||||
|
@ -332,15 +290,16 @@ export const actions: Actions = {
|
|||
}
|
||||
|
||||
const fullUrl = `${serverEndpoint}${url}`;
|
||||
console.log(fullUrl);
|
||||
console.log(serverEndpoint);
|
||||
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
|
||||
try {
|
||||
const response = await fetch(fullUrl, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
}
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
|
@ -6,9 +6,10 @@ const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
|||
|
||||
export const load = (async (event) => {
|
||||
const id = event.params as { id: string };
|
||||
let sessionid = event.cookies.get('sessionid');
|
||||
let request = await fetch(`${endpoint}/api/collections/${id.id}/`, {
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
Cookie: `sessionid=${sessionid}`
|
||||
}
|
||||
});
|
||||
if (!request.ok) {
|
||||
|
@ -30,7 +31,7 @@ export const load = (async (event) => {
|
|||
}) satisfies PageServerLoad;
|
||||
|
||||
import type { Actions } from '@sveltejs/kit';
|
||||
import { tryRefreshToken } from '$lib/index.server';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
|
||||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
|
@ -39,31 +40,6 @@ export const actions: Actions = {
|
|||
const id = event.params as { id: string };
|
||||
const adventureId = id.id;
|
||||
|
||||
if (!event.locals.user) {
|
||||
const refresh = event.cookies.get('refresh');
|
||||
let auth = event.cookies.get('auth');
|
||||
if (!refresh) {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
let res = await tryRefreshToken(refresh);
|
||||
if (res) {
|
||||
auth = res;
|
||||
event.cookies.set('auth', auth, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
status: 401,
|
||||
body: { message: 'Unauthorized' }
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!adventureId) {
|
||||
return {
|
||||
status: 400,
|
||||
|
@ -71,15 +47,27 @@ export const actions: Actions = {
|
|||
};
|
||||
}
|
||||
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
|
||||
if (!sessionId) {
|
||||
return {
|
||||
status: 401,
|
||||
error: new Error('Unauthorized')
|
||||
};
|
||||
}
|
||||
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
|
||||
let res = await fetch(`${serverEndpoint}/api/collections/${event.params.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
console.log(res);
|
||||
if (!res.ok) {
|
||||
return {
|
||||
status: res.status,
|
||||
|
|
|
@ -398,7 +398,6 @@
|
|||
user={data.user}
|
||||
on:edit={editAdventure}
|
||||
on:delete={deleteAdventure}
|
||||
type={adventure.type}
|
||||
{adventure}
|
||||
{collection}
|
||||
/>
|
||||
|
@ -521,7 +520,6 @@
|
|||
user={data.user}
|
||||
on:edit={editAdventure}
|
||||
on:delete={deleteAdventure}
|
||||
type={adventure.type}
|
||||
{adventure}
|
||||
/>
|
||||
{/each}
|
||||
|
|
|
@ -8,13 +8,11 @@ export const load = (async (event) => {
|
|||
if (!event.locals.user) {
|
||||
return redirect(302, '/login');
|
||||
} else {
|
||||
let next = null;
|
||||
let previous = null;
|
||||
let count = 0;
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
let adventures: Adventure[] = [];
|
||||
let initialFetch = await fetch(`${serverEndpoint}/api/collections/archived/`, {
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
if (!initialFetch.ok) {
|
||||
|
|
53
frontend/src/routes/dashboard/+page.server.ts
Normal file
53
frontend/src/routes/dashboard/+page.server.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
import type { Adventure } from '$lib/types';
|
||||
|
||||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
export const load = (async (event) => {
|
||||
if (!event.locals.user) {
|
||||
return redirect(302, '/login');
|
||||
} else {
|
||||
let adventures: Adventure[] = [];
|
||||
|
||||
let initialFetch = await event.fetch(`${serverEndpoint}/api/adventures/`, {
|
||||
headers: {
|
||||
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
let stats = null;
|
||||
|
||||
let res = await event.fetch(`${serverEndpoint}/api/stats/counts/`, {
|
||||
headers: {
|
||||
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||
}
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error('Failed to fetch user stats');
|
||||
} else {
|
||||
stats = await res.json();
|
||||
}
|
||||
|
||||
if (!initialFetch.ok) {
|
||||
let error_message = await initialFetch.json();
|
||||
console.error(error_message);
|
||||
console.error('Failed to fetch visited adventures');
|
||||
return redirect(302, '/login');
|
||||
} else {
|
||||
let res = await initialFetch.json();
|
||||
let visited = res.results as Adventure[];
|
||||
// only get the first 3 adventures or less if there are less than 3
|
||||
adventures = visited.slice(0, 3);
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
adventures,
|
||||
stats
|
||||
}
|
||||
};
|
||||
}
|
||||
}) satisfies PageServerLoad;
|
102
frontend/src/routes/dashboard/+page.svelte
Normal file
102
frontend/src/routes/dashboard/+page.svelte
Normal file
|
@ -0,0 +1,102 @@
|
|||
<script lang="ts">
|
||||
import AdventureCard from '$lib/components/AdventureCard.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const user = data.user;
|
||||
const recentAdventures = data.props.adventures;
|
||||
const stats = data.props.stats;
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-4">
|
||||
<!-- Welcome Message -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-4xl font-extrabold">{$t('dashboard.welcome_back')}, {user?.first_name}!</h1>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats shadow mb-8 w-full bg-neutral">
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-primary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="inline-block w-8 h-8 stroke-current"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"
|
||||
></path></svg
|
||||
>
|
||||
</div>
|
||||
<div class="stat-title text-neutral-content">{$t('dashboard.countries_visited')}</div>
|
||||
<div class="stat-value text-primary">{stats.country_count}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-secondary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="inline-block w-8 h-8 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
|
||||
>
|
||||
</div>
|
||||
<div class="stat-title text-neutral-content">{$t('dashboard.total_adventures')}</div>
|
||||
<div class="stat-value text-secondary">{stats.adventure_count}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-success">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="inline-block w-8 h-8 stroke-current"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
|
||||
></path></svg
|
||||
>
|
||||
</div>
|
||||
<div class="stat-title text-neutral-content">{$t('dashboard.total_visited_regions')}</div>
|
||||
<div class="stat-value text-success">{stats.visited_region_count}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Adventures -->
|
||||
{#if recentAdventures.length > 0}
|
||||
<h2 class="text-3xl font-semibold mb-4">{$t('dashboard.recent_adventures')}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
{#each recentAdventures as adventure}
|
||||
<AdventureCard {adventure} user={data.user} readOnly />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Inspiration if there are no recent adventures -->
|
||||
{#if recentAdventures.length === 0}
|
||||
<div class="flex flex-col items-center justify-center bg-neutral shadow p-8 mb-8 rounded-lg">
|
||||
<h2 class="text-3xl font-semibold mb-4">{$t('dashboard.no_recent_adventures')}</h2>
|
||||
<p class="text-lg text-center">
|
||||
{$t('dashboard.add_some')}
|
||||
</p>
|
||||
<a href="/adventures" class="btn btn-primary mt-4">{$t('map.add_adventure')}</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<svelte:head>
|
||||
<title>Dashboard | AdventureLog</title>
|
||||
<meta name="description" content="Home dashboard for AdventureLog." />
|
||||
</svelte:head>
|
|
@ -1,7 +1,8 @@
|
|||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { fail, redirect, type RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import type { Actions, PageServerLoad, RouteParams } from './$types';
|
||||
import { getRandomBackground, getRandomQuote } from '$lib';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
|
@ -24,65 +25,99 @@ export const actions: Actions = {
|
|||
default: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
const formUsername = formData.get('username');
|
||||
const formPassword = formData.get('password');
|
||||
|
||||
let username = formUsername?.toString().toLocaleLowerCase();
|
||||
|
||||
const username = formUsername?.toString().toLowerCase();
|
||||
const password = formData.get('password');
|
||||
const totp = formData.get('totp');
|
||||
|
||||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
const csrfTokenFetch = await event.fetch(`${serverEndpoint}/csrf/`);
|
||||
const csrfToken = await fetchCSRFToken();
|
||||
|
||||
if (!csrfTokenFetch.ok) {
|
||||
console.error('Failed to fetch CSRF token');
|
||||
event.locals.user = null;
|
||||
return fail(500, {
|
||||
message: 'Failed to fetch CSRF token'
|
||||
});
|
||||
}
|
||||
|
||||
const tokenPromise = await csrfTokenFetch.json();
|
||||
const csrfToken = tokenPromise.csrfToken;
|
||||
|
||||
const loginFetch = await event.fetch(`${serverEndpoint}/auth/login/`, {
|
||||
// Initial login attempt
|
||||
const loginFetch = await event.fetch(`${serverEndpoint}/_allauth/browser/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `csrftoken=${csrfToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password
|
||||
})
|
||||
body: JSON.stringify({ username, password }),
|
||||
credentials: 'include'
|
||||
});
|
||||
const loginResponse = await loginFetch.json();
|
||||
if (!loginFetch.ok) {
|
||||
// get the value of the first key in the object
|
||||
const firstKey = Object.keys(loginResponse)[0] || 'error';
|
||||
const error = loginResponse[firstKey][0] || 'Invalid username or password';
|
||||
return fail(400, {
|
||||
message: error
|
||||
});
|
||||
} else {
|
||||
const token = loginResponse.access;
|
||||
const tokenFormatted = `auth=${token}`;
|
||||
const refreshToken = `${loginResponse.refresh}`;
|
||||
event.cookies.set('auth', tokenFormatted, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
|
||||
path: '/',
|
||||
secure: false
|
||||
});
|
||||
event.cookies.set('refresh', refreshToken, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year
|
||||
path: '/',
|
||||
secure: false
|
||||
});
|
||||
|
||||
if (loginFetch.status === 200) {
|
||||
// Login successful without MFA
|
||||
handleSuccessfulLogin(event, loginFetch);
|
||||
return redirect(302, '/');
|
||||
} else if (loginFetch.status === 401) {
|
||||
// MFA required
|
||||
if (!totp) {
|
||||
return fail(401, {
|
||||
message: 'settings.mfa_required',
|
||||
mfa_required: true
|
||||
});
|
||||
} else {
|
||||
// Attempt MFA authentication
|
||||
const sessionId = extractSessionId(loginFetch.headers.get('Set-Cookie'));
|
||||
const mfaLoginFetch = await event.fetch(
|
||||
`${serverEndpoint}/_allauth/browser/v1/auth/2fa/authenticate`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`
|
||||
},
|
||||
body: JSON.stringify({ code: totp }),
|
||||
credentials: 'include'
|
||||
}
|
||||
);
|
||||
|
||||
if (mfaLoginFetch.ok) {
|
||||
// MFA successful
|
||||
handleSuccessfulLogin(event, mfaLoginFetch);
|
||||
return redirect(302, '/');
|
||||
} else {
|
||||
// MFA failed
|
||||
const mfaLoginResponse = await mfaLoginFetch.json();
|
||||
return fail(401, {
|
||||
message: mfaLoginResponse.error || 'settings.invalid_code',
|
||||
mfa_required: true
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Login failed
|
||||
const loginResponse = await loginFetch.json();
|
||||
const firstKey = Object.keys(loginResponse)[0] || 'error';
|
||||
const error = loginResponse[firstKey][0] || 'settings.invalid_credentials';
|
||||
return fail(400, { message: error });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function handleSuccessfulLogin(event: RequestEvent<RouteParams, '/login'>, response: Response) {
|
||||
const setCookieHeader = response.headers.get('Set-Cookie');
|
||||
if (setCookieHeader) {
|
||||
const sessionIdRegex = /sessionid=([^;]+).*?expires=([^;]+)/;
|
||||
const match = setCookieHeader.match(sessionIdRegex);
|
||||
if (match) {
|
||||
const [, sessionId, expiryString] = match;
|
||||
event.cookies.set('sessionid', sessionId, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: true,
|
||||
expires: new Date(expiryString)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function extractSessionId(setCookieHeader: string | null) {
|
||||
if (setCookieHeader) {
|
||||
const sessionIdRegex = /sessionid=([^;]+)/;
|
||||
const match = setCookieHeader.match(sessionIdRegex);
|
||||
return match ? match[1] : '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -51,12 +51,21 @@
|
|||
id="password"
|
||||
class="block input input-bordered w-full max-w-xs"
|
||||
/><br />
|
||||
{#if $page.form?.mfa_required}
|
||||
<label for="password">TOTP</label>
|
||||
<input
|
||||
type="password"
|
||||
name="totp"
|
||||
id="totp"
|
||||
class="block input input-bordered w-full max-w-xs"
|
||||
/><br />
|
||||
{/if}
|
||||
<button class="py-2 px-4 btn btn-primary mr-2">{$t('auth.login')}</button>
|
||||
|
||||
<div class="flex justify-between mt-4">
|
||||
<p><a href="/signup" class="underline">{$t('auth.signup')}</a></p>
|
||||
<p>
|
||||
<a href="/settings/forgot-password" class="underline">{$t('auth.forgot_password')}</a>
|
||||
<a href="/user/reset-password" class="underline">{$t('auth.forgot_password')}</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -64,7 +73,7 @@
|
|||
|
||||
{#if ($page.form?.message && $page.form?.message.length > 1) || $page.form?.type === 'error'}
|
||||
<div class="text-center text-error mt-4">
|
||||
{$page.form.message || $t('auth.login_error')}
|
||||
{$t($page.form.message) || $t('auth.login_error')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -8,15 +8,16 @@ export const load = (async (event) => {
|
|||
if (!event.locals.user) {
|
||||
return redirect(302, '/login');
|
||||
} else {
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
let visitedFetch = await fetch(`${endpoint}/api/adventures/all/?include_collections=true`, {
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
|
||||
let visitedRegionsFetch = await fetch(`${endpoint}/api/visitedregion/`, {
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -4,15 +4,16 @@ 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('auth')) {
|
||||
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: `${event.cookies.get('auth')}`
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
|
|
@ -14,12 +14,14 @@ export const load = (async (event) => {
|
|||
return { data: [] };
|
||||
}
|
||||
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
|
||||
let res = await fetch(
|
||||
`${serverEndpoint}/api/adventures/search/?query=${query}&property=${property}`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -155,7 +155,6 @@
|
|||
{#each myAdventures as adventure}
|
||||
<AdventureCard
|
||||
user={data.user}
|
||||
type={adventure.type}
|
||||
{adventure}
|
||||
on:delete={deleteAdventure}
|
||||
on:edit={editAdventure}
|
||||
|
@ -168,13 +167,7 @@
|
|||
<h2 class="text-center font-bold text-2xl mb-4">{$t('search.public_adventures')}</h2>
|
||||
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||
{#each publicAdventures as adventure}
|
||||
<AdventureCard
|
||||
user={null}
|
||||
type={adventure.type}
|
||||
{adventure}
|
||||
on:delete={deleteAdventure}
|
||||
on:edit={editAdventure}
|
||||
/>
|
||||
<AdventureCard user={null} {adventure} on:delete={deleteAdventure} on:edit={editAdventure} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -2,29 +2,65 @@ import { fail, redirect, type Actions } from '@sveltejs/kit';
|
|||
import type { PageServerLoad } from '../$types';
|
||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
import type { User } from '$lib/types';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
type MFAAuthenticatorResponse = {
|
||||
status: number;
|
||||
data: {
|
||||
type: string;
|
||||
created_at: number;
|
||||
last_used_at: number | null;
|
||||
total_code_count?: number;
|
||||
unused_code_count?: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
if (!event.locals.user) {
|
||||
return redirect(302, '/');
|
||||
}
|
||||
if (!event.cookies.get('auth')) {
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
if (!sessionId) {
|
||||
return redirect(302, '/');
|
||||
}
|
||||
let res = await fetch(`${endpoint}/auth/user/`, {
|
||||
let res = await fetch(`${endpoint}/auth/user-metadata/`, {
|
||||
headers: {
|
||||
Cookie: event.cookies.get('auth') || ''
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
let user = (await res.json()) as User;
|
||||
|
||||
if (!res.ok) {
|
||||
let emailFetch = await fetch(`${endpoint}/_allauth/browser/v1/account/email`, {
|
||||
headers: {
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
let emailResponse = (await emailFetch.json()) as {
|
||||
status: number;
|
||||
data: { email: string; verified: boolean; primary: boolean }[];
|
||||
};
|
||||
let emails = emailResponse.data;
|
||||
if (!res.ok || !emailFetch.ok) {
|
||||
return redirect(302, '/');
|
||||
}
|
||||
|
||||
let mfaAuthenticatorFetch = await fetch(
|
||||
`${endpoint}/_allauth/browser/v1/account/authenticators`,
|
||||
{
|
||||
headers: {
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
}
|
||||
);
|
||||
let mfaAuthenticatorResponse = (await mfaAuthenticatorFetch.json()) as MFAAuthenticatorResponse;
|
||||
let authenticators = (mfaAuthenticatorResponse.data.length > 0) as boolean;
|
||||
|
||||
return {
|
||||
props: {
|
||||
user
|
||||
user,
|
||||
emails,
|
||||
authenticators
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -34,7 +70,8 @@ export const actions: Actions = {
|
|||
if (!event.locals.user) {
|
||||
return redirect(302, '/');
|
||||
}
|
||||
if (!event.cookies.get('auth')) {
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
if (!sessionId) {
|
||||
return redirect(302, '/');
|
||||
}
|
||||
|
||||
|
@ -47,9 +84,9 @@ export const actions: Actions = {
|
|||
let profile_pic = formData.get('profile_pic') as File | null | undefined;
|
||||
let public_profile = formData.get('public_profile') as string | null | undefined | boolean;
|
||||
|
||||
const resCurrent = await fetch(`${endpoint}/auth/user/`, {
|
||||
const resCurrent = await fetch(`${endpoint}/auth/user-metadata/`, {
|
||||
headers: {
|
||||
Cookie: event.cookies.get('auth') || ''
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -57,12 +94,12 @@ export const actions: Actions = {
|
|||
return fail(resCurrent.status, await resCurrent.json());
|
||||
}
|
||||
|
||||
// Gets the boolean value of the public_profile input
|
||||
if (public_profile === 'on') {
|
||||
public_profile = true;
|
||||
} else {
|
||||
public_profile = false;
|
||||
}
|
||||
console.log(public_profile);
|
||||
|
||||
let currentUser = (await resCurrent.json()) as User;
|
||||
|
||||
|
@ -80,6 +117,7 @@ export const actions: Actions = {
|
|||
}
|
||||
|
||||
let formDataToSend = new FormData();
|
||||
|
||||
if (username) {
|
||||
formDataToSend.append('username', username);
|
||||
}
|
||||
|
@ -94,10 +132,13 @@ export const actions: Actions = {
|
|||
}
|
||||
formDataToSend.append('public_profile', public_profile.toString());
|
||||
|
||||
let res = await fetch(`${endpoint}/auth/user/`, {
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
|
||||
let res = await fetch(`${endpoint}/auth/update-user/`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Cookie: event.cookies.get('auth') || ''
|
||||
Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: formDataToSend
|
||||
});
|
||||
|
@ -105,47 +146,53 @@ export const actions: Actions = {
|
|||
let response = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
// change the first key in the response to 'message' for the fail function
|
||||
response = { message: Object.values(response)[0] };
|
||||
return fail(res.status, response);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
return { error: 'An error occurred while processing your request.' };
|
||||
return { error: 'settings.generic_error' };
|
||||
}
|
||||
},
|
||||
changePassword: async (event) => {
|
||||
if (!event.locals.user) {
|
||||
return redirect(302, '/');
|
||||
}
|
||||
if (!event.cookies.get('auth')) {
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
if (!sessionId) {
|
||||
return redirect(302, '/');
|
||||
}
|
||||
console.log('changePassword');
|
||||
|
||||
const formData = await event.request.formData();
|
||||
|
||||
const password1 = formData.get('password1') as string | null | undefined;
|
||||
const password2 = formData.get('password2') as string | null | undefined;
|
||||
const current_password = formData.get('current_password') as string | null | undefined;
|
||||
|
||||
if (password1 !== password2) {
|
||||
return fail(400, { message: 'Passwords do not match' });
|
||||
return fail(400, { message: 'settings.password_does_not_match' });
|
||||
}
|
||||
if (!current_password) {
|
||||
return fail(400, { message: 'settings.password_is_required' });
|
||||
}
|
||||
|
||||
let res = await fetch(`${endpoint}/auth/password/change/`, {
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
|
||||
let res = await fetch(`${endpoint}/_allauth/browser/v1/account/password/change`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Cookie: event.cookies.get('auth') || '',
|
||||
Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
new_password1: password1,
|
||||
new_password2: password2
|
||||
current_password,
|
||||
new_password: password1
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
return fail(res.status, await res.json());
|
||||
return fail(res.status, { message: 'settings.error_change_password' });
|
||||
}
|
||||
return { success: true };
|
||||
},
|
||||
|
@ -153,19 +200,22 @@ export const actions: Actions = {
|
|||
if (!event.locals.user) {
|
||||
return redirect(302, '/');
|
||||
}
|
||||
if (!event.cookies.get('auth')) {
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
if (!sessionId) {
|
||||
return redirect(302, '/');
|
||||
}
|
||||
const formData = await event.request.formData();
|
||||
const new_email = formData.get('new_email') as string | null | undefined;
|
||||
if (!new_email) {
|
||||
return fail(400, { message: 'Email is required' });
|
||||
return fail(400, { message: 'auth.email_required' });
|
||||
} else {
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
let res = await fetch(`${endpoint}/auth/change-email/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Cookie: event.cookies.get('auth') || '',
|
||||
'Content-Type': 'application/json'
|
||||
Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
new_email
|
||||
|
|
|
@ -6,13 +6,20 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { t } from 'svelte-i18n';
|
||||
import TotpModal from '$lib/components/TOTPModal.svelte';
|
||||
|
||||
export let data;
|
||||
let user: User;
|
||||
let emails: typeof data.props.emails;
|
||||
if (data.user) {
|
||||
user = data.user;
|
||||
emails = data.props.emails;
|
||||
}
|
||||
|
||||
let new_email: string = '';
|
||||
|
||||
let isMFAModalOpen: boolean = false;
|
||||
|
||||
onMount(async () => {
|
||||
if (browser) {
|
||||
const queryParams = new URLSearchParams($page.url.search);
|
||||
|
@ -34,16 +41,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// async function exportAdventures() {
|
||||
// const url = await exportData();
|
||||
|
||||
// const a = document.createElement('a');
|
||||
// a.href = url;
|
||||
// a.download = 'adventure-log-export.json';
|
||||
// a.click();
|
||||
// URL.revokeObjectURL(url);
|
||||
// }
|
||||
|
||||
async function checkVisitedRegions() {
|
||||
let res = await fetch('/api/reverse-geocode/mark_visited_region/', {
|
||||
method: 'POST',
|
||||
|
@ -58,8 +55,105 @@
|
|||
addToast('error', $t('adventures.error_updating_regions'));
|
||||
}
|
||||
}
|
||||
|
||||
async function removeEmail(email: { email: any; verified?: boolean; primary?: boolean }) {
|
||||
let res = await fetch('/_allauth/browser/v1/account/email/', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email: email.email })
|
||||
});
|
||||
if (res.ok) {
|
||||
addToast('success', $t('settings.email_removed'));
|
||||
emails = emails.filter((e) => e.email !== email.email);
|
||||
} else {
|
||||
addToast('error', $t('settings.email_removed_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyEmail(email: { email: any; verified?: boolean; primary?: boolean }) {
|
||||
let res = await fetch('/_allauth/browser/v1/account/email/', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email: email.email })
|
||||
});
|
||||
if (res.ok) {
|
||||
addToast('success', $t('settings.verify_email_success'));
|
||||
} else {
|
||||
addToast('error', $t('settings.verify_email_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function addEmail() {
|
||||
let res = await fetch('/_allauth/browser/v1/account/email/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email: new_email })
|
||||
});
|
||||
if (res.ok) {
|
||||
addToast('success', $t('settings.email_added'));
|
||||
emails = [...emails, { email: new_email, verified: false, primary: false }];
|
||||
new_email = '';
|
||||
} else {
|
||||
let error = await res.json();
|
||||
let error_code = error.errors[0].code;
|
||||
addToast('error', $t(`settings.${error_code}`) || $t('settings.generic_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function primaryEmail(email: { email: any; verified?: boolean; primary?: boolean }) {
|
||||
let res = await fetch('/_allauth/browser/v1/account/email/', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email: email.email, primary: true })
|
||||
});
|
||||
if (res.ok) {
|
||||
addToast('success', $t('settings.email_set_primary'));
|
||||
// remove primary from all other emails and set this one as primary
|
||||
emails = emails.map((e) => {
|
||||
if (e.email === email.email) {
|
||||
e.primary = true;
|
||||
} else {
|
||||
e.primary = false;
|
||||
}
|
||||
return e;
|
||||
});
|
||||
} else {
|
||||
addToast('error', $t('settings.email_set_primary_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function disableMfa() {
|
||||
const res = await fetch('/_allauth/browser/v1/account/authenticators/totp', {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (res.ok) {
|
||||
addToast('success', $t('settings.mfa_disabled'));
|
||||
data.props.authenticators = false;
|
||||
} else {
|
||||
if (res.status == 401) {
|
||||
addToast('error', $t('settings.reset_session_error'));
|
||||
}
|
||||
addToast('error', $t('settings.generic_error'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isMFAModalOpen}
|
||||
<TotpModal
|
||||
user={data.user}
|
||||
on:close={() => (isMFAModalOpen = false)}
|
||||
bind:is_enabled={data.props.authenticators}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<h1 class="text-center font-extrabold text-4xl mb-6">{$t('settings.settings_page')}</h1>
|
||||
|
||||
<h1 class="text-center font-extrabold text-xl">{$t('settings.account_settings')}</h1>
|
||||
|
@ -95,14 +189,6 @@
|
|||
id="last_name"
|
||||
class="block mb-2 input input-bordered w-full max-w-xs"
|
||||
/><br />
|
||||
<!-- <label for="first_name">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
bind:value={user.email}
|
||||
name="email"
|
||||
id="email"
|
||||
class="block mb-2 input input-bordered w-full max-w-xs"
|
||||
/><br /> -->
|
||||
<label for="profilePicture">{$t('auth.profile_picture')}</label>
|
||||
<input
|
||||
type="file"
|
||||
|
@ -131,13 +217,21 @@
|
|||
|
||||
{#if $page.form?.message}
|
||||
<div class="text-center text-error mt-4">
|
||||
{$page.form?.message}
|
||||
{$t($page.form.message)}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">{$t('settings.password_change')}</h1>
|
||||
<div class="flex justify-center">
|
||||
<form action="?/changePassword" method="post" class="w-full max-w-xs">
|
||||
<form action="?/changePassword" method="post" class="w-full max-w-xs" use:enhance>
|
||||
<input
|
||||
type="password"
|
||||
name="current_password"
|
||||
placeholder={$t('settings.current_password')}
|
||||
id="current_password"
|
||||
class="block mb-2 input input-bordered w-full max-w-xs"
|
||||
/>
|
||||
<br />
|
||||
<input
|
||||
type="password"
|
||||
name="password1"
|
||||
|
@ -153,35 +247,84 @@
|
|||
placeholder={$t('settings.confirm_new_password')}
|
||||
class="block mb-2 input input-bordered w-full max-w-xs"
|
||||
/>
|
||||
<button class="py-2 px-4 btn btn-primary mt-2">{$t('settings.password_change')}</button>
|
||||
<div class="tooltip tooltip-warning" data-tip={$t('settings.password_change_lopout_warning')}>
|
||||
<button class="py-2 px-4 btn btn-primary mt-2">{$t('settings.password_change')}</button>
|
||||
</div>
|
||||
<br />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">{$t('settings.email_change')}</h1>
|
||||
<div class="flex justify-center">
|
||||
<form action="?/changeEmail" method="post" class="w-full max-w-xs">
|
||||
<label for="current_email">{$t('settings.current_email')}</label>
|
||||
<input
|
||||
type="email"
|
||||
name="current_email"
|
||||
placeholder={user.email || $t('settings.no_email_set')}
|
||||
id="current_email"
|
||||
readonly
|
||||
class="block mb-2 input input-bordered w-full max-w-xs"
|
||||
/>
|
||||
<br />
|
||||
<input
|
||||
type="email"
|
||||
name="new_email"
|
||||
placeholder={$t('settings.new_email')}
|
||||
id="new_email"
|
||||
class="block mb-2 input input-bordered w-full max-w-xs"
|
||||
/>
|
||||
<button class="py-2 px-4 btn btn-primary mt-2">{$t('settings.email_change')}</button>
|
||||
|
||||
<div class="flex justify-center mb-4">
|
||||
<div>
|
||||
{#each emails as email}
|
||||
<p class="mb-2">
|
||||
{email.email}
|
||||
{#if email.verified}
|
||||
<div class="badge badge-success">{$t('settings.verified')}</div>
|
||||
{:else}
|
||||
<div class="badge badge-error">{$t('settings.not_verified')}</div>
|
||||
{/if}
|
||||
{#if email.primary}
|
||||
<div class="badge badge-primary">{$t('settings.primary')}</div>
|
||||
{/if}
|
||||
{#if !email.verified}
|
||||
<button class="btn btn-sm btn-secondary ml-2" on:click={() => verifyEmail(email)}
|
||||
>{$t('settings.verify')}</button
|
||||
>
|
||||
{/if}
|
||||
{#if !email.primary}
|
||||
<button class="btn btn-sm btn-secondary ml-2" on:click={() => primaryEmail(email)}
|
||||
>{$t('settings.make_primary')}</button
|
||||
>
|
||||
{/if}
|
||||
<button class="btn btn-sm btn-warning ml-2" on:click={() => removeEmail(email)}
|
||||
>{$t('adventures.remove')}</button
|
||||
>
|
||||
</p>
|
||||
{/each}
|
||||
{#if emails.length === 0}
|
||||
<p>{$t('settings.no_emai_set')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mt-4">
|
||||
<form class="w-full max-w-xs" on:submit={addEmail}>
|
||||
<div class="mb-4">
|
||||
<input
|
||||
type="email"
|
||||
name="new_email"
|
||||
placeholder={$t('settings.new_email')}
|
||||
bind:value={new_email}
|
||||
id="new_email"
|
||||
class="block mb-2 input input-bordered w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button class="py-2 px-4 mb-4 btn btn-primary">{$t('settings.email_change')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">{$t('settings.mfa_page_title')}</h1>
|
||||
|
||||
<div class="flex justify-center mb-4">
|
||||
<div>
|
||||
{#if !data.props.authenticators}
|
||||
<p>{$t('settings.mfa_not_enabled')}</p>
|
||||
<button class="btn btn-primary mt-2" on:click={() => (isMFAModalOpen = true)}
|
||||
>{$t('settings.enable_mfa')}</button
|
||||
>
|
||||
{:else}
|
||||
<button class="btn btn-warning mt-2" on:click={disableMfa}
|
||||
>{$t('settings.disable_mfa')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center mt-4">
|
||||
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">
|
||||
{$t('adventures.visited_region_check')}
|
||||
|
@ -189,22 +332,15 @@
|
|||
<p>
|
||||
{$t('adventures.visited_region_check_desc')}
|
||||
</p>
|
||||
<p>{$t('adventures.update_visited_regions_disclaimer')}</p>
|
||||
|
||||
<button class="btn btn-neutral mt-2 mb-2" on:click={checkVisitedRegions}
|
||||
>{$t('adventures.update_visited_regions')}</button
|
||||
>
|
||||
<p>{$t('adventures.update_visited_regions_disclaimer')}</p>
|
||||
</div>
|
||||
<!--
|
||||
<div class="flex flex-col items-center mt-4">
|
||||
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">Data Export</h1>
|
||||
<button class="btn btn-neutral mb-4" on:click={exportAdventures}> Export to JSON </button>
|
||||
<p>This may take a few seconds...</p>
|
||||
</div> -->
|
||||
|
||||
<small class="text-center"
|
||||
><b>For Debug Use:</b> Server PK={user.pk} | Date Joined: {user.date_joined
|
||||
? new Date(user.date_joined).toDateString()
|
||||
: ''} | Staff user: {user.is_staff}</small
|
||||
><b>For Debug Use:</b> UUID={user.uuid} | Staff user: {user.is_staff}</small
|
||||
>
|
||||
|
||||
<svelte:head>
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
export const load = (async (event) => {
|
||||
const token = event.url.searchParams.get('token');
|
||||
const uid = event.url.searchParams.get('uid');
|
||||
|
||||
return {
|
||||
props: {
|
||||
token,
|
||||
uid
|
||||
}
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
export const actions: Actions = {
|
||||
reset: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
|
||||
const new_password1 = formData.get('new_password1') as string;
|
||||
const new_password2 = formData.get('new_password2') as string;
|
||||
const token = formData.get('token') as string;
|
||||
const uid = formData.get('uid') as string;
|
||||
|
||||
if (!new_password1 || !new_password2) {
|
||||
return fail(400, { message: 'settings.password_is_required' });
|
||||
}
|
||||
|
||||
if (new_password1 !== new_password2) {
|
||||
return fail(400, { message: 'settings.password_does_not_match' });
|
||||
}
|
||||
|
||||
if (!token || !uid) {
|
||||
return redirect(302, '/settings/forgot-password');
|
||||
} else {
|
||||
let response = await fetch(`${serverEndpoint}/auth/password/reset/confirm/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: token,
|
||||
uid: uid,
|
||||
new_password1,
|
||||
new_password2
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
return fail(response.status, { message: 'settings.invalid_token' });
|
||||
} else {
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,64 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import type { PageData } from './$types';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<h1 class="text-center font-bold text-4xl mb-4">{$t('settings.change_password')}</h1>
|
||||
|
||||
{#if data.props.token && data.props.uid}
|
||||
<p class="text-center">{$t('settings.login_redir')}</p>
|
||||
<div
|
||||
class="modal-action items-center"
|
||||
style="display: flex; flex-direction: column; align-items: center; width: 100%;"
|
||||
>
|
||||
<form action="?/reset" method="post" use:enhance>
|
||||
<input type="hidden" name="uid" value={data.props.uid} />
|
||||
<input type="hidden" name="token" value={data.props.token} />
|
||||
|
||||
<div class="mb-2 w-full">
|
||||
<input
|
||||
type="password"
|
||||
class="input input-bordered w-full"
|
||||
id="new_password1"
|
||||
name="new_password1"
|
||||
placeholder={$t('settings.new_password')}
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2 w-full">
|
||||
<input
|
||||
type="password"
|
||||
class="input input-bordered w-full"
|
||||
id="new_password2"
|
||||
name="new_password2"
|
||||
placeholder={$t('settings.confirm_new_password')}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-full">{$t('settings.submit')}</button>
|
||||
{#if $page.form?.message}
|
||||
<div class="text-center text-error mt-4">
|
||||
{$t($page.form?.message)}
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex justify-center">
|
||||
<div class="items-center justify-center">
|
||||
<p class="text-center">{$t('settings.token_required')}</p>
|
||||
|
||||
<button class="btn btn-neutral" on:click={() => goto('/settings/forgot-password')}>
|
||||
{$t('settings.reset_password')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<svelte:head>
|
||||
<title>Password Reset Confirm</title>
|
||||
<meta name="description" content="Confirm your password reset and make a new password." />
|
||||
</svelte:head>
|
|
@ -8,9 +8,10 @@ export const load = (async (event) => {
|
|||
if (!event.locals.user) {
|
||||
return redirect(302, '/login');
|
||||
} else {
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
let res = await fetch(`${serverEndpoint}/api/collections/shared/`, {
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
|
|
@ -41,22 +41,26 @@ export const actions: Actions = {
|
|||
|
||||
if (!csrfTokenFetch.ok) {
|
||||
event.locals.user = null;
|
||||
return fail(500, { message: 'Failed to fetch CSRF token' });
|
||||
return fail(500, { message: 'settings.csrf_failed' });
|
||||
}
|
||||
|
||||
if (password1 !== password2) {
|
||||
return fail(400, { message: 'settings.password_does_not_match' });
|
||||
}
|
||||
|
||||
const tokenPromise = await csrfTokenFetch.json();
|
||||
const csrfToken = tokenPromise.csrfToken;
|
||||
|
||||
const loginFetch = await event.fetch(`${serverEndpoint}/auth/registration/`, {
|
||||
const loginFetch = await event.fetch(`${serverEndpoint}/_allauth/browser/v1/auth/signup`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `csrftoken=${csrfToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password1: password1,
|
||||
password2: password2,
|
||||
password: password1,
|
||||
email: email,
|
||||
first_name,
|
||||
last_name
|
||||
|
@ -65,31 +69,36 @@ export const actions: Actions = {
|
|||
const loginResponse = await loginFetch.json();
|
||||
|
||||
if (!loginFetch.ok) {
|
||||
// get the value of the first key in the object
|
||||
const firstKey = Object.keys(loginResponse)[0] || 'error';
|
||||
const error =
|
||||
loginResponse[firstKey][0] || 'Failed to register user. Check your inputs and try again.';
|
||||
return fail(400, {
|
||||
message: error
|
||||
});
|
||||
return fail(loginFetch.status, { message: loginResponse.errors[0].code });
|
||||
} else {
|
||||
const token = loginResponse.access;
|
||||
const tokenFormatted = `auth=${token}`;
|
||||
const refreshToken = `${loginResponse.refresh}`;
|
||||
event.cookies.set('auth', tokenFormatted, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
|
||||
path: '/'
|
||||
});
|
||||
event.cookies.set('refresh', refreshToken, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year
|
||||
path: '/'
|
||||
});
|
||||
const setCookieHeader = loginFetch.headers.get('Set-Cookie');
|
||||
|
||||
return redirect(302, '/');
|
||||
console.log('setCookieHeader:', setCookieHeader);
|
||||
|
||||
if (setCookieHeader) {
|
||||
// Regular expression to match sessionid cookie and its expiry
|
||||
const sessionIdRegex = /sessionid=([^;]+).*?expires=([^;]+)/;
|
||||
const match = setCookieHeader.match(sessionIdRegex);
|
||||
|
||||
if (match) {
|
||||
const sessionId = match[1];
|
||||
const expiryString = match[2];
|
||||
const expiryDate = new Date(expiryString);
|
||||
|
||||
console.log('Session ID:', sessionId);
|
||||
console.log('Expiry Date:', expiryDate);
|
||||
|
||||
// Set the sessionid cookie
|
||||
event.cookies.set('sessionid', sessionId, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: true,
|
||||
expires: expiryDate
|
||||
});
|
||||
}
|
||||
}
|
||||
redirect(302, '/');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -87,16 +87,14 @@
|
|||
<div class="flex justify-between mt-4">
|
||||
<p><a href="/login" class="underline">{$t('auth.login')}</a></p>
|
||||
<p>
|
||||
<a href="/settings/forgot-password" class="underline"
|
||||
>{$t('auth.forgot_password')}</a
|
||||
>
|
||||
<a href="/user/reset-password" class="underline">{$t('auth.forgot_password')}</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{#if $page.form?.message}
|
||||
<div class="text-center text-error mt-4">{$page.form?.message}</div>
|
||||
<div class="text-center text-error mt-4">{$t($page.form?.message)}</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex justify-center">
|
||||
|
|
|
@ -4,7 +4,8 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
|||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
export const load = (async (event) => {
|
||||
if (!event.cookies.get('auth')) {
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
if (!sessionId) {
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
const uuid = event.params.uuid;
|
||||
|
@ -13,7 +14,7 @@ export const load = (async (event) => {
|
|||
}
|
||||
let res = await fetch(`${serverEndpoint}/auth/user/${uuid}/`, {
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
import { fail, type Actions } from '@sveltejs/kit';
|
||||
|
||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
|
@ -13,10 +14,14 @@ export const actions: Actions = {
|
|||
return fail(400, { message: 'missing_email' });
|
||||
}
|
||||
|
||||
let res = await fetch(`${endpoint}/auth/password/reset/`, {
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
|
||||
let res = await fetch(`${endpoint}/_allauth/browser/v1/auth/password/request`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
Cookie: `csrftoken=${csrfToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email
|
||||
|
@ -25,10 +30,7 @@ export const actions: Actions = {
|
|||
|
||||
if (!res.ok) {
|
||||
let message = await res.json();
|
||||
|
||||
const key = Object.keys(message)[0];
|
||||
|
||||
return fail(res.status, { message: message[key] });
|
||||
return fail(res.status, message);
|
||||
}
|
||||
return { success: true };
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
|
||||
export const load = (async ({ params }) => {
|
||||
const key = params.key;
|
||||
if (!key) {
|
||||
throw redirect(302, '/');
|
||||
}
|
||||
return { key };
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
const password = formData.get('password');
|
||||
const confirm_password = formData.get('confirm_password');
|
||||
const key = event.params.key;
|
||||
|
||||
if (!password || !confirm_password) {
|
||||
return fail(400, { message: 'auth.both_passwords_required' });
|
||||
}
|
||||
|
||||
if (password !== confirm_password) {
|
||||
return fail(400, { message: 'settings.password_does_not_match' });
|
||||
}
|
||||
|
||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
const csrfToken = await fetchCSRFToken();
|
||||
|
||||
const response = await event.fetch(
|
||||
`${serverEndpoint}/_allauth/browser/v1/auth/password/reset`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `csrftoken=${csrfToken}`,
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ key: key, password: password })
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status !== 401) {
|
||||
const error_message = await response.json();
|
||||
console.error(error_message);
|
||||
console.log(response);
|
||||
return fail(response.status, { message: 'auth.reset_failed' });
|
||||
}
|
||||
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
};
|
53
frontend/src/routes/user/reset-password/[key]/+page.svelte
Normal file
53
frontend/src/routes/user/reset-password/[key]/+page.svelte
Normal file
|
@ -0,0 +1,53 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { page } from '$app/stores';
|
||||
import type { PageData } from '../../../$types';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<h1 class="text-center font-bold text-4xl mb-4">{$t('settings.change_password')}</h1>
|
||||
|
||||
<form method="POST" use:enhance class="flex flex-col items-center justify-center space-y-4">
|
||||
<div class="w-full max-w-xs">
|
||||
<label for="password" class="label">
|
||||
<span class="label-text">{$t('auth.new_password')}</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-full max-w-xs">
|
||||
<label for="confirm_password" class="label">
|
||||
<span class="label-text">{$t('auth.confirm_password')}</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirm_password"
|
||||
name="confirm_password"
|
||||
required
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{$t('settings.reset_password')}
|
||||
</button>
|
||||
|
||||
{#if $page.form?.message}
|
||||
<div class="text-error">
|
||||
{$t($page.form?.message)}
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
<svelte:head>
|
||||
<title>Password Reset Confirm</title>
|
||||
<meta name="description" content="Confirm your password reset and make a new password." />
|
||||
</svelte:head>
|
34
frontend/src/routes/user/verify-email/[key]/+page.server.ts
Normal file
34
frontend/src/routes/user/verify-email/[key]/+page.server.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async (event) => {
|
||||
// get key from route params
|
||||
const key = event.params.key;
|
||||
if (!key) {
|
||||
return { status: 404 };
|
||||
}
|
||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
const csrfToken = await fetchCSRFToken();
|
||||
|
||||
let verifyFetch = await event.fetch(`${serverEndpoint}/_allauth/browser/v1/auth/email/verify`, {
|
||||
headers: {
|
||||
Cookie: `csrftoken=${csrfToken}`,
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
|
||||
body: JSON.stringify({ key: key })
|
||||
});
|
||||
if (verifyFetch.ok || verifyFetch.status == 401) {
|
||||
return {
|
||||
verified: true
|
||||
};
|
||||
} else {
|
||||
let error_message = await verifyFetch.json();
|
||||
console.error(error_message);
|
||||
console.error('Failed to verify email');
|
||||
return { status: 404 };
|
||||
}
|
||||
}) satisfies PageServerLoad;
|
14
frontend/src/routes/user/verify-email/[key]/+page.svelte
Normal file
14
frontend/src/routes/user/verify-email/[key]/+page.svelte
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from '../$types';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
{#if data.verified}
|
||||
<h1>{$t('settings.email_verified')}</h1>
|
||||
<p>{$t('settings.email_verified_success')}</p>
|
||||
{:else}
|
||||
<h1>{$t('settings.email_verified_error')}</h1>
|
||||
<p>{$t('settings.email_verified_erorr_desc')}</p>
|
||||
{/if}
|
|
@ -4,13 +4,14 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
|||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
export const load = (async (event) => {
|
||||
if (!event.cookies.get('auth')) {
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
if (!sessionId) {
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
|
||||
const res = await fetch(`${serverEndpoint}/auth/users/`, {
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
|
|
@ -2,6 +2,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
|||
import type { Country } from '$lib/types';
|
||||
import { redirect, type Actions } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { fetchCSRFToken } from '$lib/index.server';
|
||||
|
||||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
|
@ -9,11 +10,12 @@ export const load = (async (event) => {
|
|||
if (!event.locals.user) {
|
||||
return redirect(302, '/login');
|
||||
} else {
|
||||
const res = await fetch(`${endpoint}/api/countries/`, {
|
||||
const res = await event.fetch(`${endpoint}/api/countries/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
}
|
||||
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error('Failed to fetch countries');
|
||||
|
@ -27,8 +29,6 @@ export const load = (async (event) => {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
export const actions: Actions = {
|
||||
|
@ -41,15 +41,20 @@ export const actions: Actions = {
|
|||
};
|
||||
}
|
||||
|
||||
if (!event.locals.user || !event.cookies.get('auth')) {
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
|
||||
if (!event.locals.user || !sessionId) {
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
|
||||
const res = await fetch(`${endpoint}/api/visitedregion/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`,
|
||||
'Content-Type': 'application/json'
|
||||
Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ region: body.regionId })
|
||||
});
|
||||
|
@ -75,15 +80,20 @@ export const actions: Actions = {
|
|||
|
||||
const visitId = body.visitId as number;
|
||||
|
||||
if (!event.locals.user || !event.cookies.get('auth')) {
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
|
||||
if (!event.locals.user || !sessionId) {
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
|
||||
const res = await fetch(`${endpoint}/api/visitedregion/${visitId}/`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`,
|
||||
'Content-Type': 'application/json'
|
||||
Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -12,10 +12,16 @@ export const load = (async (event) => {
|
|||
let visitedRegions: VisitedRegion[] = [];
|
||||
let country: Country;
|
||||
|
||||
let sessionId = event.cookies.get('sessionid');
|
||||
|
||||
if (!sessionId) {
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
|
||||
let res = await fetch(`${endpoint}/api/${id}/regions/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
@ -28,7 +34,7 @@ export const load = (async (event) => {
|
|||
res = await fetch(`${endpoint}/api/${id}/visits/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
@ -41,7 +47,7 @@ export const load = (async (event) => {
|
|||
res = await fetch(`${endpoint}/api/countries/${regions[0].country}/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
|
|
@ -100,6 +100,33 @@ export default {
|
|||
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
||||
},
|
||||
northernLights: {
|
||||
primary: '#479bb3', // Primary color
|
||||
'primary-focus': '#81A1C1', // Primary color - focused
|
||||
'primary-content': '#ECEFF4', // Foreground content color to use on primary color
|
||||
|
||||
secondary: '#5E81AC', // Secondary color
|
||||
'secondary-focus': '#4C566A', // Secondary color - focused
|
||||
'secondary-content': '#ECEFF4', // Foreground content color to use on secondary color
|
||||
|
||||
accent: '#B48EAD', // Accent color
|
||||
'accent-focus': '#A3BE8C', // Accent color - focused
|
||||
'accent-content': '#ECEFF4', // Foreground content color to use on accent color
|
||||
|
||||
neutral: '#4C566A', // Neutral color
|
||||
'neutral-focus': '#3B4252', // Neutral color - focused
|
||||
'neutral-content': '#D8DEE9', // Foreground content color to use on neutral color
|
||||
|
||||
'base-100': '#2E3440', // Base color of page, used for blank backgrounds
|
||||
'base-200': '#3B4252', // Base color, a little lighter
|
||||
'base-300': '#434C5E', // Base color, even more lighter
|
||||
'base-content': '#ECEFF4', // Foreground content color to use on base color
|
||||
|
||||
info: '#88C0D0', // Info
|
||||
success: '#A3BE8C', // Success
|
||||
warning: '#D08770', // Warning
|
||||
error: '#BF616A' // Error
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
FROM nginx:alpine
|
||||
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY nginx.conf /etc/nginx/conf.d
|
|
@ -1,8 +0,0 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location /media/ {
|
||||
alias /app/media/;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue