diff options
| author | soryu <soryu@soryu.co> | 2026-01-24 15:50:43 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-24 16:17:59 +0000 |
| commit | 595548db950eca303a7d73ca09f31895d291534f (patch) | |
| tree | ad6317979526e6a683fa6987bc9979ec3ac055f3 /frontend/src/components | |
| parent | abc5fbed331ea527ccaac0cd4120c4a0650f8bc0 (diff) | |
| download | soryu-595548db950eca303a7d73ca09f31895d291534f.tar.gz soryu-595548db950eca303a7d73ca09f31895d291534f.zip | |
feat(frontend): Add Templates page for managing contract templates
- Add NavStrip component for top navigation between pages
- Create Templates page with template card grid layout
- Create TemplateEditor component for editing phase deliverables
- Add navigation state management in stores
- Implement three built-in templates:
- Simple: Plan phase (Plan deliverable), Execute phase (PR deliverable)
- Specification: Research, Specify, Plan, Execute, Review phases with deliverables
- Execute: Single execute phase with no deliverables
- Support for creating custom templates with configurable phases/deliverables
- Templates are persisted to localStorage
- Add comprehensive CSS styling matching existing design patterns
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
[WIP] Heartbeat checkpoint - 2026-01-24 16:13:00 UTC
[WIP] Heartbeat checkpoint - 2026-01-24 16:14:29 UTC
Diffstat (limited to 'frontend/src/components')
| -rw-r--r-- | frontend/src/components/NavStrip.tsx | 30 | ||||
| -rw-r--r-- | frontend/src/components/VNInterface.tsx | 40 | ||||
| -rw-r--r-- | frontend/src/components/templates/TemplateEditor.tsx | 185 |
3 files changed, 252 insertions, 3 deletions
diff --git a/frontend/src/components/NavStrip.tsx b/frontend/src/components/NavStrip.tsx new file mode 100644 index 0000000..c0fc000 --- /dev/null +++ b/frontend/src/components/NavStrip.tsx @@ -0,0 +1,30 @@ +import React from 'react' + +type NavItem = { + id: string + label: string + icon?: string +} + +type Props = { + items: NavItem[] + activeId: string + onSelect: (id: string) => void +} + +export const NavStrip: React.FC<Props> = ({ items, activeId, onSelect }) => { + return ( + <nav className="nav-strip"> + {items.map((item) => ( + <button + key={item.id} + className={`nav-strip-item ${activeId === item.id ? 'active' : ''}`} + onClick={() => onSelect(item.id)} + > + {item.icon && <span className="nav-icon">{item.icon}</span>} + <span className="nav-label">{item.label}</span> + </button> + ))} + </nav> + ) +} diff --git a/frontend/src/components/VNInterface.tsx b/frontend/src/components/VNInterface.tsx index be71d27..bdc7db0 100644 --- a/frontend/src/components/VNInterface.tsx +++ b/frontend/src/components/VNInterface.tsx @@ -8,15 +8,24 @@ import { showSettingsModalStore, isVisibleStore, yenBalanceStore, + currentPageStore, toggleStandby, toggleShowChoices, - updateTime + updateTime, + navigateTo } from '../stores' +import { NavStrip } from './NavStrip' +import { TemplatesPage } from '../pages/templates' interface VNInterfaceProps { onLogout: () => void } +const NAV_ITEMS = [ + { id: 'main', label: 'Main', icon: '>' }, + { id: 'templates', label: 'Templates', icon: '>' } +] + export function VNInterface({ onLogout }: VNInterfaceProps) { const isStandby = useStore(isStandbyStore) const currentTime = useStore(currentTimeStore) @@ -25,6 +34,7 @@ export function VNInterface({ onLogout }: VNInterfaceProps) { const showSettingsModal = useStore(showSettingsModalStore) const isVisible = useStore(isVisibleStore) const yenBalance = useStore(yenBalanceStore) + const currentPage = useStore(currentPageStore) // Fade in effect on mount useEffect(() => { @@ -45,12 +55,36 @@ export function VNInterface({ onLogout }: VNInterfaceProps) { return () => clearInterval(timer) }, []) + // Show Templates page if on templates route + if (currentPage === 'templates') { + return ( + <div className={`vn-interface ${isVisible ? 'fade-in' : 'fade-out'}`}> + <div className="vn-background"> + <div className="background-overlay"></div> + </div> + <NavStrip + items={NAV_ITEMS} + activeId={currentPage} + onSelect={(id) => navigateTo(id as 'main' | 'templates')} + /> + <TemplatesPage onClose={() => navigateTo('main')} /> + </div> + ) + } + return ( <div className={`vn-interface ${isVisible ? 'fade-in' : 'fade-out'}`}> + {/* Navigation Strip */} + <NavStrip + items={NAV_ITEMS} + activeId={currentPage} + onSelect={(id) => navigateTo(id as 'main' | 'templates')} + /> + {/* Background */} <div className="vn-background"> - <img - src="/__gaogao__56242cbde8f18ac64522e410bad04e68_waifu2x_art_noise2.png" + <img + src="/__gaogao__56242cbde8f18ac64522e410bad04e68_waifu2x_art_noise2.png" alt="Background image" className="background-image" /> diff --git a/frontend/src/components/templates/TemplateEditor.tsx b/frontend/src/components/templates/TemplateEditor.tsx new file mode 100644 index 0000000..612cac0 --- /dev/null +++ b/frontend/src/components/templates/TemplateEditor.tsx @@ -0,0 +1,185 @@ +import React, { useState } from 'react' +import { ContractTemplate, Phase, Deliverable } from '../../types' + +type Props = { + template: ContractTemplate + onSave: (template: ContractTemplate) => void + onCancel: () => void +} + +export const TemplateEditor: React.FC<Props> = ({ template, onSave, onCancel }) => { + const [editedTemplate, setEditedTemplate] = useState<ContractTemplate>({ ...template, phases: template.phases.map(p => ({ ...p, deliverables: [...p.deliverables] })) }) + const [newDeliverableName, setNewDeliverableName] = useState<{ [phaseId: string]: string }>({}) + + const handlePhaseNameChange = (phaseId: string, newName: string) => { + setEditedTemplate(prev => ({ + ...prev, + phases: prev.phases.map(p => + p.id === phaseId ? { ...p, name: newName } : p + ) + })) + } + + const handleDeliverableNameChange = (phaseId: string, deliverableId: string, newName: string) => { + setEditedTemplate(prev => ({ + ...prev, + phases: prev.phases.map(p => + p.id === phaseId + ? { + ...p, + deliverables: p.deliverables.map(d => + d.id === deliverableId ? { ...d, name: newName } : d + ) + } + : p + ) + })) + } + + const handleAddDeliverable = (phaseId: string) => { + const name = newDeliverableName[phaseId]?.trim() + if (!name) return + + const newDeliverable: Deliverable = { + id: `deliverable-${Date.now()}`, + name + } + + setEditedTemplate(prev => ({ + ...prev, + phases: prev.phases.map(p => + p.id === phaseId + ? { ...p, deliverables: [...p.deliverables, newDeliverable] } + : p + ) + })) + setNewDeliverableName(prev => ({ ...prev, [phaseId]: '' })) + } + + const handleRemoveDeliverable = (phaseId: string, deliverableId: string) => { + setEditedTemplate(prev => ({ + ...prev, + phases: prev.phases.map(p => + p.id === phaseId + ? { ...p, deliverables: p.deliverables.filter(d => d.id !== deliverableId) } + : p + ) + })) + } + + const handleAddPhase = () => { + const newPhase: Phase = { + id: `phase-${Date.now()}`, + name: 'New Phase', + deliverables: [] + } + setEditedTemplate(prev => ({ + ...prev, + phases: [...prev.phases, newPhase] + })) + } + + const handleRemovePhase = (phaseId: string) => { + setEditedTemplate(prev => ({ + ...prev, + phases: prev.phases.filter(p => p.id !== phaseId) + })) + } + + return ( + <div className="template-editor"> + <div className="template-editor-header"> + <h2 className="template-editor-title">Edit Template: {template.name}</h2> + <p className="template-editor-description">{template.description}</p> + </div> + + <div className="template-editor-content"> + <div className="phases-list"> + {editedTemplate.phases.map((phase, phaseIndex) => ( + <div key={phase.id} className="phase-card"> + <div className="phase-header"> + <span className="phase-number">{phaseIndex + 1}</span> + <input + type="text" + className="phase-name-input" + value={phase.name} + onChange={(e) => handlePhaseNameChange(phase.id, e.target.value)} + placeholder="Phase name" + /> + {!template.isBuiltIn && ( + <button + className="phase-remove-btn" + onClick={() => handleRemovePhase(phase.id)} + title="Remove phase" + > + x + </button> + )} + </div> + + <div className="deliverables-list"> + {phase.deliverables.length === 0 ? ( + <div className="no-deliverables">No deliverables</div> + ) : ( + phase.deliverables.map(deliverable => ( + <div key={deliverable.id} className="deliverable-item"> + <input + type="text" + className="deliverable-name-input" + value={deliverable.name} + onChange={(e) => handleDeliverableNameChange(phase.id, deliverable.id, e.target.value)} + /> + <button + className="deliverable-remove-btn" + onClick={() => handleRemoveDeliverable(phase.id, deliverable.id)} + title="Remove deliverable" + > + x + </button> + </div> + )) + )} + + <div className="add-deliverable-row"> + <input + type="text" + className="add-deliverable-input" + placeholder="New deliverable name..." + value={newDeliverableName[phase.id] || ''} + onChange={(e) => setNewDeliverableName(prev => ({ ...prev, [phase.id]: e.target.value }))} + onKeyPress={(e) => { + if (e.key === 'Enter') { + handleAddDeliverable(phase.id) + } + }} + /> + <button + className="add-deliverable-btn" + onClick={() => handleAddDeliverable(phase.id)} + > + + Add + </button> + </div> + </div> + </div> + ))} + </div> + + {!template.isBuiltIn && ( + <button className="add-phase-btn" onClick={handleAddPhase}> + + Add Phase + </button> + )} + </div> + + <div className="template-editor-footer"> + <button className="modal-btn secondary" onClick={onCancel}> + Cancel + </button> + <button className="modal-btn primary" onClick={() => onSave(editedTemplate)}> + Save Changes + </button> + </div> + </div> + ) +} |
