Files
headquarter/apps/web/src/pages/ProjectsPage.tsx
T
Developer c353bceb97 feat: unify repository creation flow for desktop and mobile
- ProjectCard.tsx: add onAddRepository prop, show 'Add Repository' button in expanded view
- ProjectCard.tsx: add showBackButton/onBack props for mobile detail reuse
- ProjectsPage.tsx (desktop): wire onAddRepository to open RepositoryCreateDialog
- ProjectsPage.tsx (mobile): reuse ProjectCard for detail view instead of inline duplication
- pages/projects.css: add .project-add-repo style

Both platforms now use the same ProjectCard component and RepositoryCreateDialog
for adding repositories to projects.

Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
2026-06-09 09:22:51 +00:00

282 lines
7.5 KiB
TypeScript

import { useState } from "react";
import {
EmptyState,
ErrorState,
LoadingState,
} from "../components/data-states";
import { Icon } from "../components/icon";
import { useMobileViewport } from "../hooks/use-mobile-viewport";
import { ProjectCard } from "../components/features/project/ProjectCard";
import { ProjectDialog } from "../components/features/project/ProjectDialog";
import { RepositoryCreateDialog } from "../components/features/project/repository-create-dialog";
import { MobileListView } from "../components/features/mobile/mobile-list-view";
import { MobileFAB } from "../components/features/mobile/mobile-fab";
import { useProjects } from "../hooks/use-projects";
import type { ProjectWithRepos } from "../types";
type MobileView = "list" | "detail" | "create-project" | "create-repo";
export const ProjectsPage = () => {
const isMobile = useMobileViewport();
const [mobileView, setMobileView] = useState<MobileView>("list");
const [selectedProject, setSelectedProject] =
useState<ProjectWithRepos | null>(null);
const {
projects,
status,
reload,
dialogMode,
formName,
setFormName,
formDescription,
setFormDescription,
formError,
deleteConfirmId,
setDeleteConfirmId,
expandedProject,
setExpandedProject,
creatingWorkspace,
setCreatingWorkspace,
workspaceLoading,
openCreate,
openEdit,
closeDialog,
handleSubmit,
handleDelete,
handleSyncWorkspace,
handleDeleteWorkspace,
} = useProjects();
const isEmpty = status === "ready" && projects.length === 0;
/* ── Mobile views ── */
if (isMobile) {
if (mobileView === "create-project") {
return (
<div className="mobile-page">
<ProjectDialog
mode="create"
name={formName}
description={formDescription}
error={formError}
onNameChange={setFormName}
onDescriptionChange={setFormDescription}
onSubmit={handleSubmit}
onCancel={() => {
closeDialog();
setMobileView("list");
}}
/>
</div>
);
}
if (mobileView === "create-repo" && selectedProject) {
return (
<div className="mobile-page">
<RepositoryCreateDialog
projectId={selectedProject.id}
open={true}
title="Add Repository"
onClose={() => setMobileView("detail")}
onCreated={async () => {
setMobileView("detail");
await reload();
}}
/>
</div>
);
}
if (mobileView === "detail" && selectedProject) {
return (
<div className="mobile-page">
<ProjectCard
project={selectedProject}
expanded={true}
showBackButton={true}
deleteConfirm={deleteConfirmId === selectedProject.id}
workspaceLoading={workspaceLoading}
showCreateForm={
creatingWorkspace?.projectId === selectedProject.id
? creatingWorkspace.repoId
: null
}
onBack={() => {
setMobileView("list");
setSelectedProject(null);
}}
onEdit={() => {
setFormName(selectedProject.name);
setFormDescription(selectedProject.description ?? "");
setMobileView("create-project");
}}
onDelete={() => setDeleteConfirmId(selectedProject.id)}
onConfirmDelete={() => {
void handleDelete(selectedProject.id);
setMobileView("list");
setSelectedProject(null);
}}
onCancelDelete={() => setDeleteConfirmId(null)}
onCreateWorkspace={(repoId) =>
setCreatingWorkspace({
projectId: selectedProject.id,
repoId,
})
}
onWorkspaceAction={(repoId, workspace, action) => {
if (action === "sync") {
void handleSyncWorkspace(
selectedProject.id,
repoId,
workspace,
);
} else if (action === "delete") {
void handleDeleteWorkspace(workspace);
}
}}
onAddRepository={() => setMobileView("create-repo")}
onCancelCreate={() => setCreatingWorkspace(null)}
onCreated={() => {
setCreatingWorkspace(null);
reload();
}}
/>
</div>
);
}
/* Mobile list view */
return (
<div className="mobile-page">
<div className="mobile-page-header">
<h1>Projects</h1>
<span className="muted">
{projects.length} project{projects.length !== 1 ? "s" : ""}
</span>
</div>
{status === "loading" && <LoadingState message="Loading projects..." />}
{status === "error" && (
<ErrorState message="Failed to load projects" onRetry={reload} />
)}
{isEmpty && (
<EmptyState message="No projects yet. Create your first project above." />
)}
{status === "ready" && projects.length > 0 && (
<MobileListView
items={projects.map((p) => ({
id: p.id,
title: p.name,
subtitle: `${p.repositories?.length ?? 0} repo${(p.repositories?.length ?? 0) !== 1 ? "s" : ""}${p.description ? " · " + p.description : ""}`,
}))}
onItemClick={(id) => {
const project = projects.find((p) => p.id === id);
if (project) {
setSelectedProject(project);
setMobileView("detail");
}
}}
emptyMessage="No projects yet"
/>
)}
<MobileFAB
onClick={() => {
openCreate();
setMobileView("create-project");
}}
label="Create project"
/>
</div>
);
}
/* ── Desktop view ── */
return (
<section className="stack">
<div className="page-header">
<h1>Projects</h1>
<button className="primary-button" onClick={openCreate} type="button">
<Icon name="add" size="sm" />
New Project
</button>
</div>
{status === "loading" && <LoadingState message="Loading projects..." />}
{status === "error" && (
<ErrorState message="Failed to load projects" onRetry={reload} />
)}
{isEmpty && (
<EmptyState message="No projects yet. Create your first project above." />
)}
{status === "ready" && projects.length > 0 && (
<div className="project-list">
{projects.map((project) => (
<ProjectCard
key={project.id}
project={project}
expanded={expandedProject === project.id}
deleteConfirm={deleteConfirmId === project.id}
workspaceLoading={workspaceLoading}
showCreateForm={
creatingWorkspace?.projectId === project.id
? creatingWorkspace.repoId
: null
}
onToggle={() =>
setExpandedProject(
expandedProject === project.id ? null : project.id,
)
}
onEdit={() => openEdit(project)}
onDelete={() => setDeleteConfirmId(project.id)}
onConfirmDelete={() => void handleDelete(project.id)}
onCancelDelete={() => setDeleteConfirmId(null)}
onCreateWorkspace={(repoId) =>
setCreatingWorkspace({ projectId: project.id, repoId })
}
onWorkspaceAction={(repoId, workspace, action) => {
if (action === "sync") {
void handleSyncWorkspace(project.id, repoId, workspace);
} else if (action === "delete") {
void handleDeleteWorkspace(workspace);
}
}}
onCancelCreate={() => setCreatingWorkspace(null)}
onAddRepository={() => {
setSelectedProject(project);
setMobileView("create-repo");
}}
onCreated={() => {
setCreatingWorkspace(null);
reload();
}}
/>
))}
</div>
)}
{dialogMode !== "none" && (
<ProjectDialog
mode={dialogMode}
name={formName}
description={formDescription}
error={formError}
onNameChange={setFormName}
onDescriptionChange={setFormDescription}
onSubmit={handleSubmit}
onCancel={closeDialog}
/>
)}
</section>
);
};