From c22751ee68d9fdd4e5bc5147922de32a20abf484 Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 4 Apr 2024 08:34:48 +0000 Subject: add custom Bin component --- apps/web/src/assets/Bin.tsx | 104 +++++++ apps/web/src/assets/Memories.tsx | 2 - apps/web/src/components/Main.tsx | 2 + apps/web/src/components/Sidebar/MemoriesBar.tsx | 375 +++++++++++++++++------- apps/web/src/components/Sidebar/index.tsx | 61 ++-- apps/web/src/components/ui/dropdown-menu.tsx | 2 +- apps/web/src/components/ui/popover.tsx | 39 ++- 7 files changed, 440 insertions(+), 145 deletions(-) create mode 100644 apps/web/src/assets/Bin.tsx (limited to 'apps/web/src') diff --git a/apps/web/src/assets/Bin.tsx b/apps/web/src/assets/Bin.tsx new file mode 100644 index 00000000..d0793cef --- /dev/null +++ b/apps/web/src/assets/Bin.tsx @@ -0,0 +1,104 @@ +import { cn } from "@/lib/utils"; +import { useEffect, useRef } from "react"; + +export const Bin: React.FC & {}> = ({ + className, + ...props +}) => { + const icon = useRef(null); + + useEffect(() => { + let timeout: ReturnType | 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 ( +
[data-lid]]:rotate-[150deg] [&[data-open='true']>[data-lid]]:delay-0", + className, + )} + {...props} + > + + + + + + + + + + +
+ ); +}; diff --git a/apps/web/src/assets/Memories.tsx b/apps/web/src/assets/Memories.tsx index f8fd83b8..3b1c177f 100644 --- a/apps/web/src/assets/Memories.tsx +++ b/apps/web/src/assets/Memories.tsx @@ -1,5 +1,3 @@ -import { svgId } from "@/lib/utils"; - export const MemoryIcon: React.FC> = ( props, ) => ( diff --git a/apps/web/src/components/Main.tsx b/apps/web/src/components/Main.tsx index ef505db5..8796c52e 100644 --- a/apps/web/src/components/Main.tsx +++ b/apps/web/src/components/Main.tsx @@ -13,6 +13,8 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { const textArea = useRef(null); + console.log("main px", sidebarOpen); + useEffect(() => { function onResize() { if (!textArea.current || !window.visualViewport) return; diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index 2890af56..367f0173 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -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 (
@@ -109,28 +136,129 @@ export function MemoriesBar() { className="bg-rgray-4 mt-2 w-full" />
-
- {spaces.map((space) => ( - +
+ {currentSpaces.map((space) => ( + + setCurrentSpaces((prev) => prev.filter((s) => s.id !== space.id)) + } + key={space.id} + {...space} + /> ))}
); } -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 ( -
+ - + { + 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 ? ( c.image).reverse() as string[]} /> )} -
+ + ); +} + +export function SpaceMoreButton({ onDelete }: { onDelete?: () => void }) { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + + + + + + + + Open + + {}}> + + Edit + + + + Move to Trash + + + + ); } diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx index 49ce446a..fe5fcb0a 100644 --- a/apps/web/src/components/Sidebar/index.tsx +++ b/apps/web/src/components/Sidebar/index.tsx @@ -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(null); - React.useEffect(() => { - onSelectChange?.(selectedItem); - }, [selectedItem]); - const Subbar = menuItems.find((i) => i.label === selectedItem)?.content ?? (() => <>); + useEffect(() => { + onSelectChange?.(selectedItem); + }, [selectedItem]); + return ( <>
-
- {menuItemsTop.map((item, index) => ( - - ))} +
+ , + content: MemoriesBar, + }} + selectedItem={selectedItem} + setSelectedItem={setSelectedItem} + /> +
- {menuItemsBottom.map((item, index) => ( - - ))} + + , + }} + selectedItem={selectedItem} + id='trash-button' + setSelectedItem={setSelectedItem} + /> + , + }} + selectedItem={selectedItem} + setSelectedItem={setSelectedItem} + />
{selectedItem && ( @@ -84,7 +97,8 @@ const MenuItem = ({ item: { icon, label }, selectedItem, setSelectedItem, -}: { + ...props +}: React.HTMLAttributes & { item: MenuItem; selectedItem: string | null; setSelectedItem: React.Dispatch>; @@ -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} {label} diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx index 375662bb..cbc5cb1e 100644 --- a/apps/web/src/components/ui/dropdown-menu.tsx +++ b/apps/web/src/components/ui/dropdown-menu.tsx @@ -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} diff --git a/apps/web/src/components/ui/popover.tsx b/apps/web/src/components/ui/popover.tsx index 0c4563a8..cabe76a9 100644 --- a/apps/web/src/components/ui/popover.tsx +++ b/apps/web/src/components/ui/popover.tsx @@ -11,21 +11,30 @@ const PopoverTrigger = PopoverPrimitive.Trigger; const PopoverContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( - - - -)); + React.ComponentPropsWithoutRef & { + animate?: boolean; + } +>( + ( + { className, align = "center", animate = true, sideOffset = 4, ...props }, + ref, + ) => ( + + + + ), +); PopoverContent.displayName = PopoverPrimitive.Content.displayName; export { Popover, PopoverTrigger, PopoverContent }; -- cgit v1.2.3