mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-22 22:39:36 +02:00
feat: Add location_name to ReverseGeocode type and implement location fetching in stats view
This commit is contained in:
parent
60b5bbb3c8
commit
b5d6788c11
21 changed files with 1048 additions and 901 deletions
|
@ -21,10 +21,14 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
|
||||||
country_code = None
|
country_code = None
|
||||||
city = None
|
city = None
|
||||||
visited_city = None
|
visited_city = None
|
||||||
|
location_name = None
|
||||||
|
|
||||||
# town = None
|
# town = None
|
||||||
# city = None
|
# city = None
|
||||||
# county = None
|
# county = None
|
||||||
|
|
||||||
|
if 'name' in data.keys():
|
||||||
|
location_name = data['name']
|
||||||
|
|
||||||
if 'address' in data.keys():
|
if 'address' in data.keys():
|
||||||
keys = data['address'].keys()
|
keys = data['address'].keys()
|
||||||
|
@ -58,7 +62,7 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
|
||||||
if visited_city:
|
if visited_city:
|
||||||
city_visited = True
|
city_visited = True
|
||||||
if region:
|
if region:
|
||||||
return {"region_id": iso_code, "region": region.name, "country": region.country.name, "region_visited": region_visited, "display_name": display_name, "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited}
|
return {"region_id": iso_code, "region": region.name, "country": region.country.name, "region_visited": region_visited, "display_name": display_name, "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited, 'location_name': location_name}
|
||||||
return {"error": "No region found"}
|
return {"error": "No region found"}
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
|
|
|
@ -48,4 +48,11 @@ class StatsViewSet(viewsets.ViewSet):
|
||||||
'total_regions': total_regions,
|
'total_regions': total_regions,
|
||||||
'visited_country_count': visited_country_count,
|
'visited_country_count': visited_country_count,
|
||||||
'total_countries': total_countries
|
'total_countries': total_countries
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# locations - returns a list of all of the latitude, longitude locations of all public adventrues on the server
|
||||||
|
@action(detail=False, methods=['get'], url_path='locations')
|
||||||
|
def locations(self, request):
|
||||||
|
adventures = Adventure.objects.filter(
|
||||||
|
is_public=True).values('latitude', 'longitude', 'id', 'name')
|
||||||
|
return Response(adventures)
|
|
@ -61,7 +61,7 @@ INSTALLED_APPS = (
|
||||||
'users',
|
'users',
|
||||||
'integrations',
|
'integrations',
|
||||||
'django.contrib.gis',
|
'django.contrib.gis',
|
||||||
'achievements',
|
# 'achievements', # Not done yet, will be added later in a future update
|
||||||
# 'widget_tweaks',
|
# 'widget_tweaks',
|
||||||
# 'slippers',
|
# 'slippers',
|
||||||
|
|
||||||
|
@ -303,4 +303,7 @@ LOGGING = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ADVENTURELOG_CDN_URL = getenv('ADVENTURELOG_CDN_URL', 'https://cdn.adventurelog.app')
|
# ADVENTURELOG_CDN_URL = getenv('ADVENTURELOG_CDN_URL', 'https://cdn.adventurelog.app')
|
||||||
|
|
||||||
|
# https://github.com/dr5hn/countries-states-cities-database/tags
|
||||||
|
COUNTRY_REGION_JSON_VERSION = 'v2.5'
|
|
@ -68,6 +68,9 @@ class PublicUserListView(APIView):
|
||||||
for user in users:
|
for user in users:
|
||||||
user.email = None
|
user.email = None
|
||||||
serializer = PublicUserSerializer(users, many=True)
|
serializer = PublicUserSerializer(users, many=True)
|
||||||
|
# for every user, remove the field has_password
|
||||||
|
for user in serializer.data:
|
||||||
|
user.pop('has_password', None)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
class PublicUserDetailView(APIView):
|
class PublicUserDetailView(APIView):
|
||||||
|
@ -87,6 +90,8 @@ class PublicUserDetailView(APIView):
|
||||||
else:
|
else:
|
||||||
user = get_object_or_404(User, username=username, public_profile=True)
|
user = get_object_or_404(User, username=username, public_profile=True)
|
||||||
serializer = PublicUserSerializer(user)
|
serializer = PublicUserSerializer(user)
|
||||||
|
# for every user, remove the field has_password
|
||||||
|
serializer.data.pop('has_password', None)
|
||||||
|
|
||||||
# remove the email address from the response
|
# remove the email address from the response
|
||||||
user.email = None
|
user.email = None
|
||||||
|
|
|
@ -8,7 +8,7 @@ import ijson
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
ADVENTURELOG_CDN_URL = settings.ADVENTURELOG_CDN_URL
|
COUNTRY_REGION_JSON_VERSION = settings.COUNTRY_REGION_JSON_VERSION
|
||||||
|
|
||||||
media_root = settings.MEDIA_ROOT
|
media_root = settings.MEDIA_ROOT
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ def saveCountryFlag(country_code):
|
||||||
print(f'Flag for {country_code} already exists')
|
print(f'Flag for {country_code} already exists')
|
||||||
return
|
return
|
||||||
|
|
||||||
res = requests.get(f'{ADVENTURELOG_CDN_URL}/data/flags/{country_code}.png'.lower())
|
res = requests.get(f'https://flagcdn.com/h240/{country_code}.png'.lower())
|
||||||
if res.status_code == 200:
|
if res.status_code == 200:
|
||||||
with open(flag_path, 'wb') as f:
|
with open(flag_path, 'wb') as f:
|
||||||
f.write(res.content)
|
f.write(res.content)
|
||||||
|
@ -39,56 +39,30 @@ class Command(BaseCommand):
|
||||||
help = 'Imports the world travel data'
|
help = 'Imports the world travel data'
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument('--force', action='store_true', help='Force re-download of AdventureLog setup content from the CDN')
|
parser.add_argument('--force', action='store_true', help='Force download the countries+regions+states.json file')
|
||||||
|
|
||||||
def handle(self, **options):
|
def handle(self, **options):
|
||||||
force = options['force']
|
force = options['force']
|
||||||
batch_size = 100
|
batch_size = 100
|
||||||
current_version_json = os.path.join(settings.MEDIA_ROOT, 'data_version.json')
|
countries_json_path = os.path.join(settings.MEDIA_ROOT, f'countries+regions+states-{COUNTRY_REGION_JSON_VERSION}.json')
|
||||||
try:
|
if not os.path.exists(countries_json_path) or force:
|
||||||
cdn_version_json = requests.get(f'{ADVENTURELOG_CDN_URL}/data/version.json')
|
res = requests.get(f'https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/{COUNTRY_REGION_JSON_VERSION}/json/countries%2Bstates%2Bcities.json')
|
||||||
cdn_version_json.raise_for_status()
|
if res.status_code == 200:
|
||||||
cdn_version = cdn_version_json.json().get('version')
|
with open(countries_json_path, 'w') as f:
|
||||||
if os.path.exists(current_version_json):
|
f.write(res.text)
|
||||||
with open(current_version_json, 'r') as f:
|
self.stdout.write(self.style.SUCCESS('countries+regions+states.json downloaded successfully'))
|
||||||
local_version = f.read().strip()
|
|
||||||
self.stdout.write(self.style.SUCCESS(f'Local version: {local_version}'))
|
|
||||||
else:
|
else:
|
||||||
local_version = None
|
self.stdout.write(self.style.ERROR('Error downloading countries+regions+states.json'))
|
||||||
|
|
||||||
if force or local_version != cdn_version:
|
|
||||||
with open(current_version_json, 'w') as f:
|
|
||||||
f.write(cdn_version)
|
|
||||||
self.stdout.write(self.style.SUCCESS('Version updated successfully to ' + cdn_version))
|
|
||||||
else:
|
|
||||||
self.stdout.write(self.style.SUCCESS('Data is already up-to-date. Run with --force to re-download'))
|
|
||||||
return
|
return
|
||||||
except requests.RequestException as e:
|
elif not os.path.isfile(countries_json_path):
|
||||||
self.stdout.write(self.style.ERROR(f'Error fetching version from the CDN: {e}, skipping data import. Try restarting the container once CDN connection has been restored.'))
|
self.stdout.write(self.style.ERROR('countries+regions+states.json is not a file'))
|
||||||
return
|
return
|
||||||
|
elif os.path.getsize(countries_json_path) == 0:
|
||||||
self.stdout.write(self.style.SUCCESS('Fetching latest data from the AdventureLog CDN located at: ' + ADVENTURELOG_CDN_URL))
|
self.stdout.write(self.style.ERROR('countries+regions+states.json is empty'))
|
||||||
|
elif Country.objects.count() == 0 or Region.objects.count() == 0 or City.objects.count() == 0:
|
||||||
# Delete the existing flags
|
self.stdout.write(self.style.WARNING('Some region data is missing. Re-importing all data.'))
|
||||||
flags_dir = os.path.join(media_root, 'flags')
|
|
||||||
if os.path.exists(flags_dir):
|
|
||||||
for file in os.listdir(flags_dir):
|
|
||||||
os.remove(os.path.join(flags_dir, file))
|
|
||||||
|
|
||||||
# Delete the existing countries, regions, and cities json files
|
|
||||||
countries_json_path = os.path.join(media_root, 'countries_states_cities.json')
|
|
||||||
if os.path.exists(countries_json_path):
|
|
||||||
os.remove(countries_json_path)
|
|
||||||
self.stdout.write(self.style.SUCCESS('countries_states_cities.json deleted successfully'))
|
|
||||||
|
|
||||||
# Download the latest countries, regions, and cities json file
|
|
||||||
res = requests.get(f'{ADVENTURELOG_CDN_URL}/data/countries_states_cities.json')
|
|
||||||
if res.status_code == 200:
|
|
||||||
with open(countries_json_path, 'w') as f:
|
|
||||||
f.write(res.text)
|
|
||||||
self.stdout.write(self.style.SUCCESS('countries_states_cities.json downloaded successfully'))
|
|
||||||
else:
|
else:
|
||||||
self.stdout.write(self.style.ERROR('Error downloading countries_states_cities.json'))
|
self.stdout.write(self.style.SUCCESS('Latest country, region, and state data already downloaded.'))
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(countries_json_path, 'r') as f:
|
with open(countries_json_path, 'r') as f:
|
||||||
|
|
|
@ -18,9 +18,7 @@
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log(res);
|
|
||||||
let data = await res.json();
|
let data = await res.json();
|
||||||
console.log('ACTIVITIES' + data.activities);
|
|
||||||
if (data && data.activities) {
|
if (data && data.activities) {
|
||||||
allActivities = data.activities;
|
allActivities = data.activities;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
|
|
||||||
let categories: Category[] = [];
|
let categories: Category[] = [];
|
||||||
|
|
||||||
|
export let initialLatLng: { lat: number; lng: number } | null = null; // Used to pass the location from the map selection to the modal
|
||||||
|
|
||||||
let fileInput: HTMLInputElement;
|
let fileInput: HTMLInputElement;
|
||||||
let immichIntegration: boolean = false;
|
let immichIntegration: boolean = false;
|
||||||
|
|
||||||
|
@ -87,7 +89,6 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||||
modal.showModal();
|
modal.showModal();
|
||||||
console.log('open');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let url: string = '';
|
let url: string = '';
|
||||||
|
@ -142,14 +143,11 @@
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
if (input.files && input.files.length) {
|
if (input.files && input.files.length) {
|
||||||
selectedFile = input.files[0];
|
selectedFile = input.files[0];
|
||||||
console.log('Selected file:', selectedFile);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadAttachment(event: Event) {
|
async function uploadAttachment(event: Event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
console.log('UPLOAD');
|
|
||||||
console.log(selectedFile);
|
|
||||||
|
|
||||||
if (!selectedFile) {
|
if (!selectedFile) {
|
||||||
console.error('No files selected');
|
console.error('No files selected');
|
||||||
|
@ -157,23 +155,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = selectedFile;
|
const file = selectedFile;
|
||||||
console.log(file);
|
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('adventure', adventure.id);
|
formData.append('adventure', adventure.id);
|
||||||
formData.append('name', attachmentName);
|
formData.append('name', attachmentName);
|
||||||
|
|
||||||
console.log(formData);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/adventures?/attachment', {
|
const res = await fetch('/adventures?/attachment', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(res);
|
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const newData = deserialize(await res.text()) as { data: Attachment };
|
const newData = deserialize(await res.text()) as { data: Attachment };
|
||||||
adventure.attachments = [...adventure.attachments, newData.data];
|
adventure.attachments = [...adventure.attachments, newData.data];
|
||||||
|
@ -202,7 +195,6 @@
|
||||||
if (res.status === 204) {
|
if (res.status === 204) {
|
||||||
images = images.filter((image) => image.id !== id);
|
images = images.filter((image) => image.id !== id);
|
||||||
adventure.images = images;
|
adventure.images = images;
|
||||||
console.log(images);
|
|
||||||
addToast('success', $t('adventures.image_removed_success'));
|
addToast('success', $t('adventures.image_removed_success'));
|
||||||
} else {
|
} else {
|
||||||
addToast('error', $t('adventures.image_removed_error'));
|
addToast('error', $t('adventures.image_removed_error'));
|
||||||
|
@ -255,9 +247,7 @@
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let newData = deserialize(await res.text()) as { data: { id: string; image: string } };
|
let newData = deserialize(await res.text()) as { data: { id: string; image: string } };
|
||||||
console.log(newData);
|
|
||||||
let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false };
|
let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false };
|
||||||
console.log(newImage);
|
|
||||||
images = [...images, newImage];
|
images = [...images, newImage];
|
||||||
adventure.images = images;
|
adventure.images = images;
|
||||||
addToast('success', $t('adventures.image_upload_success'));
|
addToast('success', $t('adventures.image_upload_success'));
|
||||||
|
@ -308,9 +298,7 @@
|
||||||
});
|
});
|
||||||
if (res2.ok) {
|
if (res2.ok) {
|
||||||
let newData = deserialize(await res2.text()) as { data: { id: string; image: string } };
|
let newData = deserialize(await res2.text()) as { data: { id: string; image: string } };
|
||||||
console.log(newData);
|
|
||||||
let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false };
|
let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false };
|
||||||
console.log(newImage);
|
|
||||||
images = [...images, newImage];
|
images = [...images, newImage];
|
||||||
adventure.images = images;
|
adventure.images = images;
|
||||||
addToast('success', $t('adventures.image_upload_success'));
|
addToast('success', $t('adventures.image_upload_success'));
|
||||||
|
@ -371,30 +359,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function imageSubmit() {
|
|
||||||
return async ({ result }: any) => {
|
|
||||||
if (result.type === 'success') {
|
|
||||||
if (result.data.id && result.data.image) {
|
|
||||||
adventure.images = [...adventure.images, result.data];
|
|
||||||
images = [...images, result.data];
|
|
||||||
addToast('success', $t('adventures.image_upload_success'));
|
|
||||||
|
|
||||||
fileInput.value = '';
|
|
||||||
console.log(adventure);
|
|
||||||
} else {
|
|
||||||
addToast('error', result.data.error || $t('adventures.image_upload_error'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit(event: Event) {
|
async function handleSubmit(event: Event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
triggerMarkVisted = true;
|
triggerMarkVisted = true;
|
||||||
|
|
||||||
console.log(adventure);
|
|
||||||
if (adventure.id === '') {
|
if (adventure.id === '') {
|
||||||
console.log(categories);
|
|
||||||
if (adventure.category?.display_name == '') {
|
if (adventure.category?.display_name == '') {
|
||||||
if (categories.some((category) => category.name === 'general')) {
|
if (categories.some((category) => category.name === 'general')) {
|
||||||
adventure.category = categories.find(
|
adventure.category = categories.find(
|
||||||
|
@ -597,7 +566,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LocationDropdown bind:item={adventure} bind:triggerMarkVisted />
|
<LocationDropdown bind:item={adventure} bind:triggerMarkVisted {initialLatLng} />
|
||||||
|
|
||||||
<div class="collapse collapse-plus bg-base-200 mb-4 overflow-visible">
|
<div class="collapse collapse-plus bg-base-200 mb-4 overflow-visible">
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
|
@ -958,7 +927,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if adventure.is_public && adventure.id}
|
{#if adventure.is_public && adventure.id}
|
||||||
<div class="bg-neutral p-4 mt-2 rounded-md shadow-sm">
|
<div class="bg-neutral p-4 mt-2 rounded-md shadow-sm text-neutral-content">
|
||||||
<p class=" font-semibold">{$t('adventures.share_adventure')}</p>
|
<p class=" font-semibold">{$t('adventures.share_adventure')}</p>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<p class="text-card-foreground font-mono">
|
<p class="text-card-foreground font-mono">
|
||||||
|
|
|
@ -40,6 +40,9 @@
|
||||||
<li><button on:click={() => goto('/adventures')}>{$t('navbar.my_adventures')}</button></li>
|
<li><button on:click={() => goto('/adventures')}>{$t('navbar.my_adventures')}</button></li>
|
||||||
<li><button on:click={() => goto('/shared')}>{$t('navbar.shared_with_me')}</button></li>
|
<li><button on:click={() => goto('/shared')}>{$t('navbar.shared_with_me')}</button></li>
|
||||||
<li><button on:click={() => goto('/settings')}>{$t('navbar.settings')}</button></li>
|
<li><button on:click={() => goto('/settings')}>{$t('navbar.settings')}</button></li>
|
||||||
|
{#if user.is_staff}
|
||||||
|
<li><button on:click={() => goto('/admin')}>{$t('navbar.admin_panel')}</button></li>
|
||||||
|
{/if}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<li><button formaction="/?/logout">{$t('navbar.logout')}</button></li>
|
<li><button formaction="/?/logout">{$t('navbar.logout')}</button></li>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -2,12 +2,15 @@
|
||||||
import { appVersion } from '$lib/config';
|
import { appVersion } from '$lib/config';
|
||||||
import { addToast } from '$lib/toasts';
|
import { addToast } from '$lib/toasts';
|
||||||
import type { Adventure, Lodging, OpenStreetMapPlace, Point, ReverseGeocode } from '$lib/types';
|
import type { Adventure, Lodging, OpenStreetMapPlace, Point, ReverseGeocode } from '$lib/types';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
|
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
|
||||||
|
|
||||||
export let item: Adventure | Lodging;
|
export let item: Adventure | Lodging;
|
||||||
export let triggerMarkVisted: boolean = false;
|
export let triggerMarkVisted: boolean = false;
|
||||||
|
|
||||||
|
export let initialLatLng: { lat: number; lng: number } | null = null; // Used to pass the location from the map selection to the modal
|
||||||
|
|
||||||
let reverseGeocodePlace: ReverseGeocode | null = null;
|
let reverseGeocodePlace: ReverseGeocode | null = null;
|
||||||
let markers: Point[] = [];
|
let markers: Point[] = [];
|
||||||
|
|
||||||
|
@ -19,6 +22,22 @@
|
||||||
let places: OpenStreetMapPlace[] = [];
|
let places: OpenStreetMapPlace[] = [];
|
||||||
let noPlaces: boolean = false;
|
let noPlaces: boolean = false;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (initialLatLng) {
|
||||||
|
markers = [
|
||||||
|
{
|
||||||
|
lngLat: { lng: initialLatLng.lng, lat: initialLatLng.lat },
|
||||||
|
name: '',
|
||||||
|
location: '',
|
||||||
|
activity_type: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
item.latitude = initialLatLng.lat;
|
||||||
|
item.longitude = initialLatLng.lng;
|
||||||
|
reverseGeocode();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$: if (markers.length > 0) {
|
$: if (markers.length > 0) {
|
||||||
const newLat = Math.round(markers[0].lngLat.lat * 1e6) / 1e6;
|
const newLat = Math.round(markers[0].lngLat.lat * 1e6) / 1e6;
|
||||||
const newLng = Math.round(markers[0].lngLat.lng * 1e6) / 1e6;
|
const newLng = Math.round(markers[0].lngLat.lng * 1e6) / 1e6;
|
||||||
|
@ -35,10 +54,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
|
||||||
console.log(triggerMarkVisted);
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (triggerMarkVisted && willBeMarkedVisited) {
|
$: if (triggerMarkVisted && willBeMarkedVisited) {
|
||||||
markVisited();
|
markVisited();
|
||||||
triggerMarkVisted = false;
|
triggerMarkVisted = false;
|
||||||
|
@ -84,8 +99,6 @@
|
||||||
break; // Exit the loop since we've determined the result
|
break; // Exit the loop since we've determined the result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('WMBV:', willBeMarkedVisited);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,6 +193,9 @@
|
||||||
) {
|
) {
|
||||||
old_display_name = reverseGeocodePlace.display_name;
|
old_display_name = reverseGeocodePlace.display_name;
|
||||||
item.location = reverseGeocodePlace.display_name;
|
item.location = reverseGeocodePlace.display_name;
|
||||||
|
if (reverseGeocodePlace.location_name) {
|
||||||
|
item.name = reverseGeocodePlace.location_name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.log(data);
|
console.log(data);
|
||||||
}
|
}
|
||||||
|
@ -187,6 +203,8 @@
|
||||||
function clearMap() {
|
function clearMap() {
|
||||||
console.log('CLEAR');
|
console.log('CLEAR');
|
||||||
markers = [];
|
markers = [];
|
||||||
|
item.latitude = null;
|
||||||
|
item.longitude = null;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -268,6 +286,8 @@
|
||||||
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
||||||
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full rounded-lg"
|
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full rounded-lg"
|
||||||
standardControls
|
standardControls
|
||||||
|
zoom={item.latitude && item.longitude ? 12 : 1}
|
||||||
|
center={{ lng: item.longitude || 0, lat: item.latitude || 0 }}
|
||||||
>
|
>
|
||||||
<!-- MapEvents gives you access to map events even from other components inside the map,
|
<!-- MapEvents gives you access to map events even from other components inside the map,
|
||||||
where you might not have access to the top-level `MapLibre` component. In this case
|
where you might not have access to the top-level `MapLibre` component. In this case
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
import { addToast } from '$lib/toasts';
|
import { addToast } from '$lib/toasts';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import DeleteWarning from './DeleteWarning.svelte';
|
import DeleteWarning from './DeleteWarning.svelte';
|
||||||
// import ArrowDownThick from '~icons/mdi/arrow-down-thick';
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
@ -36,20 +35,6 @@
|
||||||
let collectionStartDate = new Date(collection.start_date);
|
let collectionStartDate = new Date(collection.start_date);
|
||||||
let collectionEndDate = new Date(collection.end_date);
|
let collectionEndDate = new Date(collection.end_date);
|
||||||
|
|
||||||
// // Debugging outputs
|
|
||||||
// console.log(
|
|
||||||
// 'Transportation Start Date:',
|
|
||||||
// transportationStartDate,
|
|
||||||
// 'Transportation End Date:',
|
|
||||||
// transportationEndDate
|
|
||||||
// );
|
|
||||||
// console.log(
|
|
||||||
// 'Collection Start Date:',
|
|
||||||
// collectionStartDate,
|
|
||||||
// 'Collection End Date:',
|
|
||||||
// collectionEndDate
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Check if the collection range is outside the transportation range
|
// Check if the collection range is outside the transportation range
|
||||||
const startOutsideRange =
|
const startOutsideRange =
|
||||||
transportationStartDate &&
|
transportationStartDate &&
|
||||||
|
@ -88,9 +73,9 @@
|
||||||
|
|
||||||
{#if isWarningModalOpen}
|
{#if isWarningModalOpen}
|
||||||
<DeleteWarning
|
<DeleteWarning
|
||||||
title={$t('adventures.delete_transportation')}
|
title={$t('adventures.delete_lodging')}
|
||||||
button_text="Delete"
|
button_text="Delete"
|
||||||
description={$t('adventures.transportation_delete_confirm')}
|
description={$t('adventures.lodging_delete_confirm')}
|
||||||
is_warning={false}
|
is_warning={false}
|
||||||
on:close={() => (isWarningModalOpen = false)}
|
on:close={() => (isWarningModalOpen = false)}
|
||||||
on:confirm={deleteTransportation}
|
on:confirm={deleteTransportation}
|
||||||
|
@ -106,7 +91,7 @@
|
||||||
<h2 class="card-title text-lg font-semibold truncate">{lodging.name}</h2>
|
<h2 class="card-title text-lg font-semibold truncate">{lodging.name}</h2>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="badge badge-secondary">
|
<div class="badge badge-secondary">
|
||||||
{lodging.type}
|
{$t(`lodging.${lodging.type}`)}
|
||||||
</div>
|
</div>
|
||||||
<!-- {#if hotel.type == 'plane' && hotel.flight_number}
|
<!-- {#if hotel.type == 'plane' && hotel.flight_number}
|
||||||
<div class="badge badge-neutral-200">{hotel.flight_number}</div>
|
<div class="badge badge-neutral-200">{hotel.flight_number}</div>
|
||||||
|
@ -117,36 +102,35 @@
|
||||||
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Locations -->
|
<!-- Location -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#if lodging.location}
|
{#if lodging.location}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-medium text-sm">{$t('adventures.from')}:</span>
|
<span class="font-medium text-sm">{$t('adventures.location')}:</span>
|
||||||
<p class="break-words">{lodging.location}</p>
|
<p>{lodging.location}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if lodging.check_in && lodging.check_out}
|
{#if lodging.check_in && lodging.check_out}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-medium text-sm">{$t('adventures.start')}:</span>
|
<span class="font-medium text-sm">{$t('adventures.dates')}:</span>
|
||||||
<p>{new Date(lodging.check_in).toLocaleDateString(undefined, { timeZone: 'UTC' })}</p>
|
<p>
|
||||||
</div>
|
{new Date(lodging.check_in).toLocaleString('en-US', {
|
||||||
{/if}
|
month: 'short',
|
||||||
</div>
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
<!-- Dates -->
|
hour: 'numeric',
|
||||||
<div class="space-y-2">
|
minute: 'numeric'
|
||||||
{#if lodging.location}
|
})}
|
||||||
<!-- <ArrowDownThick class="w-4 h-4" /> -->
|
-
|
||||||
<div class="flex items-center gap-2">
|
{new Date(lodging.check_out).toLocaleString('en-US', {
|
||||||
<span class="font-medium text-sm">{$t('adventures.to')}:</span>
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
<p class="break-words">{lodging.location}</p>
|
year: 'numeric',
|
||||||
</div>
|
hour: 'numeric',
|
||||||
{/if}
|
minute: 'numeric'
|
||||||
{#if lodging.check_out}
|
})}
|
||||||
<div class="flex items-center gap-2">
|
</p>
|
||||||
<span class="font-medium text-sm">{$t('adventures.end')}:</span>
|
|
||||||
<p>{new Date(lodging.check_out).toLocaleDateString(undefined, { timeZone: 'UTC' })}</p>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -53,8 +53,8 @@
|
||||||
description: hotelToEdit?.description || '',
|
description: hotelToEdit?.description || '',
|
||||||
rating: hotelToEdit?.rating || NaN,
|
rating: hotelToEdit?.rating || NaN,
|
||||||
link: hotelToEdit?.link || '',
|
link: hotelToEdit?.link || '',
|
||||||
check_in: hotelToEdit?.check_in || null,
|
check_in: hotelToEdit?.check_in ? toLocalDatetime(hotelToEdit.check_in) : null,
|
||||||
check_out: hotelToEdit?.check_out || null,
|
check_out: hotelToEdit?.check_out ? toLocalDatetime(hotelToEdit.check_out) : null,
|
||||||
reservation_number: hotelToEdit?.reservation_number || '',
|
reservation_number: hotelToEdit?.reservation_number || '',
|
||||||
price: hotelToEdit?.price || null,
|
price: hotelToEdit?.price || null,
|
||||||
latitude: hotelToEdit?.latitude || null,
|
latitude: hotelToEdit?.latitude || null,
|
||||||
|
@ -166,6 +166,32 @@
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="type">
|
||||||
|
{$t('transportation.type')}<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
class="select select-bordered w-full max-w-xs"
|
||||||
|
name="type"
|
||||||
|
id="type"
|
||||||
|
bind:value={lodging.type}
|
||||||
|
>
|
||||||
|
<option disabled selected>{$t('transportation.type')}</option>
|
||||||
|
<option value="hotel">{$t('lodging.hotel')}</option>
|
||||||
|
<option value="hostel">{$t('lodging.hostel')}</option>
|
||||||
|
<option value="resort">{$t('lodging.resort')}</option>
|
||||||
|
<option value="bnb">{$t('lodging.bnb')}</option>
|
||||||
|
<option value="campground">{$t('lodging.campground')}</option>
|
||||||
|
<option value="cabin">{$t('lodging.cabin')}</option>
|
||||||
|
<option value="apartment">{$t('lodging.apartment')}</option>
|
||||||
|
<option value="house">{$t('lodging.house')}</option>
|
||||||
|
<option value="villa">{$t('lodging.villa')}</option>
|
||||||
|
<option value="motel">{$t('lodging.motel')}</option>
|
||||||
|
<option value="other">{$t('lodging.other')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<div>
|
<div>
|
||||||
<label for="description">{$t('adventures.description')}</label><br />
|
<label for="description">{$t('adventures.description')}</label><br />
|
||||||
|
@ -250,13 +276,53 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">
|
||||||
|
{$t('adventures.lodging_information')}
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<!-- Reservation Number -->
|
||||||
|
<div>
|
||||||
|
<label for="date">
|
||||||
|
{$t('lodging.reservation_number')}
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="reservation_number"
|
||||||
|
name="reservation_number"
|
||||||
|
bind:value={lodging.reservation_number}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Price -->
|
||||||
|
<div>
|
||||||
|
<label for="price">
|
||||||
|
{$t('adventures.price')}
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="price"
|
||||||
|
name="price"
|
||||||
|
bind:value={lodging.price}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="collapse collapse-plus bg-base-200 mb-4">
|
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
{$t('adventures.date_information')}
|
{$t('adventures.date_information')}
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<!-- Start Date -->
|
<!-- Check In -->
|
||||||
<div>
|
<div>
|
||||||
<label for="date">
|
<label for="date">
|
||||||
{$t('lodging.check_in')}
|
{$t('lodging.check_in')}
|
||||||
|
|
|
@ -243,6 +243,20 @@
|
||||||
{$t('adventures.basic_information')}
|
{$t('adventures.basic_information')}
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
|
<!-- Name -->
|
||||||
|
<div>
|
||||||
|
<label for="name">
|
||||||
|
{$t('adventures.name')}<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
bind:value={transportation.name}
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<!-- Type selection -->
|
<!-- Type selection -->
|
||||||
<div>
|
<div>
|
||||||
<label for="type">
|
<label for="type">
|
||||||
|
@ -267,20 +281,6 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Name -->
|
|
||||||
<div>
|
|
||||||
<label for="name">
|
|
||||||
{$t('adventures.name')}<span class="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
bind:value={transportation.name}
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<div>
|
<div>
|
||||||
<label for="description">{$t('adventures.description')}</label><br />
|
<label for="description">{$t('adventures.description')}</label><br />
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type {
|
||||||
Background,
|
Background,
|
||||||
Checklist,
|
Checklist,
|
||||||
Collection,
|
Collection,
|
||||||
|
Lodging,
|
||||||
Note,
|
Note,
|
||||||
Transportation,
|
Transportation,
|
||||||
User
|
User
|
||||||
|
@ -149,6 +150,50 @@ export function groupTransportationsByDate(
|
||||||
return groupedTransportations;
|
return groupedTransportations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function groupLodgingByDate(
|
||||||
|
transportations: Lodging[],
|
||||||
|
startDate: Date,
|
||||||
|
numberOfDays: number
|
||||||
|
): Record<string, Lodging[]> {
|
||||||
|
const groupedTransportations: Record<string, Lodging[]> = {};
|
||||||
|
|
||||||
|
// Initialize all days in the range
|
||||||
|
for (let i = 0; i < numberOfDays; i++) {
|
||||||
|
const currentDate = new Date(startDate);
|
||||||
|
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||||
|
const dateString = currentDate.toISOString().split('T')[0];
|
||||||
|
groupedTransportations[dateString] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
transportations.forEach((transportation) => {
|
||||||
|
if (transportation.check_in) {
|
||||||
|
const transportationDate = new Date(transportation.check_in).toISOString().split('T')[0];
|
||||||
|
if (transportation.check_out) {
|
||||||
|
const endDate = new Date(transportation.check_out).toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Loop through all days and include transportation if it falls within the range
|
||||||
|
for (let i = 0; i < numberOfDays; i++) {
|
||||||
|
const currentDate = new Date(startDate);
|
||||||
|
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||||
|
const dateString = currentDate.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Include the current day if it falls within the transportation date range
|
||||||
|
if (dateString >= transportationDate && dateString <= endDate) {
|
||||||
|
if (groupedTransportations[dateString]) {
|
||||||
|
groupedTransportations[dateString].push(transportation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (groupedTransportations[transportationDate]) {
|
||||||
|
// If there's no end date, add transportation to the start date only
|
||||||
|
groupedTransportations[transportationDate].push(transportation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupedTransportations;
|
||||||
|
}
|
||||||
|
|
||||||
export function groupNotesByDate(
|
export function groupNotesByDate(
|
||||||
notes: Note[],
|
notes: Note[],
|
||||||
startDate: Date,
|
startDate: Date,
|
||||||
|
@ -473,4 +518,4 @@ export function debounce(func: Function, timeout: number) {
|
||||||
func(...args);
|
func(...args);
|
||||||
}, timeout);
|
}, timeout);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -210,6 +210,7 @@ export type ReverseGeocode = {
|
||||||
display_name: string;
|
display_name: string;
|
||||||
city: string;
|
city: string;
|
||||||
city_id: string;
|
city_id: string;
|
||||||
|
location_name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Category = {
|
export type Category = {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -11,6 +11,19 @@ const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
export const load = (async (event) => {
|
export const load = (async (event) => {
|
||||||
if (event.locals.user) {
|
if (event.locals.user) {
|
||||||
return redirect(302, '/dashboard');
|
return redirect(302, '/dashboard');
|
||||||
|
} else {
|
||||||
|
let res = await fetch(`${serverEndpoint}/api/stats/locations/`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let data = await res.json();
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
locations: data
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
|
|
|
@ -1,126 +1,141 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import AdventureOverlook from '$lib/assets/AdventureOverlook.webp';
|
|
||||||
import MapWithPins from '$lib/assets/MapWithPins.webp';
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import { DefaultMarker, MapLibre, Marker, Popup } from 'svelte-maplibre';
|
||||||
|
|
||||||
|
import MapWithPins from '$lib/assets/MapWithPins.webp';
|
||||||
|
|
||||||
|
import InformationSlabCircleOutline from '~icons/mdi/information-slab-circle-outline';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="flex items-center justify-center w-full py-12 md:py-24 lg:py-32">
|
<!-- Hero Section -->
|
||||||
<div class="container px-4 md:px-6">
|
<section class="flex items-center justify-center w-full py-20 bg-gray-50 dark:bg-gray-800">
|
||||||
<div class="grid gap-6 lg:grid-cols-[1fr_550px] lg:gap-12 xl:grid-cols-[1fr_650px]">
|
<div class="container mx-auto px-4 flex flex-col-reverse md:flex-row items-center gap-8">
|
||||||
<div class="flex flex-col justify-center space-y-4">
|
<!-- Text Content -->
|
||||||
<div class="space-y-2">
|
<div class="w-full md:w-1/2 space-y-6">
|
||||||
{#if data.user}
|
{#if data.user}
|
||||||
{#if data.user.first_name && data.user.first_name !== null}
|
{#if data.user.first_name && data.user.first_name !== null}
|
||||||
<h1
|
<h1
|
||||||
class="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pb-4"
|
class="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||||
>
|
>
|
||||||
{data.user.first_name.charAt(0).toUpperCase() + data.user.first_name.slice(1)},
|
{data.user.first_name.charAt(0).toUpperCase() + data.user.first_name.slice(1)}, {$t(
|
||||||
{$t('home.hero_1')}
|
'home.hero_1'
|
||||||
</h1>
|
)}
|
||||||
{:else}
|
</h1>
|
||||||
<h1
|
{:else}
|
||||||
class="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pb-4"
|
<h1
|
||||||
>
|
class="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||||
{$t('home.hero_1')}
|
>
|
||||||
</h1>
|
{$t('home.hero_1')}
|
||||||
{/if}
|
</h1>
|
||||||
{:else}
|
{/if}
|
||||||
<h1
|
{:else}
|
||||||
class="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pb-4"
|
<h1
|
||||||
>
|
class="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||||
{$t('home.hero_1')}
|
>
|
||||||
</h1>
|
{$t('home.hero_1')}
|
||||||
{/if}
|
</h1>
|
||||||
<p class="max-w-[600px] text-gray-500 md:text-xl dark:text-gray-400">
|
{/if}
|
||||||
{$t('home.hero_2')}
|
<p class="text-xl text-gray-600 dark:text-gray-300 max-w-xl">
|
||||||
</p>
|
{$t('home.hero_2')}
|
||||||
</div>
|
</p>
|
||||||
<div class="flex flex-col gap-2 min-[400px]:flex-row">
|
<div class="flex flex-col sm:flex-row gap-4">
|
||||||
{#if data.user}
|
{#if data.user}
|
||||||
<button on:click={() => goto('/adventures')} class="btn btn-primary">
|
<button on:click={() => goto('/adventures')} class="btn btn-primary">
|
||||||
{$t('home.go_to')}
|
{$t('home.go_to')}
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<button on:click={() => goto('/login')} class="btn btn-primary">
|
<button on:click={() => goto('/login')} class="btn btn-primary">
|
||||||
{$t('auth.login')}
|
{$t('auth.login')}
|
||||||
</button>
|
</button>
|
||||||
<button on:click={() => goto('/signup')} class="btn btn-neutral">
|
<button on:click={() => goto('/signup')} class="btn btn-secondary">
|
||||||
{$t('auth.signup')}
|
{$t('auth.signup')}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<img
|
</div>
|
||||||
src={AdventureOverlook}
|
<!-- Image -->
|
||||||
width="550"
|
<div class="w-full md:w-1/2">
|
||||||
height="550"
|
<p class="flex items-center text-neutral-content">
|
||||||
alt="Hero"
|
<InformationSlabCircleOutline class="w-4 h-4 mr-1" />
|
||||||
class="mx-auto aspect-video overflow-hidden rounded-xl object-cover sm:w-full lg:order-last"
|
{$t('adventures.welcome_map_info')}
|
||||||
/>
|
</p>
|
||||||
|
<MapLibre
|
||||||
|
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
||||||
|
class="flex items-center self-center justify-center aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12 rounded-lg"
|
||||||
|
standardControls
|
||||||
|
>
|
||||||
|
{#each data.props.locations as location}
|
||||||
|
{#if location.latitude && location.longitude}
|
||||||
|
<DefaultMarker
|
||||||
|
lngLat={[location.longitude, location.latitude]}
|
||||||
|
on:click={() => goto(`/locations/${location.id}`)}
|
||||||
|
>
|
||||||
|
<span class="text-xl">{location.name}</span>
|
||||||
|
<Popup openOn="click" offset={[0, -10]}>
|
||||||
|
<div class="text-lg text-black font-bold">{location.name}</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-neutral btn-wide btn-sm mt-4"
|
||||||
|
on:click={() => goto(`/adventures/${location.id}`)}
|
||||||
|
>
|
||||||
|
{$t('map.view_details')}
|
||||||
|
</button>
|
||||||
|
</Popup>
|
||||||
|
</DefaultMarker>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</MapLibre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section
|
|
||||||
class="flex items-center justify-center w-full py-12 md:py-24 lg:py-32 bg-gray-100 dark:bg-gray-800"
|
<!-- Features Section -->
|
||||||
>
|
<section id="features" class="py-16 bg-white dark:bg-gray-900">
|
||||||
<div class="container px-4 md:px-6">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex flex-col items-center justify-center space-y-4 text-center">
|
<div class="text-center mb-12">
|
||||||
<div class="space-y-2">
|
<div class="inline-block text-neutral-content bg-neutral px-4 py-2 rounded-full">
|
||||||
<div
|
{$t('home.key_features')}
|
||||||
class="inline-block rounded-lg bg-gray-100 px-3 py-1 text-md dark:bg-gray-800 dark:text-gray-400"
|
|
||||||
>
|
|
||||||
{$t('home.key_features')}
|
|
||||||
</div>
|
|
||||||
<h2
|
|
||||||
class="text-3xl font-bold tracking-tighter sm:text-5xl bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
|
||||||
>
|
|
||||||
{$t('home.desc_1')}
|
|
||||||
</h2>
|
|
||||||
<p
|
|
||||||
class="max-w-[900px] text-gray-500 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed dark:text-gray-400"
|
|
||||||
>
|
|
||||||
{$t('home.desc_2')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h2
|
||||||
|
class="mt-4 text-3xl md:text-4xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||||
|
>
|
||||||
|
{$t('home.desc_1')}
|
||||||
|
</h2>
|
||||||
|
<p class="mt-4 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto text-lg">
|
||||||
|
{$t('home.desc_2')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-auto grid max-w-5xl items-center gap-6 py-12 lg:grid-cols-2 lg:gap-12">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
||||||
<!-- svelte-ignore a11y-img-redundant-alt -->
|
<!-- Image for Features -->
|
||||||
<img
|
<div class="order-1 md:order-2">
|
||||||
src={MapWithPins}
|
<img
|
||||||
width="550"
|
src={MapWithPins}
|
||||||
height="310"
|
alt="World map with pins"
|
||||||
alt="Image"
|
class="rounded-lg shadow-lg object-cover"
|
||||||
class="mx-auto aspect-video overflow-hidden rounded-xl object-cover object-center sm:w-full lg:order-last"
|
/>
|
||||||
/>
|
</div>
|
||||||
<div class="flex flex-col justify-center space-y-4">
|
<!-- Feature List -->
|
||||||
<ul class="grid gap-6">
|
<div class="order-2 md:order-1">
|
||||||
<li>
|
<ul class="space-y-6">
|
||||||
<div class="grid gap-1">
|
<li class="space-y-2">
|
||||||
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_1')}</h3>
|
<h3 class="text-xl font-semibold dark:text-gray-300">{$t('home.feature_1')}</h3>
|
||||||
<p class="text-gray-500 dark:text-gray-400">
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
{$t('home.feature_1_desc')}
|
{$t('home.feature_1_desc')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li class="space-y-2">
|
||||||
<div class="grid gap-1">
|
<h3 class="text-xl font-semibold dark:text-gray-300">{$t('home.feature_2')}</h3>
|
||||||
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_2')}</h3>
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
<p class="text-gray-500 dark:text-gray-400">
|
{$t('home.feature_2_desc')}
|
||||||
{$t('home.feature_2_desc')}
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li class="space-y-2">
|
||||||
<div class="grid gap-1">
|
<h3 class="text-xl font-semibold dark:text-gray-300">{$t('home.feature_3')}</h3>
|
||||||
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_3')}</h3>
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
<p class="text-gray-500 dark:text-gray-400">
|
{$t('home.feature_3_desc')}
|
||||||
{$t('home.feature_3_desc')}
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
import toGeoJSON from '@mapbox/togeojson';
|
import toGeoJSON from '@mapbox/togeojson';
|
||||||
|
|
||||||
import LightbulbOn from '~icons/mdi/lightbulb-on';
|
import LightbulbOn from '~icons/mdi/lightbulb-on';
|
||||||
import Account from '~icons/mdi/account';
|
|
||||||
|
|
||||||
let geojson: any;
|
let geojson: any;
|
||||||
|
|
||||||
|
@ -186,7 +185,10 @@
|
||||||
alt={adventure.name}
|
alt={adventure.name}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<div class="flex justify-center w-full py-2 gap-2">
|
<!-- Scrollable button container -->
|
||||||
|
<div
|
||||||
|
class="flex w-full py-2 gap-2 overflow-x-auto whitespace-nowrap scrollbar-hide justify-start"
|
||||||
|
>
|
||||||
{#each adventure.images as _, i}
|
{#each adventure.images as _, i}
|
||||||
<button
|
<button
|
||||||
on:click={() => goToSlide(i)}
|
on:click={() => goToSlide(i)}
|
||||||
|
@ -198,6 +200,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="grid gap-4">
|
<div class="grid gap-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
@ -222,40 +225,43 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<div class="flex items-center gap-2">
|
{#if adventure.user}
|
||||||
{#if adventure.user.profile_pic}
|
<div class="flex items-center gap-2">
|
||||||
<div class="avatar">
|
{#if adventure.user.profile_pic}
|
||||||
<div class="w-8 rounded-full">
|
<div class="avatar">
|
||||||
<img src={adventure.user.profile_pic} />
|
<div class="w-8 rounded-full">
|
||||||
|
<img src={adventure.user.profile_pic} alt={adventure.user.username} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="avatar placeholder">
|
|
||||||
<div class="bg-neutral text-neutral-content w-8 rounded-full">
|
|
||||||
<span class="text-lg"
|
|
||||||
>{adventure.user.first_name
|
|
||||||
? adventure.user.first_name.charAt(0)
|
|
||||||
: adventure.user.username.charAt(0)}{adventure.user.last_name
|
|
||||||
? adventure.user.last_name.charAt(0)
|
|
||||||
: ''}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div>
|
|
||||||
{#if adventure.user.public_profile}
|
|
||||||
<a href={`/profile/${adventure.user.username}`} class="text-base font-medium">
|
|
||||||
{adventure.user.first_name || adventure.user.username}{' '}
|
|
||||||
{adventure.user.last_name}
|
|
||||||
</a>
|
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-base font-medium">
|
<div class="avatar placeholder">
|
||||||
{adventure.user.first_name || adventure.user.username}{' '}
|
<div class="bg-neutral text-neutral-content w-8 rounded-full">
|
||||||
{adventure.user.last_name}
|
<span class="text-lg"
|
||||||
</span>
|
>{adventure.user.first_name
|
||||||
|
? adventure.user.first_name.charAt(0)
|
||||||
|
: adventure.user.username.charAt(0)}{adventure.user.last_name
|
||||||
|
? adventure.user.last_name.charAt(0)
|
||||||
|
: ''}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if adventure.user.public_profile}
|
||||||
|
<a href={`/profile/${adventure.user.username}`} class="text-base font-medium">
|
||||||
|
{adventure.user.first_name || adventure.user.username}{' '}
|
||||||
|
{adventure.user.last_name}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<span class="text-base font-medium">
|
||||||
|
{adventure.user.first_name || adventure.user.username}{' '}
|
||||||
|
{adventure.user.last_name}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
|
@ -27,7 +27,8 @@
|
||||||
groupNotesByDate,
|
groupNotesByDate,
|
||||||
groupTransportationsByDate,
|
groupTransportationsByDate,
|
||||||
groupChecklistsByDate,
|
groupChecklistsByDate,
|
||||||
osmTagToEmoji
|
osmTagToEmoji,
|
||||||
|
groupLodgingByDate
|
||||||
} from '$lib';
|
} from '$lib';
|
||||||
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
|
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
|
||||||
import ChecklistModal from '$lib/components/ChecklistModal.svelte';
|
import ChecklistModal from '$lib/components/ChecklistModal.svelte';
|
||||||
|
@ -861,6 +862,10 @@
|
||||||
new Date(collection.start_date),
|
new Date(collection.start_date),
|
||||||
numberOfDays
|
numberOfDays
|
||||||
)[dateString] || []}
|
)[dateString] || []}
|
||||||
|
{@const dayLodging =
|
||||||
|
groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays)[
|
||||||
|
dateString
|
||||||
|
] || []}
|
||||||
{@const dayNotes =
|
{@const dayNotes =
|
||||||
groupNotesByDate(notes, new Date(collection.start_date), numberOfDays)[dateString] ||
|
groupNotesByDate(notes, new Date(collection.start_date), numberOfDays)[dateString] ||
|
||||||
[]}
|
[]}
|
||||||
|
@ -922,6 +927,18 @@
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if dayLodging.length > 0}
|
||||||
|
{#each dayLodging as hotel}
|
||||||
|
<LodgingCard
|
||||||
|
lodging={hotel}
|
||||||
|
user={data?.user}
|
||||||
|
on:delete={(event) => {
|
||||||
|
lodging = lodging.filter((t) => t.id != event.detail);
|
||||||
|
}}
|
||||||
|
on:edit={editLodging}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
{#if dayChecklists.length > 0}
|
{#if dayChecklists.length > 0}
|
||||||
{#each dayChecklists as checklist}
|
{#each dayChecklists as checklist}
|
||||||
<ChecklistCard
|
<ChecklistCard
|
||||||
|
@ -939,7 +956,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0}
|
{#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0 && dayLodging.length == 0}
|
||||||
<p class="text-center text-lg mt-2 italic">{$t('adventures.nothing_planned')}</p>
|
<p class="text-center text-lg mt-2 italic">{$t('adventures.nothing_planned')}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1068,7 +1085,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if currentView == 'recommendations'}
|
{#if currentView == 'recommendations' && data.user}
|
||||||
<div class="card bg-base-200 shadow-xl my-8 mx-auto w-10/12">
|
<div class="card bg-base-200 shadow-xl my-8 mx-auto w-10/12">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-3xl justify-center mb-4">Adventure Recommendations</h2>
|
<h2 class="card-title text-3xl justify-center mb-4">Adventure Recommendations</h2>
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
let createModalOpen: boolean = false;
|
let createModalOpen: boolean = false;
|
||||||
let showGeo: boolean = false;
|
let showGeo: boolean = false;
|
||||||
|
|
||||||
|
export let initialLatLng: { lat: number; lng: number } | null = null;
|
||||||
|
|
||||||
let visitedRegions: VisitedRegion[] = data.props.visitedRegions;
|
let visitedRegions: VisitedRegion[] = data.props.visitedRegions;
|
||||||
let adventures: Adventure[] = data.props.adventures;
|
let adventures: Adventure[] = data.props.adventures;
|
||||||
|
|
||||||
|
@ -49,6 +51,11 @@
|
||||||
newLatitude = e.detail.lngLat.lat;
|
newLatitude = e.detail.lngLat.lat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function newAdventure() {
|
||||||
|
initialLatLng = { lat: newLatitude, lng: newLongitude } as { lat: number; lng: number };
|
||||||
|
createModalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
function createNewAdventure(event: CustomEvent) {
|
function createNewAdventure(event: CustomEvent) {
|
||||||
adventures = [...adventures, event.detail];
|
adventures = [...adventures, event.detail];
|
||||||
newMarker = null;
|
newMarker = null;
|
||||||
|
@ -86,7 +93,7 @@
|
||||||
/>
|
/>
|
||||||
<div class="divider divider-horizontal"></div>
|
<div class="divider divider-horizontal"></div>
|
||||||
{#if newMarker}
|
{#if newMarker}
|
||||||
<button type="button" class="btn btn-primary mb-2" on:click={() => (createModalOpen = true)}
|
<button type="button" class="btn btn-primary mb-2" on:click={newAdventure}
|
||||||
>{$t('map.add_adventure_at_marker')}</button
|
>{$t('map.add_adventure_at_marker')}</button
|
||||||
>
|
>
|
||||||
<button type="button" class="btn btn-neutral mb-2" on:click={() => (newMarker = null)}
|
<button type="button" class="btn btn-neutral mb-2" on:click={() => (newMarker = null)}
|
||||||
|
@ -105,14 +112,13 @@
|
||||||
<AdventureModal
|
<AdventureModal
|
||||||
on:close={() => (createModalOpen = false)}
|
on:close={() => (createModalOpen = false)}
|
||||||
on:save={createNewAdventure}
|
on:save={createNewAdventure}
|
||||||
latitude={newLatitude}
|
{initialLatLng}
|
||||||
longitude={newLongitude}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<MapLibre
|
<MapLibre
|
||||||
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
||||||
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full"
|
class="mx-auto aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12 rounded-lg"
|
||||||
standardControls
|
standardControls
|
||||||
>
|
>
|
||||||
{#each filteredAdventures as adventure}
|
{#each filteredAdventures as adventure}
|
||||||
|
|
|
@ -458,7 +458,7 @@
|
||||||
<!-- Social Auth Settings -->
|
<!-- Social Auth Settings -->
|
||||||
<section class="space-y-8">
|
<section class="space-y-8">
|
||||||
<h2 class="text-2xl font-semibold text-center mt-8">{$t('settings.social_oidc_auth')}</h2>
|
<h2 class="text-2xl font-semibold text-center mt-8">{$t('settings.social_oidc_auth')}</h2>
|
||||||
<div class="bg-neutral p-6 rounded-lg shadow-md text-center">
|
<div class="bg-neutral p-6 rounded-lg shadow-md text-center text-neutral-content">
|
||||||
<p>
|
<p>
|
||||||
{$t('settings.social_auth_desc')}
|
{$t('settings.social_auth_desc')}
|
||||||
</p>
|
</p>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue