Fixing utils helper, changing header for logged in to be a dropdown menu, adding list and signout to dropdown, adding spinner to logout.

This commit is contained in:
Bradley Shellnut 2023-09-18 09:28:15 +12:00
parent 7b9b9a1d64
commit 66ef49436f
17 changed files with 439 additions and 199 deletions

View file

@ -1,10 +1,9 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { LogOut } from 'lucide-svelte';
import { ListChecks, ListTodo, LogOut, User } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button';
// import { Toggle } from "$lib/components/ui/toggle"; -- TODO: Add light/dark toggle
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import * as Avatar from "$lib/components/ui/avatar";
import * as Sheet from "$lib/components/ui/sheet";
import Logo from '$components/logo.svelte';
export let user: any;
@ -23,57 +22,49 @@
{#if user}
<a href="/collection" title="Go to your collection" data-sveltekit-preload-data>Collection</a>
<a href="/wishlist" title="Go to your wishlist" data-sveltekit-preload-data>Wishlist</a>
<Sheet.Root>
<Sheet.Trigger>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Avatar.Root asChild>
<Avatar.Fallback class="text-3xl font-medium text-magnum-700 h-16 w-16 bg-neutral-100">
{avatar}
</Avatar.Fallback>
</Avatar.Root>
</Sheet.Trigger>
<Sheet.Content side="right">
<Sheet.Header>
<Sheet.Title>Menu</Sheet.Title>
</Sheet.Header>
<div class="menu">
<Sheet.Close asChild let:builder>
<div class="item">
<Button builders={[builder]} variant="link" class="text-secondary-foreground" href="/profile">View Profile</Button>
</div>
</Sheet.Close>
<Sheet.Close asChild let:builder>
<div class="item">
<Button builders={[builder]} variant="link" class="text-secondary-foreground" href="/collection">Your Collection</Button>
</div>
</Sheet.Close>
<Sheet.Close asChild let:builder>
<div class="item">
<Button builders={[builder]} variant="link" class="text-secondary-foreground" href="/wishlist">Your Wishlist</Button>
</div>
</Sheet.Close>
<div class="separator" />
<div class="item">
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.Label>My Account</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item>
<User class="mr-2 h-4 w-4" />
<Button variant="link" class="text-secondary-foreground" href="/profile">Profile</Button>
<DropdownMenu.Shortcut>⇧⌘P</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Item>
<ListChecks class="mr-2 h-4 w-4" />
<Button variant="link" class="text-secondary-foreground" href="/collection">Collection</Button>
<DropdownMenu.Shortcut>⇧⌘C</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Item>
<ListTodo class="mr-2 h-4 w-4" />
<Button variant="link" class="text-secondary-foreground" href="/wishlist">Wishlist</Button>
<DropdownMenu.Shortcut>⇧⌘W</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Item>
<form
use:enhance
action="/logout"
method="POST"
>
<Sheet.Close asChild let:builder>
<Button builders={[builder]} type="submit">
<LogOut class="mr-2 h-4 w-4"/>
Sign out
</Button>
</Sheet.Close>
<Button type="submit">
<LogOut class="mr-2 h-4 w-4"/>
Sign out
</Button>
</form>
</div>
</div>
<Sheet.Footer>
<Sheet.Close asChild let:builder>
<Button builders={[builder]} type="button">Close</Button>
</Sheet.Close>
</Sheet.Footer>
</Sheet.Content>
</Sheet.Root>
<DropdownMenu.Shortcut>⇧⌘Q</DropdownMenu.Shortcut>
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
{/if}
{#if !user}
<a href="/login">
@ -103,10 +94,6 @@
display: grid;
}
.item {
/* margin: 0.2rem 0; */
}
.corner {
width: 3em;
height: 3em;

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { Check } from "lucide-svelte";
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:checked
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.CheckboxIndicator>
<Check class="h-4 w-4" />
</DropdownMenuPrimitive.CheckboxIndicator>
</span>
<slot />
</DropdownMenuPrimitive.CheckboxItem>

View file

@ -0,0 +1,25 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils";
type $$Props = DropdownMenuPrimitive.ContentProps;
type $$Events = DropdownMenuPrimitive.ContentEvents;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Content
{transition}
{transitionConfig}
class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none",
className
)}
{...$$restProps}
on:keydown
>
<slot />
</DropdownMenuPrimitive.Content>

View file

