blob: 312e6aa063084fb327a6733293d86ceb9452275d [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]);
giod0026612025-05-08 13:00:36 +0000120 return (
gio74ab7852025-05-13 13:19:31 +0000121 <>
giob45b1862025-05-20 11:42:20 +0000122 <Select onValueChange={onSelect} value={projectId}>
gio8cadbc72025-05-16 07:51:02 +0000123 <SelectTrigger
124 className={cn("!border-none", "!shadow-none", "!focus:ring-0", "!focus:ring-offset-0", className)}
125 >
gio74ab7852025-05-13 13:19:31 +0000126 <SelectValue placeholder="Choose Project" />
127 </SelectTrigger>
128 <SelectContent>
giob45b1862025-05-20 11:42:20 +0000129 {projects?.map((p) => (
gio74ab7852025-05-13 13:19:31 +0000130 <SelectItem key={p.id} value={p.id}>
131 {p.name}
132 </SelectItem>
133 ))}
giob45b1862025-05-20 11:42:20 +0000134 {(projects || []).length > 0 && <Separator />}
135 <SelectItem key="create-new" value={"create-new"}>
gio74ab7852025-05-13 13:19:31 +0000136 <div className="flex flex-row items-center gap-1">
giobc47f9f2025-05-12 08:31:07 +0000137 <Plus />
gio74ab7852025-05-13 13:19:31 +0000138 <div>New project</div>
139 </div>
140 </SelectItem>
141 </SelectContent>
142 </Select>
143 <Dialog open={createNewOpen} onOpenChange={setCreateNewOpen}>
144 <DialogContent>
145 <DialogHeader>
146 <DialogTitle>New project</DialogTitle>
147 </DialogHeader>
148 <Input
149 type="text"
150 placeholder="Name"
151 onChange={updateName}
152 value={name || ""}
153 onKeyDown={(e) => {
154 if (e.key === "Enter") {
155 createNew();
156 }
157 }}
158 />
159 <DialogFooter>
160 <Button onClick={createNew}>Create</Button>
161 </DialogFooter>
162 </DialogContent>
163 </Dialog>
164 </>
giod0026612025-05-08 13:00:36 +0000165 );
166}