add custom Bin component

This commit is contained in:
Yash 2024-04-04 08:34:48 +00:00
parent a23fd73a0b
commit c22751ee68
7 changed files with 440 additions and 145 deletions

104
apps/web/src/assets/Bin.tsx Normal file
View file

@ -0,0 +1,104 @@
import { cn } from "@/lib/utils";
import { useEffect, useRef } from "react";
export const Bin: React.FC<React.HTMLAttributes<HTMLDivElement> & {}> = ({
className,
...props
}) => {
const icon = useRef<HTMLDivElement>(null);
useEffect(() => {
let timeout: ReturnType<typeof setTimeout> | undefined;
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (
mutation.type === "attributes" &&
mutation.attributeName === "data-open" &&
(mutation.oldValue === "false" || mutation.oldValue === null) &&
icon.current?.dataset["open"] === "true"
) {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
icon.current!.dataset["open"] = "false";
}, 2000);
}
});
});
observer.observe(icon.current!, {
attributes: true, //configure it to listen to attribute changes
});
return () => {
observer.disconnect();
};
}, []);
return (
<div
ref={icon}
data-open="false"
className={cn(
"relative z-[100] flex w-full origin-bottom flex-col items-center justify-center transition-transform delay-500 duration-500 data-[open='true']:-translate-y-2 data-[open='true']:scale-150 data-[open='true']:delay-0 [&[data-open='true']>[data-lid]]:rotate-[150deg] [&[data-open='true']>[data-lid]]:delay-0",
className,
)}
{...props}
>
<svg
data-lid
className="w-full origin-[90%_80%] transition-transform delay-500 duration-500 ease-in-out"
viewBox="0 0 24 7"
fill="none"
strokeWidth={1}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 6H21"
stroke="currentColor"
strokeWidth={1}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8 6V4C8 3 9 2 10 2H14C15 2 16 3 16 4V6"
stroke="currentColor"
strokeWidth={1}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<svg
className="-mt-[1px] w-full"
viewBox="0 0 24 19"
fill="none"
strokeWidth={1}
data-trash-bin
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 1V15C19 16 18 17 17 17H7C6 17 5 16 5 15V1"
stroke="currentColor"
strokeWidth={1}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10 6V12"
stroke="currentColor"
strokeWidth={1}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M14 6V12"
stroke="currentColor"
strokeWidth={1}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
);
};

View file

