aboutsummaryrefslogtreecommitdiff
path: root/apps/web/app/(dash)
diff options
context:
space:
mode:
authorcodetorso <[email protected]>2024-07-20 07:17:25 +0530
committercodetorso <[email protected]>2024-07-20 07:17:25 +0530
commit21fe55a96c36892fc50e8198248da825a5a4bd6f (patch)
tree83832e20adb0e4acfa529ac624e7f2b914f7a25a /apps/web/app/(dash)
parentfix links (diff)
downloadsupermemory-torso.tar.xz
supermemory-torso.zip
for god's sake this should work ;)torso
Diffstat (limited to 'apps/web/app/(dash)')
-rw-r--r--apps/web/app/(dash)/chat/chatWindow.tsx218
-rw-r--r--apps/web/app/(dash)/chat/queryinput.tsx83
-rw-r--r--apps/web/app/(dash)/dialogContentContainer.tsx224
-rw-r--r--apps/web/app/(dash)/dialogTriggerWrapper.tsx50
-rw-r--r--apps/web/app/(dash)/home/filterSpaces.tsx108
-rw-r--r--apps/web/app/(dash)/home/heading.tsx38
-rw-r--r--apps/web/app/(dash)/home/history.tsx50
-rw-r--r--apps/web/app/(dash)/home/homeVariants.ts50
-rw-r--r--apps/web/app/(dash)/home/page.tsx97
-rw-r--r--apps/web/app/(dash)/home/queryinput.tsx163
-rw-r--r--apps/web/app/(dash)/layout.tsx8
-rw-r--r--apps/web/app/(dash)/menu.tsx483
12 files changed, 877 insertions, 695 deletions
diff --git a/apps/web/app/(dash)/chat/chatWindow.tsx b/apps/web/app/(dash)/chat/chatWindow.tsx
index f0827a3d..066e7d20 100644
--- a/apps/web/app/(dash)/chat/chatWindow.tsx
+++ b/apps/web/app/(dash)/chat/chatWindow.tsx
@@ -2,7 +2,7 @@
import { AnimatePresence } from "framer-motion";
import React, { useEffect, useRef, useState } from "react";
-import QueryInput from "../home/queryinput";
+import QueryInput from "./queryinput";
import { cn } from "@repo/ui/lib/utils";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
@@ -224,9 +224,73 @@ function ChatWindow({
{chat.question}
</h2>
- <div className="flex flex-col mt-2">
+ <div className="flex flex-col">
+ {/* Related memories */}
+ <div
+ className={`space-y-4 ${chat.answer.sources.length > 0 || chat.answer.parts.length === 0 ? "flex" : "hidden"}`}
+ >
+ <Accordion
+ defaultValue={
+ idx === chatHistory.length - 1 ? "memories" : ""
+ }
+ type="single"
+ collapsible
+ >
+ <AccordionItem value="memories">
+ <AccordionTrigger className="text-foreground-menu">
+ Related Memories
+ </AccordionTrigger>
+ {/* TODO: fade out content on the right side, the fade goes away when the user scrolls */}
+ <AccordionContent
+ className="flex items-center no-scrollbar overflow-auto gap-4 relative max-w-3xl no-scrollbar"
+ defaultChecked
+ >
+ {/* Loading state */}
+ {chat.answer.sources.length > 0 ||
+ (chat.answer.parts.length === 0 && (
+ <>
+ {[1, 2, 3, 4].map((_, idx) => (
+ <div
+ key={`loadingState-${idx}`}
+ className="w-[350px] shrink-0 p-4 gap-2 rounded-2xl flex flex-col bg-secondary animate-pulse"
+ >
+ <div className="bg-slate-700 h-2 rounded-full w-1/2"></div>
+ <div className="bg-slate-700 h-2 rounded-full w-full"></div>
+ </div>
+ ))}
+ </>
+ ))}
+ {chat.answer.sources.map((source, idx) => (
+ <Link
+ href={source.source}
+ key={idx}
+ className="w-[350px] shrink-0 p-4 gap-2 rounded-2xl flex flex-col bg-secondary"
+ >
+ <div className="flex justify-between text-foreground-menu text-sm">
+ <span>{source.type}</span>
+
+ {source.numChunks > 1 && (
+ <span>{source.numChunks} chunks</span>
+ )}
+ </div>
+ <div className="text-base">
+ {source.title}
+ </div>
+ <div className="text-xs line-clamp-2">
+ {source.content.length > 100
+ ? source.content.slice(0, 100) + "..."
+ : source.content}
+ </div>
+ </Link>
+ ))}
+ </AccordionContent>
+ </AccordionItem>
+ </Accordion>
+ </div>
+
+ {/* Summary */}
<div>
- <div className="text-foreground-menu py-2">Answer</div>
+ <div className="text-foreground-menu py-2">Summary</div>
<div className="text-base">
{/* Loading state */}
{(chat.answer.parts.length === 0 ||
@@ -283,108 +347,60 @@ function ChatWindow({
>
<ClipboardIcon className="size-[18px] group-hover:text-primary" />
</button>
+ <button
+ onClick={async () => {
+ const isWebShareSupported =
+ navigator.share !== undefined;
+ if (isWebShareSupported) {
+ try {
+ await navigator.share({
+ title: "Your Share Title",
+ text: "Your share text or description",
+ url: "https://your-url-to-share.com",
+ });
+ } catch (e) {
+ console.error("Error sharing:", e);
+ }
+ } else {
+ console.error("web share is not supported!");
+ }
+ }}
+ className="group h-8 w-8 flex justify-center items-center active:scale-75 duration-200"
+ >
+ <SendIcon className="size-[18px] group-hover:text-primary" />
+ </button>
</div>
</div>
</div>
-
- <div
- className={`space-y-4 ${chat.answer.sources.length > 0 || chat.answer.parts.length === 0 ? "flex" : "hidden"}`}
- >
- <Accordion
- defaultValue={
- idx === chatHistory.length - 1 ? "memories" : ""
- }
- type="single"
- collapsible
- >
- <AccordionItem value="memories">
- <AccordionTrigger className="text-foreground-menu">
- Related Memories
- </AccordionTrigger>
- {/* TODO: fade out content on the right side, the fade goes away when the user scrolls */}
- <AccordionContent
- className="flex flex-col no-scrollbar overflow-auto gap-4 relative max-w-3xl no-scrollbar"
- defaultChecked
+ {/* Justification */}
+ {chat.answer.justification &&
+ chat.answer.justification.length && (
+ <div
+ className={`${chat.answer.justification && chat.answer.justification.length > 0 ? "flex" : "hidden"}`}
+ >
+ <Accordion
+ defaultValue={""}
+ type="single"
+ collapsible
>
- <div className="w-full no-scrollbar flex gap-4">
- {/* Loading state */}
- {chat.answer.sources.length > 0 ||
- (chat.answer.parts.length === 0 && (
- <>
- {[1, 2, 3, 4].map((_, idx) => (
- <div
- key={`loadingState-${idx}`}
- className="w-[350px] shrink-0 p-4 gap-2 rounded-2xl flex flex-col bg-secondary animate-pulse"
- >
- <div className="bg-slate-700 h-2 rounded-full w-1/2"></div>
- <div className="bg-slate-700 h-2 rounded-full w-full"></div>
- </div>
- ))}
- </>
- ))}
- {chat.answer.sources.map((source, idx) => (
- <Link
- href={source.source}
- key={idx}
- className="w-[350px] shrink-0 p-4 gap-2 rounded-2xl flex flex-col bg-secondary"
- >
- <div className="flex justify-between text-foreground-menu text-sm">
- <span>{source.type}</span>
-
- {source.numChunks > 1 && (
- <span>{source.numChunks} chunks</span>
- )}
- </div>
- <div className="text-base">
- {source.title}
- </div>
- <div className="text-xs line-clamp-2">
- {source.content.length > 100
- ? source.content.slice(0, 100) + "..."
- : source.content}
- </div>
- </Link>
- ))}
- </div>
-
- {chat.answer.justification &&
- chat.answer.justification.length && (
- <div
- className={`${chat.answer.justification && chat.answer.justification.length > 0 ? "flex" : "hidden"}`}
- >
- <Accordion
- defaultValue={""}
- type="single"
- collapsible
- >
- <AccordionItem value="justification">
- <AccordionTrigger className="text-foreground-menu">
- Justification
- </AccordionTrigger>
- <AccordionContent
- className="relative flex gap-2 max-w-3xl overflow-auto no-scrollbar"
- defaultChecked
- >
- {chat.answer.justification.length > 0
- ? chat.answer.justification
- .replaceAll(
- "<justification>",
- "",
- )
- .replaceAll(
- "</justification>",
- "",
- )
- : "No justification provided."}
- </AccordionContent>
- </AccordionItem>
- </Accordion>
- </div>
- )}
- </AccordionContent>
- </AccordionItem>
- </Accordion>
- </div>
+ <AccordionItem value="justification">
+ <AccordionTrigger className="text-foreground-menu">
+ Justification
+ </AccordionTrigger>
+ <AccordionContent
+ className="relative flex gap-2 max-w-3xl overflow-auto no-scrollbar"
+ defaultChecked
+ >
+ {chat.answer.justification.length > 0
+ ? chat.answer.justification
+ .replaceAll("<justification>", "")
+ .replaceAll("</justification>", "")
+ : "No justification provided."}
+ </AccordionContent>
+ </AccordionItem>
+ </Accordion>
+ </div>
+ )}
</div>
</div>
</div>
diff --git a/apps/web/app/(dash)/chat/queryinput.tsx b/apps/web/app/(dash)/chat/queryinput.tsx
new file mode 100644
index 00000000..99f55986
--- /dev/null
+++ b/apps/web/app/(dash)/chat/queryinput.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import { ArrowRightIcon } from "@repo/ui/icons";
+import Image from "next/image";
+import React, { useState } from "react";
+
+function QueryInput({
+ initialSpaces,
+ initialQuery = "",
+ disabled = false,
+ className,
+ mini = false,
+ handleSubmit,
+}: {
+ initialQuery?: string;
+ initialSpaces?: {
+ id: number;
+ name: string;
+ }[];
+ disabled?: boolean;
+ className?: string;
+ mini?: boolean;
+ handleSubmit: (q: string, spaces: { id: number; name: string }[]) => void;
+}) {
+ const [q, setQ] = useState(initialQuery);
+
+ const [selectedSpaces, setSelectedSpaces] = useState<
+ { id: number; name: string }[]
+ >([]);
+
+ return (
+ <div className={`${className}`}>
+ <div
+ className={`bg-[#1F2428] overflow-hidden border-2 border-gray-700/50 shadow-md shadow-[#1d1d1dc7] rounded-3xl`}
+ >
+ {/* input and action button */}
+ <form
+ action={async () => {
+ if (q.trim().length === 0) {
+ return;
+ }
+ handleSubmit(q, selectedSpaces);
+ setQ("");
+ }}
+ className="flex gap-4 p-3"
+ >
+ <textarea
+ autoFocus
+ name="q"
+ cols={30}
+ rows={mini ? 2 : 4}
+ className="bg-transparent pt-2.5 text-lg placeholder:text-[#9B9B9B] focus:text-gray-200 duration-200 tracking-[3%] outline-none resize-none w-full p-4"
+ placeholder="Ask your second brain..."
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ if (q.trim().length === 0) {
+ return;
+ }
+ handleSubmit(q, selectedSpaces);
+ setQ("");
+ }
+ }}
+ onChange={(e) => setQ(e.target.value)}
+ value={q}
+ disabled={disabled}
+ />
+
+ <button
+ type="submit"
+ disabled={disabled}
+ className="h-12 w-12 rounded-[14px] bg-border all-center shrink-0 hover:brightness-125 duration-200 outline-none focus:outline focus:outline-primary active:scale-90"
+ >
+ <Image src={ArrowRightIcon} alt="Right arrow icon" />
+ </button>
+ </form>{" "}
+ </div>
+ {/* selected sources */}
+ </div>
+ );
+}
+
+export default QueryInput;
diff --git a/apps/web/app/(dash)/dialogContentContainer.tsx b/apps/web/app/(dash)/dialogContentContainer.tsx
new file mode 100644
index 00000000..7ad68f17
--- /dev/null
+++ b/apps/web/app/(dash)/dialogContentContainer.tsx
@@ -0,0 +1,224 @@
+import { StoredSpace } from "@/server/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([]);
+
+ if (cont.success) {
+ toast.success("Memory created", {
+ richColors: true,
+ });
+ } else {
+ toast.error(`Memory creation failed: ${cont.error}`);
+ }
+};
+
+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] backdrop-blur-md">
+ <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 ??
+ "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>
+);
+} \ No newline at end of file
diff --git a/apps/web/app/(dash)/dialogTriggerWrapper.tsx b/apps/web/app/(dash)/dialogTriggerWrapper.tsx
new file mode 100644
index 00000000..7dcfc355
--- /dev/null
+++ b/apps/web/app/(dash)/dialogTriggerWrapper.tsx
@@ -0,0 +1,50 @@
+"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-[#1F2428] 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)/home/filterSpaces.tsx b/apps/web/app/(dash)/home/filterSpaces.tsx
new file mode 100644
index 00000000..8818791a
--- /dev/null
+++ b/apps/web/app/(dash)/home/filterSpaces.tsx
@@ -0,0 +1,108 @@
+import { ChevronUpDownIcon } from "@heroicons/react/24/outline";
+import { ArrowRightIcon } from "@repo/ui/icons";
+import {
+ Command,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@repo/ui/shadcn/command";
+import { Check } from "lucide-react";
+import Image from "next/image";
+import React, { useState } from "react";
+
+type space = {
+ id: number;
+ name: string;
+};
+
+export function FilterSpaces({
+ initialSpaces,
+ selectedSpaces,
+ setSelectedSpaces,
+}: {
+ initialSpaces: space[];
+ selectedSpaces: space[];
+ setSelectedSpaces: React.Dispatch<React.SetStateAction<space[]>>;
+}) {
+ const [input, setInput] = useState<string>("");
+
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Backspace" && input === "") {
+ setSelectedSpaces((prevValue) => prevValue.slice(0, -1));
+ }
+ };
+
+ const handleSelect = (selectedSpace: space) => {
+ setSelectedSpaces((current) =>
+ current.some((space) => space.id === selectedSpace.id)
+ ? current.filter((space) => space.id !== selectedSpace.id)
+ : [...current, selectedSpace],
+ );
+ };
+
+ return (
+ <div className="flex p-2 px-3 w-full items-center justify-between rounded-xl overflow-hidden">
+ <div className="flex bg-[#2C3338] rounded-xl overflow-hidden pl-1">
+ <div className="flex rounded-lg items-center">
+ {selectedSpaces.map((v) => (
+ <button
+ key={v.id}
+ onClick={() => handleSelect(v)}
+ className="bg-[#3a4248] text-white max-w-32 truncate-wor truncate whitespace-nowrap py-1 rounded-md px-2 mx-1 aria-selected:outline"
+ >
+ {v.name}
+ </button>
+ ))}
+ </div>
+ <Command
+ className={`group transition-all border-0 bg-[#2c3338] text-white outline-0 ${
+ selectedSpaces.length ? "w-5 hover:w-24 focus-within:w-20" : "w-44"
+ }`}
+ >
+ <div className="relative">
+ <CommandInput
+ placeholder={selectedSpaces.length ? "" : "Search in Spaces"}
+ onKeyDown={handleKeyDown}
+ className="text-white peer placeholder:text-white pl-2"
+ onChangeCapture={(e) => setInput(e.currentTarget.value)}
+ value={input}
+ />
+ <ChevronUpDownIcon
+ className={`h-6 w-6 text-[#858B92] pointer-events-none absolute top-1/2 -translate-y-1/2 right-2 ${
+ selectedSpaces.length && "opacity-0"
+ }`}
+ />
+ </div>
+ <CommandList className="z-10 translate-y-12 translate-x-5 opacity-0 absolute group-focus-within:opacity-100 transition-opacity p-2 rounded-lg max-w-64 bg-[#2C3338]">
+ <CommandGroup className="pointer-events-none opacity-0 group-focus-within:opacity-100 scale-50 scale-y-50 group-focus-within:scale-y-100 group-focus-within:scale-100 group-focus-within:pointer-events-auto transition-all origin-top">
+ {initialSpaces.map((space) => {
+ if (!selectedSpaces.some((v) => v.id === space.id)) {
+ return (
+ <CommandItem
+ className="text-[#eaeaea] data-[disabled]:opacity-90"
+ value={space.name}
+ key={space.id}
+ onSelect={() => handleSelect(space)}
+ >
+ <Check
+ className={`mr-2 h-4 w-4 ${selectedSpaces.some((v) => v.id === space.id) ? "opacity-100" : "opacity-0"}`}
+ />
+ {space.name}
+ </CommandItem>
+ );
+ }
+ })}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </div>
+ <button
+ type="submit"
+ className="rounded-lg bg-[#369DFD1A] p-3"
+ >
+ <Image src={ArrowRightIcon} alt="Enter" />
+ </button>
+ </div>
+ );
+}
diff --git a/apps/web/app/(dash)/home/heading.tsx b/apps/web/app/(dash)/home/heading.tsx
new file mode 100644
index 00000000..dc5b8799
--- /dev/null
+++ b/apps/web/app/(dash)/home/heading.tsx
@@ -0,0 +1,38 @@
+import { useEffect, useState } from "react";
+import { AnimatePresence, motion } from "framer-motion";
+import { Inter } from "next/font/google";
+
+const poppins = Inter({ subsets: ["latin"], weight: ["600"] });
+
+const headings = [
+ "Unlock your digital brain",
+ "Save everything.",
+ " Connect anything.",
+ "Turn your bookmarks into insights.",
+ "The smart way to use your digital treasure.",
+];
+
+export function Heading({ queryPresent }: { queryPresent: boolean }) {
+ const [showHeading, setShowHeading] = useState<number>(0);
+ useEffect(() => {
+ setShowHeading(Math.floor(Math.random() * headings.length));
+ }, [queryPresent]);
+ return (
+ <div className="h-[7rem] flex items-end justify-center overflow-hidden text-white">
+ <AnimatePresence mode="popLayout">
+ {!queryPresent && (
+ <motion.h1
+ initial={{ opacity: 0, y: "20%" }}
+ animate={{ opacity: 1, y: "0%" }}
+ exit={{ opacity: 0, y: "20%", whiteSpace: "nowrap" }}
+ className={`text-[2.45rem] font-semibold ${
+ queryPresent ? "pointer-events-none" : "pointer-events-auto"
+ } transition-opacity text-center ${poppins.className}`}
+ >
+ {headings[showHeading]}
+ </motion.h1>
+ )}
+ </AnimatePresence>
+ </div>
+ );
+}
diff --git a/apps/web/app/(dash)/home/history.tsx b/apps/web/app/(dash)/home/history.tsx
new file mode 100644
index 00000000..9c6757e5
--- /dev/null
+++ b/apps/web/app/(dash)/home/history.tsx
@@ -0,0 +1,50 @@
+import { getRecentChats } from "@/app/actions/fetchers";
+import { ArrowLongRightIcon } from "@heroicons/react/24/outline";
+import { Skeleton } from "@repo/ui/shadcn/skeleton";
+import Link from "next/link";
+import { memo, useEffect, useState } from "react";
+import { motion } from "framer-motion";
+
+const History = memo(() => {
+ const [chatThreads, setChatThreads] = useState(null);
+
+ useEffect(() => {
+ (async () => {
+ const chatThreads = await getRecentChats();
+
+ // @ts-ignore
+ setChatThreads(chatThreads);
+ })();
+ }, []);
+
+ if (!chatThreads) {
+ return (
+ <>
+ <Skeleton className="w-[80%] h-4 bg-[#3b444b] "></Skeleton>
+ <Skeleton className="w-[40%] h-4 bg-[#3b444b] "></Skeleton>
+ <Skeleton className="w-[60%] h-4 bg-[#3b444b] "></Skeleton>
+ </>
+ );
+ }
+
+ // @ts-ignore, time wastage
+ if (!chatThreads.success || !chatThreads.data) {
+ return <div>Error fetching chat threads</div>;
+ }
+
+ return (
+ <ul className="text-base list-none space-y-3 text-[#b9b9b9]">
+ {/* @ts-ignore */}
+ {chatThreads.data.map((thread) => (
+ <motion.li initial={{opacity: 0, filter: "blur(1px)"}} animate={{opacity: 1, filter: "blur(0px)"}} className="flex items-center gap-2 truncate">
+ <ArrowLongRightIcon className="h-5" />{" "}
+ <Link prefetch={false} href={`/chat/${thread.id}`}>
+ {thread.firstMessage}
+ </Link>
+ </motion.li>
+ ))}
+ </ul>
+ );
+});
+
+export default History; \ No newline at end of file
diff --git a/apps/web/app/(dash)/home/homeVariants.ts b/apps/web/app/(dash)/home/homeVariants.ts
deleted file mode 100644
index 1b44bab9..00000000
--- a/apps/web/app/(dash)/home/homeVariants.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-export const variants = [
- [
- {
- type: "text",
- content: "Unlock your",
- },
- {
- type: "highlighted",
- content: " digital brain",
- },
- ],
- [
- {
- type: "text",
- content: "Save",
- },
- {
- type: "highlighted",
- content: " everything.",
- },
- {
- type: "text",
- content: " Connect",
- },
- {
- type: "highlighted",
- content: " anything.",
- },
- ],
- [
- {
- type: "text",
- content: "Turn your bookmarks into",
- },
- {
- type: "highlighted",
- content: " insights.",
- },
- ],
- [
- {
- type: "text",
- content: "The smart way to use your",
- },
- {
- type: "highlighted",
- content: " digital treasure.",
- },
- ],
-];
diff --git a/apps/web/app/(dash)/home/page.tsx b/apps/web/app/(dash)/home/page.tsx
index 378acdf8..630c4306 100644
--- a/apps/web/app/(dash)/home/page.tsx
+++ b/apps/web/app/(dash)/home/page.tsx
@@ -6,21 +6,18 @@ import { getSessionAuthToken, getSpaces } from "@/app/actions/fetchers";
import { useRouter } from "next/navigation";
import { createChatThread, linkTelegramToUser } from "@/app/actions/doers";
import { toast } from "sonner";
-import { motion } from "framer-motion";
-import { variants } from "./homeVariants";
+import { Heading } from "./heading";
+import History from "./history";
import { ChromeIcon, GithubIcon, TwitterIcon } from "lucide-react";
-const slap = {
- initial: {
- opacity: 0,
- scale: 1.1,
- },
- whileInView: { opacity: 1, scale: 1 },
- transition: {
- duration: 0.5,
- ease: "easeInOut",
- },
- viewport: { once: true },
+const linkTelegram = async (telegramUser: string) => {
+ const response = await linkTelegramToUser(telegramUser);
+
+ if (response.success) {
+ toast.success("Your telegram has been linked successfully.");
+ } else {
+ toast.error("Failed to link telegram. Please try again.");
+ }
};
function Page({
@@ -28,41 +25,24 @@ function Page({
}: {
searchParams: Record<string, string | string[] | undefined>;
}) {
- // TODO: use this to show a welcome page/modal
- // const { firstTime } = homeSearchParamsCache.parse(searchParams);
-
- const [telegramUser, setTelegramUser] = useState<string | undefined>(
- searchParams.telegramUser as string,
- );
- const [extensionInstalled, setExtensionInstalled] = useState<
- string | undefined
- >(searchParams.extension as string);
-
const { push } = useRouter();
const [spaces, setSpaces] = useState<{ id: number; name: string }[]>([]);
- const [showVariant, setShowVariant] = useState<number>(0);
+ const [queryPresent, setQueryPresent] = useState<boolean>(false);
useEffect(() => {
+ // telegram bot
+ const telegramUser = searchParams.extension as string;
if (telegramUser) {
- const linkTelegram = async () => {
- const response = await linkTelegramToUser(telegramUser);
-
- if (response.success) {
- toast.success("Your telegram has been linked successfully.");
- } else {
- toast.error("Failed to link telegram. Please try again.");
- }
- };
-
- linkTelegram();
+ linkTelegram(telegramUser);
}
- if (extensionInstalled) {
+ if (searchParams.extension as string) {
toast.success("Extension installed successfully");
}
+ // fetch spaces
getSpaces().then((res) => {
if (res.success && res.data) {
setSpaces(res.data);
@@ -71,44 +51,18 @@ function Page({
// TODO: HANDLE ERROR
});
- setShowVariant(Math.floor(Math.random() * variants.length));
-
getSessionAuthToken().then((token) => {
if (typeof window === "undefined") return;
window.postMessage({ token: token.data }, "*");
});
- }, [telegramUser]);
+ }, []);
return (
- <div className="max-w-3xl h-full justify-center flex mx-auto w-full flex-col px-2 md:px-0">
- {/* all content goes here */}
- {/* <div className="">hi {firstTime ? 'first time' : ''}</div> */}
-
- <motion.h1
- {...{
- ...slap,
- transition: { ...slap.transition, delay: 0.2 },
- }}
- className="text-center mx-auto bg-[linear-gradient(180deg,_#FFF_0%,_rgba(255,_255,_255,_0.00)_202.08%)] bg-clip-text text-4xl tracking-tighter text-transparent md:text-5xl"
- >
- {variants[showVariant]!.map((v, i) => {
- return (
- <span
- key={i}
- className={
- v.type === "highlighted"
- ? "bg-gradient-to-r to-blue-200 from-zinc-300 text-transparent bg-clip-text"
- : ""
- }
- >
- {v.content}
- </span>
- );
- })}
- </motion.h1>
-
- <div className="w-full pb-20 mt-12">
+ <div className="max-w-3xl mt-[18vh] mx-auto w-full px-2 md:px-0">
+ <Heading queryPresent={queryPresent} />
+ <div className="w-full py-12">
<QueryInput
+ setQueryPresent={(t: boolean) => setQueryPresent(t)}
handleSubmit={async (q, spaces) => {
if (q.length === 0) {
toast.error("Query is required");
@@ -127,9 +81,12 @@ function Page({
);
}}
initialSpaces={spaces}
- setInitialSpaces={setSpaces}
/>
</div>
+ <div className="space-y-5">
+ <h3 className="text-lg">Recent Searches</h3>
+ <History />
+ </div>
<div className="w-full fixed bottom-0 left-0 p-4">
<div className="flex items-center justify-center gap-8">
@@ -143,7 +100,7 @@ function Page({
Install extension
</a>
<a
- href="https://github.com/supermemoryai/supermemory/issues/new"
+ href="https://github.com/Dhravya/supermemory/issues/new"
target="_blank"
rel="noreferrer"
className="flex items-center gap-2 text-muted-foreground"
@@ -152,7 +109,7 @@ function Page({
Bug report
</a>
<a
- href="https://x.com/supermemoryai"
+ href="https://x.com/supermemory.ai"
target="_blank"
rel="noreferrer"
className="flex items-center gap-2 text-muted-foreground"
diff --git a/apps/web/app/(dash)/home/queryinput.tsx b/apps/web/app/(dash)/home/queryinput.tsx
index c7267298..f15a712b 100644
--- a/apps/web/app/(dash)/home/queryinput.tsx
+++ b/apps/web/app/(dash)/home/queryinput.tsx
@@ -1,83 +1,48 @@
"use client";
-import { ArrowRightIcon } from "@repo/ui/icons";
-import Image from "next/image";
-import React, { useEffect, useMemo, useState } from "react";
-import Divider from "@repo/ui/shadcn/divider";
-import { useRouter } from "next/navigation";
-import { getSpaces } from "@/app/actions/fetchers";
-import Combobox from "@repo/ui/shadcn/combobox";
-import { MinusIcon } from "lucide-react";
-import { toast } from "sonner";
-import { createSpace } from "@/app/actions/doers";
+import React, { useState } from "react";
+import { FilterSpaces } from "./filterSpaces";
function QueryInput({
- initialQuery = "",
- initialSpaces = [],
- disabled = false,
- className,
- mini = false,
+ setQueryPresent,
+ initialSpaces,
handleSubmit,
- setInitialSpaces,
}: {
- initialQuery?: string;
+ setQueryPresent: (t: boolean) => void;
initialSpaces?: {
id: number;
name: string;
}[];
- disabled?: boolean;
- className?: string;
mini?: boolean;
handleSubmit: (q: string, spaces: { id: number; name: string }[]) => void;
- setInitialSpaces?: React.Dispatch<
- React.SetStateAction<{ id: number; name: string }[]>
- >;
}) {
- const [q, setQ] = useState(initialQuery);
+ const [q, setQ] = useState("");
- const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]);
-
- const options = useMemo(
- () =>
- initialSpaces.map((x) => ({
- label: x.name,
- value: x.id.toString(),
- })),
- [initialSpaces],
- );
-
- const preparedSpaces = useMemo(
- () =>
- initialSpaces
- .filter((x) => selectedSpaces.includes(x.id))
- .map((x) => {
- return {
- id: x.id,
- name: x.name,
- };
- }),
- [selectedSpaces, initialSpaces],
- );
+ const [selectedSpaces, setSelectedSpaces] = useState<
+ { id: number; name: string }[]
+ >([]);
return (
- <div className={`${className}`}>
+ <div className={`w-full`}>
<div
- className={`bg-secondary border-2 border-b-0 border-border ${!mini ? "rounded-t-3xl" : "rounded-3xl"}`}
+ className={`bg-[#1F2428] overflow-hidden border-2 border-gray-700/50 shadow-md shadow-[#1d1d1dc7] rounded-3xl`}
>
{/* input and action button */}
<form
action={async () => {
- handleSubmit(q, preparedSpaces);
+ if (q.trim().length === 0) {
+ return;
+ }
+ handleSubmit(q, selectedSpaces);
setQ("");
}}
- className="flex gap-4 p-3"
>
<textarea
autoFocus
name="q"
cols={30}
- rows={mini ? 2 : 4}
- className="bg-transparent pt-2.5 text-base placeholder:text-[#9B9B9B] focus:text-gray-200 duration-200 tracking-[3%] outline-none resize-none w-full p-4"
+ rows={4}
+ className="bg-transparent pt-2.5 text-lg placeholder:text-[#9B9B9B] text-gray-200 tracking-[3%] outline-none resize-none w-full p-4"
placeholder="Ask your second brain..."
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
@@ -85,95 +50,25 @@ function QueryInput({
if (q.trim().length === 0) {
return;
}
- handleSubmit(q, preparedSpaces);
+ handleSubmit(q, selectedSpaces);
setQ("");
}
}}
- onChange={(e) => setQ(e.target.value)}
+ onChange={(e) =>
+ setQ((prev) => {
+ setQueryPresent(!!e.target.value.length);
+ return e.target.value;
+ })
+ }
value={q}
- disabled={disabled}
/>
-
- <button
- type="submit"
- onClick={(e) => {
- e.preventDefault();
- if (q.trim().length === 0) {
- return;
- }
- handleSubmit(q, preparedSpaces);
- }}
- disabled={disabled}
- className="h-12 w-12 rounded-[14px] bg-border all-center shrink-0 hover:brightness-125 duration-200 outline-none focus:outline focus:outline-primary active:scale-90"
- >
- <Image src={ArrowRightIcon} alt="Right arrow icon" />
- </button>
+ <FilterSpaces
+ selectedSpaces={selectedSpaces}
+ setSelectedSpaces={setSelectedSpaces}
+ initialSpaces={initialSpaces || []}
+ />
</form>
</div>
- {/* selected sources */}
- {!mini && (
- <>
- <Divider />
- <div className="flex justify-between items-center gap-6 h-auto bg-secondary rounded-b-3xl border-2 border-border">
- <Combobox
- options={options}
- className="rounded-bl-3xl bg-[#3C464D] w-44"
- 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);
- setInitialSpaces?.((prev) => [
- ...prev,
- {
- name: spaceName,
- id: creationTask.data!,
- },
- ]);
- setSelectedSpaces((prev) => [...prev, creationTask.data!]);
- } else {
- toast.error(
- "Space creation failed: " + creationTask.error ??
- "Unknown error",
- );
- }
- }}
- placeholder="Chat with a space..."
- />
-
- <div className="flex flex-row gap-0.5 h-full">
- {preparedSpaces.map((x, idx) => (
- <button
- key={x.id}
- onClick={() =>
- setSelectedSpaces((prev) => prev.filter((y) => y !== x.id))
- }
- className={`relative group p-2 py-3 bg-[#3C464D] max-w-32 ${idx === preparedSpaces.length - 1 ? "rounded-br-xl" : ""}`}
- >
- <p className="line-clamp-1">{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>
);
}
diff --git a/apps/web/app/(dash)/layout.tsx b/apps/web/app/(dash)/layout.tsx
index b2b27a4f..24857fb1 100644
--- a/apps/web/app/(dash)/layout.tsx
+++ b/apps/web/app/(dash)/layout.tsx
@@ -3,7 +3,6 @@ import Menu from "./menu";
import { redirect } from "next/navigation";
import { auth } from "../../server/auth";
import { Toaster } from "@repo/ui/shadcn/sonner";
-import BackgroundPlus from "../(landing)/GridPatterns/PlusGrid";
async function Layout({ children }: { children: React.ReactNode }) {
const info = await auth();
@@ -13,18 +12,15 @@ async function Layout({ children }: { children: React.ReactNode }) {
}
return (
- <main className="h-screen flex flex-col">
+ <main className="h-screen bg flex flex-col">
<div className="fixed top-0 left-0 w-full z-40">
<Header />
</div>
- <div className="relative flex justify-center z-40 pointer-events-none">
<div
- className="absolute -z-10 left-0 top-[10%] h-32 w-[90%] overflow-x-hidden bg-[rgb(54,157,253)] bg-opacity-100 md:bg-opacity-70 blur-[337.4px]"
+ className="absolute z-[100] left-0 top-[10%] h-32 w-[90%] overflow-x-hidden bg-[rgb(54,157,253)] bg-opacity-100 pointer-events-none md:bg-opacity-70 blur-[337.4px]"
style={{ transform: "rotate(-30deg)" }}
/>
- </div>
- <BackgroundPlus className="absolute top-0 left-0 w-full h-full -z-50 opacity-70" />
<Menu />
diff --git a/apps/web/app/(dash)/menu.tsx b/apps/web/app/(dash)/menu.tsx
index 711b081c..3cb79309 100644
--- a/apps/web/app/(dash)/menu.tsx
+++ b/apps/web/app/(dash)/menu.tsx
@@ -1,359 +1,174 @@
-"use client";
-
-import React, { useEffect, useMemo, useState } from "react";
+import React from "react";
import Image from "next/image";
import Link from "next/link";
-import { MemoriesIcon, ExploreIcon, CanvasIcon, AddIcon } 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 { MemoriesIcon, CanvasIcon, AddIcon } from "@repo/ui/icons";
+import { DialogTrigger } from "@repo/ui/shadcn/dialog";
+
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";
+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,
+ },
+];
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: MemoriesIcon,
- text: "Memories",
- url: "/memories",
- disabled: false,
- },
- {
- icon: CanvasIcon,
- text: "Canvas",
- url: "/canvas",
- disabled: true,
- },
- ];
-
- 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 */}
- <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 className="hidden lg:flex items-center pointer-events-none z-[39] fixed left-0 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-blue-500/20 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-[#1F2428] px-3 py-4 text-[#b9b9b9] shadow-md shadow-[#1d1d1dc7]">
+ {items.map((v) => (
+ <NavItem {...v} />
))}
</div>
</div>
-
- <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 "
+ </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"}`}
>
- <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...");
+ <HomeIcon width={24} height={24} />
+ <p className="text-xs text-foreground-menu mt-2">Home</p>
+ </Link>
- 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">
+ <DialogMobileTrigger />
+ {menuItems.map((item) => (
<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`}
+ 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"
+ }`}
>
<Image
- src={AddIcon}
- alt="Logo"
+ src={item.icon}
+ alt={`${item.text} icon`}
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.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>
+ <p className="text-xs text-foreground-menu mt-2">{item.text}</p>
+ </Link>
+ ))}
</div>
- </Dialog>
+ </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-[1px] border-gray-700/50 bg-[#1f24289b] 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">
+ <button className="cursor-pointer px-1 hover:scale-105 hover:text-[#bfc4c9] active:scale-90">
+ <PlusIcon className="h-6 w-6" />
+ </button>
+ <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-xl px-2 py-1">
+ Add Memories
+ </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>
+ );
+}
+
export default Menu;