summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes/templates.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-02 02:34:50 +0000
committersoryu <soryu@soryu.co>2026-02-02 02:34:50 +0000
commit151e9d87e117b7980e6aad522ac8f3633eeca87a (patch)
treee80fb4301361b3b12e5abf8e442603db2d0622dc /makima/frontend/src/routes/templates.tsx
parenta2c147ddd59f55a07b5be0c8970169726b55c876 (diff)
downloadsoryu-151e9d87e117b7980e6aad522ac8f3633eeca87a.tar.gz
soryu-151e9d87e117b7980e6aad522ac8f3633eeca87a.zip
Make makima more opinionated and structured
Diffstat (limited to 'makima/frontend/src/routes/templates.tsx')
-rw-r--r--makima/frontend/src/routes/templates.tsx388
1 files changed, 0 insertions, 388 deletions
diff --git a/makima/frontend/src/routes/templates.tsx b/makima/frontend/src/routes/templates.tsx
deleted file mode 100644
index b2c9974..0000000
--- a/makima/frontend/src/routes/templates.tsx
+++ /dev/null
@@ -1,388 +0,0 @@
-import { useState, useEffect, useCallback } 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";
-import {
- listContractTypes,
- createContractTemplate,
- updateContractTemplate,
- deleteContractTemplate,
- type PhaseDefinition,
- type DeliverableDefinition,
-} from "../lib/api";
-
-export default function TemplatesPage() {
- const navigate = useNavigate();
- const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
-
- const [templates, setTemplates] = useState<ContractTemplate[]>(DEFAULT_TEMPLATES);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState<string | null>(null);
- const [saving, setSaving] = useState(false);
-
- 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]);
-
- // Fetch templates from API
- const fetchTemplates = useCallback(async () => {
- try {
- setLoading(true);
- setError(null);
- const response = await listContractTypes();
-
- // Convert API response to ContractTemplate format
- const apiTemplates: ContractTemplate[] = response.contractTypes.map((t) => ({
- id: t.id,
- name: t.name,
- description: t.description,
- isBuiltIn: t.isBuiltin,
- phases: t.phases.map((phaseId) => ({
- id: phaseId,
- name: t.phaseNames?.[phaseId] || phaseId.charAt(0).toUpperCase() + phaseId.slice(1),
- deliverables: [], // Deliverables are managed server-side
- })),
- }));
-
- // Merge with DEFAULT_TEMPLATES to ensure we have full phase/deliverable info for built-ins
- const mergedTemplates = apiTemplates.map((apiTemplate) => {
- const defaultTemplate = DEFAULT_TEMPLATES.find((d) => d.id === apiTemplate.id);
- if (defaultTemplate && apiTemplate.isBuiltIn) {
- return defaultTemplate; // Use the richer default template for built-ins
- }
- return apiTemplate;
- });
-
- setTemplates(mergedTemplates);
- } catch (err) {
- console.error("Failed to fetch templates:", err);
- setError(err instanceof Error ? err.message : "Failed to fetch templates");
- // Fall back to default templates
- setTemplates(DEFAULT_TEMPLATES);
- } finally {
- setLoading(false);
- }
- }, []);
-
- useEffect(() => {
- if (!authLoading && isAuthenticated) {
- fetchTemplates();
- } else if (!authLoading && !isAuthConfigured) {
- // No auth configured, just show defaults
- setTemplates(DEFAULT_TEMPLATES);
- setLoading(false);
- }
- }, [authLoading, isAuthenticated, isAuthConfigured, fetchTemplates]);
-
- const handleSaveTemplate = async (updatedTemplate: ContractTemplate) => {
- if (updatedTemplate.isBuiltIn) {
- // Built-in templates are read-only, just close the editor
- setEditingTemplate(null);
- return;
- }
-
- try {
- setSaving(true);
- setError(null);
-
- // Convert to API format
- const phases: PhaseDefinition[] = updatedTemplate.phases.map((p, index) => ({
- id: p.id,
- name: p.name,
- order: index,
- }));
-
- const deliverables: Record<string, DeliverableDefinition[]> = {};
- for (const phase of updatedTemplate.phases) {
- if (phase.deliverables.length > 0) {
- deliverables[phase.id] = phase.deliverables.map((d) => ({
- id: d.id,
- name: d.name,
- priority: "required" as const,
- }));
- }
- }
-
- await updateContractTemplate(updatedTemplate.id, {
- name: updatedTemplate.name,
- description: updatedTemplate.description,
- phases,
- defaultPhase: phases[0]?.id || "execute",
- deliverables: Object.keys(deliverables).length > 0 ? deliverables : undefined,
- });
-
- // Refresh templates from server
- await fetchTemplates();
- setEditingTemplate(null);
- } catch (err) {
- console.error("Failed to update template:", err);
- setError(err instanceof Error ? err.message : "Failed to update template");
- } finally {
- setSaving(false);
- }
- };
-
- const handleCreateTemplate = async () => {
- if (!newTemplateName.trim()) return;
-
- try {
- setSaving(true);
- setError(null);
-
- const phases: PhaseDefinition[] = [
- { id: "execute", name: "Execute", order: 0 },
- ];
-
- await createContractTemplate({
- name: newTemplateName.trim(),
- description: newTemplateDescription.trim() || "Custom contract template",
- phases,
- defaultPhase: "execute",
- });
-
- // Refresh templates from server
- await fetchTemplates();
-
- setNewTemplateName("");
- setNewTemplateDescription("");
- setShowNewTemplateForm(false);
- } catch (err) {
- console.error("Failed to create template:", err);
- setError(err instanceof Error ? err.message : "Failed to create template");
- } finally {
- setSaving(false);
- }
- };
-
- const handleDeleteTemplate = async (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}"?`)) {
- try {
- setSaving(true);
- setError(null);
- await deleteContractTemplate(templateId);
- await fetchTemplates();
- } catch (err) {
- console.error("Failed to delete template:", err);
- setError(err instanceof Error ? err.message : "Failed to delete template");
- } finally {
- setSaving(false);
- }
- }
- };
-
- const handleRefresh = () => {
- fetchTemplates();
- };
-
- // Show loading state
- if (authLoading || loading) {
- 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)}
- readOnly={editingTemplate.isBuiltIn}
- />
- </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">
- {/* Error display */}
- {error && (
- <div className="mb-4 p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-xs">
- {error}
- <button
- type="button"
- onClick={() => setError(null)}
- className="ml-2 text-red-400/70 hover:text-red-400"
- >
- Dismiss
- </button>
- </div>
- )}
-
- {/* 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={handleRefresh}
- disabled={saving}
- 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 disabled:opacity-50"
- >
- Refresh
- </button>
- <button
- type="button"
- onClick={() => setShowNewTemplateForm(true)}
- disabled={saving}
- 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 disabled:opacity-50"
- >
- + 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)}
- disabled={saving}
- />
- <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)}
- disabled={saving}
- />
- <button
- type="button"
- onClick={handleCreateTemplate}
- disabled={saving || !newTemplateName.trim()}
- 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 disabled:opacity-50"
- >
- {saving ? "Creating..." : "Create"}
- </button>
- <button
- type="button"
- onClick={() => setShowNewTemplateForm(false)}
- disabled={saving}
- 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 disabled:opacity-50"
- >
- 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)}
- disabled={saving}
- 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 disabled:opacity-50"
- >
- {template.isBuiltIn ? "View" : "Edit"}
- </button>
- {!template.isBuiltIn && (
- <button
- type="button"
- onClick={() => handleDeleteTemplate(template.id)}
- disabled={saving}
- 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 disabled:opacity-50"
- >
- Delete
- </button>
- )}
- </div>
- </div>
- ))}
- </div>
- </main>
- </div>
- );
-}