Files
headquarter/apps/web/src/pages/projects.tsx
T
alex fb0f2f7b9b feat: add project settings page navigation
- Remove inline edit modal from projects listing page
- Add Settings link to project cards navigating to /projects/:id/settings
- Reposition Open Workspace button to rightmost action for easier access
- Update tests for new UI flow
- Update documentation to reflect new editing workflow
- Sync specs: frontend-foundation and project-management

Quality gates: npm run lint passed
2026-05-22 18:43:40 +00:00

207 lines
6.3 KiB
TypeScript

import { useCallback, useEffect, useState } from "react";
import { Link } from "react-router-dom";
import {
createProject,
deleteProject,
listProjects,
type ProjectCreateInput,
} from "../api/projects";
import { Icon } from "../components/icon";
import type { Project } from "../types";
type ProjectsStatus = "loading" | "ready" | "error";
export const ProjectsPage = () => {
const [status, setStatus] = useState<ProjectsStatus>("loading");
const [projects, setProjects] = useState<Project[]>([]);
const [showCreate, setShowCreate] = useState(false);
const [formName, setFormName] = useState("");
const [formDescription, setFormDescription] = useState("");
const [formError, setFormError] = useState<string | null>(null);
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
const loadProjects = useCallback(async () => {
setStatus("loading");
try {
const data = await listProjects();
setProjects(data);
setStatus("ready");
} catch {
setProjects([]);
setStatus("error");
}
}, []);
useEffect(() => {
void loadProjects();
}, [loadProjects]);
const openCreate = () => {
setFormName("");
setFormDescription("");
setFormError(null);
setShowCreate(true);
};
const closeCreate = () => {
setShowCreate(false);
setFormError(null);
};
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
setFormError(null);
if (!formName.trim()) {
setFormError("Project name is required");
return;
}
try {
const input: ProjectCreateInput = {
name: formName.trim(),
description: formDescription.trim() || null,
};
await createProject(input);
closeCreate();
await loadProjects();
} catch {
setFormError("Failed to save project");
}
};
const handleDelete = async (projectId: string) => {
try {
await deleteProject(projectId);
setDeleteConfirmId(null);
await loadProjects();
} catch {
setDeleteConfirmId(null);
}
};
const isEmpty = status === "ready" && projects.length === 0;
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" && <p className="muted">Loading projects...</p>}
{status === "error" && (
<div className="card stack">
<p>Failed to load projects</p>
<button className="secondary-button" onClick={() => void loadProjects()} type="button">
<Icon name="refresh" size="sm" />
Retry
</button>
</div>
)}
{isEmpty && <p className="muted">No projects yet. Create your first project above.</p>}
{status === "ready" && projects.length > 0 && (
<div className="project-list">
{projects.map((project) => (
<article className="card project-card" key={project.id}>
<div className="project-info">
<h3>{project.name}</h3>
{project.description && <p className="muted">{project.description}</p>}
</div>
<div className="project-actions">
<Link
className="ghost-button"
to={`/projects/${project.id}/settings`}
>
<Icon name="settings" size="sm" />
Settings
</Link>
{deleteConfirmId === project.id ? (
<div className="delete-confirm">
<span>Are you sure?</span>
<button
className="danger-button"
onClick={() => void handleDelete(project.id)}
type="button"
>
<Icon name="delete" size="sm" />
Delete
</button>
<button
className="ghost-button"
onClick={() => setDeleteConfirmId(null)}
type="button"
>
<Icon name="cancel" size="sm" />
Cancel
</button>
</div>
) : (
<button
className="ghost-button danger-text"
onClick={() => setDeleteConfirmId(project.id)}
type="button"
>
<Icon name="delete" size="sm" />
Delete
</button>
)}
<Link className="ghost-button" to={`/projects/${project.id}`}>
Open Workspace
</Link>
</div>
</article>
))}
</div>
)}
{showCreate && (
<div className="dialog-overlay" role="dialog" aria-modal="true">
<div className="dialog">
<h2>Create Project</h2>
<form onSubmit={handleCreate} className="stack">
<label className="form-field">
Name
<input
type="text"
value={formName}
onChange={(e) => setFormName(e.target.value)}
placeholder="Project name"
/>
</label>
<label className="form-field">
Description
<textarea
value={formDescription}
onChange={(e) => setFormDescription(e.target.value)}
placeholder="Optional description"
rows={3}
/>
</label>
{formError && <p className="error-text">{formError}</p>}
<div className="dialog-actions">
<button className="secondary-button" onClick={closeCreate} type="button">
<Icon name="cancel" size="sm" />
Cancel
</button>
<button className="primary-button" type="submit">
<Icon name="add" size="sm" />
Create
</button>
</div>
</form>
</div>
</div>
)}
</section>
);
};