diff options
| author | soryu <soryu@soryu.co> | 2026-02-03 22:01:29 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-03 22:01:37 +0000 |
| commit | cf0a25af1d2834bfe6c5ea892ce5769936e5a673 (patch) | |
| tree | 476ba326ac1752281a441b5c17d2b3be4b23a2a9 /makima/frontend/src/components/chains/ChainList.tsx | |
| parent | 8361916ce67f3d2ba191ebf27cb50e79cb42e39c (diff) | |
| download | soryu-cf0a25af1d2834bfe6c5ea892ce5769936e5a673.tar.gz soryu-cf0a25af1d2834bfe6c5ea892ce5769936e5a673.zip | |
Add makima chain mechanism
Diffstat (limited to 'makima/frontend/src/components/chains/ChainList.tsx')
| -rw-r--r-- | makima/frontend/src/components/chains/ChainList.tsx | 205 |
1 files changed, 205 insertions, 0 deletions
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<ChainStatus | "all">("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 ( + <svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor"> + <circle cx="12" cy="12" r="4" /> + </svg> + ); + case "completed": + return ( + <svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3"> + <polyline points="20 6 9 17 4 12" /> + </svg> + ); + case "archived": + return ( + <svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> + <path d="M21 8v13H3V8" /> + <path d="M1 3h22v5H1z" /> + <path d="M10 12h4" /> + </svg> + ); + default: + return null; + } + }; + + return ( + <div className="panel h-full flex flex-col" onClick={closeContextMenu}> + {/* Header */} + <div className="p-3 border-b border-[rgba(117,170,252,0.2)]"> + <div className="flex items-center justify-between mb-3"> + <h2 className="font-mono text-sm text-[#75aafc] uppercase">Chains</h2> + <button + onClick={onCreate} + className="px-3 py-1 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors" + > + + New + </button> + </div> + + {/* Status filter */} + <div className="flex gap-1"> + {(["all", "active", "completed", "archived"] as const).map((status) => ( + <button + key={status} + onClick={() => setStatusFilter(status)} + className={`px-2 py-1 font-mono text-[10px] uppercase transition-colors ${ + statusFilter === status + ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]" + : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]" + }`} + > + {status} + </button> + ))} + </div> + </div> + + {/* Chain list */} + <div className="flex-1 overflow-y-auto"> + {loading ? ( + <div className="flex items-center justify-center h-32"> + <p className="font-mono text-xs text-[#8b949e]">Loading chains...</p> + </div> + ) : filteredChains.length === 0 ? ( + <div className="flex items-center justify-center h-32"> + <p className="font-mono text-xs text-[#8b949e]"> + {statusFilter === "all" ? "No chains yet" : `No ${statusFilter} chains`} + </p> + </div> + ) : ( + <div className="divide-y divide-[rgba(117,170,252,0.1)]"> + {filteredChains.map((chain) => ( + <div + key={chain.id} + onClick={() => 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)]" + }`} + > + <div className="flex items-center justify-between mb-1"> + <span className="font-mono text-sm text-[#dbe7ff] truncate"> + {chain.name} + </span> + <span + className={`flex items-center gap-1 px-1.5 py-0.5 font-mono text-[10px] uppercase rounded ${getStatusColor( + chain.status + )}`} + > + {getStatusIcon(chain.status)} + {chain.status} + </span> + </div> + {chain.description && ( + <p className="font-mono text-xs text-[#8b949e] truncate mb-1"> + {chain.description} + </p> + )} + <div className="flex items-center gap-3 font-mono text-[10px] text-[#556677]"> + <span>{chain.contractCount} contracts</span> + <span> + {new Date(chain.updatedAt).toLocaleDateString()} + </span> + </div> + </div> + ))} + </div> + )} + </div> + + {/* Context menu */} + {contextMenu && ( + <div + className="fixed z-50 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-lg py-1" + style={{ top: contextMenu.y, left: contextMenu.x }} + > + <button + onClick={() => { + onSelect(contextMenu.chain.id); + setContextMenu(null); + }} + className="w-full px-4 py-2 text-left font-mono text-xs text-[#dbe7ff] hover:bg-[rgba(117,170,252,0.1)]" + > + View Details + </button> + {contextMenu.chain.status !== "archived" && ( + <button + onClick={handleArchive} + className="w-full px-4 py-2 text-left font-mono text-xs text-red-400 hover:bg-red-400/10" + > + Archive + </button> + )} + </div> + )} + </div> + ); +} |
