diff options
| author | Dhravya Shah <[email protected]> | 2024-07-21 21:05:37 -0500 |
|---|---|---|
| committer | Dhravya Shah <[email protected]> | 2024-07-21 21:05:37 -0500 |
| commit | 99c08602dfc30e8a0bd5efeb858514445f025d23 (patch) | |
| tree | 9c562ef27bee37eaba21a7cc099dcb77c74d29e7 | |
| parent | producthunt link (diff) | |
| download | supermemory-99c08602dfc30e8a0bd5efeb858514445f025d23.tar.xz supermemory-99c08602dfc30e8a0bd5efeb858514445f025d23.zip | |
revert menu
| -rw-r--r-- | apps/web/app/(dash)/menu.tsx | 491 | ||||
| -rw-r--r-- | apps/web/app/(landing)/Hero.tsx | 12 | ||||
| -rw-r--r-- | packages/ui/icons/index.ts | 2 |
3 files changed, 354 insertions, 151 deletions
diff --git a/apps/web/app/(dash)/menu.tsx b/apps/web/app/(dash)/menu.tsx index e5501958..396623ff 100644 --- a/apps/web/app/(dash)/menu.tsx +++ b/apps/web/app/(dash)/menu.tsx @@ -1,175 +1,364 @@ -import React from "react"; +"use client"; + +import React, { useEffect, useMemo, useState } from "react"; import Image from "next/image"; import Link from "next/link"; -import { MemoriesIcon, CanvasIcon, AddIcon } from "@repo/ui/icons"; -import { DialogTrigger } from "@repo/ui/shadcn/dialog"; - -import { HomeIcon } from "@heroicons/react/24/solid"; import { - PencilSquareIcon, - PlusIcon, - PresentationChartLineIcon, - RectangleStackIcon, -} from "@heroicons/react/24/solid"; -import DialogTriggerWrapper, { - DialogDesktopTrigger, - DialogMobileTrigger, -} from "./dialogTriggerWrapper"; - -const menuItems = [ - { - icon: MemoriesIcon, - text: "Memories", - url: "/memories", - disabled: false, - }, - { - icon: CanvasIcon, - text: "Canvas", - url: "/canvas", - disabled: true, - }, -]; - -const items = [ - { - icon: <HomeIcon className="h-6 w-6" />, - name: "home", - url: "/home", - disabled: false, - }, - { - icon: <RectangleStackIcon className="h-6 w-6" />, - name: "memories", - url: "/memories", - disabled: false, - }, - // { - // icon: <PencilSquareIcon className="h-6 w-6" />, - // name: 'editor', - // url: '/#', - // disabled: true, - // }, - // { - // icon: <PresentationChartLineIcon className="h-6 w-6" />, - // name: 'thinkpad', - // url: '/#', - // disabled: true, - // }, -]; + MemoriesIcon, + ExploreIcon, + CanvasIcon, + AddIcon, + HomeIcon as HomeIconWeb, +} from "@repo/ui/icons"; +import { Button } from "@repo/ui/shadcn/button"; +import { MinusIcon, PlusCircleIcon } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@repo/ui/shadcn/dialog"; +import { Label } from "@repo/ui/shadcn/label"; +import { Textarea } from "@repo/ui/shadcn/textarea"; +import { toast } from "sonner"; +import { getSpaces } from "../actions/fetchers"; +import { HomeIcon } from "@heroicons/react/24/solid"; +import { createMemory, createSpace } from "../actions/doers"; +import ComboboxWithCreate from "@repo/ui/shadcn/combobox"; +import { StoredSpace } from "@/server/db/schema"; +import useMeasure from "react-use-measure"; function Menu() { + 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 menuItems = [ + { + icon: HomeIconWeb, + text: "Home", + url: "/home", + disabled: false, + }, + { + icon: MemoriesIcon, + text: "Memories", + url: "/memories", + disabled: false, + }, + ]; + + const [content, setContent] = useState(""); + const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]); + + 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 [dialogOpen, setDialogOpen] = useState(false); + + const options = useMemo( + () => + spaces.map((x) => ({ + label: x.name, + value: x.id.toString(), + })), + [spaces], + ); + + const handleSubmit = async (content?: string, spaces?: number[]) => { + setDialogOpen(false); + + 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([]); + + if (cont.success) { + toast.success("Memory created", { + richColors: true, + }); + } else { + toast.error(`Memory creation failed: ${cont.error}`); + } + }; + return ( <> {/* Desktop Menu */} - <div className="hidden lg:flex items-center pointer-events-none z-[39] fixed left-2 top-0 h-screen flex-col justify-center px-2"> - <div className="pointer-events-none z-10 absolute top-1/2 h-1/3 w-full -translate-y-1/2 bg-secondary blur-[300px] "></div> - <div className="pointer-events-auto flex flex-col gap-2"> - <DialogDesktopTrigger /> - <div className="inline-flex w-14 flex-col items-start gap-6 rounded-2xl border-[1px] border-gray-700/50 bg-secondary px-3 py-4 text-[#b9b9b9] shadow-md shadow-[#1d1d1dc7]"> - {items.map((v) => ( - <NavItem {...v} /> + <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> - </div> - - {/* 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"}`} + + <DialogContent className="sm:max-w-[475px] text-[#F2F3F5] rounded-2xl bg-background z-[39] backdrop-blur-md"> + <form + action={async (e: FormData) => { + const content = e.get("content")?.toString(); + + await handleSubmit(content, selectedSpaces); + }} + className="flex flex-col gap-4 " > - <HomeIcon width={24} height={24} /> - <p className="text-xs text-foreground-menu mt-2">Home</p> - </Link> + <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..."); - <DialogMobileTrigger /> - {menuItems.map((item) => ( + 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 ?? + "Unknown 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> + + {/* 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 - aria-disabled={item.disabled} - href={item.disabled ? "#" : item.url} - key={item.url} - className={`flex flex-col items-center ${ - item.disabled - ? "opacity-50 pointer-events-none" - : "cursor-pointer" - }`} + 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={item.icon} - alt={`${item.text} icon`} + 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">{item.text}</p> - </Link> - ))} - </div> - </div> - </> - ); -} - -export function Navbar() { - return ( - <div className="pointer-events-none fixed left-0 top-0 flex h-screen flex-col justify-center px-2"> - <div className="pointer-events-none absolute top-1/2 h-1/3 w-full -translate-y-1/2 bg-blue-500/20 blur-[300px] "></div> - <div className="pointer-events-auto"> - <div className="inline-flex w-14 flex-col items-start gap-6 rounded-2xl border-2 border-border px-3 py-4 text-[#b9b9b9] shadow-md shadow-[#1d1d1dc7]"> - <Top /> - {items.map((v) => ( - <NavItem {...v} /> - ))} - </div> - </div> - </div> - ); -} - -function Top() { - return ( - <DialogTriggerWrapper> - <DialogTrigger> - <div className="space-y-4 group relative"> - <div className="cursor-pointer px-1 hover:scale-105 hover:text-[#bfc4c9] active:scale-90"> - <PlusIcon className="h-6 w-6" /> - </div> - <div className="h-[1px] w-full bg-[#323b41]"></div> - <div className="opacity-0 group-hover:opacity-100 scale-x-50 group-hover:scale-x-100 origin-left transition-all absolute whitespace-nowrap -top-1 -translate-y-1/2 left-[150%] pointer-events-none border-gray-700/50 border-[1px] bg-[#1F2428] shadow-md shadow-[#1d1d1dc7] rounded-2xl px-2 py-1"> - Add Memories + <p className="text-xs text-foreground-menu mt-2">Add</p> + </DialogTrigger> + {menuItems.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> - </DialogTrigger> - </DialogTriggerWrapper> - ); -} - -function NavItem({ - disabled, - icon, - url, - name, -}: { - disabled: boolean; - icon: React.JSX.Element; - name: string; - url: string; -}) { - return ( - <div className="relative group"> - <Link aria-disabled={disabled} href={disabled ? "#" : url}> - <div - className={`cursor-pointer px-1 hover:scale-105 hover:text-[#bfc4c9] active:scale-90 ${disabled && "opacity-50"}`} - > - {icon} - </div> - </Link> - <div className="opacity-0 group-hover:opacity-100 scale-x-50 group-hover:scale-x-100 origin-left transition-all absolute whitespace-nowrap top-1/2 -translate-y-1/2 left-[150%] pointer-events-none border-gray-700/50 border-[1px] bg-[#1F2428] shadow-md shadow-[#1d1d1dc7] rounded-xl px-2 py-1"> - {name} - </div> - </div> + </Dialog> + </> ); } diff --git a/apps/web/app/(landing)/Hero.tsx b/apps/web/app/(landing)/Hero.tsx index 4a9a8e04..3c7b49e0 100644 --- a/apps/web/app/(landing)/Hero.tsx +++ b/apps/web/app/(landing)/Hero.tsx @@ -26,6 +26,18 @@ function Hero() { return ( <> <section className="flex relative flex-col gap-5 justify-center items-center mt-24 max-w-xl md:mt-32 md:max-w-2xl lg:max-w-3xl"> + <a + href="https://www.producthunt.com/posts/supermemory?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-supermemory" + target="_blank" + > + <img + src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=472686&theme=dark" + alt="Supermemory - AI second brain for all your saved stuff | Product Hunt" + style={{ width: "250px", height: "54px" }} + width="250" + height="54" + /> + </a> <TwitterBorder /> <motion.h1 {...{ diff --git a/packages/ui/icons/index.ts b/packages/ui/icons/index.ts index 516e3c81..6dc00679 100644 --- a/packages/ui/icons/index.ts +++ b/packages/ui/icons/index.ts @@ -15,6 +15,7 @@ import AutocompleteIcon from "./autocomplete.svg"; import BlockIcon from "./block.svg"; import DragIcon from "./drag.svg"; import SettingsIcon from "./settings.svg"; +import HomeIcon from "./home.svg"; export { AddIcon, @@ -34,4 +35,5 @@ export { BlockIcon, DragIcon, SettingsIcon, + HomeIcon, }; |