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