feat(#35): add new mobile menu

This commit is contained in:
OllyNicholass
2024-10-24 00:28:28 +01:00
parent 6ed1b25583
commit 3a5e961d54
6 changed files with 264 additions and 201 deletions
+6 -1
View File
@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { cubicInOut } from 'svelte/easing';
import { slide } from 'svelte/transition';
export let cookiesAccepted; export let cookiesAccepted;
let showBanner = false; let showBanner = false;
@@ -18,7 +20,10 @@
</script> </script>
{#if showBanner} {#if showBanner}
<div class="fixed bottom-0 left-0 w-full bg-gray-800 text-white p-4"> <div
class="fixed bottom-0 left-0 w-full bg-gray-800 text-white p-4"
transition:slide={{ duration: 200, easing: cubicInOut }}
>
<p class="text-sm"> <p class="text-sm">
We use cookies to ensure you get the best experience on our website. By continuing, you agree We use cookies to ensure you get the best experience on our website. By continuing, you agree
to our use of cookies. to our use of cookies.
+39 -31
View File
@@ -1,43 +1,51 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { cubicInOut } from 'svelte/easing';
import { slide } from 'svelte/transition';
export let indicator: boolean = false; export let indicator: boolean = false;
export let posRight: boolean = false; export let posRight: boolean = false;
let isOpen = false; let isOpen = false;
let dropdown: HTMLDivElement; let dropdown: HTMLDivElement;
const DropdownIcon = { const DropdownIcon = {
closed: '&#x25BC;', closed: '&#x25BC;',
open: '&#x25B2;' open: '&#x25B2;'
}; };
onMount(() => { onMount(() => {
const handleClick = (event: MouseEvent) => { const handleClick = (event: MouseEvent) => {
if (!isOpen) return; if (!isOpen) return;
if (!dropdown.contains(event.target as Node) || event.target instanceof HTMLAnchorElement) { if (!dropdown.contains(event.target as Node) || event.target instanceof HTMLAnchorElement) {
isOpen = false; isOpen = false;
} }
}; };
document.addEventListener('click', handleClick); document.addEventListener('click', handleClick);
return () => { return () => {
document.removeEventListener('click', handleClick); document.removeEventListener('click', handleClick);
}; };
}); });
</script> </script>
<div bind:this={dropdown} class="relative"> <div bind:this={dropdown} class="relative block">
<div class="dropdown-title"> <div class="dropdown-title block w-full">
<button class="flex" on:click={() => isOpen = !isOpen}> <button class="flex w-full" on:click={() => (isOpen = !isOpen)}>
{#if indicator} {#if indicator}
<span class="mr-1">{@html isOpen ? DropdownIcon.open : DropdownIcon.closed }</span> <span class="mr-1">{@html isOpen ? DropdownIcon.open : DropdownIcon.closed}</span>
{/if} {/if}
<slot name="dropdown"></slot> <slot name="dropdown"></slot>
</button> </button>
</div> </div>
<ul class:flex={isOpen} class:hidden={!isOpen} class:right-0={posRight} class="absolute rounded-box min-w-52 shadow z-[1] menu p-2 bg-base-100 text-base-content"> {#if isOpen}
<slot /> <ul
</ul> class:right-0={posRight}
class="absolute rounded-box min-w-52 shadow z-[1] m-0 menu p-2 bg-base-100 text-base-content block"
transition:slide={{ duration: 200, easing: cubicInOut }}
>
<slot />
</ul>
{/if}
</div> </div>
+124 -98
View File
@@ -1,110 +1,136 @@
<script lang="ts"> <script lang="ts">
import type { Session, SupabaseClient } from "@supabase/supabase-js"; import type { Session, SupabaseClient } from '@supabase/supabase-js';
import MagicLink from "./MagicLink.svelte"; import MagicLink from './MagicLink.svelte';
import NavLinks from "./NavLinks.svelte"; import NavLinks from './NavLinks.svelte';
import SignOut from "./SignOut.svelte"; import SignOut from './SignOut.svelte';
import Dropdown from "./Dropdown.svelte"; import Dropdown from './Dropdown.svelte';
import { basket, type Basket } from '$lib/stores/basket.js'; import { basket, type Basket } from '$lib/stores/basket.js';
import { onDestroy } from "svelte"; import { onDestroy } from 'svelte';
export let session: Session; export let session: Session | null;
export let supabase: SupabaseClient<any, "public", any>; export let supabase: SupabaseClient<any, 'public', any>;
let isMobileMenuOpen = false; let localBasket: Basket;
function toggleMobileMenu() {
isMobileMenuOpen = !isMobileMenuOpen;
}
let localBasket: Basket;
const unsubscribeToBasket = basket.subscribe((value) => { const unsubscribeToBasket = basket.subscribe((value) => {
localBasket = value; localBasket = value;
}); });
onDestroy(() => { onDestroy(() => {
unsubscribeToBasket(); unsubscribeToBasket();
}); });
</script> </script>
<nav class="bg-primary text-primary-content p-4"> <nav class="bg-primary text-primary-content p-4">
<div class="container mx-auto flex justify-between items-center"> <div class="container mx-auto flex justify-between items-center">
<!-- Hamburger Menu Button (Mobile) --> <!-- Hamburger Menu Button (Mobile) -->
<button on:click={toggleMobileMenu} class="block md:hidden text-white focus:outline-none"> <div class="lg:hidden mr-4">
<svg <Dropdown>
class="w-6 h-6" <div slot="dropdown">
fill="none" <button class="block text-white focus:outline-none">
stroke="currentColor" <svg
viewBox="0 0 24 24" class="w-6 h-6"
xmlns="http://www.w3.org/2000/svg" fill="none"
> stroke="currentColor"
<path viewBox="0 0 24 24"
stroke-linecap="round" xmlns="http://www.w3.org/2000/svg"
stroke-linejoin="round" >
stroke-width="2" <path
d="M4 6h16M4 12h8m-8 6h16" stroke-linecap="round"
></path> stroke-linejoin="round"
</svg> stroke-width="2"
</button> d="M4 6h16M4 12h8m-8 6h16"
></path>
<a href="/" class="text-white font-bold text-xl">SvelteKit SaaS Boilerplate</a> </svg>
<NavLinks /> </button>
</div>
<!-- Right Side: Basket & User Account --> <!-- Dropdown content -->
<div class="flex items-center space-x-4"> <NavLinks isMobile />
<!-- Basket/Checkout Icon --> </Dropdown>
<a href="/checkout" class="btn btn-ghost btn-circle"> </div>
<div class="indicator"> <a href="/" class="text-white font-bold text-xl">SvelteKit SaaS Boilerplate</a>
<svg <div class="hidden lg:block">
xmlns="http://www.w3.org/2000/svg" <NavLinks />
class="h-5 w-5" </div>
fill="none" <!-- Right Side: Basket & User Account -->
viewBox="0 0 24 24" <div class="flex items-center space-x-4">
stroke="currentColor" <!-- Basket/Checkout Icon -->
> <a href="/checkout" class="btn btn-ghost btn-circle hidden lg:flex">
<path <div class="indicator">
stroke-linecap="round" <svg
stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"
stroke-width="2" class="h-5 w-5"
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" fill="none"
/> viewBox="0 0 24 24"
</svg> stroke="currentColor"
{#if localBasket.items.length > 0} >
<span class="badge badge-sm indicator-item">{localBasket.items.length}</span> <path
{/if} stroke-linecap="round"
</div> stroke-linejoin="round"
</a> stroke-width="2"
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
<!-- User Account Dropdown --> />
<Dropdown posRight> </svg>
<div slot="dropdown"> {#if localBasket.items.length > 0}
<div class="btn btn-ghost btn-circle avatar placeholder"> <span class="badge badge-sm indicator-item">{localBasket.items.length}</span>
<div class="w-10 rounded-full"> {/if}
{#if session?.user} </div>
<div class="user-circle text-primary-content border-primary-content"> </a>
<span class="text-xl">{session?.user.email ? session?.user.email[0].toUpperCase() : '?'}</span> <!-- User Account Dropdown -->
</div> <Dropdown posRight>
{:else} <div slot="dropdown">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <div class="btn btn-ghost btn-circle avatar placeholder">
<path <div class="w-10 rounded-full">
fill-rule="evenodd" {#if session?.user}
d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" <div class="user-circle text-primary-content border-primary-content">
clip-rule="evenodd" <span class="text-xl"
/> >{session?.user.email ? session?.user.email[0].toUpperCase() : '?'}</span
</svg> >
{/if} </div>
</div> {:else}
</div> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
</div> <path
fill-rule="evenodd"
{#if session?.user} d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"
<li><a href="/account">Account</a></li> clip-rule="evenodd"
<li><a href="/settings">Settings</a></li> />
<li><SignOut {supabase} /></li> </svg>
{:else} {/if}
<MagicLink {supabase} /> </div>
{/if} </div>
</Dropdown> </div>
</div> <!-- Dropdown content -->
</div> {#if session?.user}
<li class="lg:hidden">
<a href="/checkout">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
Basket
{#if localBasket.items.length > 0}
<span class="badge badge-md indicator-item bg-primary text-primary-content"
>{localBasket.items.length}</span
>
{/if}
</a>
</li>
<li><a href="/account">Account</a></li>
<li><a href="/settings">Settings</a></li>
<li><SignOut {supabase} /></li>
{:else}
<MagicLink {supabase} />
{/if}
</Dropdown>
</div>
</div>
</nav> </nav>
+34 -9
View File
@@ -1,14 +1,39 @@
<script lang="ts"> <script lang="ts">
import { type NavLinkItem } from '$lib/types/Nav/NavLinkItem'; import { type NavLinkItem } from '$lib/types/Nav/NavLinkItem';
import { cubicInOut } from 'svelte/easing';
import Dropdown from './Dropdown.svelte'; import Dropdown from './Dropdown.svelte';
import { slide } from 'svelte/transition';
export let text: string; export let text: string;
export let children: NavLinkItem[] | undefined; export let children: NavLinkItem[] | undefined;
export let isMobile: boolean;
let isOpen = false;
</script> </script>
<Dropdown indicator> {#if !isMobile}
<div slot="dropdown">{text}</div> <Dropdown indicator>
{#each children ?? [] as child} <div slot="dropdown">{text}</div>
<li><a href={child.href}>{child.text}</a></li> {#each children ?? [] as child}
{/each} <li><a href={child.href}>{child.text}</a></li>
</Dropdown> {/each}
</Dropdown>
{:else}
<button
class:menu-dropdown-show={isOpen}
class="menu-dropdown-toggle"
on:click={() => (isOpen = !isOpen)}
>
{text}
</button>
{#if isOpen}
<ul
class="menu-dropdown menu-dropdown-show"
transition:slide={{ duration: 200, easing: cubicInOut }}
>
{#each children ?? [] as child}
<li><a href={child.href}>{child.text}</a></li>
{/each}
</ul>
{/if}
{/if}
+11 -9
View File
@@ -2,10 +2,12 @@
import { type NavLinkItem } from '$lib/types/Nav/NavLinkItem'; import { type NavLinkItem } from '$lib/types/Nav/NavLinkItem';
import NavLinkParent from './NavLinkParent.svelte'; import NavLinkParent from './NavLinkParent.svelte';
export let isMobile = false;
const links: NavLinkItem[] = [ const links: NavLinkItem[] = [
{ {
text: 'Products', text: 'Products',
href: '/products', href: '/products'
}, },
{ {
text: 'Parent', text: 'Parent',
@@ -13,26 +15,26 @@
children: [ children: [
{ {
text: 'Submenu 1', text: 'Submenu 1',
href: '/', href: '/'
}, },
{ {
text: 'Submenu 2', text: 'Submenu 2',
href: '/', href: '/'
}, }
], ]
}, },
{ {
text: 'Example Product', text: 'Example Product',
href: '/tools/exampleproduct', href: '/tools/exampleproduct'
}, }
]; ];
</script> </script>
<ul class="flex space-x-4"> <ul class:flex={!isMobile} class:space-x-4={!isMobile}>
{#each links as link} {#each links as link}
<li> <li>
{#if link.isParent} {#if link.isParent}
<NavLinkParent text={link.text} children={link.children} /> <NavLinkParent text={link.text} children={link.children} {isMobile} />
{:else} {:else}
<a href={link.href}>{link.text}</a> <a href={link.href}>{link.text}</a>
{/if} {/if}
+48 -51
View File
@@ -6,13 +6,13 @@
import { invalidate } from '$app/navigation'; import { invalidate } from '$app/navigation';
import CookieConsent from '$lib/components/CookieConsent.svelte'; import CookieConsent from '$lib/components/CookieConsent.svelte';
import Nav from '$lib/components/Nav.svelte'; import Nav from '$lib/components/Nav.svelte';
import { slide } from 'svelte/transition';
import { cubicInOut } from 'svelte/easing';
export let data; export let data;
let { supabase, session } = data; let { supabase, session } = data;
$: ({ supabase, session } = data); $: ({ supabase, session } = data);
let localGeneral: any; let localGeneral: any;
const unsubscribeToGeneralStore = general.subscribe((value) => { const unsubscribeToGeneralStore = general.subscribe((value) => {
localGeneral = value; localGeneral = value;
@@ -40,10 +40,6 @@
return () => data.subscription.unsubscribe(); return () => data.subscription.unsubscribe();
}); });
</script> </script>
<svelte:head> <svelte:head>
@@ -126,52 +122,53 @@
</nav> </nav>
</footer> </footer>
<div {#if !localGeneral.hideToast}
id={localGeneral.toastType == 'success' ? 'success-toast' : 'error-toast'} <div
role="alert" id={localGeneral.toastType == 'success' ? 'success-toast' : 'error-toast'}
class="alert {localGeneral.toastType == 'success' role="alert"
? 'alert-success' class="alert {localGeneral.toastType == 'success'
: 'alert-error'} fixed top-20 right-3 max-w-52 text-white {localGeneral.hideToast ? 'alert-success'
? 'hidden' : 'alert-error'} fixed top-20 right-3 max-w-52 text-white"
: ''}" transition:slide={{ duration: 200, easing: cubicInOut }}
> >
{#if localGeneral.toastType == 'success'} {#if localGeneral.toastType == 'success'}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6" class="stroke-current shrink-0 h-6 w-6"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
><path ><path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/></svg /></svg
> >
{:else} {:else}
<svg <svg
class="stroke-current shrink-0 h-6 w-6" class="stroke-current shrink-0 h-6 w-6"
viewBox="0 0 32 32" viewBox="0 0 32 32"
xml:space="preserve" xml:space="preserve"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
><g ><g
><g id="Error_1_" ><g id="Error_1_"
><g id="Error"> ><g id="Error">
<circle cx="16" cy="16" id="BG" r="16" style="fill:#E6E6E6;" /> <circle cx="16" cy="16" id="BG" r="16" style="fill:#E6E6E6;" />
<path <path
d="M14.5,25h3v-3h-3V25z M14.5,6v13h3V6H14.5z" d="M14.5,25h3v-3h-3V25z M14.5,6v13h3V6H14.5z"
id="Exclamatory_x5F_Sign" id="Exclamatory_x5F_Sign"
style="fill:#D72828;" style="fill:#D72828;"
/> />
</g> </g>
</g></g </g></g
></svg ></svg
> >
{/if} {/if}
<span>{localGeneral.toastMessage == '' ? 'Added to basket' : localGeneral.toastMessage}</span> <span>{localGeneral.toastMessage == '' ? 'Added to basket' : localGeneral.toastMessage}</span>
</div> </div>
{/if}
<style scoped lang="postcss"> <style scoped lang="postcss">
.user-circle { .user-circle {