Files
headquarter/apps/web/src/components/features/project/ProjectCard.tsx
T
Developer 070e960c05 refactor: extract ProjectsPage components
- Extract use-projects hook for state management
- Extract ProjectCard and ProjectDialog components
- Slim ProjectsPage from 433 to ~100 lines

Quality gates: tsc --noEmit passes, npm run build passes
2026-06-05 19:31:21 +00:00

121 lines
4.7 KiB
TypeScript

import { Icon } from "../../icon";
import { WorkspaceCreateForm } from "../workspace/workspace-create-form";
import type { ProjectWithRepos, WorkspaceSummary } from "../../../types";
interface Props {
project: ProjectWithRepos;
expanded: boolean;
deleteConfirm: boolean;
workspaceLoading: string | null;
showCreateForm: string | null;
onToggle: () => void;
onEdit: () => void;
onDelete: () => void;
onConfirmDelete: () => void;
onCancelDelete: () => void;
onCreateWorkspace: (repoId: string) => void;
onWorkspaceAction: (repoId: string, workspace: WorkspaceSummary, action: "sync" | "delete") => void;
onCancelCreate: () => void;
onCreated: () => void;
}
export const ProjectCard = ({
project,
expanded,
deleteConfirm,
workspaceLoading,
showCreateForm,
onToggle,
onEdit,
onDelete,
onConfirmDelete,
onCancelDelete,
onCreateWorkspace,
onWorkspaceAction,
onCancelCreate,
onCreated,
}: Props) => {
return (
<article className="card project-card">
<div className="project-info-row">
<button className="project-toggle" onClick={onToggle} type="button" aria-expanded={expanded}>
<Icon name={expanded ? "chevron-down" : "chevron-right"} size="sm" />
<h3>{project.name}</h3>
{project.repositories.length > 0 && (
<span className="repo-count">
{project.repositories.length} repo{project.repositories.length > 1 ? "s" : ""}
</span>
)}
</button>
<div className="project-actions">
<button className="ghost-button" onClick={onEdit} type="button">
<Icon name="edit" size="sm" /> Edit
</button>
{deleteConfirm ? (
<div className="delete-confirm">
<span>Are you sure?</span>
<button className="danger-button" onClick={onConfirmDelete} type="button">
<Icon name="delete" size="sm" /> Delete
</button>
<button className="ghost-button" onClick={onCancelDelete} type="button">
<Icon name="cancel" size="sm" /> Cancel
</button>
</div>
) : (
<button className="ghost-button danger-text" onClick={onDelete} type="button">
<Icon name="delete" size="sm" /> Delete
</button>
)}
</div>
</div>
{expanded && (
<div className="project-detail">
{project.repositories.length === 0 ? (
<p className="muted">No repositories yet.</p>
) : (
<div className="repo-list">
{project.repositories.map((repo) => (
<div key={repo.id} className="repo-block">
<div className="repo-header">
<h4>{repo.name}</h4>
<button className="btn btn-sm btn-primary" onClick={() => onCreateWorkspace(repo.id)} type="button">
<Icon name="add" size="sm" /> New Workspace
</button>
</div>
{showCreateForm === repo.id && (
<WorkspaceCreateForm defaultProjectId={project.id} defaultRepoId={repo.id} onSubmit={onCreated} onCancel={onCancelCreate} />
)}
{repo.workspaces.length === 0 ? (
<p className="muted">No workspaces.</p>
) : (
<div className="workspace-grid">
{repo.workspaces.map((ws) => (
<div key={ws.id} className={`workspace-chip ${ws.status}`}>
<a href={`/workspaces/${ws.id}`}>{ws.name}</a>
<span className="ws-branch"><Icon name="branch" size="sm" /> {ws.branch}</span>
{ws.instance_count > 0 && (
<span className="ws-instances">{ws.instance_count} tool{ws.instance_count > 1 ? "s" : ""}</span>
)}
<div className="ws-actions">
<button type="button" disabled={workspaceLoading === ws.id} onClick={() => onWorkspaceAction(repo.id, ws, "sync")}>
<Icon name="refresh" size="sm" />
</button>
<button type="button" className="danger-text" disabled={workspaceLoading === ws.id} onClick={() => onWorkspaceAction(repo.id, ws, "delete")}>
<Icon name="delete" size="sm" />
</button>
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
)}
</article>
);
};