diff options
| author | soryu <soryu@soryu.co> | 2026-02-07 01:11:26 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-07 01:11:26 +0000 |
| commit | 9e9f18884c78c21f5785908fb7ccd00e2fa5436b (patch) | |
| tree | f2ca7c2a3db5350186282ae0be0e539aa77c0320 /makima/frontend/src/routes | |
| parent | b8d563d45f14a2b1db1f684aa0a8dcd7e5b6ad56 (diff) | |
| download | soryu-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.tsx | 209 |
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> + ); +} |
