summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/chains/ChainList.tsx
blob: eda79d7292ef7cbeb6e4a1a9ea9f76ecfad6123b (plain) (tree)












































































































































































































                                                                                                                                            
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>
  );
}