1
0
Fork 0
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:
Sean Morley 2025-02-15 19:44:11 -05:00
parent 60b5bbb3c8
commit b5d6788c11
21 changed files with 1048 additions and 901 deletions

View file

@ -21,11 +21,15 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
country_code = None
city = None
visited_city = None
location_name = None
# town = None
# city = None
# county = None
if 'name' in data.keys():
location_name = data['name']
if 'address' in data.keys():
keys = data['address'].keys()
for key in keys:
@ -58,7 +62,7 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
if visited_city:
city_visited = True
if region:
return {"region_id": iso_code, "region": region.name, "country": region.country.name, "region_visited": region_visited, "display_name": display_name, "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited}
return {"region_id": iso_code, "region": region.name, "country": region.country.name, "region_visited": region_visited, "display_name": display_name, "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited, 'location_name': location_name}
return {"error": "No region found"}
@action(detail=False, methods=['get'])

View file

@ -49,3 +49,10 @@ class StatsViewSet(viewsets.ViewSet):
'visited_country_count': visited_country_count,
'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)

View file

@ -61,7 +61,7 @@ INSTALLED_APPS = (
'users',
'integrations',
'django.contrib.gis',
'achievements',
# 'achievements', # Not done yet, will be added later in a future update
# 'widget_tweaks',
# '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'

View file

@ -68,6 +68,9 @@ class PublicUserListView(APIView):
for user in users:
user.email = None
serializer = PublicUserSerializer(users, many=True)
# for every user, remove the field has_password
for user in serializer.data:
user.pop('has_password', None)
return Response(serializer.data, status=status.HTTP_200_OK)
class PublicUserDetailView(APIView):
@ -87,6 +90,8 @@ class PublicUserDetailView(APIView):
else:
user = get_object_or_404(User, username=username, public_profile=True)
serializer = PublicUserSerializer(user)
# for every user, remove the field has_password
serializer.data.pop('has_password', None)
# remove the email address from the response
user.email = None

View file

@ -8,7 +8,7 @@ import ijson
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
@ -27,7 +27,7 @@ def saveCountryFlag(country_code):
print(f'Flag for {country_code} already exists')
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:
with open(flag_path, 'wb') as f:
f.write(res.content)
@ -39,56 +39,30 @@ class Command(BaseCommand):
help = 'Imports the world travel data'
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):
force = options['force']
batch_size = 100
current_version_json = os.path.join(settings.MEDIA_ROOT, 'data_version.json')
try:
cdn_version_json = requests.get(f'{ADVENTURELOG_CDN_URL}/data/version.json')
cdn_version_json.raise_for_status()
cdn_version = cdn_version_json.json().get('version')
if os.path.exists(current_version_json):
with open(current_version_json, 'r') as f:
local_version = f.read().strip()
self.stdout.write(self.style.SUCCESS(f'Local version: {local_version}'))
else:
local_version = None
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
except requests.RequestException as e:
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.'))
return
self.stdout.write(self.style.SUCCESS('Fetching latest data from the AdventureLog CDN located at: ' + ADVENTURELOG_CDN_URL))
# Delete the existing flags
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')
countries_json_path = os.path.join(settings.MEDIA_ROOT, f'countries+regions+states-{COUNTRY_REGION_JSON_VERSION}.json')
if not os.path.exists(countries_json_path) or force:
res = requests.get(f'https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/{COUNTRY_REGION_JSON_VERSION}/json/countries%2Bstates%2Bcities.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'))
self.stdout.write(self.style.SUCCESS('countries+regions+states.json downloaded successfully'))
else:
self.stdout.write(self.style.ERROR('Error downloading countries_states_cities.json'))
self.stdout.write(self.style.ERROR('Error downloading countries+regions+states.json'))
return
elif not os.path.isfile(countries_json_path):
self.stdout.write(self.style.ERROR('countries+regions+states.json is not a file'))
return
elif os.path.getsize(countries_json_path) == 0:
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:
self.stdout.write(self.style.WARNING('Some region data is missing. Re-importing all data.'))
else:
self.stdout.write(self.style.SUCCESS('Latest country, region, and state data already downloaded.'))
return
with open(countries_json_path, 'r') as f:

View file

@ -18,9 +18,7 @@
'Content-Type': 'application/json'
}
});
console.log(res);
let data = await res.json();
console.log('ACTIVITIES' + data.activities);
if (data && data.activities) {
allActivities = data.activities;
}

