import { useState, useCallback, useEffect } from "react";
import { useParams, useNavigate } from "react-router";
import { Masthead } from "../components/Masthead";
import { ChainList } from "../components/chains/ChainList";
import { ChainEditor } from "../components/chains/ChainEditor";
import { useChains } from "../hooks/useChains";
import { useAuth } from "../contexts/AuthContext";
import type {
ChainSummary,
ChainWithContracts,
ChainGraphResponse,
CreateChainRequest,
AddChainRepositoryRequest,
RepositoryHistoryEntry,
} from "../lib/api";
import { getRepositorySuggestions } from "../lib/api";
export default function ChainsPage() {
const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
const navigate = useNavigate();
// Redirect to login if not authenticated (when auth is configured)
useEffect(() => {
if (!authLoading && isAuthConfigured && !isAuthenticated) {
navigate("/login");
}
}, [authLoading, isAuthConfigured, isAuthenticated, navigate]);
// Show loading while checking auth
if (authLoading) {
return (
<div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
<Masthead showNav />
<main className="flex-1 flex items-center justify-center">
<p className="text-[#7788aa] font-mono text-sm">Loading...</p>
</main>
</div>
);
}
// Don't render if not authenticated (will redirect)
if (isAuthConfigured && !isAuthenticated) {
return null;
}
return <ChainsPageContent />;
}
function ChainsPageContent() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const {
chains,
loading,
error,
createNewChain,
archiveExistingChain,
getChainById,
getGraph,
} = useChains();
const [chainDetail, setChainDetail] = useState<ChainWithContracts | null>(null);
const [chainGraph, setChainGraph] = useState<ChainGraphResponse | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
const [isCreating, setIsCreating] = useState(false);
// Load chain detail when ID changes
useEffect(() => {
if (id) {
setDetailLoading(true);
Promise.all([getChainById(id), getGraph(id)]).then(([chain, graph]) => {
setChainDetail(chain);
setChainGraph(graph);
setDetailLoading(false);
});
} else {
setChainDetail(null);
setChainGraph(null);
}
}, [id, getChainById, getGraph]);
const handleSelect = useCallback(
(chainId: string) => {
navigate(`/chains/${chainId}`);
},
[navigate]
);
const handleBack = useCallback(() => {
navigate("/chains");
}, [navigate]);
const handleCreate = useCallback(() => {
setIsCreating(true);
}, []);
const handleCreateSubmit = useCallback(
async (name: string, description: string, repositories: AddChainRepositoryRequest[]) => {
const data: CreateChainRequest = {
name: name.trim(),
description: description.trim() || undefined,
repositories: repositories.length > 0 ? repositories : undefined,
};
try {
const result = await createNewChain(data);
if (result) {
setIsCreating(false);
navigate(`/chains/${result.id}`);
}
} catch (err) {
console.error("Failed to create chain:", err);
}
},
[createNewChain, navigate]
);
const handleCreateCancel = useCallback(() => {
setIsCreating(false);
}, []);
const handleArchive = useCallback(
async (chain: ChainSummary) => {
if (confirm(`Are you sure you want to archive "${chain.name}"?`)) {
const success = await archiveExistingChain(chain.id);
if (success && chain.id === id) {
navigate("/chains");
}
}
},
[archiveExistingChain, id, navigate]
);
const handleRefresh = useCallback(async () => {
if (id) {
const [chain, graph] = await Promise.all([getChainById(id), getGraph(id)]);
setChainDetail(chain);
setChainGraph(graph);
}
}, [id, getChainById, getGraph]);
const handleContractClick = useCallback(
(contractId: string) => {
navigate(`/contracts/${contractId}`);
},
[navigate]
);
return (
<div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
<Masthead showNav />
<main className="flex-1 flex flex-col p-4 pt-2 gap-4 overflow-hidden">
{error && (
<div className="p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-sm">
{error}
</div>
)}
{/* Create chain modal */}
{isCreating && (
<CreateChainModal
onSubmit={handleCreateSubmit}
onCancel={handleCreateCancel}
/>
)}
<div className="flex-1 grid grid-cols-[350px_1fr] gap-4 min-h-0">
{/* Chain list */}
<ChainList
chains={chains}
loading={loading}
onSelect={handleSelect}
onCreate={handleCreate}
selectedId={id}
onArchive={handleArchive}
/>
{/* Chain detail/editor or empty state */}
{chainDetail ? (
<ChainEditor
chain={chainDetail}
graph={chainGraph}
loading={detailLoading}
onBack={handleBack}
onRefresh={handleRefresh}
onContractClick={handleContractClick}
/>
) : (
<div className="panel h-full flex items-center justify-center">
<div className="text-center">
<p className="font-mono text-sm text-[#555] mb-4">
Select a chain or create a new one
</p>
<button
onClick={handleCreate}
className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
>
+ New Chain
</button>
</div>
</div>
)}
</div>
</main>
</div>
);
}
interface CreateChainModalProps {
onSubmit: (name: string, description: string, repositories: AddChainRepositoryRequest[]) => void;
onCancel: () => void;
}
type RepoMode = "remote" | "local" | null;
function CreateChainModal({ onSubmit, onCancel }: CreateChainModalProps) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [repositories, setRepositories] = useState<AddChainRepositoryRequest[]>([]);
// Repository input state
const [repoMode, setRepoMode] = useState<RepoMode>(null);
const [repoName, setRepoName] = useState("");
const [repoUrl, setRepoUrl] = useState("");
const [repoPath, setRepoPath] = useState("");
// Suggestions
const [suggestions, setSuggestions] = useState<RepositoryHistoryEntry[]>([]);
const [showSuggestions, setShowSuggestions] = useState(false);
// Load suggestions when mode changes
useEffect(() => {
if (repoMode) {
getRepositorySuggestions(repoMode, undefined, 10)
.then((res) => {
setSuggestions(res.entries);
setShowSuggestions(res.entries.length > 0);
})
.catch(() => {
setSuggestions([]);
setShowSuggestions(false);
});
} else {
setSuggestions([]);
setShowSuggestions(false);
}
}, [repoMode]);
const applySuggestion = (suggestion: RepositoryHistoryEntry) => {
setRepoName(suggestion.name);
if (suggestion.repositoryUrl) setRepoUrl(suggestion.repositoryUrl);
if (suggestion.localPath) setRepoPath(suggestion.localPath);
setShowSuggestions(false);
};
const handleAddRepo = () => {
if (!repoName.trim()) return;
if (repoMode === "remote" && !repoUrl.trim()) return;
if (repoMode === "local" && !repoPath.trim()) return;
const newRepo: AddChainRepositoryRequest = {
name: repoName.trim(),
sourceType: repoMode || "remote",
isPrimary: repositories.length === 0, // First one is primary
...(repoMode === "remote" ? { repositoryUrl: repoUrl.trim() } : { localPath: repoPath.trim() }),
};
setRepositories([...repositories, newRepo]);
setRepoMode(null);
setRepoName("");
setRepoUrl("");
setRepoPath("");
};
const handleRemoveRepo = (index: number) => {
const newRepos = repositories.filter((_, i) => i !== index);
// If we removed the primary, make the first one primary
if (newRepos.length > 0 && repositories[index]?.isPrimary) {
newRepos[0].isPrimary = true;
}
setRepositories(newRepos);
};
const handleSubmit = () => {
if (name.trim()) {
onSubmit(name.trim(), description.trim(), repositories);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] max-h-[90vh] overflow-y-auto">
<h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4">
Create Chain
</h3>
<div className="space-y-4">
{/* Chain name */}
<div>
<label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
Chain Name *
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., Feature Implementation"
className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
autoFocus
/>
</div>
{/* Description */}
<div>
<label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
Description (optional)
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Describe what this chain accomplishes..."
rows={2}
className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none"
/>
</div>
{/* Repositories */}
<div>
<label className="block font-mono text-xs text-[#8b949e] uppercase mb-2">
Repositories
</label>
{/* Added repositories */}
{repositories.length > 0 && (
<div className="space-y-2 mb-3">
{repositories.map((repo, index) => (
<div
key={index}
className="flex items-center gap-2 px-3 py-2 bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)]"
>
<span className="font-mono text-[10px] text-[#556677] uppercase">
{repo.sourceType === "remote" ? "URL" : "Local"}
</span>
<span className="font-mono text-xs text-[#dbe7ff] flex-1 truncate">
{repo.name}
</span>
{repo.isPrimary && (
<span className="font-mono text-[8px] text-[#75aafc] uppercase px-1 border border-[#75aafc]/30">
primary
</span>
)}
<button
onClick={() => handleRemoveRepo(index)}
className="font-mono text-xs text-[#556677] hover:text-red-400"
>
✕
</button>
</div>
))}
</div>
)}
{/* Add repository form */}
{repoMode ? (
<div className="p-3 border border-[rgba(117,170,252,0.3)] space-y-3">
<div className="flex items-center justify-between">
<span className="font-mono text-[10px] text-[#75aafc] uppercase">
Add {repoMode === "remote" ? "Remote" : "Local"} Repository
</span>
{suggestions.length > 0 && (
<button
onClick={() => setShowSuggestions(!showSuggestions)}
className="font-mono text-[10px] text-[#556677] hover:text-[#9bc3ff]"
>
{showSuggestions ? "Hide" : `${suggestions.length} suggestions`}
</button>
)}
</div>
{/* Suggestions dropdown */}
{showSuggestions && suggestions.length > 0 && (
<div className="border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-28 overflow-y-auto">
{suggestions.map((s) => (
<button
key={s.id}
onClick={() => applySuggestion(s)}
className="w-full text-left px-3 py-2 font-mono text-xs hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0"
>
<div className="flex items-center justify-between">
<span className="text-[#9bc3ff] truncate">{s.name}</span>
<span className="text-[10px] text-[#556677]">{s.useCount}×</span>
</div>
<div className="text-[10px] text-[#556677] truncate">
{repoMode === "local" ? s.localPath : s.repositoryUrl}
</div>
</button>
))}
</div>
)}
<input
type="text"
value={repoName}
onChange={(e) => setRepoName(e.target.value)}
placeholder="Repository name"
className="w-full px-3 py-2 bg-[#050d18] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]"
/>
{repoMode === "remote" ? (
<input
type="text"
value={repoUrl}
onChange={(e) => setRepoUrl(e.target.value)}
placeholder="https://github.com/owner/repo"
className="w-full px-3 py-2 bg-[#050d18] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]"
/>
) : (
<input
type="text"
value={repoPath}
onChange={(e) => setRepoPath(e.target.value)}
placeholder="/path/to/repository"
className="w-full px-3 py-2 bg-[#050d18] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]"
/>
)}
<div className="flex gap-2">
<button
onClick={() => setRepoMode(null)}
className="px-3 py-1.5 font-mono text-xs text-[#556677] hover:text-[#9bc3ff]"
>
Cancel
</button>
<button
onClick={handleAddRepo}
disabled={
!repoName.trim() ||
(repoMode === "remote" && !repoUrl.trim()) ||
(repoMode === "local" && !repoPath.trim())
}
className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] disabled:opacity-50"
>
Add
</button>
</div>
</div>
) : (
<div className="flex gap-2">
<button
onClick={() => setRepoMode("remote")}
className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3]"
>
+ Remote
</button>
<button
onClick={() => setRepoMode("local")}
className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3]"
>
+ Local
</button>
</div>
)}
{repositories.length === 0 && !repoMode && (
<p className="font-mono text-[10px] text-[#556677] mt-2">
Add repositories that contracts in this chain will work with
</p>
)}
</div>
<p className="font-mono text-xs text-[#8b949e]">
A chain links multiple contracts together in a DAG. Contracts depend on each
other and start automatically when dependencies complete.
</p>
{/* Actions */}
<div className="flex gap-2 justify-end pt-2">
<button
onClick={onCancel}
className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={!name.trim()}
className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Create
</button>
</div>
</div>
</div>
</div>
);
}