Skip to main content
Glama
ChangeDetail.tsx7.19 kB
import { useState, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; import { changesApi, tasksApi } from '../api/client'; export default function ChangeDetail() { const { id } = useParams<{ id: string }>(); const [change, setChange] = useState<any>(null); const [tasks, setTasks] = useState<any[]>([]); const [progress, setProgress] = useState<any>(null); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState<'proposal' | 'tasks' | 'design'>('proposal'); useEffect(() => { async function fetchData() { if (!id) return; try { const [changeRes, tasksRes] = await Promise.all([ changesApi.get(id), tasksApi.get(id), ]); setChange(changeRes.change); setTasks(tasksRes.tasks); setProgress(tasksRes.progress); } catch (error) { console.error('Failed to fetch change:', error); } finally { setLoading(false); } } fetchData(); }, [id]); const handleTaskUpdate = async (taskId: string, status: string) => { if (!id) return; try { await tasksApi.update(id, taskId, status); const tasksRes = await tasksApi.get(id); setTasks(tasksRes.tasks); setProgress(tasksRes.progress); } catch (error) { console.error('Failed to update task:', error); } }; if (loading) { return ( <div className="flex justify-center items-center h-64"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div> </div> ); } if (!change) { return ( <div className="text-center py-12"> <h2 className="text-xl font-semibold text-gray-900">Change not found</h2> <Link to="/changes" className="text-blue-500 hover:text-blue-700 mt-2 inline-block"> ← Back to changes </Link> </div> ); } return ( <div className="space-y-6"> {/* Header */} <div className="flex justify-between items-start"> <div> <Link to="/changes" className="text-blue-500 hover:text-blue-700 text-sm"> ← Back to changes </Link> <h2 className="text-2xl font-bold text-gray-900 mt-2">{change.title}</h2> <p className="text-gray-500">{change.id}</p> </div> <span className={`status-badge ${ change.status === 'active' ? 'status-implementing' : 'status-completed' }`} > {change.status} </span> </div> {/* Progress */} {progress && ( <div className="bg-white rounded-lg shadow p-4"> <div className="flex justify-between items-center mb-2"> <span className="text-sm font-medium text-gray-700">Progress</span> <span className="text-sm text-gray-500"> {progress.completed}/{progress.total} tasks ({progress.percentage}%) </span> </div> <div className="w-full h-3 bg-gray-200 rounded-full"> <div className="h-full bg-blue-500 rounded-full transition-all duration-300" style={{ width: `${progress.percentage}%` }} /> </div> <div className="flex justify-between mt-2 text-xs text-gray-500"> <span>✅ {progress.completed} done</span> <span>🔄 {progress.inProgress} in progress</span> <span>⏳ {progress.pending} pending</span> </div> </div> )} {/* Tabs */} <div className="border-b border-gray-200"> <nav className="flex space-x-8"> {['proposal', 'tasks', 'design'].map((tab) => ( <button key={tab} onClick={() => setActiveTab(tab as any)} className={`py-4 px-1 border-b-2 font-medium text-sm ${ activeTab === tab ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }`} > {tab.charAt(0).toUpperCase() + tab.slice(1)} </button> ))} </nav> </div> {/* Tab Content */} <div className="bg-white rounded-lg shadow"> {activeTab === 'proposal' && ( <div className="p-6"> <pre className="whitespace-pre-wrap text-sm font-mono bg-gray-50 p-4 rounded"> {change.proposal || 'No proposal content.'} </pre> </div> )} {activeTab === 'tasks' && ( <div className="p-6"> {tasks.length === 0 ? ( <p className="text-gray-500">No tasks defined.</p> ) : ( <div className="space-y-2"> {tasks.map((task) => ( <div key={task.id} className="flex items-center justify-between p-3 rounded border hover:bg-gray-50" > <div className="flex items-center space-x-3"> <button onClick={() => { const nextStatus = task.status === 'pending' ? 'in_progress' : task.status === 'in_progress' ? 'done' : 'pending'; handleTaskUpdate(task.id, nextStatus); }} className={`w-6 h-6 rounded-full border-2 flex items-center justify-center ${ task.status === 'done' ? 'bg-green-500 border-green-500 text-white' : task.status === 'in_progress' ? 'bg-blue-500 border-blue-500 text-white' : 'border-gray-300' }`} > {task.status === 'done' && '✓'} {task.status === 'in_progress' && '●'} </button> <div> <span className="font-mono text-sm text-gray-500">[{task.id}]</span> <span className="ml-2">{task.title}</span> </div> </div> <span className={`text-xs ${ task.status === 'done' ? 'text-green-600' : task.status === 'in_progress' ? 'text-blue-600' : 'text-gray-400' }`} > {task.status} </span> </div> ))} </div> )} </div> )} {activeTab === 'design' && ( <div className="p-6"> <pre className="whitespace-pre-wrap text-sm font-mono bg-gray-50 p-4 rounded"> {change.design || 'No design document.'} </pre> </div> )} </div> </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Lumiaqian/openspec-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server