diff options
| author | soryu <soryu@soryu.co> | 2026-02-02 02:34:50 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-02 02:34:50 +0000 |
| commit | 151e9d87e117b7980e6aad522ac8f3633eeca87a (patch) | |
| tree | e80fb4301361b3b12e5abf8e442603db2d0622dc /makima/frontend/src/routes/templates.tsx | |
| parent | a2c147ddd59f55a07b5be0c8970169726b55c876 (diff) | |
| download | soryu-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.tsx | 388 |
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> - ); -} |
