Canvas: build application infrastructure with drag and drop

Change-Id: I5cfd12e67794f3376c5c025af29470d52d77cf16
diff --git a/apps/canvas/src/Header.tsx b/apps/canvas/src/Header.tsx
new file mode 100644
index 0000000..2d6d68d
--- /dev/null
+++ b/apps/canvas/src/Header.tsx
@@ -0,0 +1,115 @@
+import { ChangeEvent, useCallback, useEffect, useState } from "react";
+import { Project, useProjectId, useStateStore } from "./lib/state";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./components/ui/select";
+import { useReactFlow } from "@xyflow/react";
+import { Input } from "./components/ui/input";
+import { Button } from "./components/ui/button";
+import { Dialog, DialogContent, DialogTrigger } from "./components/ui/dialog";
+import { useToast } from "@/hooks/use-toast";
+
+export function Header() {
+    const { toast } = useToast();
+    const store = useStateStore();
+    const [projects, setProjects] = useState<Project[]>([]);
+    // TODO(gio): sth fishy is here
+    useEffect(() => {
+        store.setProjects(projects);
+    }, [projects]);
+    const refreshProjects = useCallback(async () => {
+        try {
+            const resp = await fetch("/api/project");
+            setProjects(await resp.json());
+        } catch (e) {
+            console.log(e);
+        }
+    }, [setProjects]);
+    useEffect(() => {
+        refreshProjects();
+    }, [refreshProjects]);
+    const project = useProjectId();
+    const [createNewOpen, setCreateNewOpen] = useState(false);
+    const onSelect = useCallback((projectId: string) => {
+        if (projectId === "create-new") {
+            setCreateNewOpen(true);
+        } else {
+            store.setProject(projectId);
+        }
+    }, [store]);
+    const instance = useReactFlow();
+    const restoreSaved = useCallback(async (projectId: string) => {
+        const resp = await fetch(`/api/project/${projectId}/saved`, {
+            method: "GET",
+        });
+        const inst = await resp.json();
+        const { x = 0, y = 0, zoom = 1 } = inst.viewport;
+        instance.setNodes(inst.nodes || []);
+        instance.setEdges(inst.edges || []);
+        instance.setViewport({ x, y, zoom });
+    }, [instance]);
+    useEffect(() => {
+        if (project == null) {
+            return;
+        }
+        restoreSaved(project)
+    }, [project, restoreSaved]);
+    const [name, setName] = useState<string | undefined>(undefined);
+    const updateName = useCallback((e: ChangeEvent<HTMLInputElement>) => {
+        setName(e.target.value);
+    }, [setName]);
+    const createNew = useCallback(() => {
+        console.log(name);
+        if (!name) {
+            return;
+        }
+        fetch("/api/project", {
+            method: "POST",
+            headers: {
+                "Content-Type": "application/json",
+            },
+            body: JSON.stringify({
+                name: name,
+            }),
+        }).then(async (resp) => {
+            if (!resp.ok) {
+                return;
+            }
+            const { id } = await resp.json();
+            await refreshProjects();
+            store.setProject(id as string);
+            setCreateNewOpen(false);
+            toast({
+                title: `Created project: ${name}`,
+            });
+        }).catch((e) => {
+            console.log(e);
+            toast({
+                variant: "destructive",
+                title: `Failed to create project: ${name}`,
+            });
+        });
+    }, [name, setCreateNewOpen, toast]); // store
+    return (
+        <div className="flex flex-row h-9">
+            <Select onValueChange={onSelect} value={project}>
+                <SelectTrigger>
+                    <SelectValue placeholder="Choose Project" defaultValue={project} />
+                </SelectTrigger>
+                <SelectContent>
+                    {projects.map((p) => (
+                        <SelectItem value={p.id}>{p.name}</SelectItem>
+                    ))}
+                    <SelectItem value={"create-new"}>
+                        <Dialog open={createNewOpen} onOpenChange={setCreateNewOpen}>
+                            <DialogTrigger>Create New</DialogTrigger>
+                            <DialogContent>
+                                <Input type="text" placeholder="Name" onChange={updateName} />
+                                <Button onClick={createNew}>Create New</Button>
+                            </DialogContent>
+                        </Dialog>
+                    </SelectItem>
+                </SelectContent>
+            </Select>
+            
+        </div>
+    );
+}
\ No newline at end of file