View file

@ -14,6 +14,8 @@
let categories: Category[] = [];
export let initialLatLng: { lat: number; lng: number } | null = null; // Used to pass the location from the map selection to the modal
let fileInput: HTMLInputElement;
let immichIntegration: boolean = false;
@ -87,7 +89,6 @@
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
modal.showModal();
console.log('open');
});
let url: string = '';
@ -142,14 +143,11 @@
const input = event.target as HTMLInputElement;
if (input.files && input.files.length) {
selectedFile = input.files[0];
console.log('Selected file:', selectedFile);
}
}
async function uploadAttachment(event: Event) {
event.preventDefault();
console.log('UPLOAD');
console.log(selectedFile);
if (!selectedFile) {
console.error('No files selected');
@ -157,23 +155,18 @@
}
const file = selectedFile;
console.log(file);
const formData = new FormData();
formData.append('file', file);
formData.append('adventure', adventure.id);
formData.append('name', attachmentName);
console.log(formData);
try {
const res = await fetch('/adventures?/attachment', {
method: 'POST',
body: formData
});
console.log(res);
if (res.ok) {
const newData = deserialize(await res.text()) as { data: Attachment };
adventure.attachments = [...adventure.attachments, newData.data];
@ -202,7 +195,6 @@
if (res.status === 204) {
images = images.filter((image) => image.id !== id);
adventure.images = images;
console.log(images);
addToast('success', $t('adventures.image_removed_success'));
} else {
addToast('error', $t('adventures.image_removed_error'));
@ -255,9 +247,7 @@
});
if (res.ok) {
let newData = deserialize(await res.text()) as { data: { id: string; image: string } };
console.log(newData);
let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false };
console.log(newImage);
images = [...images, newImage];
adventure.images = images;
addToast('success', $t('adventures.image_upload_success'));
@ -308,9 +298,7 @@
});
if (res2.ok) {
let newData = deserialize(await res2.text()) as { data: { id: string; image: string } };
console.log(newData);
let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false };
console.log(newImage);
images = [...images, newImage];
adventure.images = images;
addToast('success', $t('adventures.image_upload_success'));
@ -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) {
event.preventDefault();
triggerMarkVisted = true;
console.log(adventure);
if (adventure.id === '') {
console.log(categories);
if (adventure.category?.display_name == '') {
if (categories.some((category) => category.name === 'general')) {
adventure.category = categories.find(
@ -597,7 +566,7 @@
</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">
<input type="checkbox" />
@ -958,7 +927,7 @@
{/if}
{#if adventure.is_public && adventure.id}
<div class="bg-neutral p-4 mt-2 rounded-md shadow-sm">
<div class="bg-neutral p-4 mt-2 rounded-md shadow-sm text-neutral-content">
<p class=" font-semibold">{$t('adventures.share_adventure')}</p>
<div class="flex items-center justify-between">
<p class="text-card-foreground font-mono">

View file

@ -40,6 +40,9 @@
<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('/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">
<li><button formaction="/?/logout">{$t('navbar.logout')}</button></li>
</form>

View file

@ -2,12 +2,15 @@
import { appVersion } from '$lib/config';
import { addToast } from '$lib/toasts';
import type { Adventure, Lodging, OpenStreetMapPlace, Point, ReverseGeocode } from '$lib/types';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
export let item: Adventure | Lodging;
export let triggerMarkVisted: boolean = false;
export let initialLatLng: { lat: number; lng: number } | null = null; // Used to pass the location from the map selection to the modal
let reverseGeocodePlace: ReverseGeocode | null = null;
let markers: Point[] = [];
@ -19,6 +22,22 @@
let places: OpenStreetMapPlace[] = [];
let noPlaces: boolean = false;
onMount(() => {
if (initialLatLng) {
markers = [
{
lngLat: { lng: initialLatLng.lng, lat: initialLatLng.lat },
name: '',
location: '',
activity_type: ''
}
];
item.latitude = initialLatLng.lat;
item.longitude = initialLatLng.lng;
reverseGeocode();
}
});
$: if (markers.length > 0) {
const newLat = Math.round(markers[0].lngLat.lat * 1e6) / 1e6;
const newLng = Math.round(markers[0].lngLat.lng * 1e6) / 1e6;
@ -35,10 +54,6 @@
}
}
$: {
console.log(triggerMarkVisted);
}
$: if (triggerMarkVisted && willBeMarkedVisited) {
markVisited();
triggerMarkVisted = false;
@ -84,8 +99,6 @@
break; // Exit the loop since we've determined the result
}
}
console.log('WMBV:', willBeMarkedVisited);
}
}
@ -180,6 +193,9 @@
) {
old_display_name = reverseGeocodePlace.display_name;
item.location = reverseGeocodePlace.display_name;
if (reverseGeocodePlace.location_name) {
item.name = reverseGeocodePlace.location_name;
}
}
console.log(data);
}
@ -187,6 +203,8 @@
function clearMap() {
console.log('CLEAR');
markers = [];
item.latitude = null;
item.longitude = null;
}
</script>
@ -268,6 +286,8 @@
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full rounded-lg"
standardControls
zoom={item.latitude && item.longitude ? 12 : 1}
center={{ lng: item.longitude || 0, lat: item.latitude || 0 }}
>
<!-- MapEvents gives you access to map events even from other components inside the map,
where you might not have access to the top-level `MapLibre` component. In this case

View file

@ -6,7 +6,6 @@
import { addToast } from '$lib/toasts';
import { t } from 'svelte-i18n';
import DeleteWarning from './DeleteWarning.svelte';
// import ArrowDownThick from '~icons/mdi/arrow-down-thick';
const dispatch = createEventDispatcher();
@ -36,20 +35,6 @@
let collectionStartDate = new Date(collection.start_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
const startOutsideRange =
transportationStartDate &&
@ -88,9 +73,9 @@
{#if isWarningModalOpen}
<DeleteWarning
title={$t('adventures.delete_transportation')}
title={$t('adventures.delete_lodging')}
button_text="Delete"
description={$t('adventures.transportation_delete_confirm')}
description={$t('adventures.lodging_delete_confirm')}
is_warning={false}
on:close={() => (isWarningModalOpen = false)}
on:confirm={deleteTransportation}
@ -106,7 +91,7 @@
<h2 class="card-title text-lg font-semibold truncate">{lodging.name}</h2>
<div class="flex items-center gap-2">
<div class="badge badge-secondary">
{lodging.type}
{$t(`lodging.${lodging.type}`)}
</div>
<!-- {#if hotel.type == 'plane' && hotel.flight_number}
<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>
{/if}
<!-- Locations -->
<!-- Location -->
<div class="space-y-2">
{#if lodging.location}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.from')}:</span>
<p class="break-words">{lodging.location}</p>
<span class="font-medium text-sm">{$t('adventures.location')}:</span>
<p>{lodging.location}</p>
</div>
{/if}
{#if lodging.check_in && lodging.check_out}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.start')}:</span>
<p>{new Date(lodging.check_in).toLocaleDateString(undefined, { timeZone: 'UTC' })}</p>
</div>
{/if}
</div>
<!-- Dates -->
<div class="space-y-2">
{#if lodging.location}
<!-- <ArrowDownThick class="w-4 h-4" /> -->
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.to')}:</span>
<p class="break-words">{lodging.location}</p>
</div>
{/if}
{#if lodging.check_out}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.end')}:</span>
<p>{new Date(lodging.check_out).toLocaleDateString(undefined, { timeZone: 'UTC' })}</p>
<span class="font-medium text-sm">{$t('adventures.dates')}:</span>
<p>
{new Date(lodging.check_in).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric'
})}
-
{new Date(lodging.check_out).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric'
})}
</p>
</div>
{/if}
</div>

View file

@ -53,8 +53,8 @@
description: hotelToEdit?.description || '',
rating: hotelToEdit?.rating || NaN,
link: hotelToEdit?.link || '',
check_in: hotelToEdit?.check_in || null,
check_out: hotelToEdit?.check_out || null,
check_in: hotelToEdit?.check_in ? toLocalDatetime(hotelToEdit.check_in) : null,
check_out: hotelToEdit?.check_out ? toLocalDatetime(hotelToEdit.check_out) : null,
reservation_number: hotelToEdit?.reservation_number || '',
price: hotelToEdit?.price || null,
latitude: hotelToEdit?.latitude || null,
@ -166,6 +166,32 @@
required
/>
</div>
<div>
<label for="type">
{$t('transportation.type')}<span class="text-red-500">*</span>
</label>
<div>
<select
class="select select-bordered w-full max-w-xs"
name="type"
id="type"
bind:value={lodging.type}
>
<option disabled selected>{$t('transportation.type')}</option>
<option value="hotel">{$t('lodging.hotel')}</option>
<option value="hostel">{$t('lodging.hostel')}</option>
<option value="resort">{$t('lodging.resort')}</option>
<option value="bnb">{$t('lodging.bnb')}</option>
<option value="campground">{$t('lodging.campground')}</option>
<option value="cabin">{$t('lodging.cabin')}</option>
<option value="apartment">{$t('lodging.apartment')}</option>
<option value="house">{$t('lodging.house')}</option>
<option value="villa">{$t('lodging.villa')}</option>
<option value="motel">{$t('lodging.motel')}</option>
<option value="other">{$t('lodging.other')}</option>
</select>
</div>
</div>
<!-- Description -->
<div>
<label for="description">{$t('adventures.description')}</label><br />
@ -250,13 +276,53 @@
</div>
</div>
</div>
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
{$t('adventures.lodging_information')}
</div>
<div class="collapse-content">
<!-- Reservation Number -->
<div>
<label for="date">
{$t('lodging.reservation_number')}
</label>
<div>
<input
type="text"
id="reservation_number"
name="reservation_number"
bind:value={lodging.reservation_number}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
</div>
<!-- Price -->
<div>
<label for="price">
{$t('adventures.price')}
</label>
<div>
<input
type="number"
id="price"
name="price"
bind:value={lodging.price}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
</div>
</div>
</div>
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
{$t('adventures.date_information')}
</div>
<div class="collapse-content">
<!-- Start Date -->
<!-- Check In -->
<div>
<label for="date">
{$t('lodging.check_in')}

View file

@ -243,6 +243,20 @@
{$t('adventures.basic_information')}
</div>
<div class="collapse-content">
<!-- Name -->
<div>
<label for="name">
{$t('adventures.name')}<span class="text-red-500">*</span>
</label>
<input
type="text"
id="name"
name="name"
bind:value={transportation.name}
class="input input-bordered w-full"
required
/>
</div>
<!-- Type selection -->
<div>
<label for="type">
@ -267,20 +281,6 @@
</select>
</div>
</div>
<!-- Name -->
<div>
<label for="name">
{$t('adventures.name')}<span class="text-red-500">*</span>
</label>
<input
type="text"
id="name"
name="name"
bind:value={transportation.name}
class="input input-bordered w-full"
required
/>
</div>
<!-- Description -->
<div>
<label for="description">{$t('adventures.description')}</label><br />

View file

@ -5,6 +5,7 @@ import type {
Background,
Checklist,
Collection,
Lodging,
Note,
Transportation,
User
@ -149,6 +150,50 @@ export function groupTransportationsByDate(
return groupedTransportations;
}
export function groupLodgingByDate(
transportations: Lodging[],
startDate: Date,
numberOfDays: number
): Record<string, Lodging[]> {
const groupedTransportations: Record<string, Lodging[]> = {};
// Initialize all days in the range
for (let i = 0; i < numberOfDays; i++) {
const currentDate = new Date(startDate);
currentDate.setUTCDate(startDate.getUTCDate() + i);
const dateString = currentDate.toISOString().split('T')[0];
groupedTransportations[dateString] = [];
}
transportations.forEach((transportation) => {
if (transportation.check_in) {
const transportationDate = new Date(transportation.check_in).toISOString().split('T')[0];
if (transportation.check_out) {
const endDate = new Date(transportation.check_out).toISOString().split('T')[0];
// Loop through all days and include transportation if it falls within the range
for (let i = 0; i < numberOfDays; i++) {
const currentDate = new Date(startDate);
currentDate.setUTCDate(startDate.getUTCDate() + i);
const dateString = currentDate.toISOString().split('T')[0];
// Include the current day if it falls within the transportation date range
if (dateString >= transportationDate && dateString <= endDate) {
if (groupedTransportations[dateString]) {
groupedTransportations[dateString].push(transportation);
}
}
}
} else if (groupedTransportations[transportationDate]) {
// If there's no end date, add transportation to the start date only
groupedTransportations[transportationDate].push(transportation);
}
}
});
return groupedTransportations;
}
export function groupNotesByDate(
notes: Note[],
startDate: Date,

View file

@ -210,6 +210,7 @@ export type ReverseGeocode = {
display_name: string;
city: string;
city_id: string;
location_name: string;
};
export type Category = {

View file

@ -21,6 +21,7 @@
"support": "Support",
"calendar": "Calendar",
"theme_selection": "Theme Selection",
"admin_panel": "Admin Panel",
"themes": {
"light": "Light",
"dark": "Dark",
@ -67,9 +68,11 @@
"checklist_delete_confirm": "Are you sure you want to delete this checklist? This action cannot be undone.",
"note_delete_confirm": "Are you sure you want to delete this note? This action cannot be undone.",
"transportation_delete_confirm": "Are you sure you want to delete this transportation? This action cannot be undone.",
"lodging_delete_confirm": "Are you sure you want to delete this lodging location? This action cannot be undone.",
"delete_checklist": "Delete Checklist",
"delete_note": "Delete Note",
"delete_transportation": "Delete Transportation",
"delete_lodging": "Delete Lodging",
"open_details": "Open Details",
"edit_adventure": "Edit Adventure",
"remove_from_collection": "Remove from Collection",
@ -263,6 +266,7 @@
"attachment_update_success": "Attachment updated successfully!",
"attachment_name": "Attachment Name",
"gpx_tip": "Upload GPX files to attachments to view them on the map!",
"welcome_map_info": "Public adventures on this server",
"attachment_update_error": "Error updating attachment",
"activities": {
"general": "General 🌍",
@ -287,7 +291,9 @@
"spiritual_journeys": "Spiritual Journeys 🧘‍♀️",
"volunteer_work": "Volunteer Work 🤝",
"other": "Other"
}
},
"lodging_information": "Lodging Information",
"price": "Price"
},
"worldtravel": {
"country_list": "Country List",
@ -505,17 +511,22 @@
"check_in": "Check In",
"check_out": "Check Out",
"edit": "Edit",
"modes": {
"hotel": "Hotel",
"hostel": "Hostel",
"airbnb": "Airbnb",
"camping": "Camping",
"other": "Other"
},
"lodging_edit_success": "Lodging edited successfully!",
"edit_lodging": "Edit Lodging",
"start": "Start",
"date_and_time": "Date & Time"
"date_and_time": "Date & Time",
"hotel": "Hotel",
"hostel": "Hostel",
"resort": "Resort",
"bnb": "Bed and Breakfast",
"campground": "Campground",
"cabin": "Cabin",
"apartment": "Apartment",
"house": "House",
"villa": "Villa",
"motel": "Motel",
"other": "Other",
"reservation_number": "Reservation Number"
},
"search": {
"adventurelog_results": "AdventureLog Results",

View file

@ -11,6 +11,19 @@ const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load = (async (event) => {
if (event.locals.user) {
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;

View file

@ -1,45 +1,47 @@
<script lang="ts">
import { goto } from '$app/navigation';
import AdventureOverlook from '$lib/assets/AdventureOverlook.webp';
import MapWithPins from '$lib/assets/MapWithPins.webp';
import { t } from 'svelte-i18n';
import { 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;
</script>
<section class="flex items-center justify-center w-full py-12 md:py-24 lg:py-32">
<div class="container px-4 md:px-6">
<div class="grid gap-6 lg:grid-cols-[1fr_550px] lg:gap-12 xl:grid-cols-[1fr_650px]">
<div class="flex flex-col justify-center space-y-4">
<div class="space-y-2">
<!-- Hero Section -->
<section class="flex items-center justify-center w-full py-20 bg-gray-50 dark:bg-gray-800">
<div class="container mx-auto px-4 flex flex-col-reverse md:flex-row items-center gap-8">
<!-- Text Content -->
<div class="w-full md:w-1/2 space-y-6">
{#if data.user}
{#if data.user.first_name && data.user.first_name !== null}
<h1
class="text-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)},
{$t('home.hero_1')}
{data.user.first_name.charAt(0).toUpperCase() + data.user.first_name.slice(1)}, {$t(
'home.hero_1'
)}
</h1>
{:else}
<h1
class="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pb-4"
class="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
>
{$t('home.hero_1')}
</h1>
{/if}
{:else}
<h1
class="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pb-4"
class="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
>
{$t('home.hero_1')}
</h1>
{/if}
<p class="max-w-[600px] text-gray-500 md:text-xl dark:text-gray-400">
<p class="text-xl text-gray-600 dark:text-gray-300 max-w-xl">
{$t('home.hero_2')}
</p>
</div>
<div class="flex flex-col gap-2 min-[400px]:flex-row">
<div class="flex flex-col sm:flex-row gap-4">
{#if data.user}
<button on:click={() => goto('/adventures')} class="btn btn-primary">
{$t('home.go_to')}
@ -48,79 +50,92 @@
<button on:click={() => goto('/login')} class="btn btn-primary">
{$t('auth.login')}
</button>
<button on:click={() => goto('/signup')} class="btn btn-neutral">
<button on:click={() => goto('/signup')} class="btn btn-secondary">
{$t('auth.signup')}
</button>
{/if}
</div>
</div>
<img
src={AdventureOverlook}
width="550"
height="550"
alt="Hero"
class="mx-auto aspect-video overflow-hidden rounded-xl object-cover sm:w-full lg:order-last"
/>
<!-- Image -->
<div class="w-full md:w-1/2">
<p class="flex items-center text-neutral-content">
<InformationSlabCircleOutline class="w-4 h-4 mr-1" />
{$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>
</section>
<section
class="flex items-center justify-center w-full py-12 md:py-24 lg:py-32 bg-gray-100 dark:bg-gray-800"
>
<div class="container px-4 md:px-6">
<div class="flex flex-col items-center justify-center space-y-4 text-center">
<div class="space-y-2">
<div
class="inline-block rounded-lg bg-gray-100 px-3 py-1 text-md dark:bg-gray-800 dark:text-gray-400"
>
<!-- Features Section -->
<section id="features" class="py-16 bg-white dark:bg-gray-900">
<div class="container mx-auto px-4">
<div class="text-center mb-12">
<div class="inline-block text-neutral-content bg-neutral px-4 py-2 rounded-full">
{$t('home.key_features')}
</div>
<h2
class="text-3xl font-bold tracking-tighter sm:text-5xl bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
class="mt-4 text-3xl md:text-4xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
>
{$t('home.desc_1')}
</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"
>
<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">
<!-- svelte-ignore a11y-img-redundant-alt -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
<!-- Image for Features -->
<div class="order-1 md:order-2">
<img
src={MapWithPins}
width="550"
height="310"
alt="Image"
class="mx-auto aspect-video overflow-hidden rounded-xl object-cover object-center sm:w-full lg:order-last"
alt="World map with pins"
class="rounded-lg shadow-lg object-cover"
/>
<div class="flex flex-col justify-center space-y-4">
<ul class="grid gap-6">
<li>
<div class="grid gap-1">
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_1')}</h3>
<p class="text-gray-500 dark:text-gray-400">
</div>
<!-- Feature List -->
<div class="order-2 md:order-1">
<ul class="space-y-6">
<li class="space-y-2">
<h3 class="text-xl font-semibold dark:text-gray-300">{$t('home.feature_1')}</h3>
<p class="text-gray-600 dark:text-gray-400">
{$t('home.feature_1_desc')}
</p>
</div>
</li>
<li>
<div class="grid gap-1">
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_2')}</h3>
<p class="text-gray-500 dark:text-gray-400">
<li class="space-y-2">
<h3 class="text-xl font-semibold dark:text-gray-300">{$t('home.feature_2')}</h3>
<p class="text-gray-600 dark:text-gray-400">
{$t('home.feature_2_desc')}
</p>
</div>
</li>
<li>
<div class="grid gap-1">
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_3')}</h3>
<p class="text-gray-500 dark:text-gray-400">
<li class="space-y-2">
<h3 class="text-xl font-semibold dark:text-gray-300">{$t('home.feature_3')}</h3>
<p class="text-gray-600 dark:text-gray-400">
{$t('home.feature_3_desc')}
</p>
</div>
</li>
</ul>
</div>

View file

@ -12,7 +12,6 @@
import toGeoJSON from '@mapbox/togeojson';
import LightbulbOn from '~icons/mdi/lightbulb-on';
import Account from '~icons/mdi/account';
let geojson: any;
@ -186,7 +185,10 @@
alt={adventure.name}
/>
</a>
<div class="flex justify-center w-full py-2 gap-2">
<!-- Scrollable button container -->
<div
class="flex w-full py-2 gap-2 overflow-x-auto whitespace-nowrap scrollbar-hide justify-start"
>
{#each adventure.images as _, i}
<button
on:click={() => goToSlide(i)}
@ -198,6 +200,7 @@
{/each}
</div>
{/if}
<div class="grid gap-4">
<div class="flex items-center justify-between">
<div>
@ -222,11 +225,12 @@
</div>
</div>
<div class="grid gap-2">
{#if adventure.user}
<div class="flex items-center gap-2">
{#if adventure.user.profile_pic}
<div class="avatar">
<div class="w-8 rounded-full">
<img src={adventure.user.profile_pic} />
<img src={adventure.user.profile_pic} alt={adventure.user.username} />
</div>
</div>
{:else}
@ -242,6 +246,7 @@
</div>
</div>
{/if}
<div>
{#if adventure.user.public_profile}
<a href={`/profile/${adventure.user.username}`} class="text-base font-medium">
@ -256,6 +261,7 @@
{/if}
</div>
</div>
{/if}
<div class="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"

View file

@ -27,7 +27,8 @@
groupNotesByDate,
groupTransportationsByDate,
groupChecklistsByDate,
osmTagToEmoji
osmTagToEmoji,
groupLodgingByDate
} from '$lib';
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
import ChecklistModal from '$lib/components/ChecklistModal.svelte';
@ -861,6 +862,10 @@
new Date(collection.start_date),
numberOfDays
)[dateString] || []}
{@const dayLodging =
groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays)[
dateString
] || []}
{@const dayNotes =
groupNotesByDate(notes, new Date(collection.start_date), numberOfDays)[dateString] ||
[]}
@ -922,6 +927,18 @@
/>
{/each}
{/if}
{#if dayLodging.length > 0}
{#each dayLodging as hotel}
<LodgingCard
lodging={hotel}
user={data?.user}
on:delete={(event) => {
lodging = lodging.filter((t) => t.id != event.detail);
}}
on:edit={editLodging}
/>
{/each}
{/if}
{#if dayChecklists.length > 0}
{#each dayChecklists as checklist}
<ChecklistCard
@ -939,7 +956,7 @@
{/if}
</div>
{#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0}
{#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0 && dayLodging.length == 0}
<p class="text-center text-lg mt-2 italic">{$t('adventures.nothing_planned')}</p>
{/if}
</div>
@ -1068,7 +1085,7 @@
</div>
</div>
{/if}
{#if currentView == 'recommendations'}
{#if currentView == 'recommendations' && data.user}
<div class="card bg-base-200 shadow-xl my-8 mx-auto w-10/12">
<div class="card-body">
<h2 class="card-title text-3xl justify-center mb-4">Adventure Recommendations</h2>

View file

@ -10,6 +10,8 @@
let createModalOpen: boolean = false;
let showGeo: boolean = false;
export let initialLatLng: { lat: number; lng: number } | null = null;
let visitedRegions: VisitedRegion[] = data.props.visitedRegions;
let adventures: Adventure[] = data.props.adventures;
@ -49,6 +51,11 @@
newLatitude = e.detail.lngLat.lat;
}
function newAdventure() {
initialLatLng = { lat: newLatitude, lng: newLongitude } as { lat: number; lng: number };
createModalOpen = true;
}
function createNewAdventure(event: CustomEvent) {
adventures = [...adventures, event.detail];
newMarker = null;
@ -86,7 +93,7 @@
/>
<div class="divider divider-horizontal"></div>
{#if newMarker}
<button type="button" class="btn btn-primary mb-2" on:click={() => (createModalOpen = true)}
<button type="button" class="btn btn-primary mb-2" on:click={newAdventure}
>{$t('map.add_adventure_at_marker')}</button
>
<button type="button" class="btn btn-neutral mb-2" on:click={() => (newMarker = null)}
@ -105,14 +112,13 @@
<AdventureModal
on:close={() => (createModalOpen = false)}
on:save={createNewAdventure}
latitude={newLatitude}
longitude={newLongitude}
{initialLatLng}
/>
{/if}
<MapLibre
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full"
class="mx-auto aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12 rounded-lg"
standardControls
>
{#each filteredAdventures as adventure}

View file

@ -458,7 +458,7 @@
<!-- Social Auth Settings -->
<section class="space-y-8">
<h2 class="text-2xl font-semibold text-center mt-8">{$t('settings.social_oidc_auth')}</h2>
<div class="bg-neutral p-6 rounded-lg shadow-md text-center">
<div class="bg-neutral p-6 rounded-lg shadow-md text-center text-neutral-content">
<p>
{$t('settings.social_auth_desc')}
</p>