adds mobile drawer

This commit is contained in:
Christian Beutel
2024-02-24 16:01:09 +01:00
parent 90a683f268
commit 275874fba9
13 changed files with 215 additions and 67 deletions

View File

@@ -23,7 +23,7 @@
} }
.btn-icon { .btn-icon {
@apply rounded-full aspect-square h-7 hover:bg-secondary-hover focus:ring-4 ring-input-ring transition-colors; @apply rounded-full aspect-square h-8 text-lg hover:bg-secondary-hover focus:ring-4 ring-input-ring transition-colors;
} }
.tab { .tab {

View File

@@ -40,7 +40,7 @@
--menu-item-background-hover: 43, 46, 69; --menu-item-background-hover: 43, 46, 69;
--menu-item-background-focus: 25, 26, 41; --menu-item-background-focus: 25, 26, 41;
--footer-background: 10, 14, 48; --footer-background: 26, 29, 51;
--separator: 108, 115, 150; --separator: 108, 115, 150;

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import { browser } from "$app/environment";
import { beforeNavigate } from "$app/navigation";
import { backIn, backOut } from "svelte/easing";
import { slide } from "svelte/transition";
export let open: boolean = false;
$: if (open && browser) {
document.body.style.position = "fixed";
document.body.style.top = `-${window.scrollY}px`;
} else if (browser) {
const scrollY = document.body.style.top;
document.body.style.position = "";
document.body.style.top = "";
window.scrollTo(0, parseInt(scrollY || "0") * -1);
}
beforeNavigate(() => {
open = false;
});
</script>
{#if open}
<div
class="fixed drawer-overlay h-screen w-screen backdrop-blur-md z-50 bg-gray-500/50 cursor-pointer"
on:click={() => (open = false)}
on:keydown={(e) => {
if (e.key == "Escape") open = false;
}}
role="button"
tabindex="0"
></div>
<div
class="absolute flex flex-col overflow-y-auto w-72 h-screen bg-background right-0 top-0 z-50"
in:slide={{ axis: "x", easing: backOut }}
out:slide={{ axis: "x", easing: backIn }}
>
<slot />
</div>
{/if}

View File

@@ -2,10 +2,19 @@
export let id: string; export let id: string;
export let title: string; export let title: string;
export function openModal() { export function openModal() {
document.body.style.position = "fixed";
document.body.style.width = "100%";
document.body.style.top = `-${window.scrollY}px`;
(document.getElementById(id) as HTMLDialogElement).showModal(); (document.getElementById(id) as HTMLDialogElement).showModal();
} }
export function closeModal() { export function closeModal() {
const scrollY = document.body.style.top;
document.body.style.position = "";
document.body.style.top = "";
window.scrollTo(0, parseInt(scrollY || "0") * -1);
(document.getElementById(id) as HTMLDialogElement).close(); (document.getElementById(id) as HTMLDialogElement).close();
} }
</script> </script>
@@ -40,13 +49,14 @@
<slot name="content" /> <slot name="content" />
</div> </div>
<!-- Modal footer --> <!-- Modal footer -->
<div <div class="p-4 md:p-5 border-t border-separator rounded-b">
class="p-4 md:p-5 border-t border-separator rounded-b"
>
<slot name="footer" {closeModal} /> <slot name="footer" {closeModal} />
</div> </div>
</div> </div>
</dialog> </dialog>
<style> <style>
dialog::backdrop {
@apply bg-gray-500/50;
}
</style> </style>

View File

@@ -6,7 +6,7 @@
</script> </script>
<div <div
class="category-card relative rounded-2xl shadow-md w-48 h-48 overflow-hidden cursor-pointer" class="category-card relative rounded-2xl shadow-md max-h-48 aspect-video overflow-hidden cursor-pointer"
> >
<img <img
class="w-full h-full" class="w-full h-full"

View File

@@ -7,6 +7,7 @@
import { tweened } from "svelte/motion"; import { tweened } from "svelte/motion";
import Dropdown from "./base/dropdown.svelte"; import Dropdown from "./base/dropdown.svelte";
import LogoTextLight from "./logo/logo_text_light.svelte"; import LogoTextLight from "./logo/logo_text_light.svelte";
import Drawer from "./base/drawer.svelte";
const navBarItems = [ const navBarItems = [
{ text: "Home", value: "/" }, { text: "Home", value: "/" },
@@ -35,6 +36,8 @@
easing: backInOut, easing: backInOut,
}); });
let drawerOpen: boolean = false;
afterNavigate((e) => { afterNavigate((e) => {
const routeId = e.to?.route.id; const routeId = e.to?.route.id;
const navBarLinks = document.getElementById("nav-bar-links"); const navBarLinks = document.getElementById("nav-bar-links");
@@ -76,18 +79,63 @@
} }
</script> </script>
<Drawer bind:open={drawerOpen}>
<div class="flex gap-4 items-center m-4">
<div class="basis-full"></div>
<button
class="btn-icon fa-regular fa-{$theme === 'light' ? 'sun' : 'moon'}"
on:click={() => toggleTheme()}
></button>
<button
class="btn-icon block fa fa-close float-right"
on:click={() => (drawerOpen = false)}
></button>
</div>
<div class="flex flex-col px-12 gap-8">
{#each navBarItems as item}
<a
class="font-semibold text-xl"
href={item.value}
data-sveltekit-preload-data="off">{item.text}</a
>
{/each}
</div>
<hr class="my-6 border-input-border" />
<div class="flex flex-col basis-full gap-y-3">
<a class="btn-primary btn-large text-center mx-4" href="/trail/edit/new"
><i class="fa fa-plus mr-2"></i>New Trail</a
>
{#if $currentUser}
<div class="basis-full"></div>
<hr class="border-input-border" />
<div class="flex gap-4 items-center mx-4">
<img
class="rounded-full w-8 aspect-square"
src="https://api.dicebear.com/7.x/initials/svg?seed={$currentUser.username}&backgroundType=gradientLinear"
alt=""
/>
<div>
<p class="text-sm">{$currentUser.username}</p>
<p class="text-sm text-gray-500">{$currentUser.email}</p>
</div>
<button class="btn-icon tooltip" data-title="Logout"
><i class="fa-solid fa-arrow-right-from-bracket"
></i></button
>
</div>
{/if}
</div>
</Drawer>
<nav class="flex justify-between items-center p-6"> <nav class="flex justify-between items-center p-6">
<a href="/" data-sveltekit-preload-data="tap"> <a href="/" data-sveltekit-preload-data="off">
{#if $theme == "light"} {#if $theme == "light"}
<LogoText></LogoText> <LogoText></LogoText>
{:else} {:else}
<LogoTextLight></LogoTextLight> <LogoTextLight></LogoTextLight>
{/if} {/if}
</a> </a>
<menu <menu id="nav-bar-links" class="hidden md:flex gap-8 relative py-1 px-2">
id="nav-bar-links"
class="hidden md:flex gap-8 relative py-1 px-2 "
>
<div <div
class="absolute h-full w-16 bg-menu-item-background-hover rounded-xl top-0 z-0" class="absolute h-full w-16 bg-menu-item-background-hover rounded-xl top-0 z-0"
style="width: {$indicatorWidth}px; left: {$indicatorPosition}px; scale: {$indicatorScale}" style="width: {$indicatorWidth}px; left: {$indicatorPosition}px; scale: {$indicatorScale}"
@@ -96,7 +144,7 @@
<a <a
class="font-semibold z-10" class="font-semibold z-10"
href={item.value} href={item.value}
data-sveltekit-preload-data="tap">{item.text}</a data-sveltekit-preload-data="off">{item.text}</a
> >
{/each} {/each}
</menu> </menu>
@@ -133,4 +181,8 @@
<a class="btn-primary btn-large" href="/login">Login</a> <a class="btn-primary btn-large" href="/login">Login</a>
</div> </div>
{/if} {/if}
<button
class="btn-icon fa fa-bars md:hidden"
on:click={() => (drawerOpen = !drawerOpen)}
></button>
</nav> </nav>

View File

@@ -134,6 +134,7 @@
<input <input
id="{category.name}-checkbox" id="{category.name}-checkbox"
type="checkbox" type="checkbox"
checked={filter.category.includes(category.id)}
value={category.id} value={category.id}
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2" class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
on:change={() => setCategoryFilter(category)} on:change={() => setCategoryFilter(category)}

View File

@@ -7,9 +7,11 @@
import "../css/app.css"; import "../css/app.css";
import "../css/components.css"; import "../css/components.css";
import "../css/theme.css"; import "../css/theme.css";
</script> </script>
<svelte:head>
<title> | wanderer</title>
</svelte:head>
<div class="theme {$theme}"> <div class="theme {$theme}">
<NavBar></NavBar> <NavBar></NavBar>
<Toast></Toast> <Toast></Toast>

View File

@@ -64,7 +64,7 @@
style="height: calc(100vh - 112px)" style="height: calc(100vh - 112px)"
> >
<div <div
class="flex flex-col justify-center gap-8 max-w-md mx-8 md:mx-auto -mt-24" class="flex flex-col justify-center gap-8 max-w-md mx-8 md:mx-auto lg:-mt-24"
> >
<h2 class="text-7xl font-bold"> <h2 class="text-7xl font-bold">
Welcome to <span class="-tracking-[0.075em]">wanderer</span> Welcome to <span class="-tracking-[0.075em]">wanderer</span>
@@ -90,7 +90,10 @@
<section <section
class="max-w-7xl mx-auto mt-8 px-8 xl:px-0 grid grid-cols-1 md:grid-cols-2 items-center" class="max-w-7xl mx-auto mt-8 px-8 xl:px-0 grid grid-cols-1 md:grid-cols-2 items-center"
> >
<div id="trails" class="flex flex-wrap justify-items-center gap-8 py-8 order-1 md:order-none"> <div
id="trails"
class="flex flex-wrap justify-items-center gap-8 py-8 order-1 md:order-none"
>
{#each $trails as trail} {#each $trails as trail}
<a href="/trail/view/{trail.id}"> <a href="/trail/view/{trail.id}">
<TrailCard {trail}></TrailCard></a <TrailCard {trail}></TrailCard></a
@@ -108,7 +111,7 @@
<a <a
class="inline-block btn-primary btn-large" class="inline-block btn-primary btn-large"
href="/trails" href="/trails"
role="button">All Trails</a role="button">Explore</a
> >
</div> </div>
</section> </section>
@@ -118,12 +121,18 @@
<div class="max-w-md md:mx-auto space-y-8"> <div class="max-w-md md:mx-auto space-y-8">
<h2 class="text-4xl md:text-5xl font-bold">Categories</h2> <h2 class="text-4xl md:text-5xl font-bold">Categories</h2>
<h5> <h5>
Did you know? You cannot only save you hiking trails. There are many categories for all your adventures. Did you know? You cannot only save you hiking trails. There are many
categories for all your adventures.
</h5> </h5>
</div> </div>
<div id="categories" class="flex flex-wrap justify-items-center gap-8 py-8"> <div
id="categories"
class="grid grid-cols-1 lg:grid-cols-2 justify-items-center gap-8 py-8"
>
{#each $categories as category} {#each $categories as category}
<CategoryCard {category}></CategoryCard> <a href="/trails?category={category.id}">
<CategoryCard {category}></CategoryCard>
</a>
{/each} {/each}
</div> </div>
</section> </section>

View File

@@ -29,6 +29,7 @@
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte"; import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
import { categories } from "$lib/stores/category_store"; import { categories } from "$lib/stores/category_store";
import { slide } from "svelte/transition"; import { slide } from "svelte/transition";
import { browser } from "$app/environment";
let L: any; let L: any;
let map: Map; let map: Map;
@@ -38,6 +39,7 @@
let searchDropdownItems: SearchItem[] = []; let searchDropdownItems: SearchItem[] = [];
let showFilter: boolean = false; let showFilter: boolean = false;
let showMap: boolean = true;
let filter: TrailFilter; let filter: TrailFilter;
@@ -232,11 +234,11 @@
<main class="grid grid-cols-1 md:grid-cols-[400px_1fr]"> <main class="grid grid-cols-1 md:grid-cols-[400px_1fr]">
<div <div
class="overflow-y-auto overflow-x-hidden flex flex-col items-stretch gap-4 px-8" id="trail-list"
style="height: calc(100vh - 124px)" class="md:overflow-y-auto md:overflow-x-hidden flex flex-col items-stretch gap-4 px-8"
> >
<div class="sticky top-0 z-10 bg-background pb-4 space-y-4"> <div class="sticky top-0 z-10 bg-background pb-4 space-y-4">
<div class="flex items-center gap-4"> <div class="flex items-center gap-2 md:gap-4">
<Search <Search
extraClasses="w-full" extraClasses="w-full"
on:update={(e) => search(e.detail)} on:update={(e) => search(e.detail)}
@@ -249,6 +251,11 @@
on:click={() => (showFilter = !showFilter)} on:click={() => (showFilter = !showFilter)}
><i class="fa fa-sliders"></i></button ><i class="fa fa-sliders"></i></button
> >
<button
class="btn-icon md:hidden"
on:click={() => (showMap = !showMap)}
><i class="fa-regular fa-{showMap ? 'rectangle-list' : 'map'}"></i></button
>
</div> </div>
{#if showFilter} {#if showFilter}
<div in:slide out:slide> <div in:slide out:slide>
@@ -263,27 +270,34 @@
{/if} {/if}
</div> </div>
{#if $trails.length == 0} {#if !showMap || (browser && window.innerWidth >= 768)}
<EmptyStateSearch></EmptyStateSearch> {#if $trails.length == 0}
<EmptyStateSearch></EmptyStateSearch>
{/if}
{#each $trails as trail}
<a href="/trail/view/{trail.id}">
<TrailCard
{trail}
on:mouseenter={() => handleTrailCardMouseEnter(trail)}
on:mouseleave={() => handleTrailCardMouseLeave(trail)}
></TrailCard>
</a>
{/each}
{/if} {/if}
{#each $trails as trail}
<a href="/trail/view/{trail.id}">
<TrailCard
{trail}
on:mouseenter={() => handleTrailCardMouseEnter(trail)}
on:mouseleave={() => handleTrailCardMouseLeave(trail)}
></TrailCard>
</a>
{/each}
</div> </div>
<div class="rounded-xl" id="map"></div> <div id="map" class="rounded-xl z-0" class:hidden={!showMap && browser && window.innerWidth < 768}></div>
</main> </main>
<style> <style>
#map { #map {
height: calc(100vh - 124px); height: calc(100vh - 180px);
}
@media only screen and (min-width: 768px) {
#map,
#trail-list {
height: calc(100vh - 124px);
}
} }
:global(.leaflet-popup-content) { :global(.leaflet-popup-content) {
width: max-content !important; width: max-content !important;
max-width: 100%; max-width: 100%;

View File

@@ -81,7 +81,10 @@
) as HTMLFormElement; ) as HTMLFormElement;
const formData = new FormData(htmlForm); const formData = new FormData(htmlForm);
if (!submittedTrail.id) { if (!submittedTrail.id) {
const createdTrail = await trails_create(submittedTrail, formData); const createdTrail = await trails_create(
submittedTrail,
formData,
);
$form.id = createdTrail.id; $form.id = createdTrail.id;
} else { } else {
await trails_update($trail, submittedTrail, formData); await trails_update($trail, submittedTrail, formData);
@@ -194,11 +197,13 @@
reader.onload = async function (e) { reader.onload = async function (e) {
await addGPXLayer(e.target?.result as string); await addGPXLayer(e.target?.result as string);
const closestCity = (await ms.index("cities500").search("", { const closestCity = (
filter: [`_geoRadius(${$form.lat}, ${$form.lon}, 10000)`], await ms.index("cities500").search("", {
sort: [`_geoPoint(${$form.lat}, ${$form.lon}):asc`], filter: [`_geoRadius(${$form.lat}, ${$form.lon}, 10000)`],
limit: 1, sort: [`_geoPoint(${$form.lat}, ${$form.lon}):asc`],
})).hits[0]; limit: 1,
})
).hits[0];
$form.location = closestCity.name; $form.location = closestCity.name;
}; };
@@ -339,10 +344,10 @@
} }
</script> </script>
<main class="grid grid-cols-[400px_1fr]"> <main class="grid grid-cols-1 md:grid-cols-[400px_1fr]">
<form <form
id="trail-form" id="trail-form"
class="overflow-y-auto overflow-x-hidden flex flex-col gap-4 px-8" class="overflow-y-auto overflow-x-hidden flex flex-col gap-4 px-8 order-1 md:order-none mt-8 md:mt-0"
on:submit={handleSubmit} on:submit={handleSubmit}
> >
<h3 class="text-xl font-semibold">Pick a trail</h3> <h3 class="text-xl font-semibold">Pick a trail</h3>
@@ -412,7 +417,7 @@
></Select> ></Select>
{/if} {/if}
<Toggle name="public" label="Public" bind:value={$form.public}></Toggle> <Toggle name="public" label="Public" bind:value={$form.public}></Toggle>
<hr class="border-separator"/> <hr class="border-separator" />
<h3 class="text-xl font-semibold">Waypoints</h3> <h3 class="text-xl font-semibold">Waypoints</h3>
<ul> <ul>
{#each $form.expand.waypoints ?? [] as waypoint, i} {#each $form.expand.waypoints ?? [] as waypoint, i}
@@ -432,7 +437,7 @@
on:click={beforeWaypointModalOpen} on:click={beforeWaypointModalOpen}
><i class="fa fa-plus mr-2"></i>Add Waypoint</button ><i class="fa fa-plus mr-2"></i>Add Waypoint</button
> >
<hr class="border-separator"/> <hr class="border-separator" />
<h3 class="text-xl font-semibold">Photos</h3> <h3 class="text-xl font-semibold">Photos</h3>
<div class="flex gap-4 max-w-full overflow-x-auto shrink-0"> <div class="flex gap-4 max-w-full overflow-x-auto shrink-0">
<button <button
@@ -459,7 +464,7 @@
</div> </div>
{/each} {/each}
</div> </div>
<hr class="border-separator"/> <hr class="border-separator" />
<h3 class="text-xl font-semibold">Summit Book</h3> <h3 class="text-xl font-semibold">Summit Book</h3>
<ul> <ul>
{#each $form.expand.summit_logs ?? [] as log, i} {#each $form.expand.summit_logs ?? [] as log, i}
@@ -478,7 +483,7 @@
on:click={beforeSummitLogModalOpen} on:click={beforeSummitLogModalOpen}
><i class="fa fa-plus mr-2"></i>Add Entry</button ><i class="fa fa-plus mr-2"></i>Add Entry</button
> >
<hr class="border-separator"/> <hr class="border-separator" />
<Button <Button
primary={true} primary={true}
large={true} large={true}
@@ -495,8 +500,13 @@
></SummitLogModal> ></SummitLogModal>
<style> <style>
#map, #map {
form { height: calc(400px);
height: calc(100vh - 124px); }
@media only screen and (min-width: 768px) {
#map,
form {
height: calc(100vh - 124px);
}
} }
</style> </style>

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { page } from "$app/stores";
import Select, { import Select, {
type SelectItem, type SelectItem,
} from "$lib/components/base/select.svelte"; } from "$lib/components/base/select.svelte";
@@ -26,19 +27,7 @@
let filterExpanded: boolean = true; let filterExpanded: boolean = true;
export const filter: TrailFilter = { const filter: TrailFilter = $page.data.filter;
q: "",
category: [],
near: {
radius: 2000,
},
distanceMin: 0,
distanceMax: 20000,
elevationGainMin: 0,
elevationGainMax: 4000,
sort: "created",
sortOrder: "+",
};
onMount(() => { onMount(() => {
const storedDisplayOption = localStorage.getItem("displayOption"); const storedDisplayOption = localStorage.getItem("displayOption");

View File

@@ -1,8 +1,28 @@
import type { TrailFilter } from "$lib/models/trail";
import { categories_index } from "$lib/stores/category_store"; import { categories_index } from "$lib/stores/category_store";
import { trails_index } from "$lib/stores/trail_store"; import { trails_search_filter } from "$lib/stores/trail_store";
import type { ServerLoad } from "@sveltejs/kit"; import type { ServerLoad } from "@sveltejs/kit";
export const load: ServerLoad = async ({ params, locals }) => { export const load: ServerLoad = async ({ params, locals, url }) => {
await trails_index() const filter: TrailFilter = {
q: "",
category: [],
near: {
radius: 2000,
},
distanceMin: 0,
distanceMax: 20000,
elevationGainMin: 0,
elevationGainMax: 4000,
sort: "created",
sortOrder: "+",
};
const paramCategory = url.searchParams.get("category");
if (paramCategory) {
filter.category.push(paramCategory);
}
await trails_search_filter(filter);
await categories_index() await categories_index()
return {filter};
}; };