aboutsummaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
authorYash <[email protected]>2024-04-06 05:03:29 +0000
committerYash <[email protected]>2024-04-06 05:03:29 +0000
commit30a4ddc0541041cefd210e2463905f710cdc888b (patch)
treeaf599c83c80cc9f3e218f8f49ed6b3266879fa43 /apps/web/src
parentadd modal (diff)
parentchanged data structure to fetch spaces (diff)
downloadsupermemory-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.tsx11
-rw-r--r--apps/web/src/app/api/store/route.ts34
-rw-r--r--apps/web/src/app/page.tsx18
-rw-r--r--apps/web/src/components/Sidebar.tsx6
-rw-r--r--apps/web/src/components/Sidebar/CategoryItem.tsx298
-rw-r--r--apps/web/src/components/Sidebar/index.tsx5
-rw-r--r--apps/web/src/server/db/schema.ts20
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),
}),
);