diff options
| -rw-r--r-- | apps/web/app/(dash)/dialogContentContainer.tsx | 223 | ||||
| -rw-r--r-- | apps/web/app/(dash)/dialogTriggerWrapper.tsx | 50 | ||||
| -rw-r--r-- | apps/web/app/(dash)/menu.tsx | 355 | ||||
| -rw-r--r-- | apps/web/migrations/0001_Adding_jobs_table.sql | 19 | ||||
| -rw-r--r-- | packages/ui/shadcn/combobox.tsx | 87 |
5 files changed, 215 insertions, 519 deletions
diff --git a/apps/web/app/(dash)/dialogContentContainer.tsx b/apps/web/app/(dash)/dialogContentContainer.tsx deleted file mode 100644 index 0b2b615c..00000000 --- a/apps/web/app/(dash)/dialogContentContainer.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { StoredSpace } from "@repo/db/schema"; -import { useEffect, useMemo, useState } from "react"; -import { createMemory, createSpace } from "../actions/doers"; -import ComboboxWithCreate from "@repo/ui/shadcn/combobox"; -import { toast } from "sonner"; -import { getSpaces } from "../actions/fetchers"; -import { MinusIcon, PlusCircleIcon } from "lucide-react"; -import { - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@repo/ui/shadcn/dialog"; -import { Label } from "@repo/ui/shadcn/label"; -import { Textarea } from "@repo/ui/shadcn/textarea"; -import { Button } from "@repo/ui/shadcn/button"; - -export function DialogContentContainer({ - DialogClose, -}: { - DialogClose: () => void; -}) { - const [spaces, setSpaces] = useState<StoredSpace[]>([]); - const [content, setContent] = useState(""); - const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]); - - const options = useMemo( - () => - spaces.map((x) => ({ - label: x.name, - value: x.id.toString(), - })), - [spaces], - ); - - const autoDetectedType = useMemo(() => { - if (content.length === 0) { - return "none"; - } - - if ( - content.match(/https?:\/\/(x\.com|twitter\.com)\/[\w]+\/[\w]+\/[\d]+/) - ) { - return "tweet"; - } else if (content.match(/https?:\/\/[\w\.]+/)) { - return "page"; - } else if (content.match(/https?:\/\/www\.[\w\.]+/)) { - return "page"; - } else { - return "note"; - } - }, [content]); - - const handleSubmit = async (content?: string, spaces?: number[]) => { - DialogClose(); - - toast.info("Creating memory...", { - icon: <PlusCircleIcon className="w-4 h-4 text-white animate-spin" />, - duration: 7500, - }); - - if (!content || content.length === 0) { - toast.error("Content is required"); - return; - } - - console.log(spaces); - - const cont = await createMemory({ - content: content, - spaces: spaces ?? undefined, - }); - - setContent(""); - setSelectedSpaces([]); - }; - - useEffect(() => { - (async () => { - let spaces = await getSpaces(); - - if (!spaces.success || !spaces.data) { - toast.warning("Unable to get spaces", { - richColors: true, - }); - setSpaces([]); - return; - } - setSpaces(spaces.data); - })(); - }, []); - - return ( - <DialogContent className="sm:max-w-[475px] text-[#F2F3F5] rounded-2xl bg-background z-[39]"> - <form - action={async (e: FormData) => { - const content = e.get("content")?.toString(); - - await handleSubmit(content, selectedSpaces); - }} - className="flex flex-col gap-4 " - > - <DialogHeader> - <DialogTitle>Add memory</DialogTitle> - <DialogDescription className="text-[#F2F3F5]"> - A "Memory" is a bookmark, something you want to remember. - </DialogDescription> - </DialogHeader> - - <div> - <Label htmlFor="name">Resource (URL or content)</Label> - <Textarea - className={`bg-[#2F353C] text-[#DBDEE1] max-h-[35vh] overflow-auto focus-visible:ring-0 border-none focus-visible:ring-offset-0 mt-2 ${/^https?:\/\/\S+$/i.test(content) && "text-[#1D9BF0] underline underline-offset-2"}`} - id="content" - name="content" - rows={8} - placeholder="Start typing a note or paste a URL here. I'll remember it." - value={content} - onChange={(e) => setContent(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSubmit(content, selectedSpaces); - } - }} - /> - </div> - - <div> - <Label className="space-y-1" htmlFor="space"> - <h3 className="font-semibold text-lg tracking-tight"> - Spaces (Optional) - </h3> - <p className="leading-normal text-[#F2F3F5] text-sm"> - A space is a collection of memories. It's a way to organise your - memories. - </p> - </Label> - - <ComboboxWithCreate - options={spaces.map((x) => ({ - label: x.name, - value: x.id.toString(), - }))} - onSelect={(v) => - setSelectedSpaces((prev) => { - if (v === "") { - return []; - } - return [...prev, parseInt(v)]; - }) - } - onSubmit={async (spaceName) => { - const space = options.find((x) => x.label === spaceName); - toast.info("Creating space..."); - - if (space) { - toast.error("A space with that name already exists."); - } - - const creationTask = await createSpace(spaceName); - if (creationTask.success && creationTask.data) { - toast.success("Space created " + creationTask.data); - setSpaces((prev) => [ - ...prev, - { - name: spaceName, - id: creationTask.data!, - createdAt: new Date(), - user: null, - numItems: 0, - }, - ]); - setSelectedSpaces((prev) => [...prev, creationTask.data!]); - } else { - toast.error("Space creation failed: " + creationTask.error); - } - }} - placeholder="Select or create a new space." - className="bg-[#2F353C] h-min rounded-md mt-4 mb-4" - /> - - <div> - {selectedSpaces.length > 0 && ( - <div className="flex flex-row flex-wrap gap-0.5 h-min"> - {[...new Set(selectedSpaces)].map((x, idx) => ( - <button - key={x} - type="button" - onClick={() => - setSelectedSpaces((prev) => prev.filter((y) => y !== x)) - } - className={`relative group p-2 py-3 bg-[#3C464D] max-w-32 ${ - idx === selectedSpaces.length - 1 ? "rounded-br-xl" : "" - }`} - > - <p className="line-clamp-1"> - {spaces.find((y) => y.id === x)?.name} - </p> - <div className="absolute h-full right-0 top-0 p-1 opacity-0 group-hover:opacity-100 items-center"> - <MinusIcon className="w-6 h-6 rounded-full bg-secondary" /> - </div> - </button> - ))} - </div> - )} - </div> - </div> - - <DialogFooter> - <Button - disabled={autoDetectedType === "none"} - variant={"secondary"} - type="submit" - > - Save {autoDetectedType != "none" && autoDetectedType} - </Button> - </DialogFooter> - </form> - </DialogContent> - ); -} diff --git a/apps/web/app/(dash)/dialogTriggerWrapper.tsx b/apps/web/app/(dash)/dialogTriggerWrapper.tsx deleted file mode 100644 index 1e07e429..00000000 --- a/apps/web/app/(dash)/dialogTriggerWrapper.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client"; - -import { Dialog, DialogTrigger } from "@repo/ui/shadcn/dialog"; -import { useState } from "react"; -import { DialogContentContainer } from "./dialogContentContainer"; -import { PlusIcon } from "@heroicons/react/24/solid"; - -export function DialogDesktopTrigger() { - return ( - <DialogTriggerWrapper> - <div className="border-gray-700/50 border-[1px] space-y-4 group relative bg-secondary shadow-md shadow-[#1d1d1dc7] rounded-xl flex justify-center"> - <button className="cursor-pointer p-2 hover:scale-105 hover:text-[#bfc4c9] active:scale-90"> - <PlusIcon className="h-6 w-6" /> - </button> - <div className="opacity-0 group-hover:opacity-100 scale-x-50 group-hover:scale-x-100 origin-left transition-all absolute whitespace-nowrap pointer-events-none border-gray-700/50 border-[1px] bg-[#1F2428] shadow-md shadow-[#1d1d1dc7] rounded-xl px-2 py-1 left-[120%] -top-2"> - Add Memories - </div> - </div> - </DialogTriggerWrapper> - ); -} - -export function DialogMobileTrigger() { - return ( - <DialogTriggerWrapper> - <div className={`flex flex-col items-center cursor-pointer text-white`}> - <PlusIcon className="h-6 w-6 hover:brightness-125 focus:brightness-125 duration-200 stroke-white" /> - <p className="text-xs text-foreground-menu mt-2">Add</p> - </div> - </DialogTriggerWrapper> - ); -} -export default function DialogTriggerWrapper({ - children, -}: { - children: React.ReactNode; -}) { - const [dialogOpen, setDialogOpen] = useState(false); - - return ( - <Dialog open={dialogOpen} onOpenChange={setDialogOpen}> - <DialogTrigger>{children}</DialogTrigger> - <DialogContentContainer - DialogClose={() => { - setDialogOpen(false); - }} - /> - </Dialog> - ); -} diff --git a/apps/web/app/(dash)/menu.tsx b/apps/web/app/(dash)/menu.tsx index bdc1aa8a..51e97108 100644 --- a/apps/web/app/(dash)/menu.tsx +++ b/apps/web/app/(dash)/menu.tsx @@ -5,7 +5,6 @@ import Image from "next/image"; import Link from "next/link"; import { MemoriesIcon, - ExploreIcon, CanvasIcon, AddIcon, HomeIcon as HomeIconWeb, @@ -29,36 +28,10 @@ import { HomeIcon } from "@heroicons/react/24/solid"; import { createMemory, createSpace } from "../actions/doers"; import ComboboxWithCreate from "@repo/ui/shadcn/combobox"; import { StoredSpace } from "@repo/db/schema"; -import useMeasure from "react-use-measure"; import { useKeyPress } from "@/lib/useKeyPress"; import { useFormStatus } from "react-dom"; function Menu() { - const [spaces, setSpaces] = useState<StoredSpace[]>([]); - - function SubmitButton() { - const status = useFormStatus(); - return ( - <Button disabled={status.pending} variant={"secondary"} type="submit"> - Save {autoDetectedType != "none" && autoDetectedType} - </Button> - ); - } - - useEffect(() => { - (async () => { - let spaces = await getSpaces(); - - if (!spaces.success || !spaces.data) { - toast.warning("Unable to get spaces", { - richColors: true, - }); - setSpaces([]); - return; - } - setSpaces(spaces.data); - })(); - }, []); useKeyPress("a", () => { if (!dialogOpen) { setDialogOpen(true); @@ -85,6 +58,130 @@ function Menu() { }, ]; + + + const [dialogOpen, setDialogOpen] = useState(false); + + return ( + <> + {/* Desktop Menu */} + <Dialog open={dialogOpen} onOpenChange={setDialogOpen}> + <div className="hidden lg:flex fixed h-screen pb-20 w-full p-4 items-center justify-start top-0 left-0 pointer-events-none z-[39]"> + <div className="pointer-events-auto group flex w-14 text-foreground-menu text-[15px] font-medium flex-col items-start gap-6 overflow-hidden rounded-[28px] border-2 border-border bg-secondary px-3 py-4 duration-200 hover:w-40 z-[99999]"> + <div className="border-b border-border pb-4 w-full"> + <DialogTrigger + className={`flex w-full text-white brightness-75 hover:brightness-125 focus:brightness-125 cursor-pointer items-center gap-3 px-1 duration-200 justify-start`} + > + <Image + src={AddIcon} + alt="Logo" + width={24} + height={24} + className="hover:brightness-125 focus:brightness-125 duration-200 text-white" + /> + <p className="opacity-0 duration-200 group-hover:opacity-100"> + Add + </p> + </DialogTrigger> + </div> + {menuItems.map((item) => ( + <Link + aria-disabled={item.disabled} + href={item.disabled ? "#" : item.url} + key={item.url} + className={`flex w-full ${ + item.disabled + ? "cursor-not-allowed opacity-30" + : "text-white brightness-75 hover:brightness-125 cursor-pointer" + } items-center gap-3 px-1 duration-200 hover:scale-105 active:scale-90 justify-start`} + > + <Image + src={item.icon} + alt={`${item.text} icon`} + width={24} + height={24} + className="hover:brightness-125 duration-200" + /> + <p className="opacity-0 duration-200 group-hover:opacity-100"> + {item.text} + </p> + </Link> + ))} + </div> + </div> + + <DialogContentMenu setDialogClose={()=> setDialogOpen(false)} /> + + {/* Mobile Menu */} + <div className="lg:hidden fixed bottom-0 left-0 w-full p-4 bg-secondary z-50 border-t-2 border-border"> + <div className="flex justify-around items-center"> + <Link + href={"/"} + className={`flex flex-col items-center text-white ${"cursor-pointer"}`} + > + <HomeIcon width={24} height={24} /> + <p className="text-xs text-foreground-menu mt-2">Home</p> + </Link> + + <DialogTrigger + className={`flex flex-col items-center cursor-pointer text-white`} + > + <Image + src={AddIcon} + alt="Logo" + width={24} + height={24} + className="hover:brightness-125 focus:brightness-125 duration-200 stroke-white" + /> + <p className="text-xs text-foreground-menu mt-2">Add</p> + </DialogTrigger> + {menuItems.slice(1, 2).map((item) => ( + <Link + aria-disabled={item.disabled} + href={item.disabled ? "#" : item.url} + key={item.url} + className={`flex flex-col items-center ${ + item.disabled + ? "opacity-50 cursor-not-allowed" + : "cursor-pointer" + }`} + onClick={(e) => item.disabled && e.preventDefault()} + > + <Image + src={item.icon} + alt={`${item.text} icon`} + width={24} + height={24} + /> + <p className="text-xs text-foreground-menu mt-2">{item.text}</p> + </Link> + ))} + </div> + </div> + </Dialog> + </> + ); +} + +function DialogContentMenu({setDialogClose}: {setDialogClose: ()=> void}){ + + const [spaces, setSpaces] = useState<StoredSpace[]>([]); + + useEffect(() => { + (async () => { + let spaces = await getSpaces(); + + if (!spaces.success || !spaces.data) { + toast.warning("Unable to get spaces", { + richColors: true, + }); + setSpaces([]); + return; + } + setSpaces(spaces.data); + })(); + }, []); + const [content, setContent] = useState(""); const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]); @@ -108,8 +205,6 @@ function Menu() { } }, [content]); - const [dialogOpen, setDialogOpen] = useState(false); - const options = useMemo( () => spaces.map((x) => ({ @@ -120,7 +215,7 @@ function Menu() { ); const handleSubmit = async (content?: string, spaces?: number[]) => { - setDialogOpen(false); + setDialogClose(); if (!content || content.length === 0) { throw new Error("Content is required"); } @@ -131,7 +226,7 @@ function Menu() { setContent(""); setSelectedSpaces([]); return cont; - }; + } const formSubmit = () => { toast.promise(handleSubmit(content, selectedSpaces), { @@ -146,95 +241,18 @@ function Menu() { richColors: true, }); }; - return ( - <> - {/* Desktop Menu */} - <Dialog open={dialogOpen} onOpenChange={setDialogOpen}> - <div className="hidden lg:flex fixed h-screen pb-20 w-full p-4 items-center justify-start top-0 left-0 pointer-events-none z-[39]"> - <div className="pointer-events-auto group flex w-14 text-foreground-menu text-[15px] font-medium flex-col items-start gap-6 overflow-hidden rounded-[28px] border-2 border-border bg-secondary px-3 py-4 duration-200 hover:w-40 z-[99999]"> - <div className="border-b border-border pb-4 w-full"> - <DialogTrigger - className={`flex w-full text-white brightness-75 hover:brightness-125 focus:brightness-125 cursor-pointer items-center gap-3 px-1 duration-200 justify-start`} - > - <Image - src={AddIcon} - alt="Logo" - width={24} - height={24} - className="hover:brightness-125 focus:brightness-125 duration-200 text-white" - /> - <p className="opacity-0 duration-200 group-hover:opacity-100"> - Add - </p> - </DialogTrigger> - </div> - {menuItems.map((item) => ( - <Link - aria-disabled={item.disabled} - href={item.disabled ? "#" : item.url} - key={item.url} - className={`flex w-full ${ - item.disabled - ? "cursor-not-allowed opacity-30" - : "text-white brightness-75 hover:brightness-125 cursor-pointer" - } items-center gap-3 px-1 duration-200 hover:scale-105 active:scale-90 justify-start`} - > - <Image - src={item.icon} - alt={`${item.text} icon`} - width={24} - height={24} - className="hover:brightness-125 duration-200" - /> - <p className="opacity-0 duration-200 group-hover:opacity-100"> - {item.text} - </p> - </Link> - ))} - </div> - </div> - - <DialogContent className="sm:max-w-[475px] text-[#F2F3F5] rounded-2xl bg-background z-[39]"> + <DialogContent className="sm:max-w-[575px] text-[#F2F3F5] rounded-2xl bg-background z-[39]"> <form action={formSubmit} className="flex flex-col gap-4 "> - <DialogHeader> - <DialogTitle>Add memory</DialogTitle> - <DialogDescription className="text-[#F2F3F5]"> - A "Memory" is a bookmark, something you want to remember. - </DialogDescription> - </DialogHeader> - - <div> - <Label htmlFor="name">Resource (URL or content)</Label> - <Textarea - className={`bg-[#2F353C] text-[#DBDEE1] max-h-[35vh] overflow-auto focus-visible:ring-0 border-none focus-visible:ring-offset-0 mt-2 ${/^https?:\/\/\S+$/i.test(content) && "text-[#1D9BF0] underline underline-offset-2"}`} - id="content" - name="content" - rows={8} - placeholder="Start typing a note or paste a URL here. I'll remember it." - value={content} - onChange={(e) => setContent(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - formSubmit(); - } - }} - /> - </div> + <DialogHeader> + <DialogTitle>Add memory</DialogTitle> + </DialogHeader> <div> - <Label className="space-y-1" htmlFor="space"> - <h3 className="font-semibold text-lg tracking-tight"> - Spaces (Optional) - </h3> - <p className="leading-normal text-[#F2F3F5] text-sm"> - A space is a collection of memories. It's a way to organise - your memories. - </p> - </Label> - + <Label htmlFor="space">Save to Spaces (Optional)</Label> <ComboboxWithCreate + setSelectedSpaces={setSelectedSpaces} + selectedSpaces={selectedSpaces} options={spaces.map((x) => ({ label: x.name, value: x.id.toString(), @@ -273,95 +291,42 @@ function Menu() { toast.error("Space creation failed: " + creationTask.error); } }} - placeholder="Select or create a new space." - className="bg-[#2F353C] h-min rounded-md mt-4 mb-4" /> + </div> - <div> - {selectedSpaces.length > 0 && ( - <div className="flex flex-row flex-wrap gap-0.5 h-min"> - {[...new Set(selectedSpaces)].map((x, idx) => ( - <button - key={x} - type="button" - onClick={() => - setSelectedSpaces((prev) => - prev.filter((y) => y !== x), - ) - } - className={`relative group p-2 py-3 bg-[#3C464D] max-w-32 ${ - idx === selectedSpaces.length - 1 - ? "rounded-br-xl" - : "" - }`} - > - <p className="line-clamp-1"> - {spaces.find((y) => y.id === x)?.name} - </p> - <div className="absolute h-full right-0 top-0 p-1 opacity-0 group-hover:opacity-100 items-center"> - <MinusIcon className="w-6 h-6 rounded-full bg-secondary" /> - </div> - </button> - ))} - </div> - )} - </div> + <div> + <Label htmlFor="name">Resource (URL or content)</Label> + <Textarea + className={`bg-[#2F353C] text-[#DBDEE1] max-h-[35vh] overflow-auto focus-visible:ring-0 border-none focus-visible:ring-offset-0 mt-2 ${/^https?:\/\/\S+$/i.test(content) && "text-[#1D9BF0] underline underline-offset-2"}`} + id="content" + name="content" + rows={8} + placeholder="Start typing a note or paste a URL here. I'll remember it." + value={content} + onChange={(e) => setContent(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + formSubmit(); + } + }} + /> </div> <DialogFooter> - <SubmitButton /> + <SubmitButton autoDetectedType={autoDetectedType} /> </DialogFooter> </form> </DialogContent> + ) +} - {/* Mobile Menu */} - <div className="lg:hidden fixed bottom-0 left-0 w-full p-4 bg-secondary z-50 border-t-2 border-border"> - <div className="flex justify-around items-center"> - <Link - href={"/"} - className={`flex flex-col items-center text-white ${"cursor-pointer"}`} - > - <HomeIcon width={24} height={24} /> - <p className="text-xs text-foreground-menu mt-2">Home</p> - </Link> - - <DialogTrigger - className={`flex flex-col items-center cursor-pointer text-white`} - > - <Image - src={AddIcon} - alt="Logo" - width={24} - height={24} - className="hover:brightness-125 focus:brightness-125 duration-200 stroke-white" - /> - <p className="text-xs text-foreground-menu mt-2">Add</p> - </DialogTrigger> - {menuItems.slice(1, 2).map((item) => ( - <Link - aria-disabled={item.disabled} - href={item.disabled ? "#" : item.url} - key={item.url} - className={`flex flex-col items-center ${ - item.disabled - ? "opacity-50 cursor-not-allowed" - : "cursor-pointer" - }`} - onClick={(e) => item.disabled && e.preventDefault()} - > - <Image - src={item.icon} - alt={`${item.text} icon`} - width={24} - height={24} - /> - <p className="text-xs text-foreground-menu mt-2">{item.text}</p> - </Link> - ))} - </div> - </div> - </Dialog> - </> +function SubmitButton({autoDetectedType}: {autoDetectedType: string}) { + const status = useFormStatus(); + return ( + <Button disabled={status.pending} variant={"secondary"} type="submit"> + Save {autoDetectedType != "none" && autoDetectedType} + </Button> ); } diff --git a/apps/web/migrations/0001_Adding_jobs_table.sql b/apps/web/migrations/0001_Adding_jobs_table.sql deleted file mode 100644 index 7a687f72..00000000 --- a/apps/web/migrations/0001_Adding_jobs_table.sql +++ /dev/null @@ -1,19 +0,0 @@ --- Migration number: 0001 2024-08-05T18:05:16.793Z -CREATE TABLE `jobs` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `userId` text NOT NULL, - `url` text NOT NULL, - `status` text NOT NULL, - `attempts` integer DEFAULT 0 NOT NULL, - `lastAttemptAt` integer, - `error` blob, - `createdAt` integer NOT NULL, - `updatedAt` integer NOT NULL, - FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade -); - - -CREATE INDEX `jobs_userId_idx` ON `jobs` (`userId`);--> statement-breakpoint -CREATE INDEX `jobs_status_idx` ON `jobs` (`status`);--> statement-breakpoint -CREATE INDEX `jobs_createdAt_idx` ON `jobs` (`createdAt`);--> statement-breakpoint -CREATE INDEX `jobs_url_idx` ON `jobs` (`url`);--> statement-breakpoint
\ No newline at end of file diff --git a/packages/ui/shadcn/combobox.tsx b/packages/ui/shadcn/combobox.tsx index bb8de9df..7ebb1ef4 100644 --- a/packages/ui/shadcn/combobox.tsx +++ b/packages/ui/shadcn/combobox.tsx @@ -1,7 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; -import { cn } from "../lib/utils"; +import { useState } from "react"; import { Button } from "./button"; import { Command, @@ -20,59 +19,83 @@ interface ComboboxWithCreateProps { options: Option[]; onSelect: (value: string) => void; onSubmit: (newName: string) => void; - placeholder?: string; - emptyMessage?: string; - createNewMessage?: string; - className?: string; + selectedSpaces: number[]; + setSelectedSpaces: React.Dispatch<React.SetStateAction<number[]>>; } -const ComboboxWithCreate: React.FC<ComboboxWithCreateProps> = ({ - options: initialOptions, +const ComboboxWithCreate = ({ + options, onSelect, onSubmit, - placeholder = "Select an option", - emptyMessage = "No option found.", - createNewMessage = "Create - ", - className, -}) => { - const [options, setOptions] = useState<Option[]>(initialOptions); + selectedSpaces, + setSelectedSpaces, +}: ComboboxWithCreateProps) => { const [inputValue, setInputValue] = useState(""); - useEffect(() => { - setOptions(initialOptions); - }, [initialOptions]); + const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { + setInputValue(e.target.value); + }; + + const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + if ( + e.key === "Backspace" && + inputValue === "" && + selectedSpaces.length > 0 + ) { + setSelectedSpaces((prev) => prev.slice(0, -1)); + } + }; + + const filteredOptions = options.filter( + (option) => !selectedSpaces.includes(parseInt(option.value)), + ); return ( - <Command className={cn("group", className)}> + <Command + className={`group flex bg-[#2F353C] h-min rounded-md ${selectedSpaces.length > 0 && "p-2"} transition-all mt-4 mb-4`} + > + <div className="inline-flex flex-wrap gap-1"> + {selectedSpaces.map((spaceId) => ( + <button + key={spaceId} + type="button" + onClick={() => + setSelectedSpaces((prev) => prev.filter((id) => id !== spaceId)) + } + className="relative group rounded-md py-1 px-2 bg-[#3C464D] max-w-32" + > + <p className="line-clamp-1"> + {options.find((opt) => opt.value === spaceId.toString())?.label} + </p> + </button> + ))} + </div> <CommandInput - onChangeCapture={(e: React.ChangeEvent<HTMLInputElement>) => - setInputValue(e.currentTarget.value) - } - placeholder={placeholder} + onChangeCapture={handleInputChange} + onKeyDown={handleKeyDown} + placeholder="Select or create a new space." value={inputValue} /> - <CommandList className="z-10 translate-y-12 translate-x-5 opacity-0 absolute group-focus-within:opacity-100 bg-secondary p-2 rounded-b-xl max-w-64"> + <CommandList className={`z-10 translate-x-5 opacity-0 transition-all absolute group-focus-within:opacity-100 bg-secondary p-2 rounded-b-xl max-w-64 ${selectedSpaces.length > 0 ?"translate-y-20": "translate-y-12"}`}> <CommandGroup className="hidden group-focus-within:block"> - {options.map((option, idx) => ( + {filteredOptions.map((option) => ( <CommandItem - key={`opt-${idx}`} + key={option.value} onSelect={() => onSelect(option.value)} > {option.label} </CommandItem> ))} - {!options.map((opts) => opts.label).includes(inputValue) && ( + {!options.map((opt) => opt.label).includes(inputValue) && ( <Button className="px-1" type="button" - onClick={async () => onSubmit(inputValue)} + onClick={() => onSubmit(inputValue)} variant="link" - disabled={inputValue.length === 0} + disabled={inputValue.length < 1} > {inputValue.length > 0 ? ( - <> - {createNewMessage} "{inputValue}" - </> + <>Create - "{inputValue}"</> ) : ( <>Type to create a new space</> )} @@ -84,4 +107,4 @@ const ComboboxWithCreate: React.FC<ComboboxWithCreateProps> = ({ ); }; -export default ComboboxWithCreate; +export default ComboboxWithCreate;
\ No newline at end of file |