From cf0a25af1d2834bfe6c5ea892ce5769936e5a673 Mon Sep 17 00:00:00 2001 From: soryu Date: Tue, 3 Feb 2026 22:01:29 +0000 Subject: Add makima chain mechanism --- .../frontend/src/components/chains/ChainList.tsx | 205 +++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 makima/frontend/src/components/chains/ChainList.tsx (limited to 'makima/frontend/src/components/chains/ChainList.tsx') diff --git a/makima/frontend/src/components/chains/ChainList.tsx b/makima/frontend/src/components/chains/ChainList.tsx new file mode 100644 index 0000000..eda79d7 --- /dev/null +++ b/makima/frontend/src/components/chains/ChainList.tsx @@ -0,0 +1,205 @@ +import { useState, useCallback } from "react"; +import type { ChainSummary, ChainStatus } from "../../lib/api"; + +interface ChainListProps { + chains: ChainSummary[]; + loading: boolean; + onSelect: (chainId: string) => void; + onCreate: () => void; + selectedId?: string; + onArchive: (chain: ChainSummary) => void; +} + +export function ChainList({ + chains, + loading, + onSelect, + onCreate, + selectedId, + onArchive, +}: ChainListProps) { + const [statusFilter, setStatusFilter] = useState("all"); + const [contextMenu, setContextMenu] = useState<{ + chain: ChainSummary; + x: number; + y: number; + } | null>(null); + + const filteredChains = chains.filter((chain) => + statusFilter === "all" ? true : chain.status === statusFilter + ); + + const handleContextMenu = useCallback( + (e: React.MouseEvent, chain: ChainSummary) => { + e.preventDefault(); + setContextMenu({ chain, x: e.clientX, y: e.clientY }); + }, + [] + ); + + const closeContextMenu = useCallback(() => { + setContextMenu(null); + }, []); + + const handleArchive = useCallback(() => { + if (contextMenu) { + onArchive(contextMenu.chain); + setContextMenu(null); + } + }, [contextMenu, onArchive]); + + const getStatusColor = (status: ChainStatus) => { + switch (status) { + case "active": + return "text-[#4ade80] bg-[#4ade80]/10"; + case "completed": + return "text-[#60a5fa] bg-[#60a5fa]/10"; + case "archived": + return "text-[#6b7280] bg-[#6b7280]/10"; + default: + return "text-[#8b949e] bg-[#8b949e]/10"; + } + }; + + const getStatusIcon = (status: ChainStatus) => { + switch (status) { + case "active": + return ( + + + + ); + case "completed": + return ( + + + + ); + case "archived": + return ( + + + + + + ); + default: + return null; + } + }; + + return ( +
+ {/* Header */} +
+
+

Chains

+ +
+ + {/* Status filter */} +
+ {(["all", "active", "completed", "archived"] as const).map((status) => ( + + ))} +
+
+ + {/* Chain list */} +
+ {loading ? ( +
+

Loading chains...

+
+ ) : filteredChains.length === 0 ? ( +
+

+ {statusFilter === "all" ? "No chains yet" : `No ${statusFilter} chains`} +

+
+ ) : ( +
+ {filteredChains.map((chain) => ( +
onSelect(chain.id)} + onContextMenu={(e) => handleContextMenu(e, chain)} + className={`p-3 cursor-pointer transition-colors ${ + selectedId === chain.id + ? "bg-[rgba(117,170,252,0.15)]" + : "hover:bg-[rgba(117,170,252,0.05)]" + }`} + > +
+ + {chain.name} + + + {getStatusIcon(chain.status)} + {chain.status} + +
+ {chain.description && ( +

+ {chain.description} +

+ )} +
+ {chain.contractCount} contracts + + {new Date(chain.updatedAt).toLocaleDateString()} + +
+
+ ))} +
+ )} +
+ + {/* Context menu */} + {contextMenu && ( +
+ + {contextMenu.chain.status !== "archived" && ( + + )} +
+ )} +
+ ); +} -- cgit v1.2.3