blob: 9b8bb452ac7497d093817975b38cf9d9508f4a98 [file] [log] [blame]
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 { Input } from "./components/ui/input";
import { Button } from "./components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "./components/ui/dialog";
import { useToast } from "@/hooks/use-toast";
import { Separator } from "./components/ui/separator";
import { Plus } from "lucide-react";
import { z } from "zod";
import { cn } from "./lib/utils";
const createNewSchema = z.object({
id: z.string().min(1),
});
export function ProjectSelect({ className }: { className?: string }) {
const { toast } = useToast();
const store = useStateStore();
const [projects, setProjects] = useState<Project[]>([]);
const projectId = useProjectId();
// Track the selected project ID locally to ensure UI consistency
const [selectedId, setSelectedId] = useState<string | undefined>(projectId);
// Keep local state in sync with global state
useEffect(() => {
setSelectedId(projectId);
}, [projectId]);
const refreshProjects = useCallback(
async (id?: string) => {
console.log("refreshProjects", id);
try {
const resp = await fetch("/api/project");
const projectList = await resp.json();
const sortedProjects = [...projectList].sort((a, b) =>
a.name.localeCompare(b.name, undefined, { sensitivity: "base" }),
);
setProjects(sortedProjects);
if (id && !sortedProjects.some((p) => p.id === id)) {
throw new Error("MUST NOT REACH!");
}
if (id == null) {
id = sortedProjects[0].id;
}
if (id !== selectedId) {
setSelectedId(id);
store.setProject(id);
}
} catch (e) {
console.log(e);
}
},
[selectedId, store, setProjects, setSelectedId],
);
useEffect(() => {
refreshProjects();
}, [refreshProjects]);
const [createNewOpen, setCreateNewOpen] = useState(false);
const onSelect = useCallback(
(projectId: string) => {
if (projectId === "create-new") {
setCreateNewOpen(true);
} else {
setSelectedId(projectId);
store.setProject(projectId);
}
},
[store, setSelectedId],
);
const [name, setName] = useState<string | undefined>(undefined);
const updateName = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
},
[setName],
);
const createNew = useCallback(() => {
if (!name) {
toast({
variant: "destructive",
title: "Name is required",
});
return;
}
fetch("/api/project", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: name,
}),
})
.then(async (resp) => {
if (!resp.ok) {
return false;
}
const result = createNewSchema.safeParse(await resp.json());
if (!result.success) {
toast({
variant: "destructive",
title: `Failed to create project: ${name}`,
});
return;
}
const { id } = result.data;
await refreshProjects(id);
setCreateNewOpen(false);
setName(undefined); // Clear the input for next time
toast({
title: `Created project: ${name}`,
});
})
.catch((e) => {
console.log(e);
toast({
variant: "destructive",
title: `Failed to create project: ${name}`,
});
});
}, [name, setCreateNewOpen, toast, refreshProjects]);
return (
<>
<Select onValueChange={onSelect} value={selectedId}>
<SelectTrigger
className={cn("!border-none", "!shadow-none", "!focus:ring-0", "!focus:ring-offset-0", className)}
>
<SelectValue placeholder="Choose Project" />
</SelectTrigger>
<SelectContent>
{projects.map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.name}
</SelectItem>
))}
<Separator />
<SelectItem value={"create-new"}>
<div className="flex flex-row items-center gap-1">
<Plus />
<div>New project</div>
</div>
</SelectItem>
</SelectContent>
</Select>
<Dialog open={createNewOpen} onOpenChange={setCreateNewOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>New project</DialogTitle>
</DialogHeader>
<Input
type="text"
placeholder="Name"
onChange={updateName}
value={name || ""}
onKeyDown={(e) => {
if (e.key === "Enter") {
createNew();
}
}}
/>
<DialogFooter>
<Button onClick={createNew}>Create</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}