mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
Merge branch 'new-ui' of https://github.com/Dhravya/supermemory into new-ui
This commit is contained in:
commit
30a4ddc054
9 changed files with 413 additions and 34 deletions
|
|
@ -16,6 +16,14 @@ CREATE TABLE `account` (
|
|||
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `contentToSpace` (
|
||||
`contentId` integer NOT NULL,
|
||||
`spaceId` integer NOT NULL,
|
||||
PRIMARY KEY(`contentId`, `spaceId`),
|
||||
FOREIGN KEY (`contentId`) REFERENCES `storedContent`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`spaceId`) REFERENCES `space`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `session` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`sessionToken` text(255) NOT NULL,
|
||||
|
|
@ -24,10 +32,11 @@ CREATE TABLE `session` (
|
|||
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `spaces` (
|
||||
CREATE TABLE `space` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text DEFAULT 'all' NOT NULL,
|
||||
`description` text(255)
|
||||
`user` text(255),
|
||||
FOREIGN KEY (`user`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `storedContent` (
|
||||
|
|
@ -36,12 +45,10 @@ CREATE TABLE `storedContent` (
|
|||
`title` text(255),
|
||||
`description` text(255),
|
||||
`url` text NOT NULL,
|
||||
`space` text(255) DEFAULT 'all',
|
||||
`savedAt` integer NOT NULL,
|
||||
`baseUrl` text(255),
|
||||
`image` text(255),
|
||||
`user` text(255),
|
||||
FOREIGN KEY (`space`) REFERENCES `spaces`(`name`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`user`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
|
|
@ -62,9 +69,9 @@ CREATE TABLE `verificationToken` (
|
|||
--> statement-breakpoint
|
||||
CREATE INDEX `account_userId_idx` ON `account` (`userId`);--> statement-breakpoint
|
||||
CREATE INDEX `session_userId_idx` ON `session` (`userId`);--> statement-breakpoint
|
||||
CREATE INDEX `spaces_name_idx` ON `spaces` (`name`);--> statement-breakpoint
|
||||
CREATE INDEX `spaces_name_idx` ON `space` (`name`);--> statement-breakpoint
|
||||
CREATE INDEX `spaces_user_idx` ON `space` (`user`);--> statement-breakpoint
|
||||
CREATE INDEX `storedContent_url_idx` ON `storedContent` (`url`);--> statement-breakpoint
|
||||
CREATE INDEX `storedContent_savedAt_idx` ON `storedContent` (`savedAt`);--> statement-breakpoint
|
||||
CREATE INDEX `storedContent_title_idx` ON `storedContent` (`title`);--> statement-breakpoint
|
||||
CREATE INDEX `storedContent_space_idx` ON `storedContent` (`space`);--> statement-breakpoint
|
||||
CREATE INDEX `storedContent_user_idx` ON `storedContent` (`user`);
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
298
apps/web/src/components/Sidebar/CategoryItem.tsx
Normal file
298
apps/web/src/components/Sidebar/CategoryItem.tsx
Normal file
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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> = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,37 @@
|
|||
import { StoredContent } from '@/server/db/schema';
|
||||
import { db } from '@/server/db';
|
||||
import { contentToSpace, space, StoredContent } from '@/server/db/schema';
|
||||
import { eq, inArray, or } from 'drizzle-orm';
|
||||
|
||||
export const transformContent = (content: StoredContent[]): CollectedSpaces[] => {
|
||||
const spaces = Array.from(new Set(content.map((c) => c.space)));
|
||||
export const transformContent = async (
|
||||
content: StoredContent[],
|
||||
): Promise<CollectedSpaces[]> => {
|
||||
const collectedSpaces = await db
|
||||
.select({
|
||||
id: space.id,
|
||||
name: space.name,
|
||||
contentId: contentToSpace.contentId,
|
||||
})
|
||||
.from(space)
|
||||
.leftJoin(contentToSpace, eq(space.id, contentToSpace.spaceId))
|
||||
.where(
|
||||
inArray(
|
||||
contentToSpace.contentId,
|
||||
content.map((c: StoredContent) => c.id),
|
||||
),
|
||||
)
|
||||
.all();
|
||||
|
||||
const spaceContent = spaces.map((space, i) => ({
|
||||
title: space!,
|
||||
id: i + 1,
|
||||
description: '',
|
||||
content: content.filter((c) => c.space === space),
|
||||
const result = collectedSpaces.map((s) => ({
|
||||
id: s.id,
|
||||
title: s.name,
|
||||
content: content.filter((c) => c.id === s.contentId),
|
||||
}));
|
||||
|
||||
return spaceContent;
|
||||
return result;
|
||||
};
|
||||
|
||||
export type CollectedSpaces = {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
content: StoredContent[];
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue