- Grove → Project - Tree → Package - Fruit → Artifact - Graft → Tag - Cultivate → Upload - Harvest → Download Updated across: - Backend models, schemas, and routes - Frontend types, API client, and components - README documentation - API endpoints now use /project/:project/packages pattern
129 lines
4.1 KiB
TypeScript
129 lines
4.1 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { Project } from '../types';
|
|
import { listProjects, createProject } from '../api';
|
|
import './Home.css';
|
|
|
|
function Home() {
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [newProject, setNewProject] = useState({ name: '', description: '', is_public: true });
|
|
const [creating, setCreating] = useState(false);
|
|
|
|
useEffect(() => {
|
|
loadProjects();
|
|
}, []);
|
|
|
|
async function loadProjects() {
|
|
try {
|
|
setLoading(true);
|
|
const data = await listProjects();
|
|
setProjects(data);
|
|
setError(null);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to load projects');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleCreateProject(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
try {
|
|
setCreating(true);
|
|
await createProject(newProject);
|
|
setNewProject({ name: '', description: '', is_public: true });
|
|
setShowForm(false);
|
|
loadProjects();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to create project');
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return <div className="loading">Loading projects...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="home">
|
|
<div className="page-header">
|
|
<h1>Projects</h1>
|
|
<button className="btn btn-primary" onClick={() => setShowForm(!showForm)}>
|
|
{showForm ? 'Cancel' : '+ New Project'}
|
|
</button>
|
|
</div>
|
|
|
|
{error && <div className="error-message">{error}</div>}
|
|
|
|
{showForm && (
|
|
<form className="form card" onSubmit={handleCreateProject}>
|
|
<h3>Create New Project</h3>
|
|
<div className="form-group">
|
|
<label htmlFor="name">Name</label>
|
|
<input
|
|
id="name"
|
|
type="text"
|
|
value={newProject.name}
|
|
onChange={(e) => setNewProject({ ...newProject, name: e.target.value })}
|
|
placeholder="my-project"
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="form-group">
|
|
<label htmlFor="description">Description</label>
|
|
<input
|
|
id="description"
|
|
type="text"
|
|
value={newProject.description}
|
|
onChange={(e) => setNewProject({ ...newProject, description: e.target.value })}
|
|
placeholder="Optional description"
|
|
/>
|
|
</div>
|
|
<div className="form-group checkbox">
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
checked={newProject.is_public}
|
|
onChange={(e) => setNewProject({ ...newProject, is_public: e.target.checked })}
|
|
/>
|
|
Public
|
|
</label>
|
|
</div>
|
|
<button type="submit" className="btn btn-primary" disabled={creating}>
|
|
{creating ? 'Creating...' : 'Create Project'}
|
|
</button>
|
|
</form>
|
|
)}
|
|
|
|
{projects.length === 0 ? (
|
|
<div className="empty-state">
|
|
<p>No projects yet. Create your first project to get started!</p>
|
|
</div>
|
|
) : (
|
|
<div className="project-grid">
|
|
{projects.map((project) => (
|
|
<Link to={`/project/${project.name}`} key={project.id} className="project-card card">
|
|
<h3>{project.name}</h3>
|
|
{project.description && <p>{project.description}</p>}
|
|
<div className="project-meta">
|
|
<span className={`badge ${project.is_public ? 'badge-public' : 'badge-private'}`}>
|
|
{project.is_public ? 'Public' : 'Private'}
|
|
</span>
|
|
<span className="date">
|
|
Created {new Date(project.created_at).toLocaleDateString()}
|
|
</span>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default Home;
|