@ -0,0 +1,31 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
type $$Props = DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Item
class={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<slot />
</DropdownMenuPrimitive.Item>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
type $$Props = DropdownMenuPrimitive.LabelProps & {
inset?: boolean;
};
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Label
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...$$restProps}
>
<slot />
</DropdownMenuPrimitive.Label>

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
export let value: $$Props["value"] = undefined;
</script>
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
<slot />
</DropdownMenuPrimitive.RadioGroup>

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { Circle } from "lucide-svelte";
type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<DropdownMenuPrimitive.RadioItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{value}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.RadioIndicator>
<Circle class="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.RadioIndicator>
</span>
<slot />
</DropdownMenuPrimitive.RadioItem>

View file

@ -0,0 +1,14 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
type $$Props = DropdownMenuPrimitive.SeparatorProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-muted", className)}
{...$$restProps}
/>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span
class={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...$$restProps}
>
<slot />
</span>

View file

@ -0,0 +1,30 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils";
type $$Props = DropdownMenuPrimitive.SubContentProps;
type $$Events = DropdownMenuPrimitive.SubContentEvents;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = {
x: -10,
y: 0
};
export { className as class };
</script>
<DropdownMenuPrimitive.SubContent
{transition}
{transitionConfig}
class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none",
className
)}
{...$$restProps}
on:keydown
on:focusout
on:pointermove
>
<slot />
</DropdownMenuPrimitive.SubContent>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { ChevronRight } from "lucide-svelte";
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.SubTrigger
class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
>
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>

View file

@ -0,0 +1,48 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import Content from "./dropdown-menu-content.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
const Trigger = DropdownMenuPrimitive.Trigger;
const Group = DropdownMenuPrimitive.Group;
export {
Sub,
Root,
Item,
Label,
Group,
Trigger,
Content,
Shortcut,
Separator,
RadioItem,
SubContent,
SubTrigger,
RadioGroup,
CheckboxItem,
//
Root as DropdownMenu,
Sub as DropdownMenuSub,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
Group as DropdownMenuGroup,
Content as DropdownMenuContent,
Trigger as DropdownMenuTrigger,
Shortcut as DropdownMenuShortcut,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
RadioGroup as DropdownMenuRadioGroup,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
CheckboxItem as DropdownMenuCheckboxItem
};

View file

@ -1,6 +1,47 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { cubicOut } from 'svelte/easing';
import type { TransitionConfig } from 'svelte/transition';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (style: Record<string, number | string | undefined>): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + key + ':' + style[key] + ';';
}, '');
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: transform + 'translate3d(' + x + 'px, ' + y + 'px, 0) scale(' + scale + ')',
opacity: t
});
},
easing: cubicOut
};
};

View file

