summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-07 01:11:26 +0000
committersoryu <soryu@soryu.co>2026-02-07 01:11:26 +0000
commit9e9f18884c78c21f5785908fb7ccd00e2fa5436b (patch)
treef2ca7c2a3db5350186282ae0be0e539aa77c0320 /makima/frontend/src/routes
parentb8d563d45f14a2b1db1f684aa0a8dcd7e5b6ad56 (diff)
downloadsoryu-9e9f18884c78c21f5785908fb7ccd00e2fa5436b.tar.gz
soryu-9e9f18884c78c21f5785908fb7ccd00e2fa5436b.zip
Add new directive initial implementation
Diffstat (limited to 'makima/frontend/src/routes')
-rw-r--r--makima/frontend/src/routes/directives.tsx209
1 files changed, 209 insertions, 0 deletions
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>
+ );
+}