120 lines
3.7 KiB
TypeScript
120 lines
3.7 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { Plus, Trash2, ChevronRight } from "lucide-react";
|
|
|
|
interface Project {
|
|
id: number;
|
|
name: string;
|
|
trigger_branch: string;
|
|
created_at: number;
|
|
}
|
|
|
|
export default function Projects() {
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
const [creating, setCreating] = useState(false);
|
|
const [newName, setNewName] = useState("");
|
|
const navigate = useNavigate();
|
|
|
|
const load = () =>
|
|
fetch("/api/projects")
|
|
.then((r) => r.json())
|
|
.then(setProjects);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, []);
|
|
|
|
const create = async () => {
|
|
if (!newName.trim()) return;
|
|
await fetch("/api/projects", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name: newName.trim() }),
|
|
});
|
|
setNewName("");
|
|
setCreating(false);
|
|
load();
|
|
};
|
|
|
|
const remove = async (e: React.MouseEvent, id: number) => {
|
|
e.stopPropagation();
|
|
if (!confirm("Delete this project and all its builds?")) return;
|
|
await fetch(`/api/projects/${id}`, { method: "DELETE" });
|
|
load();
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h1 className="text-xl font-semibold">Projects</h1>
|
|
<button
|
|
onClick={() => setCreating(true)}
|
|
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 text-white text-sm px-3 py-2 rounded-md transition-colors"
|
|
>
|
|
<Plus className="w-4 h-4" /> New Project
|
|
</button>
|
|
</div>
|
|
|
|
{creating && (
|
|
<div className="mb-4 flex gap-2">
|
|
<input
|
|
autoFocus
|
|
value={newName}
|
|
onChange={(e) => setNewName(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") create();
|
|
if (e.key === "Escape") setCreating(false);
|
|
}}
|
|
placeholder="Project name"
|
|
className="flex-1 bg-gray-800 border border-gray-700 rounded-md px-3 py-2 text-sm outline-none focus:border-blue-500"
|
|
/>
|
|
<button
|
|
onClick={create}
|
|
className="bg-blue-600 hover:bg-blue-500 text-white text-sm px-4 py-2 rounded-md transition-colors"
|
|
>
|
|
Create
|
|
</button>
|
|
<button
|
|
onClick={() => setCreating(false)}
|
|
className="text-gray-400 hover:text-gray-200 text-sm px-3 py-2 rounded-md transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{projects.length === 0 && !creating ? (
|
|
<p className="text-gray-500 text-sm">
|
|
No projects yet. Create one to get started.
|
|
</p>
|
|
) : (
|
|
<ul className="space-y-2">
|
|
{projects.map((p) => (
|
|
<li
|
|
key={p.id}
|
|
onClick={() => navigate(`/projects/${p.id}`)}
|
|
className="flex items-center justify-between bg-gray-900 border border-gray-800 hover:border-gray-600 rounded-lg px-4 py-3 cursor-pointer transition-colors group"
|
|
>
|
|
<div>
|
|
<p className="font-medium text-sm">{p.name}</p>
|
|
<p className="text-xs text-gray-500 mt-0.5">
|
|
branch: {p.trigger_branch}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={(e) => remove(e, p.id)}
|
|
className="text-gray-600 hover:text-red-400 transition-colors opacity-0 group-hover:opacity-100"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
<ChevronRight className="w-4 h-4 text-gray-600" />
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|