@ -4,159 +4,20 @@
import Header from '$lib/components/header/index.svelte';
import Footer from '$lib/components/footer.svelte';
// $: {
// if ($navigating) {
// debounce(() => {
// boredState.update((n) => ({ ...n, loading: true }));
// }, 250);
// }
// if (!$navigating) {
// boredState.update((n) => ({ ...n, loading: false }));
// }
// }
// $: isOpen = $boredState?.dialog?.isOpen;
// if (browser) {
// const collator = new Intl.Collator('en');
// let collectionEmpty = $collectionStore.length === 0 || false;
// let wishlistEmpty = $wishlistStore.length === 0 || false;
// if (wishlistEmpty && localStorage?.wishlist && localStorage?.wishlist?.length !== 0) {
// const wishlist: SavedGameType[] = JSON.parse(localStorage.wishlist);
// if (wishlist?.length !== 0) {
// wishlist.sort((a, b) => collator.compare(a.name, b.name));
// for (const item of wishlist) {
// if (!item?.searchTerms) {
// item.searchTerms = `${item?.name?.toLowerCase()}`;
// }
// if (!item?.includeInRandom) {
// item.includeInRandom = false;
// }
// }
// wishlistStore.addAll(wishlist);
// }
// }
// if (collectionEmpty && localStorage?.collection && localStorage?.collection?.length !== 0) {
// const collection: SavedGameType[] = JSON.parse(localStorage.collection);
// if (collection?.length !== 0) {
// collection.sort((a, b) => collator.compare(a.name, b.name));
// for (const item of collection) {
// if (!item?.searchTerms) {
// item.searchTerms = `${item?.name?.toLowerCase()}`;
// }
// }
// collectionStore.addAll(collection);
// }
// }
// }
// const dev = process.env.NODE_ENV !== 'production';
export let data;
// $: ({ user } = data);
// const flash = getFlash(page, {
// clearAfterMs: 6000
// });
// let flashType;
// let flashMessage;
// $: flashType = $flash?.type;
// $: flashMessage = $flash?.message;
// if ($flash && flashType && flashMessage) {
// switch (flashType) {
// case 'success':
// toast.success(flashMessage);
// break;
// case 'error':
// toast.error(flashMessage);
// break;
// default:
// toast.error(flashMessage);
// }
// }
// onMount(() => {
// // set the theme to the user's active theme
// $theme = user?.theme || 'system';
// document.querySelector('html')?.setAttribute('data-theme', $theme);
// });
// flash.subscribe(($flash) => {
// if (!$flash) return;
// if ($flash.type == 'success') {
// toast.success($flash.message);
// } else {
// toast.error($flash.message, {
// duration: 5000
// });
// }
// // Clearing the flash message could sometimes
// // be required here to avoid double-toasting.
// flash.set(undefined);
// });
</script>
<!-- {#if !dev}
<Analytics />
{/if} -->
<Header user={data.user} />
<!-- <div class="wrapper"> -->
<Header user={data.user} />
<main>
<Transition url={data.url} transition={{ type: 'page' }}>
<slot />
</Transition>
</main>
<main>
<Transition url={data.url} transition={{ type: 'page' }}>
<slot />
</Transition>
</main>
<Footer />
<!-- </div> -->
<!-- <Toaster /> -->
<!-- {#if $boredState?.loading}
<Portal>
<div class="loading">
<Loading></Loading>
<h3>Loading...</h3>
</div>
<div class="background"></div>
</Portal>
{/if}
{#if isOpen}
<div class="container">
<svelte:component this={$boredState?.dialog?.content}></svelte:component>
</div>
{/if} -->
<Footer />
<style lang="postcss">
/* .flash {
display: inline-block;
position: absolute;
place-items: center;
padding: 0.5rem;
border-radius: 2px;
} */
.loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 101;
display: grid;
place-items: center;
gap: 1rem;
& h3 {
color: white;
}
}
.background {
background: black;
opacity: 0.8;

View file

@ -6,8 +6,6 @@
// import Random from '$lib/components/random/index.svelte';
export let data;
export let formData;
console.log('formData', formData);
</script>
<h1>Search Boardgames!</h1>

View file

@ -9,6 +9,9 @@
import { Input } from '$components/ui/input';
import { Button } from '$components/ui/button';
import * as Alert from "$components/ui/alert";
import { boredState } from '$lib/stores/boredState.js';
import Portal from '$lib/Portal.svelte';
import Loading from '$components/loading.svelte';
export let data;
const { form, errors, enhance, delayed } = superForm(data.form, {
@ -26,7 +29,7 @@
taintedMessage: null,
validators: signInSchema,
validationMethod: 'oninput',
delayMs: 0,
delayMs: 250,
});
const flash = flashModule.getFlash(page);
@ -76,9 +79,35 @@
</a>.
</p>
</form>
{#if $delayed}
<Portal>
<!-- <Transition transition={{ type: 'fade', duration: 0 }}> -->
<div class="loading">
<Loading />
<h3>Loading...</h3>
</div>
<!-- </Transition> -->
<div class="background" />
</Portal>
{/if}
</div>
<style lang="postcss">
.loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 101;
display: grid;
place-items: center;
gap: 1rem;
& h3 {
color: white;
}
}
.login {
display: flex;
margin-top: 1.5rem;

View file

@ -8,6 +8,8 @@
import debounce from 'just-debounce-it';
import 'iconify-icon';
import Analytics from '$lib/components/analytics.svelte';
import Portal from "$lib/Portal.svelte";
import Loading from "$components/loading.svelte";
import { boredState } from '$lib/stores/boredState';
import { theme } from '$state/theme';
@ -48,6 +50,7 @@
}
$: isOpen = $boredState?.dialog?.isOpen;
$: loading = $boredState?.loading;
onMount(() => {
// set the theme to the user's active theme
@ -82,9 +85,34 @@
<slot />
</div>
<!-- {#if loading}
<Portal>
<div class="loading">
<Loading />
<h3>Loading...</h3>
</div>
<div class="background" />
</Portal>
{/if} -->
<Toaster />
<style lang="postcss">
.loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 101;
display: grid;
place-items: center;
gap: 1rem;
& h3 {
color: white;
}
}
.layout {
display: flex;
position: relative;