summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-25 02:19:01 +0000
committersoryu <soryu@soryu.co>2026-01-25 02:19:01 +0000
commita4c5e9a601b49d08e5ef3d7a36cdd29372ce2003 (patch)
tree061a880c6ea2cd3bee2fa80137a2e7e3bf3ec6fb /makima/frontend/src/routes
parent1f223e55be79805bb1061213db4351925bc0b368 (diff)
parent2003544969e5b7248ecd242b5cec50b324fa751b (diff)
downloadsoryu-makima/files-under-contracts-combined.tar.gz
soryu-makima/files-under-contracts-combined.zip
Merge origin/master into makima/files-under-contracts-combined - resolve import conflictsmakima/files-under-contracts-combined
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/frontend/src/routes')
-rw-r--r--makima/frontend/src/routes/templates.tsx268
1 files changed, 268 insertions, 0 deletions
diff --git a/makima/frontend/src/routes/templates.tsx b/makima/frontend/src/routes/templates.tsx
new file mode 100644
index 0000000..15bf95c
--- /dev/null
+++ b/makima/frontend/src/routes/templates.tsx
@@ -0,0 +1,268 @@
+import { useState, useEffect } from "react";
+import { useNavigate } from "react-router";
+import { Masthead } from "../components/Masthead";
+import { TemplateEditor } from "../components/templates/TemplateEditor";
+import { useAuth } from "../contexts/AuthContext";
+import type { ContractTemplate } from "../types/templates";
+import { DEFAULT_TEMPLATES } from "../types/templates";
+
+const STORAGE_KEY = "makima_contract_templates";
+
+export default function TemplatesPage() {
+ const navigate = useNavigate();
+ const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
+
+ const [templates, setTemplates] = useState<ContractTemplate[]>(() => {
+ const saved = localStorage.getItem(STORAGE_KEY);
+ if (saved) {
+ try {
+ return JSON.parse(saved);
+ } catch {
+ return DEFAULT_TEMPLATES;
+ }
+ }
+ return DEFAULT_TEMPLATES;
+ });
+
+ const [editingTemplate, setEditingTemplate] = useState<ContractTemplate | null>(
+ null
+ );
+ const [showNewTemplateForm, setShowNewTemplateForm] = useState(false);
+ const [newTemplateName, setNewTemplateName] = useState("");
+ const [newTemplateDescription, setNewTemplateDescription] = useState("");
+
+ // Redirect to login if not authenticated
+ useEffect(() => {
+ if (!authLoading && isAuthConfigured && !isAuthenticated) {
+ navigate("/login");
+ }
+ }, [authLoading, isAuthConfigured, isAuthenticated, navigate]);
+
+ const saveTemplates = (newTemplates: ContractTemplate[]) => {
+ setTemplates(newTemplates);
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(newTemplates));
+ };
+
+ const handleSaveTemplate = (updatedTemplate: ContractTemplate) => {
+ const newTemplates = templates.map((t) =>
+ t.id === updatedTemplate.id ? updatedTemplate : t
+ );
+ saveTemplates(newTemplates);
+ setEditingTemplate(null);
+ };
+
+ const handleCreateTemplate = () => {
+ if (!newTemplateName.trim()) return;
+
+ const newTemplate: ContractTemplate = {
+ id: `custom-${Date.now()}`,
+ name: newTemplateName.trim(),
+ description: newTemplateDescription.trim() || "Custom contract template",
+ isBuiltIn: false,
+ phases: [
+ {
+ id: `phase-${Date.now()}`,
+ name: "Execute",
+ deliverables: [],
+ },
+ ],
+ };
+
+ saveTemplates([...templates, newTemplate]);
+ setNewTemplateName("");
+ setNewTemplateDescription("");
+ setShowNewTemplateForm(false);
+ };
+
+ const handleDeleteTemplate = (templateId: string) => {
+ const template = templates.find((t) => t.id === templateId);
+ if (template?.isBuiltIn) return;
+
+ if (window.confirm(`Are you sure you want to delete "${template?.name}"?`)) {
+ saveTemplates(templates.filter((t) => t.id !== templateId));
+ }
+ };
+
+ const handleResetToDefaults = () => {
+ if (
+ window.confirm(
+ "Reset all templates to defaults? This will remove any custom templates."
+ )
+ ) {
+ saveTemplates(DEFAULT_TEMPLATES);
+ }
+ };
+
+ // Show loading state
+ if (authLoading) {
+ return (
+ <div className="relative z-10 min-h-screen flex items-center justify-center bg-[#0a1628]">
+ <div className="text-[#75aafc] font-mono text-sm animate-pulse">
+ Loading...
+ </div>
+ </div>
+ );
+ }
+
+ // Editor view
+ if (editingTemplate) {
+ return (
+ <div className="relative z-10 min-h-screen bg-[#0a1628]">
+ <Masthead />
+ <main className="max-w-4xl mx-auto px-4 py-6">
+ <TemplateEditor
+ template={editingTemplate}
+ onSave={handleSaveTemplate}
+ onCancel={() => setEditingTemplate(null)}
+ />
+ </main>
+ </div>
+ );
+ }
+
+ return (
+ <div className="relative z-10 min-h-screen bg-[#0a1628]">
+ <Masthead />
+ <main className="max-w-6xl mx-auto px-4 py-6">
+ {/* Header */}
+ <div className="flex justify-between items-start mb-6 pb-4 border-b border-[rgba(117,170,252,0.15)]">
+ <div>
+ <h1 className="text-lg font-mono uppercase tracking-wide text-[#9bc3ff] mb-1">
+ Contract Templates
+ </h1>
+ <p className="text-xs font-mono text-[#75aafc] opacity-70">
+ Manage contract types and their phase deliverables
+ </p>
+ </div>
+ <div className="flex gap-3">
+ <button
+ type="button"
+ onClick={handleResetToDefaults}
+ className="px-3 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors"
+ >
+ Reset to Defaults
+ </button>
+ <button
+ type="button"
+ onClick={() => setShowNewTemplateForm(true)}
+ className="px-3 py-2 border border-[#3f6fb3] bg-[rgba(117,170,252,0.15)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:bg-[rgba(117,170,252,0.25)] transition-colors"
+ >
+ + New Template
+ </button>
+ </div>
+ </div>
+
+ {/* New Template Form */}
+ {showNewTemplateForm && (
+ <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] p-4 mb-6">
+ <div className="flex gap-3 items-center">
+ <input
+ type="text"
+ className="flex-1 px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]"
+ placeholder="Template name..."
+ value={newTemplateName}
+ onChange={(e) => setNewTemplateName(e.target.value)}
+ />
+ <input
+ type="text"
+ className="flex-1 px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]"
+ placeholder="Description (optional)..."
+ value={newTemplateDescription}
+ onChange={(e) => setNewTemplateDescription(e.target.value)}
+ />
+ <button
+ type="button"
+ onClick={handleCreateTemplate}
+ className="px-4 py-2 border border-[#3f6fb3] bg-[rgba(117,170,252,0.15)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:bg-[rgba(117,170,252,0.25)] transition-colors"
+ >
+ Create
+ </button>
+ <button
+ type="button"
+ onClick={() => setShowNewTemplateForm(false)}
+ className="px-4 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors"
+ >
+ Cancel
+ </button>
+ </div>
+ </div>
+ )}
+
+ {/* Templates Grid */}
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+ {templates.map((template) => (
+ <div
+ key={template.id}
+ className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.25)] hover:border-[rgba(117,170,252,0.45)] transition-colors"
+ >
+ {/* Card Header */}
+ <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.15)] flex items-center justify-between">
+ <h3 className="font-mono text-sm text-white">{template.name}</h3>
+ {template.isBuiltIn ? (
+ <span className="px-2 py-0.5 bg-[rgba(117,170,252,0.15)] text-[#75aafc] font-mono text-[10px] uppercase tracking-wide">
+ Built-in
+ </span>
+ ) : (
+ <span className="px-2 py-0.5 bg-[rgba(100,200,100,0.15)] text-[#7bc97b] font-mono text-[10px] uppercase tracking-wide">
+ Custom
+ </span>
+ )}
+ </div>
+
+ {/* Card Body */}
+ <div className="px-4 py-3">
+ <p className="text-xs font-mono text-[#75aafc] opacity-70 mb-4 min-h-[2.5rem]">
+ {template.description}
+ </p>
+
+ {/* Phases */}
+ <div className="space-y-2 mb-4">
+ {template.phases.map((phase, index) => (
+ <div key={phase.id} className="flex items-start gap-2">
+ <div className="flex flex-col items-center">
+ <span className="w-2 h-2 rounded-full bg-[#3f6fb3]" />
+ {index < template.phases.length - 1 && (
+ <span className="w-px h-4 bg-[rgba(117,170,252,0.25)]" />
+ )}
+ </div>
+ <div className="flex-1 min-w-0">
+ <span className="text-xs font-mono text-[#9bc3ff]">
+ {phase.name}
+ </span>
+ <span className="text-[10px] font-mono text-[#556677] ml-2">
+ {phase.deliverables.length === 0
+ ? "(no deliverables)"
+ : phase.deliverables.map((d) => d.name).join(", ")}
+ </span>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+
+ {/* Card Footer */}
+ <div className="px-4 py-3 border-t border-[rgba(117,170,252,0.15)] flex gap-2">
+ <button
+ type="button"
+ onClick={() => setEditingTemplate(template)}
+ className="flex-1 px-3 py-1.5 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors"
+ >
+ Edit
+ </button>
+ {!template.isBuiltIn && (
+ <button
+ type="button"
+ onClick={() => handleDeleteTemplate(template.id)}
+ className="px-3 py-1.5 border border-[rgba(255,100,100,0.25)] text-[#ff6464] font-mono text-xs uppercase tracking-wide hover:border-[rgba(255,100,100,0.5)] hover:bg-[rgba(255,100,100,0.05)] transition-colors"
+ >
+ Delete
+ </button>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ </main>
+ </div>
+ );
+}