blob: 278db67e197eb012fec3447157659496dd47de9f [file] [log] [blame]
gio5f2f1002025-03-20 18:38:48 +04001import { ChangeEvent, useCallback, useEffect, useState } from "react";
giob45b1862025-05-20 11:42:20 +00002import { Project, useProjectId, useSetProject } from "./lib/state";
gio5f2f1002025-03-20 18:38:48 +04003import { 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();
giob45b1862025-05-20 11:42:20 +000019 const [projects, setProjects] = useState<Project[] | null>(null);
gio7461e502025-05-12 10:11:55 +000020 const projectId = useProjectId();
giob45b1862025-05-20 11:42:20 +000021 const setProject = useSetProject();
gio74ab7852025-05-13 13:19:31 +000022 const refreshProjects = useCallback(
23 async (id?: string) => {
24 console.log("refreshProjects", id);
25 try {
26 const resp = await fetch("/api/project");
27 const projectList = await resp.json();
28 const sortedProjects = [...projectList].sort((a, b) =>
29 a.name.localeCompare(b.name, undefined, { sensitivity: "base" }),
30 );
giob45b1862025-05-20 11:42:20 +000031 console.log(projectId, id, sortedProjects);
gio74ab7852025-05-13 13:19:31 +000032 setProjects(sortedProjects);
33 if (id && !sortedProjects.some((p) => p.id === id)) {
34 throw new Error("MUST NOT REACH!");
35 }
36 if (id == null) {
giob45b1862025-05-20 11:42:20 +000037 if (projectId == null) {
38 id = sortedProjects[0].id;
39 } else {
40 id = projectId;
41 }
gio74ab7852025-05-13 13:19:31 +000042 }
giob45b1862025-05-20 11:42:20 +000043 setProject(id);
gio74ab7852025-05-13 13:19:31 +000044 } catch (e) {
45 console.log(e);
46 }
47 },
giob45b1862025-05-20 11:42:20 +000048 [projectId, setProject, setProjects],
gio74ab7852025-05-13 13:19:31 +000049 );
giod0026612025-05-08 13:00:36 +000050 useEffect(() => {
giob45b1862025-05-20 11:42:20 +000051 if (projects == null) {
52 refreshProjects();
53 }
54 }, [refreshProjects, projects]);
giod0026612025-05-08 13:00:36 +000055 const [createNewOpen, setCreateNewOpen] = useState(false);
56 const onSelect = useCallback(
giob45b1862025-05-20 11:42:20 +000057 (id: string) => {
58 if (id === "create-new") {
giod0026612025-05-08 13:00:36 +000059 setCreateNewOpen(true);
60 } else {
giob45b1862025-05-20 11:42:20 +000061 setProject(id);
giod0026612025-05-08 13:00:36 +000062 }
63 },
giob45b1862025-05-20 11:42:20 +000064 [setProject],
giod0026612025-05-08 13:00:36 +000065 );
gio74ab7852025-05-13 13:19:31 +000066
giod0026612025-05-08 13:00:36 +000067 const [name, setName] = useState<string | undefined>(undefined);
68 const updateName = useCallback(
69 (e: ChangeEvent<HTMLInputElement>) => {
70 setName(e.target.value);
71 },
72 [setName],
73 );
gio74ab7852025-05-13 13:19:31 +000074
giod0026612025-05-08 13:00:36 +000075 const createNew = useCallback(() => {
giod0026612025-05-08 13:00:36 +000076 if (!name) {
gio74ab7852025-05-13 13:19:31 +000077 toast({
78 variant: "destructive",
79 title: "Name is required",
80 });
giod0026612025-05-08 13:00:36 +000081 return;
82 }
83 fetch("/api/project", {
84 method: "POST",
85 headers: {
86 "Content-Type": "application/json",
87 },
88 body: JSON.stringify({
89 name: name,
90 }),
91 })
92 .then(async (resp) => {
93 if (!resp.ok) {
gio74ab7852025-05-13 13:19:31 +000094 return false;
95 }
96 const result = createNewSchema.safeParse(await resp.json());
97 if (!result.success) {
98 toast({
99 variant: "destructive",
100 title: `Failed to create project: ${name}`,
101 });
giod0026612025-05-08 13:00:36 +0000102 return;
103 }
gio74ab7852025-05-13 13:19:31 +0000104 const { id } = result.data;
105 await refreshProjects(id);
giod0026612025-05-08 13:00:36 +0000106 setCreateNewOpen(false);
gio74ab7852025-05-13 13:19:31 +0000107 setName(undefined); // Clear the input for next time
giod0026612025-05-08 13:00:36 +0000108 toast({
109 title: `Created project: ${name}`,
110 });
111 })
112 .catch((e) => {
113 console.log(e);
114 toast({
115 variant: "destructive",
116 title: `Failed to create project: ${name}`,
117 });
118 });
gio74ab7852025-05-13 13:19:31 +0000119 }, [name, setCreateNewOpen, toast, refreshProjects]);
giob45b1862025-05-20 11:42:20 +0000120 console.log("asd", projectId);
giod0026612025-05-08 13:00:36 +0000121 return (
gio74ab7852025-05-13 13:19:31 +0000122 <>
giob45b1862025-05-20 11:42:20 +0000123 <Select onValueChange={onSelect} value={projectId}>
gio8cadbc72025-05-16 07:51:02 +0000124 <SelectTrigger
125 className={cn("!border-none", "!shadow-none", "!focus:ring-0", "!focus:ring-offset-0", className)}
126 >
gio74ab7852025-05-13 13:19:31 +0000127 <SelectValue placeholder="Choose Project" />
128 </SelectTrigger>
129 <SelectContent>
giob45b1862025-05-20 11:42:20 +0000130 {projects?.map((p) => (
gio74ab7852025-05-13 13:19:31 +0000131 <SelectItem key={p.id} value={p.id}>
132 {p.name}
133 </SelectItem>
134 ))}
giob45b1862025-05-20 11:42:20 +0000135 {(projects || []).length > 0 && <Separator />}
136 <SelectItem key="create-new" value={"create-new"}>
gio74ab7852025-05-13 13:19:31 +0000137 <div className="flex flex-row items-center gap-1">
giobc47f9f2025-05-12 08:31:07 +0000138 <Plus />
gio74ab7852025-05-13 13:19:31 +0000139 <div>New project</div>
140 </div>
141 </SelectItem>
142 </SelectContent>
143 </Select>
144 <Dialog open={createNewOpen} onOpenChange={setCreateNewOpen}>
145 <DialogContent>
146 <DialogHeader>
147 <DialogTitle>New project</DialogTitle>
148 </DialogHeader>
149 <Input
150 type="text"
151 placeholder="Name"
152 onChange={updateName}
153 value={name || ""}
154 onKeyDown={(e) => {
155 if (e.key === "Enter") {
156 createNew();
157 }
158 }}
159 />
160 <DialogFooter>
161 <Button onClick={createNew}>Create</Button>
162 </DialogFooter>
163 </DialogContent>
164 </Dialog>
165 </>
giod0026612025-05-08 13:00:36 +0000166 );
167}