blob: 9b8bb452ac7497d093817975b38cf9d9508f4a98 [file] [log] [blame]
gio5f2f1002025-03-20 18:38:48 +04001import { ChangeEvent, useCallback, useEffect, useState } from "react";
2import { Project, useProjectId, useStateStore } from "./lib/state";
3import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./components/ui/select";
gio5f2f1002025-03-20 18:38:48 +04004import { Input } from "./components/ui/input";
5import { Button } from "./components/ui/button";
gio74ab7852025-05-13 13:19:31 +00006import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "./components/ui/dialog";
gio5f2f1002025-03-20 18:38:48 +04007import { useToast } from "@/hooks/use-toast";
giobc47f9f2025-05-12 08:31:07 +00008import { Separator } from "./components/ui/separator";
9import { Plus } from "lucide-react";
gio74ab7852025-05-13 13:19:31 +000010import { z } from "zod";
gio8cadbc72025-05-16 07:51:02 +000011import { cn } from "./lib/utils";
gio74ab7852025-05-13 13:19:31 +000012
13const createNewSchema = z.object({
14 id: z.string().min(1),
15});
gio5f2f1002025-03-20 18:38:48 +040016
gio8cadbc72025-05-16 07:51:02 +000017export function ProjectSelect({ className }: { className?: string }) {
giod0026612025-05-08 13:00:36 +000018 const { toast } = useToast();
19 const store = useStateStore();
20 const [projects, setProjects] = useState<Project[]>([]);
gio7461e502025-05-12 10:11:55 +000021 const projectId = useProjectId();
gio74ab7852025-05-13 13:19:31 +000022 // Track the selected project ID locally to ensure UI consistency
23 const [selectedId, setSelectedId] = useState<string | undefined>(projectId);
gio7461e502025-05-12 10:11:55 +000024
gio74ab7852025-05-13 13:19:31 +000025 // Keep local state in sync with global state
gio7461e502025-05-12 10:11:55 +000026 useEffect(() => {
gio74ab7852025-05-13 13:19:31 +000027 setSelectedId(projectId);
28 }, [projectId]);
29
30 const refreshProjects = useCallback(
31 async (id?: string) => {
32 console.log("refreshProjects", id);
33 try {
34 const resp = await fetch("/api/project");
35 const projectList = await resp.json();
36 const sortedProjects = [...projectList].sort((a, b) =>
37 a.name.localeCompare(b.name, undefined, { sensitivity: "base" }),
38 );
39 setProjects(sortedProjects);
40 if (id && !sortedProjects.some((p) => p.id === id)) {
41 throw new Error("MUST NOT REACH!");
42 }
43 if (id == null) {
44 id = sortedProjects[0].id;
45 }
46 if (id !== selectedId) {
47 setSelectedId(id);
48 store.setProject(id);
49 }
50 } catch (e) {
51 console.log(e);
52 }
53 },
54 [selectedId, store, setProjects, setSelectedId],
55 );
56
giod0026612025-05-08 13:00:36 +000057 useEffect(() => {
58 refreshProjects();
gio359a6852025-05-14 03:38:24 +000059 }, [refreshProjects]);
gio7461e502025-05-12 10:11:55 +000060
giod0026612025-05-08 13:00:36 +000061 const [createNewOpen, setCreateNewOpen] = useState(false);
62 const onSelect = useCallback(
63 (projectId: string) => {
64 if (projectId === "create-new") {
65 setCreateNewOpen(true);
66 } else {
gio74ab7852025-05-13 13:19:31 +000067 setSelectedId(projectId);
giod0026612025-05-08 13:00:36 +000068 store.setProject(projectId);
69 }
70 },
gio74ab7852025-05-13 13:19:31 +000071 [store, setSelectedId],
giod0026612025-05-08 13:00:36 +000072 );
gio74ab7852025-05-13 13:19:31 +000073
giod0026612025-05-08 13:00:36 +000074 const [name, setName] = useState<string | undefined>(undefined);
75 const updateName = useCallback(
76 (e: ChangeEvent<HTMLInputElement>) => {
77 setName(e.target.value);
78 },
79 [setName],
80 );
gio74ab7852025-05-13 13:19:31 +000081
giod0026612025-05-08 13:00:36 +000082 const createNew = useCallback(() => {
giod0026612025-05-08 13:00:36 +000083 if (!name) {
gio74ab7852025-05-13 13:19:31 +000084 toast({
85 variant: "destructive",
86 title: "Name is required",
87 });
giod0026612025-05-08 13:00:36 +000088 return;
89 }
90 fetch("/api/project", {
91 method: "POST",
92 headers: {
93 "Content-Type": "application/json",
94 },
95 body: JSON.stringify({
96 name: name,
97 }),
98 })
99 .then(async (resp) => {
100 if (!resp.ok) {
gio74ab7852025-05-13 13:19:31 +0000101 return false;
102 }
103 const result = createNewSchema.safeParse(await resp.json());
104 if (!result.success) {
105 toast({
106 variant: "destructive",
107 title: `Failed to create project: ${name}`,
108 });
giod0026612025-05-08 13:00:36 +0000109 return;
110 }
gio74ab7852025-05-13 13:19:31 +0000111 const { id } = result.data;
112 await refreshProjects(id);
giod0026612025-05-08 13:00:36 +0000113 setCreateNewOpen(false);
gio74ab7852025-05-13 13:19:31 +0000114 setName(undefined); // Clear the input for next time
giod0026612025-05-08 13:00:36 +0000115 toast({
116 title: `Created project: ${name}`,
117 });
118 })
119 .catch((e) => {
120 console.log(e);
121 toast({
122 variant: "destructive",
123 title: `Failed to create project: ${name}`,
124 });
125 });
gio74ab7852025-05-13 13:19:31 +0000126 }, [name, setCreateNewOpen, toast, refreshProjects]);
127
giod0026612025-05-08 13:00:36 +0000128 return (
gio74ab7852025-05-13 13:19:31 +0000129 <>
130 <Select onValueChange={onSelect} value={selectedId}>
gio8cadbc72025-05-16 07:51:02 +0000131 <SelectTrigger
132 className={cn("!border-none", "!shadow-none", "!focus:ring-0", "!focus:ring-offset-0", className)}
133 >
gio74ab7852025-05-13 13:19:31 +0000134 <SelectValue placeholder="Choose Project" />
135 </SelectTrigger>
136 <SelectContent>
137 {projects.map((p) => (
138 <SelectItem key={p.id} value={p.id}>
139 {p.name}
140 </SelectItem>
141 ))}
142 <Separator />
143 <SelectItem value={"create-new"}>
144 <div className="flex flex-row items-center gap-1">
giobc47f9f2025-05-12 08:31:07 +0000145 <Plus />
gio74ab7852025-05-13 13:19:31 +0000146 <div>New project</div>
147 </div>
148 </SelectItem>
149 </SelectContent>
150 </Select>
151 <Dialog open={createNewOpen} onOpenChange={setCreateNewOpen}>
152 <DialogContent>
153 <DialogHeader>
154 <DialogTitle>New project</DialogTitle>
155 </DialogHeader>
156 <Input
157 type="text"
158 placeholder="Name"
159 onChange={updateName}
160 value={name || ""}
161 onKeyDown={(e) => {
162 if (e.key === "Enter") {
163 createNew();
164 }
165 }}
166 />
167 <DialogFooter>
168 <Button onClick={createNew}>Create</Button>
169 </DialogFooter>
170 </DialogContent>
171 </Dialog>
172 </>
giod0026612025-05-08 13:00:36 +0000173 );
174}