aboutsummaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/app/globals.css6
-rw-r--r--apps/web/src/app/layout.tsx6
-rw-r--r--apps/web/src/app/ui/page.tsx2
-rw-r--r--apps/web/src/components/Sidebar.tsx145
-rw-r--r--apps/web/src/components/Sidebar/PagesItem.tsx189
-rw-r--r--apps/web/src/components/Sidebar/index.tsx46
-rw-r--r--apps/web/src/components/ui/drawer.tsx118
-rw-r--r--apps/web/src/components/ui/input.tsx18
-rw-r--r--apps/web/src/components/ui/popover.tsx31
-rw-r--r--apps/web/src/components/ui/textarea.tsx24
-rw-r--r--apps/web/src/lib/utils.ts19
11 files changed, 444 insertions, 160 deletions
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index 394bda43..2bb3159a 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -20,7 +20,7 @@
}
body {
- @apply bg-rgray-1 text-rgray-11;
+ @apply bg-rgray-2 text-rgray-11;
/* color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
@@ -30,6 +30,10 @@ body {
rgb(var(--background-start-rgb)); */
}
+[vaul-drawer-wrapper] {
+ @apply bg-rgray-1;
+}
+
@layer utilities {
.text-balance {
text-wrap: balance;
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 5464c1d3..5b2ced94 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -16,7 +16,11 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
- <body className={roboto.className}>{children}</body>
+ <body className={roboto.className}>
+ <div vaul-drawer-wrapper="" className="min-w-screen overflow-x-hidden">
+ {children}
+ </div>
+ </body>
</html>
);
}
diff --git a/apps/web/src/app/ui/page.tsx b/apps/web/src/app/ui/page.tsx
index 2f7c6a4b..43cff341 100644
--- a/apps/web/src/app/ui/page.tsx
+++ b/apps/web/src/app/ui/page.tsx
@@ -1,4 +1,4 @@
-import Sidebar from "@/components/Sidebar";
+import Sidebar from "@/components/Sidebar/index";
export default async function Home() {
return (
diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx
deleted file mode 100644
index 0644d779..00000000
--- a/apps/web/src/components/Sidebar.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-"use client";
-import { StoredContent } from "@/server/db/schema";
-import {
- Plus,
- MoreHorizontal,
- ArrowUpRight,
- Edit3,
- Trash2,
-} from "lucide-react";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "./ui/dropdown-menu";
-
-import { useState, useEffect, useRef } from "react";
-
-export default function Sidebar() {
- const websties: StoredContent[] = [
- {
- id: 1,
- content: "",
- title: "Visual Studio Code",
- url: "https://code.visualstudio.com",
- description: "",
- image: "https://code.visualstudio.com/favicon.ico",
- baseUrl: "https://code.visualstudio.com",
- savedAt: new Date(),
- },
- {
- id: 1,
- content: "",
- title: "yxshv/vscode: An unofficial remake of vscode's landing page",
- url: "https://github.com/yxshv/vscode",
- description: "",
- image: "https://github.com/favicon.ico",
- baseUrl: "https://github.com",
- savedAt: new Date(),
- },
- ];
-
- return (
- <aside className="bg-rgray-3 flex h-screen w-[25%] flex-col items-start justify-between py-5 pb-[50vh] font-light">
- <div className="flex items-center justify-center gap-1 px-5 text-xl font-normal">
- <img src="/brain.png" alt="logo" className="h-10 w-10" />
- SuperMemory
- </div>
- <div className="flex w-full flex-col items-start justify-center p-2">
- <h1 className="mb-1 flex w-full items-center justify-center px-3 font-normal">
- Websites
- <button className="ml-auto ">
- <Plus className="h-4 w-4 min-w-4" />
- </button>
- </h1>
- {websties.map((item) => (
- <ListItem key={item.id} item={item} />
- ))}
- </div>
- </aside>
- );
-}
-
-export const ListItem: React.FC<{ item: StoredContent }> = ({ item }) => {
- const [isEditing, setIsEditing] = useState(false);
- const editInputRef = useRef<HTMLInputElement>(null);
-
- useEffect(() => {
- if (isEditing) {
- setTimeout(() => {
- editInputRef.current?.focus();
- }, 500);
- }
- }, [isEditing]);
-
- return (
- <div className="hover:bg-rgray-5 focus-within:bg-rgray-5 flex w-full items-center rounded-full py-1 pl-3 pr-2 transition [&:hover>a>[data-upright-icon]]:block [&:hover>a>img]:hidden [&:hover>button]:opacity-100">
- <a
- href={item.url}
- target="_blank"
- onClick={(e) => isEditing && e.preventDefault()}
- className="flex w-[90%] items-center gap-2 focus:outline-none"
- >
- {isEditing ? (
- <Edit3 className="h-4 w-4" strokeWidth={1.5} />
- ) : (
- <>
- <img
- src={item.image ?? "/brain.png"}
- alt={item.title ?? "Untitiled website"}
- className="h-4 w-4"
- />
- <ArrowUpRight
- data-upright-icon
- className="hidden h-4 w-4 min-w-4 scale-125"
- strokeWidth={1.5}
- />
- </>
- )}
- {isEditing ? (
- <input
- ref={editInputRef}
- autoFocus
- className="text-rgray-12 w-full bg-transparent focus:outline-none"
- placeholder={item.title ?? "Untitled website"}
- onBlur={(e) => setIsEditing(false)}
- onKeyDown={(e) => e.key === "Escape" && setIsEditing(false)}
- />
- ) : (
- <span className="w-full truncate text-nowrap">
- {item.title ?? "Untitled website"}
- </span>
- )}
- </a>
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <button className="ml-auto w-4 min-w-4 rounded-[0.15rem] opacity-0 focus:opacity-100 focus:outline-none">
- <MoreHorizontal className="h-4 w-4 min-w-4" />
- </button>
- </DropdownMenuTrigger>
- <DropdownMenuContent className="w-5">
- <DropdownMenuItem onClick={() => window.open(item.url)}>
- <ArrowUpRight
- className="mr-2 h-4 w-4 scale-125"
- strokeWidth={1.5}
- />
- Open
- </DropdownMenuItem>
- <DropdownMenuItem
- onClick={(e) => {
- setIsEditing(true);
- }}
- >
- <Edit3 className="mr-2 h-4 w-4 " strokeWidth={1.5} />
- Edit
- </DropdownMenuItem>
- <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400 dark:focus:bg-red-100/10">
- <Trash2 className="mr-2 h-4 w-4 " strokeWidth={1.5} />
- Delete
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- );
-};
diff --git a/apps/web/src/components/Sidebar/PagesItem.tsx b/apps/web/src/components/Sidebar/PagesItem.tsx
new file mode 100644
index 00000000..ce762ae5
--- /dev/null
+++ b/apps/web/src/components/Sidebar/PagesItem.tsx
@@ -0,0 +1,189 @@
+"use client";
+import { cleanUrl } from "@/lib/utils";
+import { StoredContent } from "@/server/db/schema";
+import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+} from "../ui/dropdown-menu";
+import { Label } from "../ui/label";
+import {
+ ArrowUpRight,
+ MoreHorizontal,
+ Edit3,
+ Trash2,
+ Save,
+ ChevronRight,
+ Plus,
+} from "lucide-react";
+import { useState } from "react";
+import {
+ Drawer,
+ DrawerContent,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerClose,
+} from "../ui/drawer";
+import { Input } from "../ui/input";
+import { Textarea } from "../ui/textarea";
+import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
+
+export const PageItem: React.FC<{ item: StoredContent }> = ({ item }) => {
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+ const [isEditDrawerOpen, setIsEditDrawerOpen] = useState(false);
+
+ return (
+ <div className="hover:bg-rgray-5 focus-within:bg-rgray-5 flex w-full items-center rounded-full py-1 pl-3 pr-2 transition [&:hover>a>div>[data-upright-icon]]:scale-125 [&:hover>a>div>[data-upright-icon]]:opacity-100 [&:hover>a>div>[data-upright-icon]]:delay-150 [&:hover>a>div>img]:scale-75 [&:hover>a>div>img]:opacity-0 [&:hover>a>div>img]:delay-0 [&:hover>button]:opacity-100">
+ <a
+ href={item.url}
+ target="_blank"
+ className="flex w-[90%] items-center gap-2 focus-visible:outline-none"
+ >
+ <div className="relative h-4 min-w-4">
+ <img
+ src={item.image ?? "/brain.png"}
+ alt={item.title ?? "Untitiled website"}
+ className="z-1 h-4 w-4 transition-[transform,opacity] delay-150 duration-150"
+ />
+ <ArrowUpRight
+ data-upright-icon
+ className="absolute left-1/2 top-1/2 z-[2] h-4 w-4 min-w-4 -translate-x-1/2 -translate-y-1/2 scale-75 opacity-0 transition-[transform,opacity] duration-150"
+ strokeWidth={1.5}
+ />
+ </div>
+
+ <span className="w-full truncate text-nowrap">
+ {item.title ?? "Untitled website"}
+ </span>
+ </a>
+ <DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
+ <DropdownMenuTrigger asChild>
+ <button className="ml-auto w-4 min-w-4 rounded-[0.15rem] opacity-0 focus-visible:opacity-100 focus-visible:outline-none">
+ <MoreHorizontal className="h-4 w-4 min-w-4" />
+ </button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent className="w-5">
+ <DropdownMenuItem onClick={() => window.open(item.url)}>
+ <ArrowUpRight
+ className="mr-2 h-4 w-4 scale-125"
+ strokeWidth={1.5}
+ />
+ Open
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ onClick={() => {
+ setIsDropdownOpen(false);
+ setIsEditDrawerOpen(true);
+ }}
+ >
+ <Edit3 className="mr-2 h-4 w-4" strokeWidth={1.5} />
+ Edit
+ </DropdownMenuItem>
+ <DropdownMenuItem className="focus-visible:bg-red-100 focus-visible:text-red-400 dark:focus-visible:bg-red-100/10">
+ <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} />
+ Delete
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ <Drawer
+ shouldScaleBackground
+ open={isEditDrawerOpen}
+ onOpenChange={setIsEditDrawerOpen}
+ >
+ <DrawerContent className="pb-10 lg:px-[25vw]">
+ <DrawerHeader className="relative mt-10 px-0">
+ <DrawerTitle className=" flex w-full justify-between">
+ Edit Page Details
+ </DrawerTitle>
+ <DrawerDescription>Change the page details</DrawerDescription>
+ <a
+ target="_blank"
+ href={item.url}
+ className="text-rgray-11/90 bg-rgray-3 text-md absolute right-0 top-0 flex w-min translate-y-1/2 items-center justify-center gap-1 rounded-full px-5 py-1"
+ >
+ <img src={item.image ?? "/brain.png"} className="h-4 w-4" />
+ {cleanUrl(item.url)}
+ </a>
+ </DrawerHeader>
+
+ <div className="mt-5">
+ <Label>Title</Label>
+ <Input
+ className=""
+ required
+ value={item.title ?? ""}
+ placeholder={item.title ?? "Enter the title for the page"}
+ />
+ </div>
+ <div className="mt-5">
+ <Label>Additional Context</Label>
+ <Textarea
+ className=""
+ value={item.content ?? ""}
+ placeholder={"Enter additional context for this page"}
+ />
+ </div>
+ <DrawerFooter className="flex flex-row-reverse items-center justify-end px-0 pt-5">
+ <DrawerClose className="flex items-center justify-center rounded-md px-3 py-2 ring-2 ring-transparent transition hover:bg-blue-100 hover:text-blue-400 focus-visible:bg-blue-100 focus-visible:text-blue-400 focus-visible:outline-none focus-visible:ring-blue-200 dark:hover:bg-blue-100/10 dark:focus-visible:bg-blue-100/10 dark:focus-visible:ring-blue-200/30">
+ <Save className="mr-2 h-4 w-4 " strokeWidth={1.5} />
+ Save
+ </DrawerClose>
+ <DrawerClose className="hover:bg-rgray-3 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 flex items-center justify-center rounded-md px-3 py-2 ring-2 ring-transparent transition focus-visible:outline-none">
+ Cancel
+ </DrawerClose>
+ <DrawerClose className="mr-auto flex items-center justify-center rounded-md bg-red-100 px-3 py-2 text-red-400 ring-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-red-200 dark:bg-red-100/10 dark:focus-visible:ring-red-200/30">
+ <Trash2 className="mr-2 h-4 w-4 " strokeWidth={1.5} />
+ Delete
+ </DrawerClose>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ </div>
+ );
+};
+
+export const AddNewPagePopover: React.FC<{
+ addNewUrl?: (url: string) => Promise<void>;
+}> = ({ addNewUrl }) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [url, setUrl] = useState("");
+
+ return (
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
+ <PopoverTrigger asChild>
+ <button className="focus-visible:ring-rgray-7 ring-offset-rgray-3 ml-auto rounded-sm ring-2 ring-transparent ring-offset-2 focus-visible:outline-none">
+ <Plus className="h-4 w-4 min-w-4" />
+ </button>
+ </PopoverTrigger>
+ <PopoverContent align="start" side="top">
+ <h1 className="mb-2 flex items-center justify-between ">
+ Add a new page
+ <button
+ onClick={() => {
+ setIsOpen(false);
+ addNewUrl?.(url);
+ }}
+ className="hover:bg-rgray-3 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 ring-offset-rgray-3 rounded-sm ring-2 ring-transparent ring-offset-2 transition focus-visible:outline-none"
+ >
+ <ChevronRight className="h-4 w-4" />
+ </button>
+ </h1>
+ <Input
+ className="w-full"
+ autoFocus
+ onChange={(e) => setUrl(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ setIsOpen(false);
+ addNewUrl?.(url);
+ }
+ }}
+ placeholder="Enter the URL of the page"
+ />
+ </PopoverContent>
+ </Popover>
+ );
+};
diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx
new file mode 100644
index 00000000..80d4beb5
--- /dev/null
+++ b/apps/web/src/components/Sidebar/index.tsx
@@ -0,0 +1,46 @@
+"use server";
+import { StoredContent } from "@/server/db/schema";
+import { AddNewPagePopover, PageItem } from "./PagesItem";
+
+export default async function Sidebar() {
+ const pages: StoredContent[] = [
+ {
+ id: 1,
+ content: "",
+ title: "Visual Studio Code",
+ url: "https://code.visualstudio.com",
+ description: "",
+ image: "https://code.visualstudio.com/favicon.ico",
+ baseUrl: "https://code.visualstudio.com",
+ savedAt: new Date(),
+ },
+ {
+ id: 2,
+ content: "",
+ title: "yxshv/vscode: An unofficial remake of vscode's landing page",
+ url: "https://github.com/yxshv/vscode",
+ description: "",
+ image: "https://github.com/favicon.ico",
+ baseUrl: "https://github.com",
+ savedAt: new Date(),
+ },
+ ];
+
+ return (
+ <aside className="bg-rgray-3 flex h-screen w-[25%] flex-col items-start justify-between py-5 pb-[50vh] font-light">
+ <div className="flex items-center justify-center gap-1 px-5 text-xl font-normal">
+ <img src="/brain.png" alt="logo" className="h-10 w-10" />
+ SuperMemory
+ </div>
+ <div className="flex w-full flex-col items-start justify-center p-2">
+ <h1 className="mb-1 flex w-full items-center justify-center px-3 font-normal">
+ Pages
+ <AddNewPagePopover />
+ </h1>
+ {pages.map((item) => (
+ <PageItem key={item.id} item={item} />
+ ))}
+ </div>
+ </aside>
+ );
+}
diff --git a/apps/web/src/components/ui/drawer.tsx b/apps/web/src/components/ui/drawer.tsx
new file mode 100644
index 00000000..705ca01c
--- /dev/null
+++ b/apps/web/src/components/ui/drawer.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import * as React from "react";
+import { Drawer as DrawerPrimitive } from "vaul";
+
+import { cn } from "@/lib/utils";
+
+const Drawer = ({
+ shouldScaleBackground = true,
+ ...props
+}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
+ <DrawerPrimitive.Root
+ shouldScaleBackground={shouldScaleBackground}
+ {...props}
+ />
+);
+Drawer.displayName = "Drawer";
+
+const DrawerTrigger = DrawerPrimitive.Trigger;
+
+const DrawerPortal = DrawerPrimitive.Portal;
+
+const DrawerClose = DrawerPrimitive.Close;
+
+const DrawerOverlay = React.forwardRef<
+ React.ElementRef<typeof DrawerPrimitive.Overlay>,
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
+>(({ className, ...props }, ref) => (
+ <DrawerPrimitive.Overlay
+ ref={ref}
+ className={cn("fixed inset-0 z-50 bg-black/80", className)}
+ {...props}
+ />
+));
+DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
+
+const DrawerContent = React.forwardRef<
+ React.ElementRef<typeof DrawerPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
+>(({ className, children, ...props }, ref) => (
+ <DrawerPortal>
+ <DrawerOverlay />
+ <DrawerPrimitive.Content
+ ref={ref}
+ className={cn(
+ "border-rgray-6 bg-rgray-2 fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border",
+ className,
+ )}
+ {...props}
+ >
+ <div className="bg-rgray-4 mx-auto mt-4 h-2 w-[100px] rounded-full " />
+ {children}
+ </DrawerPrimitive.Content>
+ </DrawerPortal>
+));
+DrawerContent.displayName = "DrawerContent";
+
+const DrawerHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+ <div
+ className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
+ {...props}
+ />
+);
+DrawerHeader.displayName = "DrawerHeader";
+
+const DrawerFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+ <div
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
+ {...props}
+ />
+);
+DrawerFooter.displayName = "DrawerFooter";
+
+const DrawerTitle = React.forwardRef<
+ React.ElementRef<typeof DrawerPrimitive.Title>,
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
+>(({ className, ...props }, ref) => (
+ <DrawerPrimitive.Title
+ ref={ref}
+ className={cn(
+ "text-rgray-12 text-xl font-medium leading-none tracking-tight",
+ className,
+ )}
+ {...props}
+ />
+));
+DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
+
+const DrawerDescription = React.forwardRef<
+ React.ElementRef<typeof DrawerPrimitive.Description>,
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
+>(({ className, ...props }, ref) => (
+ <DrawerPrimitive.Description
+ ref={ref}
+ className={cn("text-rgray-11 text-md", className)}
+ {...props}
+ />
+));
+DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
+
+export {
+ Drawer,
+ DrawerPortal,
+ DrawerOverlay,
+ DrawerTrigger,
+ DrawerClose,
+ DrawerContent,
+ DrawerHeader,
+ DrawerFooter,
+ DrawerTitle,
+ DrawerDescription,
+};
diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx
index aae15c80..8a7a9340 100644
--- a/apps/web/src/components/ui/input.tsx
+++ b/apps/web/src/components/ui/input.tsx
@@ -1,6 +1,6 @@
-import * as React from "react"
+import * as React from "react";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
@@ -11,15 +11,15 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
- "flex h-10 w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-800 dark:bg-gray-950 dark:ring-offset-gray-950 dark:placeholder:text-gray-400 dark:focus-visible:ring-gray-300",
- className
+ "border-rgray-6 text-rgray-12 ring-offset-rgray-2 placeholder:text-rgray-11 focus-visible:ring-rgray-7 flex h-10 w-full rounded-md border bg-transparent px-3 py-2 text-sm transition file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ",
+ className,
)}
ref={ref}
{...props}
/>
- )
- }
-)
-Input.displayName = "Input"
+ );
+ },
+);
+Input.displayName = "Input";
-export { Input }
+export { Input };
diff --git a/apps/web/src/components/ui/popover.tsx b/apps/web/src/components/ui/popover.tsx
new file mode 100644
index 00000000..0c4563a8
--- /dev/null
+++ b/apps/web/src/components/ui/popover.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import * as React from "react";
+import * as PopoverPrimitive from "@radix-ui/react-popover";
+
+import { cn } from "@/lib/utils";
+
+const Popover = PopoverPrimitive.Root;
+
+const PopoverTrigger = PopoverPrimitive.Trigger;
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef<typeof PopoverPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+ <PopoverPrimitive.Portal>
+ <PopoverPrimitive.Content
+ ref={ref}
+ align={align}
+ sideOffset={sideOffset}
+ className={cn(
+ "border-rgray-6 bg-rgray-3 text-rgray-11 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none",
+ className,
+ )}
+ {...props}
+ />
+ </PopoverPrimitive.Portal>
+));
+PopoverContent.displayName = PopoverPrimitive.Content.displayName;
+
+export { Popover, PopoverTrigger, PopoverContent };
diff --git a/apps/web/src/components/ui/textarea.tsx b/apps/web/src/components/ui/textarea.tsx
new file mode 100644
index 00000000..68d8e79e
--- /dev/null
+++ b/apps/web/src/components/ui/textarea.tsx
@@ -0,0 +1,24 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
+
+const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
+ ({ className, ...props }, ref) => {
+ return (
+ <textarea
+ className={cn(
+ "border-rgray-6 ring-offset-rgray-2 placeholder:text-rgray-11 focus-visible:ring-rgray-7 flex min-h-[80px] w-full rounded-md border bg-transparent px-3 py-2 text-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+ className,
+ )}
+ ref={ref}
+ {...props}
+ />
+ );
+ },
+);
+Textarea.displayName = "Textarea";
+
+export { Textarea };
diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts
index d084ccad..6b8f3fd9 100644
--- a/apps/web/src/lib/utils.ts
+++ b/apps/web/src/lib/utils.ts
@@ -1,6 +1,19 @@
-import { type ClassValue, clsx } from "clsx"
-import { twMerge } from "tailwind-merge"
+"use client";
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
+}
+
+// removes http(s?):// and / from the url
+export function cleanUrl(url: string) {
+ if (url.endsWith("/")) {
+ url = url.slice(0, -1);
+ }
+ return url.startsWith("https://")
+ ? url.slice(8)
+ : url.startsWith("http://")
+ ? url.slice(7)
+ : url;
}