@ -1,5 +1,3 @@
import { svgId } from "@/lib/utils";
export const MemoryIcon: React.FC<React.SVGAttributes<SVGElement>> = (
props,
) => (

View file

@ -13,6 +13,8 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
const textArea = useRef<HTMLTextAreaElement>(null);
console.log("main px", sidebarOpen);
useEffect(() => {
function onResize() {
if (!textArea.current || !window.visualViewport) return;

View file

@ -1,3 +1,4 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import {
MemoryWithImage,
MemoryWithImages3,
@ -5,99 +6,125 @@ import {
} from "@/assets/MemoryWithImages";
import { type Space } from "../../../types/memory";
import { InputWithIcon } from "../ui/input";
import { MoreHorizontal, Search } from "lucide-react";
import {
ArrowUpRight,
Edit3,
MoreHorizontal,
Search,
Trash2,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import {
animate,
AnimatePresence,
LayoutGroup,
motion,
useAnimate,
Variant,
} from "framer-motion";
import { useRef, useState } from "react";
const spaces: Space[] = [
{
id: 1,
title: "Cool Tech",
description: "Really cool mind blowing tech",
content: [
{
id: 1,
title: "Perplexity",
description: "A good ui",
content: "",
image: "https://perplexity.ai/favicon.ico",
url: "https://perplexity.ai",
savedAt: new Date(),
baseUrl: "https://perplexity.ai",
space: "Cool tech",
},
{
id: 2,
title: "Pi.ai",
description: "A good ui",
content: "",
image: "https://pi.ai/pi-logo-192.png?v=2",
url: "https://pi.ai",
savedAt: new Date(),
baseUrl: "https://pi.ai",
space: "Cool tech",
},
{
id: 3,
title: "Visual Studio Code",
description: "A good ui",
content: "",
image: "https://code.visualstudio.com/favicon.ico",
url: "https://code.visualstudio.com",
savedAt: new Date(),
baseUrl: "https://code.visualstudio.com",
space: "Cool tech",
},
],
},
{
id: 2,
title: "Cool Courses",
description: "Amazng",
content: [
{
id: 1,
title: "Animation on the web",
description: "A good ui",
content: "",
image: "https://animations.dev/favicon.ico",
url: "https://animations.dev",
savedAt: new Date(),
baseUrl: "https://animations.dev",
space: "Cool courses",
},
{
id: 2,
title: "Tailwind Course",
description: "A good ui",
content: "",
image:
"https://tailwindcss.com/_next/static/media/tailwindcss-mark.3c5441fc7a190fb1800d4a5c7f07ba4b1345a9c8.svg",
url: "https://tailwindcss.com",
savedAt: new Date(),
baseUrl: "https://tailwindcss.com",
space: "Cool courses",
},
],
},
{
id: 3,
title: "Cool Libraries",
description: "Really cool mind blowing tech",
content: [
{
id: 1,
title: "Perplexity",
description: "A good ui",
content: "",
image: "https://yashverma.me/logo.jpg",
url: "https://perplexity.ai",
savedAt: new Date(),
baseUrl: "https://perplexity.ai",
space: "Cool libraries",
},
],
},
];
export function MemoriesBar() {
const spaces: Space[] = [
{
id: 1,
title: "Cool Tech",
description: "Really cool mind blowing tech",
content: [
{
id: 1,
title: "Perplexity",
description: "A good ui",
content: "",
image: "https://perplexity.ai/favicon.ico",
url: "https://perplexity.ai",
savedAt: new Date(),
baseUrl: "https://perplexity.ai",
space: "Cool tech",
},
{
id: 2,
title: "Pi.ai",
description: "A good ui",
content: "",
image: "https://pi.ai/pi-logo-192.png?v=2",
url: "https://pi.ai",
savedAt: new Date(),
baseUrl: "https://pi.ai",
space: "Cool tech",
},
{
id: 3,
title: "Visual Studio Code",
description: "A good ui",
content: "",
image: "https://code.visualstudio.com/favicon.ico",
url: "https://code.visualstudio.com",
savedAt: new Date(),
baseUrl: "https://code.visualstudio.com",
space: "Cool tech",
},
],
},
{
id: 2,
title: "Cool Courses",
description: "Amazng",
content: [
{
id: 1,
title: "Animation on the web",
description: "A good ui",
content: "",
image: "https://animations.dev/favicon.ico",
url: "https://animations.dev",
savedAt: new Date(),
baseUrl: "https://animations.dev",
space: "Cool courses",
},
{
id: 2,
title: "Tailwind Course",
description: "A good ui",
content: "",
image:
"https://tailwindcss.com/_next/static/media/tailwindcss-mark.3c5441fc7a190fb1800d4a5c7f07ba4b1345a9c8.svg",
url: "https://tailwindcss.com",
savedAt: new Date(),
baseUrl: "https://tailwindcss.com",
space: "Cool courses",
},
],
},
{
id: 3,
title: "Cool Libraries",
description: "Really cool mind blowing tech",
content: [
{
id: 1,
title: "Perplexity",
description: "A good ui",
content: "",
image: "https://yashverma.me/logo.jpg",
url: "https://perplexity.ai",
savedAt: new Date(),
baseUrl: "https://perplexity.ai",
space: "Cool libraries",
},
],
},
];
const [parent, enableAnimations] = useAutoAnimate();
const [currentSpaces, setCurrentSpaces] = useState(spaces);
console.log("currentSpaces: ", currentSpaces);
return (
<div className="text-rgray-11 flex w-full flex-col items-start py-8 text-left">
@ -109,28 +136,129 @@ export function MemoriesBar() {
className="bg-rgray-4 mt-2 w-full"
/>
</div>
<div className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5">
{spaces.map((space) => (
<Space key={space.id} {...space} />
<div
ref={parent}
className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5"
>
{currentSpaces.map((space) => (
<SpaceItem
onDelete={() =>
setCurrentSpaces((prev) => prev.filter((s) => s.id !== space.id))
}
key={space.id}
{...space}
/>
))}
</div>
</div>
);
}
export function Space({ title, description, content, id }: Space) {
console.log(title, content.map((c) => c.image).reverse());
const SpaceExitVariant: Variant = {
opacity: 0,
scale: 0,
borderRadius: "50%",
background: "var(--gray-1)",
transition: {
duration: 0.2,
},
};
export function SpaceItem({
title,
description,
content,
id,
onDelete,
}: Space & { onDelete: () => void }) {
const [itemRef, animateItem] = useAnimate();
return (
<div className="hover:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 [&:hover>[data-more-button]]:opacity-100">
<motion.div
ref={itemRef}
className="hover:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 [&:hover>[data-more-button]]:opacity-100"
>
<button data-space-text className="focus-visible:outline-none">
{title}
</button>
<button
data-more-button
className="hover:bg-rgray-3 focus-visible:bg-rgray-3 focus-visible:ring-rgray-7 absolute right-2 top-2 rounded-md p-1 opacity-0 ring-transparent transition focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2"
>
<MoreHorizontal className="text-rgray-11 h-5 w-5" />
</button>
<SpaceMoreButton
onDelete={() => {
if (!itemRef.current) return;
const trash = document.querySelector("#trash")! as HTMLDivElement;
const trashBin = document.querySelector("#trash-button")!;
const trashRect = trashBin.getBoundingClientRect();
const scopeRect = itemRef.current.getBoundingClientRect();
const el = document.createElement("div");
el.style.position = "fixed";
el.style.top = "0";
el.style.left = "0";
el.style.width = "15px";
el.style.height = "15px";
el.style.backgroundColor = "var(--gray-7)";
el.style.zIndex = "60";
el.style.borderRadius = "50%";
el.style.transform = "scale(5)";
el.style.opacity = "0";
trash.dataset["open"] = "true";
const initial = {
x: scopeRect.left + scopeRect.width / 2,
y: scopeRect.top + scopeRect.height / 2,
};
const delta = {
x:
trashRect.left +
trashRect.width / 2 -
scopeRect.left +
scopeRect.width / 2,
y:
trashRect.top +
trashRect.height / 4 -
scopeRect.top +
scopeRect.height / 2,
};
const end = {
x: trashRect.left + trashRect.width / 2,
y: trashRect.top + trashRect.height / 4,
};
el.style.offsetPath = `path('M ${initial.x} ${initial.y} Q ${delta.x * 0.01} ${delta.y * 0.01} ${end.x} ${end.y}`;
animateItem(itemRef.current, SpaceExitVariant, {
duration: 0.2,
}).then(() => {
itemRef.current.style.scale = "0";
onDelete();
});
document.body.appendChild(el);
el.animate(
{
transform: ["scale(5)", "scale(1)"],
opacity: [0, 0.3, 1],
},
{
duration: 200,
easing: "cubic-bezier(0.64, 0.57, 0.67, 1.53)",
fill: "forwards",
},
);
el.animate(
{
offsetDistance: ["0%", "100%"],
},
{
duration: 2000,
easing: "cubic-bezier(0.64, 0.57, 0.67, 1.53)",
fill: "forwards",
delay: 200,
},
).onfinish = () => {
el.animate(
{ transform: "scale(0)", opacity: 0 },
{ duration: 200, fill: "forwards" },
).onfinish = () => {
el.remove();
};
};
}}
/>
{content.length > 2 ? (
<MemoryWithImages3
className="h-24 w-24"
@ -150,6 +278,45 @@ export function Space({ title, description, content, id }: Space) {
images={content.map((c) => c.image).reverse() as string[]}
/>
)}
</div>
</motion.div>
);
}
export function SpaceMoreButton({ onDelete }: { onDelete?: () => void }) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<button
data-more-button
className="hover:bg-rgray-3 focus-visible:bg-rgray-3 focus-visible:ring-rgray-7 absolute right-2 top-2 rounded-md p-1 opacity-0 ring-transparent transition focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2"
>
<MoreHorizontal className="text-rgray-11 h-5 w-5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<ArrowUpRight
className="mr-2 h-4 w-4 scale-125"
strokeWidth={1.5}
/>
Open
</DropdownMenuItem>
<DropdownMenuItem onClick={() => {}}>
<Edit3 className="mr-2 h-4 w-4" strokeWidth={1.5} />
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={onDelete}
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} />
Move to Trash
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
);
}

View file

@ -2,9 +2,10 @@
import { StoredContent } from "@/server/db/schema";
import { MemoryIcon } from "../../assets/Memories";
import { Trash2, User2 } from "lucide-react";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { MemoriesBar } from "./MemoriesBar";
import { AnimatePresence, motion } from "framer-motion";
import { Bin } from "@/assets/Bin";
export type MenuItem = {
icon: React.ReactNode | React.ReactNode[];
@ -39,34 +40,46 @@ export default function Sidebar({
const menuItems = [...menuItemsTop, ...menuItemsBottom];
const [selectedItem, setSelectedItem] = useState<string | null>(null);
React.useEffect(() => {
onSelectChange?.(selectedItem);
}, [selectedItem]);
const Subbar =
menuItems.find((i) => i.label === selectedItem)?.content ?? (() => <></>);
useEffect(() => {
onSelectChange?.(selectedItem);
}, [selectedItem]);
return (
<>
<div className="relative hidden h-screen max-h-screen w-max flex-col items-center text-sm font-light md:flex">
<div className="bg-rgray-2 border-r-rgray-6 relative z-[10000] flex h-full w-full flex-col items-center justify-center border-r px-2 py-5 ">
{menuItemsTop.map((item, index) => (
<MenuItem
key={index}
item={item}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
))}
<div className="bg-rgray-2 border-r-rgray-6 relative z-[50] flex h-full w-full flex-col items-center justify-center border-r px-2 py-5 ">
<MenuItem
item={{
label: "Memories",
icon: <MemoryIcon className="h-10 w-10" />,
content: MemoriesBar,
}}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
<div className="mt-auto" />
{menuItemsBottom.map((item, index) => (
<MenuItem
key={index}
item={item}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
))}
<MenuItem
item={{
label: "Trash",
icon: <Bin id="trash" className="z-[300] h-7 w-7" />,
}}
selectedItem={selectedItem}
id='trash-button'
setSelectedItem={setSelectedItem}
/>
<MenuItem
item={{
label: "Profile",
icon: <User2 strokeWidth={1.3} className="h-7 w-7" />,
}}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
</div>
<AnimatePresence>
{selectedItem && (
@ -84,7 +97,8 @@ const MenuItem = ({
item: { icon, label },
selectedItem,
setSelectedItem,
}: {
...props
}: React.HTMLAttributes<HTMLButtonElement> & {
item: MenuItem;
selectedItem: string | null;
setSelectedItem: React.Dispatch<React.SetStateAction<string | null>>;
@ -93,6 +107,7 @@ const MenuItem = ({
data-state-on={selectedItem === label}
onClick={() => setSelectedItem((prev) => (prev === label ? null : label))}
className="on:opacity-100 on:bg-rgray-4 focus-visible:ring-rgray-7 relative z-[100] flex w-full flex-col items-center justify-center rounded-md px-3 py-3 opacity-80 ring-2 ring-transparent transition hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none"
{...props}
>
{icon}
<span className="">{label}</span>

View file

@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"data-[state=open]:animate-in bg-rgray-3 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 border-rgray-6 text-rgray-11 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md",
"data-[state=open]:animate-in bg-rgray-3 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 border-rgray-6 text-rgray-11 z-50 min-w-[9rem] overflow-hidden rounded-md border p-1 shadow-md",
className,
)}
{...props}

View file

@ -11,21 +11,30 @@ 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>
));
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
animate?: boolean;
}
>(
(
{ className, align = "center", animate = true, 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 z-50 w-72 rounded-md border p-4 shadow-md outline-none",
animate &&
"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",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
),
);
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };