diff --git a/web/src/components/DeckEditor.tsx b/web/src/components/DeckEditor.tsx index 4c28929..3de197c 100644 --- a/web/src/components/DeckEditor.tsx +++ b/web/src/components/DeckEditor.tsx @@ -1,6 +1,7 @@ import { createSignal, Show } from "solid-js"; import { api } from "../lib/api"; import type { Visibility } from "../lib/store"; +import { toast } from "../lib/toast"; export function DeckEditor() { const [title, setTitle] = createSignal(""); @@ -20,9 +21,12 @@ export function DeckEditor() { const payload = { title: title(), description: description(), tags: [], visibility }; - await api.post("/decks", payload); - // TODO: Navigate or show success - alert("Deck created!"); + try { + await api.post("/decks", payload); + toast.success("Deck created!"); + } catch { + toast.error("Failed to create deck"); + } }; return ( diff --git a/web/src/components/layout/AppLayout.tsx b/web/src/components/layout/AppLayout.tsx index 3ce00e6..e3a3abe 100644 --- a/web/src/components/layout/AppLayout.tsx +++ b/web/src/components/layout/AppLayout.tsx @@ -1,13 +1,13 @@ import type { Component, JSX } from "solid-js"; +import { Toaster } from "../ui/Toast"; import { Header } from "./Header"; -interface AppLayoutProps { - children?: JSX.Element; -} +type AppLayoutProps = { children?: JSX.Element }; export const AppLayout: Component = (props) => { return (
+
{props.children}
diff --git a/web/src/components/ui/Toast.tsx b/web/src/components/ui/Toast.tsx new file mode 100644 index 0000000..7df0b2f --- /dev/null +++ b/web/src/components/ui/Toast.tsx @@ -0,0 +1,98 @@ +import { For, Match, Switch } from "solid-js"; +import type { Component } from "solid-js"; +import { toast, toasts, type ToastType } from "../../lib/toast"; + +const borderColors: Record = { + success: "border-l-4 border-green-500", + error: "border-l-4 border-red-500", + warning: "border-l-4 border-yellow-500", + info: "border-l-4 border-blue-500", +}; + +const InfoIcon: Component = () => ( + + + +); + +export const Toaster: Component = () => { + return ( +
+ + {(t) => ( + + )} + +
+ ); +}; diff --git a/web/src/lib/toast.test.ts b/web/src/lib/toast.test.ts new file mode 100644 index 0000000..71bbb5e --- /dev/null +++ b/web/src/lib/toast.test.ts @@ -0,0 +1,45 @@ +import { createRoot } from "solid-js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { toast, toasts } from "./toast"; + +describe("Toast Store", () => { + beforeEach(() => { + const currentToasts = toasts(); + currentToasts.forEach(t => toast.remove(t.id)); + }); + + it("should add a toast", () => { + createRoot(() => { + toast.success("Success message"); + const currentToasts = toasts(); + expect(currentToasts.length).toBe(1); + expect(currentToasts[0].message).toBe("Success message"); + expect(currentToasts[0].type).toBe("success"); + }); + }); + + it("should remove a toast", () => { + createRoot(() => { + toast.error("Error message"); + let currentToasts = toasts(); + expect(currentToasts.length).toBe(1); + const id = currentToasts[0].id; + + toast.remove(id); + currentToasts = toasts(); + expect(currentToasts.length).toBe(0); + }); + }); + + it("should auto-dismiss toast", () => { + vi.useFakeTimers(); + createRoot(() => { + toast.info("Info message", 1000); + expect(toasts().length).toBe(1); + + vi.advanceTimersByTime(1000); + expect(toasts().length).toBe(0); + }); + vi.useRealTimers(); + }); +}); diff --git a/web/src/lib/toast.ts b/web/src/lib/toast.ts new file mode 100644 index 0000000..fd5ff92 --- /dev/null +++ b/web/src/lib/toast.ts @@ -0,0 +1,37 @@ +import { createSignal } from "solid-js"; + +export type ToastType = "success" | "error" | "info" | "warning"; + +export interface ToastData { + id: string; + type: ToastType; + message: string; + duration?: number; +} + +const [toasts, setToasts] = createSignal([]); + +const addToast = (type: ToastType, message: string, duration = 5000) => { + const id = Math.random().toString(36).substring(2, 9); + setToasts((prev) => [...prev, { id, type, message, duration }]); + + if (duration > 0) { + setTimeout(() => { + removeToast(id); + }, duration); + } +}; + +const removeToast = (id: string) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); +}; + +export const toast = { + success: (message: string, duration?: number) => addToast("success", message, duration), + error: (message: string, duration?: number) => addToast("error", message, duration), + info: (message: string, duration?: number) => addToast("info", message, duration), + warning: (message: string, duration?: number) => addToast("warning", message, duration), + remove: removeToast, +}; + +export { toasts };