mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-05 05:05:17 +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
|
@ -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;
|
||||
|
||||
|
|
|
@ -1,126 +1,141 @@
|
|||
<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">
|
||||
{#if data.user}
|
||||
{#if data.user.first_name && data.user.first_name !== null}
|
||||
<h1
|
||||
class="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pb-4"
|
||||
>
|
||||
{data.user.first_name.charAt(0).toUpperCase() + data.user.first_name.slice(1)},
|
||||
{$t('home.hero_1')}
|
||||
</h1>
|
||||
{:else}
|
||||
<h1
|
||||
class="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pb-4"
|
||||
>
|
||||
{$t('home.hero_1')}
|
||||
</h1>
|
||||
{/if}
|
||||
{:else}
|
||||
<h1
|
||||
class="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pb-4"
|
||||
>
|
||||
{$t('home.hero_1')}
|
||||
</h1>
|
||||
{/if}
|
||||
<p class="max-w-[600px] text-gray-500 md:text-xl dark:text-gray-400">
|
||||
{$t('home.hero_2')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 min-[400px]:flex-row">
|
||||
{#if data.user}
|
||||
<button on:click={() => goto('/adventures')} class="btn btn-primary">
|
||||
{$t('home.go_to')}
|
||||
</button>
|
||||
{:else}
|
||||
<button on:click={() => goto('/login')} class="btn btn-primary">
|
||||
{$t('auth.login')}
|
||||
</button>
|
||||
<button on:click={() => goto('/signup')} class="btn btn-neutral">
|
||||
{$t('auth.signup')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Hero Section -->
|
||||
<section class="flex items-center justify-center w-full py-20 bg-gray-50 dark:bg-gray-800">
|
||||
<div class="container mx-auto px-4 flex flex-col-reverse md:flex-row items-center gap-8">
|
||||
<!-- Text Content -->
|
||||
<div class="w-full md:w-1/2 space-y-6">
|
||||
{#if data.user}
|
||||
{#if data.user.first_name && data.user.first_name !== null}
|
||||
<h1
|
||||
class="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||
>
|
||||
{data.user.first_name.charAt(0).toUpperCase() + data.user.first_name.slice(1)}, {$t(
|
||||
'home.hero_1'
|
||||
)}
|
||||
</h1>
|
||||
{:else}
|
||||
<h1
|
||||
class="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||
>
|
||||
{$t('home.hero_1')}
|
||||
</h1>
|
||||
{/if}
|
||||
{:else}
|
||||
<h1
|
||||
class="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||
>
|
||||
{$t('home.hero_1')}
|
||||
</h1>
|
||||
{/if}
|
||||
<p class="text-xl text-gray-600 dark:text-gray-300 max-w-xl">
|
||||
{$t('home.hero_2')}
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
{#if data.user}
|
||||
<button on:click={() => goto('/adventures')} class="btn btn-primary">
|
||||
{$t('home.go_to')}
|
||||
</button>
|
||||
{:else}
|
||||
<button on:click={() => goto('/login')} class="btn btn-primary">
|
||||
{$t('auth.login')}
|
||||
</button>
|
||||
<button on:click={() => goto('/signup')} class="btn btn-secondary">
|
||||
{$t('auth.signup')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
<!-- 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"
|
||||
>
|
||||
{$t('home.key_features')}
|
||||
</div>
|
||||
<h2
|
||||
class="text-3xl font-bold tracking-tighter sm:text-5xl bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||
>
|
||||
{$t('home.desc_1')}
|
||||
</h2>
|
||||
<p
|
||||
class="max-w-[900px] text-gray-500 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed dark:text-gray-400"
|
||||
>
|
||||
{$t('home.desc_2')}
|
||||
</p>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section id="features" class="py-16 bg-white dark:bg-gray-900">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="text-center mb-12">
|
||||
<div class="inline-block text-neutral-content bg-neutral px-4 py-2 rounded-full">
|
||||
{$t('home.key_features')}
|
||||
</div>
|
||||
<h2
|
||||
class="mt-4 text-3xl md:text-4xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||
>
|
||||
{$t('home.desc_1')}
|
||||
</h2>
|
||||
<p class="mt-4 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto text-lg">
|
||||
{$t('home.desc_2')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mx-auto grid max-w-5xl items-center gap-6 py-12 lg:grid-cols-2 lg:gap-12">
|
||||
<!-- svelte-ignore a11y-img-redundant-alt -->
|
||||
<img
|
||||
src={MapWithPins}
|
||||
width="550"
|
||||
height="310"
|
||||
alt="Image"
|
||||
class="mx-auto aspect-video overflow-hidden rounded-xl object-cover object-center sm:w-full lg:order-last"
|
||||
/>
|
||||
<div class="flex flex-col justify-center space-y-4">
|
||||
<ul class="grid gap-6">
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_1')}</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
{$t('home.feature_1_desc')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
||||
<!-- Image for Features -->
|
||||
<div class="order-1 md:order-2">
|
||||
<img
|
||||
src={MapWithPins}
|
||||
alt="World map with pins"
|
||||
class="rounded-lg shadow-lg object-cover"
|
||||
/>
|
||||
</div>
|
||||
<!-- Feature List -->
|
||||
<div class="order-2 md:order-1">
|
||||
<ul class="space-y-6">
|
||||
<li class="space-y-2">
|
||||
<h3 class="text-xl font-semibold dark:text-gray-300">{$t('home.feature_1')}</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
{$t('home.feature_1_desc')}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_2')}</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
{$t('home.feature_2_desc')}
|
||||
</p>
|
||||
</div>
|
||||
<li class="space-y-2">
|
||||
<h3 class="text-xl font-semibold dark:text-gray-300">{$t('home.feature_2')}</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
{$t('home.feature_2_desc')}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<div class="grid gap-1">
|
||||
<h3 class="text-xl font-bold dark:text-gray-400">{$t('home.feature_3')}</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
{$t('home.feature_3_desc')}
|
||||
</p>
|
||||
</div>
|
||||
<li class="space-y-2">
|
||||
<h3 class="text-xl font-semibold dark:text-gray-300">{$t('home.feature_3')}</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
{$t('home.feature_3_desc')}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -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,40 +225,43 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<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} />
|
||||
{#if adventure.user}
|
||||
<div class="flex items-center gap-2">
|
||||
{#if adventure.user.profile_pic}
|
||||
<div class="avatar">
|
||||
<div class="w-8 rounded-full">
|
||||
<img src={adventure.user.profile_pic} alt={adventure.user.username} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-neutral text-neutral-content w-8 rounded-full">
|
||||
<span class="text-lg"
|
||||
>{adventure.user.first_name
|
||||
? adventure.user.first_name.charAt(0)
|
||||
: adventure.user.username.charAt(0)}{adventure.user.last_name
|
||||
? adventure.user.last_name.charAt(0)
|
||||
: ''}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if adventure.user.public_profile}
|
||||
<a href={`/profile/${adventure.user.username}`} class="text-base font-medium">
|
||||
{adventure.user.first_name || adventure.user.username}{' '}
|
||||
{adventure.user.last_name}
|
||||
</a>
|
||||
{:else}
|
||||
<span class="text-base font-medium">
|
||||
{adventure.user.first_name || adventure.user.username}{' '}
|
||||
{adventure.user.last_name}
|
||||
</span>
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-neutral text-neutral-content w-8 rounded-full">
|
||||
<span class="text-lg"
|
||||
>{adventure.user.first_name
|
||||
? adventure.user.first_name.charAt(0)
|
||||
: adventure.user.username.charAt(0)}{adventure.user.last_name
|
||||
? adventure.user.last_name.charAt(0)
|
||||
: ''}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
{#if adventure.user.public_profile}
|
||||
<a href={`/profile/${adventure.user.username}`} class="text-base font-medium">
|
||||
{adventure.user.first_name || adventure.user.username}{' '}
|
||||
{adventure.user.last_name}
|
||||
</a>
|
||||
{:else}
|
||||
<span class="text-base font-medium">
|
||||
{adventure.user.first_name || adventure.user.username}{' '}
|
||||
{adventure.user.last_name}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue