diff options
| author | Yash <[email protected]> | 2024-04-06 05:03:29 +0000 |
|---|---|---|
| committer | Yash <[email protected]> | 2024-04-06 05:03:29 +0000 |
| commit | 30a4ddc0541041cefd210e2463905f710cdc888b (patch) | |
| tree | af599c83c80cc9f3e218f8f49ed6b3266879fa43 /apps/web/src | |
| parent | add modal (diff) | |
| parent | changed data structure to fetch spaces (diff) | |
| download | supermemory-30a4ddc0541041cefd210e2463905f710cdc888b.tar.xz supermemory-30a4ddc0541041cefd210e2463905f710cdc888b.zip | |
Merge branch 'new-ui' of https://github.com/Dhravya/supermemory into new-ui
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/app/MessagePoster.tsx | 11 | ||||
| -rw-r--r-- | apps/web/src/app/api/store/route.ts | 34 | ||||
| -rw-r--r-- | apps/web/src/app/page.tsx | 18 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar.tsx | 6 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/CategoryItem.tsx | 298 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/index.tsx | 5 | ||||
| -rw-r--r-- | apps/web/src/server/db/schema.ts | 20 |
7 files changed, 374 insertions, 18 deletions
diff --git a/apps/web/src/app/MessagePoster.tsx b/apps/web/src/app/MessagePoster.tsx index 3d0bbe7e..64dc89fd 100644 --- a/apps/web/src/app/MessagePoster.tsx +++ b/apps/web/src/app/MessagePoster.tsx @@ -8,7 +8,16 @@ function MessagePoster({ jwt }: { jwt: string }) { window.postMessage({ jwt }, '*'); }, [jwt]); - return null; + return ( + <button + onClick={() => { + if (typeof window === 'undefined') return; + window.postMessage({ jwt }, '*'); + }} + > + Send message + </button> + ); } export default MessagePoster; diff --git a/apps/web/src/app/api/store/route.ts b/apps/web/src/app/api/store/route.ts index 3a4f7e27..06db08b9 100644 --- a/apps/web/src/app/api/store/route.ts +++ b/apps/web/src/app/api/store/route.ts @@ -1,6 +1,6 @@ import { db } from "@/server/db"; -import { eq } from "drizzle-orm"; -import { sessions, storedContent, users } from "@/server/db/schema"; +import { and, eq } from "drizzle-orm"; +import { contentToSpace, sessions, storedContent, users, space } from "@/server/db/schema"; import { type NextRequest, NextResponse } from "next/server"; import { env } from "@/env"; import { getMetaData } from "@/server/helpers"; @@ -31,6 +31,7 @@ export async function POST(req: NextRequest) { const data = await req.json() as { pageContent: string, url: string, + space?: string }; const metadata = await getMetaData(data.url); @@ -38,6 +39,12 @@ export async function POST(req: NextRequest) { let id: number | undefined = undefined; + let storeToSpace = data.space + + if (!storeToSpace) { + storeToSpace = 'all' + } + const storedContentId = await db.insert(storedContent).values({ content: data.pageContent, title: metadata.title, @@ -46,12 +53,33 @@ export async function POST(req: NextRequest) { baseUrl: metadata.baseUrl, image: metadata.image, savedAt: new Date(), - space: "all", user: session.user.id }) id = storedContentId.meta.last_row_id; + if (!id) { + return NextResponse.json({ message: "Error", error: "Error in CF function" }, { status: 500 }); + } + + let spaceID = 0; + + const spaceData = await db.select().from(space).where(and(eq(space.name, storeToSpace), eq(space.user, session.user.id))).limit(1) + spaceID = spaceData[0]?.id + + if (!spaceData || spaceData.length === 0) { + const spaceId = await db.insert(space).values({ + name: storeToSpace, + user: session.user.id + }) + spaceID = spaceId.meta.last_row_id; + } + + await db.insert(contentToSpace).values({ + contentId: id as number, + spaceId: spaceID + }) + const res = await Promise.race([ fetch("https://cf-ai-backend.dhravya.workers.dev/add", { method: "POST", diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index d1d47ae5..221ce2b4 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,5 +1,12 @@ import { db } from '@/server/db'; -import { sessions, storedContent, users } from '@/server/db/schema'; +import { + contentToSpace, + sessions, + space, + StoredContent, + storedContent, + users, +} from '@/server/db/schema'; import { eq, inArray } from 'drizzle-orm'; import { cookies, headers } from 'next/headers'; import { redirect } from 'next/navigation'; @@ -47,12 +54,15 @@ export default async function Home() { return redirect('/api/auth/signin'); } - const posts = await db + // Fetch all content for the user + const contents = await db .select() .from(storedContent) - .where(eq(storedContent.user, userData.id)); + .where(eq(storedContent.user, userData.id)) + .all(); - const collectedSpaces = transformContent(posts); + const collectedSpaces = + contents.length > 0 ? await transformContent(contents) : []; return ( <div className="flex w-screen"> diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 1af37025..66ca1652 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -26,8 +26,7 @@ export default function Sidebar() { description: '', image: 'https://code.visualstudio.com/favicon.ico', baseUrl: 'https://code.visualstudio.com', - savedAt: new Date(), - space: 'Development', + savedAt: new Date() }, { id: 1, @@ -37,8 +36,7 @@ export default function Sidebar() { description: '', image: 'https://github.com/favicon.ico', baseUrl: 'https://github.com', - savedAt: new Date(), - space: 'Development', + savedAt: new Date() }, ]; diff --git a/apps/web/src/components/Sidebar/CategoryItem.tsx b/apps/web/src/components/Sidebar/CategoryItem.tsx new file mode 100644 index 00000000..0cf8a70c --- /dev/null +++ b/apps/web/src/components/Sidebar/CategoryItem.tsx @@ -0,0 +1,298 @@ +'use client'; +import { cleanUrl } from '@/lib/utils'; +import { StoredContent } from '@/server/db/schema'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from '../ui/dropdown-menu'; +import { Label } from '../ui/label'; +import { + ArrowUpRight, + MoreHorizontal, + Tags, + ChevronDown, + Edit3, + Trash2, + Save, + ChevronRight, + Plus, + Minus, +} from 'lucide-react'; +import { useState } from 'react'; +import { + Drawer, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerDescription, + DrawerFooter, + DrawerClose, +} from '../ui/drawer'; +import { Input } from '../ui/input'; +import { Textarea } from '../ui/textarea'; +import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; +import { + AnimatePresence, + motion, + Reorder, + useMotionValue, +} from 'framer-motion'; + +const pages: StoredContent[] = [ + { + id: 1, + content: '', + title: 'Visual Studio Code', + url: 'https://code.visualstudio.com', + description: '', + image: 'https://code.visualstudio.com/favicon.ico', + baseUrl: 'https://code.visualstudio.com', + savedAt: new Date(), + }, + { + id: 2, + content: '', + title: "yxshv/vscode: An unofficial remake of vscode's landing page", + url: 'https://github.com/yxshv/vscode', + description: '', + image: 'https://github.com/favicon.ico', + baseUrl: 'https://github.com', + savedAt: new Date(), + }, + { + id: 3, + content: '', + title: "yxshv/vscode: An unofficial remake of vscode's landing page", + url: 'https://github.com/yxshv/vscode', + description: '', + image: 'https://github.com/favicon.ico', + baseUrl: 'https://github.com', + savedAt: new Date(), + }, + { + id: 4, + content: '', + title: "yxshv/vscode: An unofficial remake of vscode's landing page", + url: 'https://github.com/yxshv/vscode', + description: '', + image: 'https://github.com/favicon.ico', + baseUrl: 'https://github.com', + savedAt: new Date(), + }, + { + id: 5, + content: '', + title: "yxshv/vscode: An unofficial remake of vscode's landing page", + url: 'https://github.com/yxshv/vscode', + description: '', + image: 'https://github.com/favicon.ico', + baseUrl: 'https://github.com', + savedAt: new Date(), + }, + { + id: 6, + content: '', + title: "yxshv/vscode: An unofficial remake of vscode's landing page", + url: 'https://github.com/yxshv/vscode', + description: '', + image: 'https://github.com/favicon.ico', + baseUrl: 'https://github.com', + savedAt: new Date(), + }, + { + id: 7, + content: '', + title: "yxshv/vscode: An unofficial remake of vscode's landing page", + url: 'https://github.com/yxshv/vscode', + description: '', + image: 'https://github.com/favicon.ico', + baseUrl: 'https://github.com', + savedAt: new Date(), + }, + { + id: 8, + content: '', + title: "yxshv/vscode: An unofficial remake of vscode's landing page", + url: 'https://github.com/yxshv/vscode', + description: '', + image: 'https://github.com/favicon.ico', + baseUrl: 'https://github.com', + savedAt: new Date(), + }, + { + id: 9, + content: '', + title: "yxshv/vscode: An unofficial remake of vscode's landing page", + url: 'https://github.com/yxshv/vscode', + description: '', + image: 'https://github.com/favicon.ico', + baseUrl: 'https://github.com', + savedAt: new Date(), + }, +]; +export const CategoryItem: React.FC<{ item: StoredContent }> = ({ item }) => { + const [isExpanded, setIsExpanded] = useState(false); + const [isEditDrawerOpen, setIsEditDrawerOpen] = useState(false); + + const [items, setItems] = useState<StoredContent[]>(pages); + + return ( + <> + <div className="hover:bg-rgray-5 has-[button:focus]:bg-rgray-5 flex w-full items-center rounded-full py-1 pl-3 pr-2 transition [&:hover>button>div>[data-down-icon]]:scale-125 [&:hover>button>div>[data-down-icon]]:opacity-100 [&:hover>button>div>[data-down-icon]]:delay-150 [&:hover>button>div>[data-tags-icon]]:scale-75 [&:hover>button>div>[data-tags-icon]]:opacity-0 [&:hover>button>div>[data-tags-icon]]:delay-0 [&:hover>button]:opacity-100"> + <button + onClick={() => setIsExpanded((prev) => !prev)} + className="flex w-full items-center gap-2 focus-visible:outline-none" + > + <div className="relative h-5 min-w-5"> + <Tags + data-tags-icon + className="z-1 h-5 w-5 transition-[transform,opacity] delay-150 duration-150" + strokeWidth={1.5} + /> + <ChevronDown + data-down-icon + className={`absolute left-1/2 top-1/2 z-[2] h-4 w-4 min-w-4 -translate-x-1/2 -translate-y-1/2 scale-75 opacity-0 transition-[transform,opacity] duration-150 ${isExpanded ? 'rotate-180' : 'rotate-0'}`} + strokeWidth={1.5} + /> + </div> + + <span className="w-full truncate text-nowrap text-left"> + {item.title ?? 'Untitled website'} + </span> + </button> + <Drawer + shouldScaleBackground + open={isEditDrawerOpen} + onOpenChange={setIsEditDrawerOpen} + > + <DrawerContent className="pb-10 lg:px-[25vw]"> + <DrawerHeader className="relative mt-10 px-0"> + <DrawerTitle className=" flex w-full justify-between"> + Edit Page Details + </DrawerTitle> + <DrawerDescription>Change the page details</DrawerDescription> + <a + target="_blank" + href={item.url} + className="text-rgray-11/90 bg-rgray-3 text-md absolute right-0 top-0 flex w-min translate-y-1/2 items-center justify-center gap-1 rounded-full px-5 py-1" + > + <img src={item.image ?? '/brain.png'} className="h-4 w-4" /> + {cleanUrl(item.url)} + </a> + </DrawerHeader> + + <div className="mt-5"> + <Label>Title</Label> + <Input + className="" + required + value={item.title ?? ''} + placeholder={item.title ?? 'Enter the title for the page'} + /> + </div> + <div className="mt-5"> + <Label>Additional Context</Label> + <Textarea + className="" + value={item.content ?? ''} + placeholder={'Enter additional context for this page'} + /> + </div> + <DrawerFooter className="flex flex-row-reverse items-center justify-end px-0 pt-5"> + <DrawerClose className="flex items-center justify-center rounded-md px-3 py-2 ring-2 ring-transparent transition hover:bg-blue-100 hover:text-blue-400 focus-visible:bg-blue-100 focus-visible:text-blue-400 focus-visible:outline-none focus-visible:ring-blue-200 dark:hover:bg-blue-100/10 dark:focus-visible:bg-blue-100/10 dark:focus-visible:ring-blue-200/30"> + <Save className="mr-2 h-4 w-4 " strokeWidth={1.5} /> + Save + </DrawerClose> + <DrawerClose className="hover:bg-rgray-3 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 flex items-center justify-center rounded-md px-3 py-2 ring-2 ring-transparent transition focus-visible:outline-none"> + Cancel + </DrawerClose> + <DrawerClose className="mr-auto flex items-center justify-center rounded-md bg-red-100 px-3 py-2 text-red-400 ring-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-red-200 dark:bg-red-100/10 dark:focus-visible:ring-red-200/30"> + <Trash2 className="mr-2 h-4 w-4 " strokeWidth={1.5} /> + Delete + </DrawerClose> + </DrawerFooter> + </DrawerContent> + </Drawer> + </div> + <AnimatePresence> + {isExpanded && ( + <Reorder.Group + axis="y" + values={items} + onReorder={setItems} + as="div" + initial={{ height: 0 }} + animate={{ height: 'auto' }} + exit={{ + height: 0, + transition: {}, + }} + layoutScroll + className="flex max-h-32 w-full flex-col items-center overflow-y-auto pl-7" + > + <AnimatePresence> + {items.map((item, i) => ( + <CategoryPage + key={item.id} + index={i} + item={item} + onRemove={() => + setItems((prev) => prev.filter((_, index) => i !== index)) + } + /> + ))} + </AnimatePresence> + </Reorder.Group> + )} + </AnimatePresence> + </> + ); +}; + +export const CategoryPage: React.FC<{ + item: StoredContent; + index: number; + onRemove?: () => void; +}> = ({ item, onRemove, index }) => { + return ( + <Reorder.Item + value={item} + as="div" + key={index} + exit={{ opacity: 0, scale: 0.8 }} + dragListener={false} + className="hover:bg-rgray-5 has-[a:focus]:bg-rgray-5 flex w-full items-center rounded-full py-1 pl-3 pr-2 transition [&:hover>a>div>[data-icon]]:scale-125 [&:hover>a>div>[data-icon]]:opacity-100 [&:hover>a>div>[data-icon]]:delay-150 [&:hover>a>div>img]:scale-75 [&:hover>a>div>img]:opacity-0 [&:hover>a>div>img]:delay-0 [&:hover>button]:opacity-100" + > + <a + href={item.url} + target="_blank" + className="flex w-[90%] items-center gap-2 focus-visible:outline-none" + > + <div className="relative h-4 min-w-4"> + <img + src={item.image ?? '/brain.png'} + alt={item.title ?? 'Untitiled website'} + className="z-1 h-4 w-4 transition-[transform,opacity] delay-150 duration-150" + /> + <ArrowUpRight + data-icon + className="absolute left-1/2 top-1/2 z-[2] h-4 w-4 min-w-4 -translate-x-1/2 -translate-y-1/2 scale-75 opacity-0 transition-[transform,opacity] duration-150" + strokeWidth={1.5} + /> + </div> + + <span className="w-full truncate text-nowrap"> + {item.title ?? 'Untitled website'} + </span> + </a> + <button + onClick={() => onRemove?.()} + className="ml-auto w-4 min-w-4 rounded-[0.15rem] opacity-0 focus-visible:opacity-100 focus-visible:outline-none" + > + <Minus className="h-4 w-4 min-w-4" /> + </button> + </Reorder.Item> + ); +}; diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx index b8c1fbb8..52bab0f9 100644 --- a/apps/web/src/components/Sidebar/index.tsx +++ b/apps/web/src/components/Sidebar/index.tsx @@ -28,10 +28,13 @@ const menuItemsBottom: Array<MenuItem> = [ export default function Sidebar({ selectChange, + spaces }: { selectChange?: (selectedItem: string | null) => void; + spaces: CollectedSpaces[]; }) { - const { spaces } = useMemory(); + // TODO: @yxshv, put spaces in context here + // const { spaces } = useMemory(); const menuItemsTop: Array<MenuItem> = [ { diff --git a/apps/web/src/server/db/schema.ts b/apps/web/src/server/db/schema.ts index d66965c4..a80eb7cf 100644 --- a/apps/web/src/server/db/schema.ts +++ b/apps/web/src/server/db/schema.ts @@ -87,7 +87,6 @@ export const storedContent = createTable( title: text("title", { length: 255 }), description: text("description", { length: 255 }), url: text("url").notNull(), - space: text("space", { length: 255 }).references(() => spaces.name).default('all'), savedAt: int("savedAt", { mode: "timestamp" }).notNull(), baseUrl: text("baseUrl", { length: 255 }), image: text("image", { length: 255 }), @@ -97,20 +96,31 @@ export const storedContent = createTable( urlIdx: index("storedContent_url_idx").on(sc.url), savedAtIdx: index("storedContent_savedAt_idx").on(sc.savedAt), titleInx: index("storedContent_title_idx").on(sc.title), - spaceIdx: index("storedContent_space_idx").on(sc.space), userIdx: index("storedContent_user_idx").on(sc.user), }), ); -export const spaces = createTable( - "spaces", +export const contentToSpace = createTable( + "contentToSpace", + { + contentId: integer("contentId").notNull().references(() => storedContent.id), + spaceId: integer("spaceId").notNull().references(() => space.id), + }, + (cts) => ({ + compoundKey: primaryKey({ columns: [cts.contentId, cts.spaceId] }), + }), +); + +export const space = createTable( + "space", { id: integer("id").notNull().primaryKey({ autoIncrement: true }), name: text('name').notNull().default('all'), - description: text("description", { length: 255 }), + user: text("user", { length: 255 }).references(() => users.id), }, (space) => ({ nameIdx: index("spaces_name_idx").on(space.name), + userIdx: index("spaces_user_idx").on(space.user), }), ); |