aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apps/web/app/(dash)/dialogContentContainer.tsx223
-rw-r--r--apps/web/app/(dash)/dialogTriggerWrapper.tsx50
-rw-r--r--apps/web/app/(dash)/menu.tsx355
-rw-r--r--apps/web/migrations/0001_Adding_jobs_table.sql19
-rw-r--r--packages/ui/shadcn/combobox.tsx87
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