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 type { NotificationData } from './toast/notification';
|
||||
|
||||
export const store = $state<{
|
||||
pageTitle: string;
|
||||
notifications: NotificationData[];
|
||||
}>({
|
||||
pageTitle: env.PUBLIC_SITE_NAME ?? '',
|
||||
notifications: [],
|
||||
});
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
import IconDeviceFloppy from '~icons/tabler/device-floppy';
|
||||
import IconTrash from '~icons/tabler/trash';
|
||||
import IconX from '~icons/tabler/x';
|
||||
import { Toast } from '../toast/notification';
|
||||
|
||||
let { children, createOnly } = $props();
|
||||
</script>
|
||||
@ -13,7 +14,15 @@
|
||||
method="POST"
|
||||
action="?/update"
|
||||
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 });
|
||||
};
|
||||
}}
|
||||
|
||||
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">
|
||||
import { store } from '$lib/v2/globalStore.svelte';
|
||||
import Notification from '$lib/v2/toast/Notification.svelte';
|
||||
import '../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@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 EditPage from '$lib/v2/snippets/EditPage.svelte';
|
||||
|
||||
let { data, form } = $props();
|
||||
let { data } = $props();
|
||||
|
||||
store.pageTitle = 'Account details';
|
||||
</script>
|
||||
|
||||
<p class="text-white">{form?.error}</p>
|
||||
<EditPage createOnly>
|
||||
<InputText
|
||||
name="name"
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<p class="text-white">{form?.error}</p>
|
||||
<EditPage createOnly={!data.user}>
|
||||
<InputText
|
||||
name="name"
|
||||
|
||||
@ -1,11 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import InputText from '$lib/v2/forms/InputText.svelte';
|
||||
import { Toast } from '$lib/v2/toast/notification';
|
||||
import { Button } from 'bits-ui';
|
||||
import { untrack } from 'svelte';
|
||||
import IconLogin2 from '~icons/tabler/login2';
|
||||
import IconX from '~icons/tabler/x';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { form }: PageProps = $props();
|
||||
|
||||
$effect(() => {
|
||||
if (form?.error) {
|
||||
untrack(() => {
|
||||
Toast.add({
|
||||
Icon: IconX,
|
||||
content: form.error,
|
||||
theme: 'error',
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<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
|
||||
rounded-2xl border border-neutral-700 shadow-xl"
|
||||
>
|
||||
<form method="POST">
|
||||
<form method="POST" use:enhance>
|
||||
<InputText
|
||||
name="username"
|
||||
label="Username"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user