From 9e9f18884c78c21f5785908fb7ccd00e2fa5436b Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Sat, 7 Feb 2026 01:11:26 +0000
Subject: Add new directive initial implementation

---
 makima/frontend/src/routes/directives.tsx | 209 ++++++++++++++++++++++++++++++
 1 file changed, 209 insertions(+)
 create mode 100644 makima/frontend/src/routes/directives.tsx

(limited to 'makima/frontend/src/routes/directives.tsx')

diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx
new file mode 100644
index 0000000..89535e2
--- /dev/null
+++ b/makima/frontend/src/routes/directives.tsx
@@ -0,0 +1,209 @@
+import { useState, useCallback, useEffect } from "react";
+import { useParams, useNavigate } from "react-router";
+import { Masthead } from "../components/Masthead";
+import { DirectiveList } from "../components/directives/DirectiveList";
+import { DirectiveDetail } from "../components/directives/DirectiveDetail";
+import { useDirectives } from "../hooks/useDirectives";
+import { useAuth } from "../contexts/AuthContext";
+import type {
+  DirectiveWithChains,
+  CreateDirectiveRequest,
+} from "../lib/api";
+
+export default function DirectivesPage() {
+  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]);
+
+  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>
+    );
+  }
+
+  return (
+    <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
+      <Masthead showNav />
+      <DirectivesContent />
+    </div>
+  );
+}
+
+function DirectivesContent() {
+  const { id } = useParams<{ id?: string }>();
+  const navigate = useNavigate();
+  const {
+    directives,
+    loading,
+    error,
+    fetchDirective,
+    saveDirective,
+    removeDirective,
+  } = useDirectives();
+
+  const [selectedDirective, setSelectedDirective] =
+    useState<DirectiveWithChains | null>(null);
+  const [detailLoading, setDetailLoading] = useState(false);
+  const [showCreateForm, setShowCreateForm] = useState(false);
+  const [createTitle, setCreateTitle] = useState("");
+  const [createGoal, setCreateGoal] = useState("");
+  const [createRepoUrl, setCreateRepoUrl] = useState("");
+
+  // Load directive when ID changes
+  useEffect(() => {
+    if (id) {
+      setDetailLoading(true);
+      fetchDirective(id).then((d) => {
+        setSelectedDirective(d);
+        setDetailLoading(false);
+      });
+    } else {
+      setSelectedDirective(null);
+    }
+  }, [id, fetchDirective]);
+
+  const handleSelect = useCallback(
+    (directiveId: string) => {
+      navigate(`/directives/${directiveId}`);
+    },
+    [navigate]
+  );
+
+  const handleBack = useCallback(() => {
+    navigate("/directives");
+  }, [navigate]);
+
+  const handleCreate = useCallback(async () => {
+    if (!createTitle.trim() || !createGoal.trim()) return;
+
+    const data: CreateDirectiveRequest = {
+      title: createTitle.trim(),
+      goal: createGoal.trim(),
+    };
+    if (createRepoUrl.trim()) {
+      data.repositoryUrl = createRepoUrl.trim();
+    }
+
+    const result = await saveDirective(data);
+    if (result) {
+      setShowCreateForm(false);
+      setCreateTitle("");
+      setCreateGoal("");
+      setCreateRepoUrl("");
+    }
+  }, [createTitle, createGoal, createRepoUrl, saveDirective]);
+
+  const handleDelete = useCallback(
+    async (directiveId: string) => {
+      const ok = await removeDirective(directiveId);
+      if (ok && id === directiveId) {
+        navigate("/directives");
+      }
+    },
+    [removeDirective, id, navigate]
+  );
+
+  // Detail view
+  if (id) {
+    if (detailLoading) {
+      return (
+        <main className="flex-1 flex items-center justify-center">
+          <p className="text-[#7788aa] font-mono text-sm">Loading directive...</p>
+        </main>
+      );
+    }
+    if (!selectedDirective) {
+      return (
+        <main className="flex-1 flex items-center justify-center">
+          <p className="text-[#7788aa] font-mono text-sm">Directive not found</p>
+        </main>
+      );
+    }
+    return (
+      <main className="flex-1 p-4 max-w-4xl mx-auto w-full">
+        <DirectiveDetail
+          directive={selectedDirective}
+          onBack={handleBack}
+          onDelete={handleDelete}
+        />
+      </main>
+    );
+  }
+
+  // List view
+  return (
+    <main className="flex-1 p-4 max-w-4xl mx-auto w-full">
+      {error && (
+        <div className="mb-4 p-2 border border-red-400/30 bg-red-400/5 font-mono text-xs text-red-400">
+          {error}
+        </div>
+      )}
+
+      {showCreateForm && (
+        <div className="mb-4 p-4 border border-dashed border-[rgba(117,170,252,0.35)] bg-[rgba(117,170,252,0.03)]">
+          <h3 className="font-mono text-xs text-[#75aafc] uppercase tracking-wider mb-3">
+            New Directive
+          </h3>
+          <div className="space-y-2">
+            <input
+              type="text"
+              placeholder="Title"
+              value={createTitle}
+              onChange={(e) => setCreateTitle(e.target.value)}
+              className="w-full px-2 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0a1628] border border-[rgba(117,170,252,0.3)] focus:border-[#75aafc] outline-none"
+            />
+            <textarea
+              placeholder="Goal - what should be accomplished?"
+              value={createGoal}
+              onChange={(e) => setCreateGoal(e.target.value)}
+              rows={3}
+              className="w-full px-2 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0a1628] border border-[rgba(117,170,252,0.3)] focus:border-[#75aafc] outline-none resize-none"
+            />
+            <input
+              type="text"
+              placeholder="Repository URL (optional)"
+              value={createRepoUrl}
+              onChange={(e) => setCreateRepoUrl(e.target.value)}
+              className="w-full px-2 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0a1628] border border-[rgba(117,170,252,0.3)] focus:border-[#75aafc] outline-none"
+            />
+            <div className="flex gap-2">
+              <button
+                onClick={handleCreate}
+                disabled={!createTitle.trim() || !createGoal.trim()}
+                className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase disabled:opacity-50 disabled:cursor-not-allowed"
+              >
+                Create
+              </button>
+              <button
+                onClick={() => setShowCreateForm(false)}
+                className="px-3 py-1.5 font-mono text-xs text-[#7788aa] hover:text-[#dbe7ff] transition-colors uppercase"
+              >
+                Cancel
+              </button>
+            </div>
+          </div>
+        </div>
+      )}
+
+      <DirectiveList
+        directives={directives}
+        loading={loading}
+        onSelect={handleSelect}
+        onCreate={() => setShowCreateForm(true)}
+        onDelete={(d) => handleDelete(d.id)}
+        selectedId={id}
+      />
+    </main>
+  );
+}
-- 
cgit v1.2.3

