summaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-25 00:01:25 +0000
committerGitHub <noreply@github.com>2026-01-25 00:01:25 +0000
commita279ec29efb863fefd1ca82e5b490f2e8784cf3c (patch)
treeaf207e559e7eef5557b2229714384bf78c530976 /frontend/src
parent6364363d1418728351f252b799d397b756e1f985 (diff)
downloadsoryu-a279ec29efb863fefd1ca82e5b490f2e8784cf3c.tar.gz
soryu-a279ec29efb863fefd1ca82e5b490f2e8784cf3c.zip
Move files tab and file pages to be accessible via contracts (#27)
* feat: remove Files from top-level navigation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: update file links to use contract-scoped routes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add contract context to FileDetail component - Add contractId, contractName, and onContractClick props to FileDetailProps - Update breadcrumb navigation to show contract name with path separator when viewing file within a contract context - Fall back to "Back to list" when no contract context is provided - This enables the FileDetail component to be used within the /contracts/:contractId/files/:fileId route Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: update routes to nest files under contracts - Add react-router-dom for client-side routing - Create ContractList component to list all contracts - Create ContractDetail component with tabs (overview, files, tasks, repos) - Create FileDetail component to view individual files - Configure routes: - /contracts - list all contracts - /contracts/:id - view contract details with Files tab - /contracts/:contractId/files/:fileId - view file in contract context - Remove standalone file routes (/files, /files/:id) Files are now only accessible through their parent contract. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Task completion checkpoint * Task completion checkpoint * Task completion checkpoint * Task completion checkpoint * Task completion checkpoint * Task completion checkpoint * Task completion checkpoint * Task completion checkpoint --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/components/ContractDetail.tsx232
-rw-r--r--frontend/src/components/ContractList.tsx83
-rw-r--r--frontend/src/components/FileDetail.tsx97
-rw-r--r--frontend/src/main.tsx33
4 files changed, 444 insertions, 1 deletions
diff --git a/frontend/src/components/ContractDetail.tsx b/frontend/src/components/ContractDetail.tsx
new file mode 100644
index 0000000..72527ce
--- /dev/null
+++ b/frontend/src/components/ContractDetail.tsx
@@ -0,0 +1,232 @@
+import React, { useEffect, useState } from 'react'
+import { useParams, Link } from 'react-router-dom'
+
+interface FileSummary {
+ id: string
+ name: string
+ description?: string
+ contract_phase?: string
+}
+
+interface TaskSummary {
+ id: string
+ name: string
+ status: string
+}
+
+interface ContractRepository {
+ id: string
+ name: string
+ source_type: string
+ is_primary: boolean
+}
+
+interface Contract {
+ id: string
+ name: string
+ description?: string
+ contract_type: string
+ phase: string
+ status: string
+ version: number
+ created_at: string
+}
+
+interface ContractWithRelations {
+ contract: Contract
+ repositories: ContractRepository[]
+ files: FileSummary[]
+ tasks: TaskSummary[]
+}
+
+type Tab = 'overview' | 'files' | 'tasks' | 'repositories'
+
+export function ContractDetail() {
+ const { id } = useParams<{ id: string }>()
+ const [data, setData] = useState<ContractWithRelations | null>(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState<string | null>(null)
+ const [activeTab, setActiveTab] = useState<Tab>('overview')
+
+ useEffect(() => {
+ async function fetchContract() {
+ if (!id) return
+
+ try {
+ setLoading(true)
+ const response = await fetch(`/api/v1/contracts/${id}`)
+ if (!response.ok) {
+ throw new Error(`Failed to fetch contract: ${response.statusText}`)
+ }
+ const contractData = await response.json()
+ setData(contractData)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unknown error')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchContract()
+ }, [id])
+
+ if (loading) {
+ return (
+ <div className="contract-detail-container">
+ <div className="loading">Loading contract...</div>
+ </div>
+ )
+ }
+
+ if (error) {
+ return (
+ <div className="contract-detail-container">
+ <div className="error">Error: {error}</div>
+ <Link to="/contracts" className="back-link">
+ Back to Contracts
+ </Link>
+ </div>
+ )
+ }
+
+ if (!data) {
+ return (
+ <div className="contract-detail-container">
+ <div className="not-found">Contract not found</div>
+ <Link to="/contracts" className="back-link">
+ Back to Contracts
+ </Link>
+ </div>
+ )
+ }
+
+ const { contract, repositories, files, tasks } = data
+
+ return (
+ <div className="contract-detail-container">
+ <div className="contract-detail-header">
+ <Link to="/contracts" className="back-link">
+ Back to Contracts
+ </Link>
+ <h1 className="contract-title">{contract.name}</h1>
+ {contract.description && (
+ <p className="contract-description">{contract.description}</p>
+ )}
+ <div className="contract-meta">
+ <span>Phase: {contract.phase}</span>
+ <span>Status: {contract.status}</span>
+ <span>Version: {contract.version}</span>
+ </div>
+ </div>
+
+ <div className="contract-tabs">
+ <button
+ className={`tab-button ${activeTab === 'overview' ? 'active' : ''}`}
+ onClick={() => setActiveTab('overview')}
+ >
+ Overview
+ </button>
+ <button
+ className={`tab-button ${activeTab === 'files' ? 'active' : ''}`}
+ onClick={() => setActiveTab('files')}
+ >
+ Files ({files.length})
+ </button>
+ <button
+ className={`tab-button ${activeTab === 'tasks' ? 'active' : ''}`}
+ onClick={() => setActiveTab('tasks')}
+ >
+ Tasks ({tasks.length})
+ </button>
+ <button
+ className={`tab-button ${activeTab === 'repositories' ? 'active' : ''}`}
+ onClick={() => setActiveTab('repositories')}
+ >
+ Repositories ({repositories.length})
+ </button>
+ </div>
+
+ <div className="contract-tab-content">
+ {activeTab === 'overview' && (
+ <div className="tab-panel">
+ <h2>Contract Overview</h2>
+ <dl className="overview-list">
+ <dt>Type</dt>
+ <dd>{contract.contract_type}</dd>
+ <dt>Phase</dt>
+ <dd>{contract.phase}</dd>
+ <dt>Status</dt>
+ <dd>{contract.status}</dd>
+ <dt>Created</dt>
+ <dd>{new Date(contract.created_at).toLocaleString()}</dd>
+ </dl>
+ </div>
+ )}
+
+ {activeTab === 'files' && (
+ <div className="tab-panel">
+ <h2>Files</h2>
+ {files.length === 0 ? (
+ <p>No files in this contract</p>
+ ) : (
+ <ul className="file-list">
+ {files.map((file) => (
+ <li key={file.id} className="file-item">
+ <Link to={`/contracts/${contract.id}/files/${file.id}`}>
+ <h3>{file.name}</h3>
+ {file.description && <p>{file.description}</p>}
+ {file.contract_phase && (
+ <span className="file-phase">Phase: {file.contract_phase}</span>
+ )}
+ </Link>
+ </li>
+ ))}
+ </ul>
+ )}
+ </div>
+ )}
+
+ {activeTab === 'tasks' && (
+ <div className="tab-panel">
+ <h2>Tasks</h2>
+ {tasks.length === 0 ? (
+ <p>No tasks in this contract</p>
+ ) : (
+ <ul className="task-list">
+ {tasks.map((task) => (
+ <li key={task.id} className="task-item">
+ <h3>{task.name}</h3>
+ <span className={`task-status status-${task.status}`}>
+ {task.status}
+ </span>
+ </li>
+ ))}
+ </ul>
+ )}
+ </div>
+ )}
+
+ {activeTab === 'repositories' && (
+ <div className="tab-panel">
+ <h2>Repositories</h2>
+ {repositories.length === 0 ? (
+ <p>No repositories linked to this contract</p>
+ ) : (
+ <ul className="repository-list">
+ {repositories.map((repo) => (
+ <li key={repo.id} className="repository-item">
+ <h3>
+ {repo.name}
+ {repo.is_primary && <span className="primary-badge">Primary</span>}
+ </h3>
+ <span className="repo-type">{repo.source_type}</span>
+ </li>
+ ))}
+ </ul>
+ )}
+ </div>
+ )}
+ </div>
+ </div>
+ )
+}
diff --git a/frontend/src/components/ContractList.tsx b/frontend/src/components/ContractList.tsx
new file mode 100644
index 0000000..77012db
--- /dev/null
+++ b/frontend/src/components/ContractList.tsx
@@ -0,0 +1,83 @@
+import React, { useEffect, useState } from 'react'
+import { Link } from 'react-router-dom'
+
+interface ContractSummary {
+ id: string
+ name: string
+ description?: string
+ contract_type: string
+ phase: string
+ status: string
+ file_count: number
+ task_count: number
+ repository_count: number
+ created_at: string
+}
+
+export function ContractList() {
+ const [contracts, setContracts] = useState<ContractSummary[]>([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState<string | null>(null)
+
+ useEffect(() => {
+ async function fetchContracts() {
+ try {
+ setLoading(true)
+ const response = await fetch('/api/v1/contracts')
+ if (!response.ok) {
+ throw new Error(`Failed to fetch contracts: ${response.statusText}`)
+ }
+ const data = await response.json()
+ setContracts(data.contracts || [])
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unknown error')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchContracts()
+ }, [])
+
+ if (loading) {
+ return (
+ <div className="contract-list-container">
+ <div className="loading">Loading contracts...</div>
+ </div>
+ )
+ }
+
+ if (error) {
+ return (
+ <div className="contract-list-container">
+ <div className="error">Error: {error}</div>
+ </div>
+ )
+ }
+
+ return (
+ <div className="contract-list-container">
+ <h1>Contracts</h1>
+ {contracts.length === 0 ? (
+ <p>No contracts found</p>
+ ) : (
+ <ul className="contract-list">
+ {contracts.map((contract) => (
+ <li key={contract.id} className="contract-item">
+ <Link to={`/contracts/${contract.id}`}>
+ <h2>{contract.name}</h2>
+ {contract.description && <p>{contract.description}</p>}
+ <div className="contract-meta">
+ <span>Phase: {contract.phase}</span>
+ <span>Status: {contract.status}</span>
+ <span>Files: {contract.file_count}</span>
+ <span>Tasks: {contract.task_count}</span>
+ </div>
+ </Link>
+ </li>
+ ))}
+ </ul>
+ )}
+ </div>
+ )
+}
diff --git a/frontend/src/components/FileDetail.tsx b/frontend/src/components/FileDetail.tsx
new file mode 100644
index 0000000..31228ef
--- /dev/null
+++ b/frontend/src/components/FileDetail.tsx
@@ -0,0 +1,97 @@
+import React, { useEffect, useState } from 'react'
+import { useParams, Link } from 'react-router-dom'
+
+interface File {
+ id: string
+ name: string
+ description?: string
+ body?: string
+ contract_id?: string
+ version: number
+ created_at: string
+}
+
+export function FileDetail() {
+ const { contractId, fileId } = useParams<{ contractId: string; fileId: string }>()
+ const [file, setFile] = useState<File | null>(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState<string | null>(null)
+
+ useEffect(() => {
+ async function fetchFile() {
+ if (!fileId) return
+
+ try {
+ setLoading(true)
+ const response = await fetch(`/api/v1/files/${fileId}`)
+ if (!response.ok) {
+ throw new Error(`Failed to fetch file: ${response.statusText}`)
+ }
+ const data = await response.json()
+ setFile(data)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unknown error')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchFile()
+ }, [fileId])
+
+ if (loading) {
+ return (
+ <div className="file-detail-container">
+ <div className="loading">Loading file...</div>
+ </div>
+ )
+ }
+
+ if (error) {
+ return (
+ <div className="file-detail-container">
+ <div className="error">Error: {error}</div>
+ <Link to={`/contracts/${contractId}`} className="back-link">
+ Back to Contract
+ </Link>
+ </div>
+ )
+ }
+
+ if (!file) {
+ return (
+ <div className="file-detail-container">
+ <div className="not-found">File not found</div>
+ <Link to={`/contracts/${contractId}`} className="back-link">
+ Back to Contract
+ </Link>
+ </div>
+ )
+ }
+
+ return (
+ <div className="file-detail-container">
+ <div className="file-detail-header">
+ <Link to={`/contracts/${contractId}`} className="back-link">
+ Back to Contract
+ </Link>
+ <h1 className="file-title">{file.name}</h1>
+ {file.description && (
+ <p className="file-description">{file.description}</p>
+ )}
+ <div className="file-meta">
+ <span>Version: {file.version}</span>
+ <span>Created: {new Date(file.created_at).toLocaleString()}</span>
+ </div>
+ </div>
+
+ <div className="file-detail-body">
+ {file.body ? (
+ <pre className="file-content">{file.body}</pre>
+ ) : (
+ <p className="no-content">No content</p>
+ )}
+ </div>
+ </div>
+ )
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 9373927..a6eae5b 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,11 +1,42 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
+import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import App from './App'
+import { ContractList } from './components/ContractList'
+import { ContractDetail } from './components/ContractDetail'
+import { FileDetail } from './components/FileDetail'
import './styles/pc98.css'
import './styles/mobile.css'
+// Route configuration:
+// - /contracts - List all contracts
+// - /contracts/:id - View contract details with tabs (including Files tab)
+// - /contracts/:contractId/files/:fileId - View a specific file within contract context
+//
+// Note: Standalone file routes (/files, /files/:id) have been removed.
+// Files are now only accessible through their parent contract.
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: <App />,
+ },
+ {
+ path: '/contracts',
+ element: <ContractList />,
+ },
+ {
+ path: '/contracts/:id',
+ element: <ContractDetail />,
+ },
+ {
+ path: '/contracts/:contractId/files/:fileId',
+ element: <FileDetail />,
+ },
+])
+
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
- <App />
+ <RouterProvider router={router} />
</React.StrictMode>
)