feat: add toast notification system with success, error, and info variants

This commit is contained in:
Your Name
2026-02-15 20:30:17 +02:00
parent 61a5f11f25
commit 6341897487
5 changed files with 76 additions and 1 deletions

View File

@@ -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 (

View File

@@ -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<HTMLInputElement>(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

View File

@@ -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 (
<div className="pointer-events-none fixed bottom-4 right-4 z-[100] flex flex-col gap-2">
<AnimatePresence>
{toasts.map((toast) => (
<motion.div
key={toast.id}
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
className={`pointer-events-auto rounded-lg border px-4 py-2 text-sm shadow-md ${TYPE_STYLES[toast.type]}`}
>
{toast.message}
</motion.div>
))}
</AnimatePresence>
</div>
);
}