diff --git a/src/App.tsx b/src/App.tsx index 372008e..11bc6ae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { BoardList } from "@/components/boards/BoardList"; import { BoardView } from "@/components/board/BoardView"; import { CommandPalette } from "@/components/command-palette/CommandPalette"; import { SettingsDialog } from "@/components/settings/SettingsDialog"; +import { ToastContainer } from "@/components/toast/ToastContainer"; import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts"; export default function App() { @@ -64,6 +65,7 @@ export default function App() { + ); } diff --git a/src/components/boards/BoardCard.tsx b/src/components/boards/BoardCard.tsx index 20b9686..e8f2ee4 100644 --- a/src/components/boards/BoardCard.tsx +++ b/src/components/boards/BoardCard.tsx @@ -21,6 +21,7 @@ import { Button } from "@/components/ui/button"; import type { BoardMeta } from "@/types/board"; import { useAppStore } from "@/stores/app-store"; import { useBoardStore } from "@/stores/board-store"; +import { useToastStore } from "@/stores/toast-store"; import { deleteBoard, loadBoard, saveBoard } from "@/lib/storage"; interface BoardCardProps { @@ -31,6 +32,7 @@ interface BoardCardProps { export function BoardCard({ board, index = 0 }: BoardCardProps) { const [confirmDelete, setConfirmDelete] = useState(false); const prefersReducedMotion = useReducedMotion(); + const addToast = useToastStore((s) => s.addToast); const setView = useAppStore((s) => s.setView); const addRecentBoard = useAppStore((s) => s.addRecentBoard); @@ -51,6 +53,7 @@ export function BoardCard({ board, index = 0 }: BoardCardProps) { await deleteBoard(board.id); await refreshBoards(); setConfirmDelete(false); + addToast(`"${board.title}" deleted`, "info"); } async function handleDuplicate() { @@ -66,6 +69,7 @@ export function BoardCard({ board, index = 0 }: BoardCardProps) { }; await saveBoard(duplicated); await refreshBoards(); + addToast(`"${board.title}" duplicated`, "success"); } return ( diff --git a/src/components/import-export/ImportExportButtons.tsx b/src/components/import-export/ImportExportButtons.tsx index a9a6caf..c0e15ab 100644 --- a/src/components/import-export/ImportExportButtons.tsx +++ b/src/components/import-export/ImportExportButtons.tsx @@ -9,6 +9,7 @@ import { } from "@/components/ui/dropdown-menu"; import { useAppStore } from "@/stores/app-store"; import { useBoardStore } from "@/stores/board-store"; +import { useToastStore } from "@/stores/toast-store"; import { saveBoard } from "@/lib/storage"; import { exportBoardAsJson, @@ -31,6 +32,7 @@ function downloadBlob(content: string, filename: string, mimeType: string) { export function ImportExportButtons() { const fileInputRef = useRef(null); + const addToast = useToastStore((s) => s.addToast); const board = useBoardStore((s) => s.board); const refreshBoards = useAppStore((s) => s.refreshBoards); const setView = useAppStore((s) => s.setView); @@ -42,6 +44,7 @@ export function ImportExportButtons() { const json = exportBoardAsJson(board); const safeName = board.title.replace(/[^a-zA-Z0-9-_]/g, "_"); downloadBlob(json, `${safeName}.json`, "application/json"); + addToast("Board exported as JSON", "success"); } function handleExportCsv() { @@ -49,6 +52,7 @@ export function ImportExportButtons() { const csv = exportBoardAsCsv(board); const safeName = board.title.replace(/[^a-zA-Z0-9-_]/g, "_"); downloadBlob(csv, `${safeName}.csv`, "text/csv"); + addToast("Board exported as CSV", "success"); } function handleImportClick() { @@ -77,9 +81,10 @@ export function ImportExportButtons() { await openBoard(imported.id); setView({ type: "board", boardId: imported.id }); addRecentBoard(imported.id); + addToast("Board imported successfully", "success"); } catch (err) { console.error("Import failed:", err); - // Could show a toast here in the future + addToast("Import failed — check file format", "error"); } // Reset the input so the same file can be re-imported diff --git a/src/components/toast/ToastContainer.tsx b/src/components/toast/ToastContainer.tsx new file mode 100644 index 0000000..5fac598 --- /dev/null +++ b/src/components/toast/ToastContainer.tsx @@ -0,0 +1,31 @@ +import { AnimatePresence, motion } from "framer-motion"; +import { useToastStore } from "@/stores/toast-store"; + +const TYPE_STYLES = { + success: "bg-pylon-accent/10 text-pylon-accent border-pylon-accent/20", + error: "bg-pylon-danger/10 text-pylon-danger border-pylon-danger/20", + info: "bg-pylon-surface text-pylon-text border-border", +} as const; + +export function ToastContainer() { + const toasts = useToastStore((s) => s.toasts); + + return ( +
+ + {toasts.map((toast) => ( + + {toast.message} + + ))} + +
+ ); +} diff --git a/src/stores/toast-store.ts b/src/stores/toast-store.ts new file mode 100644 index 0000000..f918f39 --- /dev/null +++ b/src/stores/toast-store.ts @@ -0,0 +1,33 @@ +import { create } from "zustand"; + +export type ToastType = "success" | "error" | "info"; + +interface Toast { + id: string; + message: string; + type: ToastType; +} + +interface ToastState { + toasts: Toast[]; + addToast: (message: string, type?: ToastType) => void; + removeToast: (id: string) => void; +} + +let nextId = 0; + +export const useToastStore = create((set) => ({ + toasts: [], + + addToast: (message, type = "info") => { + const id = String(++nextId); + set((s) => ({ toasts: [...s.toasts, { id, message, type }] })); + setTimeout(() => { + set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })); + }, 3000); + }, + + removeToast: (id) => { + set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })); + }, +}));