summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/chains/ChainList.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-03 22:01:29 +0000
committersoryu <soryu@soryu.co>2026-02-03 22:01:37 +0000
commitcf0a25af1d2834bfe6c5ea892ce5769936e5a673 (patch)
tree476ba326ac1752281a441b5c17d2b3be4b23a2a9 /makima/frontend/src/components/chains/ChainList.tsx
parent8361916ce67f3d2ba191ebf27cb50e79cb42e39c (diff)
downloadsoryu-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.tsx205
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>
+ );
+}