diff options
| author | yxshv <[email protected]> | 2024-04-14 14:29:23 +0530 |
|---|---|---|
| committer | yxshv <[email protected]> | 2024-04-14 14:29:23 +0530 |
| commit | fa39265142a7aa452a273e4290d58757af2786bb (patch) | |
| tree | 52e2e07d2a20009d650ed0b3ebe60aaab87d81ff /apps/web/src | |
| parent | fixed notes vectorize (diff) | |
| download | supermemory-fa39265142a7aa452a273e4290d58757af2786bb.tar.xz supermemory-fa39265142a7aa452a273e4290d58757af2786bb.zip | |
new modals
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/actions/db.ts | 268 | ||||
| -rw-r--r-- | apps/web/src/app/globals.css | 12 | ||||
| -rw-r--r-- | apps/web/src/app/page.tsx | 4 | ||||
| -rw-r--r-- | apps/web/src/components/ChatMessage.tsx | 22 | ||||
| -rw-r--r-- | apps/web/src/components/Main.tsx | 25 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/AddMemoryDialog.tsx | 3 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/DeleteConfirmation.tsx | 35 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/EditNoteDialog.tsx | 152 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/FilterCombobox.tsx | 5 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/MemoriesBar.tsx | 98 | ||||
| -rw-r--r-- | apps/web/src/contexts/MemoryContext.tsx | 40 | ||||
| -rw-r--r-- | apps/web/src/lib/utils.ts | 24 |
12 files changed, 578 insertions, 110 deletions
diff --git a/apps/web/src/actions/db.ts b/apps/web/src/actions/db.ts index cd1a0f1d..20aa4de6 100644 --- a/apps/web/src/actions/db.ts +++ b/apps/web/src/actions/db.ts @@ -4,14 +4,12 @@ import { db } from "@/server/db"; import { contentToSpace, sessions, - StoredContent, storedContent, - StoredSpace, users, space, } from "@/server/db/schema"; import { SearchResult } from "@/contexts/MemoryContext"; -import { like, eq, and, sql, exists, asc, notExists } from "drizzle-orm"; +import { like, eq, and, sql, exists, asc, notExists, inArray, notInArray } from "drizzle-orm"; import { union } from "drizzle-orm/sqlite-core"; import { env } from "@/env"; @@ -82,6 +80,22 @@ export async function searchMemoriesAndSpaces( } } +export async function getMemoriesFromUrl(urls: string[]) { + + const user = await getUser(); + + if (!user) { + return []; + } + + return urls.length > 0 ? await db.select() + .from(storedContent) + .where(and( + inArray(storedContent.url, urls), + eq(storedContent.user, user.id) + )).all() : [] +} + async function getUser() { const token = cookies().get("next-auth.session-token")?.value ?? @@ -167,6 +181,38 @@ export async function addSpace(name: string, memories: number[]) { }; } +export async function fetchContent(id: number) { + + + const user = await getUser(); + + if (!user) { + return null; + } + + const fetchedMemory = await db.select() + .from(storedContent) + .where(and( + eq(storedContent.id, id), + eq(storedContent.user, user.id) + )); + + const memory = fetchedMemory.length > 0 ? fetchedMemory[0] : null + + const spaces = memory ? await db.select() + .from(contentToSpace) + .where( + eq(contentToSpace.contentId, memory.id) + ) : [] + + + return { + memory, + spaces: spaces.map(s => s.spaceId) + } + +} + export async function fetchContentForSpace( spaceId: number, range?: { @@ -174,6 +220,13 @@ export async function fetchContentForSpace( limit: number; }, ) { + + const user = await getUser(); + + if (!user) { + return null; + } + const query = db .select() .from(storedContent) @@ -184,9 +237,19 @@ export async function fetchContentForSpace( .from(contentToSpace) .where( and( - eq(contentToSpace.spaceId, spaceId), - eq(contentToSpace.contentId, storedContent.id), - ), + and( + eq(contentToSpace.spaceId, spaceId), + eq(contentToSpace.contentId, storedContent.id), + ), + exists( + db.select() + .from(space) + .where(and( + eq(space.user, user.id), + eq(space.id, contentToSpace.spaceId) + )) + ) + ) ), ), ) @@ -207,25 +270,30 @@ export async function fetchFreeMemories(range?: { return []; } - const query = db - .select() - .from(storedContent) - .where( - and( - notExists( - db - .select() - .from(contentToSpace) - .where(eq(contentToSpace.contentId, storedContent.id)), - ), - eq(storedContent.user, user.id), - ), - ) - .orderBy(asc(storedContent.savedAt)); + try { + const query = db + .select() + .from(storedContent) + .where( + and( + notExists( + db + .select() + .from(contentToSpace) + .where(eq(contentToSpace.contentId, storedContent.id)), + ), + eq(storedContent.user, user.id), + ), + ) + .orderBy(asc(storedContent.savedAt)); + + return range + ? await query.limit(range.limit).offset(range.offset) + : await query.all(); + } catch { + return [] + } - return range - ? await query.limit(range.limit).offset(range.offset) - : await query.all(); } export async function addMemory( @@ -238,7 +306,7 @@ export async function addMemory( return null; } - if (!content.content || content.content == "") { + if (!content.content || content.content.trim() === "") { const resp = await fetch( `https://cf-ai-backend.dhravya.workers.dev/getPageContent?url=${content.url}`, { @@ -259,9 +327,36 @@ export async function addMemory( return null; } - console.log(content); + let [addedMemory] = await db + .insert(storedContent) + .values({ + user: user.id, + ...content, + }) + .returning(); + + const addedToSpaces = + spaces.length > 0 + ? await db + .insert(contentToSpace) + .values( + spaces.map((s) => ({ + contentId: addedMemory.id, + spaceId: s, + })), + ) + .returning() + : []; + + if (content.type === 'note') { + addedMemory = (await db.update(storedContent) + .set({ + url: addedMemory.url + addedMemory.id + }) + .where(eq(storedContent.id, addedMemory.id)) + .returning())[0] + } - console.log({ ...content, user: user.email }); // Add to vectorDB const res = (await Promise.race([ @@ -271,9 +366,9 @@ export async function addMemory( "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, }, body: JSON.stringify({ - pageContent: content.content, - title: content.title, - url: content.url, + pageContent: addedMemory.content, + title: addedMemory.title, + url: addedMemory.url, user: user.email, }), }), @@ -282,30 +377,110 @@ export async function addMemory( ), ])) as Response; - const [addedMemory] = await db - .insert(storedContent) - .values({ - user: user.id, - ...content, - }) + return { + memory: addedMemory, + addedToSpaces, + }; +} + + +export async function updateMemory( + id: number, + { title, content, spaces }: { + title?: string; + content?: string; + spaces?: number[] + } +) { + const user = await getUser(); + + if (!user) { + return null; + } + + console.log("updating") + + const [prev] = await db.select() + .from(storedContent) + .where(and( + eq(storedContent.user, user.id), + eq(storedContent.id, id) + )); + + if (!prev) { + return null + } + + const newContent = { + ...(title ? { title }: {}), + ...(content ? { content }: {}), + } + + const updated = { + ...newContent, + ...prev + } + + // Add to vectorDB + const res = (await Promise.race([ + fetch("https://cf-ai-backend.dhravya.workers.dev/edit?uniqueUrl="+updated.url , { + method: "POST", + headers: { + "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, + }, + body: JSON.stringify({ + pageContent: updated.content, + title: updated.title, + url: updated.url, + user: user.email, + }), + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Request timed out")), 40000), + ), + ])) as Response; + + const [updatedMemory] = await db + .update(storedContent) + .set(newContent) + .where( + eq(storedContent.id, id) + ) .returning(); + + console.log(updatedMemory, newContent) + + const removedFromSpaces = spaces ? + spaces.length > 0 ? + await db.delete(contentToSpace) + .where(and( + notInArray(contentToSpace.spaceId, spaces), + eq(contentToSpace.contentId, id) + )).returning() + : await db.delete(contentToSpace) + .where( + eq(contentToSpace.contentId, id) + ) + : []; const addedToSpaces = - spaces.length > 0 + (spaces && spaces.length > 0) ? await db .insert(contentToSpace) .values( spaces.map((s) => ({ - contentId: addedMemory.id, + contentId: id, spaceId: s, })), ) + .onConflictDoNothing() .returning() : []; return { - memory: addedMemory, + memory: updatedMemory, addedToSpaces, + removedFromSpaces }; } @@ -340,5 +515,20 @@ export async function deleteMemory(id: number) { .where(and(eq(storedContent.user, user.id), eq(storedContent.id, id))) .returning(); + if (deleted) { + + const res = (await Promise.race([ + fetch(`https://cf-ai-backend.dhravya.workers.dev/delete?websiteUrl=${deleted.url}&user=${user.email}` , { + method: "DELETE", + headers: { + "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, + } + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Request timed out")), 40000), + ), + ])) as Response; + } + return deleted; } diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index b09627ba..895b87e4 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -57,18 +57,22 @@ body { padding-bottom: 15dvh; } -.chat-answer pre { - @apply bg-rgray-3 rounded-md border border-rgray-5 p-3 text-sm my-5; +.chat-answer code { + @apply bg-rgray-3 rounded-md border border-rgray-5 p-1 text-sm text-rgray-11; } .novel-editor pre { - @apply bg-rgray-3 rounded-md border border-rgray-5 p-4 text-sm text-rgray-11; + @apply bg-rgray-3 rounded-md border border-rgray-5 p-4 my-5 text-sm text-rgray-11; } .chat-answer h1 { @apply text-rgray-11 my-5 text-xl font-medium; } +.chat-answer a { + @apply underline underline-offset-1 opacity-90 hover:opacity-100; +} + .chat-answer img { @apply rounded-md font-medium my-5; } @@ -122,4 +126,4 @@ body { .novel-editor .drag-handle { @apply hidden; -}
\ No newline at end of file +} diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 1cc21adf..8df5dad3 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -64,8 +64,6 @@ export default async function Home() { // Fetch only first 3 content of each spaces let contents: ChachedSpaceContent[] = []; - //console.log(await db.select().from(storedContent).) - await Promise.all([ collectedSpaces.forEach(async (space) => { console.log("fetching "); @@ -82,7 +80,7 @@ export default async function Home() { }), ]); - console.log(contents); + console.log('contents', contents); // freeMemories const freeMemories = await fetchFreeMemories(userData.id); diff --git a/apps/web/src/components/ChatMessage.tsx b/apps/web/src/components/ChatMessage.tsx index db0778c4..1567a9ac 100644 --- a/apps/web/src/components/ChatMessage.tsx +++ b/apps/web/src/components/ChatMessage.tsx @@ -1,9 +1,10 @@ import React, { useEffect } from "react"; import { motion } from "framer-motion"; -import { ArrowUpRight, Globe } from "lucide-react"; +import { ArrowUpRight, Globe, Text } from "lucide-react"; import { convertRemToPixels } from "@/lib/utils"; import { SpaceIcon } from "@/assets/Memories"; import Markdown from "react-markdown"; +import { ChatHistory } from "../../types/memory"; export function ChatAnswer({ children: message, @@ -11,7 +12,7 @@ export function ChatAnswer({ loading = false, }: { children: string; - sources?: string[]; + sources?: ChatHistory['answer']['sources']; loading?: boolean; }) { return ( @@ -29,15 +30,22 @@ export function ChatAnswer({ <SpaceIcon className="h-6 w-6 -translate-y-[2px]" /> Related Memories </h1> - <div className="animate-fade-in -mt-3 flex items-center justify-start opacity-0 [animation-duration:1s]"> - {sources?.map((source) => ( + <div className="animate-fade-in gap-1 -mt-3 flex items-center justify-start opacity-0 [animation-duration:1s]"> + {sources?.map((source) => source.isNote ? ( + <button + className="bg-rgray-3 flex items-center justify-center gap-2 rounded-full py-1 pl-2 pr-3 text-sm" + > + <Text className="w-4 h-4" /> + {source.source} + </button> + ) : ( <a className="bg-rgray-3 flex items-center justify-center gap-2 rounded-full py-1 pl-2 pr-3 text-sm" - key={source} - href={source} + key={source.source} + href={source.source} > <Globe className="h-4 w-4" /> - {cleanUrl(source)} + {cleanUrl(source.source)} </a> ))} </div> diff --git a/apps/web/src/components/Main.tsx b/apps/web/src/components/Main.tsx index bbdb630d..df6a08bf 100644 --- a/apps/web/src/components/Main.tsx +++ b/apps/web/src/components/Main.tsx @@ -13,6 +13,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useMemory } from "@/contexts/MemoryContext"; import Image from "next/image"; +import { getMemoriesFromUrl } from "@/actions/db"; function supportsDVH() { try { @@ -185,11 +186,25 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { }, ); - const sourcesInJson = (await sourcesResponse.json()) as { - ids: string[]; - }; - console.log(sourcesInJson); + const sourcesInJson = getIdsFromSource(((await sourcesResponse.json()) as { + ids: string[] + }).ids) ?? []; + + + const notesInSources = sourcesInJson.filter( + (urls) => urls.startsWith("https://notes.supermemory.dhr.wtf/") + ) + const nonNotes = sourcesInJson.filter( + i => !notesInSources.includes(i) + ) + + const fetchedTitles = await getMemoriesFromUrl(notesInSources); + + const sources = [ + ...nonNotes.map(n => ({ isNote: false, source: n ?? "<unnamed>" })), + ...fetchedTitles.map(n => ({ isNote: true, source: n.title ?? "<unnamed>" })) + ] setIsAiLoading(false); setChatHistory((prev) => { @@ -200,7 +215,7 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { ...lastMessage, answer: { parts: lastMessage.answer.parts, - sources: getIdsFromSource(sourcesInJson.ids) ?? [], + sources }, }, ]; diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx index 95bb3d22..f6a7224f 100644 --- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx +++ b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx @@ -165,13 +165,12 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { onClick={() => { if (check()) { setLoading(true); - const randomId = Math.floor(Math.random() * 1000000); addMemory( { content, title: name, type: "note", - url: `https://notes.supermemory.dhr.wtf/${randomId}`, + url: `https://notes.supermemory.dhr.wtf/`, image: "", savedAt: new Date(), }, diff --git a/apps/web/src/components/Sidebar/DeleteConfirmation.tsx b/apps/web/src/components/Sidebar/DeleteConfirmation.tsx new file mode 100644 index 00000000..9324b147 --- /dev/null +++ b/apps/web/src/components/Sidebar/DeleteConfirmation.tsx @@ -0,0 +1,35 @@ +import { Dialog, DialogContent, DialogTrigger, DialogTitle, DialogDescription, DialogClose, DialogFooter } from "../ui/dialog"; + +export default function DeleteConfirmation({ onDelete, trigger = true, children }: { trigger?: boolean, onDelete?: () => void; children: React.ReactNode }) { + return ( + <Dialog> + {trigger ? ( + <DialogTrigger asChild> + {children} + </DialogTrigger> + ) : ( + <> + {children} + </> + )} + <DialogContent> + <DialogTitle className="text-xl">Are you sure?</DialogTitle> + <DialogDescription className="text-md"> + You will not be able to recover this it. + </DialogDescription> + <DialogFooter> + <DialogClose + type={undefined} + onClick={onDelete} + className="ml-auto flex items-center justify-center rounded-md text-red-400 bg-red-100/10 px-3 py-2 transition hover:bg-red-100/5 focus-visible:bg-red-100/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-100/30" + > + Delete + </DialogClose> + <DialogClose className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 hover:bg-rgray-4 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition focus-visible:outline-none focus-visible:ring-2"> + Cancel + </DialogClose> + </DialogFooter> + </DialogContent> + </Dialog> + ) +} diff --git a/apps/web/src/components/Sidebar/EditNoteDialog.tsx b/apps/web/src/components/Sidebar/EditNoteDialog.tsx new file mode 100644 index 00000000..b7760656 --- /dev/null +++ b/apps/web/src/components/Sidebar/EditNoteDialog.tsx @@ -0,0 +1,152 @@ + +import { Editor } from "novel"; +import { + DialogClose, + DialogFooter, +} from "../ui/dialog"; +import { Input } from "../ui/input"; +import { Markdown } from "tiptap-markdown"; +import { useEffect, useRef, useState } from "react"; +import { FilterSpaces } from "./FilterCombobox"; +import { useMemory } from "@/contexts/MemoryContext"; +import { Loader, Plus, Trash, X } from "lucide-react"; +import { motion } from "framer-motion"; +import { StoredContent } from "@/server/db/schema"; +import { fetchContent } from "@/actions/db"; +import { isArraysEqual } from "@/lib/utils"; +import DeleteConfirmation from "./DeleteConfirmation"; + + +export function NoteEdit({ memory, closeDialog }: { memory: StoredContent, closeDialog: () => any }) { + const { updateMemory, deleteMemory } = useMemory(); + + const [initialSpaces, setInitialSpaces] = useState<number[]>([]) + const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]); + + const inputRef = useRef<HTMLInputElement>(null); + const [name, setName] = useState(memory.title ?? ""); + const [content, setContent] = useState(memory.content); + const [loading, setLoading] = useState(false); + + function check(): boolean { + const data = { + name: name.trim(), + content, + }; + if (!data.name || data.name.length < 1) { + if (!inputRef.current) { + alert("Please enter a name for the note"); + return false; + } + inputRef.current.value = ""; + inputRef.current.placeholder = "Please enter a title for the note"; + inputRef.current.dataset["error"] = "true"; + setTimeout(() => { + inputRef.current!.placeholder = "Title of the note"; + inputRef.current!.dataset["error"] = "false"; + }, 500); + inputRef.current.focus(); + return false; + } + return true; + } + + useEffect(() => { + fetchContent(memory.id).then((data) => { + if (data?.spaces) { + setInitialSpaces(data.spaces) + setSelectedSpacesId(data.spaces) + } + }) + }, []) + + return ( + <div> + <Input + ref={inputRef} + data-error="false" + className="w-full border-none p-0 text-xl ring-0 placeholder:text-white/30 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400" + placeholder="Title of the note" + value={name} + disabled={loading} + onChange={(e) => setName(e.target.value)} + /> + <Editor + disableLocalStorage + defaultValue={memory.content} + onUpdate={(editor) => { + if (!editor) return; + setContent(editor.storage.markdown.getMarkdown()); + }} + extensions={[Markdown]} + className="novel-editor bg-rgray-4 border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] w-[50vw] overflow-y-auto rounded-lg border [&>div>div]:p-5" + /> + <DialogFooter> + <FilterSpaces + selectedSpaces={selectedSpacesId} + setSelectedSpaces={setSelectedSpacesId} + className="hover:bg-rgray-5 mr-auto bg-white/5" + name={"Spaces"} + /> + <DeleteConfirmation onDelete={() => { + deleteMemory(memory.id) + }}> + <button + type={undefined} + disabled={loading} + className="focus-visible:bg-red-100 focus-visible:text-red-400 dark:focus-visible:bg-red-100/10 hover:bg-red-100 dark:hover:bg-red-100/10 hover:text-red-400 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" + > + <Trash className="w-5 h-5" /> + </button> + </DeleteConfirmation> + <button + onClick={() => { + if (check()) { + setLoading(true); + console.log( + + { + title: name === memory.title ? undefined : name, + content: content === memory.content ? undefined : content, + spaces: isArraysEqual(initialSpaces, selectedSpacesId) ? undefined : selectedSpacesId, + }, + ) + updateMemory( + memory.id, + { + title: name === memory.title ? undefined : name, + content: content === memory.content ? undefined : content, + spaces: isArraysEqual(initialSpaces, selectedSpacesId) ? undefined : selectedSpacesId, + }, + ).then(closeDialog); + } + }} + disabled={loading} + className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" + > + <motion.div + initial={{ x: "-50%", y: "-100%" }} + animate={loading && { y: "-50%", x: "-50%", opacity: 1 }} + className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0" + > + <Loader className="text-rgray-11 h-5 w-5 animate-spin" /> + </motion.div> + <motion.div + initial={{ y: "0%" }} + animate={loading && { opacity: 0, y: "30%" }} + > + Save + </motion.div> + </button> + <DialogClose + type={undefined} + disabled={loading} + className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" + > + Cancel + </DialogClose> + </DialogFooter> + </div> + ); +} + diff --git a/apps/web/src/components/Sidebar/FilterCombobox.tsx b/apps/web/src/components/Sidebar/FilterCombobox.tsx index f93ae710..7625e2b6 100644 --- a/apps/web/src/components/Sidebar/FilterCombobox.tsx +++ b/apps/web/src/components/Sidebar/FilterCombobox.tsx @@ -90,6 +90,7 @@ export function FilterSpaces({ align={align} side={side} className="w-[200px] p-0" + onCloseAutoFocus={e => e.preventDefault()} > <Command filter={(val, search) => @@ -128,7 +129,7 @@ export function FilterSpaces({ className="text-rgray-11" > <SpaceIcon className="mr-2 h-4 w-4" /> - {space.name} + {space.name.length > 10 ? space.name.slice(0, 10) + "..." : space.name} {selectedSpaces.includes(space.id)} <Check data-state-on={selectedSpaces.includes(space.id)} @@ -267,7 +268,7 @@ export function FilterMemories({ } className="mr-2 h-4 w-4" /> - {m.title} + {(m.title && m.title?.length > 14) ? m.title?.slice(0, 14) + "..." : m.title} <Check data-state-on={ selected.find((i) => i.id === m.id) !== undefined diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index 6c640e26..1218407b 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -37,15 +37,15 @@ import { DialogFooter, DialogClose, } from "../ui/dialog"; -import { Label } from "../ui/label"; import useViewport from "@/hooks/useViewport"; import useTouchHold from "@/hooks/useTouchHold"; import { DialogTrigger } from "@radix-ui/react-dialog"; import { AddMemoryPage, NoteAddPage, SpaceAddPage } from "./AddMemoryDialog"; import { ExpandedSpace } from "./ExpandedSpace"; import { StoredContent, StoredSpace } from "@/server/db/schema"; -import Image from "next/image"; import { useDebounce } from "@/hooks/useDebounce"; +import { NoteEdit } from "./EditNoteDialog"; +import DeleteConfirmation from "./DeleteConfirmation"; export function MemoriesBar() { const [parent, enableAnimations] = useAutoAnimate(); @@ -194,39 +194,58 @@ const SpaceExitVariant: Variant = { }, }; -export function MemoryItem({ id, title, image, type }: StoredContent) { +export function MemoryItem(props: StoredContent) { + + const { id, title, image, type } = props + const name = title ? title.length > 10 ? title.slice(0, 10) + "..." : title : "<no title>"; + const [isDialogOpen, setIsDialogOpen] = useState(false); + return ( - <div className="hover:bg-rgray-2 has-[[data-state='true']]: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 select-none 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 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100"> - <button data-space-text className="focus-visible:outline-none"> - {name} - </button> + <Dialog open={type === "note" ? isDialogOpen : false} onOpenChange={setIsDialogOpen}> + <div onClick={() => setIsDialogOpen(true)} className="cursor-pointer hover:bg-rgray-2 has-[[data-state='true']]: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 select-none 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 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100"> + { + type === "note" ? + ( + <DialogTrigger asChild> + <button data-space-text className="focus-visible:outline-none"> + {name} + </button> + </DialogTrigger> + ) : ( + <button data-space-text className="focus-visible:outline-none"> + {name} + </button> + ) + } - <div className="flex h-24 w-24 items-center justify-center"> - {type === "page" ? ( - <img - className="h-16 w-16" - id={id.toString()} - src={image!} - onError={(e) => { - (e.target as HTMLImageElement).src = - "/icons/white_without_bg.png"; - }} - /> - ) : type === "note" ? ( - <div className="bg-rgray-4 flex items-center justify-center rounded-md p-2 shadow-md"> - <Text className="h-10 w-10" /> - </div> - ) : ( - <></> - )} - </div> - </div> + <div className="flex h-24 w-24 items-center justify-center"> + {type === "page" ? ( + <img + className="h-16 w-16" + id={id.toString()} + src={image!} + onError={(e) => { + (e.target as HTMLImageElement).src = + "/icons/white_without_bg.png"; + }} + /> + ) : type === "note" ? ( + <Text className="h-16 w-16" /> + ) : ( + <></> + )} + </div> + </div> + <DialogContent className="w-max max-w-[auto]"> + <NoteEdit closeDialog={() => setIsDialogOpen(false)} memory={props} /> + </DialogContent> + </Dialog> ); } @@ -254,6 +273,9 @@ export function SpaceItem({ }, [cachedMemories]); const _name = name.length > 10 ? name.slice(0, 10) + "..." : name; + + console.log(spaceMemories) + return ( <motion.div ref={itemRef} @@ -396,7 +418,7 @@ export function SpaceMoreButton({ setIsOpen?: (open: boolean) => void; }) { return ( - <Dialog> + <DeleteConfirmation onDelete={onDelete} trigger={false}> <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> <DropdownMenuTrigger asChild> <button @@ -426,25 +448,7 @@ export function SpaceMoreButton({ </DialogTrigger> </DropdownMenuContent> </DropdownMenu> - <DialogContent> - <DialogTitle className="text-xl">Are you sure?</DialogTitle> - <DialogDescription className="text-md"> - You will not be able to recover this space - </DialogDescription> - <DialogFooter> - <DialogClose - type={undefined} - onClick={onDelete} - className="ml-auto flex items-center justify-center rounded-md bg-red-500/40 px-3 py-2 transition hover:bg-red-500/60 focus-visible:bg-red-500/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500" - > - Delete - </DialogClose> - <DialogClose className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 hover:bg-rgray-4 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition focus-visible:outline-none focus-visible:ring-2"> - Cancel - </DialogClose> - </DialogFooter> - </DialogContent> - </Dialog> + </DeleteConfirmation> ); } diff --git a/apps/web/src/contexts/MemoryContext.tsx b/apps/web/src/contexts/MemoryContext.tsx index e10984bb..d8a147a5 100644 --- a/apps/web/src/contexts/MemoryContext.tsx +++ b/apps/web/src/contexts/MemoryContext.tsx @@ -14,6 +14,7 @@ import { deleteSpace, deleteMemory, fetchFreeMemories, + updateMemory, } from "@/actions/db"; import { User } from "next-auth"; @@ -33,6 +34,7 @@ export const MemoryContext = React.createContext<{ search: typeof searchMemoriesAndSpaces; deleteSpace: typeof deleteSpace; deleteMemory: typeof deleteMemory; + updateMemory: typeof updateMemory; }>({ spaces: [], freeMemories: [], @@ -42,6 +44,7 @@ export const MemoryContext = React.createContext<{ search: async () => [], deleteMemory: (() => {}) as unknown as typeof deleteMemory, deleteSpace: (() => {}) as unknown as typeof deleteSpace, + updateMemory: (() => {}) as unknown as typeof updateMemory, }); export const MemoryProvider: React.FC< @@ -98,7 +101,7 @@ export const MemoryProvider: React.FC< await fetchContentForSpace(addedSpace.id, { offset: 0, limit: 3, - }) + }) ?? [] ).map((m) => ({ ...m, space: addedSpace.id })); setCachedMemories((prev) => [...prev, ...cachedMemories]); @@ -132,6 +135,40 @@ export const MemoryProvider: React.FC< }; }; + const _updateMemory: typeof updateMemory = async (id, _data) => { + const data = await updateMemory(id, _data); + + console.log(data) + + if (data) { + if (!_data.spaces) { + console.log("non spaces", freeMemories.map(i => i.id === data.memory.id ? data.memory : i )) + setCachedMemories(prev => prev.map(i => i.id === data.memory.id ? { ...data.memory, space: i.space } : i )) + setFreeMemories(prev => prev.map(i => i.id === data.memory.id ? data.memory : i )) + return data + } + setCachedMemories(prev => prev.filter(i => i.id !== data.memory.id)) + setFreeMemories(prev => prev.filter(i => i.id !== data.memory.id)) + if (_data.spaces.length > 0) { + console.log('has space') + setCachedMemories( + prev => [ + ...prev, + ..._data.spaces!.map(s => ({ + ...data.memory, + space: s + })) + ] + ) + } else { + console.log('does nto have space') + setFreeMemories(prev => [...prev, data.memory]) + } + } + + return data + } + return ( <MemoryContext.Provider value={{ @@ -143,6 +180,7 @@ export const MemoryProvider: React.FC< cachedMemories, deleteMemory: _deleteMemory, addMemory: _addMemory, + updateMemory: _updateMemory }} > {children} diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts index f50b526d..0fe5bdfd 100644 --- a/apps/web/src/lib/utils.ts +++ b/apps/web/src/lib/utils.ts @@ -87,3 +87,27 @@ export function countLines(textarea: HTMLTextAreaElement): number { export function convertRemToPixels(rem: number) { return rem * parseFloat(getComputedStyle(document.documentElement).fontSize); } + +export function isArraysEqual(a: any[], b: any[]) { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + + let isEqual = true; + + a.forEach(i => { + if (!isEqual) return + isEqual = b.includes(i) + }) + + if (!isEqual) + return isEqual + + b.forEach(i => { + if (!isEqual) return + isEqual = a.includes(i) + }) + + return isEqual + +} |