mirror of
https://github.com/jcreek/SvelteKitSaasBoilerplate.git
synced 2026-07-12 18:43:50 +00:00
Merge pull request #52 from jcreek/35-replace-navbar-with-a-better-one
35 replace navbar with a better one
This commit is contained in:
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
"stripe": "stripe listen --forward-to localhost:5173/api/webhook/stripe",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "prettier --check . && eslint .",
|
"lint": "prettier --check . && eslint .",
|
||||||
|
|||||||
@@ -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. For details, please see our <a href="/privacy" class="underline"
|
to our use of cookies. For details, please see our <a href="/privacy" class="underline"
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<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;
|
||||||
|
|
||||||
|
let isOpen = false;
|
||||||
|
let dropdown: HTMLDivElement;
|
||||||
|
|
||||||
|
const DropdownIcon = {
|
||||||
|
closed: '▼',
|
||||||
|
open: '▲'
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const handleClickAnywhere = ({ target }: MouseEvent) => {
|
||||||
|
if (!isOpen || !target) return;
|
||||||
|
const clickTarget = target as HTMLElement;
|
||||||
|
if (!dropdown.contains(clickTarget) || clickTarget.closest('a')) {
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('click', handleClickAnywhere);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', handleClickAnywhere);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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>
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { scheduleToast } from '$lib/stores/toastStore.js';
|
||||||
export let supabase: any;
|
export let supabase: any;
|
||||||
|
const SUCCESS_MESSAGE = 'Check your email for the magic link.';
|
||||||
|
|
||||||
let error = '',
|
let loading = false,
|
||||||
message = '',
|
|
||||||
loading = false,
|
|
||||||
email = '';
|
email = '';
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
error = '';
|
|
||||||
message = '';
|
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
const { error: err } = await supabase.auth.signInWithOtp({ email });
|
const { error: err } = await supabase.auth.signInWithOtp({ email });
|
||||||
|
|
||||||
if (err) error = err.message;
|
if (err) scheduleToast(err.message, 'error', 5000);
|
||||||
else message = 'Check your email for the magic link.';
|
else scheduleToast(SUCCESS_MESSAGE, 'success', 5000);
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@@ -37,18 +35,6 @@
|
|||||||
<button class="btn btn-active btn-primary w-full" on:click disabled={loading}
|
<button class="btn btn-active btn-primary w-full" on:click disabled={loading}
|
||||||
>Send magic link</button
|
>Send magic link</button
|
||||||
>
|
>
|
||||||
|
|
||||||
{#if message}
|
|
||||||
<div class="text-success">
|
|
||||||
{message}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if error}
|
|
||||||
<div class="text-error">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isMobile: boolean = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<li><a href="/products">Products</a></li>
|
|
||||||
<li class={isMobile ? '' : 'dropdown'}>
|
|
||||||
{#if isMobile}
|
|
||||||
<div tabindex="0" role="button" class="">Parent</div>
|
|
||||||
{:else}
|
|
||||||
<details>
|
|
||||||
<summary class="flex items-center"><span class="mr-1">▼</span>Parent </summary>
|
|
||||||
</details>
|
|
||||||
{/if}
|
|
||||||
<ul
|
|
||||||
class="{isMobile
|
|
||||||
? ''
|
|
||||||
: 'dropdown-content rounded-box w-52 shadow'} z-[1] menu p-2 bg-base-100 text-base-content"
|
|
||||||
>
|
|
||||||
<li><a href="/">Submenu 1</a></li>
|
|
||||||
<li><a href="/">Submenu 2</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><a href="/tools/exampleproduct">Example Product</a></li>
|
|
||||||
@@ -45,4 +45,4 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button on:click={signOut}>Sign Out</button>
|
<button on:click={signOut} aria-label="Sign out of your account" type="button">Sign Out</button>
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<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 { basket, type Basket } from '$lib/stores/basket.js';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import type { Database } from '$lib/types/supabase';
|
||||||
|
|
||||||
|
export let session: Session | null;
|
||||||
|
export let supabase: SupabaseClient<Database>;
|
||||||
|
|
||||||
|
let localBasket: Basket;
|
||||||
|
const unsubscribeToBasket = basket.subscribe((value) => {
|
||||||
|
localBasket = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
unsubscribeToBasket();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="bg-primary text-primary-content p-4">
|
||||||
|
<!-- Nav Container -->
|
||||||
|
<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" aria-label="Open navigation menu">
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h8m-8 6h16"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Dropdown content -->
|
||||||
|
<NavLinks isMobile {session} {supabase} />
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<!-- Logo -->
|
||||||
|
<a href="/" class="text-white font-bold text-xl">SvelteKit SaaS Boilerplate</a>
|
||||||
|
<div class="hidden lg:block">
|
||||||
|
<NavLinks {session} />
|
||||||
|
</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" aria-label="View basket">
|
||||||
|
<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"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<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 hidden md:flex"
|
||||||
|
aria-label="User account menu"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<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><a href="/account">Account</a></li>
|
||||||
|
<li><SignOut {supabase} /></li>
|
||||||
|
{:else}
|
||||||
|
<MagicLink {supabase} />
|
||||||
|
{/if}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { NavLinkItem } from '$lib/types/Nav/NavLinkItem';
|
||||||
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
import SignOut from '../SignOut.svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
export let linkItem: NavLinkItem;
|
||||||
|
export let supabase: SupabaseClient | null = null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !linkItem.isSignout}
|
||||||
|
<a
|
||||||
|
role="menuitem"
|
||||||
|
href={linkItem.href}
|
||||||
|
aria-current={linkItem.href === $page.url.pathname ? 'page' : undefined}>{linkItem.text}</a
|
||||||
|
>
|
||||||
|
{:else if supabase && linkItem.isSignout}
|
||||||
|
<SignOut {supabase} />
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type NavLinkItem } from '$lib/types/Nav/NavLinkItem';
|
||||||
|
import { cubicInOut } from 'svelte/easing';
|
||||||
|
import Dropdown from '../Dropdown.svelte';
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
|
import NavLink from './NavLink.svelte';
|
||||||
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
export let linkItem: NavLinkItem;
|
||||||
|
export let isMobile: boolean;
|
||||||
|
export let supabase: SupabaseClient | null;
|
||||||
|
|
||||||
|
let isOpen = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !isMobile}
|
||||||
|
<Dropdown indicator>
|
||||||
|
<div slot="dropdown">{linkItem.text}</div>
|
||||||
|
{#each linkItem.children ?? [] as child (child.text.replace(' ', '-').toLowerCase())}
|
||||||
|
<li><NavLink linkItem={child} /></li>
|
||||||
|
{/each}
|
||||||
|
</Dropdown>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class:menu-dropdown-show={isOpen}
|
||||||
|
class="menu-dropdown-toggle"
|
||||||
|
on:click={() => (isOpen = !isOpen)}
|
||||||
|
>
|
||||||
|
{linkItem.text}
|
||||||
|
</button>
|
||||||
|
{#if isOpen}
|
||||||
|
<ul
|
||||||
|
role="menu"
|
||||||
|
class="menu-dropdown menu-dropdown-show"
|
||||||
|
transition:slide={{ duration: 200, easing: cubicInOut }}
|
||||||
|
>
|
||||||
|
{#each linkItem.children ?? [] as child (child.text.replace(' ', '-').toLowerCase())}
|
||||||
|
<li><NavLink linkItem={child} {supabase} /></li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type NavLinkItem } from '$lib/types/Nav/NavLinkItem';
|
||||||
|
import type { Session } from '@supabase/supabase-js';
|
||||||
|
import NavLinkParent from './NavLinkParent.svelte';
|
||||||
|
import type SupabaseClient from '@supabase/supabase-js/dist/module/SupabaseClient';
|
||||||
|
import NavLink from './NavLink.svelte';
|
||||||
|
|
||||||
|
export let session: Session | null;
|
||||||
|
export let supabase: SupabaseClient | null = null;
|
||||||
|
export let isMobile = false;
|
||||||
|
|
||||||
|
const userAccountLinks: NavLinkItem[] = [];
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
if (session) {
|
||||||
|
userAccountLinks.push({
|
||||||
|
text: 'User', // TODO - #62 replace with user's name - will require a database call to get the user's name - suggest setting up a user model to store user data.
|
||||||
|
isParent: true,
|
||||||
|
isUserAccount: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
text: 'Account',
|
||||||
|
href: '/account'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Sign Out',
|
||||||
|
isSignout: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
userAccountLinks.push({
|
||||||
|
text: 'Log in',
|
||||||
|
href: '/login'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const links: NavLinkItem[] = [
|
||||||
|
...userAccountLinks,
|
||||||
|
{
|
||||||
|
text: 'Products',
|
||||||
|
href: '/products'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Parent',
|
||||||
|
isParent: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
text: 'Submenu 1',
|
||||||
|
href: '/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Submenu 2',
|
||||||
|
href: '/'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Example Product',
|
||||||
|
href: '/tools/exampleproduct'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul role="menu" class:flex={!isMobile} class:space-x-4={!isMobile}>
|
||||||
|
{#each links as link (link.text.replace(' ', '-').toLowerCase())}
|
||||||
|
<li class:md:hidden={link.isUserAccount}>
|
||||||
|
{#if link.isParent}
|
||||||
|
<NavLinkParent linkItem={link} {isMobile} {supabase} />
|
||||||
|
{:else}
|
||||||
|
<NavLink linkItem={link} />
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
|
import { cubicInOut } from 'svelte/easing';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import { toast } from '$lib/stores/toastStore.js';
|
||||||
|
import type { Toast } from '$lib/types/Shared/Toast';
|
||||||
|
|
||||||
|
let localToast: Toast;
|
||||||
|
const unsubscribeToToastStore = toast.subscribe((value: Toast) => {
|
||||||
|
localToast = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
unsubscribeToToastStore();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !localToast.hideToast}
|
||||||
|
<div
|
||||||
|
id={localToast.toastType == 'success' ? 'success-toast' : 'error-toast'}
|
||||||
|
role="alert"
|
||||||
|
aria-live="polite"
|
||||||
|
class="alert {localToast.toastType == 'success'
|
||||||
|
? 'alert-success'
|
||||||
|
: 'alert-error'} fixed top-20 right-3 max-w-80 z-50 text-white"
|
||||||
|
transition:slide={{ duration: 200, easing: cubicInOut }}
|
||||||
|
>
|
||||||
|
{#if localToast.toastType == 'success'}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="stroke-current shrink-0 h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
><path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
class="stroke-current shrink-0 h-6 w-6"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
><g
|
||||||
|
><g id="Error_1_"
|
||||||
|
><g id="Error">
|
||||||
|
<circle cx="16" cy="16" id="BG" r="16" style="fill:#E6E6E6;" />
|
||||||
|
<path
|
||||||
|
d="M14.5,25h3v-3h-3V25z M14.5,6v13h3V6H14.5z"
|
||||||
|
id="Exclamatory_x5F_Sign"
|
||||||
|
style="fill:#D72828;"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g></g
|
||||||
|
></svg
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<span>{localToast.toastMessage}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { writable } from 'svelte/store';
|
|
||||||
|
|
||||||
export const general = writable({ hideToast: true, toastMessage: '', toastType: 'success' });
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Toast } from '$lib/types/Shared/Toast';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
const MIN_DELAY = 3000;
|
||||||
|
|
||||||
|
export const toast = writable<Toast>({ hideToast: true, toastMessage: '', toastType: 'success' });
|
||||||
|
export function scheduleToast(message: string, toastType: 'success' | 'error', delay: number = MIN_DELAY) {
|
||||||
|
|
||||||
|
toast.update((value) => {
|
||||||
|
return {
|
||||||
|
...value,
|
||||||
|
hideToast: false,
|
||||||
|
toastMessage: message,
|
||||||
|
toastType: toastType
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.update((value) => {
|
||||||
|
return { ...value, hideToast: true, toastMessage: '' };
|
||||||
|
});
|
||||||
|
}, Math.max(delay, MIN_DELAY));
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export interface NavLinkItem {
|
||||||
|
text: string;
|
||||||
|
href?: string;
|
||||||
|
isParent?: boolean;
|
||||||
|
children?: NavLinkItem[];
|
||||||
|
isSignout?: boolean;
|
||||||
|
isUserAccount?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type Toast = {
|
||||||
|
hideToast: boolean,
|
||||||
|
toastMessage: string,
|
||||||
|
toastType: 'success' | 'error'
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
export async function signOut(supabase: SupabaseClient, callback: () => void): Promise<void> {
|
||||||
|
// TODO - #61 use the error from the response
|
||||||
|
const { error } = await supabase.auth.signOut().then(callback);
|
||||||
|
}
|
||||||
+5
-176
@@ -1,34 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '../app.postcss';
|
import '../app.postcss';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { isBrowser } from '@supabase/ssr';
|
import { isBrowser } from '@supabase/ssr';
|
||||||
import { basket, type Basket, type Item } from '$lib/stores/basket.js';
|
|
||||||
import { general } from '$lib/stores/generalStore.js';
|
|
||||||
import { invalidate } from '$app/navigation';
|
import { invalidate } from '$app/navigation';
|
||||||
import SignOut from '$lib/components/SignOut.svelte';
|
|
||||||
import CookieConsent from '$lib/components/CookieConsent.svelte';
|
import CookieConsent from '$lib/components/CookieConsent.svelte';
|
||||||
import NavLinks from '$lib/components/NavLinks.svelte';
|
import Nav from '$lib/components/navigation/Nav.svelte';
|
||||||
import MagicLink from '$lib/components/MagicLink.svelte';
|
import Toast from '$lib/components/shared/Toast.svelte';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
let { supabase, session } = data;
|
let { supabase, session } = data;
|
||||||
$: ({ supabase, session } = data);
|
$: ({ supabase, session } = data);
|
||||||
|
|
||||||
let localBasket: Basket;
|
|
||||||
const unsubscribeToBasket = basket.subscribe((value) => {
|
|
||||||
localBasket = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
let localGeneral: any;
|
|
||||||
const unsubscribeToGeneralStore = general.subscribe((value) => {
|
|
||||||
localGeneral = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
unsubscribeToBasket();
|
|
||||||
unsubscribeToGeneralStore();
|
|
||||||
});
|
|
||||||
|
|
||||||
let cookiesAccepted = false;
|
let cookiesAccepted = false;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -47,12 +29,6 @@
|
|||||||
|
|
||||||
return () => data.subscription.unsubscribe();
|
return () => data.subscription.unsubscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
let isMobileMenuOpen = false;
|
|
||||||
|
|
||||||
function toggleMobileMenu() {
|
|
||||||
isMobileMenuOpen = !isMobileMenuOpen;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -69,109 +45,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<nav class="bg-primary text-primary-content p-4">
|
<Nav {session} {supabase} />
|
||||||
<div class="container mx-auto flex justify-between items-center">
|
|
||||||
<a href="/" class="text-white font-bold text-xl">SvelteKit SaaS Boilerplate</a>
|
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<!-- Navbar Links (hidden on mobile, visible on larger screens) -->
|
|
||||||
<div class="hidden md:flex space-x-4">
|
|
||||||
<ul class="flex flex-col md:flex-row md:space-x-4">
|
|
||||||
<NavLinks isMobile={false} />
|
|
||||||
</ul>
|
|
||||||
</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">
|
|
||||||
<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>
|
|
||||||
<span class="badge badge-sm indicator-item">{localBasket.items.length}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- User Account Dropdown -->
|
|
||||||
<details class="dropdown dropdown-end">
|
|
||||||
<summary tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">
|
|
||||||
<div class="w-10 rounded-full">
|
|
||||||
{#if session?.user}
|
|
||||||
<div class="user-circle text-primary-content border-primary-content">
|
|
||||||
{session?.user.email[0].toUpperCase()}
|
|
||||||
</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>
|
|
||||||
</summary>
|
|
||||||
|
|
||||||
{#if session?.user}
|
|
||||||
<ul
|
|
||||||
tabindex="-1"
|
|
||||||
class="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 text-base-content rounded-box min-w-[13rem] w-auto"
|
|
||||||
>
|
|
||||||
<li><a href="/account">Account</a></li>
|
|
||||||
<li><a href="/settings">Settings</a></li>
|
|
||||||
<li><SignOut {supabase} /></li>
|
|
||||||
</ul>
|
|
||||||
{:else}
|
|
||||||
<div
|
|
||||||
class="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 text-base-content rounded-box min-w-[13rem] w-auto"
|
|
||||||
>
|
|
||||||
<MagicLink {supabase} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mobile Menu -->
|
|
||||||
<div
|
|
||||||
class="bg-base-100 text-base-content shadow rounded-box p-4 transition duration-300 ease-in-out md:hidden"
|
|
||||||
class:block={isMobileMenuOpen}
|
|
||||||
class:hidden={!isMobileMenuOpen}
|
|
||||||
>
|
|
||||||
<ul class="flex flex-col space-y-4">
|
|
||||||
<NavLinks isMobile={true} />
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
@@ -241,52 +115,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<div
|
<Toast />
|
||||||
id={localGeneral.toastType == 'success' ? 'success-toast' : 'error-toast'}
|
|
||||||
role="alert"
|
|
||||||
class="alert {localGeneral.toastType == 'success'
|
|
||||||
? 'alert-success'
|
|
||||||
: 'alert-error'} fixed top-20 right-3 max-w-52 text-white {localGeneral.hideToast
|
|
||||||
? 'hidden'
|
|
||||||
: ''}"
|
|
||||||
>
|
|
||||||
{#if localGeneral.toastType == 'success'}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="stroke-current shrink-0 h-6 w-6"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
><path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
{:else}
|
|
||||||
<svg
|
|
||||||
class="stroke-current shrink-0 h-6 w-6"
|
|
||||||
viewBox="0 0 32 32"
|
|
||||||
xml:space="preserve"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
><g
|
|
||||||
><g id="Error_1_"
|
|
||||||
><g id="Error">
|
|
||||||
<circle cx="16" cy="16" id="BG" r="16" style="fill:#E6E6E6;" />
|
|
||||||
<path
|
|
||||||
d="M14.5,25h3v-3h-3V25z M14.5,6v13h3V6H14.5z"
|
|
||||||
id="Exclamatory_x5F_Sign"
|
|
||||||
style="fill:#D72828;"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</g></g
|
|
||||||
></svg
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<span>{localGeneral.toastMessage == '' ? 'Added to basket' : localGeneral.toastMessage}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style scoped lang="postcss">
|
<style scoped lang="postcss">
|
||||||
.user-circle {
|
.user-circle {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script src="https://js.stripe.com/v3/" lang="ts">
|
<script src="https://js.stripe.com/v3/" lang="ts">
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { basket, type Basket, type Item } from '$lib/stores/basket.js';
|
import { basket, type Basket } from '$lib/stores/basket.js';
|
||||||
import BasketItem from '$lib/components/checkout/BasketItem.svelte';
|
import BasketItem from '$lib/components/checkout/BasketItem.svelte';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
let { supabase, session, url } = data;
|
let { supabase, session } = data;
|
||||||
$: ({ supabase, session, url } = data);
|
$: ({ supabase, session } = data);
|
||||||
|
|
||||||
let localBasket: Basket;
|
let localBasket: Basket;
|
||||||
const unsubscribe = basket.subscribe((value) => {
|
const unsubscribe = basket.subscribe((value) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { basket, type Basket, type Item } from '$lib/stores/basket.js';
|
import { basket, type Basket } from '$lib/stores/basket.js';
|
||||||
import OrderConfirmationItem from '$lib/components/checkout/OrderConfirmationItem.svelte';
|
import OrderConfirmationItem from '$lib/components/checkout/OrderConfirmationItem.svelte';
|
||||||
import { formatCurrency } from '$lib/utils/currency';
|
import { formatCurrency } from '$lib/utils/currency';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import { basket, type Basket, type Item } from '$lib/stores/basket.js';
|
import { basket, type Basket } from '$lib/stores/basket.js';
|
||||||
import { general } from '$lib/stores/generalStore.js';
|
import { scheduleToast } from '$lib/stores/toastStore.js';
|
||||||
|
|
||||||
/** @type {import('./$types').PageData} */
|
/** @type {import('./$types').PageData} */
|
||||||
export let data;
|
export let data;
|
||||||
@@ -40,15 +40,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
general.update((value) => {
|
scheduleToast('Added to basket', 'success', 5000);
|
||||||
return { ...value, hideToast: false };
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
general.update((value) => {
|
|
||||||
return { ...value, hideToast: true };
|
|
||||||
});
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user