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">
import { onMount } from 'svelte';
import { cubicInOut } from 'svelte/easing';
import { slide } from 'svelte/transition';
export let cookiesAccepted;
let showBanner = false;
@@ -18,7 +20,10 @@
</script>
{#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">
We use cookies to ensure you get the best experience on our website. By continuing, you agree
to our use of cookies.
+41 -33
View File
@@ -1,43 +1,51 @@
<script lang="ts">
import { onMount } from 'svelte';
import { cubicInOut } from 'svelte/easing';
import { slide } from 'svelte/transition';
export let indicator: boolean = false;
export let posRight: boolean = false;
export let indicator: boolean = false;
export let posRight: boolean = false;
let isOpen = false;
let dropdown: HTMLDivElement;
const DropdownIcon = {
let isOpen = false;
let dropdown: HTMLDivElement;
const DropdownIcon = {
closed: '&#x25BC;',
open: '&#x25B2;'
};
open: '&#x25B2;'
};
onMount(() => {
const handleClick = (event: MouseEvent) => {
if (!isOpen) return;
if (!dropdown.contains(event.target as Node) || event.target instanceof HTMLAnchorElement) {
isOpen = false;
}
};
onMount(() => {
const handleClick = (event: MouseEvent) => {
if (!isOpen) return;
if (!dropdown.contains(event.target as Node) || event.target instanceof HTMLAnchorElement) {
isOpen = false;
}
};
document.addEventListener('click', handleClick);
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
});
return () => {
document.removeEventListener('click', handleClick);
};
});
</script>
<div bind:this={dropdown} class="relative">
<div class="dropdown-title">
<button class="flex" on:click={() => isOpen = !isOpen}>
{#if indicator}
<span class="mr-1">{@html isOpen ? DropdownIcon.open : DropdownIcon.closed }</span>
{/if}
<slot name="dropdown"></slot>
</button>
</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">
<slot />
</ul>
</div>
<div bind:this={dropdown} class="relative block">
<div class="dropdown-title block w-full">
<button class="flex w-full" on:click={() => (isOpen = !isOpen)}>
{#if indicator}
<span class="mr-1">{@html isOpen ? DropdownIcon.open : DropdownIcon.closed}</span>
{/if}
<slot name="dropdown"></slot>
</button>
</div>
{#if isOpen}
<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>
+124 -98
View File
@@ -1,110 +1,136 @@
<script lang="ts">
import type { Session, SupabaseClient } from "@supabase/supabase-js";
import MagicLink from "./MagicLink.svelte";
import NavLinks from "./NavLinks.svelte";
import SignOut from "./SignOut.svelte";
import Dropdown from "./Dropdown.svelte";
import type { Session, SupabaseClient } from '@supabase/supabase-js';
import MagicLink from './MagicLink.svelte';
import NavLinks from './NavLinks.svelte';
import SignOut from './SignOut.svelte';
import Dropdown from './Dropdown.svelte';
import { basket, type Basket } from '$lib/stores/basket.js';
import { onDestroy } from "svelte";
import { onDestroy } from 'svelte';
export let session: Session;
export let supabase: SupabaseClient<any, "public", any>;
export let session: Session | null;
export let supabase: SupabaseClient<any, 'public', any>;
let isMobileMenuOpen = false;
function toggleMobileMenu() {
isMobileMenuOpen = !isMobileMenuOpen;
}
let localBasket: Basket;
let localBasket: Basket;
const unsubscribeToBasket = basket.subscribe((value) => {
localBasket = value;
});
onDestroy(() => {
unsubscribeToBasket();
});
onDestroy(() => {
unsubscribeToBasket();
});
</script>
<nav class="bg-primary text-primary-content p-4">
<div class="container mx-auto flex justify-between items-center">
<!-- Hamburger Menu Button (Mobile) -->
<button on:click={toggleMobileMenu} class="block md:hidden text-white focus:outline-none">
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h8m-8 6h16"
></path>
</svg>
</button>
<a href="/" class="text-white font-bold text-xl">SvelteKit SaaS Boilerplate</a>
<NavLinks />
<!-- Right Side: Basket & User Account -->
<div class="flex items-center space-x-4">
<!-- Basket/Checkout Icon -->
<a href="/checkout" class="btn btn-ghost btn-circle">
<div class="indicator">
<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>
{#if localBasket.items.length > 0}
<span class="badge badge-sm indicator-item">{localBasket.items.length}</span>
{/if}
</div>
</a>
<!-- User Account Dropdown -->
<Dropdown posRight>
<div slot="dropdown">
<div class="btn btn-ghost btn-circle avatar placeholder">
<div class="w-10 rounded-full">
{#if session?.user}
<div class="user-circle text-primary-content border-primary-content">
<span class="text-xl">{session?.user.email ? session?.user.email[0].toUpperCase() : '?'}</span>
</div>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
fill-rule="evenodd"
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"
clip-rule="evenodd"
/>
</svg>
{/if}
</div>
</div>
</div>
{#if session?.user}
<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>
<div class="container mx-auto flex justify-between items-center">
<!-- Hamburger Menu Button (Mobile) -->
<div class="lg:hidden mr-4">
<Dropdown>
<div slot="dropdown">
<button class="block text-white focus:outline-none">
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h8m-8 6h16"
></path>
</svg>
</button>
</div>
<!-- Dropdown content -->
<NavLinks isMobile />
</Dropdown>
</div>
<a href="/" class="text-white font-bold text-xl">SvelteKit SaaS Boilerplate</a>
<div class="hidden lg:block">
<NavLinks />
</div>
<!-- Right Side: Basket & User Account -->
<div class="flex items-center space-x-4">
<!-- Basket/Checkout Icon -->
<a href="/checkout" class="btn btn-ghost btn-circle hidden lg:flex">
<div class="indicator">
<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>
{#if localBasket.items.length > 0}
<span class="badge badge-sm indicator-item">{localBasket.items.length}</span>
{/if}
</div>
</a>
<!-- User Account Dropdown -->
<Dropdown posRight>
<div slot="dropdown">
<div class="btn btn-ghost btn-circle avatar placeholder">
<div class="w-10 rounded-full">
{#if session?.user}
<div class="user-circle text-primary-content border-primary-content">
<span class="text-xl"
>{session?.user.email ? session?.user.email[0].toUpperCase() : '?'}</span
>
</div>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
fill-rule="evenodd"
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"
clip-rule="evenodd"
/>
</svg>
{/if}
</div>
</div>
</div>
<!-- Dropdown content -->
{#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>
+34 -9
View File
@@ -1,14 +1,39 @@
<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 { slide } from 'svelte/transition';
export let text: string;
export let children: NavLinkItem[] | undefined;
export let text: string;
export let children: NavLinkItem[] | undefined;
export let isMobile: boolean;
let isOpen = false;
</script>
<Dropdown indicator>
<div slot="dropdown">{text}</div>
{#each children ?? [] as child}
<li><a href={child.href}>{child.text}</a></li>
{/each}
</Dropdown>
{#if !isMobile}
<Dropdown indicator>
<div slot="dropdown">{text}</div>
{#each children ?? [] as child}
<li><a href={child.href}>{child.text}</a></li>
{/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 NavLinkParent from './NavLinkParent.svelte';
export let isMobile = false;
const links: NavLinkItem[] = [
{
text: 'Products',
href: '/products',
href: '/products'
},
{
text: 'Parent',
@@ -13,26 +15,26 @@
children: [
{
text: 'Submenu 1',
href: '/',
href: '/'
},
{
text: 'Submenu 2',
href: '/',
},
],
href: '/'
}
]
},
{
text: 'Example Product',
href: '/tools/exampleproduct',
},
href: '/tools/exampleproduct'
}
];
</script>
<ul class="flex space-x-4">
<ul class:flex={!isMobile} class:space-x-4={!isMobile}>
{#each links as link}
<li>
{#if link.isParent}
<NavLinkParent text={link.text} children={link.children} />
<NavLinkParent text={link.text} children={link.children} {isMobile} />
{:else}
<a href={link.href}>{link.text}</a>
{/if}