diff options
| author | Dhravya <[email protected]> | 2024-05-25 18:41:26 -0500 |
|---|---|---|
| committer | Dhravya <[email protected]> | 2024-05-25 18:41:26 -0500 |
| commit | 075f45986fd4d198292226e64afb71b3515576b4 (patch) | |
| tree | 5c728356cd0310f1c1c012fd6618c72a836c314b /apps/web/src | |
| parent | added social material (diff) | |
| download | supermemory-075f45986fd4d198292226e64afb71b3515576b4.tar.xz supermemory-075f45986fd4d198292226e64afb71b3515576b4.zip | |
refactored UI, with shared components and UI, better rules and million lint
Diffstat (limited to 'apps/web/src')
65 files changed, 0 insertions, 8210 deletions
diff --git a/apps/web/src/actions/db.ts b/apps/web/src/actions/db.ts deleted file mode 100644 index 59eb8976..00000000 --- a/apps/web/src/actions/db.ts +++ /dev/null @@ -1,676 +0,0 @@ -"use server"; -import { cookies, headers } from "next/headers"; -import { db } from "@/server/db"; -import { - contentToSpace, - sessions, - storedContent, - users, - space, - StoredContent, -} from "@/server/db/schema"; -import { SearchResult } from "@/contexts/MemoryContext"; -import { - like, - eq, - and, - sql, - exists, - asc, - notExists, - inArray, - notInArray, -} from "drizzle-orm"; -import { union } from "drizzle-orm/sqlite-core"; -import { env } from "@/env"; - -// @todo: (future) pagination not yet needed -export async function searchMemoriesAndSpaces( - query: string, - opts?: { - filter?: { memories?: boolean; spaces?: boolean }; - range?: { offset: number; limit: number }; - memoriesRelativeToSpace?: { - fromSpaces?: number[]; - notInSpaces?: number[]; - }; - }, -): Promise<SearchResult[]> { - const user = await getUser(); - - if (!user) { - return []; - } - - const defaultWhere = and( - eq(storedContent.user, user.id), - like(storedContent.title, `%${query}%`), - ); - const extraWheres = []; - - if (opts?.memoriesRelativeToSpace) { - if (opts.memoriesRelativeToSpace.fromSpaces) { - extraWheres.push( - exists( - db - .select() - .from(contentToSpace) - .where( - and( - eq(contentToSpace.contentId, storedContent.id), - inArray( - contentToSpace.spaceId, - opts.memoriesRelativeToSpace.fromSpaces, - ), - ), - ), - ), - ); - } - if (opts.memoriesRelativeToSpace.notInSpaces) { - extraWheres.push( - notExists( - db - .select() - .from(contentToSpace) - .where( - and( - eq(contentToSpace.contentId, storedContent.id), - inArray( - contentToSpace.spaceId, - opts.memoriesRelativeToSpace.notInSpaces, - ), - ), - ), - ), - ); - } - } - - try { - let searchMemoriesQuery = db - .select({ - type: sql<string>`'memory'`, - space: sql`NULL`, - memory: storedContent as any, - }) - .from(storedContent) - .where( - extraWheres.length == 2 - ? and(and(...extraWheres), defaultWhere) - : extraWheres.length == 1 - ? and(...extraWheres, defaultWhere) - : defaultWhere, - ) - .orderBy(asc(storedContent.savedAt)); - - let 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 getMemoriesFromUrl(urls: string[]) { - const user = await getUser(); - - if (!user) { - return []; - } - - return urls.length > 0 - ? await db - .select() - .from(storedContent) - .where( - and( - inArray(storedContent.url, urls), - eq(storedContent.user, user.id), - ), - ) - .all() - : []; -} - -async function getUser() { - const token = - cookies().get("next-auth.session-token")?.value ?? - cookies().get("__Secure-authjs.session-token")?.value ?? - cookies().get("authjs.session-token")?.value ?? - headers().get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return null; - } - - const session = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!session || session.length === 0) { - return null; - } - - const [userData] = await db - .select() - .from(users) - .where(eq(users.id, session[0].userId)) - .limit(1); - - if (!userData) { - return null; - } - - return userData; -} - -export async function getSpace(id: number) { - const user = await getUser(); - - if (!user) { - return null; - } - - return ( - await db - .select() - .from(space) - .where(and(eq(space.id, id), eq(space.user, user.id))) - )[0]; -} - -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 fetchContent(id: number) { - const user = await getUser(); - - if (!user) { - return null; - } - - const fetchedMemory = await db - .select() - .from(storedContent) - .where(and(eq(storedContent.id, id), eq(storedContent.user, user.id))); - - const memory = fetchedMemory.length > 0 ? fetchedMemory[0] : null; - - const spaces = memory - ? await db - .select() - .from(contentToSpace) - .where(eq(contentToSpace.contentId, memory.id)) - : []; - - return { - memory, - spaces: spaces.map((s) => s.spaceId), - }; -} - -export async function fetchContentForSpace( - spaceId: number, - range?: { - offset: number; - limit: number; - }, -) { - const user = await getUser(); - - if (!user) { - return null; - } - - const query = db - .select() - .from(storedContent) - .where( - exists( - db - .select() - .from(contentToSpace) - .where( - and( - and( - eq(contentToSpace.spaceId, spaceId), - eq(contentToSpace.contentId, storedContent.id), - ), - exists( - db - .select() - .from(space) - .where( - and( - eq(space.user, user.id), - eq(space.id, contentToSpace.spaceId), - ), - ), - ), - ), - ), - ), - ) - .orderBy(asc(storedContent.savedAt)); - - 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 []; - } - - try { - 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(); - } catch { - return []; - } -} - -export async function updateSpaceTitle(id: number, title: string) { - const user = await getUser(); - - if (!user) { - return null; - } - - return ( - await db - .update(space) - .set({ name: title }) - .where(and(eq(space.id, id), eq(space.user, user.id))) - .returning() - )[0]; -} - -export async function addMemory( - content: typeof storedContent.$inferInsert, - spaces: number[], -) { - const user = await getUser(); - - if (!user) { - return null; - } - - if (!content.content || content.content.trim() === "") { - const resp = await fetch( - `https://cf-ai-backend.dhravya.workers.dev/getPageContent?url=${content.url}`, - { - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - }, - ); - - const data = await resp.text(); - - console.log(data); - - content.content = data; - } - - if (!content.content || content.content == "") { - return null; - } - - let [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() - : []; - - if (content.type === "note") { - addedMemory = ( - await db - .update(storedContent) - .set({ - url: addedMemory.url + addedMemory.id, - }) - .where(eq(storedContent.id, addedMemory.id)) - .returning() - )[0]; - } - - console.log("adding with:", `${addedMemory.url}-${user.email}`); - // Add to vectorDB - const res = (await Promise.race([ - fetch("https://cf-ai-backend.dhravya.workers.dev/add", { - method: "POST", - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - body: JSON.stringify({ - pageContent: addedMemory.content, - title: addedMemory.title, - url: addedMemory.url, - user: user.email, - }), - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Request timed out")), 40000), - ), - ])) as Response; - - return { - memory: addedMemory, - addedToSpaces, - }; -} - -export async function addContentInSpaces(id: number, contents: number[]) { - const user = await getUser(); - - if (!user) { - return null; - } - - const data = - contents.length > 0 - ? await db - .insert(contentToSpace) - .values( - contents.map((i) => ({ - spaceId: id, - contentId: i, - })), - ) - .returning() - : []; - - return data; -} - -export async function updateMemory( - id: number, - { - title, - content, - spaces, - removedFromSpaces: removeSpaces, - }: { - title?: string; - content?: string; - spaces?: number[]; - removedFromSpaces?: number[]; - }, -) { - const user = await getUser(); - - if (!user) { - return null; - } - - let updatedMemory: StoredContent | null = null; - - if (title && content) { - const [prev] = await db - .select() - .from(storedContent) - .where(and(eq(storedContent.user, user.id), eq(storedContent.id, id))); - - if (!prev) { - return null; - } - - const newContent = { - ...(title ? { title } : {}), - ...(content ? { content } : {}), - }; - - const updated = { - ...newContent, - ...prev, - }; - - console.log("adding with:", `${updated.url}-${user.email}`); - // Add to vectorDB - const res = (await Promise.race([ - fetch("https://cf-ai-backend.dhravya.workers.dev/edit", { - method: "POST", - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - body: JSON.stringify({ - pageContent: updated.content, - title: updated.title, - url: updated.url, - user: user.email, - uniqueUrl: updated.url, - }), - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Request timed out")), 40000), - ), - ])) as Response; - - [updatedMemory] = await db - .update(storedContent) - .set(newContent) - .where(and(eq(storedContent.id, id), eq(storedContent.user, user.id))) - .returning(); - - console.log(updatedMemory, newContent); - } - - if (!updatedMemory) { - [updatedMemory] = await db - .select() - .from(storedContent) - .where(and(eq(storedContent.id, id), eq(storedContent.user, user.id))); - } - - const removedFromSpaces = removeSpaces - ? removeSpaces.length > 0 - ? await db - .delete(contentToSpace) - .where( - and( - inArray(contentToSpace.spaceId, removeSpaces), - eq(contentToSpace.contentId, id), - ), - ) - .returning() - : [] - : spaces - ? spaces.length > 0 - ? await db - .delete(contentToSpace) - .where( - and( - notInArray(contentToSpace.spaceId, spaces), - eq(contentToSpace.contentId, id), - ), - ) - .returning() - : await db - .delete(contentToSpace) - .where(eq(contentToSpace.contentId, id)) - : []; - - const addedToSpaces = - spaces && spaces.length > 0 - ? await db - .insert(contentToSpace) - .values( - spaces.map((s) => ({ - contentId: id, - spaceId: s, - })), - ) - .onConflictDoNothing() - .returning() - : []; - - const resultedSpaces = - ( - await db - .select() - .from(contentToSpace) - .where(eq(contentToSpace.contentId, id)) - .all() - ).map((i) => i.spaceId) ?? []; - - return { - memory: updatedMemory, - addedToSpaces, - removedFromSpaces, - resultedSpaces, - }; -} - -export async function deleteSpace(id: number) { - 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(); - - return deleted; -} - -export async function deleteMemory(id: number) { - 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(); - - if (deleted) { - console.log("adding with:", `${deleted.url}-${user.email}`); - const res = (await Promise.race([ - fetch(`https://cf-ai-backend.dhravya.workers.dev/delete`, { - method: "DELETE", - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - body: JSON.stringify({ - websiteUrl: deleted.url, - user: user.email, - }), - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Request timed out")), 40000), - ), - ])) as Response; - } - - return deleted; -} diff --git a/apps/web/src/app/MessagePoster.tsx b/apps/web/src/app/MessagePoster.tsx deleted file mode 100644 index 1abad1be..00000000 --- a/apps/web/src/app/MessagePoster.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import { useEffect } from "react"; - -function MessagePoster({ jwt }: { jwt: string }) { - useEffect(() => { - if (typeof window === "undefined") return; - window.postMessage({ jwt }, "*"); - }, [jwt]); - - return ( - <button - className="p-2" - onClick={() => { - if (typeof window === "undefined") return; - window.postMessage({ jwt }, "*"); - }} - > - Extension Auth - </button> - ); -} - -export default MessagePoster; diff --git a/apps/web/src/app/api/[...nextauth]/route.ts b/apps/web/src/app/api/[...nextauth]/route.ts deleted file mode 100644 index db7d1fb8..00000000 --- a/apps/web/src/app/api/[...nextauth]/route.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { GET, POST } from "@/server/auth"; -export const runtime = "edge"; diff --git a/apps/web/src/app/api/addTweetsToDb/route.ts b/apps/web/src/app/api/addTweetsToDb/route.ts deleted file mode 100644 index 7fe2edba..00000000 --- a/apps/web/src/app/api/addTweetsToDb/route.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { db } from "@/server/db"; -import { eq } from "drizzle-orm"; -import { sessions, storedContent, users } from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; - -export const runtime = "edge"; - -interface TweetData { - tweetText: string; - postUrl: string; - authorName: string; - handle: string; - time: string; - saveToUser: string; -} - -export async function POST(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return new Response( - JSON.stringify({ message: "Invalid Key, TOKEN not found." }), - { status: 404 }, - ); - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const user = await db - .select() - .from(users) - .where(eq(users.id, sessionData[0].userId)) - .limit(1); - - if (!user || user.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - const session = { session: sessionData[0], user: user[0] }; - - const data = (await req.json()) as TweetData[]; - - for (const tweet of data) { - const { id } = ( - await db - .insert(storedContent) - .values({ - content: tweet.tweetText, - title: "Twitter Bookmark", - description: "", - url: tweet.postUrl, - baseUrl: "https://twitter.com", - image: "https://supermemory.dhr.wtf/twitter.svg", - savedAt: new Date(), - user: session.user.id, - type: "twitter-bookmark", - }) - .returning({ id: storedContent.id }) - )[0]; - - if (!id) { - return NextResponse.json( - { - message: "Error", - error: - "Something went wrong when inserting the tweet to storedContent", - }, - { status: 500 }, - ); - } - } - - return NextResponse.json({ message: "OK", data: "Success" }, { status: 200 }); -} diff --git a/apps/web/src/app/api/ask/route.ts b/apps/web/src/app/api/ask/route.ts deleted file mode 100644 index 17b24b3e..00000000 --- a/apps/web/src/app/api/ask/route.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { db } from "@/server/db"; -import { eq } from "drizzle-orm"; -import { sessions, users } from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; -import { env } from "@/env"; - -export const runtime = "edge"; - -export async function POST(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (process.env.RATELIMITER) { - const { success } = await process.env.RATELIMITER.limit({ key: token }); - - if (!success) { - return new Response(JSON.stringify({ message: "Rate limit exceeded" }), { - status: 429, - }); - } - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const user = await db - .select() - .from(users) - .where(eq(users.id, sessionData[0].userId)) - .limit(1); - - if (!user || user.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - const body = (await req.json()) as { - query: string; - }; - - const resp = await fetch(`https://cf-ai-backend.dhravya.workers.dev/ask`, { - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - method: "POST", - body: JSON.stringify({ - query: body.query, - }), - }); - - if (resp.status !== 200 || !resp.ok) { - const errorData = await resp.json(); - return new Response( - JSON.stringify({ message: "Error in CF function", error: errorData }), - { status: resp.status }, - ); - } - - // Stream the response back to the client - const { readable, writable } = new TransformStream(); - resp && resp.body!.pipeTo(writable); - - return new Response(readable, { status: 200 }); -} diff --git a/apps/web/src/app/api/chat/route.ts b/apps/web/src/app/api/chat/route.ts deleted file mode 100644 index c815070b..00000000 --- a/apps/web/src/app/api/chat/route.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { db } from "@/server/db"; -import { eq } from "drizzle-orm"; -import { sessions, users } from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; -import { env } from "@/env"; -import { ChatHistory } from "../../../../types/memory"; - -export const runtime = "edge"; - -export async function POST(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (process.env.RATELIMITER) { - const { success } = await process.env.RATELIMITER.limit({ key: token }); - - if (!success) { - return new Response(JSON.stringify({ message: "Rate limit exceeded" }), { - status: 429, - }); - } - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const user = await db - .select() - .from(users) - .where(eq(users.id, sessionData[0].userId)) - .limit(1); - - if (!user || user.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - const session = { session: sessionData[0], user: user[0] }; - - const query = new URL(req.url).searchParams.get("q"); - const spaces = new URL(req.url).searchParams.get("spaces"); - - const sourcesOnly = - new URL(req.url).searchParams.get("sourcesOnly") ?? "false"; - - const chatHistory = (await req.json()) as { - chatHistory: ChatHistory[]; - }; - - console.log("CHathistory", chatHistory); - - if (!query) { - return new Response(JSON.stringify({ message: "Invalid query" }), { - status: 400, - }); - } - - try { - const resp = await fetch( - `https://cf-ai-backend.dhravya.workers.dev/chat?q=${query}&user=${session.user.email ?? session.user.name}&sourcesOnly=${sourcesOnly}&spaces=${spaces}`, - { - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - method: "POST", - body: JSON.stringify({ - chatHistory: chatHistory.chatHistory ?? [], - }), - }, - ); - - console.log("sourcesOnly", sourcesOnly); - - if (sourcesOnly == "true") { - const data = await resp.json(); - console.log("data", data); - return new Response(JSON.stringify(data), { status: 200 }); - } - - if (resp.status !== 200 || !resp.ok) { - const errorData = await resp.json(); - console.log(errorData); - return new Response( - JSON.stringify({ message: "Error in CF function", error: errorData }), - { status: resp.status }, - ); - } - - // Stream the response back to the client - const { readable, writable } = new TransformStream(); - resp && resp.body!.pipeTo(writable); - - return new Response(readable, { status: 200 }); - } catch {} -} diff --git a/apps/web/src/app/api/getCount/route.ts b/apps/web/src/app/api/getCount/route.ts deleted file mode 100644 index 9fe54f78..00000000 --- a/apps/web/src/app/api/getCount/route.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { db } from "@/server/db"; -import { and, eq, ne, sql } from "drizzle-orm"; -import { sessions, storedContent, users } from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; - -export const runtime = "edge"; - -export async function GET(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const user = await db - .select() - .from(users) - .where(eq(users.id, sessionData[0].userId)) - .limit(1); - - if (!user || user.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - const session = { session: sessionData[0], user: user[0] }; - - const tweetsCount = await db - .select({ - count: sql<number>`count(*)`.mapWith(Number), - }) - .from(storedContent) - .where( - and( - eq(storedContent.user, session.user.id), - eq(storedContent.type, "twitter-bookmark"), - ), - ); - - const pageCount = await db - .select({ - count: sql<number>`count(*)`.mapWith(Number), - }) - .from(storedContent) - .where( - and( - eq(storedContent.user, session.user.id), - ne(storedContent.type, "twitter-bookmark"), - ), - ); - - return NextResponse.json({ - tweetsCount: tweetsCount[0].count, - tweetsLimit: 1000, - pageCount: pageCount[0].count, - pageLimit: 100, - user: session.user.email, - }); -} diff --git a/apps/web/src/app/api/me/route.ts b/apps/web/src/app/api/me/route.ts deleted file mode 100644 index 6d269872..00000000 --- a/apps/web/src/app/api/me/route.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { db } from "@/server/db"; -import { eq } from "drizzle-orm"; -import { sessions, users } from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; -import { env } from "@/env"; - -export const runtime = "edge"; - -export async function GET(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - const session = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!session || session.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const user = await db - .select() - .from(users) - .where(eq(users.id, session[0].userId)) - .limit(1); - - if (!user || user.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - return new Response( - JSON.stringify({ - message: "OK", - data: { session: session[0], user: user[0] }, - }), - { status: 200 }, - ); -} diff --git a/apps/web/src/app/api/query/route.ts b/apps/web/src/app/api/query/route.ts deleted file mode 100644 index 5806841e..00000000 --- a/apps/web/src/app/api/query/route.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { db } from "@/server/db"; -import { eq } from "drizzle-orm"; -import { sessions, users } from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; -import { env } from "@/env"; - -export const runtime = "edge"; - -export async function GET(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (process.env.RATELIMITER) { - const { success } = await process.env.RATELIMITER.limit({ key: token }); - - if (!success) { - return new Response(JSON.stringify({ message: "Rate limit exceeded" }), { - status: 429, - }); - } - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const user = await db - .select() - .from(users) - .where(eq(users.id, sessionData[0].userId)) - .limit(1); - - if (!user || user.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - const session = { session: sessionData[0], user: user[0] }; - - const query = new URL(req.url).searchParams.get("q"); - const sourcesOnly = - new URL(req.url).searchParams.get("sourcesOnly") ?? "false"; - - if (!query) { - return new Response(JSON.stringify({ message: "Invalid query" }), { - status: 400, - }); - } - - const resp = await fetch( - `https://cf-ai-backend.dhravya.workers.dev/query?q=${query}&user=${session.user.email ?? session.user.name}&sourcesOnly=${sourcesOnly}`, - { - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - }, - ); - - console.log(resp.status); - - if (resp.status !== 200 || !resp.ok) { - const errorData = await resp.json(); - console.log(errorData); - return new Response( - JSON.stringify({ message: "Error in CF function", error: errorData }), - { status: resp.status }, - ); - } - - // Stream the response back to the client - const { readable, writable } = new TransformStream(); - resp && resp.body!.pipeTo(writable); - - return new Response(readable, { status: 200 }); -} diff --git a/apps/web/src/app/api/spaces/route.ts b/apps/web/src/app/api/spaces/route.ts deleted file mode 100644 index d2685e9f..00000000 --- a/apps/web/src/app/api/spaces/route.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { db } from "@/server/db"; -import { sessions, space, users } from "@/server/db/schema"; -import { eq } from "drizzle-orm"; -import { NextRequest, NextResponse } from "next/server"; - -export const runtime = "edge"; - -export async function GET(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - if (process.env.RATELIMITER) { - const { success } = await process.env.RATELIMITER.limit({ key: token }); - - if (!success) { - return new Response(JSON.stringify({ message: "Rate limit exceeded" }), { - status: 429, - }); - } - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const userData = await db - .select() - .from(users) - .where(eq(users.id, sessionData[0].userId)) - .limit(1); - - if (!userData || userData.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - const user = userData[0]; - - const spaces = await db - .select() - .from(space) - .where(eq(space.user, user.id)) - .all(); - - return NextResponse.json( - { - message: "OK", - data: spaces, - }, - { status: 200 }, - ); -} diff --git a/apps/web/src/app/api/store/route.ts b/apps/web/src/app/api/store/route.ts deleted file mode 100644 index 457eae2e..00000000 --- a/apps/web/src/app/api/store/route.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { db } from "@/server/db"; -import { and, eq, sql, inArray } 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"; - -export const runtime = "edge"; - -export async function POST(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - if (process.env.RATELIMITER) { - const { success } = await process.env.RATELIMITER.limit({ key: token }); - - if (!success) { - return new Response(JSON.stringify({ message: "Rate limit exceeded" }), { - status: 429, - }); - } - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const user = await db - .select() - .from(users) - .where(eq(users.id, sessionData[0].userId)) - .limit(1); - - if (!user || user.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - const session = { session: sessionData[0], user: user[0] }; - - const data = (await req.json()) as { - pageContent: string; - url: string; - spaces?: string[]; - }; - - const metadata = await getMetaData(data.url); - let storeToSpaces = data.spaces; - - if (!storeToSpaces) { - storeToSpaces = []; - } - - const count = await db - .select({ - count: sql<number>`count(*)`.mapWith(Number), - }) - .from(storedContent) - .where( - and( - eq(storedContent.user, session.user.id), - eq(storedContent.type, "page"), - ), - ); - - if (count[0].count > 100) { - return NextResponse.json( - { message: "Error", error: "Limit exceeded" }, - { status: 499 }, - ); - } - - const { id } = ( - await db - .insert(storedContent) - .values({ - content: data.pageContent, - title: metadata.title, - description: metadata.description, - url: data.url, - baseUrl: metadata.baseUrl, - image: metadata.image, - savedAt: new Date(), - user: session.user.id, - }) - .returning({ id: storedContent.id }) - )[0]; - - if (!id) { - return NextResponse.json( - { message: "Error", error: "Error in CF function" }, - { status: 500 }, - ); - } - - if (storeToSpaces.length > 0) { - const spaceData = await db - .select() - .from(space) - .where( - and( - inArray(space.name, storeToSpaces ?? []), - eq(space.user, session.user.id), - ), - ) - .all(); - - await Promise.all([ - spaceData.forEach(async (space) => { - await db - .insert(contentToSpace) - .values({ contentId: id, spaceId: space.id }); - }), - ]); - } - - const res = (await Promise.race([ - fetch("https://cf-ai-backend.dhravya.workers.dev/add", { - method: "POST", - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - body: JSON.stringify({ ...data, user: session.user.email }), - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Request timed out")), 40000), - ), - ])) as Response; - - if (res.status !== 200) { - console.log(res.status, res.statusText); - return NextResponse.json( - { message: "Error", error: "Error in CF function" }, - { status: 500 }, - ); - } - - return NextResponse.json({ message: "OK", data: "Success" }, { status: 200 }); -} diff --git a/apps/web/src/app/api/vectorizeTweets/route.ts b/apps/web/src/app/api/vectorizeTweets/route.ts deleted file mode 100644 index 63aa38f0..00000000 --- a/apps/web/src/app/api/vectorizeTweets/route.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { db } from "@/server/db"; -import { eq } from "drizzle-orm"; -import { sessions, storedContent, users } from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; -import { env } from "@/env"; - -export const runtime = "edge"; - -interface TweetData { - tweetText: string; - postUrl: string; - authorName: string; - handle: string; - time: string; - saveToUser: string; -} - -export async function POST(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const body = (await req.json()) as TweetData[]; - - console.log(body); - - const resp = await fetch( - `https://cf-ai-backend.dhravya.workers.dev/batchUploadTweets`, - { - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - method: "POST", - body: JSON.stringify(body), - }, - ); - - return new Response(await resp.text(), { - status: resp.status, - headers: resp.headers, - }); -} diff --git a/apps/web/src/app/content.tsx b/apps/web/src/app/content.tsx deleted file mode 100644 index 5a68d902..00000000 --- a/apps/web/src/app/content.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; -import SessionProviderWrapper from "@/components/dev/SessionProviderWrapper"; -import Main from "@/components/Main"; -import Sidebar from "@/components/Sidebar/index"; -import { useState } from "react"; - -export default function Content({ jwt }: { jwt: string }) { - const [selectedItem, setSelectedItem] = useState<string | null>(null); - - return ( - <SessionProviderWrapper> - <div className="flex w-screen"> - <Sidebar jwt={jwt} selectChange={setSelectedItem} /> - <Main sidebarOpen={selectedItem !== null} /> - </div> - </SessionProviderWrapper> - ); -} diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css deleted file mode 100644 index bed9278b..00000000 --- a/apps/web/src/app/globals.css +++ /dev/null @@ -1,140 +0,0 @@ -@import "@radix-ui/colors/gray"; -@import "@radix-ui/colors/gray-dark"; - -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - @apply text-rgray-11 max-h-screen overflow-y-hidden bg-white; - /* color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); */ -} - -[vaul-drawer-wrapper] { - @apply bg-rgray-2; -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } -} - -.sidebar { - height: 100vh; - height: 100dvh; - max-height: 100vh; - max-height: 100dvh; -} - -.DrawerContent { - padding-top: 5vh; - padding-top: 5dvh; -} - -.main-hidden { - padding-bottom: 20vh; - padding-bottom: 15dvh; -} - -.bottom-padding { - bottom: 20vh; - bottom: 20dvh; -} - -@media (min-width: 768px) { - .bottom-padding { - bottom: 0; - } -} - -.chat-answer code { - @apply bg-rgray-3 border-rgray-5 text-rgray-11 text-wrap rounded-md border p-1 text-sm; -} - -.novel-editor pre { - @apply bg-rgray-3 border-rgray-5 text-rgray-11 my-5 rounded-md border p-4 text-sm; -} - -.chat-answer h1 { - @apply text-rgray-11 my-5 text-xl font-medium; -} - -.chat-answer a { - @apply underline underline-offset-1 opacity-90 hover:opacity-100; -} - -.chat-answer img { - @apply my-5 rounded-md font-medium; -} - -.tippy-box { - @apply bg-rgray-3 text-rgray-11 border-rgray-5 rounded-md border py-0; -} - -.tippy-content #slash-command { - @apply text-rgray-11 border-none bg-transparent; -} - -#slash-command button { - @apply text-rgray-11 py-2; -} - -#slash-command button div:first-child { - @apply text-rgray-11 bg-rgray-4 border-rgray-5; -} - -#slash-command button.novel-bg-stone-100 { - @apply bg-rgray-1; -} - -.novel-editor [data-type="taskList"] > li { - @apply my-0; -} - -.novel-editor input[type="checkbox"] { - @apply accent-rgray-4 rounded-md; - - background: var(--gray-4) !important; - border: 1px solid var(--gray-10) !important; -} - -.novel-editor .is-empty::before { - content: "Press '/' for commands" !important; -} - -.novel-editor h1 { - @apply text-2xl; -} - -.novel-editor h2 { - @apply text-xl; -} - -.novel-editor h3 { - @apply text-lg; -} - -.novel-editor .drag-handle { - @apply hidden; -} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx deleted file mode 100644 index e96df271..00000000 --- a/apps/web/src/app/layout.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import type { Metadata } from "next"; -import { Roboto, Inter } from "next/font/google"; -import "./globals.css"; - -const inter = Inter({ weight: ["300", "400", "500"], subsets: ["latin"] }); - -export const metadata: Metadata = { - title: "Supermemory - Your second brain", - description: "Save your memories forever, build your own second brain.", - openGraph: { - images: [ - { - url: "https://supermemory.dhr.wtf/og-image.png", - width: 1200, - height: 630, - }, - ], - siteName: "Supermemory", - title: "Supermemory - Your second brain", - description: "Save your memories forever, build your own second brain.", - }, - twitter: { - card: "summary_large_image", - site: "https://supermemory.dhr.wtf", - creator: "@dhravyashah", - description: "Save your memories forever, build your own second brain.", - images: [ - { - url: "https://supermemory.dhr.wtf/og-image.png", - width: 1200, - height: 630, - }, - ], - }, -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - <html lang="en" className="dark"> - <head> - <meta - name="og:image" - content="https://supermemory.dhr.wtf/og-image.png" - /> - <script - async - src="https://u.dhr.wtf/script.js" - data-website-id="731dfc2e-b1c0-4696-a7b3-efd27b19dfdf" - ></script> - </head> - <body className={inter.className}> - <div - vaul-drawer-wrapper="" - className="min-w-screen overflow-x-hidden text-black" - > - {children} - </div> - </body> - </html> - ); -} diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx deleted file mode 100644 index 3409889a..00000000 --- a/apps/web/src/app/not-found.tsx +++ /dev/null @@ -1,58 +0,0 @@ -export const runtime = "edge"; - -export default function NotFound() { - return ( - <> - <title>404: This page could not be found.</title> - <div style={styles.error}> - <div> - <style - dangerouslySetInnerHTML={{ - __html: `body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}`, - }} - /> - <h1 className="next-error-h1" style={styles.h1}> - 404 - </h1> - <div style={styles.desc}> - <h2 style={styles.h2}>This page could not be found.</h2> - </div> - </div> - </div> - </> - ); -} - -const styles = { - error: { - fontFamily: - 'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"', - height: "100vh", - textAlign: "center", - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - }, - - desc: { - display: "inline-block", - }, - - h1: { - display: "inline-block", - margin: "0 20px 0 0", - padding: "0 23px 0 0", - fontSize: 24, - fontWeight: 500, - verticalAlign: "top", - lineHeight: "49px", - }, - - h2: { - fontSize: 14, - fontWeight: 400, - lineHeight: "49px", - margin: 0, - }, -} as const; diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx deleted file mode 100644 index 05cd1ab8..00000000 --- a/apps/web/src/app/page.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { db } from "@/server/db"; -import { - ChachedSpaceContent, - sessions, - space, - storedContent, - users, -} from "@/server/db/schema"; -import { and, eq, inArray, not } from "drizzle-orm"; -import { cookies, headers } from "next/headers"; -import { redirect } from "next/navigation"; -import { fetchContentForSpace, fetchFreeMemories } from "@/actions/db"; -import { MemoryProvider } from "@/contexts/MemoryContext"; -import Content from "./content"; -import Main from "@/components/Main"; -import { TailwindIndicator } from "@/components/dev/tailwindindicator"; - -export const runtime = "edge"; - -export default async function Home() { - const token = - cookies().get("next-auth.session-token")?.value ?? - cookies().get("__Secure-authjs.session-token")?.value ?? - cookies().get("authjs.session-token")?.value ?? - headers().get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return redirect("/api/auth/signin"); - } - - const session = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!session || session.length === 0) { - return redirect("/api/auth/signin"); - } - - const [userData] = await db - .select() - .from(users) - .where(eq(users.id, session[0].userId)) - .limit(1); - - if (!userData) { - return redirect("/api/auth/signin"); - } - - console.log(storedContent.user.name); - - const collectedSpaces = await db - .select() - .from(space) - .where(eq(space.user, userData.id)) - .all(); - - console.log(collectedSpaces); - - // Fetch only first 3 content of each spaces - let contents: ChachedSpaceContent[] = []; - - 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, ...data]; - }), - ]); - - console.log("contents", contents); - - // freeMemories - const freeMemories = await fetchFreeMemories(); - console.log("free", freeMemories); - - return ( - <MemoryProvider - user={userData} - spaces={collectedSpaces} - freeMemories={freeMemories} - cachedMemories={contents} - > - <Content jwt={token} /> - <TailwindIndicator /> - {/* <MessagePoster jwt={token} /> */} - </MemoryProvider> - ); -} diff --git a/apps/web/src/app/privacy/page.tsx b/apps/web/src/app/privacy/page.tsx deleted file mode 100644 index 5e40cbe9..00000000 --- a/apps/web/src/app/privacy/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; -import Markdown from "react-markdown"; -import { policy } from "./privacy"; - -export const runtime = "edge"; - -function Page() { - return ( - <div> - <Markdown>{policy}</Markdown> - </div> - ); -} - -export default Page; diff --git a/apps/web/src/app/privacy/privacy.ts b/apps/web/src/app/privacy/privacy.ts deleted file mode 100644 index 2034f191..00000000 --- a/apps/web/src/app/privacy/privacy.ts +++ /dev/null @@ -1,49 +0,0 @@ -export const policy = ` -# Privacy Policy for AnyContext - -## Introduction - -This Privacy Policy provides detailed information on the handling, storage, and protection of your personal information by AnyContext, a browser extension developed and owned by Dhravya Shah in 2024. The extension is designed to enhance your browsing experience by providing contextual information based on the content of the web pages you visit. This policy outlines the types of data collected by AnyContext, how it is used, and the measures we take to protect your privacy. - -## Information Collection - -AnyContext collects the following types of information: - -1. **Web Browsing Data**: The extension has the capability to see all websites that users visit. However, AnyContext only stores data when the user actively clicks on the extension button while browsing. The browser history is not recorded, ensuring that your browsing activities remain private. - -2. **Current Page Data**: Upon activation (click) by the user, AnyContext stores data from the current HTML page. This data is used to provide relevant contextual information based on the content of the page you are viewing. - -3. **Personal Information**: When you interact with AnyContext, we may collect personal information including but not limited to your email address, session data, name, and profile picture. This information is collected to improve your user experience and to provide personalized services. - -## Data Storage and Security - -All collected data is securely stored in a SQLite database hosted on Cloudflare D1. We employ industry-standard security measures to protect your information from unauthorized access, alteration, disclosure, or destruction. Despite our efforts, no method of transmission over the Internet or method of electronic storage is 100% secure. Therefore, while we strive to use commercially acceptable means to protect your personal information, we cannot guarantee its absolute security. - -## Use of Information - -AnyContext uses the collected information for the following purposes: - -- To provide and improve the functionality of the extension. -- To offer personalized user experiences. -- To communicate with users regarding updates, support, and promotional offers, if consented. -- To ensure the security of our services and to detect, prevent, or address technical issues. - -## Sharing of Information - -AnyContext does not sell, trade, or rent users' personal identification information to others. We may share generic aggregated demographic information not linked to any personal identification information regarding visitors and users with our business partners, trusted affiliates, and advertisers for the purposes outlined above. - -## Your Privacy Rights - -You have the right to access, update, or delete your personal information that we hold. If you wish to exercise these rights, please contact us at the details provided below. - -## Changes to This Privacy Policy - -AnyContext reserves the right to update this privacy policy at any time. When we do, we will post a notification on our website and update the date at the top of this page. We encourage users to frequently check this page for any changes to stay informed about how we are protecting the personal information we collect. Your continued use of the service after the posting of changes to this policy will be deemed your acceptance of those changes. - -## Contact Us - -If you have any questions about this Privacy Policy, the practices of this site, or your dealings with this site, please contact us at: - -- Email: [email protected] - -This document was last updated on March 2, 2024.`; diff --git a/apps/web/src/assets/Bin.tsx b/apps/web/src/assets/Bin.tsx deleted file mode 100644 index d0793cef..00000000 --- a/apps/web/src/assets/Bin.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { cn } from "@/lib/utils"; -import { useEffect, useRef } from "react"; - -export const Bin: React.FC<React.HTMLAttributes<HTMLDivElement> & {}> = ({ - className, - ...props -}) => { - const icon = useRef<HTMLDivElement>(null); - - useEffect(() => { - let timeout: ReturnType<typeof setTimeout> | undefined; - - const observer = new MutationObserver(function (mutations) { - mutations.forEach(function (mutation) { - if ( - mutation.type === "attributes" && - mutation.attributeName === "data-open" && - (mutation.oldValue === "false" || mutation.oldValue === null) && - icon.current?.dataset["open"] === "true" - ) { - if (timeout) clearTimeout(timeout); - timeout = setTimeout(() => { - icon.current!.dataset["open"] = "false"; - }, 2000); - } - }); - }); - - observer.observe(icon.current!, { - attributes: true, //configure it to listen to attribute changes - }); - - return () => { - observer.disconnect(); - }; - }, []); - - return ( - <div - ref={icon} - data-open="false" - className={cn( - "relative z-[100] flex w-full origin-bottom flex-col items-center justify-center transition-transform delay-500 duration-500 data-[open='true']:-translate-y-2 data-[open='true']:scale-150 data-[open='true']:delay-0 [&[data-open='true']>[data-lid]]:rotate-[150deg] [&[data-open='true']>[data-lid]]:delay-0", - className, - )} - {...props} - > - <svg - data-lid - className="w-full origin-[90%_80%] transition-transform delay-500 duration-500 ease-in-out" - viewBox="0 0 24 7" - fill="none" - strokeWidth={1} - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M3 6H21" - stroke="currentColor" - strokeWidth={1} - strokeLinecap="round" - strokeLinejoin="round" - /> - <path - d="M8 6V4C8 3 9 2 10 2H14C15 2 16 3 16 4V6" - stroke="currentColor" - strokeWidth={1} - strokeLinecap="round" - strokeLinejoin="round" - /> - </svg> - - <svg - className="-mt-[1px] w-full" - viewBox="0 0 24 19" - fill="none" - strokeWidth={1} - data-trash-bin - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M19 1V15C19 16 18 17 17 17H7C6 17 5 16 5 15V1" - stroke="currentColor" - strokeWidth={1} - strokeLinecap="round" - strokeLinejoin="round" - /> - <path - d="M10 6V12" - stroke="currentColor" - strokeWidth={1} - strokeLinecap="round" - strokeLinejoin="round" - /> - <path - d="M14 6V12" - stroke="currentColor" - strokeWidth={1} - strokeLinecap="round" - strokeLinejoin="round" - /> - </svg> - </div> - ); -}; diff --git a/apps/web/src/assets/Memories.tsx b/apps/web/src/assets/Memories.tsx deleted file mode 100644 index cafcd54f..00000000 --- a/apps/web/src/assets/Memories.tsx +++ /dev/null @@ -1,69 +0,0 @@ -export const MemoryIcon: React.FC<React.SVGAttributes<SVGElement>> = ( - props, -) => ( - <svg - viewBox="0 0 89 53" - fill="none" - xmlns="http://www.w3.org/2000/svg" - {...props} - > - <rect - x="0.40697" - y="8.52821" - width="43.0286" - height="43.0286" - rx="5.5" - transform="rotate(-12 0.40697 8.52821)" - fill="white" - stroke="black" - /> - <rect - x="20.8257" - y="9.19775" - width="43" - height="43" - rx="5.5" - fill="white" - stroke="black" - /> - <rect - x="47.6965" - y="-0.612372" - width="43.0286" - height="43.0286" - rx="5.5" - transform="rotate(15 47.6965 -0.612372)" - fill="white" - stroke="black" - /> - </svg> -); - -export const SpaceIcon: React.FC<React.SVGAttributes<SVGElement>> = (props) => ( - <svg - viewBox="0 0 34 30" - fill="none" - xmlns="http://www.w3.org/2000/svg" - {...props} - > - <rect - x="1.39502" - y="5.2229" - width="24" - height="24" - rx="5.5" - fill="var(--gray-5)" - stroke="var(--gray-10)" - /> - <rect - x="11.2231" - y="-0.157702" - width="24" - height="24" - rx="5.5" - transform="rotate(20 11.2231 -0.157702)" - fill="var(--gray-5)" - stroke="var(--gray-10)" - /> - </svg> -); diff --git a/apps/web/src/assets/MemoryWithImages.tsx b/apps/web/src/assets/MemoryWithImages.tsx deleted file mode 100644 index 6f7ba90a..00000000 --- a/apps/web/src/assets/MemoryWithImages.tsx +++ /dev/null @@ -1,531 +0,0 @@ -import { svgId } from "@/lib/utils"; - -export const MemoryWithImage: React.FC< - { image: string; id: string } & React.SVGAttributes<SVGElement> -> = ({ image, id: _id, ...props }) => { - const id = "space-1-" + _id; - - return ( - <svg - viewBox="0 0 137 137" - fill="none" - xmlns="http://www.w3.org/2000/svg" - xmlnsXlink="http://www.w3.org/1999/xlink" - {...props} - > - <g filter={`url(#${svgId(id, "filter0_d_83_72")})`}> - <rect - x="33" - y="33" - width="72.207" - height="72.207" - rx="10" - fill="white" - /> - </g> - <rect - x="47.0637" - y="46.9976" - width="45" - height="45" - fill={`url(#${svgId(id, "pattern0")})`} - /> - <defs> - <filter - id={svgId(id, "filter0_d_83_72")} - x="21" - y="21" - width="96.207" - height="96.207" - filterUnits="userSpaceOnUse" - colorInterpolationFilters="sRGB" - > - <feFlood floodOpacity="0" result="BackgroundImageFix" /> - <feColorMatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - result="hardAlpha" - /> - <feMorphology - radius="2" - operator="dilate" - in="SourceAlpha" - result="effect1_dropShadow_83_72" - /> - <feOffset /> - <feGaussianBlur stdDeviation="5" /> - <feComposite in2="hardAlpha" operator="out" /> - <feColorMatrix - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" - /> - <feBlend - mode="normal" - in2="BackgroundImageFix" - result="effect1_dropShadow_83_72" - /> - <feBlend - mode="normal" - in="SourceGraphic" - in2="effect1_dropShadow_83_72" - result="shape" - /> - </filter> - <pattern - id={svgId(id, "pattern0")} - patternContentUnits="objectBoundingBox" - width="1" - height="1" - > - <use - xlinkHref={`#${svgId(id, "image0_83_72")}`} - transform="scale(0.00520833)" - /> - </pattern> - <image - id={svgId(id, "image0_83_72")} - width="192" - height="192" - xlinkHref={image} - /> - </defs> - </svg> - ); -}; - -export const MemoryWithImages2: React.FC< - { images: string[]; id: string } & React.SVGAttributes<SVGElement> -> = ({ images, id: _id, ...props }) => { - const id = "space-2-" + _id; - - return ( - <svg - viewBox="0 0 137 137" - fill="none" - xmlns="http://www.w3.org/2000/svg" - xmlnsXlink="http://www.w3.org/1999/xlink" - {...props} - > - <g clipPath={`url(#${svgId(id, "clip0_80_62")})`}> - <g filter={`url(#${svgId(id, "filter0_d_80_62")})`}> - <rect - x="7" - y="51.5427" - width="72.207" - height="72.207" - rx="10" - transform="rotate(-24.1922 7 51.5427)" - fill="#F4F3F2" - /> - </g> - <rect - x="26.2664" - y="58.9253" - width="45" - height="45" - transform="rotate(-24.2 26.2664 58.9253)" - fill={`url(#${svgId(id, "pattern0")})`} - /> - <g filter={`url(#${svgId(id, "filter1_d_80_62")})`}> - <rect - x="59.9409" - y="42.2124" - width="72.207" - height="72.207" - rx="10" - transform="rotate(10.2301 59.9409 42.2124)" - fill="#ffffff" - /> - </g> - <rect - x="71.2952" - y="58.4851" - width="45" - height="45" - transform="rotate(10.23 71.2952 58.4851)" - fill={`url(#${svgId(id, "pattern1")})`} - /> - </g> - <defs> - <filter - id={svgId(id, "filter0_d_80_62")} - x="-1.78271" - y="13.1697" - width="113.021" - height="113.021" - filterUnits="userSpaceOnUse" - colorInterpolationFilters="sRGB" - > - <feFlood floodOpacity="0" result="BackgroundImageFix" /> - <feColorMatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - result="hardAlpha" - /> - <feMorphology - radius="2" - operator="dilate" - in="SourceAlpha" - result="effect1_dropShadow_80_62" - /> - <feOffset /> - <feGaussianBlur stdDeviation="5" /> - <feComposite in2="hardAlpha" operator="out" /> - <feColorMatrix - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" - /> - <feBlend - mode="normal" - in2="BackgroundImageFix" - result="effect1_dropShadow_80_62" - /> - <feBlend - mode="normal" - in="SourceGraphic" - in2="effect1_dropShadow_80_62" - result="shape" - /> - </filter> - <pattern - id={svgId(id, "pattern0")} - patternContentUnits="objectBoundingBox" - width="1" - height="1" - > - <use - xlinkHref={`#${svgId(id, "image0_80_62")}`} - transform="scale(0.0111111)" - /> - </pattern> - <filter - id={svgId(id, "filter1_d_80_62")} - x="36.7322" - y="31.8276" - width="104.652" - height="104.653" - filterUnits="userSpaceOnUse" - colorInterpolationFilters="sRGB" - > - <feFlood floodOpacity="0" result="BackgroundImageFix" /> - <feColorMatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - result="hardAlpha" - /> - <feMorphology - radius="2" - operator="dilate" - in="SourceAlpha" - result="effect1_dropShadow_80_62" - /> - <feOffset /> - <feGaussianBlur stdDeviation="5" /> - <feComposite in2="hardAlpha" operator="out" /> - <feColorMatrix - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" - /> - <feBlend - mode="normal" - in2="BackgroundImageFix" - result="effect1_dropShadow_80_62" - /> - <feBlend - mode="normal" - in="SourceGraphic" - in2="effect1_dropShadow_80_62" - result="shape" - /> - </filter> - <pattern - id={svgId(id, "pattern1")} - patternContentUnits="objectBoundingBox" - width="1" - height="1" - > - <use - xlinkHref={`#${svgId(id, "image1_80_62")}`} - transform="scale(0.00520833)" - /> - </pattern> - <clipPath id={svgId(id, "clip0_80_62")}> - <rect width="137" height="137" fill="white" /> - </clipPath> - <image - id={svgId(id, "image0_80_62")} - width="90" - height="90" - xlinkHref={images[0]} - /> - <image - id={svgId(id, "image1_80_62")} - width="192" - height="192" - xlinkHref={images[1]} - /> - </defs> - </svg> - ); -}; - -export const MemoryWithImages3: React.FC< - { images: string[]; id: string } & React.SVGAttributes<SVGElement> -> = ({ images, id: _id, ...props }) => { - const id = "space-3-" + _id; - - return ( - <svg - viewBox="0 0 137 137" - fill="none" - xmlns="http://www.w3.org/2000/svg" - xmlnsXlink="http://www.w3.org/1999/xlink" - {...props} - > - <g clipPath={`url(#${svgId(id, "clip0_79_36")})`}> - <g filter={`url(#${svgId(id, "filter0_d_79_36")})`}> - <rect - x="53.5242" - y="12" - width="72.207" - height="72.207" - rx="10" - transform="rotate(14.9009 53.5242 12)" - fill="white" - className="shadow-md" - /> - </g> - <rect - x="63.3663" - y="27.3052" - width="45" - height="45" - transform="rotate(14.9 63.3663 27.3052)" - fill={`url(#${svgId(id, "pattern0")})`} - /> - <g filter={`url(#${svgId(id, "filter1_d_79_36")})`}> - <rect - x="7" - y="51.5427" - width="72.207" - height="72.207" - rx="10" - transform="rotate(-24.1922 7 51.5427)" - fill="#F4F3F2" - className="shadow-md" - /> - </g> - <rect - x="26.2664" - y="58.9253" - width="45" - height="45" - transform="rotate(-24.2 26.2664 58.9253)" - fill={`url(#${svgId(id, "pattern1")})`} - /> - <g filter={`url(#${svgId(id, "filter2_d_79_36")})`}> - <rect - x="59.9409" - y="42.2124" - width="72.207" - height="72.207" - rx="10" - transform="rotate(10.2301 59.9409 42.2124)" - fill="#E7E5E4" - className="shadow-md" - /> - </g> - <rect - x="71.2952" - y="58.4851" - width="45" - height="45" - transform="rotate(10.23 71.2952 58.4851)" - fill={`url(#${svgId(id, "pattern2")})`} - /> - </g> - <defs> - <filter - id={svgId(id, "filter0_d_79_36")} - x="25.189" - y="2.23267" - width="107.881" - height="107.881" - filterUnits="userSpaceOnUse" - colorInterpolationFilters="sRGB" - > - <feFlood floodOpacity="0" result="BackgroundImageFix" /> - <feColorMatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - result="hardAlpha" - /> - <feMorphology - radius="2" - operator="dilate" - in="SourceAlpha" - result="effect1_dropShadow_79_36" - /> - <feOffset /> - <feGaussianBlur stdDeviation="5" /> - <feComposite in2="hardAlpha" operator="out" /> - <feColorMatrix - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" - /> - <feBlend - mode="normal" - in2="BackgroundImageFix" - result="effect1_dropShadow_79_36" - /> - <feBlend - mode="normal" - in="SourceGraphic" - in2="effect1_dropShadow_79_36" - result="shape" - /> - </filter> - <pattern - id={svgId(id, "pattern0")} - patternContentUnits="objectBoundingBox" - width="1" - height="1" - > - <use - xlinkHref={`#${svgId(id, "image0_79_36")}`} - transform="scale(0.0111111)" - /> - </pattern> - <filter - id={svgId(id, "filter1_d_79_36")} - x="-1.78271" - y="13.1697" - width="113.021" - height="113.021" - filterUnits="userSpaceOnUse" - colorInterpolationFilters="sRGB" - > - <feFlood floodOpacity="0" result="BackgroundImageFix" /> - <feColorMatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - result="hardAlpha" - /> - <feMorphology - radius="2" - operator="dilate" - in="SourceAlpha" - result="effect1_dropShadow_79_36" - /> - <feOffset /> - <feGaussianBlur stdDeviation="5" /> - <feComposite in2="hardAlpha" operator="out" /> - <feColorMatrix - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" - /> - <feBlend - mode="normal" - in2="BackgroundImageFix" - result="effect1_dropShadow_79_36" - /> - <feBlend - mode="normal" - in="SourceGraphic" - in2="effect1_dropShadow_79_36" - result="shape" - /> - </filter> - <pattern - id={svgId(id, "pattern1")} - patternContentUnits="objectBoundingBox" - width="1" - height="1" - > - <use - xlinkHref={`#${svgId(id, "image1_79_36")}`} - transform="scale(0.0111111)" - /> - </pattern> - <filter - id={svgId(id, "filter2_d_79_36")} - x="36.7322" - y="31.8276" - width="104.652" - height="104.653" - filterUnits="userSpaceOnUse" - colorInterpolationFilters="sRGB" - > - <feFlood floodOpacity="0" result="BackgroundImageFix" /> - <feColorMatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - result="hardAlpha" - /> - <feMorphology - radius="2" - operator="dilate" - in="SourceAlpha" - result="effect1_dropShadow_79_36" - /> - <feOffset /> - <feGaussianBlur stdDeviation="5" /> - <feComposite in2="hardAlpha" operator="out" /> - <feColorMatrix - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" - /> - <feBlend - mode="normal" - in2="BackgroundImageFix" - result="effect1_dropShadow_79_36" - /> - <feBlend - mode="normal" - in="SourceGraphic" - in2="effect1_dropShadow_79_36" - result="shape" - /> - </filter> - <pattern - id={svgId(id, "pattern2")} - patternContentUnits="objectBoundingBox" - width="1" - height="1" - > - <use - xlinkHref={`#${svgId(id, "image2_79_36")}`} - transform="scale(0.00520833)" - /> - </pattern> - <clipPath id={svgId(id, "clip0_79_36")}> - <rect width="137" height="137" fill="white" /> - </clipPath> - <image - id={svgId(id, "image0_79_36")} - width="90" - height="90" - xlinkHref={images[0]} - /> - <image - id={svgId(id, "image1_79_36")} - width="90" - height="90" - xlinkHref={images[1]} - /> - <image - id={svgId(id, "image2_79_36")} - width="192" - height="192" - xlinkHref={images[2]} - /> - </defs> - </svg> - ); -}; diff --git a/apps/web/src/assets/Note.tsx b/apps/web/src/assets/Note.tsx deleted file mode 100644 index e69de29b..00000000 --- a/apps/web/src/assets/Note.tsx +++ /dev/null diff --git a/apps/web/src/components/ChatMessage.tsx b/apps/web/src/components/ChatMessage.tsx deleted file mode 100644 index 58ef9870..00000000 --- a/apps/web/src/components/ChatMessage.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React, { useEffect } from "react"; -import { motion } from "framer-motion"; -import { ArrowUpRight, Globe, Text } from "lucide-react"; -import { convertRemToPixels } from "@/lib/utils"; -import { SpaceIcon } from "@/assets/Memories"; -import Markdown from "react-markdown"; -import { ChatHistory } from "../../types/memory"; - -export function ChatAnswer({ - children: message, - sources, - loading = false, -}: { - children: string; - sources?: ChatHistory["answer"]["sources"]; - loading?: boolean; -}) { - return ( - <div className="flex h-max w-full flex-col items-start gap-5"> - {loading ? ( - <MessageSkeleton /> - ) : ( - <div className="chat-answer h-full w-full text-lg text-white/60"> - <Markdown>{message}</Markdown> - </div> - )} - {!loading && sources && sources?.length > 0 && ( - <> - <h1 className="animate-fade-in text-rgray-12 text-md flex items-center justify-center gap-2 opacity-0 [animation-duration:1s]"> - <SpaceIcon className="h-6 w-6 -translate-y-[2px]" /> - Related Memories - </h1> - <div className="animate-fade-in -mt-3 flex items-center justify-start gap-1 opacity-0 [animation-duration:1s]"> - {sources?.map((source) => - source.isNote ? ( - <button className="bg-rgray-3 flex items-center justify-center gap-2 rounded-full py-1 pl-2 pr-3 text-sm"> - <Text className="h-4 w-4" /> - {source.source} - </button> - ) : ( - <a - className="bg-rgray-3 flex items-center justify-center gap-2 rounded-full py-1 pl-2 pr-3 text-sm" - key={source.source} - href={source.source} - target="_blank" - > - <Globe className="h-4 w-4" /> - {cleanUrl(source.source)} - </a> - ), - )} - </div> - </> - )} - </div> - ); -} - -export function ChatQuestion({ children }: { children: string }) { - return ( - <div - className={`text-rgray-12 h-max w-full text-left ${children.length > 200 ? "text-xl" : "text-2xl"}`} - > - {children} - </div> - ); -} - -export function ChatMessage({ - children, - isLast = false, - index, -}: { - children: React.ReactNode | React.ReactNode[]; - isLast?: boolean; - index: number; -}) { - const messageRef = React.useRef<HTMLDivElement>(null); - - useEffect(() => { - if (!isLast) return; - messageRef.current?.parentElement?.scrollTo({ - top: messageRef.current?.offsetTop, - behavior: "smooth", - }); - }, []); - - return ( - <motion.div - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ - type: "tween", - duration: 0.5, - }} - ref={messageRef} - className={`${index === 0 ? "pt-16" : "pt-28"} flex h-max w-full resize-y flex-col items-start justify-start gap-5 transition-[height] ${isLast ? "min-h-screen pb-[40vh]" : "h-max"}`} - > - {children} - </motion.div> - ); -} - -function MessageSkeleton() { - return ( - <div className="animate-fade-in flex w-full flex-col items-start gap-3 opacity-0 [animation-delay:0.5s] [animation-duration:1s]"> - <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div> - <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div> - <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div> - <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div> - <div className="bg-rgray-5 h-6 w-[70%] animate-pulse rounded-md text-lg"></div> - </div> - ); -} - -function cleanUrl(url: string) { - if (url.startsWith("https://")) { - url = url.slice(8); - } else if (url.startsWith("http://")) { - url = url.slice(7); - } - - if (url.endsWith("/")) { - url = url.slice(0, -1); - } - - return url; -} diff --git a/apps/web/src/components/Main-2.tsx b/apps/web/src/components/Main-2.tsx deleted file mode 100644 index 1b602712..00000000 --- a/apps/web/src/components/Main-2.tsx +++ /dev/null @@ -1,709 +0,0 @@ -"use client"; -import { MemoryDrawer } from "./MemoryDrawer"; -import useViewport from "@/hooks/useViewport"; -import { AnimatePresence } from "framer-motion"; -import { cn } from "@/lib/utils"; - -import { Editor } from "novel"; -import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { - MemoryWithImage, - MemoryWithImages3, - MemoryWithImages2, -} from "@/assets/MemoryWithImages"; -import { Input, InputWithIcon } from "./ui/input"; -import { - ArrowUpRight, - Edit3, - Loader, - Minus, - MoreHorizontal, - Plus, - Search, - Sparkles, - Text, - Trash2, -} from "lucide-react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "./ui/dropdown-menu"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { Variant, useAnimate, motion } from "framer-motion"; -import { SearchResult, useMemory } from "@/contexts/MemoryContext"; -import { SpaceIcon } from "@/assets/Memories"; -import { Dialog, DialogContent } from "./ui/dialog"; -import useTouchHold from "@/hooks/useTouchHold"; -import { DialogTrigger } from "@radix-ui/react-dialog"; -import { - AddExistingMemoryToSpace, - AddMemoryPage, - NoteAddPage, - SpaceAddPage, -} from "./Sidebar/AddMemoryDialog"; -import { ExpandedSpace } from "./Sidebar/ExpandedSpace"; -import { StoredContent, StoredSpace } from "@/server/db/schema"; -import { useDebounce } from "@/hooks/useDebounce"; -import { NoteEdit } from "./Sidebar/EditNoteDialog"; -import DeleteConfirmation from "./Sidebar/DeleteConfirmation"; - -import { ProfileDrawer } from "./ProfileDrawer"; - -function supportsDVH() { - try { - return CSS.supports("height: 100dvh"); - } catch { - return false; - } -} - -function pseudoRandomizeColorWithName(name: string) { - const colorsAvailable = [ - "99e9f2", - "a5d8ff", - "d0bfff", - "eebefa", - "fcc2d7", - "b2f2bb", - "96f2d7", - "ffec99", - "ffd8a8", - "ffc9c9", - ]; - - const colorIndex = - name - .split("") - .map((char) => char.charCodeAt(0)) - .reduce((acc, charCode) => acc + charCode, 0) % colorsAvailable.length; - - return colorsAvailable[colorIndex]; -} - -export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { - const { width } = useViewport(); - - const [parent, enableAnimations] = useAutoAnimate(); - const { spaces, deleteSpace, freeMemories, search } = useMemory(); - - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [addMemoryState, setAddMemoryState] = useState< - "page" | "note" | "space" | "existing-memory" | null - >(null); - - const [expandedSpace, setExpandedSpace] = useState<number | null>(null); - - const [searchQuery, setSearcyQuery] = useState(""); - const [searchLoading, setSearchLoading] = useState(false); - const query = useDebounce(searchQuery, 500); - - const [searchResults, setSearchResults] = useState<SearchResult[]>([]); - - useEffect(() => { - const q = query.trim(); - if (q.length < 1) { - setSearchResults([]); - return; - } - - setSearchLoading(true); - - (async () => { - setSearchResults(await search(q)); - setSearchLoading(false); - })(); - }, [query]); - - // useEffect(() => { - // if (!isOpen) { - // setExpandedSpace(null); - // } - // }, [isOpen]); - - if (expandedSpace) { - return ( - <ExpandedSpace - spaceId={expandedSpace} - back={() => setExpandedSpace(null)} - // close={() => setExpandedSpace(null)} - /> - ); - } - - return ( - <> - <AnimatePresence mode="wait"> - <main - data-sidebar-open={sidebarOpen} - className={cn( - "sidebar relative flex w-full flex-col items-end gap-5 overflow-auto bg-[#FFF] px-5 pt-5 transition-[padding-left,padding-top,padding-right] delay-200 duration-200 md:items-center md:gap-10 md:px-72 [&[data-sidebar-open='true']]:pr-10 [&[data-sidebar-open='true']]:delay-0 md:[&[data-sidebar-open='true']]:pl-[calc(2.5rem+30vw)]", - )} - > - <div className="mt-16 w-full"> - <div className="flex justify-between gap-4"> - <h1 className="w-full text-3xl font-medium tracking-tight"> - Your Memories - </h1> - <div className="flex w-full"> - <AddMemoryModal type={addMemoryState}> - <DropdownMenu - open={isDropdownOpen} - onOpenChange={setIsDropdownOpen} - > - <DropdownMenuTrigger asChild> - <button className="focus-visible:ring-rgray-7 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition hover:bg-stone-200 focus-visible:bg-white focus-visible:outline-none focus-visible:ring-2"> - <Plus className="mr-2 h-5 w-5" /> - Add - </button> - </DropdownMenuTrigger> - <DropdownMenuContent - onCloseAutoFocus={(e) => e.preventDefault()} - > - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("page"); - }} - > - <Sparkles className="mr-2 h-4 w-4" /> - Page to Memory - </DropdownMenuItem> - </DialogTrigger> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("note"); - }} - > - <Text className="mr-2 h-4 w-4" /> - Note - </DropdownMenuItem> - </DialogTrigger> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("space"); - }} - > - <SpaceIcon className="mr-2 h-4 w-4" /> - Space - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </AddMemoryModal> - </div> - </div> - <InputWithIcon - placeholder="Search" - icon={ - searchLoading ? ( - <Loader className="h-5 w-5 animate-spin opacity-50" /> - ) : ( - <Search className="h-5 w-5 opacity-50" /> - ) - } - className="mt-4 w-full text-black" - value={searchQuery} - onChange={(e) => setSearcyQuery(e.target.value)} - /> - </div> - <div - ref={parent} - className="grid w-full grid-flow-row grid-cols-3 gap-4 px-2 py-5" - > - {typeof window !== "undefined" ? ( - query.trim().length > 0 ? ( - <> - {searchResults.map(({ type, space, memory }, i) => ( - <> - {type === "memory" && ( - <MemoryItem - {...memory!} - key={i} - onDelete={() => { - setSearchResults((prev) => - prev.filter((i) => i.memory?.id !== memory.id), - ); - }} - /> - )} - {type === "space" && ( - <SpaceItem - {...space!} - key={i} - onDelete={() => { - setSearchResults((prev) => - prev.filter((i) => i.space?.id !== space.id), - ); - deleteSpace(space.id); - }} - /> - )} - </> - ))} - </> - ) : ( - <> - {spaces.map((space) => ( - <SpaceItem - onDelete={() => deleteSpace(space.id)} - key={space.id} - onClick={() => setExpandedSpace(space.id)} - {...space} - /> - ))} - {freeMemories.map((m) => ( - <MemoryItem {...m} key={m.id} /> - ))} - </> - ) - ) : ( - <> - {Array.from({ - length: spaces.length + freeMemories.length, - }).map((_, i) => ( - <div className="h-32 w-full animate-pulse rounded-2xl bg-stone-300/50"></div> - ))} - </> - )} - </div> - <div className="absolute right-10 top-10 z-[100] block md:hidden"> - {width <= 768 && <ProfileDrawer />} - </div> - </main> - {width <= 768 && <MemoryDrawer />} - </AnimatePresence> - </> - ); -} - -export function MemoryItem( - props: StoredContent & { - onDelete?: () => void; - removeFromSpace?: () => Promise<void>; - }, -) { - const { id, title, image, type, url, onDelete, removeFromSpace } = props; - - const { deleteMemory } = useMemory(); - - const name = title - ? title.length > 20 - ? title.slice(0, 20) + "..." - : title - : "Untitled Memory"; - - const [isDialogOpen, setIsDialogOpen] = useState(false); - - const [moreDropdownOpen, setMoreDropdownOpen] = useState(false); - - const touchEventProps = useTouchHold({ - onHold() { - setMoreDropdownOpen(true); - }, - }); - return ( - <Dialog - open={type === "note" ? isDialogOpen : false} - onOpenChange={setIsDialogOpen} - > - <DialogTrigger asChild> - <button - onClick={() => (type === "page" ? window.open(url) : null)} - data-space-text - className="relative flex h-min select-none flex-col items-center justify-center gap-2 text-center font-normal focus-visible:outline-none" - > - <div - {...touchEventProps} - className="has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 flex h-32 w-full items-center justify-center rounded-2xl border-2 border-black/20 p-2 pb-4 shadow-sm ring-transparent transition duration-150 ease-out hover:scale-105 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" - style={{ - backgroundColor: `#${pseudoRandomizeColorWithName(name)}`, - }} - > - {type === "page" ? ( - <PageMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - removeFromSpace={removeFromSpace} - onDelete={() => { - deleteMemory(id); - onDelete?.(); - }} - url={url} - /> - ) : type === "note" ? ( - <NoteMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - removeFromSpace={removeFromSpace} - onEdit={() => setIsDialogOpen(true)} - onDelete={() => { - deleteMemory(id); - onDelete?.(); - }} - /> - ) : null} - - <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" ? ( - <Text - onClick={() => setIsDialogOpen(true)} - className="h-16 w-16" - /> - ) : ( - <></> - )} - </div> - </div> - {name} - </button> - </DialogTrigger> - <DialogContent className="w-max max-w-[auto]"> - <NoteEdit - onDelete={onDelete} - closeDialog={() => setIsDialogOpen(false)} - memory={props} - /> - </DialogContent> - </Dialog> - ); -} - -export function SpaceItem({ - name, - id, - onDelete, - onClick, -}: StoredSpace & { onDelete: () => void; onClick?: () => void }) { - const { cachedMemories } = useMemory(); - - const [itemRef, animateItem] = useAnimate(); - const { width } = useViewport(); - - const [moreDropdownOpen, setMoreDropdownOpen] = useState(false); - - const touchEventProps = useTouchHold({ - onHold() { - setMoreDropdownOpen(true); - }, - }); - - const spaceMemories = useMemo(() => { - return cachedMemories.filter((m) => m.space === id); - }, [cachedMemories]); - - const _name = name.length > 20 ? name.slice(0, 20) + "..." : name; - - return ( - <button - onClick={onClick} - data-space-text - className="relative flex h-min select-none flex-col items-center justify-center gap-2 text-center font-normal focus-visible:outline-none" - > - <motion.div - ref={itemRef} - {...touchEventProps} - className="has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 flex h-32 w-full items-center justify-center rounded-2xl border-2 border-black/20 p-2 pb-4 shadow-sm ring-transparent transition duration-150 ease-out hover:scale-105 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" - style={{ - backgroundColor: `#${pseudoRandomizeColorWithName(name)}`, - }} - > - <SpaceMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - onEdit={onClick} - onDelete={onDelete} - /> - {spaceMemories.length > 2 ? ( - <MemoryWithImages3 - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - images={ - spaceMemories - .map((c) => (c.type === "note" ? "/note.svg" : c.image)) - .reverse() as string[] - } - /> - ) : spaceMemories.length > 1 ? ( - <MemoryWithImages2 - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - images={ - spaceMemories - .map((c) => (c.type === "note" ? "/note.svg" : c.image)) - .reverse() as string[] - } - /> - ) : spaceMemories.length === 1 ? ( - <MemoryWithImage - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - image={ - spaceMemories[0].type === "note" - ? "/note.svg" - : spaceMemories[0].image! - } - /> - ) : ( - <div - onClick={onClick} - className="flex items-center justify-center gap-2" - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth="1.5" - stroke="currentColor" - className="h-8 w-8" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" - /> - </svg> - <span className="text-stone-800/80">Empty Space</span> - </div> - )} - </motion.div> - - {_name} - </button> - ); -} - -export function SpaceMoreButton({ - onDelete, - isOpen, - setIsOpen, - onEdit, -}: { - onDelete?: () => void; - isOpen?: boolean; - onEdit?: () => void; - setIsOpen?: (open: boolean) => void; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition hover:bg-white focus-visible:bg-white focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="h-5 w-5 text-black" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={onEdit}> - <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"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function PageMoreButton({ - onDelete, - isOpen, - setIsOpen, - url, - removeFromSpace, -}: { - onDelete?: () => void; - isOpen?: boolean; - url: string; - setIsOpen?: (open: boolean) => void; - removeFromSpace?: () => Promise<void>; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition hover:bg-white focus-visible:bg-white focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="h-5 w-5 text-black" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={() => window.open(url)}> - <ArrowUpRight - className="mr-2 h-4 w-4 scale-125" - strokeWidth={1.5} - /> - Open - </DropdownMenuItem> - {removeFromSpace && ( - <DropdownMenuItem onClick={removeFromSpace}> - <Minus className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Remove from space - </DropdownMenuItem> - )} - <DialogTrigger asChild> - <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function NoteMoreButton({ - onDelete, - isOpen, - setIsOpen, - onEdit, - removeFromSpace, -}: { - onDelete?: () => void; - isOpen?: boolean; - onEdit?: () => void; - setIsOpen?: (open: boolean) => void; - removeFromSpace?: () => Promise<void>; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition hover:bg-white focus-visible:bg-white focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="h-5 w-5 text-black" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={onEdit}> - <Edit3 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Edit - </DropdownMenuItem> - {removeFromSpace && ( - <DropdownMenuItem onClick={removeFromSpace}> - <Minus className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Remove from space - </DropdownMenuItem> - )} - <DialogTrigger asChild> - <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function AddMemoryModal({ - type, - children, - defaultSpaces, - onAdd, - data, -}: { - type: "page" | "note" | "space" | "existing-memory" | null; - children?: React.ReactNode | React.ReactNode[]; - defaultSpaces?: number[]; - data?: { - space?: { - title: string; - id: number; - }; - fromSpaces?: number[]; - notInSpaces?: number[]; - }; - onAdd?: (data?: StoredSpace | StoredContent | StoredContent[]) => void; -}) { - const [isDialogOpen, setIsDialogOpen] = useState(false); - - return ( - <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> - {children} - <DialogContent - onOpenAutoFocus={(e) => { - e.preventDefault(); - const novel = document.querySelector('[contenteditable="true"]') as - | HTMLDivElement - | undefined; - if (novel) { - novel.autofocus = false; - novel.onfocus = () => { - ( - document.querySelector("[data-modal-autofocus]") as - | HTMLInputElement - | undefined - )?.focus(); - novel.onfocus = null; - }; - } - ( - document.querySelector("[data-modal-autofocus]") as - | HTMLInputElement - | undefined - )?.focus(); - }} - className="w-max max-w-[auto]" - > - {type === "page" ? ( - <AddMemoryPage - onAdd={onAdd} - defaultSpaces={defaultSpaces} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "note" ? ( - <NoteAddPage - onAdd={onAdd} - defaultSpaces={defaultSpaces} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "space" ? ( - <SpaceAddPage - onAdd={onAdd} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "existing-memory" ? ( - <AddExistingMemoryToSpace - onAdd={onAdd} - fromSpaces={data?.fromSpaces} - notInSpaces={data?.notInSpaces} - space={data!.space!} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : ( - <></> - )} - </DialogContent> - </Dialog> - ); -} diff --git a/apps/web/src/components/Main.tsx b/apps/web/src/components/Main.tsx deleted file mode 100644 index 8ac52569..00000000 --- a/apps/web/src/components/Main.tsx +++ /dev/null @@ -1,533 +0,0 @@ -"use client"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { FilterSpaces } from "./Sidebar/FilterCombobox"; -import { Textarea2 } from "./ui/textarea"; -import { ArrowRight, ArrowUp } from "lucide-react"; -import { MemoryDrawer } from "./MemoryDrawer"; -import useViewport from "@/hooks/useViewport"; -import { AnimatePresence, motion } from "framer-motion"; -import { cn, countLines, getIdsFromSource } from "@/lib/utils"; -import { ChatHistory } from "../../types/memory"; -import { ChatAnswer, ChatMessage, ChatQuestion } from "./ChatMessage"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useMemory } from "@/contexts/MemoryContext"; - -import Image from "next/image"; -import { getMemoriesFromUrl } from "@/actions/db"; -import { ProfileDrawer } from "./ProfileDrawer"; - -function supportsDVH() { - try { - return CSS.supports("height: 100dvh"); - } catch { - return false; - } -} - -export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { - const searchParams = useSearchParams(); - - const [hide, setHide] = useState(false); - const [layout, setLayout] = useState<"chat" | "initial">("initial"); - const [value, setValue] = useState(""); - const { width } = useViewport(); - const [isAiLoading, setIsAiLoading] = useState(false); - - const { spaces } = useMemory(); - - // Variable to keep track of the chat history in this session - const [chatHistory, setChatHistory] = useState<ChatHistory[]>([]); - - const [toBeParsed, setToBeParsed] = useState(""); - - const textArea = useRef<HTMLDivElement>(null); - const main = useRef<HTMLDivElement>(null); - - const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]); - - const [isStreaming, setIsStreaming] = useState(false); - - useEffect(() => { - const search = searchParams.get("q"); - if (search && search.trim().length > 0) { - setValue(search); - onSend(); - //router.push("/"); - } - }, []); - - useEffect(() => { - // function onResize() { - // if (!main.current || !window.visualViewport) return; - // if ( - // window.visualViewport.height < window.innerHeight + 20 && - // window.visualViewport.height > window.innerHeight - 20 - // ) { - // setHide(false); - // window.scrollTo(0, 0); - // } else { - // setHide(true); - // window.scrollTo(0, document.body.scrollHeight); - // } - // } - // window.visualViewport?.addEventListener("resize", onResize); - // return () => { - // window.visualViewport?.removeEventListener("resize", onResize); - // }; - }, []); - - useEffect(() => { - // Define a function to try parsing the accumulated data - const tryParseAccumulatedData = () => { - // Attempt to parse the "toBeParsed" state as JSON - try { - // Split the accumulated data by the known delimiter "\n\n" - const parts = toBeParsed.split("\n\n"); - let remainingData = ""; - - // Process each part to extract JSON objects - parts.forEach((part, index) => { - try { - const parsedPart = JSON.parse(part.replace("data: ", "")); // Try to parse the part as JSON - - // If the part is the last one and couldn't be parsed, keep it to accumulate more data - if (index === parts.length - 1 && !parsedPart) { - remainingData = part; - } else if (parsedPart && parsedPart.response) { - // Append to chat history in this way: - // If the last message was from the model, append to that message - // Otherwise, Start a new message from the model and append to that - if (chatHistory.length > 0) { - setChatHistory((prev: ChatHistory[]) => { - const lastMessage = prev[prev.length - 1]; - const newParts = [ - ...lastMessage.answer.parts, - { text: parsedPart.response }, - ]; - return [ - ...prev.slice(0, prev.length - 1), - { - ...lastMessage, - answer: { - parts: newParts, - sources: lastMessage.answer.sources, - }, - }, - ]; - }); - } else { - } - } - } catch (error) { - // If parsing fails and it's not the last part, it's a malformed JSON - if (index !== parts.length - 1) { - console.error("Malformed JSON part: ", part); - } else { - // If it's the last part, it may be incomplete, so keep it - remainingData = part; - } - } - }); - - // Update the toBeParsed state to only contain the unparsed remainder - if (remainingData !== toBeParsed) { - setToBeParsed(remainingData); - } - } catch (error) { - console.error("Error parsing accumulated data: ", error); - } - }; - - // Call the parsing function if there's data to be parsed - if (toBeParsed) { - tryParseAccumulatedData(); - } - }, [toBeParsed]); - - const modifyChatHistory = useCallback((old: ChatHistory[]) => { - const final: { role: "user" | "model"; parts: { text: string }[] }[] = []; - old.forEach((chat) => { - final.push({ - role: "user", - parts: [{ text: chat.question }], - }); - final.push({ - role: "model", - parts: chat.answer.parts.map((part) => ({ text: part.text })), - }); - }); - - return final; - }, []); - - const getSearchResults = async () => { - setIsAiLoading(true); - - const _value = value.trim(); - setValue(""); - - setChatHistory((prev) => [ - ...prev, - { - question: _value, - answer: { - parts: [], - sources: [], - }, - }, - ]); - - const sourcesResponse = await fetch( - `/api/chat?sourcesOnly=true&q=${_value}`, - { - method: "POST", - body: JSON.stringify({ - chatHistory: modifyChatHistory(chatHistory), - }), - }, - ); - - console.log("sources", sourcesResponse); - - const sourcesInJson = - getIdsFromSource( - ( - (await sourcesResponse.json()) as { - ids: string[]; - } - ).ids, - ) ?? []; - - const notesInSources = sourcesInJson.filter((urls) => - urls.startsWith("https://notes.supermemory.dhr.wtf/"), - ); - const nonNotes = sourcesInJson.filter((i) => !notesInSources.includes(i)); - - const fetchedTitles = await getMemoriesFromUrl(notesInSources); - - const sources = [ - ...nonNotes.map((n) => ({ isNote: false, source: n ?? "<unnamed>" })), - ...fetchedTitles.map((n) => ({ - isNote: true, - source: n.title ?? "<unnamed>", - })), - ]; - - setIsAiLoading(false); - setChatHistory((prev) => { - const lastMessage = prev[prev.length - 1]; - return [ - ...prev.slice(0, prev.length - 1), - { - ...lastMessage, - answer: { - parts: lastMessage.answer.parts, - sources, - }, - }, - ]; - }); - - const actualSelectedSpaces = selectedSpaces.map( - (space) => spaces.find((s) => s.id === space)?.name ?? "", - ); - - const response = await fetch( - `/api/chat?q=${_value}&spaces=${actualSelectedSpaces.join(",")}`, - { - method: "POST", - body: JSON.stringify({ - chatHistory: modifyChatHistory(chatHistory), - }), - }, - ); - - if (response.status !== 200) { - setIsAiLoading(false); - return; - } - - setIsStreaming(true); - - if (response.body) { - let reader = response.body?.getReader(); - let decoder = new TextDecoder("utf-8"); - let result = ""; - - // @ts-ignore - reader.read().then(function processText({ done, value }) { - if (done) { - setIsAiLoading(false); - setToBeParsed(""); - - return; - } - setToBeParsed((prev) => prev + decoder.decode(value)); - - return reader?.read().then(processText); - }); - } - }; - - const onSend = () => { - if (value.trim().length < 1) return; - setLayout("chat"); - getSearchResults(); - }; - - function onValueChange(e: React.ChangeEvent<HTMLTextAreaElement>) { - const value = e.target.value; - setValue(value); - const lines = countLines(e.target); - e.target.rows = Math.min(5, lines); - } - - return ( - <> - <AnimatePresence mode="wait"> - {layout === "chat" ? ( - <Chat - key="chat" - isLoading={isAiLoading} - chatHistory={chatHistory} - sidebarOpen={sidebarOpen} - askQuestion={onSend} - setValue={setValue} - value={value} - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - ) : ( - <main - key="intial" - data-sidebar-open={sidebarOpen} - ref={main} - className={cn( - "sidebar relative flex w-full flex-col items-end justify-center gap-5 px-5 pt-5 transition-[padding-left,padding-top,padding-right] delay-200 duration-200 md:items-center md:gap-10 md:px-72 [&[data-sidebar-open='true']]:pr-10 [&[data-sidebar-open='true']]:delay-0 md:[&[data-sidebar-open='true']]:pl-[calc(2.5rem+30vw)]", - hide ? "" : "main-hidden", - )} - > - <Image - className="absolute right-10 top-10 hidden rounded-md md:block" - src="/icons/logo_bw_without_bg.png" - alt="Smort logo" - width={50} - height={50} - /> - <div className="absolute right-10 top-10 block md:hidden"> - {width <= 768 && <ProfileDrawer hide={hide} />} - </div> - <h1 className="text-rgray-11 mt-auto w-full text-center text-3xl font-bold tracking-tight md:mt-0"> - Ask your second brain - </h1> - - <FilterSpaces - name={"Filter"} - onClose={() => { - textArea.current?.querySelector("textarea")?.focus(); - }} - side="top" - align="start" - className="mr-auto bg-[#252525] md:hidden" - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - <Textarea2 - ref={textArea} - className="bg-rgray-2 h-auto w-full flex-row items-start justify-center overflow-auto px-3 md:hidden md:items-center md:justify-center" - textAreaProps={{ - placeholder: "Ask your SuperMemory...", - className: - "overflow-auto h-auto p-3 md:resize-none text-lg w-auto resize-y text-rgray-11 w-full", - value, - rows: 1, - autoFocus: true, - onChange: onValueChange, - onKeyDown: (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - onSend(); - } - }, - }} - > - <div className="text-rgray-11/70 ml-auto mt-auto flex h-full w-min items-center justify-center pb-3 pr-2 md:hidden"> - <FilterSpaces - name={"Filter"} - onClose={() => { - textArea.current?.querySelector("textarea")?.focus(); - }} - className="hidden md:flex" - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - <button - onClick={onSend} - disabled={value.trim().length < 1} - className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent transition-[filter] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" - > - <ArrowUp className="h-5 w-5" /> - </button> - </div> - </Textarea2> - - <Textarea2 - ref={textArea} - exit={{ - opacity: 0, - y: 50, - }} - transition={{ - type: "tween", - duration: 0.2, - }} - textAreaProps={{ - placeholder: "Ask your second brain...", - className: - "h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-2 pt-2 md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]", - value, - autoFocus: true, - onChange: (e) => setValue(e.target.value), - onKeyDown: (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - onSend(); - } - }, - }} - className="hidden md:flex" - > - <div className="text-rgray-11/70 flex h-full w-fit items-center justify-center pl-0 md:w-full md:p-2"> - <FilterSpaces - name={"Filter"} - onClose={() => { - textArea.current?.querySelector("textarea")?.focus(); - }} - className="hidden md:flex" - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - <button - onClick={onSend} - disabled={value.trim().length < 1} - className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:ml-auto md:mt-0" - > - <ArrowRight className="h-5 w-5" /> - </button> - </div> - </Textarea2> - </main> - )} - {width <= 768 && <MemoryDrawer hide={hide} />} - </AnimatePresence> - </> - ); -} - -export function Chat({ - sidebarOpen, - chatHistory, - isLoading = false, - askQuestion, - setValue, - value, - selectedSpaces, - setSelectedSpaces, -}: { - sidebarOpen: boolean; - isLoading?: boolean; - chatHistory: ChatHistory[]; - askQuestion: () => void; - setValue: (value: string) => void; - value: string; - selectedSpaces: number[]; - setSelectedSpaces: React.Dispatch<React.SetStateAction<number[]>>; -}) { - const textArea = useRef<HTMLDivElement>(null); - - function onValueChange(e: React.ChangeEvent<HTMLTextAreaElement>) { - const value = e.target.value; - setValue(value); - const lines = countLines(e.target); - e.target.rows = Math.min(5, lines); - } - - const { width } = useViewport(); - - return ( - <main - data-sidebar-open={sidebarOpen} - className={cn( - "sidebar relative flex w-full flex-col items-end gap-5 px-5 pt-5 transition-[padding-left,padding-top,padding-right] delay-200 duration-200 md:items-center md:gap-10 md:px-72 [&[data-sidebar-open='true']]:pr-10 [&[data-sidebar-open='true']]:delay-0 md:[&[data-sidebar-open='true']]:pl-[calc(2.5rem+30vw)]", - )} - > - <div className="absolute right-10 top-10 z-[100] block md:hidden"> - {width <= 768 && <ProfileDrawer />} - </div> - <div className="scrollbar-none h-[70vh] w-full overflow-y-auto px-2 md:h-screen md:px-5"> - {chatHistory.map((msg, i) => ( - <ChatMessage index={i} key={i} isLast={i === chatHistory.length - 1}> - <ChatQuestion>{msg.question}</ChatQuestion> - <ChatAnswer - loading={i === chatHistory.length - 1 ? isLoading : false} - sources={msg.answer.sources} - > - {msg.answer.parts - .map((part) => part.text) - .join("") - .replace("</s>", "")} - </ChatAnswer> - </ChatMessage> - ))} - </div> - <div className="from-rgray-2 via-rgray-2 to-rgray-2/0 absolute bottom-0 left-0 w-full bg-gradient-to-t" /> - <div - data-sidebar-open={sidebarOpen} - className="absolute flex w-full items-center justify-center" - > - <div className="animate-from-top bottom-padding fixed left-1/2 mt-auto flex w-[90%] -translate-x-1/2 flex-col items-center justify-center gap-2 md:bottom-10 md:left-[auto] md:w-[50%] md:translate-x-0"> - <FilterSpaces - name={"Filter"} - onClose={() => { - textArea.current?.querySelector("textarea")?.focus(); - }} - side="top" - align="start" - className="mr-auto bg-[#252525]" - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - <Textarea2 - ref={textArea} - className="bg-rgray-2 h-auto w-full flex-row items-start justify-center overflow-auto px-3 md:items-center md:justify-center" - textAreaProps={{ - placeholder: "Ask your SuperMemory...", - className: - "overflow-auto h-auto p-3 md:resize-none text-lg w-auto resize-y text-rgray-11 w-full", - value, - rows: 1, - autoFocus: true, - onChange: onValueChange, - onKeyDown: (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - askQuestion(); - } - }, - }} - > - <div className="text-rgray-11/70 ml-auto mt-auto flex h-full w-min items-center justify-center pb-3 pr-2"> - <button - onClick={askQuestion} - disabled={value.trim().length < 1} - className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent transition-[filter] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" - > - <ArrowUp className="h-5 w-5" /> - </button> - </div> - </Textarea2> - </div> - </div> - </main> - ); -} diff --git a/apps/web/src/components/MemoryDrawer.tsx b/apps/web/src/components/MemoryDrawer.tsx deleted file mode 100644 index 14283281..00000000 --- a/apps/web/src/components/MemoryDrawer.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useRef, useState } from "react"; -import { Drawer, DrawerContent, DrawerOverlay } from "./ui/drawer"; -import { MemoryIcon } from "@/assets/Memories"; -import { cn } from "@/lib/utils"; -import { MemoriesBar } from "./Sidebar/MemoriesBar"; - -export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> { - hide?: boolean; -} - -export function MemoryDrawer({ className, hide = false, ...props }: Props) { - const [activeSnapPoint, setActiveSnapPoint] = useState< - number | null | string - >(0.1); - - return ( - <Drawer - snapPoints={[0.1, 0.9]} - activeSnapPoint={activeSnapPoint} - shouldScaleBackground={false} - setActiveSnapPoint={setActiveSnapPoint} - open={true} - dismissible={false} - modal={false} - > - <DrawerContent - overlay={false} - data-expanded={activeSnapPoint === 0.9} - className={cn( - "border-rgray-6 DrawerContent data-[expanded=true]:bg-rgray-3 h-full w-screen border transition-[background] focus-visible:outline-none", - hide ? "hidden" : "", - )} - handle={false} - > - <button - onClick={() => - setActiveSnapPoint((prev) => (prev === 0.9 ? 0.1 : 0.9)) - } - className="bg-rgray-4 border-rgray-6 text-rgray-11 absolute left-1/2 top-0 flex w-fit -translate-x-1/2 -translate-y-1/2 items-center justify-center gap-2 rounded-md border px-3 py-2" - > - <MemoryIcon className="h-7 w-7" /> - Memories - </button> - <div className="h-full w-full overflow-y-auto"> - <MemoriesBar isOpen={true} /> - </div> - </DrawerContent> - <DrawerOverlay className="relative bg-transparent" /> - </Drawer> - ); -} diff --git a/apps/web/src/components/ProfileDrawer.tsx b/apps/web/src/components/ProfileDrawer.tsx deleted file mode 100644 index bdb32e03..00000000 --- a/apps/web/src/components/ProfileDrawer.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useRef, useState } from "react"; -import { - Drawer, - DrawerContent, - DrawerOverlay, - DrawerTrigger, -} from "./ui/drawer"; -import { cn } from "@/lib/utils"; -import { SettingsTab } from "./Sidebar/SettingsTab"; -import { useSession } from "next-auth/react"; - -export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> { - hide?: boolean; -} - -export function ProfileDrawer({ className, hide = false, ...props }: Props) { - const { data: session } = useSession(); - - return ( - <Drawer snapPoints={[0.9]} shouldScaleBackground={false}> - <DrawerTrigger> - <img - src={session?.user?.image ?? "/icons/white_without_bg.png"} - className="h-10 w-10 rounded-full" - /> - </DrawerTrigger> - <DrawerContent - overlay={false} - className={cn( - "border-rgray-6 DrawerContent data-[expanded=true]:bg-rgray-3 z-[101] h-full w-screen border bg-white transition-[background] focus-visible:outline-none", - hide ? "hidden" : "", - )} - > - <div className="h-[85vh] w-full overflow-y-auto"> - <SettingsTab open={true} /> - </div> - </DrawerContent> - </Drawer> - ); -} diff --git a/apps/web/src/components/SearchResults.tsx b/apps/web/src/components/SearchResults.tsx deleted file mode 100644 index d348814e..00000000 --- a/apps/web/src/components/SearchResults.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -import React from "react"; -import { Card, CardContent } from "./ui/card"; -import Markdown from "react-markdown"; -import remarkGfm from "remark-gfm"; - -function SearchResults({ - aiResponse, - sources, -}: { - aiResponse: string; - sources: string[]; -}) { - return ( - <div - style={{ - backgroundImage: `linear-gradient(to right, #E5D9F2, #CDC1FF)`, - }} - className="mx-auto mt-4 w-full max-w-2xl space-y-6 rounded-xl border px-4 py-6" - > - <div className="text-start"> - <div className="text-xl text-black"> - <Markdown remarkPlugins={[remarkGfm]}> - {aiResponse.replace("</s>", "")} - </Markdown> - </div> - </div> - <div className="grid gap-6"> - {sources.map((value, index) => ( - <Card key={index}> - <CardContent className="space-y-2">{value}</CardContent> - </Card> - ))} - </div> - </div> - ); -} - -export default SearchResults; diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx deleted file mode 100644 index 64147b1e..00000000 --- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx +++ /dev/null @@ -1,480 +0,0 @@ -import { Editor } from "novel"; -import { - DialogClose, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "../ui/dialog"; -import { Input } from "../ui/input"; -import { Label } from "../ui/label"; -import { Markdown } from "tiptap-markdown"; -import { useEffect, useRef, useState } from "react"; -import { FilterMemories, FilterSpaces } from "./FilterCombobox"; -import { useMemory } from "@/contexts/MemoryContext"; -import { Loader, Plus, X } from "lucide-react"; -import { StoredContent, StoredSpace } from "@/server/db/schema"; -import { cleanUrl } from "@/lib/utils"; -import { motion } from "framer-motion"; -import { getMetaData } from "@/server/helpers"; - -export function AddMemoryPage({ - closeDialog, - defaultSpaces, - onAdd, -}: { - closeDialog: () => void; - defaultSpaces?: number[]; - onAdd?: (addedData: StoredContent) => void; -}) { - const { addMemory } = useMemory(); - - const [loading, setLoading] = useState(false); - const [url, setUrl] = useState(""); - const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>( - defaultSpaces ?? [], - ); - - return ( - <div className="w-[80vw] max-w-[80vw] md:w-[40vw]"> - <DialogHeader> - <DialogTitle>Add a web page to memory</DialogTitle> - <DialogDescription> - This will fetch the content of the web page and add it to the memory - </DialogDescription> - </DialogHeader> - <Label className="mt-5 block">URL</Label> - <Input - placeholder="Enter the URL of the page" - type="url" - data-modal-autofocus - className="mt-2 w-full disabled:cursor-not-allowed disabled:opacity-70" - value={url} - onChange={(e) => setUrl(e.target.value)} - disabled={loading} - /> - <DialogFooter> - <FilterSpaces - selectedSpaces={selectedSpacesId} - setSelectedSpaces={setSelectedSpacesId} - className="mr-auto bg-white/5 hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-70" - name={"Spaces"} - disabled={loading} - /> - <button - type={"submit"} - disabled={loading} - onClick={async () => { - setLoading(true); - const metadata = await getMetaData(url); - const data = await addMemory( - { - title: metadata.title, - description: metadata.description, - content: "", - type: "page", - url: url, - image: metadata.image, - savedAt: new Date(), - }, - selectedSpacesId, - ); - if (data) onAdd?.(data.memory); - closeDialog(); - }} - className="bg-rgray-4 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition hover:bg-slate-100 focus-visible:bg-slate-100 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="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="focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:bg-[#F4F3F2] focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - Cancel - </DialogClose> - </DialogFooter> - </div> - ); -} - -export function NoteAddPage({ - closeDialog, - defaultSpaces, - onAdd, -}: { - closeDialog: () => void; - defaultSpaces?: number[]; - onAdd?: (addedData: StoredContent) => void; -}) { - const { addMemory } = useMemory(); - - const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>( - defaultSpaces ?? [], - ); - - const inputRef = useRef<HTMLInputElement>(null); - const [name, setName] = useState(""); - const [content, setContent] = useState(""); - const [loading, setLoading] = useState(false); - - function check(): boolean { - const data = { - name: name.trim(), - content, - }; - if (!data.name || data.name.length < 1) { - if (!inputRef.current) { - alert("Please enter a name for the note"); - return false; - } - inputRef.current.value = ""; - inputRef.current.placeholder = "Please enter a title for the note"; - inputRef.current.dataset["error"] = "true"; - setTimeout(() => { - inputRef.current!.placeholder = "Title of the note"; - inputRef.current!.dataset["error"] = "false"; - }, 500); - inputRef.current.focus(); - return false; - } - return true; - } - - return ( - <div className="w-[80vw] md:w-auto"> - <Input - ref={inputRef} - data-error="false" - className="w-full border-none p-0 text-xl ring-0 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400" - placeholder="Title of the note" - data-modal-autofocus - value={name} - disabled={loading} - onChange={(e) => setName(e.target.value)} - /> - <Editor - disableLocalStorage - defaultValue={""} - onUpdate={(editor) => { - if (!editor) return; - setContent(editor.storage.markdown.getMarkdown()); - }} - extensions={[Markdown]} - className="novel-editor border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] w-full overflow-y-auto rounded-lg border bg-white md:w-[50vw] [&>div>div]:p-5" - /> - <DialogFooter> - <FilterSpaces - selectedSpaces={selectedSpacesId} - setSelectedSpaces={setSelectedSpacesId} - className="hover:bg-rgray-5 mr-auto bg-white/5" - name={"Spaces"} - /> - <button - onClick={() => { - if (check()) { - setLoading(true); - addMemory( - { - content, - title: name, - type: "note", - url: `https://notes.supermemory.dhr.wtf/`, - image: "", - savedAt: new Date(), - }, - selectedSpacesId, - ).then((data) => { - if (data?.memory) onAdd?.(data.memory); - closeDialog(); - }); - } - }} - disabled={loading} - className="hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md bg-[#F4F3F2] 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="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="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> - </div> - ); -} - -export function SpaceAddPage({ - closeDialog, - onAdd, -}: { - closeDialog: () => void; - onAdd?: (addedData: StoredSpace) => void; -}) { - const { addSpace } = useMemory(); - - const inputRef = useRef<HTMLInputElement>(null); - const [name, setName] = useState(""); - - const [loading, setLoading] = useState(false); - - const [selected, setSelected] = useState<StoredContent[]>([]); - - function check(): boolean { - const data = { - name: name.trim(), - }; - if (!data.name || data.name.length < 1) { - if (!inputRef.current) { - alert("Please enter a name for the note"); - return false; - } - inputRef.current.value = ""; - inputRef.current.placeholder = "Please enter a title for the space"; - inputRef.current.dataset["error"] = "true"; - setTimeout(() => { - inputRef.current!.placeholder = "Enter the name of the space"; - inputRef.current!.dataset["error"] = "false"; - }, 500); - inputRef.current.focus(); - return false; - } - return true; - } - - return ( - <div className="w-[80vw] md:w-[40vw]"> - <DialogHeader> - <DialogTitle>Add a space</DialogTitle> - </DialogHeader> - <Label className="mt-5 block">Name</Label> - <Input - 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="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 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:hover:bg-slate-100 focus-visible:hover:bg-slate-100 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((data) => { - if (data) onAdd?.(data.space); - closeDialog(); - }); - } - }} - disabled={loading} - className="bg-rgray-4 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition hover:hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-70" - > - <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="focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:bg-[#F4F3F2] focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - Cancel - </DialogClose> - </DialogFooter> - </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-2 px-3 text-sm [&:hover_[data-icon]]:block [&:hover_img]:hidden"> - <button - onClick={onRemove} - className="ring-rgray-7 ring-offset-rgray-3 m-0 h-5 w-5 rounded-sm p-0 ring-offset-2 focus-visible:outline-none focus-visible:ring-2 [&:focus-visible>[data-icon]]:block [&:focus-visible>img]:hidden" - > - <img - src={ - type === "note" - ? "/note.svg" - : image ?? "/icons/logo_without_bg.png" - } - className="h-5 w-5" - /> - <X data-icon className="hidden h-5 w-5 scale-90" /> - </button> - <span>{title}</span> - <span className="ml-auto block opacity-50"> - {type === "note" ? "Note" : cleanUrl(url)} - </span> - </div> - ); -} - -export function AddExistingMemoryToSpace({ - space, - closeDialog, - fromSpaces, - notInSpaces, - onAdd, -}: { - space: { title: string; id: number }; - closeDialog: () => void; - fromSpaces?: number[]; - notInSpaces?: number[]; - onAdd?: () => void; -}) { - const { addMemoriesToSpace } = useMemory(); - - const [loading, setLoading] = useState(false); - - const [selected, setSelected] = useState<StoredContent[]>([]); - - return ( - <div className="w-[80vw] md:w-[40vw]"> - <DialogHeader> - <DialogTitle>Add an existing memory to {space.title}</DialogTitle> - <DialogDescription> - Pick the memories you want to add to this space - </DialogDescription> - </DialogHeader> - {selected.length > 0 && ( - <> - <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} - fromSpaces={fromSpaces} - notInSpaces={notInSpaces} - 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={() => { - setLoading(true); - addMemoriesToSpace( - space.id, - selected.map((i) => i.id), - ).then(() => { - onAdd?.(); - 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="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="focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:bg-[#F4F3F2] focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - Cancel - </DialogClose> - </DialogFooter> - </div> - ); -} diff --git a/apps/web/src/components/Sidebar/DeleteConfirmation.tsx b/apps/web/src/components/Sidebar/DeleteConfirmation.tsx deleted file mode 100644 index 7955df0d..00000000 --- a/apps/web/src/components/Sidebar/DeleteConfirmation.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { - Dialog, - DialogContent, - DialogTrigger, - DialogTitle, - DialogDescription, - DialogClose, - DialogFooter, -} from "../ui/dialog"; - -export default function DeleteConfirmation({ - onDelete, - trigger = true, - children, -}: { - trigger?: boolean; - onDelete?: () => void; - children: React.ReactNode; -}) { - return ( - <Dialog> - {trigger ? ( - <DialogTrigger asChild>{children}</DialogTrigger> - ) : ( - <>{children}</> - )} - <DialogContent> - <DialogTitle className="text-xl">Are you sure?</DialogTitle> - <DialogDescription className="text-md"> - You will not be able to recover this it. - </DialogDescription> - <DialogFooter> - <DialogClose - type={undefined} - onClick={onDelete} - className="ml-auto flex items-center justify-center rounded-md bg-red-100/10 px-3 py-2 text-red-400 transition hover:bg-red-100/5 focus-visible:bg-red-100/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-100/30" - > - 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/Sidebar/EditNoteDialog.tsx b/apps/web/src/components/Sidebar/EditNoteDialog.tsx deleted file mode 100644 index c0ad716d..00000000 --- a/apps/web/src/components/Sidebar/EditNoteDialog.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { Editor } from "novel"; -import { DialogClose, DialogFooter } from "../ui/dialog"; -import { Input } from "../ui/input"; -import { Markdown } from "tiptap-markdown"; -import { useEffect, useRef, useState } from "react"; -import { FilterSpaces } from "./FilterCombobox"; -import { useMemory } from "@/contexts/MemoryContext"; -import { Loader, Plus, Trash, X } from "lucide-react"; -import { motion } from "framer-motion"; -import { StoredContent } from "@/server/db/schema"; -import { fetchContent } from "@/actions/db"; -import { isArraysEqual } from "@/lib/utils"; -import DeleteConfirmation from "./DeleteConfirmation"; - -export function NoteEdit({ - memory, - closeDialog, - onDelete, -}: { - memory: StoredContent; - closeDialog: () => any; - onDelete?: () => void; -}) { - const { updateMemory, deleteMemory } = useMemory(); - - const [initialSpaces, setInitialSpaces] = useState<number[]>([]); - const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]); - - const inputRef = useRef<HTMLInputElement>(null); - const [name, setName] = useState(memory.title ?? ""); - const [content, setContent] = useState(memory.content); - const [loading, setLoading] = useState(false); - - function check(): boolean { - const data = { - name: name.trim(), - content, - }; - if (!data.name || data.name.length < 1) { - if (!inputRef.current) { - alert("Please enter a name for the note"); - return false; - } - inputRef.current.value = ""; - inputRef.current.placeholder = "Please enter a title for the note"; - inputRef.current.dataset["error"] = "true"; - setTimeout(() => { - inputRef.current!.placeholder = "Title of the note"; - inputRef.current!.dataset["error"] = "false"; - }, 500); - inputRef.current.focus(); - return false; - } - return true; - } - - useEffect(() => { - fetchContent(memory.id).then((data) => { - if (data?.spaces) { - setInitialSpaces(data.spaces); - setSelectedSpacesId(data.spaces); - } - }); - }, []); - - return ( - <div> - <Input - ref={inputRef} - data-error="false" - className="w-full border-none p-0 text-xl ring-0 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400" - placeholder="Title of the note" - value={name} - disabled={loading} - onChange={(e) => setName(e.target.value)} - /> - <Editor - disableLocalStorage - defaultValue={memory.content} - onUpdate={(editor) => { - if (!editor) return; - setContent(editor.storage.markdown.getMarkdown()); - }} - extensions={[Markdown]} - className="novel-editor border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] w-[50vw] overflow-y-auto rounded-lg border bg-white [&>div>div]:p-5" - /> - <DialogFooter> - <FilterSpaces - selectedSpaces={selectedSpacesId} - setSelectedSpaces={setSelectedSpacesId} - className="mr-auto bg-white hover:bg-slate-100" - name={"Spaces"} - /> - <DeleteConfirmation - onDelete={() => { - deleteMemory(memory.id); - onDelete?.(); - }} - > - <button - type={undefined} - disabled={loading} - className="rounded-md px-3 py-2 ring-transparent transition hover:bg-red-100 hover:text-red-400 focus-visible:bg-red-100 focus-visible:text-red-400 focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - <Trash className="h-5 w-5" /> - </button> - </DeleteConfirmation> - <button - onClick={() => { - if (check()) { - setLoading(true); - console.log({ - title: name === memory.title ? undefined : name, - content: content === memory.content ? undefined : content, - spaces: isArraysEqual(initialSpaces, selectedSpacesId) - ? undefined - : selectedSpacesId, - }); - updateMemory(memory.id, { - title: name === memory.title ? undefined : name, - content: content === memory.content ? undefined : content, - spaces: isArraysEqual(initialSpaces, selectedSpacesId) - ? undefined - : selectedSpacesId, - }).then(closeDialog); - } - }} - disabled={loading} - className="focus-visible:ring-rgray-7 relative rounded-md bg-white px-4 py-2 ring-transparent transition hover:bg-slate-100 focus-visible:bg-slate-100 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="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0" - > - <Loader className="h-5 w-5 animate-spin" /> - </motion.div> - <motion.div - initial={{ y: "0%" }} - animate={loading && { opacity: 0, y: "30%" }} - > - Save - </motion.div> - </button> - <DialogClose - type={undefined} - disabled={loading} - className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - Cancel - </DialogClose> - </DialogFooter> - </div> - ); -} diff --git a/apps/web/src/components/Sidebar/ExpandedSpace.tsx b/apps/web/src/components/Sidebar/ExpandedSpace.tsx deleted file mode 100644 index 55d3f3f8..00000000 --- a/apps/web/src/components/Sidebar/ExpandedSpace.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import { fetchContentForSpace, getSpace } from "@/actions/db"; -import { useMemory } from "@/contexts/MemoryContext"; -import { StoredContent, StoredSpace } from "@/server/db/schema"; -import { - Edit3, - Loader, - Plus, - Search, - Sparkles, - StickyNote, - Text, - Undo2, -} from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { Input, InputWithIcon } from "../ui/input"; -import { useDebounce } from "@/hooks/useDebounce"; -import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { AddMemoryModal, MemoryItem } from "./MemoriesBar"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "../ui/dropdown-menu"; -import { DialogTrigger } from "../ui/dialog"; - -export function ExpandedSpace({ - spaceId, - back, -}: { - spaceId: number; - back: () => void; -}) { - const { updateMemory, updateSpace, search } = useMemory(); - - const [parent, enableAnimations] = useAutoAnimate(); - - const inputRef = useRef<HTMLInputElement>(null); - - const [contentForSpace, setContentForSpace] = useState<StoredContent[]>([]); - - const [lastUpdatedTitle, setLastUpdatedTitle] = useState<string | null>(null); - - const [title, setTitle] = useState<string>(""); - const debouncedTitle = useDebounce(title, 500); - - const [loading, setLoading] = useState(true); - - const [saveLoading, setSaveLoading] = useState(false); - - const [searchQuery, setSearcyQuery] = useState(""); - const [searchLoading, setSearchLoading] = useState(false); - const query = useDebounce(searchQuery, 500); - - const [searchResults, setSearchResults] = useState<StoredContent[]>([]); - - const [addMemoryState, setAddMemoryState] = useState< - "page" | "note" | "existing-memory" | "space" | null - >(null); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - - useEffect(() => { - (async () => { - const title = (await getSpace(spaceId))?.name ?? ""; - setTitle(title); - setLastUpdatedTitle(title); - setContentForSpace((await fetchContentForSpace(spaceId)) ?? []); - setLoading(false); - })(); - }, []); - - useEffect(() => { - if ( - debouncedTitle.trim().length < 1 || - debouncedTitle.trim() === lastUpdatedTitle?.trim() - ) - return; - (async () => { - setSaveLoading(true); - await updateSpace(spaceId, debouncedTitle.trim()); - setLastUpdatedTitle(debouncedTitle); - setSaveLoading(false); - })(); - }, [debouncedTitle]); - - useEffect(() => { - const q = query.trim(); - if (q.length < 1) { - setSearchResults([]); - return; - } - - setSearchLoading(true); - - (async () => { - setSearchResults( - ( - await search(q, { - filter: { spaces: false }, - memoriesRelativeToSpace: { - fromSpaces: [spaceId], - }, - }) - ).map((i) => i.memory!), - ); - setSearchLoading(false); - })(); - }, [query]); - - if (loading) { - return ( - <div className="flex h-full w-full items-center justify-center"> - <Loader className="h-5 w-5 animate-spin" /> - </div> - ); - } - - return ( - <div className="text-rgray-11 flex w-full flex-col items-start py-8 text-left"> - <div className="flex w-full items-center justify-start gap-2 px-8"> - <button - onClick={back} - className="focus-visible:ring-offset-rgray-3 focus-visible:ring-rgray-7 rounded-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2" - > - <Undo2 className="h-5 w-5" /> - </button> - <Input - ref={inputRef} - data-error="false" - className="w-full border-none p-0 text-xl ring-0 placeholder:text-white/30 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400" - placeholder="Title of the space" - data-modal-autofocus - value={title} - onChange={(e) => setTitle(e.target.value)} - /> - <button - onClick={() => { - inputRef.current?.focus(); - inputRef.current?.animate( - { - opacity: [1, 0.2, 1], - }, - { - duration: 100, - }, - ); - }} - className="focus-visible:ring-offset-rgray-3 focus-visible:ring-rgray-7 rounded-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2" - > - {saveLoading ? ( - <Loader className="h-5 w-5 animate-spin opacity-70" /> - ) : ( - <Edit3 className="h-5 w-5 opacity-70" /> - )} - </button> - </div> - <div className="w-full px-8"> - <InputWithIcon - placeholder="Search" - 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)} - /> - </div> - <div className="mt-2 w-full px-8"> - <AddMemoryModal - onAdd={(data) => { - if (!data) { - setLoading(true); - (async () => { - const title = (await getSpace(spaceId))?.name ?? ""; - setTitle(title); - setLastUpdatedTitle(title); - setContentForSpace((await fetchContentForSpace(spaceId)) ?? []); - setLoading(false); - })(); - } else if (Object.hasOwn(data, "url")) { - const _data = data as StoredContent; - setContentForSpace((prev) => [...prev, _data]); - } - }} - data={{ space: { title, id: spaceId }, notInSpaces: [spaceId] }} - defaultSpaces={[spaceId]} - type={addMemoryState} - > - <DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}> - <DropdownMenuTrigger asChild> - <button 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"> - <Plus className="mr-2 h-5 w-5" /> - Add - </button> - </DropdownMenuTrigger> - <DropdownMenuContent onCloseAutoFocus={(e) => e.preventDefault()}> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("existing-memory"); - }} - > - <Sparkles className="mr-2 h-4 w-4" /> - Existing Memory - </DropdownMenuItem> - </DialogTrigger> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("page"); - }} - > - <StickyNote className="mr-2 h-4 w-4" /> - Page - </DropdownMenuItem> - </DialogTrigger> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("note"); - }} - > - <Text className="mr-2 h-4 w-4" /> - Note - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </AddMemoryModal> - </div> - <div - ref={parent} - className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5" - > - {query.trim().length > 0 ? ( - <> - {searchResults.map((memory, i) => ( - <MemoryItem - removeFromSpace={async () => { - await updateMemory(memory.id, { - removedFromSpaces: [spaceId], - }); - setContentForSpace((prev) => - prev.filter((s) => s.id !== memory.id), - ); - setSearchResults((prev) => - prev.filter((i) => i.id !== memory.id), - ); - }} - {...memory!} - key={i} - onDelete={() => { - setContentForSpace((prev) => - prev.filter((s) => s.id !== memory.id), - ); - setSearchResults((prev) => - prev.filter((i) => i.id !== memory.id), - ); - }} - /> - ))} - </> - ) : ( - contentForSpace.map((m) => ( - <MemoryItem - key={m.id} - {...m} - onDelete={() => - setContentForSpace((prev) => prev.filter((s) => s.id !== m.id)) - } - removeFromSpace={async () => { - await updateMemory(m.id, { - removedFromSpaces: [spaceId], - }); - setContentForSpace((prev) => prev.filter((s) => s.id !== m.id)); - }} - /> - )) - )} - </div> - </div> - ); -} diff --git a/apps/web/src/components/Sidebar/FilterCombobox.tsx b/apps/web/src/components/Sidebar/FilterCombobox.tsx deleted file mode 100644 index 634a09e3..00000000 --- a/apps/web/src/components/Sidebar/FilterCombobox.tsx +++ /dev/null @@ -1,303 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Check, ChevronsUpDown } from "lucide-react"; - -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { SpaceIcon } from "@/assets/Memories"; -import { AnimatePresence, LayoutGroup, motion } from "framer-motion"; -import { SearchResult, useMemory } from "@/contexts/MemoryContext"; -import { useDebounce } from "@/hooks/useDebounce"; -import { StoredContent } from "@/server/db/schema"; - -export interface FilterSpacesProps - extends React.ButtonHTMLAttributes<HTMLButtonElement> { - side?: "top" | "bottom"; - align?: "end" | "start" | "center"; - onClose?: () => void; - selectedSpaces: number[]; - setSelectedSpaces: ( - spaces: number[] | ((prev: number[]) => number[]), - ) => void; - name: string; -} - -export function FilterSpaces({ - className, - side = "bottom", - align = "center", - onClose, - selectedSpaces, - setSelectedSpaces, - name, - ...props -}: FilterSpacesProps) { - const { spaces } = useMemory(); - const [open, setOpen] = React.useState(false); - - const sortedSpaces = spaces.sort(({ id: a }, { id: b }) => - selectedSpaces.includes(a) && !selectedSpaces.includes(b) - ? -1 - : selectedSpaces.includes(b) && !selectedSpaces.includes(a) - ? 1 - : 0, - ); - - React.useEffect(() => { - if (!open) { - onClose?.(); - } - }, [open]); - - return ( - <Popover open={open} onOpenChange={setOpen}> - <PopoverTrigger asChild> - <button - type={undefined} - data-state-on={open} - className={cn( - "focus-visible:ring-rgray-8 hover:bg-rgray-3 relative flex items-center justify-center gap-1 rounded-md px-3 py-1.5 ring-2 ring-transparent focus-visible:outline-none", - className, - )} - {...props} - > - <SpaceIcon className="mr-1 h-5 w-5" /> - {name} - <ChevronsUpDown className="h-4 w-4" /> - <div - data-state-on={selectedSpaces.length > 0} - className="on:flex text-rgray-11 border-rgray-6 bg-rgray-2 absolute left-0 top-0 hidden aspect-[1] h-4 w-4 -translate-x-1/3 -translate-y-1/3 items-center justify-center rounded-full border text-center text-[9px]" - > - {selectedSpaces.length} - </div> - </button> - </PopoverTrigger> - <PopoverContent - align={align} - side={side} - className="w-[200px] p-0" - onCloseAutoFocus={(e) => e.preventDefault()} - > - <Command - filter={(val, search) => - spaces - .find((s) => s.id.toString() === val) - ?.name.toLowerCase() - .includes(search.toLowerCase().trim()) - ? 1 - : 0 - } - > - <CommandInput placeholder="Filter spaces..." /> - <CommandList asChild> - <motion.div layoutScroll> - <CommandEmpty>Nothing found</CommandEmpty> - <CommandGroup> - {sortedSpaces.map((space) => ( - <CommandItem - key={space.id} - value={space.id.toString()} - onSelect={(val) => { - setSelectedSpaces((prev: number[]) => - prev.includes(parseInt(val)) - ? prev.filter((v) => v !== parseInt(val)) - : [...prev, parseInt(val)], - ); - }} - asChild - > - <motion.div - initial={{ opacity: 0 }} - animate={{ opacity: 1, transition: { delay: 0.05 } }} - transition={{ duration: 0.15 }} - layout - layoutId={`space-combobox-${space.id}`} - className="text-rgray-11" - > - <SpaceIcon className="mr-2 h-4 w-4" /> - {space.name.length > 10 - ? space.name.slice(0, 10) + "..." - : space.name} - {selectedSpaces.includes(space.id)} - <Check - data-state-on={selectedSpaces.includes(space.id)} - className={cn( - "on:opacity-100 ml-auto h-4 w-4 opacity-0", - )} - /> - </motion.div> - </CommandItem> - ))} - </CommandGroup> - </motion.div> - </CommandList> - </Command> - </PopoverContent> - </Popover> - ); -} - -export type FilterMemoriesProps = { - side?: "top" | "bottom"; - align?: "end" | "start" | "center"; - onClose?: () => void; - selected: StoredContent[]; - setSelected: React.Dispatch<React.SetStateAction<StoredContent[]>>; - fromSpaces?: number[]; - notInSpaces?: number[]; -} & React.ButtonHTMLAttributes<HTMLButtonElement>; - -export function FilterMemories({ - className, - side = "bottom", - align = "center", - onClose, - selected, - setSelected, - fromSpaces, - notInSpaces, - ...props -}: FilterMemoriesProps) { - const { search } = useMemory(); - - const [open, setOpen] = React.useState(false); - const [searchQuery, setSearchQuery] = React.useState(""); - const query = useDebounce(searchQuery, 500); - - const [searchResults, setSearchResults] = React.useState<SearchResult[]>([]); - const [isSearching, setIsSearching] = React.useState(false); - - const results = React.useMemo(() => { - return searchResults.map((r) => r.memory); - }, [searchResults]); - - 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, - }, - memoriesRelativeToSpace: { - fromSpaces, - notInSpaces, - }, - }); - setSearchResults(results); - setIsSearching(false); - })(); - } else { - setSearchResults([]); - } - }, [query]); - - React.useEffect(() => { - if (!open) { - onClose?.(); - } - }, [open]); - - console.log(searchResults); - return ( - <AnimatePresence mode="popLayout"> - <LayoutGroup> - <Popover open={open} onOpenChange={setOpen}> - <PopoverTrigger asChild> - <button - type={undefined} - data-state-on={open} - className={cn( - "text-rgray-11/70 on:bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-3 relative flex items-center justify-center gap-1 rounded-md px-3 py-1.5 ring-2 ring-transparent focus-visible:outline-none", - className, - )} - {...props} - > - {props.children} - </button> - </PopoverTrigger> - <PopoverContent - onCloseAutoFocus={(e) => e.preventDefault()} - align={align} - side={side} - className="w-[200px] p-0" - > - <Command shouldFilter={false}> - <CommandInput - isSearching={isSearching} - value={searchQuery} - onValueChange={setSearchQuery} - placeholder="Filter memories..." - /> - <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 && m.title?.length > 14 - ? m.title?.slice(0, 14) + "..." - : 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> - </LayoutGroup> - </AnimatePresence> - ); -} diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx deleted file mode 100644 index a81d00c0..00000000 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ /dev/null @@ -1,709 +0,0 @@ -import { Editor } from "novel"; -import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { - MemoryWithImage, - MemoryWithImages3, - MemoryWithImages2, -} from "@/assets/MemoryWithImages"; -import { Input, InputWithIcon } from "../ui/input"; -import { - ArrowUpRight, - Edit3, - Loader, - Minus, - MoreHorizontal, - Plus, - Search, - Sparkles, - Text, - Trash2, -} from "lucide-react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "../ui/dropdown-menu"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { Variant, useAnimate, motion } from "framer-motion"; -import { SearchResult, useMemory } from "@/contexts/MemoryContext"; -import { SpaceIcon } from "@/assets/Memories"; -import { Dialog, DialogContent } from "../ui/dialog"; -import useViewport from "@/hooks/useViewport"; -import useTouchHold from "@/hooks/useTouchHold"; -import { DialogTrigger } from "@radix-ui/react-dialog"; -import { - AddExistingMemoryToSpace, - AddMemoryPage, - NoteAddPage, - SpaceAddPage, -} from "./AddMemoryDialog"; -import { ExpandedSpace } from "./ExpandedSpace"; -import { StoredContent, StoredSpace } from "@/server/db/schema"; -import { useDebounce } from "@/hooks/useDebounce"; -import { NoteEdit } from "./EditNoteDialog"; -import DeleteConfirmation from "./DeleteConfirmation"; - -export function MemoriesBar({ isOpen }: { isOpen: boolean }) { - const [parent, enableAnimations] = useAutoAnimate(); - const { spaces, deleteSpace, freeMemories, search } = useMemory(); - - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [addMemoryState, setAddMemoryState] = useState< - "page" | "note" | "space" | "existing-memory" | null - >(null); - - const [expandedSpace, setExpandedSpace] = useState<number | null>(null); - - const [searchQuery, setSearcyQuery] = useState(""); - const [searchLoading, setSearchLoading] = useState(false); - const query = useDebounce(searchQuery, 500); - - const [searchResults, setSearchResults] = useState<SearchResult[]>([]); - - useEffect(() => { - const q = query.trim(); - if (q.length < 1) { - setSearchResults([]); - return; - } - - setSearchLoading(true); - - (async () => { - setSearchResults(await search(q)); - setSearchLoading(false); - })(); - }, [query]); - - useEffect(() => { - if (!isOpen) { - setExpandedSpace(null); - } - }, [isOpen]); - - if (expandedSpace) { - return ( - <ExpandedSpace - spaceId={expandedSpace} - back={() => setExpandedSpace(null)} - // close={() => setExpandedSpace(null)} - /> - ); - } - - return ( - <div className="text-rgray-11 flex w-full flex-col items-start py-8 text-left"> - <div className="w-full px-8"> - <h1 className="w-full text-2xl">Your Memories</h1> - <InputWithIcon - placeholder="Search" - 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)} - /> - </div> - <div className="mt-2 flex w-full px-8"> - <AddMemoryModal type={addMemoryState}> - <DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}> - <DropdownMenuTrigger asChild> - <button 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"> - <Plus className="mr-2 h-5 w-5" /> - Add - </button> - </DropdownMenuTrigger> - <DropdownMenuContent onCloseAutoFocus={(e) => e.preventDefault()}> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("page"); - }} - > - <Sparkles className="mr-2 h-4 w-4" /> - Page to Memory - </DropdownMenuItem> - </DialogTrigger> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("note"); - }} - > - <Text className="mr-2 h-4 w-4" /> - Note - </DropdownMenuItem> - </DialogTrigger> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("space"); - }} - > - <SpaceIcon className="mr-2 h-4 w-4" /> - Space - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </AddMemoryModal> - </div> - <div - 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} - onDelete={() => { - setSearchResults((prev) => - prev.filter((i) => i.memory?.id !== memory.id), - ); - }} - /> - )} - {type === "space" && ( - <SpaceItem - {...space!} - key={i} - onDelete={() => { - setSearchResults((prev) => - prev.filter((i) => i.space?.id !== space.id), - ); - deleteSpace(space.id); - }} - /> - )} - </> - ))} - </> - ) : ( - <> - {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> - ); -} - -const SpaceExitVariant: Variant = { - opacity: 0, - scale: 0, - borderRadius: "50%", - background: "var(--gray-1)", - transition: { - duration: 0.2, - }, -}; - -export function MemoryItem( - props: StoredContent & { - onDelete?: () => void; - removeFromSpace?: () => Promise<void>; - }, -) { - const { id, title, image, type, url, onDelete, removeFromSpace } = props; - - const { deleteMemory } = useMemory(); - - const name = title - ? title.length > 10 - ? title.slice(0, 10) + "..." - : title - : "<no title>"; - - const [isDialogOpen, setIsDialogOpen] = useState(false); - - const [moreDropdownOpen, setMoreDropdownOpen] = useState(false); - - const touchEventProps = useTouchHold({ - onHold() { - setMoreDropdownOpen(true); - }, - }); - return ( - <Dialog - open={type === "note" ? isDialogOpen : false} - onOpenChange={setIsDialogOpen} - > - <div - {...touchEventProps} - 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 cursor-pointer 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" - > - {type === "note" ? ( - <DialogTrigger asChild> - <button data-space-text className="focus-visible:outline-none"> - {name} - </button> - </DialogTrigger> - ) : ( - <button - onClick={() => window.open(url)} - data-space-text - className="focus-visible:outline-none" - > - {name} - </button> - )} - - {type === "page" ? ( - <PageMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - removeFromSpace={removeFromSpace} - onDelete={() => { - deleteMemory(id); - onDelete?.(); - }} - url={url} - /> - ) : type === "note" ? ( - <NoteMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - removeFromSpace={removeFromSpace} - onEdit={() => setIsDialogOpen(true)} - onDelete={() => { - deleteMemory(id); - onDelete?.(); - }} - /> - ) : null} - - <div className="flex h-24 w-24 items-center justify-center"> - {type === "page" ? ( - <img - onClick={() => window.open(url)} - className="h-16 w-16" - id={id.toString()} - src={image!} - onError={(e) => { - (e.target as HTMLImageElement).src = - "/icons/white_without_bg.png"; - }} - /> - ) : type === "note" ? ( - <Text onClick={() => setIsDialogOpen(true)} className="h-16 w-16" /> - ) : ( - <></> - )} - </div> - </div> - <DialogContent className="w-max max-w-[auto]"> - <NoteEdit - onDelete={onDelete} - closeDialog={() => setIsDialogOpen(false)} - memory={props} - /> - </DialogContent> - </Dialog> - ); -} - -export function SpaceItem({ - name, - id, - onDelete, - onClick, -}: StoredSpace & { onDelete: () => void; onClick?: () => void }) { - const { cachedMemories } = useMemory(); - - const [itemRef, animateItem] = useAnimate(); - const { width } = useViewport(); - - const [moreDropdownOpen, setMoreDropdownOpen] = useState(false); - - const touchEventProps = useTouchHold({ - onHold() { - setMoreDropdownOpen(true); - }, - }); - - const spaceMemories = useMemo(() => { - return cachedMemories.filter((m) => m.space === id); - }, [cachedMemories]); - - const _name = name.length > 10 ? name.slice(0, 10) + "..." : name; - - return ( - <motion.div - ref={itemRef} - {...touchEventProps} - 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 - onClick={onClick} - data-space-text - className="focus-visible:outline-none" - > - {_name} - </button> - <SpaceMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - onEdit={onClick} - onDelete={() => { - 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(); - // }; - // }; - }} - /> - {spaceMemories.length > 2 ? ( - <MemoryWithImages3 - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - images={ - spaceMemories - .map((c) => (c.type === "note" ? "/note.svg" : c.image)) - .reverse() as string[] - } - /> - ) : spaceMemories.length > 1 ? ( - <MemoryWithImages2 - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - images={ - spaceMemories - .map((c) => (c.type === "note" ? "/note.svg" : c.image)) - .reverse() as string[] - } - /> - ) : spaceMemories.length === 1 ? ( - <MemoryWithImage - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - image={ - spaceMemories[0].type === "note" - ? "/note.svg" - : spaceMemories[0].image! - } - /> - ) : ( - <div - onClick={onClick} - className="bg-rgray-4 shadow- h-24 w-24 scale-50 rounded-full opacity-30" - ></div> - )} - </motion.div> - ); -} - -export function SpaceMoreButton({ - onDelete, - isOpen, - setIsOpen, - onEdit, -}: { - onDelete?: () => void; - isOpen?: boolean; - onEdit?: () => void; - setIsOpen?: (open: boolean) => void; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="hover:bg-rgray-3 focus-visible:bg-rgray-3 focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="text-rgray-11 h-5 w-5" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={onEdit}> - <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"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function PageMoreButton({ - onDelete, - isOpen, - setIsOpen, - url, - removeFromSpace, -}: { - onDelete?: () => void; - isOpen?: boolean; - url: string; - setIsOpen?: (open: boolean) => void; - removeFromSpace?: () => Promise<void>; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="hover:bg-rgray-3 focus-visible:bg-rgray-3 focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="text-rgray-11 h-5 w-5" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={() => window.open(url)}> - <ArrowUpRight - className="mr-2 h-4 w-4 scale-125" - strokeWidth={1.5} - /> - Open - </DropdownMenuItem> - {removeFromSpace && ( - <DropdownMenuItem onClick={removeFromSpace}> - <Minus className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Remove from space - </DropdownMenuItem> - )} - <DialogTrigger asChild> - <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function NoteMoreButton({ - onDelete, - isOpen, - setIsOpen, - onEdit, - removeFromSpace, -}: { - onDelete?: () => void; - isOpen?: boolean; - onEdit?: () => void; - setIsOpen?: (open: boolean) => void; - removeFromSpace?: () => Promise<void>; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="hover:bg-rgray-3 focus-visible:bg-rgray-3 focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="text-rgray-11 h-5 w-5" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={onEdit}> - <Edit3 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Edit - </DropdownMenuItem> - {removeFromSpace && ( - <DropdownMenuItem onClick={removeFromSpace}> - <Minus className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Remove from space - </DropdownMenuItem> - )} - <DialogTrigger asChild> - <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function AddMemoryModal({ - type, - children, - defaultSpaces, - onAdd, - data, -}: { - type: "page" | "note" | "space" | "existing-memory" | null; - children?: React.ReactNode | React.ReactNode[]; - defaultSpaces?: number[]; - data?: { - space?: { - title: string; - id: number; - }; - fromSpaces?: number[]; - notInSpaces?: number[]; - }; - onAdd?: (data?: StoredSpace | StoredContent | StoredContent[]) => void; -}) { - const [isDialogOpen, setIsDialogOpen] = useState(false); - - return ( - <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> - {children} - <DialogContent - onOpenAutoFocus={(e) => { - e.preventDefault(); - const novel = document.querySelector('[contenteditable="true"]') as - | HTMLDivElement - | undefined; - if (novel) { - novel.autofocus = false; - novel.onfocus = () => { - ( - document.querySelector("[data-modal-autofocus]") as - | HTMLInputElement - | undefined - )?.focus(); - novel.onfocus = null; - }; - } - ( - document.querySelector("[data-modal-autofocus]") as - | HTMLInputElement - | undefined - )?.focus(); - }} - className="w-max max-w-[auto]" - > - {type === "page" ? ( - <AddMemoryPage - onAdd={onAdd} - defaultSpaces={defaultSpaces} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "note" ? ( - <NoteAddPage - onAdd={onAdd} - defaultSpaces={defaultSpaces} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "space" ? ( - <SpaceAddPage - onAdd={onAdd} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "existing-memory" ? ( - <AddExistingMemoryToSpace - onAdd={onAdd} - fromSpaces={data?.fromSpaces} - notInSpaces={data?.notInSpaces} - space={data!.space!} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : ( - <></> - )} - </DialogContent> - </Dialog> - ); -} diff --git a/apps/web/src/components/Sidebar/SettingsTab.tsx b/apps/web/src/components/Sidebar/SettingsTab.tsx deleted file mode 100644 index 31b8380d..00000000 --- a/apps/web/src/components/Sidebar/SettingsTab.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Box, LogOut } from "lucide-react"; -import { signOut, useSession } from "next-auth/react"; -import { useEffect, useState } from "react"; - -export function SettingsTab({ open }: { open: boolean }) { - const { data: session } = useSession(); - - const [tweetStat, setTweetStat] = useState<[number, number] | null>(); - const [memoryStat, setMemoryStat] = useState<[number, number] | null>(); - - const [loading, setLoading] = useState(true); - - useEffect(() => { - fetch("/api/getCount").then(async (resp) => { - const data = (await resp.json()) as any; - setTweetStat([data.tweetsCount, data.tweetsLimit]); - setMemoryStat([data.pageCount, data.pageLimit]); - setLoading(false); - }); - }, [open]); - - return ( - <div className="flex h-full w-full flex-col items-start py-3 text-left font-normal text-black md:py-8"> - <div className="w-full px-6"> - <h1 className="w-full text-2xl font-medium">Settings</h1> - <div className="mt-5 grid w-full grid-cols-3 gap-1"> - <img - className="rounded-full" - src={session?.user?.image ?? "/icons/white_without_bg.png"} - onError={(e) => { - (e.target as HTMLImageElement).src = - "/icons/white_without_bg.png"; - }} - /> - <div className="col-span-2 flex flex-col items-start justify-center"> - <h1 className="text-xl font-medium">{session?.user?.name}</h1> - <span>{session?.user?.email}</span> - <button - onClick={() => signOut()} - className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative mt-auto flex items-center justify-center gap-2 rounded-md px-4 py-2 text-white ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - <LogOut className="h-4 w-4" /> - Logout - </button> - </div> - </div> - </div> - <div className="border-rgray-5 mt-auto w-full px-8 pt-8"> - <h1 className="flex w-full items-center gap-2 text-xl"> - <Box className="h-6 w-6" /> - Storage - </h1> - {loading ? ( - <div className="my-5 flex w-full flex-col items-center justify-center gap-5"> - <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div> - <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div> - </div> - ) : ( - <> - <div className="my-5"> - <h2 className="text-md flex w-full items-center justify-between"> - Memories - <div className="bg-rgray-4 flex rounded-md px-2 py-2 text-xs text-white/70"> - {memoryStat?.join("/")} - </div> - </h2> - <div className="mt-2 h-5 w-full overflow-hidden rounded-full bg-stone-400"> - <div - style={{ - width: `${((memoryStat?.[0] ?? 0) / (memoryStat?.[1] ?? 100)) * 100}%`, - minWidth: memoryStat?.[0] ?? 0 > 0 ? "5%" : "0%", - }} - className="bg-rgray-5 h-full rounded-full" - /> - </div> - </div> - <div className="my-5"> - <h2 className="text-md flex w-full items-center justify-between"> - Tweets - <div className="bg-rgray-4 flex rounded-md px-2 py-2 text-xs text-white/70"> - {tweetStat?.join("/")} - </div> - </h2> - <div className="mt-2 h-5 w-full overflow-hidden rounded-full bg-stone-400"> - <div - style={{ - width: `${((tweetStat?.[0] ?? 0) / (tweetStat?.[1] ?? 100)) * 100}%`, - minWidth: tweetStat?.[0] ?? 0 > 0 ? "5%" : "0%", - }} - className="h-full rounded-full bg-white" - /> - </div> - </div> - </> - )} - </div> - </div> - ); -} diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx deleted file mode 100644 index ae757afe..00000000 --- a/apps/web/src/components/Sidebar/index.tsx +++ /dev/null @@ -1,172 +0,0 @@ -"use client"; -import { MemoryIcon } from "../../assets/Memories"; -import React, { useEffect, useState } from "react"; -import { AnimatePresence, motion } from "framer-motion"; -import { signOut, useSession } from "next-auth/react"; -import MessagePoster from "@/app/MessagePoster"; -import Link from "next/link"; -import { SettingsTab } from "./SettingsTab"; -import { Avatar, AvatarImage } from "@radix-ui/react-avatar"; -import { AvatarFallback } from "../ui/avatar"; - -export type MenuItem = { - icon: React.ReactNode | React.ReactNode[]; - label: string; - content?: React.ReactNode; - labelDisplay?: React.ReactNode; -}; - -export default function Sidebar({ - selectChange, - jwt, -}: { - selectChange?: (selectedItem: string | null) => void; - jwt: string; -}) { - const { data: session } = useSession(); - - const [selectedItem, setSelectedItem] = useState<string | null>(null); - - const menuItemsTop: Array<MenuItem> = []; - - const menuItemsBottom: Array<MenuItem> = [ - { - label: "Settings", - content: <SettingsTab open={selectedItem !== null} />, - icon: <></>, - }, - ]; - - const menuItems = [...menuItemsTop, ...menuItemsBottom]; - - const Subbar = menuItems.find((i) => i.label === selectedItem)?.content ?? ( - <></> - ); - - useEffect(() => { - void selectChange?.(selectedItem); - }, [selectedItem]); - - return ( - <div className="relative hidden h-screen max-h-screen w-max flex-col items-center text-sm font-light md:flex"> - <div - className={`relative z-[50] flex h-full w-full flex-col items-center justify-center border-r bg-stone-100 px-2 py-5 `} - > - <Link - data-state-on={selectedItem === "Memories"} - href="/" - onClick={() => setSelectedItem(null)} - className="focus-visible:ring-rgray-7 relative z-[100] flex w-full flex-col items-center justify-center rounded-md px-3 py-3 opacity-80 ring-2 ring-transparent transition hover:bg-stone-300 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none" - > - <MemoryIcon className="h-12 w-12" /> - <span className="text-black">Memories</span> - </Link> - - <div className="mt-auto" /> - - <MenuItem - item={{ - label: "Settings", - icon: ( - <svg - xmlns="http://www.w3.org/2000/svg" - fill="white" - viewBox="0 0 24 24" - strokeWidth={0.5} - stroke="black" - className="h-10 w-10" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" - /> - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" - /> - </svg> - ), - content: <SettingsTab open={selectedItem !== null} />, - }} - selectedItem={selectedItem} - setSelectedItem={setSelectedItem} - /> - {/* <MessagePoster jwt={jwt} /> */} - <div className="mt-4 flex cursor-pointer flex-col items-center justify-center gap-2 rounded-b-md border-t border-stone-600 px-2 py-3 pt-4 text-black hover:bg-stone-300 hover:opacity-100"> - <Avatar> - <AvatarImage - className="h-10 w-10 rounded-full" - src={session?.user?.image!} - alt="Profile picture" - /> - <AvatarFallback> - {session?.user?.name?.split(" ").map((n) => n[0])}{" "} - </AvatarFallback> - </Avatar> - <span>{session?.user?.name?.split(" ")[0]}</span> - </div> - </div> - <AnimatePresence> - {selectedItem && <SubSidebar>{Subbar}</SubSidebar>} - </AnimatePresence> - </div> - ); -} - -const MenuItem = ({ - item: { icon, label, labelDisplay }, - selectedItem, - setSelectedItem, - ...props -}: { - item: MenuItem; - selectedItem: string | null; - setSelectedItem: React.Dispatch<React.SetStateAction<string | null>>; -}) => { - const handleClick = () => - setSelectedItem((prev) => (prev === label ? null : label)); - - return ( - <button - data-state-on={selectedItem === label} - onClick={handleClick} - className="on:opacity-100 on:bg-stone-300 focus-visible:ring-rgray-7 relative z-[100] flex w-full flex-col items-center justify-center rounded-md px-3 py-3 text-black opacity-80 ring-2 ring-transparent transition hover:bg-stone-300 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none" - {...props} - > - {icon} - <span className="">{labelDisplay ?? label}</span> - </button> - ); -}; - -export function SubSidebar({ children }: { children?: React.ReactNode }) { - return ( - <motion.div - initial={{ opacity: 0, x: "-100%" }} - animate={{ opacity: 1, x: 0 }} - exit={{ - opacity: 0, - x: "-100%", - transition: { delay: 0.2 }, - }} - transition={{ - duration: 0.2, - }} - className="absolute left-[100%] top-0 z-[10] hidden h-screen w-[30vw] items-start justify-center overflow-x-hidden border-r bg-stone-100 font-light md:flex" - > - <motion.div - initial={{ opacity: 0 }} - animate={{ opacity: 1 }} - exit={{ opacity: 0, transition: { delay: 0 } }} - transition={{ - delay: 0.2, - }} - className="z-[10] flex h-full w-full min-w-full flex-col items-center opacity-0" - > - <AnimatePresence>{children}</AnimatePresence> - </motion.div> - </motion.div> - ); -} diff --git a/apps/web/src/components/WordMark.tsx b/apps/web/src/components/WordMark.tsx deleted file mode 100644 index eb55647c..00000000 --- a/apps/web/src/components/WordMark.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { cn } from "@/lib/utils"; -import React from "react"; - -function WordMark({ className }: { className?: string }) { - return ( - <span className={cn(`text-xl font-bold tracking-tight ${className}`)}> - smort. - </span> - ); -} - -export default WordMark; diff --git a/apps/web/src/components/dev/SessionProviderWrapper.tsx b/apps/web/src/components/dev/SessionProviderWrapper.tsx deleted file mode 100644 index 71f77886..00000000 --- a/apps/web/src/components/dev/SessionProviderWrapper.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { SessionProvider } from "next-auth/react"; -import React from "react"; - -function SessionProviderWrapper({ children }: { children: React.ReactNode }) { - if (typeof window === "undefined") { - return <>{children}</>; - } else { - return <SessionProvider>{children}</SessionProvider>; - } -} - -export default SessionProviderWrapper; diff --git a/apps/web/src/components/dev/tailwindindicator.tsx b/apps/web/src/components/dev/tailwindindicator.tsx deleted file mode 100644 index fd70276d..00000000 --- a/apps/web/src/components/dev/tailwindindicator.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export function TailwindIndicator() { - if (process.env.NODE_ENV === "production") return null; - - return ( - <div> - <div className="fixed bottom-1 left-1 z-[999] flex size-6 items-center justify-center rounded-full border-2 bg-white p-3 font-mono text-xs text-black"> - <div className="block sm:hidden">xs</div> - <div className="hidden sm:block md:hidden">sm</div> - <div className="hidden md:block lg:hidden">md</div> - <div className="hidden lg:block xl:hidden">lg</div> - <div className="hidden xl:block 2xl:hidden">xl</div> - <div className="hidden 2xl:block">2xl</div> - </div> - </div> - ); -} diff --git a/apps/web/src/components/ui/avatar.tsx b/apps/web/src/components/ui/avatar.tsx deleted file mode 100644 index b36abf28..00000000 --- a/apps/web/src/components/ui/avatar.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as AvatarPrimitive from "@radix-ui/react-avatar"; - -import { cn } from "@/lib/utils"; - -const Avatar = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> ->(({ className, ...props }, ref) => ( - <AvatarPrimitive.Root - ref={ref} - className={cn( - "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", - className, - )} - {...props} - /> -)); -Avatar.displayName = AvatarPrimitive.Root.displayName; - -const AvatarImage = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Image>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> ->(({ className, ...props }, ref) => ( - <AvatarPrimitive.Image - ref={ref} - className={cn("aspect-square h-full w-full", className)} - {...props} - /> -)); -AvatarImage.displayName = AvatarPrimitive.Image.displayName; - -const AvatarFallback = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Fallback>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> ->(({ className, ...props }, ref) => ( - <AvatarPrimitive.Fallback - ref={ref} - className={cn( - "flex h-full w-full items-center justify-center rounded-full bg-gray-100", - className, - )} - {...props} - /> -)); -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; - -export { Avatar, AvatarImage, AvatarFallback }; diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx deleted file mode 100644 index fa390bec..00000000 --- a/apps/web/src/components/ui/badge.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const badgeVariants = cva( - "inline-flex items-center rounded-full border border-gray-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -export interface BadgeProps - extends React.HTMLAttributes<HTMLDivElement>, - VariantProps<typeof badgeVariants> {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( - <div className={cn(badgeVariants({ variant }), className)} {...props} /> - ); -} - -export { Badge, badgeVariants }; diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx deleted file mode 100644 index 24fa903e..00000000 --- a/apps/web/src/components/ui/button.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-gray-950 dark:focus-visible:ring-gray-300", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes<HTMLButtonElement>, - VariantProps<typeof buttonVariants> { - asChild?: boolean; -} - -const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - return ( - <button - className={cn(buttonVariants({ variant, size, className }))} - ref={ref} - {...props} - /> - ); - }, -); -Button.displayName = "Button"; - -export { Button, buttonVariants }; diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx deleted file mode 100644 index e98d500c..00000000 --- a/apps/web/src/components/ui/card.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn( - "rounded-lg border border-gray-200 bg-white text-gray-950 shadow-sm dark:border-gray-800 dark:bg-gray-950 dark:text-gray-50", - className, - )} - {...props} - /> -)); -Card.displayName = "Card"; - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("flex flex-col space-y-1.5 p-6", className)} - {...props} - /> -)); -CardHeader.displayName = "CardHeader"; - -const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLHeadingElement> ->(({ className, ...props }, ref) => ( - <h3 - ref={ref} - className={cn( - "text-2xl font-semibold leading-none tracking-tight", - className, - )} - {...props} - /> -)); -CardTitle.displayName = "CardTitle"; - -const CardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLParagraphElement> ->(({ className, ...props }, ref) => ( - <p - ref={ref} - className={cn("text-sm text-gray-500 dark:text-gray-400", className)} - {...props} - /> -)); -CardDescription.displayName = "CardDescription"; - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> -)); -CardContent.displayName = "CardContent"; - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("flex items-center p-6 pt-0", className)} - {...props} - /> -)); -CardFooter.displayName = "CardFooter"; - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardDescription, - CardContent, -}; diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx deleted file mode 100644 index afc2cf46..00000000 --- a/apps/web/src/components/ui/command.tsx +++ /dev/null @@ -1,161 +0,0 @@ -"use client"; - -import * as React from "react"; -import { type DialogProps } from "@radix-ui/react-dialog"; -import { Command as CommandPrimitive } from "cmdk"; -import { Loader, Search } from "lucide-react"; - -import { cn } from "@/lib/utils"; -import { Dialog, DialogContent } from "@/components/ui/dialog"; - -const Command = React.forwardRef< - React.ElementRef<typeof CommandPrimitive>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive> ->(({ className, ...props }, ref) => ( - <CommandPrimitive - ref={ref} - className={cn( - "bg-rgray-3 text-rgray-11 flex h-full w-full flex-col overflow-hidden rounded-md focus-visible:outline-none [&>[cmdk-list-sizer]]:max-h-[250px] [&>[cmdk-list-sizer]]:overflow-y-scroll", - className, - )} - {...props} - /> -)); -Command.displayName = CommandPrimitive.displayName; - -interface CommandDialogProps extends DialogProps {} - -const CommandDialog = ({ children, ...props }: CommandDialogProps) => { - return ( - <Dialog {...props}> - <DialogContent className="overflow-hidden p-0 shadow-lg"> - <Command className="[&_[cmdk-group-heading]]:text-rgray-11 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> - {children} - </Command> - </DialogContent> - </Dialog> - ); -}; - -const CommandInput = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Input>, - 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="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( - "placeholder:text-rgray-11/50 flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50", - className, - )} - {...props} - /> - </div> -)); - -CommandInput.displayName = CommandPrimitive.Input.displayName; - -const CommandList = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.List>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.List - ref={ref} - className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} - {...props} - /> -)); - -CommandList.displayName = CommandPrimitive.List.displayName; - -const CommandEmpty = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Empty>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> ->((props, ref) => ( - <CommandPrimitive.Empty - ref={ref} - className="py-6 text-center text-sm" - {...props} - /> -)); - -CommandEmpty.displayName = CommandPrimitive.Empty.displayName; - -const CommandGroup = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Group>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Group - ref={ref} - className={cn( - "text-rgray-12 [&_[cmdk-group-heading]]:text-rgray-11 overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium", - className, - )} - {...props} - /> -)); - -CommandGroup.displayName = CommandPrimitive.Group.displayName; - -const CommandSeparator = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Separator - ref={ref} - className={cn("bg-rgray-3 -mx-1 h-px", className)} - {...props} - /> -)); -CommandSeparator.displayName = CommandPrimitive.Separator.displayName; - -const CommandItem = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Item - ref={ref} - className={cn( - "aria-selected:bg-rgray-5 aria-selected:text-rgray-12 relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm opacity-70 outline-none data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50", - className, - )} - {...props} - /> -)); - -CommandItem.displayName = CommandPrimitive.Item.displayName; - -const CommandShortcut = ({ - className, - ...props -}: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn("text-gray-11 ml-auto text-xs tracking-widest", className)} - {...props} - /> - ); -}; -CommandShortcut.displayName = "CommandShortcut"; - -export { - Command, - CommandDialog, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandShortcut, - CommandSeparator, -}; diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx deleted file mode 100644 index 0da54769..00000000 --- a/apps/web/src/components/ui/dialog.tsx +++ /dev/null @@ -1,119 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { X } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const Dialog = DialogPrimitive.Root; - -const DialogTrigger = DialogPrimitive.Trigger; - -const DialogPortal = DialogPrimitive.Portal; - -const DialogClose = DialogPrimitive.Close; - -const DialogOverlay = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Overlay - ref={ref} - className={cn( - "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", - className, - )} - {...props} - /> -)); -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; - -const DialogContent = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> ->(({ className, children, ...props }, ref) => ( - <DialogPortal> - <DialogOverlay /> - <DialogPrimitive.Content - ref={ref} - className={cn( - "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] border-rgray-6 fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] rounded-lg border bg-[#F4F3F2] p-6 text-black shadow-lg duration-200", - className, - )} - {...props} - > - {children} - <DialogPrimitive.Close className="ring-offset-rgray-2 focus:ring-rgray-7 absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:text-black"> - <X className="h-4 w-4" /> - <span className="sr-only">Close</span> - </DialogPrimitive.Close> - </DialogPrimitive.Content> - </DialogPortal> -)); -DialogContent.displayName = DialogPrimitive.Content.displayName; - -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn("flex flex-col space-y-1.5 text-left", className)} - {...props} - /> -); -DialogHeader.displayName = "DialogHeader"; - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "mt-5 flex flex-row sm:flex-row sm:justify-end sm:space-x-2", - className, - )} - {...props} - /> -); -DialogFooter.displayName = "DialogFooter"; - -const DialogTitle = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Title - ref={ref} - className={cn( - "mb-1 text-xl font-medium leading-none tracking-tight", - className, - )} - {...props} - /> -)); -DialogTitle.displayName = DialogPrimitive.Title.displayName; - -const DialogDescription = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Description - ref={ref} - className={cn("text-sm text-slate-800", className)} - {...props} - /> -)); -DialogDescription.displayName = DialogPrimitive.Description.displayName; - -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -}; diff --git a/apps/web/src/components/ui/drawer.tsx b/apps/web/src/components/ui/drawer.tsx deleted file mode 100644 index 8ba01253..00000000 --- a/apps/web/src/components/ui/drawer.tsx +++ /dev/null @@ -1,124 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Drawer as DrawerPrimitive } from "vaul"; - -import { cn } from "@/lib/utils"; - -const Drawer = ({ - shouldScaleBackground = true, - ...props -}: React.ComponentProps<typeof DrawerPrimitive.Root>) => ( - <DrawerPrimitive.Root - shouldScaleBackground={shouldScaleBackground} - {...props} - /> -); -Drawer.displayName = "Drawer"; - -const DrawerTrigger = DrawerPrimitive.Trigger; - -const DrawerPortal = DrawerPrimitive.Portal; - -const DrawerClose = DrawerPrimitive.Close; - -const DrawerOverlay = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> ->(({ className, ...props }, ref) => ( - <DrawerPrimitive.Overlay - ref={ref} - data-drawer-overlay - className={cn("fixed inset-0 z-50 bg-black/80", className)} - {...props} - /> -)); -DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; - -const DrawerContent = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> & { - overlay?: boolean; - handle?: boolean; - } ->(({ className, children, overlay = true, handle = true, ...props }, ref) => ( - <DrawerPortal> - {overlay && <DrawerOverlay />} - <DrawerPrimitive.Content - ref={ref} - className={cn( - "border-rgray-6 bg-rgray-2 fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border", - className, - )} - {...props} - > - {handle && ( - <div className="bg-rgray-4 mx-auto mb-1 h-2 w-[100px] rounded-full " /> - )} - {children} - </DrawerPrimitive.Content> - </DrawerPortal> -)); -DrawerContent.displayName = "DrawerContent"; - -const DrawerHeader = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} - {...props} - /> -); -DrawerHeader.displayName = "DrawerHeader"; - -const DrawerFooter = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn("mt-auto flex flex-col gap-2 p-4", className)} - {...props} - /> -); -DrawerFooter.displayName = "DrawerFooter"; - -const DrawerTitle = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> ->(({ className, ...props }, ref) => ( - <DrawerPrimitive.Title - ref={ref} - className={cn( - "text-rgray-12 text-xl font-medium leading-none tracking-tight", - className, - )} - {...props} - /> -)); -DrawerTitle.displayName = DrawerPrimitive.Title.displayName; - -const DrawerDescription = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> ->(({ className, ...props }, ref) => ( - <DrawerPrimitive.Description - ref={ref} - className={cn("text-rgray-11 text-md", className)} - {...props} - /> -)); -DrawerDescription.displayName = DrawerPrimitive.Description.displayName; - -export { - Drawer, - DrawerPortal, - DrawerOverlay, - DrawerTrigger, - DrawerClose, - DrawerContent, - DrawerHeader, - DrawerFooter, - DrawerTitle, - DrawerDescription, -}; diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx deleted file mode 100644 index fbe2d99c..00000000 --- a/apps/web/src/components/ui/dropdown-menu.tsx +++ /dev/null @@ -1,200 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; -import { Check, ChevronRight, Circle } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const DropdownMenu = DropdownMenuPrimitive.Root; - -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; - -const DropdownMenuGroup = DropdownMenuPrimitive.Group; - -const DropdownMenuPortal = DropdownMenuPrimitive.Portal; - -const DropdownMenuSub = DropdownMenuPrimitive.Sub; - -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; - -const DropdownMenuSubTrigger = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { - inset?: boolean; - } ->(({ className, inset, children, ...props }, ref) => ( - <DropdownMenuPrimitive.SubTrigger - ref={ref} - className={cn( - "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-gray-100 data-[state=open]:bg-gray-100 dark:focus:bg-gray-800 dark:data-[state=open]:bg-gray-800", - inset && "pl-8", - className, - )} - {...props} - > - {children} - <ChevronRight className="ml-auto h-4 w-4" /> - </DropdownMenuPrimitive.SubTrigger> -)); -DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName; - -const DropdownMenuSubContent = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> ->(({ className, ...props }, ref) => ( - <DropdownMenuPrimitive.SubContent - ref={ref} - className={cn( - "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 bg-white p-1 text-gray-950 shadow-lg", - className, - )} - {...props} - /> -)); -DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName; - -const DropdownMenuContent = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> ->(({ className, sideOffset = 4, ...props }, ref) => ( - <DropdownMenuPrimitive.Portal> - <DropdownMenuPrimitive.Content - ref={ref} - sideOffset={sideOffset} - className={cn( - "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-rgray-6 z-50 min-w-[9rem] overflow-hidden rounded-md border bg-white p-1 text-black shadow-md", - className, - )} - {...props} - /> - </DropdownMenuPrimitive.Portal> -)); -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; - -const DropdownMenuItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - <DropdownMenuPrimitive.Item - ref={ref} - className={cn( - "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-stone-200 focus:text-slate-800 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 ", - inset && "pl-8", - className, - )} - {...props} - /> -)); -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; - -const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> ->(({ className, children, checked, ...props }, ref) => ( - <DropdownMenuPrimitive.CheckboxItem - ref={ref} - className={cn( - "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-50", - className, - )} - checked={checked} - {...props} - > - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Check className="h-4 w-4" /> - </DropdownMenuPrimitive.ItemIndicator> - </span> - {children} - </DropdownMenuPrimitive.CheckboxItem> -)); -DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName; - -const DropdownMenuRadioItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> ->(({ className, children, ...props }, ref) => ( - <DropdownMenuPrimitive.RadioItem - ref={ref} - className={cn( - "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-50", - className, - )} - {...props} - > - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Circle className="h-2 w-2 fill-current" /> - </DropdownMenuPrimitive.ItemIndicator> - </span> - {children} - </DropdownMenuPrimitive.RadioItem> -)); -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; - -const DropdownMenuLabel = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Label>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - <DropdownMenuPrimitive.Label - ref={ref} - className={cn( - "px-2 py-1.5 text-sm font-semibold", - inset && "pl-8", - className, - )} - {...props} - /> -)); -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; - -const DropdownMenuSeparator = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> ->(({ className, ...props }, ref) => ( - <DropdownMenuPrimitive.Separator - ref={ref} - className={cn("-mx-1 my-1 h-px bg-gray-100", className)} - {...props} - /> -)); -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; - -const DropdownMenuShortcut = ({ - className, - ...props -}: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn("ml-auto text-xs tracking-widest opacity-60", className)} - {...props} - /> - ); -}; -DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; - -export { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuRadioGroup, -}; diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx deleted file mode 100644 index 9d925512..00000000 --- a/apps/web/src/components/ui/input.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -export interface InputProps - extends React.InputHTMLAttributes<HTMLInputElement> {} - -const Input = React.forwardRef<HTMLInputElement, InputProps>( - ({ className, type, ...props }, ref) => { - return ( - <input - type={type} - className={cn( - "border-rgray-6 focus-visible:ring-rgray-7 flex h-10 w-full rounded-md border bg-transparent px-3 py-2 text-sm font-normal transition file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-black/50 focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50 ", - className, - )} - ref={ref} - {...props} - /> - ); - }, -); - -export interface InputWithIconProps - extends React.InputHTMLAttributes<HTMLInputElement> { - icon: React.ReactNode; -} - -const InputWithIcon = React.forwardRef<HTMLInputElement, InputWithIconProps>( - ({ className, type, icon, ...props }, ref) => { - return ( - <div - className={cn( - "border-rgray-1/70 text-rgray-11 focus-within:ring-rgray-7 flex h-10 w-full items-center justify-center gap-2 rounded-md border bg-transparent px-3 py-2 text-sm font-normal transition focus-within:outline-none focus-within:ring-2 ", // TODO: change to black - className, - )} - > - {icon} - <input - type={type} - className={ - "w-full bg-transparent font-normal file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-black/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" - } - ref={ref} - {...props} - /> - </div> - ); - }, -); -InputWithIcon.displayName = "Input"; - -export { Input, InputWithIcon }; diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx deleted file mode 100644 index 84f8b0c7..00000000 --- a/apps/web/src/components/ui/label.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as LabelPrimitive from "@radix-ui/react-label"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", -); - -const Label = React.forwardRef< - React.ElementRef<typeof LabelPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & - VariantProps<typeof labelVariants> ->(({ className, ...props }, ref) => ( - <LabelPrimitive.Root - ref={ref} - className={cn(labelVariants(), className)} - {...props} - /> -)); -Label.displayName = LabelPrimitive.Root.displayName; - -export { Label }; diff --git a/apps/web/src/components/ui/popover.tsx b/apps/web/src/components/ui/popover.tsx deleted file mode 100644 index cabe76a9..00000000 --- a/apps/web/src/components/ui/popover.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as PopoverPrimitive from "@radix-ui/react-popover"; - -import { cn } from "@/lib/utils"; - -const Popover = PopoverPrimitive.Root; - -const PopoverTrigger = PopoverPrimitive.Trigger; - -const PopoverContent = React.forwardRef< - React.ElementRef<typeof PopoverPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & { - animate?: boolean; - } ->( - ( - { className, align = "center", animate = true, sideOffset = 4, ...props }, - ref, - ) => ( - <PopoverPrimitive.Portal> - <PopoverPrimitive.Content - ref={ref} - align={align} - sideOffset={sideOffset} - className={cn( - "border-rgray-6 bg-rgray-3 text-rgray-11 z-50 w-72 rounded-md border p-4 shadow-md outline-none", - animate && - "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className, - )} - {...props} - /> - </PopoverPrimitive.Portal> - ), -); -PopoverContent.displayName = PopoverPrimitive.Content.displayName; - -export { Popover, PopoverTrigger, PopoverContent }; diff --git a/apps/web/src/components/ui/textarea.tsx b/apps/web/src/components/ui/textarea.tsx deleted file mode 100644 index 3b2c9ddd..00000000 --- a/apps/web/src/components/ui/textarea.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; -import { HTMLMotionProps, motion } from "framer-motion"; - -export interface TextareaProps - extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} - -const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( - ({ className, ...props }, ref) => { - return ( - <textarea - className={cn( - "border-rgray-6 text-rgray-11 placeholder:text-rgray-11/70 focus-within:ring-rgray-7 flex min-h-[80px] w-full rounded-md border bg-transparent px-3 py-2 text-sm font-normal transition focus-within:outline-none focus-within:ring-2 disabled:cursor-not-allowed disabled:opacity-50", - className, - )} - ref={ref} - {...props} - /> - ); - }, -); -Textarea.displayName = "Textarea"; - -export interface Textarea2Props extends HTMLMotionProps<"div"> { - textAreaProps?: TextareaProps; - children: React.ReactNode | React.ReactNode[]; -} - -const Textarea2 = React.forwardRef<HTMLDivElement, Textarea2Props>( - ({ className, children, textAreaProps: _textAreaProps, ...props }, ref) => { - const { className: textAreaClassName, ...textAreaProps } = - _textAreaProps || {}; - return ( - <motion.div - ref={ref} - className={cn( - "border-rgray-6 text-rgray-11 has-[textarea:focus-visible]:ring-rgray-7 flex h-auto min-h-[80px] w-full flex-col items-start justify-center rounded-md border bg-transparent px-3 py-2 text-sm transition has-[textarea:focus-visible]:ring-2", - className, - )} - {...props} - > - <textarea - className={cn( - "text-rgray-11 h-full w-full resize-none bg-transparent placeholder:text-white/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50", - textAreaClassName, - )} - {...textAreaProps} - /> - {children} - </motion.div> - ); - }, -); -Textarea2.displayName = "Textarea2"; - -export { Textarea, Textarea2 }; diff --git a/apps/web/src/contexts/MemoryContext.tsx b/apps/web/src/contexts/MemoryContext.tsx deleted file mode 100644 index 09412465..00000000 --- a/apps/web/src/contexts/MemoryContext.tsx +++ /dev/null @@ -1,241 +0,0 @@ -"use client"; -import React, { useCallback } from "react"; -import { - ChachedSpaceContent, - StoredContent, - storedContent, - StoredSpace, -} from "@/server/db/schema"; -import { - addMemory, - searchMemoriesAndSpaces, - addSpace, - fetchContentForSpace, - deleteSpace, - deleteMemory, - fetchFreeMemories, - updateMemory, - updateSpaceTitle, - addContentInSpaces, -} from "@/actions/db"; -import { User } from "next-auth"; - -export type SearchResult = { - type: "memory" | "space"; - space: StoredSpace; - memory: StoredContent; -}; - -// temperory (will change) -export const MemoryContext = React.createContext<{ - spaces: StoredSpace[]; - freeMemories: StoredContent[]; - addSpace: typeof addSpace; - addMemory: typeof addMemory; - cachedMemories: ChachedSpaceContent[]; - search: typeof searchMemoriesAndSpaces; - deleteSpace: typeof deleteSpace; - deleteMemory: typeof deleteMemory; - updateMemory: typeof updateMemory; - updateSpace: typeof updateSpaceTitle; - addMemoriesToSpace: typeof addContentInSpaces; -}>({ - spaces: [], - freeMemories: [], - 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, - updateMemory: (() => {}) as unknown as typeof updateMemory, - updateSpace: (() => {}) as unknown as typeof updateSpaceTitle, - addMemoriesToSpace: (() => {}) as unknown as typeof addContentInSpaces, -}); - -export const MemoryProvider: React.FC< - { - spaces: StoredSpace[]; - freeMemories: StoredContent[]; - cachedMemories: ChachedSpaceContent[]; - user: User; - } & React.PropsWithChildren -> = ({ - 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))!; - - setSpaces((prev) => prev.filter((i) => i.id !== deleted.id)); - setCachedMemories((prev) => prev.filter((i) => i.space !== deleted.id)); - - setFreeMemories(await fetchFreeMemories()); - - return deleted; - }; - - const _deleteMemory: typeof deleteMemory = async (...params) => { - const deleted = (await deleteMemory(...params))!; - - setCachedMemories((prev) => prev.filter((i) => i.id !== deleted.id)); - setFreeMemories(await fetchFreeMemories()); - - 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 _updateMemory: typeof updateMemory = async (id, _data) => { - const data = await updateMemory(id, _data); - - let contents: ChachedSpaceContent[] = []; - - await Promise.all([ - spaces.forEach(async (space) => { - console.log("fetching "); - const data = ( - (await fetchContentForSpace(space.id, { - offset: 0, - limit: 3, - })) ?? [] - ).map((data) => ({ - ...data, - space: space.id, - })); - contents = [...contents, ...data]; - }), - ]); - - const freeMemories = await fetchFreeMemories(); - - setCachedMemories(contents); - setFreeMemories(freeMemories); - - return data; - }; - - const _updateSpace: typeof updateSpaceTitle = async (...params) => { - const updatedSpace = await updateSpaceTitle(...params); - - if (updatedSpace) { - setSpaces((prev) => - prev.map((i) => (i.id === updatedSpace.id ? updatedSpace : i)), - ); - } - - return updatedSpace; - }; - - const addMemoriesToSpace: typeof addContentInSpaces = async (...params) => { - const data = await addContentInSpaces(...params); - - let contents: ChachedSpaceContent[] = []; - - await Promise.all([ - spaces.forEach(async (space) => { - console.log("fetching "); - const data = ( - (await fetchContentForSpace(space.id, { - offset: 0, - limit: 3, - })) ?? [] - ).map((data) => ({ - ...data, - space: space.id, - })); - contents = [...contents, ...data]; - }), - ]); - - const freeMemories = await fetchFreeMemories(); - - setCachedMemories(contents); - setFreeMemories(freeMemories); - - return data; - }; - - return ( - <MemoryContext.Provider - value={{ - updateSpace: _updateSpace, - search: searchMemoriesAndSpaces, - spaces, - addSpace: _addSpace, - deleteSpace: _deleteSpace, - freeMemories, - cachedMemories, - deleteMemory: _deleteMemory, - addMemory: _addMemory, - updateMemory: _updateMemory, - addMemoriesToSpace, - }} - > - {children} - </MemoryContext.Provider> - ); -}; - -export const useMemory = () => { - const context = React.useContext(MemoryContext); - if (context === undefined) { - throw new Error("useMemory must be used within a MemoryProvider"); - } - return context; -}; diff --git a/apps/web/src/env.js b/apps/web/src/env.js deleted file mode 100644 index 2495d75b..00000000 --- a/apps/web/src/env.js +++ /dev/null @@ -1,59 +0,0 @@ -import { createEnv } from "@t3-oss/env-nextjs"; -import { z } from "zod"; - -export const env = createEnv({ - /** - * Specify your server-side environment variables schema here. This way you can ensure the app - * isn't built with invalid env vars. - */ - server: { - NODE_ENV: z - .enum(["development", "test", "production"]) - .default("development"), - NEXTAUTH_SECRET: - process.env.NODE_ENV === "production" ? z.string() : z.string(), - NEXTAUTH_URL: z.preprocess( - // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL - // Since NextAuth.js automatically uses the VERCEL_URL if present. - (str) => process.env.VERCEL_URL ?? str, - // VERCEL_URL doesn't include `https` so it cant be validated as a URL - process.env.VERCEL ? z.string() : z.string().url(), - ), - GOOGLE_CLIENT_ID: z.string(), - GOOGLE_CLIENT_SECRET: z.string(), - BACKEND_SECURITY_KEY: z.string(), - }, - - /** - * Specify your client-side environment variables schema here. This way you can ensure the app - * isn't built with invalid env vars. To expose them to the client, prefix them with - * `NEXT_PUBLIC_`. - */ - client: { - // NEXT_PUBLIC_CLIENTVAR: z.string(), - }, - - /** - * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. - * middlewares) or client-side so we need to destruct manually. - */ - runtimeEnv: { - DATABASE_URL: process.env.DATABASE_URL, - NODE_ENV: process.env.NODE_ENV, - NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, - NEXTAUTH_URL: process.env.NEXTAUTH_URL, - GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, - BACKEND_SECURITY_KEY: process.env.BACKEND_SECURITY_KEY, - }, - /** - * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially - * useful for Docker builds. - */ - skipValidation: !!process.env.SKIP_ENV_VALIDATION, - /** - * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and - * `SOME_VAR=''` will throw an error. - */ - emptyStringAsUndefined: true, -}); diff --git a/apps/web/src/hooks/useDebounce.ts b/apps/web/src/hooks/useDebounce.ts deleted file mode 100644 index d133b1ae..00000000 --- a/apps/web/src/hooks/useDebounce.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useState } from "react"; - -/** - * Use this hook when you need to debounce a value. - * @param value - * @param delay in milliseconds - */ -export const useDebounce = <T>(value: T, delay: number) => { - // State and setters for debounced value - const [debouncedValue, setDebouncedValue] = useState<T>(value); - - useEffect(() => { - // Update debounced value after delay - const handler = setTimeout(() => { - setDebouncedValue(value); - }, delay); - - // Cancel the timeout if value changes (also on delay change or unmount) - // This is how we prevent debounced value from updating if the value is changed - // within the delay period. Timeout gets cleared and restarted. - return () => { - clearTimeout(handler); - }; - }, [value, delay]); - - return debouncedValue; -}; diff --git a/apps/web/src/hooks/useTouchHold.ts b/apps/web/src/hooks/useTouchHold.ts deleted file mode 100644 index 52e56491..00000000 --- a/apps/web/src/hooks/useTouchHold.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useState } from "react"; - -// holdDuration (in ms) -const useTouchHold = ({ - onHold, - holdDuration = 500, -}: { - holdDuration?: number; - onHold: () => Promise<void> | void; -}) => { - const [touchTimeout, setTouchTimeout] = useState<ReturnType< - typeof setTimeout - > | null>(null); - - return { - onTouchStart: () => { - setTouchTimeout(setTimeout(onHold, holdDuration)); - }, - onTouchEnd: () => { - if (touchTimeout) { - clearTimeout(touchTimeout); - } - }, - }; -}; - -export default useTouchHold; diff --git a/apps/web/src/hooks/useViewport.ts b/apps/web/src/hooks/useViewport.ts deleted file mode 100644 index 7ba90861..00000000 --- a/apps/web/src/hooks/useViewport.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useState, useEffect } from "react"; - -function getViewport() { - try { - const { innerWidth: width, innerHeight: height } = window ?? { - innerWidth: 0, - innerHeight: 0, - }; - return { - width, - height, - }; - } catch { - return { - width: 0, - height: 0, - }; - } -} - -export default function useViewport() { - const [viewport, setViewport] = useState(getViewport()); - - useEffect(() => { - function handleResize() { - setViewport(getViewport()); - } - - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }, []); - - return viewport; -} diff --git a/apps/web/src/lib/icons.tsx b/apps/web/src/lib/icons.tsx deleted file mode 100644 index 51a4714e..00000000 --- a/apps/web/src/lib/icons.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from "react"; -import type { SVGProps } from "react"; -const Chrome = (props: SVGProps<SVGSVGElement>) => ( - <svg - xmlns="http://www.w3.org/2000/svg" - preserveAspectRatio="xMidYMid" - viewBox="0 0 190.5 190.5" - width="1em" - height="1em" - {...props} - > - <path - fill="#fff" - d="M95.252 142.873c26.304 0 47.627-21.324 47.627-47.628s-21.323-47.628-47.627-47.628-47.627 21.324-47.627 47.628 21.323 47.628 47.627 47.628z" - /> - <path - fill="#229342" - d="m54.005 119.07-41.24-71.43a95.227 95.227 0 0 0-.003 95.25 95.234 95.234 0 0 0 82.496 47.61l41.24-71.43v-.011a47.613 47.613 0 0 1-17.428 17.443 47.62 47.62 0 0 1-47.632.007 47.62 47.62 0 0 1-17.433-17.437z" - /> - <path - fill="#fbc116" - d="m136.495 119.067-41.239 71.43a95.229 95.229 0 0 0 82.489-47.622A95.24 95.24 0 0 0 190.5 95.248a95.237 95.237 0 0 0-12.772-47.623H95.249l-.01.007a47.62 47.62 0 0 1 23.819 6.372 47.618 47.618 0 0 1 17.439 17.431 47.62 47.62 0 0 1-.001 47.633z" - /> - <path - fill="#1a73e8" - d="M95.252 132.961c20.824 0 37.705-16.881 37.705-37.706S116.076 57.55 95.252 57.55 57.547 74.431 57.547 95.255s16.881 37.706 37.705 37.706z" - /> - <path - fill="#e33b2e" - d="M95.252 47.628h82.479A95.237 95.237 0 0 0 142.87 12.76 95.23 95.23 0 0 0 95.245 0a95.222 95.222 0 0 0-47.623 12.767 95.23 95.23 0 0 0-34.856 34.872l41.24 71.43.011.006a47.62 47.62 0 0 1-.015-47.633 47.61 47.61 0 0 1 41.252-23.815z" - /> - </svg> -); -export default Chrome; diff --git a/apps/web/src/lib/searchParams.ts b/apps/web/src/lib/searchParams.ts deleted file mode 100644 index aae3f4c7..00000000 --- a/apps/web/src/lib/searchParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { - createSearchParamsCache, - parseAsInteger, - parseAsString, -} from "nuqs/server"; -// Note: import from 'nuqs/server' to avoid the "use client" directive - -export const searchParamsCache = createSearchParamsCache({ - // List your search param keys and associated parsers here: - q: parseAsString.withDefault(""), - maxResults: parseAsInteger.withDefault(10), -}); diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts deleted file mode 100644 index 81fa8549..00000000 --- a/apps/web/src/lib/utils.ts +++ /dev/null @@ -1,111 +0,0 @@ -"use client"; -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} - -// removes http(s?):// and / from the url -export function cleanUrl(url: string) { - if (url.endsWith("/")) { - url = url.slice(0, -1); - } - return url.startsWith("https://") - ? url.slice(8) - : url.startsWith("http://") - ? url.slice(7) - : url; -} - -export function getIdsFromSource(sourceIds: string[]) { - console.log(sourceIds); - return sourceIds.map((id) => { - const parts = id.split("-"); - if (parts.length > 1) { - return parts.slice(0, -1).join("-"); - } else { - return id; - } - }); -} - -export function generateId() { - return Math.random().toString(36).slice(2, 9); -} - -export function svgId(prefix: string, id: string) { - return `${prefix}-${id}`; -} - -export function countLines(textarea: HTMLTextAreaElement): number { - let _buffer: HTMLTextAreaElement | null = null; - - if (_buffer == null) { - _buffer = document.createElement("textarea"); - _buffer.style.border = "none"; - _buffer.style.height = "0"; - _buffer.style.overflow = "hidden"; - _buffer.style.padding = "0"; - _buffer.style.position = "absolute"; - _buffer.style.left = "0"; - _buffer.style.top = "0"; - _buffer.style.zIndex = "-1"; - document.body.appendChild(_buffer); - } - - const cs = window.getComputedStyle(textarea); - const pl = parseInt(cs.paddingLeft as string); - const pr = parseInt(cs.paddingRight as string); - let lh = parseInt(cs.lineHeight as string); - - // [cs.lineHeight] may return 'normal', which means line height = font size. - if (isNaN(lh)) lh = parseInt(cs.fontSize as string); - - // Copy content width. - if (_buffer) { - _buffer.style.width = textarea.clientWidth - pl - pr + "px"; - - // Copy text properties. - _buffer.style.font = cs.font as string; - _buffer.style.letterSpacing = cs.letterSpacing as string; - _buffer.style.whiteSpace = cs.whiteSpace as string; - _buffer.style.wordBreak = cs.wordBreak as string; - _buffer.style.wordSpacing = cs.wordSpacing as string; - _buffer.style.wordWrap = cs.wordWrap as string; - - // Copy value. - _buffer.value = textarea.value; - - const result = Math.floor(_buffer.scrollHeight / lh); - return result > 0 ? result : 1; - } - - return 0; -} - -export function convertRemToPixels(rem: number) { - return rem * parseFloat(getComputedStyle(document.documentElement).fontSize); -} - -export function isArraysEqual(a: any[], b: any[]) { - if (a === b) return true; - if (a == null || b == null) return false; - if (a.length !== b.length) return false; - - let isEqual = true; - - a.forEach((i) => { - if (!isEqual) return; - isEqual = b.includes(i); - }); - - if (!isEqual) return isEqual; - - b.forEach((i) => { - if (!isEqual) return; - isEqual = a.includes(i); - }); - - return isEqual; -} diff --git a/apps/web/src/server/auth.ts b/apps/web/src/server/auth.ts deleted file mode 100644 index 95edcf35..00000000 --- a/apps/web/src/server/auth.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { env } from "@/env"; -import NextAuth from "next-auth"; -import Google from "next-auth/providers/google"; -import { DrizzleAdapter } from "@auth/drizzle-adapter"; -import { db } from "./db"; - -export const { - handlers: { GET, POST }, - auth, -} = NextAuth({ - secret: env.NEXTAUTH_SECRET, - trustHost: true, - callbacks: { - session: ({ session, token, user }) => ({ - ...session, - user: { - ...session.user, - id: user.id, - }, - }), - }, - adapter: DrizzleAdapter(db), - providers: [ - Google({ - clientId: env.GOOGLE_CLIENT_ID, - clientSecret: env.GOOGLE_CLIENT_SECRET, - }), - ], -}); diff --git a/apps/web/src/server/db/index.ts b/apps/web/src/server/db/index.ts deleted file mode 100644 index 4d671bea..00000000 --- a/apps/web/src/server/db/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { drizzle } from "drizzle-orm/d1"; - -import * as schema from "./schema"; - -export const db = drizzle(process.env.DATABASE, { schema, logger: true }); diff --git a/apps/web/src/server/db/schema.ts b/apps/web/src/server/db/schema.ts deleted file mode 100644 index cd2756f1..00000000 --- a/apps/web/src/server/db/schema.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { relations, sql } from "drizzle-orm"; -import { - index, - int, - primaryKey, - sqliteTableCreator, - text, - integer, - unique, -} from "drizzle-orm/sqlite-core"; - -export const createTable = sqliteTableCreator((name) => `${name}`); - -export const users = createTable("user", { - id: text("id", { length: 255 }).notNull().primaryKey(), - name: text("name", { length: 255 }), - email: text("email", { length: 255 }).notNull(), - emailVerified: int("emailVerified", { mode: "timestamp" }).default( - sql`CURRENT_TIMESTAMP`, - ), - image: text("image", { length: 255 }), -}); - -export type User = typeof users.$inferSelect; - -export const usersRelations = relations(users, ({ many }) => ({ - accounts: many(accounts), - sessions: many(sessions), -})); - -export const accounts = createTable( - "account", - { - id: integer("id").notNull().primaryKey({ autoIncrement: true }), - userId: text("userId", { length: 255 }) - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - type: text("type", { length: 255 }).notNull(), - provider: text("provider", { length: 255 }).notNull(), - providerAccountId: text("providerAccountId", { length: 255 }).notNull(), - refresh_token: text("refresh_token"), - access_token: text("access_token"), - expires_at: int("expires_at"), - token_type: text("token_type", { length: 255 }), - scope: text("scope", { length: 255 }), - id_token: text("id_token"), - session_state: text("session_state", { length: 255 }), - oauth_token_secret: text("oauth_token_secret"), - oauth_token: text("oauth_token"), - }, - (account) => ({ - userIdIdx: index("account_userId_idx").on(account.userId), - }), -); - -export const sessions = createTable( - "session", - { - id: integer("id").notNull().primaryKey({ autoIncrement: true }), - sessionToken: text("sessionToken", { length: 255 }).notNull(), - userId: text("userId", { length: 255 }) - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - expires: int("expires", { mode: "timestamp" }).notNull(), - }, - (session) => ({ - userIdIdx: index("session_userId_idx").on(session.userId), - }), -); - -export const verificationTokens = createTable( - "verificationToken", - { - identifier: text("identifier", { length: 255 }).notNull(), - token: text("token", { length: 255 }).notNull(), - expires: int("expires", { mode: "timestamp" }).notNull(), - }, - (vt) => ({ - compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), - }), -); - -export const storedContent = createTable( - "storedContent", - { - id: integer("id").notNull().primaryKey({ autoIncrement: true }), - content: text("content").notNull(), - title: text("title", { length: 255 }), - description: text("description", { length: 255 }), - url: text("url").notNull(), - savedAt: int("savedAt", { mode: "timestamp" }).notNull(), - baseUrl: text("baseUrl", { length: 255 }), - type: text("type", { enum: ["note", "page", "twitter-bookmark"] }).default( - "page", - ), - image: text("image", { length: 255 }), - user: text("user", { length: 255 }).references(() => users.id, { - onDelete: "cascade", - }), - }, - (sc) => ({ - urlIdx: index("storedContent_url_idx").on(sc.url), - savedAtIdx: index("storedContent_savedAt_idx").on(sc.savedAt), - titleInx: index("storedContent_title_idx").on(sc.title), - userIdx: index("storedContent_user_idx").on(sc.user), - }), -); - -export const contentToSpace = createTable( - "contentToSpace", - { - contentId: integer("contentId") - .notNull() - .references(() => storedContent.id, { onDelete: "cascade" }), - spaceId: integer("spaceId") - .notNull() - .references(() => space.id, { onDelete: "cascade" }), - }, - (cts) => ({ - compoundKey: primaryKey({ columns: [cts.contentId, cts.spaceId] }), - }), -); - -export const space = createTable( - "space", - { - id: integer("id").notNull().primaryKey({ autoIncrement: true }), - name: text("name").notNull().unique().default("none"), - user: text("user", { length: 255 }).references(() => users.id, { - onDelete: "cascade", - }), - }, - (space) => ({ - nameIdx: index("spaces_name_idx").on(space.name), - userIdx: index("spaces_user_idx").on(space.user), - }), -); - -export type StoredContent = Omit<typeof storedContent.$inferSelect, "user">; -export type StoredSpace = typeof space.$inferSelect; -export type ChachedSpaceContent = StoredContent & { - space: number; -}; diff --git a/apps/web/src/server/db/test.ts b/apps/web/src/server/db/test.ts deleted file mode 100644 index 37969e5e..00000000 --- a/apps/web/src/server/db/test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { db } from "."; -import { space, user } from "./schema"; - -const user = await db.select(user).all(); - -await db.insert(space).values([{}]); diff --git a/apps/web/src/server/helpers.ts b/apps/web/src/server/helpers.ts deleted file mode 100644 index 9a9a9607..00000000 --- a/apps/web/src/server/helpers.ts +++ /dev/null @@ -1,39 +0,0 @@ -"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); - - // 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); - } - - // Prepare the metadata object - const metadata = { - title, - description, - image: favicon, - baseUrl, - }; - return metadata; -} |