feat: basic toast notification system - closes #5
This commit is contained in:
parent
a051ec8408
commit
f1143551d6
@ -1,7 +1,10 @@
|
|||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
|
import type { NotificationData } from './toast/notification';
|
||||||
|
|
||||||
export const store = $state<{
|
export const store = $state<{
|
||||||
pageTitle: string;
|
pageTitle: string;
|
||||||
|
notifications: NotificationData[];
|
||||||
}>({
|
}>({
|
||||||
pageTitle: env.PUBLIC_SITE_NAME ?? '',
|
pageTitle: env.PUBLIC_SITE_NAME ?? '',
|
||||||
|
notifications: [],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
import IconDeviceFloppy from '~icons/tabler/device-floppy';
|
import IconDeviceFloppy from '~icons/tabler/device-floppy';
|
||||||
import IconTrash from '~icons/tabler/trash';
|
import IconTrash from '~icons/tabler/trash';
|
||||||
import IconX from '~icons/tabler/x';
|
import IconX from '~icons/tabler/x';
|
||||||
|
import { Toast } from '../toast/notification';
|
||||||
|
|
||||||
let { children, createOnly } = $props();
|
let { children, createOnly } = $props();
|
||||||
</script>
|
</script>
|
||||||
@ -13,7 +14,15 @@
|
|||||||
method="POST"
|
method="POST"
|
||||||
action="?/update"
|
action="?/update"
|
||||||
use:enhance={() => {
|
use:enhance={() => {
|
||||||
return async ({ update }) => {
|
return async ({ update, result }) => {
|
||||||
|
if (result.type == 'failure' && result.data?.error) {
|
||||||
|
Toast.add({
|
||||||
|
Icon: IconX,
|
||||||
|
content: result.data.error as string,
|
||||||
|
theme: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
update({ reset: false });
|
update({ reset: false });
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
|
|||||||
43
src/lib/v2/toast/Notification.svelte
Normal file
43
src/lib/v2/toast/Notification.svelte
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { afterNavigate } from '$app/navigation';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { Toast, type NotificationData, type NotificationTheme } from './notification';
|
||||||
|
|
||||||
|
const themeClasses: Record<NotificationTheme, string> = {
|
||||||
|
neutral: 'bg-neutral-700 hover:bg-neutral-600 border-neutral-500',
|
||||||
|
error: 'bg-red-700 hover:bg-red-500 border-red-500',
|
||||||
|
success: 'bg-emerald-700 hover:bg-emerald-600 border-emerald-500',
|
||||||
|
};
|
||||||
|
|
||||||
|
let { data, class: otherClass = '' }: { data: NotificationData; class?: string } = $props();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (data.timeout > -1) {
|
||||||
|
setTimeout(() => Toast.remove(data.id), data.timeout * 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterNavigate(() => {
|
||||||
|
Toast.clear();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
aria-label="Dismiss alert notification"
|
||||||
|
transition:fade
|
||||||
|
class="flex items-center px-6 gap-2 py-2 cursor-pointer pointer-events-auto shadow-lg text-sm sm:text-base
|
||||||
|
rounded-full transition-all duration-300 hover:scale-99 border text-white w-fit
|
||||||
|
{themeClasses[data.theme]} {otherClass}"
|
||||||
|
onclick={() => {
|
||||||
|
if (data.dismissable) Toast.remove(data.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if data.Icon}
|
||||||
|
<data.Icon class="sm:-ml-2 {data.spin ? 'animate-spin' : ''}" />
|
||||||
|
{/if}
|
||||||
|
<p role="alert">{data.content}</p>
|
||||||
|
{#if data.dismissable}
|
||||||
|
<p>(Click to dismiss)</p>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
43
src/lib/v2/toast/notification.ts
Normal file
43
src/lib/v2/toast/notification.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { store } from '../globalStore.svelte';
|
||||||
|
|
||||||
|
export type NotificationTheme = 'neutral' | 'error' | 'success';
|
||||||
|
|
||||||
|
export type NotificationData = {
|
||||||
|
Icon?: any;
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
dismissable: boolean;
|
||||||
|
timeout: number;
|
||||||
|
spin: boolean;
|
||||||
|
theme: NotificationTheme;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BasicNotificationData = Omit<Partial<NotificationData>, 'id'> & { content: string };
|
||||||
|
|
||||||
|
export namespace Toast {
|
||||||
|
export function add(options: BasicNotificationData) {
|
||||||
|
const data: NotificationData = Object.assign(
|
||||||
|
{
|
||||||
|
id: nanoid(),
|
||||||
|
dismissable: true,
|
||||||
|
timeout: 10,
|
||||||
|
theme: 'neutral',
|
||||||
|
spin: false,
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
store.notifications.push(data);
|
||||||
|
return data.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function remove(notificationId: string) {
|
||||||
|
const currentNotifications = store.notifications;
|
||||||
|
store.notifications = currentNotifications.filter((n) => n.id !== notificationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clear() {
|
||||||
|
store.notifications = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { store } from '$lib/v2/globalStore.svelte';
|
||||||
|
import Notification from '$lib/v2/toast/Notification.svelte';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fixed flex flex-col gap-5 m-3 sm:m-6 bottom-0 left-0 pointer-events-none transition-all"
|
||||||
|
>
|
||||||
|
{#each store.notifications as n (n.id)}
|
||||||
|
<Notification data={n} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|||||||
@ -3,12 +3,11 @@
|
|||||||
import { store } from '$lib/v2/globalStore.svelte.js';
|
import { store } from '$lib/v2/globalStore.svelte.js';
|
||||||
import EditPage from '$lib/v2/snippets/EditPage.svelte';
|
import EditPage from '$lib/v2/snippets/EditPage.svelte';
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
store.pageTitle = 'Account details';
|
store.pageTitle = 'Account details';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p class="text-white">{form?.error}</p>
|
|
||||||
<EditPage createOnly>
|
<EditPage createOnly>
|
||||||
<InputText
|
<InputText
|
||||||
name="name"
|
name="name"
|
||||||
|
|||||||
@ -17,7 +17,6 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p class="text-white">{form?.error}</p>
|
|
||||||
<EditPage createOnly={!data.user}>
|
<EditPage createOnly={!data.user}>
|
||||||
<InputText
|
<InputText
|
||||||
name="name"
|
name="name"
|
||||||
|
|||||||
@ -1,11 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import InputText from '$lib/v2/forms/InputText.svelte';
|
import InputText from '$lib/v2/forms/InputText.svelte';
|
||||||
|
import { Toast } from '$lib/v2/toast/notification';
|
||||||
import { Button } from 'bits-ui';
|
import { Button } from 'bits-ui';
|
||||||
|
import { untrack } from 'svelte';
|
||||||
import IconLogin2 from '~icons/tabler/login2';
|
import IconLogin2 from '~icons/tabler/login2';
|
||||||
|
import IconX from '~icons/tabler/x';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
|
|
||||||
let { form }: PageProps = $props();
|
let { form }: PageProps = $props();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (form?.error) {
|
||||||
|
untrack(() => {
|
||||||
|
Toast.add({
|
||||||
|
Icon: IconX,
|
||||||
|
content: form.error,
|
||||||
|
theme: 'error',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -17,7 +33,7 @@
|
|||||||
class="flex flex-col m-4 w-full max-w-120 bg-neutral-950 p-8 sm:p-12 sm:px
|
class="flex flex-col m-4 w-full max-w-120 bg-neutral-950 p-8 sm:p-12 sm:px
|
||||||
rounded-2xl border border-neutral-700 shadow-xl"
|
rounded-2xl border border-neutral-700 shadow-xl"
|
||||||
>
|
>
|
||||||
<form method="POST">
|
<form method="POST" use:enhance>
|
||||||
<InputText
|
<InputText
|
||||||
name="username"
|
name="username"
|
||||||
label="Username"
|
label="Username"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user