diff --git a/src/App.tsx b/src/App.tsx index 434b8c5..bd0ae7a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,8 @@ import { useEffect } from "react"; import { useAppStore } from "@/stores/app-store"; import { AppShell } from "@/components/layout/AppShell"; +import { BoardList } from "@/components/boards/BoardList"; +import { BoardView } from "@/components/board/BoardView"; export default function App() { const initialized = useAppStore((s) => s.initialized); @@ -23,15 +25,7 @@ export default function App() { return ( - {view.type === "board-list" ? ( -
- Board List -
- ) : ( -
- Board View -
- )} + {view.type === "board-list" ? : }
); } diff --git a/src/components/boards/BoardCard.tsx b/src/components/boards/BoardCard.tsx new file mode 100644 index 0000000..9d15376 --- /dev/null +++ b/src/components/boards/BoardCard.tsx @@ -0,0 +1,146 @@ +import { useState } from "react"; +import { formatDistanceToNow } from "date-fns"; +import { Trash2, Copy } from "lucide-react"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, + ContextMenuSeparator, +} from "@/components/ui/context-menu"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +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 { deleteBoard, loadBoard, saveBoard } from "@/lib/storage"; + +interface BoardCardProps { + board: BoardMeta; +} + +export function BoardCard({ board }: BoardCardProps) { + const [confirmDelete, setConfirmDelete] = useState(false); + + const setView = useAppStore((s) => s.setView); + const addRecentBoard = useAppStore((s) => s.addRecentBoard); + const refreshBoards = useAppStore((s) => s.refreshBoards); + const openBoard = useBoardStore((s) => s.openBoard); + + const relativeTime = formatDistanceToNow(new Date(board.updatedAt), { + addSuffix: true, + }); + + async function handleOpen() { + await openBoard(board.id); + setView({ type: "board", boardId: board.id }); + addRecentBoard(board.id); + } + + async function handleDelete() { + await deleteBoard(board.id); + await refreshBoards(); + setConfirmDelete(false); + } + + async function handleDuplicate() { + const original = await loadBoard(board.id); + const { ulid } = await import("ulid"); + const ts = new Date().toISOString(); + const duplicated = { + ...original, + id: ulid(), + title: `${original.title} (copy)`, + createdAt: ts, + updatedAt: ts, + }; + await saveBoard(duplicated); + await refreshBoards(); + } + + return ( + <> + + + + + + + + + Duplicate + + + setConfirmDelete(true)} + > + + Delete + + + + + {/* Delete confirmation dialog */} + + + + + Delete Board + + + Are you sure you want to delete “{board.title}”? This + action cannot be undone. + + + + + + + + + + ); +} diff --git a/src/components/boards/BoardList.tsx b/src/components/boards/BoardList.tsx new file mode 100644 index 0000000..5938aa4 --- /dev/null +++ b/src/components/boards/BoardList.tsx @@ -0,0 +1,54 @@ +import { useState } from "react"; +import { Plus } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useAppStore } from "@/stores/app-store"; +import { BoardCard } from "@/components/boards/BoardCard"; +import { NewBoardDialog } from "@/components/boards/NewBoardDialog"; + +export function BoardList() { + const boards = useAppStore((s) => s.boards); + const [dialogOpen, setDialogOpen] = useState(false); + + if (boards.length === 0) { + return ( + <> +
+

+ Create your first board +

+ +
+ + + ); + } + + return ( + <> +
+ {/* Heading row */} +
+

+ Your Boards +

+ +
+ + {/* Board grid */} +
+ {boards.map((board) => ( + + ))} +
+
+ + + + ); +} diff --git a/src/components/boards/NewBoardDialog.tsx b/src/components/boards/NewBoardDialog.tsx new file mode 100644 index 0000000..7058c96 --- /dev/null +++ b/src/components/boards/NewBoardDialog.tsx @@ -0,0 +1,173 @@ +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { createBoard } from "@/lib/board-factory"; +import { saveBoard } from "@/lib/storage"; +import { useAppStore } from "@/stores/app-store"; +import { useBoardStore } from "@/stores/board-store"; + +const PRESET_COLORS = [ + "#6366f1", // indigo + "#8b5cf6", // violet + "#ec4899", // pink + "#f43f5e", // rose + "#f97316", // orange + "#eab308", // yellow + "#22c55e", // green + "#06b6d4", // cyan +]; + +type Template = "blank" | "kanban" | "sprint"; + +interface NewBoardDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function NewBoardDialog({ open, onOpenChange }: NewBoardDialogProps) { + const [title, setTitle] = useState(""); + const [color, setColor] = useState(PRESET_COLORS[0]); + const [template, setTemplate] = useState