pcbjam/web/apps/frontend/src/pages/ProjectsPage.tsx
Gergő Törcsvári 735e5aa8e9
feat(web): checkpoint web app init
- frontend (Vite/React) + server (Hono/Drizzle) scaffold under web/
- eeschema WASM embind kicadOpenFile hook + programmatic open-flow
- skip KiCad first-run setup wizard by seeding default config in preRun
- dev: auto-sync output/ WASM artifacts into tests/apps/kicad via link-wasm

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:32:19 +02:00

150 lines
4.4 KiB
TypeScript

import * as React from "react";
import { Link } from "react-router-dom";
import { Loader2, Plus, Trash2 } from "lucide-react";
import { useCreateProject, useDeleteProject, useProjects } from "@/lib/api";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
function CreateProjectDialog() {
const create = useCreateProject();
const [open, setOpen] = React.useState(false);
const [name, setName] = React.useState("");
const [slug, setSlug] = React.useState("");
const submit = async () => {
if (!name.trim()) return;
await create.mutateAsync({
name: name.trim(),
slug: slug.trim() || undefined,
});
setName("");
setSlug("");
setOpen(false);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<Plus /> New project
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create a project</DialogTitle>
<DialogDescription>
A project holds a tree of KiCad files you can open in the browser.
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="space-y-1.5">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
placeholder="My board"
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="slug">Slug (optional)</Label>
<Input
id="slug"
value={slug}
placeholder="auto-generated from name"
onChange={(e) => setSlug(e.target.value)}
/>
</div>
{create.error && (
<p className="text-sm text-destructive">
{(create.error as Error).message}
</p>
)}
</div>
<DialogFooter>
<Button onClick={submit} disabled={create.isPending}>
{create.isPending && <Loader2 className="animate-spin" />} Create
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export function ProjectsPage() {
const { data: projects, isLoading, error } = useProjects();
const del = useDeleteProject();
return (
<div className="container py-10">
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Projects</h1>
<p className="text-sm text-muted-foreground">
Create a project, upload KiCad files, open them in the browser.
</p>
</div>
<CreateProjectDialog />
</div>
{isLoading && (
<p className="flex items-center gap-2 text-muted-foreground">
<Loader2 className="animate-spin" /> loading
</p>
)}
{error && (
<p className="text-destructive">
Could not load projects: {(error as Error).message}
</p>
)}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{projects?.map((p) => (
<Card key={p.id}>
<CardHeader>
<CardTitle>{p.name}</CardTitle>
<CardDescription>/p/{p.slug}</CardDescription>
</CardHeader>
<CardContent className="flex items-center justify-between">
<Button asChild variant="secondary" size="sm">
<Link to={`/p/${p.slug}`}>Open</Link>
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => {
if (confirm(`Delete project "${p.name}"?`)) {
del.mutate(p.slug);
}
}}
>
<Trash2 className="text-destructive" />
</Button>
</CardContent>
</Card>
))}
</div>
{projects && projects.length === 0 && !isLoading && (
<p className="text-muted-foreground">No projects yet. Create one above.</p>
)}
</div>
);
}