diff options
| author | yxshv <[email protected]> | 2024-04-14 00:32:04 +0530 |
|---|---|---|
| committer | yxshv <[email protected]> | 2024-04-14 00:32:04 +0530 |
| commit | e0461696fc9732c240f48d5be6e824a1d5fced44 (patch) | |
| tree | 92e3e91099119db07398b1419f37d144c1d78ccc /apps/web/src | |
| parent | fix relaod bug (diff) | |
| parent | fix build fail (diff) | |
| download | supermemory-e0461696fc9732c240f48d5be6e824a1d5fced44.tar.xz supermemory-e0461696fc9732c240f48d5be6e824a1d5fced44.zip | |
Merge branch 'main' of https://github.com/dhravya/supermemory
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/actions/db.ts | 352 | ||||
| -rw-r--r-- | apps/web/src/app/api/spaces/route.ts | 23 | ||||
| -rw-r--r-- | apps/web/src/app/page.tsx | 35 | ||||
| -rw-r--r-- | apps/web/src/app/privacy/page.tsx | 2 | ||||
| -rw-r--r-- | apps/web/src/components/Main.tsx | 5 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/AddMemoryDialog.tsx | 267 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/FilterCombobox.tsx | 151 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/MemoriesBar.tsx | 405 | ||||
| -rw-r--r-- | apps/web/src/components/ui/command.tsx | 13 | ||||
| -rw-r--r-- | apps/web/src/contexts/MemoryContext.tsx | 170 | ||||
| -rw-r--r-- | apps/web/src/hooks/useDebounce.ts | 2 | ||||
| -rw-r--r-- | apps/web/src/server/db/schema.ts | 22 | ||||
| -rw-r--r-- | apps/web/src/server/db/test.ts | 12 | ||||
| -rw-r--r-- | apps/web/src/server/helpers.ts | 36 |
14 files changed, 793 insertions, 702 deletions
diff --git a/apps/web/src/actions/db.ts b/apps/web/src/actions/db.ts index 66fbc830..cd54c1bd 100644 --- a/apps/web/src/actions/db.ts +++ b/apps/web/src/actions/db.ts @@ -6,66 +6,79 @@ import { sessions, StoredContent, storedContent, - StoredSpace, + StoredSpace, users, - space, + space, } from "@/server/db/schema"; import { SearchResult } from "@/contexts/MemoryContext"; import { like, eq, and, sql, exists, asc, notExists } from "drizzle-orm"; -import { union } from "drizzle-orm/sqlite-core" +import { union } from "drizzle-orm/sqlite-core"; // @todo: (future) pagination not yet needed -export async function searchMemoriesAndSpaces(query: string, opts?: { filter?: { memories?: boolean, spaces?: boolean }, range?: { offset: number, limit: number } }): Promise<SearchResult[]> { - - const user = await getUser() - - if (!user) { - return [] - } - - try { - const searchMemoriesQuery = db.select({ - type: sql<string>`'memory'`, - space: sql`NULL`, - memory: storedContent as any - }).from(storedContent).where(and( - eq(storedContent.user, user.id), - like(storedContent.title, `%${query}%`) - )).orderBy(asc(storedContent.savedAt)); - - const searchSpacesQuery = db.select({ - type: sql<string>`'space'`, - space: space as any, - memory: sql`NULL`, - }).from(space).where( - and( - eq(space.user, user.id), - like(space.name, `%${query}%`) - ) - ).orderBy(asc(space.name)); - - let queries = []; - - console.log('adding'); - - [undefined, true].includes(opts?.filter?.memories) && queries.push(searchMemoriesQuery); - [undefined, true].includes(opts?.filter?.spaces) && queries.push(searchSpacesQuery); - - if (opts?.range) { - queries = queries.map(q => q.offset(opts.range!.offset).limit(opts.range!.limit)) - } else { - queries = queries.map(q => q.all()) - } - - const data = await Promise.all(queries) - - console.log('resp', data) - - return data.reduce((acc, i) => [...acc, ...i]) as SearchResult[] - } catch { - return [] - } +export async function searchMemoriesAndSpaces( + query: string, + opts?: { + filter?: { memories?: boolean; spaces?: boolean }; + range?: { offset: number; limit: number }; + }, +): Promise<SearchResult[]> { + const user = await getUser(); + + if (!user) { + return []; + } + try { + const searchMemoriesQuery = db + .select({ + type: sql<string>`'memory'`, + space: sql`NULL`, + memory: storedContent as any, + }) + .from(storedContent) + .where( + and( + eq(storedContent.user, user.id), + like(storedContent.title, `%${query}%`), + ), + ) + .orderBy(asc(storedContent.savedAt)); + + const searchSpacesQuery = db + .select({ + type: sql<string>`'space'`, + space: space as any, + memory: sql`NULL`, + }) + .from(space) + .where(and(eq(space.user, user.id), like(space.name, `%${query}%`))) + .orderBy(asc(space.name)); + + let queries = []; + + console.log("adding"); + + [undefined, true].includes(opts?.filter?.memories) && + queries.push(searchMemoriesQuery); + [undefined, true].includes(opts?.filter?.spaces) && + queries.push(searchSpacesQuery); + + if (opts?.range) { + queries = queries.map((q) => + q.offset(opts.range!.offset).limit(opts.range!.limit), + ); + } else { + queries = queries.map((q) => q.all()); + } + + const data = await Promise.all(queries); + + console.log("resp", data); + + return data.reduce((acc, i) => [...acc, ...i]) as SearchResult[]; + } catch { + return []; + } } async function getUser() { @@ -76,7 +89,7 @@ async function getUser() { headers().get("Authorization")?.replace("Bearer ", ""); if (!token) { - return null + return null; } const session = await db @@ -85,7 +98,7 @@ async function getUser() { .where(eq(sessions.sessionToken, token!)); if (!session || session.length === 0) { - return null + return null; } const [userData] = await db @@ -95,17 +108,17 @@ async function getUser() { .limit(1); if (!userData) { - return null + return null; } - return userData + return userData; } export async function getMemory(title: string) { const user = await getUser(); if (!user) { - return null + return null; } return await db @@ -119,34 +132,39 @@ export async function getMemory(title: string) { ); } - export async function addSpace(name: string, memories: number[]) { + const user = await getUser(); - const user = await getUser(); - - if (!user) { - return null - } - - const [addedSpace] = await db - .insert(space) - .values({ - name: name, - user: user.id - }).returning(); - - const addedMemories = memories.length > 0 ? await db.insert(contentToSpace) - .values(memories.map(m => ({ - contentId: m, - spaceId: addedSpace.id - }))).returning() : [] - - return { - space: addedSpace, - addedMemories - } -} + if (!user) { + return null; + } + const [addedSpace] = await db + .insert(space) + .values({ + name: name, + user: user.id, + }) + .returning(); + + const addedMemories = + memories.length > 0 + ? await db + .insert(contentToSpace) + .values( + memories.map((m) => ({ + contentId: m, + spaceId: addedSpace.id, + })), + ) + .returning() + : []; + + return { + space: addedSpace, + addedMemories, + }; +} export async function fetchContentForSpace( spaceId: number, @@ -155,115 +173,127 @@ export async function fetchContentForSpace( limit: number; }, ) { - const query = db .select() .from(storedContent) .where( exists( - db.select().from(contentToSpace).where(and(eq(contentToSpace.spaceId, spaceId), eq(contentToSpace.contentId, storedContent.id))), + db + .select() + .from(contentToSpace) + .where( + and( + eq(contentToSpace.spaceId, spaceId), + eq(contentToSpace.contentId, storedContent.id), + ), + ), ), - ).orderBy(asc(storedContent.savedAt)) + ) + .orderBy(asc(storedContent.savedAt)); - return range ? await query.limit(range.limit).offset(range.offset) : await query.all() + return range + ? await query.limit(range.limit).offset(range.offset) + : await query.all(); } -export async function fetchFreeMemories( - range?: { - offset: number; - limit: number; - } -) { - - const user = await getUser() +export async function fetchFreeMemories(range?: { + offset: number; + limit: number; +}) { + const user = await getUser(); - if (!user) { - return [] - } + if (!user) { + return []; + } - const query = db + 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() + 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(); } -export async function addMemory(content: typeof storedContent.$inferInsert, spaces: number[]) { - - const user = await getUser() - - if (!user) { - return null - } - - const [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() : []; +export async function addMemory( + content: typeof storedContent.$inferInsert, + spaces: number[], +) { + const user = await getUser(); - return { - memory: addedMemory, - addedToSpaces - } + if (!user) { + return null; + } + const [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() + : []; + + return { + memory: addedMemory, + addedToSpaces, + }; } export async function deleteSpace(id: number) { + const user = await getUser(); - const user = await getUser() - - if (!user) { - return null - } - - await db.delete(contentToSpace) - .where(eq(contentToSpace.spaceId, id)); - - const [deleted] = await db.delete(space) - .where(and(eq(space.user, user.id), eq(space.id, id))) - .returning(); + if (!user) { + return null; + } + await db.delete(contentToSpace).where(eq(contentToSpace.spaceId, id)); - return deleted + const [deleted] = await db + .delete(space) + .where(and(eq(space.user, user.id), eq(space.id, id))) + .returning(); + return deleted; } - export async function deleteMemory(id: number) { + const user = await getUser(); + if (!user) { + return null; + } - const user = await getUser() - - if (!user) { - return null - } - - await db.delete(contentToSpace) - .where(eq(contentToSpace.contentId, id)); - - const [deleted] = await db.delete(storedContent) - .where(and(eq(storedContent.user, user.id), eq(storedContent.id, id))) - .returning(); + await db.delete(contentToSpace).where(eq(contentToSpace.contentId, id)); - return deleted + const [deleted] = await db + .delete(storedContent) + .where(and(eq(storedContent.user, user.id), eq(storedContent.id, id))) + .returning(); + return deleted; } diff --git a/apps/web/src/app/api/spaces/route.ts b/apps/web/src/app/api/spaces/route.ts index 3fe95870..d2685e9f 100644 --- a/apps/web/src/app/api/spaces/route.ts +++ b/apps/web/src/app/api/spaces/route.ts @@ -3,13 +3,7 @@ import { sessions, space, users } from "@/server/db/schema"; import { eq } from "drizzle-orm"; import { NextRequest, NextResponse } from "next/server"; -<<<<<<< HEAD -export const runtime = "edge" - -export async function GET(req: NextRequest) { -======= export const runtime = "edge"; ->>>>>>> 7648bdaa8cbe42a90f05865f8c555c9a3911af9b export async function GET(req: NextRequest) { const token = @@ -40,7 +34,6 @@ export async function GET(req: NextRequest) { .from(sessions) .where(eq(sessions.sessionToken, token!)); - if (!sessionData || sessionData.length === 0) { return new Response( JSON.stringify({ message: "Invalid Key, session not found." }), @@ -63,27 +56,11 @@ export async function GET(req: NextRequest) { const user = userData[0]; -<<<<<<< HEAD - const spaces = await db - .select() - .from(space) - .where(eq(space.user, user.id)) - .all(); - - - console.log('data', spaces) - - return NextResponse.json({ - message: "OK", - data: spaces - }, { status: 200 }) -======= const spaces = await db .select() .from(space) .where(eq(space.user, user.id)) .all(); ->>>>>>> 7648bdaa8cbe42a90f05865f8c555c9a3911af9b return NextResponse.json( { diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 7f3abfee..1cc21adf 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,6 +1,6 @@ import { db } from "@/server/db"; import { - ChachedSpaceContent, + ChachedSpaceContent, contentToSpace, sessions, space, @@ -57,37 +57,36 @@ export default async function Home() { .select() .from(space) .where(eq(space.user, userData.id)) - .all(); + .all(); - console.log(collectedSpaces) + console.log(collectedSpaces); // Fetch only first 3 content of each spaces let contents: ChachedSpaceContent[] = []; - //console.log(await db.select().from(storedContent).) - + //console.log(await db.select().from(storedContent).) + await Promise.all([ collectedSpaces.forEach(async (space) => { - console.log("fetching ") - const data = (await fetchContentForSpace(space.id, { - offset: 0, - limit: 3, - })).map(data => ({ - ...data, - space: space.id - })) - contents = [ - ...contents, + console.log("fetching "); + const data = ( + await fetchContentForSpace(space.id, { + offset: 0, + limit: 3, + }) + ).map((data) => ({ ...data, - ]; + space: space.id, + })); + contents = [...contents, ...data]; }), ]); - console.log(contents) + console.log(contents); // freeMemories const freeMemories = await fetchFreeMemories(userData.id); - console.log('free',freeMemories) + console.log("free", freeMemories); return ( <MemoryProvider diff --git a/apps/web/src/app/privacy/page.tsx b/apps/web/src/app/privacy/page.tsx index 8d126dff..5e40cbe9 100644 --- a/apps/web/src/app/privacy/page.tsx +++ b/apps/web/src/app/privacy/page.tsx @@ -2,6 +2,8 @@ import React from "react"; import Markdown from "react-markdown"; import { policy } from "./privacy"; +export const runtime = "edge"; + function Page() { return ( <div> diff --git a/apps/web/src/components/Main.tsx b/apps/web/src/components/Main.tsx index 3c883b67..e6b31de5 100644 --- a/apps/web/src/components/Main.tsx +++ b/apps/web/src/components/Main.tsx @@ -380,7 +380,10 @@ export function Chat({ loading={i === chatHistory.length - 1 ? isLoading : false} sources={msg.answer.sources} > - {msg.answer.parts.map((part) => part.text).join(" ")} + {msg.answer.parts + .map((part) => part.text) + .join("") + .replace("</s>", "")} </ChatAnswer> </ChatMessage> ))} diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx index f21a9683..39f088e3 100644 --- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx +++ b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx @@ -15,7 +15,7 @@ import { useMemory } from "@/contexts/MemoryContext"; import { Loader, Plus, X } from "lucide-react"; import { StoredContent } from "@/server/db/schema"; import { cleanUrl } from "@/lib/utils"; -import { motion } from "framer-motion" +import { motion } from "framer-motion"; import { getMetaData } from "@/server/helpers"; export function AddMemoryPage({ closeDialog }: { closeDialog: () => void }) { @@ -39,29 +39,29 @@ export function AddMemoryPage({ closeDialog }: { closeDialog: () => void }) { placeholder="Enter the URL of the page" type="url" data-modal-autofocus - className="disabled:opacity-70 disabled:cursor-not-allowed bg-rgray-4 mt-2 w-full" + className="bg-rgray-4 mt-2 w-full disabled:cursor-not-allowed disabled:opacity-70" value={url} onChange={(e) => setUrl(e.target.value)} - disabled={loading} + disabled={loading} /> <DialogFooter> <FilterSpaces selectedSpaces={selectedSpacesId} setSelectedSpaces={setSelectedSpacesId} - className="disabled:opacity-70 disabled:cursor-not-allowed hover:bg-rgray-5 mr-auto bg-white/5" + className="hover:bg-rgray-5 mr-auto bg-white/5 disabled:cursor-not-allowed disabled:opacity-70" name={"Spaces"} - disabled={loading} + disabled={loading} /> <button type={"submit"} - disabled={loading} + disabled={loading} onClick={async () => { - setLoading(true) - const metadata = await getMetaData(url) + setLoading(true); + const metadata = await getMetaData(url); await addMemory( { title: metadata.title, - description: metadata.description, + description: metadata.description, content: "", type: "page", url: url, @@ -70,28 +70,28 @@ export function AddMemoryPage({ closeDialog }: { closeDialog: () => void }) { }, selectedSpacesId, ); - closeDialog() + closeDialog(); }} - className="relative disabled:opacity-70 disabled:cursor-not-allowed bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" + 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="opacity-0 absolute top-1/2 left-1/2 translate-y-[-100%] -translate-x-1/2" - > - <Loader className="w-5 h-5 animate-spin text-rgray-11" /> - </motion.div> - <motion.div - initial={{ y: '0%' }} - animate={loading && { opacity: 0, y: '30%' }} - > - Add - </motion.div> + <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%" }} + > + Add + </motion.div> </button> <DialogClose - disabled={loading} - className="disabled:opacity-70 disabled:cursor-not-allowed 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={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> @@ -100,8 +100,7 @@ export function AddMemoryPage({ closeDialog }: { closeDialog: () => void }) { } export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { - - const { addMemory } = useMemory() + const { addMemory } = useMemory(); const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]); @@ -142,7 +141,7 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { placeholder="Title of the note" data-modal-autofocus value={name} - disabled={loading} + disabled={loading} onChange={(e) => setName(e.target.value)} /> <Editor @@ -165,39 +164,41 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { <button onClick={() => { if (check()) { - setLoading(true) - addMemory({ - content, - title: name, - type: "note", - url: "https://notes.supermemory.dhr.wtf/", - image: '', - savedAt: new Date() - }, selectedSpacesId).then(closeDialog) + setLoading(true); + addMemory( + { + content, + title: name, + type: "note", + url: "https://notes.supermemory.dhr.wtf/", + image: "", + savedAt: new Date(), + }, + selectedSpacesId, + ).then(closeDialog); } }} - disabled={loading} - className="relative disabled:opacity-70 disabled:cursor-not-allowed bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" + 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="opacity-0 absolute top-1/2 left-1/2 translate-y-[-100%] -translate-x-1/2" - > - <Loader className="w-5 h-5 animate-spin text-rgray-11" /> - </motion.div> - <motion.div - initial={{ y: '0%' }} - animate={loading && { opacity: 0, y: '30%' }} - > - Add - </motion.div> + <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%" }} + > + Add + </motion.div> </button> <DialogClose type={undefined} - disabled={loading} - className="disabled:opacity-70 disabled:cursor-not-allowed 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={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> @@ -207,16 +208,14 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { } export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { - - const { addSpace } = useMemory() + const { addSpace } = useMemory(); const inputRef = useRef<HTMLInputElement>(null); const [name, setName] = useState(""); const [loading, setLoading] = useState(false); - const [selected, setSelected] = useState<StoredContent[]>([]); - + const [selected, setSelected] = useState<StoredContent[]>([]); function check(): boolean { const data = { @@ -247,65 +246,73 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { </DialogHeader> <Label className="mt-5 block">Name</Label> <Input - ref={inputRef} + ref={inputRef} placeholder="Enter the name of the space" type="url" data-modal-autofocus - value={name} - disabled={loading} - onChange={e => setName(e.target.value)} - className="bg-rgray-4 mt-2 w-full focus-visible:data-[error=true]:ring-red-500/10 data-[error=true]:placeholder:text-red-400 placeholder:transition placeholder:duration-500" + value={name} + disabled={loading} + onChange={(e) => setName(e.target.value)} + className="bg-rgray-4 mt-2 w-full placeholder:transition placeholder:duration-500 data-[error=true]:placeholder:text-red-400 focus-visible:data-[error=true]:ring-red-500/10" /> {selected.length > 0 && ( - <> - <Label className="mt-5 block">Add Memories</Label> - <div className="flex min-h-5 py-2 flex-col justify-center items-center"> - {selected.map(i => ( - <MemorySelectedItem - key={i.id} - onRemove={() => setSelected(prev => prev.filter(p => p.id !== i.id))} - {...i} - /> - ))} - </div> - </> - )} + <> + <Label className="mt-5 block">Add Memories</Label> + <div className="flex min-h-5 flex-col items-center justify-center py-2"> + {selected.map((i) => ( + <MemorySelectedItem + key={i.id} + onRemove={() => + setSelected((prev) => prev.filter((p) => p.id !== i.id)) + } + {...i} + /> + ))} + </div> + </> + )} <DialogFooter> - <FilterMemories - selected={selected} - setSelected={setSelected} - disabled={loading} - className="mr-auto bg-white/5 hover:bg-rgray-4 focus-visible:bg-rgray-4 disabled:opacity-70 disabled:cursor-not-allowed" - > - <Plus className="w-5 h-5" /> - Memory - </FilterMemories> + <FilterMemories + selected={selected} + setSelected={setSelected} + disabled={loading} + className="hover:bg-rgray-4 focus-visible:bg-rgray-4 mr-auto bg-white/5 disabled:cursor-not-allowed disabled:opacity-70" + > + <Plus className="h-5 w-5" /> + Memory + </FilterMemories> <button type={undefined} - onClick={() => { - if (check()) { - setLoading(true) - addSpace(name, selected.map(s => s.id)).then(() => closeDialog()) - } - }} - disabled={loading} - className="relative disabled:opacity-70 disabled:cursor-not-allowed bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" + onClick={() => { + if (check()) { + setLoading(true); + addSpace( + name, + selected.map((s) => s.id), + ).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="opacity-0 absolute top-1/2 left-1/2 translate-y-[-100%] -translate-x-1/2" - > - <Loader className="w-5 h-5 animate-spin text-rgray-11" /> - </motion.div> - <motion.div - initial={{ y: '0%' }} - animate={loading && { opacity: 0, y: '30%' }} - > - Add - </motion.div> + <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%" }} + > + Add + </motion.div> </button> - <DialogClose disabled={loading} className="disabled:opacity-70 disabled:cursor-not-allowed 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"> + <DialogClose + 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> @@ -313,15 +320,33 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { ); } -export function MemorySelectedItem({ id, title, url, type, image, onRemove }: StoredContent & { onRemove: () => void; }) { - return ( - <div className="flex justify-start gap-2 p-1 px-2 w-full items-center text-sm rounded-md hover:bg-rgray-4 focus-within-bg-rgray-4 [&:hover>[data-icon]]:block [&:hover>img]:hidden"> - <img src={type === 'note'? '/note.svg' : image ?? "/icons/logo_without_bg.png"} className="h-5 w-5" /> - <button onClick={onRemove} data-icon className="w-5 h-5 p-0 m-0 hidden focus-visible:outline-none"> - <X className="w-5 h-5 scale-90" /> - </button> - <span>{title}</span> - <span className="ml-auto block opacity-50">{type ==='note' ? 'Note' : cleanUrl(url)}</span> - </div> - ) +export function MemorySelectedItem({ + id, + title, + url, + type, + image, + onRemove, +}: StoredContent & { onRemove: () => void }) { + return ( + <div className="hover:bg-rgray-4 focus-within-bg-rgray-4 flex w-full items-center justify-start gap-2 rounded-md p-1 px-2 text-sm [&:hover>[data-icon]]:block [&:hover>img]:hidden"> + <img + src={ + type === "note" ? "/note.svg" : image ?? "/icons/logo_without_bg.png" + } + className="h-5 w-5" + /> + <button + onClick={onRemove} + data-icon + className="m-0 hidden h-5 w-5 p-0 focus-visible:outline-none" + > + <X className="h-5 w-5 scale-90" /> + </button> + <span>{title}</span> + <span className="ml-auto block opacity-50"> + {type === "note" ? "Note" : cleanUrl(url)} + </span> + </div> + ); } diff --git a/apps/web/src/components/Sidebar/FilterCombobox.tsx b/apps/web/src/components/Sidebar/FilterCombobox.tsx index de2d5fe8..f93ae710 100644 --- a/apps/web/src/components/Sidebar/FilterCombobox.tsx +++ b/apps/web/src/components/Sidebar/FilterCombobox.tsx @@ -24,7 +24,8 @@ import { SearchResult, useMemory } from "@/contexts/MemoryContext"; import { useDebounce } from "@/hooks/useDebounce"; import { StoredContent } from "@/server/db/schema"; -export interface FilterSpacesProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { +export interface FilterSpacesProps + extends React.ButtonHTMLAttributes<HTMLButtonElement> { side?: "top" | "bottom"; align?: "end" | "start" | "center"; onClose?: () => void; @@ -153,7 +154,7 @@ export type FilterMemoriesProps = { onClose?: () => void; selected: StoredContent[]; setSelected: React.Dispatch<React.SetStateAction<StoredContent[]>>; -} & React.ButtonHTMLAttributes<HTMLButtonElement> +} & React.ButtonHTMLAttributes<HTMLButtonElement>; export function FilterMemories({ className, @@ -164,40 +165,39 @@ export function FilterMemories({ setSelected, ...props }: FilterMemoriesProps) { - - const { search } = useMemory(); + const { search } = useMemory(); const [open, setOpen] = React.useState(false); - const [searchQuery, setSearchQuery] = React.useState(""); - const query = useDebounce(searchQuery, 500) + const [searchQuery, setSearchQuery] = React.useState(""); + const query = useDebounce(searchQuery, 500); - const [searchResults, setSearchResults] = React.useState<SearchResult[]>([]); - const [isSearching, setIsSearching] = React.useState(false) + const [searchResults, setSearchResults] = React.useState<SearchResult[]>([]); + const [isSearching, setIsSearching] = React.useState(false); - const results = React.useMemo(() => { - return searchResults.map(r => r.memory) - }, [searchResults]) + const results = React.useMemo(() => { + return searchResults.map((r) => r.memory); + }, [searchResults]); - console.log('memoized', results) + console.log("memoized", results); - React.useEffect(() => { - const q = query.trim() - if (q.length > 0) { - setIsSearching(true); - (async () => { - const results = await search(q, { - filter: { - memories: true, - spaces: false - } - }) - setSearchResults(results) - setIsSearching(false) - })(); - } else { - setSearchResults([]) - } - }, [query]) + React.useEffect(() => { + const q = query.trim(); + if (q.length > 0) { + setIsSearching(true); + (async () => { + const results = await search(q, { + filter: { + memories: true, + spaces: false, + }, + }); + setSearchResults(results); + setIsSearching(false); + })(); + } else { + setSearchResults([]); + } + }, [query]); React.useEffect(() => { if (!open) { @@ -205,7 +205,7 @@ export function FilterMemories({ } }, [open]); - console.log(searchResults); + console.log(searchResults); return ( <AnimatePresence mode="popLayout"> <LayoutGroup> @@ -220,7 +220,7 @@ export function FilterMemories({ )} {...props} > - {props.children} + {props.children} </button> </PopoverTrigger> <PopoverContent @@ -229,43 +229,58 @@ export function FilterMemories({ side={side} className="w-[200px] p-0" > - <Command - shouldFilter={false} - > - <CommandInput isSearching={isSearching} value={searchQuery} onValueChange={setSearchQuery} placeholder="Filter memories..." /> + <Command shouldFilter={false}> + <CommandInput + isSearching={isSearching} + value={searchQuery} + onValueChange={setSearchQuery} + placeholder="Filter memories..." + /> <CommandList> - <CommandGroup> - <CommandEmpty className="text-rgray-11 text-sm text-center py-5">{isSearching ? "Searching..." : query.trim().length > 0 ? "Nothing Found" : "Search something"}</CommandEmpty> - {results.map((m) => ( - <CommandItem - key={m.id} - value={m.id.toString()} - onSelect={(val) => { - setSelected((prev) => - prev.find(p => p.id === parseInt(val)) - ? prev.filter((v) => v.id !== parseInt(val)) - : [...prev, m], - ); - }} - asChild - > - <div - className="text-rgray-11" - > - <img src={m.type === 'note' ? '/note.svg' : m.image ?? "/icons/logo_without_bg.png"} className="mr-2 h-4 w-4" /> - {m.title} - <Check - data-state-on={selected.find(i => i.id === m.id) !== undefined} - className={cn( - "on:opacity-100 ml-auto h-4 w-4 opacity-0", - )} - /> - </div> - </CommandItem> - ))} - - </CommandGroup> - </CommandList> + <CommandGroup> + <CommandEmpty className="text-rgray-11 py-5 text-center text-sm"> + {isSearching + ? "Searching..." + : query.trim().length > 0 + ? "Nothing Found" + : "Search something"} + </CommandEmpty> + {results.map((m) => ( + <CommandItem + key={m.id} + value={m.id.toString()} + onSelect={(val) => { + setSelected((prev) => + prev.find((p) => p.id === parseInt(val)) + ? prev.filter((v) => v.id !== parseInt(val)) + : [...prev, m], + ); + }} + asChild + > + <div className="text-rgray-11"> + <img + src={ + m.type === "note" + ? "/note.svg" + : m.image ?? "/icons/logo_without_bg.png" + } + className="mr-2 h-4 w-4" + /> + {m.title} + <Check + data-state-on={ + selected.find((i) => i.id === m.id) !== undefined + } + className={cn( + "on:opacity-100 ml-auto h-4 w-4 opacity-0", + )} + /> + </div> + </CommandItem> + ))} + </CommandGroup> + </CommandList> </Command> </PopoverContent> </Popover> diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index f474262a..6c640e26 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -44,7 +44,7 @@ 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 Image from "next/image"; import { useDebounce } from "@/hooks/useDebounce"; export function MemoriesBar() { @@ -57,11 +57,11 @@ export function MemoriesBar() { >(null); const [expandedSpace, setExpandedSpace] = useState<number | null>(null); - const [searchQuery, setSearcyQuery] = useState(""); - const [searchLoading, setSearchLoading] = useState(false) - const query = useDebounce(searchQuery, 500) + const [searchQuery, setSearcyQuery] = useState(""); + const [searchLoading, setSearchLoading] = useState(false); + const query = useDebounce(searchQuery, 500); - const [searchResults, setSearchResults] = useState<SearchResult[]>([]) + const [searchResults, setSearchResults] = useState<SearchResult[]>([]); if (expandedSpace) { return ( @@ -72,20 +72,20 @@ export function MemoriesBar() { ); } - useEffect(() => { - const q = query.trim() - if (q.length < 1) { - setSearchResults([]) - return - } + useEffect(() => { + const q = query.trim(); + if (q.length < 1) { + setSearchResults([]); + return; + } - setSearchLoading(true); + setSearchLoading(true); - (async () => { - setSearchResults(await search(q)) - setSearchLoading(false) - })(); - }, [query]) + (async () => { + setSearchResults(await search(q)); + setSearchLoading(false); + })(); + }, [query]); return ( <div className="text-rgray-11 flex w-full flex-col items-start py-8 text-left"> @@ -93,10 +93,16 @@ export function MemoriesBar() { <h1 className="w-full text-2xl">Your Memories</h1> <InputWithIcon placeholder="Search" - icon={searchLoading ? <Loader className="text-rgray-11 h-5 w-5 opacity-50 animate-spin" /> : <Search className="text-rgray-11 h-5 w-5 opacity-50" />} + icon={ + searchLoading ? ( + <Loader className="text-rgray-11 h-5 w-5 animate-spin opacity-50" /> + ) : ( + <Search className="text-rgray-11 h-5 w-5 opacity-50" /> + ) + } className="bg-rgray-4 mt-2 w-full" - value={searchQuery} - onChange={(e) => setSearcyQuery(e.target.value)} + value={searchQuery} + onChange={(e) => setSearcyQuery(e.target.value)} /> </div> <div className="mt-2 flex w-full px-8"> @@ -147,34 +153,32 @@ export function MemoriesBar() { ref={parent} className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5" > - {query.trim().length > 0 ? ( - <> - {searchResults.map(({ type, space, memory }, i) => ( - <> - {type === "memory" && ( - <MemoryItem {...memory!} key={i} /> - )} - {type === "space" && ( - <SpaceItem {...space!} key={i} onDelete={() => {}} /> - )} - </> - ))} - </> - ): ( - <> - {spaces.map((space) => ( - <SpaceItem - onDelete={() => deleteSpace(space.id)} - key={space.id} - //onClick={() => setExpandedSpace(space.id)} - {...space} - /> - ))} - {freeMemories.map(m => ( - <MemoryItem {...m} key={m.id} /> - ))} - </> - )} + {query.trim().length > 0 ? ( + <> + {searchResults.map(({ type, space, memory }, i) => ( + <> + {type === "memory" && <MemoryItem {...memory!} key={i} />} + {type === "space" && ( + <SpaceItem {...space!} key={i} onDelete={() => {}} /> + )} + </> + ))} + </> + ) : ( + <> + {spaces.map((space) => ( + <SpaceItem + onDelete={() => deleteSpace(space.id)} + key={space.id} + //onClick={() => setExpandedSpace(space.id)} + {...space} + /> + ))} + {freeMemories.map((m) => ( + <MemoryItem {...m} key={m.id} /> + ))} + </> + )} </div> </div> ); @@ -190,44 +194,40 @@ const SpaceExitVariant: Variant = { }, }; -export function MemoryItem({ - id, - title, - image, - type -}: StoredContent) { +export function MemoryItem({ id, title, image, type }: StoredContent) { + const name = title + ? title.length > 10 + ? title.slice(0, 10) + "..." + : title + : "<no title>"; - const name = title ? title.length > 10 ? title.slice(0, 10) + "..." : title : '<no title>'; - - 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"> + 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> - - <div className="w-24 h-24 flex justify-center items-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="shadow-md rounded-md bg-rgray-4 p-2 flex justify-center items-center"> - <Text className="w-10 h-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" ? ( + <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> + ); } export function SpaceItem({ @@ -236,8 +236,7 @@ export function SpaceItem({ onDelete, onClick, }: StoredSpace & { onDelete: () => void; onClick?: () => void }) { - - const { cachedMemories } = useMemory(); + const { cachedMemories } = useMemory(); const [itemRef, animateItem] = useAnimate(); const { width } = useViewport(); @@ -250,11 +249,11 @@ export function SpaceItem({ }, }); - const spaceMemories = useMemo(() => { - return cachedMemories.filter(m => m.space === id) - }, [cachedMemories]) + const spaceMemories = useMemo(() => { + return cachedMemories.filter((m) => m.space === id); + }, [cachedMemories]); - const _name = name.length > 10 ? name.slice(0, 10) + "..." : name + const _name = name.length > 10 ? name.slice(0, 10) + "..." : name; return ( <motion.div ref={itemRef} @@ -269,110 +268,120 @@ export function SpaceItem({ isOpen={moreDropdownOpen} setIsOpen={setMoreDropdownOpen} onDelete={() => { - onDelete() - return; + onDelete(); + return; if (!itemRef.current || width < 768) { onDelete(); return; } - // const trash = document.querySelector("#trash")! as HTMLDivElement; - // const trashBin = document.querySelector("#trash-button")!; - // const trashRect = trashBin.getBoundingClientRect(); - // const scopeRect = itemRef.current.getBoundingClientRect(); - // const el = document.createElement("div"); - // el.style.position = "fixed"; - // el.style.top = "0"; - // el.style.left = "0"; - // el.style.width = "15px"; - // el.style.height = "15px"; - // el.style.backgroundColor = "var(--gray-7)"; - // el.style.zIndex = "60"; - // el.style.borderRadius = "50%"; - // el.style.transform = "scale(5)"; - // el.style.opacity = "0"; - // trash.dataset["open"] = "true"; - // const initial = { - // x: scopeRect.left + scopeRect.width / 2, - // y: scopeRect.top + scopeRect.height / 2, - // }; - // const delta = { - // x: - // trashRect.left + - // trashRect.width / 2 - - // scopeRect.left + - // scopeRect.width / 2, - // y: - // trashRect.top + - // trashRect.height / 4 - - // scopeRect.top + - // scopeRect.height / 2, - // }; - // const end = { - // x: trashRect.left + trashRect.width / 2, - // y: trashRect.top + trashRect.height / 4, - // }; - // el.style.offsetPath = `path('M ${initial.x} ${initial.y} Q ${delta.x * 0.01} ${delta.y * 0.01} ${end.x} ${end.y}`; - // animateItem(itemRef.current, SpaceExitVariant, { - // duration: 0.2, - // }).then(() => { - // itemRef.current.style.scale = "0"; - // onDelete(); - // }); - // document.body.appendChild(el); - // el.animate( - // { - // transform: ["scale(5)", "scale(1)"], - // opacity: [0, 0.3, 1], - // }, - // { - // duration: 200, - // easing: "cubic-bezier(0.64, 0.57, 0.67, 1.53)", - // fill: "forwards", - // }, - // ); - // el.animate( - // { - // offsetDistance: ["0%", "100%"], - // }, - // { - // duration: 2000, - // easing: "cubic-bezier(0.64, 0.57, 0.67, 1.53)", - // fill: "forwards", - // delay: 200, - // }, - // ).onfinish = () => { - // el.animate( - // { transform: "scale(0)", opacity: 0 }, - // { duration: 200, fill: "forwards" }, - // ).onfinish = () => { - // el.remove(); - // }; - // }; + // const trash = document.querySelector("#trash")! as HTMLDivElement; + // const trashBin = document.querySelector("#trash-button")!; + // const trashRect = trashBin.getBoundingClientRect(); + // const scopeRect = itemRef.current.getBoundingClientRect(); + // const el = document.createElement("div"); + // el.style.position = "fixed"; + // el.style.top = "0"; + // el.style.left = "0"; + // el.style.width = "15px"; + // el.style.height = "15px"; + // el.style.backgroundColor = "var(--gray-7)"; + // el.style.zIndex = "60"; + // el.style.borderRadius = "50%"; + // el.style.transform = "scale(5)"; + // el.style.opacity = "0"; + // trash.dataset["open"] = "true"; + // const initial = { + // x: scopeRect.left + scopeRect.width / 2, + // y: scopeRect.top + scopeRect.height / 2, + // }; + // const delta = { + // x: + // trashRect.left + + // trashRect.width / 2 - + // scopeRect.left + + // scopeRect.width / 2, + // y: + // trashRect.top + + // trashRect.height / 4 - + // scopeRect.top + + // scopeRect.height / 2, + // }; + // const end = { + // x: trashRect.left + trashRect.width / 2, + // y: trashRect.top + trashRect.height / 4, + // }; + // el.style.offsetPath = `path('M ${initial.x} ${initial.y} Q ${delta.x * 0.01} ${delta.y * 0.01} ${end.x} ${end.y}`; + // animateItem(itemRef.current, SpaceExitVariant, { + // duration: 0.2, + // }).then(() => { + // itemRef.current.style.scale = "0"; + // onDelete(); + // }); + // document.body.appendChild(el); + // el.animate( + // { + // transform: ["scale(5)", "scale(1)"], + // opacity: [0, 0.3, 1], + // }, + // { + // duration: 200, + // easing: "cubic-bezier(0.64, 0.57, 0.67, 1.53)", + // fill: "forwards", + // }, + // ); + // el.animate( + // { + // offsetDistance: ["0%", "100%"], + // }, + // { + // duration: 2000, + // easing: "cubic-bezier(0.64, 0.57, 0.67, 1.53)", + // fill: "forwards", + // delay: 200, + // }, + // ).onfinish = () => { + // el.animate( + // { transform: "scale(0)", opacity: 0 }, + // { duration: 200, fill: "forwards" }, + // ).onfinish = () => { + // el.remove(); + // }; + // }; }} /> {spaceMemories.length > 2 ? ( <MemoryWithImages3 className="h-24 w-24" id={id.toString()} - images={spaceMemories.map((c) => c.type === 'note' ? '/note.svg' : c.image).reverse() as string[]} + images={ + spaceMemories + .map((c) => (c.type === "note" ? "/note.svg" : c.image)) + .reverse() as string[] + } /> ) : spaceMemories.length > 1 ? ( - <MemoryWithImages2 + <MemoryWithImages2 className="h-24 w-24" id={id.toString()} - images={spaceMemories.map((c) => c.type === 'note' ? '/note.svg' : c.image).reverse() as string[]} + images={ + spaceMemories + .map((c) => (c.type === "note" ? "/note.svg" : c.image)) + .reverse() as string[] + } /> - ) : spaceMemories.length === 1 ? ( + ) : spaceMemories.length === 1 ? ( <MemoryWithImage className="h-24 w-24" id={id.toString()} - image={spaceMemories[0].type === 'note' ? '/note.svg' : spaceMemories[0].image!} + image={ + spaceMemories[0].type === "note" + ? "/note.svg" + : spaceMemories[0].image! + } /> ) : ( - <div className="bg-rgray-4 opacity-30 rounded-full w-24 h-24 scale-50 shadow-"> - - </div> - )} + <div className="bg-rgray-4 shadow- h-24 w-24 scale-50 rounded-full opacity-30"></div> + )} </motion.div> ); } @@ -409,34 +418,32 @@ export function SpaceMoreButton({ <Edit3 className="mr-2 h-4 w-4" strokeWidth={1.5} /> Edit </DropdownMenuItem> - <DialogTrigger asChild> - <DropdownMenuItem - className="focus:bg-red-100 focus:text-red-400 dark:focus:bg-red-100/10" - > - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> + <DialogTrigger asChild> + <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400 dark:focus:bg-red-100/10"> + <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> + Delete + </DropdownMenuItem> + </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="bg-red-500/40 focus-visible:bg-red-500/60 focus-visible:ring-red-500 hover:bg-red-500/60 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition focus-visible:outline-none focus-visible:ring-2" - > - 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> + <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> ); } diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx index f3534b55..afc2cf46 100644 --- a/apps/web/src/components/ui/command.tsx +++ b/apps/web/src/components/ui/command.tsx @@ -7,7 +7,6 @@ import { Loader, Search } from "lucide-react"; import { cn } from "@/lib/utils"; import { Dialog, DialogContent } from "@/components/ui/dialog"; -import { isSea } from "node:sea"; const Command = React.forwardRef< React.ElementRef<typeof CommandPrimitive>, @@ -40,13 +39,19 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => { const CommandInput = React.forwardRef< React.ElementRef<typeof CommandPrimitive.Input>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> & { isSearching?: boolean } ->(({ className, isSearching = false ,...props }, ref) => ( + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> & { + isSearching?: boolean; + } +>(({ className, isSearching = false, ...props }, ref) => ( <div className="border-rgray-6 flex items-center border-b px-3" cmdk-input-wrapper="" > - {isSearching ? <Loader className="mr-2 h-4 w-4 shrink-9 opacity-50 animate-spin" /> : <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />} + {isSearching ? ( + <Loader className="shrink-9 mr-2 h-4 w-4 animate-spin opacity-50" /> + ) : ( + <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> + )} <CommandPrimitive.Input ref={ref} className={cn( diff --git a/apps/web/src/contexts/MemoryContext.tsx b/apps/web/src/contexts/MemoryContext.tsx index 67e6250e..e10984bb 100644 --- a/apps/web/src/contexts/MemoryContext.tsx +++ b/apps/web/src/contexts/MemoryContext.tsx @@ -1,14 +1,27 @@ "use client"; import React, { useCallback } from "react"; -import { ChachedSpaceContent, StoredContent, storedContent, StoredSpace } from "@/server/db/schema"; -import { addMemory, searchMemoriesAndSpaces, addSpace, fetchContentForSpace, deleteSpace, deleteMemory, fetchFreeMemories } from "@/actions/db"; +import { + ChachedSpaceContent, + StoredContent, + storedContent, + StoredSpace, +} from "@/server/db/schema"; +import { + addMemory, + searchMemoriesAndSpaces, + addSpace, + fetchContentForSpace, + deleteSpace, + deleteMemory, + fetchFreeMemories, +} from "@/actions/db"; import { User } from "next-auth"; export type SearchResult = { - type: "memory" | "space", - space: StoredSpace, - memory: StoredContent -} + type: "memory" | "space"; + space: StoredSpace; + memory: StoredContent; +}; // temperory (will change) export const MemoryContext = React.createContext<{ @@ -17,107 +30,118 @@ export const MemoryContext = React.createContext<{ addSpace: typeof addSpace; addMemory: typeof addMemory; cachedMemories: ChachedSpaceContent[]; - search: typeof searchMemoriesAndSpaces; - deleteSpace: typeof deleteSpace; - deleteMemory: typeof deleteMemory; + search: typeof searchMemoriesAndSpaces; + deleteSpace: typeof deleteSpace; + deleteMemory: typeof deleteMemory; }>({ spaces: [], freeMemories: [], - addMemory: (() => {}) as unknown as (typeof addMemory), - addSpace: (async () => {}) as unknown as (typeof addSpace), + addMemory: (() => {}) as unknown as typeof addMemory, + addSpace: (async () => {}) as unknown as typeof addSpace, cachedMemories: [], - search: async () => [], - deleteMemory: (() => {}) as unknown as (typeof deleteMemory), - deleteSpace: (() => {}) as unknown as (typeof deleteSpace) + search: async () => [], + deleteMemory: (() => {}) as unknown as typeof deleteMemory, + deleteSpace: (() => {}) as unknown as typeof deleteSpace, }); export const MemoryProvider: React.FC< { spaces: StoredSpace[]; freeMemories: StoredContent[]; - cachedMemories: ChachedSpaceContent[]; - user: User; + cachedMemories: ChachedSpaceContent[]; + user: User; } & React.PropsWithChildren -> = ({ children, user, spaces: initalSpaces, freeMemories: initialFreeMemories, cachedMemories: initialCachedMemories }) => { - +> = ({ + children, + user, + spaces: initalSpaces, + freeMemories: initialFreeMemories, + cachedMemories: initialCachedMemories, +}) => { const [spaces, setSpaces] = React.useState<StoredSpace[]>(initalSpaces); const [freeMemories, setFreeMemories] = React.useState<StoredContent[]>(initialFreeMemories); - const [cachedMemories, setCachedMemories] = React.useState<ChachedSpaceContent[]>( - initialCachedMemories - ); - - const _deleteSpace: typeof deleteSpace = async (...params) => { - const deleted = (await deleteSpace(...params))! + const [cachedMemories, setCachedMemories] = React.useState< + ChachedSpaceContent[] + >(initialCachedMemories); + + const _deleteSpace: typeof deleteSpace = async (...params) => { + const deleted = (await deleteSpace(...params))!; + + setSpaces((prev) => prev.filter((i) => i.id !== deleted.id)); + setCachedMemories((prev) => prev.filter((i) => i.space !== deleted.id)); - setSpaces(prev => prev.filter(i => i.id !== deleted.id)) - setCachedMemories(prev => prev.filter(i => i.space !== deleted.id)) + setFreeMemories(await fetchFreeMemories()); - setFreeMemories(await fetchFreeMemories()) + return deleted; + }; - return deleted - } + const _deleteMemory: typeof deleteMemory = async (...params) => { + const deleted = (await deleteMemory(...params))!; - const _deleteMemory: typeof deleteMemory = async (...params) => { - const deleted = (await deleteMemory(...params))! + setCachedMemories((prev) => prev.filter((i) => i.id !== deleted.id)); + setFreeMemories(await fetchFreeMemories()); - setCachedMemories(prev => prev.filter(i => i.id !== deleted.id)) - setFreeMemories(await fetchFreeMemories()) + return deleted; + }; - return deleted - } - // const fetchMemories = useCallback(async (query: string) => { // const response = await fetch(`/api/memories?${query}`); // }, []); - - const _addSpace: typeof addSpace = async (...params) => { - const { space: addedSpace, addedMemories } = (await addSpace(...params))!; - - setSpaces(prev => [...prev, addedSpace]) - const cachedMemories = (await fetchContentForSpace(addedSpace.id, { - offset: 0, - limit: 3 - })).map(m => ({ ...m, space: addedSpace.id })) - - setCachedMemories(prev => [...prev, ...cachedMemories]) - - setFreeMemories(await fetchFreeMemories()) - - return { - space: addedSpace, addedMemories - } - } - - const _addMemory: typeof addMemory = async (...params) => { - const { memory: addedMemory, addedToSpaces } = (await addMemory(...params))!; - - addedToSpaces.length > 0 ? setCachedMemories(prev => [ - ...prev, - ...addedToSpaces.map(s => ({ - ...addedMemory, - space: s.spaceId - })) - ]) : setFreeMemories(prev => [...prev, addedMemory]) - - return { - memory: addedMemory, - addedToSpaces - } - } + const _addSpace: typeof addSpace = async (...params) => { + const { space: addedSpace, addedMemories } = (await addSpace(...params))!; + + setSpaces((prev) => [...prev, addedSpace]); + const cachedMemories = ( + await fetchContentForSpace(addedSpace.id, { + offset: 0, + limit: 3, + }) + ).map((m) => ({ ...m, space: addedSpace.id })); + + setCachedMemories((prev) => [...prev, ...cachedMemories]); + + setFreeMemories(await fetchFreeMemories()); + + return { + space: addedSpace, + addedMemories, + }; + }; + + const _addMemory: typeof addMemory = async (...params) => { + const { memory: addedMemory, addedToSpaces } = (await addMemory( + ...params, + ))!; + + addedToSpaces.length > 0 + ? setCachedMemories((prev) => [ + ...prev, + ...addedToSpaces.map((s) => ({ + ...addedMemory, + space: s.spaceId, + })), + ]) + : setFreeMemories((prev) => [...prev, addedMemory]); + + return { + memory: addedMemory, + addedToSpaces, + }; + }; return ( <MemoryContext.Provider value={{ - search: searchMemoriesAndSpaces, + search: searchMemoriesAndSpaces, spaces, addSpace: _addSpace, deleteSpace: _deleteSpace, freeMemories, cachedMemories, - deleteMemory: _deleteMemory, + deleteMemory: _deleteMemory, addMemory: _addMemory, }} > diff --git a/apps/web/src/hooks/useDebounce.ts b/apps/web/src/hooks/useDebounce.ts index 4a47de72..d133b1ae 100644 --- a/apps/web/src/hooks/useDebounce.ts +++ b/apps/web/src/hooks/useDebounce.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState } from "react"; /** * Use this hook when you need to debounce a value. diff --git a/apps/web/src/server/db/schema.ts b/apps/web/src/server/db/schema.ts index f3eafb94..cd2756f1 100644 --- a/apps/web/src/server/db/schema.ts +++ b/apps/web/src/server/db/schema.ts @@ -21,7 +21,7 @@ export const users = createTable("user", { image: text("image", { length: 255 }), }); -export type User = typeof users.$inferSelect +export type User = typeof users.$inferSelect; export const usersRelations = relations(users, ({ many }) => ({ accounts: many(accounts), @@ -34,7 +34,7 @@ export const accounts = createTable( id: integer("id").notNull().primaryKey({ autoIncrement: true }), userId: text("userId", { length: 255 }) .notNull() - .references(() => users.id, { onDelete: 'cascade' }), + .references(() => users.id, { onDelete: "cascade" }), type: text("type", { length: 255 }).notNull(), provider: text("provider", { length: 255 }).notNull(), providerAccountId: text("providerAccountId", { length: 255 }).notNull(), @@ -60,7 +60,7 @@ export const sessions = createTable( sessionToken: text("sessionToken", { length: 255 }).notNull(), userId: text("userId", { length: 255 }) .notNull() - .references(() => users.id, { onDelete: 'cascade' }), + .references(() => users.id, { onDelete: "cascade" }), expires: int("expires", { mode: "timestamp" }).notNull(), }, (session) => ({ @@ -94,7 +94,9 @@ export const storedContent = createTable( "page", ), image: text("image", { length: 255 }), - user: text("user", { length: 255 }).references(() => users.id, { onDelete: 'cascade' }), + user: text("user", { length: 255 }).references(() => users.id, { + onDelete: "cascade", + }), }, (sc) => ({ urlIdx: index("storedContent_url_idx").on(sc.url), @@ -109,10 +111,10 @@ export const contentToSpace = createTable( { contentId: integer("contentId") .notNull() - .references(() => storedContent.id, { onDelete: 'cascade' }), + .references(() => storedContent.id, { onDelete: "cascade" }), spaceId: integer("spaceId") .notNull() - .references(() => space.id, { onDelete: 'cascade' }), + .references(() => space.id, { onDelete: "cascade" }), }, (cts) => ({ compoundKey: primaryKey({ columns: [cts.contentId, cts.spaceId] }), @@ -124,7 +126,9 @@ export const space = createTable( { id: integer("id").notNull().primaryKey({ autoIncrement: true }), name: text("name").notNull().unique().default("none"), - user: text("user", { length: 255 }).references(() => users.id, { onDelete: 'cascade' }), + user: text("user", { length: 255 }).references(() => users.id, { + onDelete: "cascade", + }), }, (space) => ({ nameIdx: index("spaces_name_idx").on(space.name), @@ -135,5 +139,5 @@ export const space = createTable( export type StoredContent = Omit<typeof storedContent.$inferSelect, "user">; export type StoredSpace = typeof space.$inferSelect; export type ChachedSpaceContent = StoredContent & { - space: number; -} + space: number; +}; diff --git a/apps/web/src/server/db/test.ts b/apps/web/src/server/db/test.ts index 9cb8f2b5..37969e5e 100644 --- a/apps/web/src/server/db/test.ts +++ b/apps/web/src/server/db/test.ts @@ -1,10 +1,6 @@ -import { db } from "." -import { space, user } from "./schema" +import { db } from "."; +import { space, user } from "./schema"; -const user = await db.select(user).all() +const user = await db.select(user).all(); -await db.insert(space).values([ - { - - } -]) +await db.insert(space).values([{}]); diff --git a/apps/web/src/server/helpers.ts b/apps/web/src/server/helpers.ts index f1ac078c..9a9a9607 100644 --- a/apps/web/src/server/helpers.ts +++ b/apps/web/src/server/helpers.ts @@ -1,28 +1,32 @@ -'use server'; -import * as cheerio from "cheerio" +"use server"; +import * as cheerio from "cheerio"; export async function getMetaData(url: string) { const response = await fetch(url); const html = await response.text(); - - const $ = cheerio.load(html) + + const $ = cheerio.load(html); // Extract the base URL const baseUrl = new URL(url).origin; // Extract title - const title = $('title').text().trim() - - const description = $('meta[name=description]').attr('content') ?? '' - - const _favicon = $('link[rel=icon]').attr('href') ?? 'https://supermemory.dhr.wtf/web.svg'; - - let favicon = _favicon.trim().length > 0 ? _favicon.trim() : 'https://supermemory.dhr.wtf/web.svg' - if (favicon.startsWith("/")) { - favicon = baseUrl + favicon - } else if (favicon.startsWith("./")) { - favicon = baseUrl + favicon.slice(1) - } + const title = $("title").text().trim(); + + const description = $("meta[name=description]").attr("content") ?? ""; + + const _favicon = + $("link[rel=icon]").attr("href") ?? "https://supermemory.dhr.wtf/web.svg"; + + let favicon = + _favicon.trim().length > 0 + ? _favicon.trim() + : "https://supermemory.dhr.wtf/web.svg"; + if (favicon.startsWith("/")) { + favicon = baseUrl + favicon; + } else if (favicon.startsWith("./")) { + favicon = baseUrl + favicon.slice(1); + } // Prepare the metadata object const metadata = { |