fb0f2f7b9b
- 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
207 lines
6.3 KiB
TypeScript
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>
|
|
);
|
|
};
|