summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes/workflow.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-11 05:52:14 +0000
committersoryu <soryu@soryu.co>2026-01-15 00:21:16 +0000
commit87044a747b47bd83249d61a45842c7f7b2eae56d (patch)
treeef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/frontend/src/routes/workflow.tsx
parent077820c4167c168072d217a1b01df840463a12a8 (diff)
downloadsoryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz
soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip
Contract system
Diffstat (limited to 'makima/frontend/src/routes/workflow.tsx')
-rw-r--r--makima/frontend/src/routes/workflow.tsx205
1 files changed, 205 insertions, 0 deletions
diff --git a/makima/frontend/src/routes/workflow.tsx b/makima/frontend/src/routes/workflow.tsx
new file mode 100644
index 0000000..cb72e9e
--- /dev/null
+++ b/makima/frontend/src/routes/workflow.tsx
@@ -0,0 +1,205 @@
+import { useState, useCallback, useEffect, useMemo } from "react";
+import { useNavigate } from "react-router";
+import { Masthead } from "../components/Masthead";
+import { WorkflowBoard } from "../components/workflow/WorkflowBoard";
+import { useContracts } from "../hooks/useContracts";
+import { useAuth } from "../contexts/AuthContext";
+import type { ContractPhase, ContractStatus } from "../lib/api";
+
+type StatusFilter = "all" | ContractStatus;
+
+export default function WorkflowPage() {
+ 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 <WorkflowPageContent />;
+}
+
+function WorkflowPageContent() {
+ const navigate = useNavigate();
+ const { contracts, loading, error, changePhase, saveContract } = useContracts();
+ const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
+ const [isCreating, setIsCreating] = useState(false);
+ const [newContractName, setNewContractName] = useState("");
+
+ // Filter contracts by status
+ const filteredContracts = useMemo(() => {
+ if (statusFilter === "all") {
+ return contracts;
+ }
+ return contracts.filter((c) => c.status === statusFilter);
+ }, [contracts, statusFilter]);
+
+ const handleContractClick = useCallback(
+ (contractId: string) => {
+ navigate(`/contracts/${contractId}`);
+ },
+ [navigate]
+ );
+
+ const handlePhaseChange = useCallback(
+ async (contractId: string, newPhase: ContractPhase) => {
+ await changePhase(contractId, newPhase);
+ },
+ [changePhase]
+ );
+
+ const handleCreateContract = useCallback(async () => {
+ if (!newContractName.trim()) return;
+ const contract = await saveContract({
+ name: newContractName.trim(),
+ });
+ if (contract) {
+ setNewContractName("");
+ setIsCreating(false);
+ navigate(`/contracts/${contract.id}`);
+ }
+ }, [newContractName, saveContract, navigate]);
+
+ const handleCancelCreate = useCallback(() => {
+ setNewContractName("");
+ setIsCreating(false);
+ }, []);
+
+ return (
+ <div className="relative z-10 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 shrink-0">
+ {error}
+ </div>
+ )}
+
+ {/* Header with filter and create button */}
+ <div className="flex items-center justify-between shrink-0">
+ <div className="flex items-center gap-4">
+ <h1 className="font-mono text-sm text-[#75aafc] uppercase tracking-wider">
+ Board
+ </h1>
+ {/* Status filter */}
+ <div className="flex items-center gap-1">
+ {(["all", "active", "completed", "archived"] as StatusFilter[]).map(
+ (status) => (
+ <button
+ key={status}
+ onClick={() => setStatusFilter(status)}
+ className={`
+ px-2 py-1 font-mono text-[10px] uppercase transition-colors
+ ${
+ statusFilter === status
+ ? "bg-[rgba(117,170,252,0.1)] text-[#9bc3ff] border border-[rgba(117,170,252,0.3)]"
+ : "text-[#555] border border-transparent hover:text-[#75aafc]"
+ }
+ `}
+ >
+ {status}
+ </button>
+ )
+ )}
+ </div>
+ </div>
+ <button
+ onClick={() => setIsCreating(true)}
+ className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors"
+ >
+ + New Contract
+ </button>
+ </div>
+
+ {/* Create contract modal */}
+ {isCreating && (
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
+ <div className="w-full max-w-md p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)]">
+ <h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4">
+ Create Contract
+ </h3>
+ <div className="space-y-4">
+ <input
+ type="text"
+ value={newContractName}
+ onChange={(e) => setNewContractName(e.target.value)}
+ placeholder="Contract name"
+ 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
+ onKeyDown={(e) => {
+ if (e.key === "Enter") handleCreateContract();
+ if (e.key === "Escape") handleCancelCreate();
+ }}
+ />
+ <div className="flex gap-2 justify-end">
+ <button
+ onClick={handleCancelCreate}
+ className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
+ >
+ Cancel
+ </button>
+ <button
+ onClick={handleCreateContract}
+ disabled={!newContractName.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>
+ )}
+
+ {/* Board */}
+ <div className="flex-1 min-h-0 overflow-hidden">
+ {loading ? (
+ <div className="h-full flex items-center justify-center">
+ <p className="font-mono text-sm text-[#555]">Loading...</p>
+ </div>
+ ) : filteredContracts.length === 0 && statusFilter === "all" ? (
+ <div className="h-full flex items-center justify-center">
+ <div className="text-center">
+ <p className="font-mono text-sm text-[#555] mb-4">
+ No contracts yet
+ </p>
+ <button
+ onClick={() => setIsCreating(true)}
+ className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
+ >
+ + Create First Contract
+ </button>
+ </div>
+ </div>
+ ) : (
+ <WorkflowBoard
+ contracts={filteredContracts}
+ onContractClick={handleContractClick}
+ onPhaseChange={handlePhaseChange}
+ />
+ )}
+ </div>
+ </main>
+ </div>
+ );
+}