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
This commit is contained in:
Developer
2026-06-05 19:31:21 +00:00
parent 96ce3f4c53
commit 070e960c05
4 changed files with 392 additions and 366 deletions
+155
View File
@@ -0,0 +1,155 @@
import { useState } from "react";
import {
createProject,
deleteProject,
listProjects,
updateProject,
type ProjectCreateInput,
type ProjectUpdateInput,
} from "../api/projects";
import { deleteWorkspace, syncWorkspace } from "../api/workspaces";
import { useAsyncData } from "./use-async-data";
import type { ProjectWithRepos, WorkspaceSummary } from "../types";
type DialogMode = "none" | "create" | "edit";
export const useProjects = () => {
const {
data: projects,
status,
reload,
} = useAsyncData<ProjectWithRepos[]>(listProjects, []);
const [dialogMode, setDialogMode] = useState<DialogMode>("none");
const [editingProject, setEditingProject] = useState<ProjectWithRepos | null>(
null,
);
const [formName, setFormName] = useState("");
const [formDescription, setFormDescription] = useState("");
const [formError, setFormError] = useState<string | null>(null);
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
const [expandedProject, setExpandedProject] = useState<string | null>(null);
const [creatingWorkspace, setCreatingWorkspace] = useState<{
projectId: string;
repoId: string;
} | null>(null);
const [workspaceLoading, setWorkspaceLoading] = useState<string | null>(null);
const safeProjects = projects ?? [];
const openCreate = () => {
setFormName("");
setFormDescription("");
setFormError(null);
setEditingProject(null);
setDialogMode("create");
};
const openEdit = (project: ProjectWithRepos) => {
setFormName(project.name);
setFormDescription(project.description ?? "");
setFormError(null);
setEditingProject(project);
setDialogMode("edit");
};
const closeDialog = () => {
setDialogMode("none");
setEditingProject(null);
setFormError(null);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setFormError(null);
if (!formName.trim()) {
setFormError("Project name is required");
return;
}
try {
if (dialogMode === "create") {
const input: ProjectCreateInput = {
name: formName.trim(),
description: formDescription.trim() || null,
};
await createProject(input);
} else if (dialogMode === "edit" && editingProject) {
const input: ProjectUpdateInput = {
name: formName.trim(),
description: formDescription.trim() || null,
};
await updateProject(editingProject.id, input);
}
closeDialog();
reload();
} catch {
setFormError("Failed to save project");
}
};
const handleDelete = async (projectId: string) => {
try {
await deleteProject(projectId);
setDeleteConfirmId(null);
reload();
} catch {
setDeleteConfirmId(null);
}
};
const handleSyncWorkspace = async (
projectId: string,
repoId: string,
workspace: WorkspaceSummary,
) => {
setWorkspaceLoading(workspace.id);
try {
await syncWorkspace(projectId, repoId, workspace.id);
reload();
} catch (err) {
alert(err instanceof Error ? err.message : "Failed to sync workspace");
} finally {
setWorkspaceLoading(null);
}
};
const handleDeleteWorkspace = async (workspace: WorkspaceSummary) => {
if (!confirm(`Delete workspace "${workspace.name}"?`)) return;
setWorkspaceLoading(workspace.id);
try {
await deleteWorkspace(workspace.id);
reload();
} catch (err) {
alert(err instanceof Error ? err.message : "Failed to delete workspace");
} finally {
setWorkspaceLoading(null);
}
};
return {
projects: safeProjects,
status,
reload,
dialogMode,
formName,
setFormName,
formDescription,
setFormDescription,
formError,
deleteConfirmId,
setDeleteConfirmId,
expandedProject,
setExpandedProject,
creatingWorkspace,
setCreatingWorkspace,
workspaceLoading,
openCreate,
openEdit,
closeDialog,
handleSubmit,
handleDelete,
handleSyncWorkspace,
handleDeleteWorkspace,
};
};