diff --git a/apps/web/src/actions/db.ts b/apps/web/src/actions/db.ts index b12ed13b..c5481392 100644 --- a/apps/web/src/actions/db.ts +++ b/apps/web/src/actions/db.ts @@ -8,10 +8,10 @@ import { storedContent, StoredSpace, users, - space + space, } from "@/server/db/schema"; import { SearchResult } from "@/contexts/MemoryContext"; -import { like, eq, and, sql } from "drizzle-orm"; +import { like, eq, and, sql, exists, asc, notExists } from "drizzle-orm"; import { union } from "drizzle-orm/sqlite-core" // @todo: (future) pagination not yet needed @@ -137,3 +137,81 @@ export async function addMemory( ); return _content; } + +export async function addSpace(name: string, memories: number[]) { + + 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 + } +} + + +export async function fetchContentForSpace( + spaceId: number, + range?: { + offset: number; + 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))), + ), + ).orderBy(asc(storedContent.title)) + + 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() + + if (!user) { + return [] + } + + const query = db + .select() + .from(storedContent) + .where( + and( + notExists( + db.select().from(contentToSpace).where(eq(contentToSpace.contentId, storedContent.id)), + ), + eq(storedContent.user, user.id), + ) + + ).orderBy(asc(storedContent.title)) + + return range ? await query.limit(range.limit).offset(range.offset) : await query.all() + +} diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx index 4f8ef734..1482774e 100644 --- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx +++ b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx @@ -12,9 +12,10 @@ import { Markdown } from "tiptap-markdown"; import { useEffect, useRef, useState } from "react"; import { FilterMemories, FilterSpaces } from "./FilterCombobox"; import { useMemory } from "@/contexts/MemoryContext"; -import { Command, Plus, X } from "lucide-react"; +import { Loader, Plus, X } from "lucide-react"; import { StoredContent } from "@/server/db/schema"; import { cleanUrl } from "@/lib/utils"; +import { motion } from "framer-motion" export function AddMemoryPage() { const { addMemory } = useMemory(); @@ -157,8 +158,11 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { + const { addSpace } = useMemory() + const inputRef = useRef(null); const [name, setName] = useState(""); + const [loading, setLoading] = useState(false); const [selected, setSelected] = useState([]); @@ -198,6 +202,7 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { 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" /> @@ -219,7 +224,8 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { Memory @@ -228,14 +234,28 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { type={undefined} onClick={() => { if (check()) { - + setLoading(true) + addSpace(name, selected.map(s => s.id)).then(() => closeDialog()) } }} - className="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="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" > - Add + + + + + Add + - + Cancel diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index 1c9e7143..88262f4e 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -196,13 +196,16 @@ export function MemoryItem({ title, image }: StoredContent) { + + const name = title ? title.length > 10 ? title.slice(0, 10) + "..." : title : ''; + return (
@@ -240,6 +243,8 @@ export function SpaceItem({ return cachedMemories.filter(m => m.space === id) }, [cachedMemories]) + const _name = name.length > 10 ? name.slice(0, 10) + "..." : name + return ( c.image).reverse() as string[]} /> - ) : spaceMemories.length === 1 ? ( + ) : spaceMemories.length > 1 ? ( + c.image).reverse() as string[]} + /> + ) : spaceMemories.length === 1 ? ( ) : ( - c.image).reverse() as string[]} - /> - )} +
+ +
+ )}
); } diff --git a/apps/web/src/contexts/MemoryContext.tsx b/apps/web/src/contexts/MemoryContext.tsx index f2f09e80..08d166a6 100644 --- a/apps/web/src/contexts/MemoryContext.tsx +++ b/apps/web/src/contexts/MemoryContext.tsx @@ -1,8 +1,7 @@ "use client"; import React, { useCallback } from "react"; -import { CollectedSpaces } from "../../types/memory"; import { ChachedSpaceContent, StoredContent, storedContent, StoredSpace } from "@/server/db/schema"; -import { addMemory, searchMemoriesAndSpaces } from "@/actions/db"; +import { addMemory, searchMemoriesAndSpaces, addSpace, fetchContentForSpace } from "@/actions/db"; import { User } from "next-auth"; export type SearchResult = { @@ -16,7 +15,7 @@ export const MemoryContext = React.createContext<{ spaces: StoredSpace[]; deleteSpace: (id: number) => Promise; freeMemories: StoredContent[]; - addSpace: (space: StoredSpace) => Promise; + addSpace: typeof addSpace; addMemory: ( memory: typeof storedContent.$inferInsert, spaces?: number[], @@ -27,7 +26,7 @@ export const MemoryContext = React.createContext<{ spaces: [], freeMemories: [], addMemory: async () => {}, - addSpace: async () => {}, + addSpace: (async () => {}) as unknown as (typeof addSpace), deleteSpace: async () => {}, cachedMemories: [], search: async () => [] @@ -49,10 +48,6 @@ export const MemoryProvider: React.FC< const [cachedMemories, setCachedMemories] = React.useState( initialCachedMemories ); - - const addSpace = async (space: StoredSpace) => { - setSpaces((prev) => [...prev, space]); - } const deleteSpace = async (id: number) => { setSpaces((prev) => prev.filter((s) => s.id !== id)); @@ -68,13 +63,29 @@ export const MemoryProvider: React.FC< ) => { const content = await addMemory(memory, spaces); } + + 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]) + + return { + space: addedSpace, addedMemories + } + } return (