1import { createSignal, For, Show } from "solid-js";
2
3export type Notification = {
4 id: string;
5 message: string;
6 progress?: number;
7 total?: number;
8 type?: "info" | "success" | "error";
9};
10
11const [notifications, setNotifications] = createSignal<Notification[]>([]);
12const [removingIds, setRemovingIds] = createSignal<Set<string>>(new Set());
13
14export const addNotification = (notification: Omit<Notification, "id">) => {
15 const id = `notification-${Date.now()}-${Math.random()}`;
16 setNotifications([...notifications(), { ...notification, id }]);
17 return id;
18};
19
20export const updateNotification = (id: string, updates: Partial<Notification>) => {
21 setNotifications(notifications().map((n) => (n.id === id ? { ...n, ...updates } : n)));
22};
23
24export const removeNotification = (id: string) => {
25 setRemovingIds(new Set([...removingIds(), id]));
26 setTimeout(() => {
27 setNotifications(notifications().filter((n) => n.id !== id));
28 setRemovingIds((ids) => {
29 const newIds = new Set(ids);
30 newIds.delete(id);
31 return newIds;
32 });
33 }, 250);
34};
35
36export const NotificationContainer = () => {
37 return (
38 <div class="pointer-events-none fixed bottom-4 left-4 z-50 flex flex-col gap-2">
39 <For each={notifications()}>
40 {(notification) => (
41 <div
42 class="dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto flex min-w-64 flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 shadow-md select-none dark:border-neutral-700"
43 classList={{
44 "border-blue-500 dark:border-blue-400": notification.type === "info",
45 "border-green-500 dark:border-green-400": notification.type === "success",
46 "border-red-500 dark:border-red-400": notification.type === "error",
47 "animate-[slideIn_0.25s_ease-in]": !removingIds().has(notification.id),
48 "animate-[slideOut_0.25s_ease-in]": removingIds().has(notification.id),
49 }}
50 onClick={() => removeNotification(notification.id)}
51 >
52 <div class="flex items-center gap-2 text-sm">
53 <Show when={notification.progress !== undefined}>
54 <span class="iconify lucide--download" />
55 </Show>
56 <Show when={notification.type === "success"}>
57 <span class="iconify lucide--check-circle text-green-600 dark:text-green-400" />
58 </Show>
59 <Show when={notification.type === "error"}>
60 <span class="iconify lucide--x-circle text-red-500 dark:text-red-400" />
61 </Show>
62 <span>{notification.message}</span>
63 </div>
64 <Show when={notification.progress !== undefined}>
65 <div class="flex flex-col gap-1">
66 <Show
67 when={notification.total !== undefined && notification.total > 0}
68 fallback={
69 <div class="text-xs text-neutral-600 dark:text-neutral-400">
70 {notification.progress} MB
71 </div>
72 }
73 >
74 <div class="h-2 w-full overflow-hidden rounded-full bg-neutral-200 dark:bg-neutral-700">
75 <div
76 class="h-full rounded-full bg-blue-500 transition-all dark:bg-blue-400"
77 style={{ width: `${notification.progress}%` }}
78 />
79 </div>
80 <div class="text-xs text-neutral-600 dark:text-neutral-400">
81 {notification.progress}%
82 </div>
83 </Show>
84 </div>
85 </Show>
86 </div>
87 )}
88 </For>
89 </div>
90 );
91};