From cd71d6b4f99c09eb4608eb59026bf884c7cc6acf Mon Sep 17 00:00:00 2001 From: yxshv Date: Sat, 13 Apr 2024 12:37:15 +0530 Subject: [PATCH 1/8] fix dialog to add spaces --- apps/extension/package.json | 1 + apps/extension/pnpm-lock.yaml | 135 ++++++++++++ apps/extension/src/SideBar.tsx | 55 +++-- apps/extension/src/background.ts | 35 +++- .../src/components/FilterCombobox.tsx | 149 +++---------- .../src/components/ui/dropdown-menu.tsx | 198 ++++++++++++++++++ apps/web/src/app/api/spaces/route.ts | 9 +- apps/web/src/app/page.tsx | 5 +- apps/web/src/server/db/test.ts | 10 + apps/web/types/memory.tsx | 7 +- 10 files changed, 456 insertions(+), 148 deletions(-) create mode 100644 apps/extension/src/components/ui/dropdown-menu.tsx create mode 100644 apps/web/src/server/db/test.ts diff --git a/apps/extension/package.json b/apps/extension/package.json index 4c12ef20..a7f34ea6 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7", "cmdk": "^1.0.0", diff --git a/apps/extension/pnpm-lock.yaml b/apps/extension/pnpm-lock.yaml index e61391eb..ad6c187d 100644 --- a/apps/extension/pnpm-lock.yaml +++ b/apps/extension/pnpm-lock.yaml @@ -8,6 +8,9 @@ dependencies: '@radix-ui/react-dialog': specifier: ^1.0.5 version: 1.0.5(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.0.6 + version: 2.0.6(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-popover': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) @@ -668,6 +671,30 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.75)(react@18.2.0) + '@types/react': 18.2.75 + '@types/react-dom': 18.2.24 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.75)(react@18.2.0): resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: @@ -730,6 +757,20 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.75)(react@18.2.0) dev: false + /@radix-ui/react-direction@1.0.1(@types/react@18.2.75)(react@18.2.0): + resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.4 + '@types/react': 18.2.75 + react: 18.2.0 + dev: false + /@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} peerDependencies: @@ -755,6 +796,33 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@types/react': 18.2.75 + '@types/react-dom': 18.2.24 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.75)(react@18.2.0): resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} peerDependencies: @@ -807,6 +875,44 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@types/react': 18.2.75 + '@types/react-dom': 18.2.24 + aria-hidden: 1.2.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.75)(react@18.2.0) + dev: false + /@radix-ui/react-popover@1.0.7(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==} peerDependencies: @@ -936,6 +1042,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.24)(@types/react@18.2.75)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.75)(react@18.2.0) + '@types/react': 18.2.75 + '@types/react-dom': 18.2.24 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-slot@1.0.2(@types/react@18.2.75)(react@18.2.0): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: diff --git a/apps/extension/src/SideBar.tsx b/apps/extension/src/SideBar.tsx index 890619ca..38b43a30 100644 --- a/apps/extension/src/SideBar.tsx +++ b/apps/extension/src/SideBar.tsx @@ -18,8 +18,9 @@ import { DialogFooter, DialogClose, } from "./components/ui/dialog"; +import { Space } from "./types/memory"; -function sendUrlToAPI() { +function sendUrlToAPI(spaces: number[]) { // get the current URL const url = window.location.href; @@ -46,11 +47,14 @@ function SideBar() { // } // }); + const [savedWebsites, setSavedWebsites] = useState([]); const [isSendingData, setIsSendingData] = useState(false); - const [selectedSpaces, setSelectedSpaces] = useState([0, 1]); + const [loading, setLoading] = useState(false) + const [spaces, setSpaces] = useState(); + const [selectedSpaces, setSelectedSpaces] = useState([]); interface TweetData { tweetText: string; @@ -60,6 +64,15 @@ function SideBar() { time: string; } + const fetchSpaces = async () => { + setLoading(true) + chrome.runtime.sendMessage({ type: "fetchSpaces" }, (resp) => { + console.log(resp) + setSpaces(resp) + setLoading(false) + }); + } + const fetchBookmarks = () => { const tweets: TweetData[] = []; // Initialize an empty array to hold all tweet elements @@ -205,7 +218,7 @@ function SideBar() { ) : ( <> )} - + open === true && fetchSpaces()}> - Add + { + sendUrlToAPI(selectedSpaces); + setIsSendingData(true); + setTimeout(() => { + setIsSendingData(false); + setSavedWebsites([ + ...savedWebsites, + window.location.href, + ]); + }, 1000); + }} + >Add Cancel diff --git a/apps/extension/src/background.ts b/apps/extension/src/background.ts index b3030e74..523a34e8 100644 --- a/apps/extension/src/background.ts +++ b/apps/extension/src/background.ts @@ -1,4 +1,5 @@ import { getEnv } from "./util"; +import { Space } from "./types/memory" const backendUrl = getEnv() === "development" @@ -37,8 +38,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { return true; } else if (request.type === "urlChange") { + const content = request.content; const url = request.url; + const spaces = request.spaces (async () => { chrome.storage.local.get(["jwt"], ({ jwt }) => { @@ -51,10 +54,40 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { headers: { Authorization: `Bearer ${jwt}`, }, - body: JSON.stringify({ pageContent: content, url }), + body: JSON.stringify({ pageContent: content, url, spaces }), }).then((ers) => console.log(ers.status)); }); })(); + } else if (request.type === "fetchSpaces") { + + const run = () => chrome.storage.local.get(["jwt"], async ({ jwt }) => { + if (!jwt) { + console.error("No JWT found"); + return; + } + const resp = await fetch(`${backendUrl}/api/spaces`, { + headers: { + Authorization: `Bearer ${jwt}`, + }, + }) + + const data: { + message: "OK" | string; + data: Space[] | undefined; + } = await resp.json(); + + if (data.message === "OK" && data.data) { + sendResponse(data.data) + } + + }); + + run() + + + return true; + + } else if (request.type === "queryApi") { const input = request.input; const jwt = request.jwt; diff --git a/apps/extension/src/components/FilterCombobox.tsx b/apps/extension/src/components/FilterCombobox.tsx index f6215c03..1ff49d38 100644 --- a/apps/extension/src/components/FilterCombobox.tsx +++ b/apps/extension/src/components/FilterCombobox.tsx @@ -1,152 +1,67 @@ import * as React from "react"; -import { Check, ChevronsUpDown, X } from "lucide-react"; - -import { cn } from "../lib/utils"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "../components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "../components/ui/popover"; +import { PlusCircleIcon, X } from "lucide-react"; import { Space } from "../types/memory"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "./ui/dropdown-menu"; +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; export interface Props extends React.ButtonHTMLAttributes { - side?: "top" | "bottom"; - align?: "end" | "start" | "center"; - onClose?: () => void; selectedSpaces: number[]; setSelectedSpaces: ( spaces: number[] | ((prev: number[]) => number[]), ) => void; name: string; spaces: Space[]; + loading: boolean; } export function FilterSpaces({ - className, - side = "bottom", - align = "center", - onClose, + loading, selectedSpaces, setSelectedSpaces, - name, spaces, - ...props }: Props) { - const [open, setOpen] = React.useState(false); console.log(selectedSpaces, spaces); - const sortedSpaces = spaces.sort(({ id: a }, { id: b }) => - selectedSpaces.includes(a) && !selectedSpaces.includes(b) - ? -1 - : selectedSpaces.includes(b) && !selectedSpaces.includes(a) - ? 1 - : 0, - ); + const filteredSpaces = spaces.filter(space => selectedSpaces.includes(space.id)) + const leftSpaces = spaces.filter(space => !selectedSpaces.includes(space.id)) - React.useEffect(() => { - if (!open) { - onClose?.(); - } - }, [open]); + if (loading) { + return "Loading..." + } return (
- {selectedSpaces.map((spaceid) => { - const space = spaces.find((s) => s.id === spaceid)!; - return {}} />; - })} + {filteredSpaces.length < 1 && "Add to a space"} + {filteredSpaces.map((space) => ( + setSelectedSpaces(prev => prev.filter(s => s !== space.id))} /> + ))} + {leftSpaces.length > 0 && ( + + + + + + {leftSpaces.map(space => ( + <> + {loading && "Loading..."} + setSelectedSpaces(prev => [...prev, space.id])}> + {space.name} + + + ))} + + + )}
); - return ( - - - - - e.preventDefault()} - align={align} - side={side} - className="anycontext-w-[200px] anycontext-p-0" - > - - spaces - .find((s) => s.id.toString() === val) - ?.name.toLowerCase() - .includes(search.toLowerCase().trim()) - ? 1 - : 0 - } - > - - -
- Nothing found - - {sortedSpaces.map((space) => ( - { - setSelectedSpaces((prev: number[]) => - prev.includes(parseInt(val)) - ? prev.filter((v) => v !== parseInt(val)) - : [...prev, parseInt(val)], - ); - }} - asChild - > -
- {space.name} - -
-
- ))} -
-
-
-
-
-
- ); } function SpaceItem({ name, onRemove }: { onRemove: () => void } & Space) { return (
- {name} diff --git a/apps/extension/src/components/ui/dropdown-menu.tsx b/apps/extension/src/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..2e9a95b8 --- /dev/null +++ b/apps/extension/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,198 @@ +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, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +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/app/api/spaces/route.ts b/apps/web/src/app/api/spaces/route.ts index 1e1aeba7..d6cc096b 100644 --- a/apps/web/src/app/api/spaces/route.ts +++ b/apps/web/src/app/api/spaces/route.ts @@ -3,6 +3,8 @@ 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 = @@ -33,6 +35,7 @@ export async function GET(req: NextRequest) { .from(sessions) .where(eq(sessions.sessionToken, token!)); + if (!sessionData || sessionData.length === 0) { return new Response( JSON.stringify({ message: "Invalid Key, session not found." }), @@ -55,12 +58,14 @@ export async function GET(req: NextRequest) { const user = userData[0] - const spaces = await db .select() .from(space) .where(eq(space.user, user.id)) - .all() + .all(); + + + console.log('data', spaces) return NextResponse.json({ message: "OK", diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 419daa5a..1cbe6217 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -56,7 +56,10 @@ export default async function Home() { const collectedSpaces = await db .select() .from(space) - .where(and(eq(space.user, userData.id), not(eq(space.name, "none")))); + .where(eq(space.user, userData.id)) + .all(); + + console.log(collectedSpaces) // Fetch only first 3 content of each spaces let contents: (typeof storedContent.$inferSelect)[] = []; diff --git a/apps/web/src/server/db/test.ts b/apps/web/src/server/db/test.ts new file mode 100644 index 00000000..9cb8f2b5 --- /dev/null +++ b/apps/web/src/server/db/test.ts @@ -0,0 +1,10 @@ +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/types/memory.tsx b/apps/web/types/memory.tsx index ff0dc94c..287f763c 100644 --- a/apps/web/types/memory.tsx +++ b/apps/web/types/memory.tsx @@ -5,7 +5,7 @@ import { storedContent, StoredContent, } from "@/server/db/schema"; -import { asc, and, eq, inArray, notExists } from "drizzle-orm"; +import { asc, and, eq, inArray, notExists, sql, exists } from "drizzle-orm"; export async function fetchContentForSpace( spaceId: number, @@ -19,9 +19,8 @@ export async function fetchContentForSpace( .select() .from(storedContent) .where( - inArray( - storedContent.id, - db.select().from(space).where(eq(space.id, spaceId)), + exists( + db.select().from(contentToSpace).where(and(eq(contentToSpace.spaceId, spaceId), eq(contentToSpace.contentId, storedContent.id))), ), ).orderBy(asc(storedContent.title)) From ccda64670cbdf9715009c0aacfe237c412255119 Mon Sep 17 00:00:00 2001 From: yxshv Date: Sat, 13 Apr 2024 12:57:00 +0530 Subject: [PATCH 2/8] e --- apps/web/src/app/page.tsx | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 1cbe6217..918b056e 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -64,32 +64,28 @@ export default async function Home() { // Fetch only first 3 content of each spaces let contents: (typeof storedContent.$inferSelect)[] = []; + //console.log(await db.select().from(storedContent).) + await Promise.all([ collectedSpaces.forEach(async (space) => { + console.log("fetching ") + const data = await fetchContentForSpace(space.id, { + offset: 0, + limit: 3, + }) + console.log(data) contents = [ ...contents, - ...(await fetchContentForSpace(space.id, { - offset: 0, - limit: 3, - })), + ...data, ]; }), ]); + console.log(contents) + // freeMemories const freeMemories = await fetchFreeMemories(userData.id); - // @dhravya test these 3 functions - fetchFreeMemories; - fetchContentForSpace; - searchMemoriesAndSpaces; - - collectedSpaces.push({ - id: 1, - name: "Cool tech", - user: null, - }); - return ( Date: Sat, 13 Apr 2024 14:09:56 +0530 Subject: [PATCH 3/8] memory item --- apps/web/src/app/page.tsx | 15 +- .../src/components/Sidebar/MemoriesBar.tsx | 244 +++++++++++------- apps/web/src/contexts/MemoryContext.tsx | 8 +- apps/web/src/server/db/schema.ts | 3 + 4 files changed, 168 insertions(+), 102 deletions(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 918b056e..7a39aafa 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,5 +1,6 @@ import { db } from "@/server/db"; import { + ChachedSpaceContent, contentToSpace, sessions, space, @@ -18,6 +19,7 @@ import { import { MemoryProvider } from "@/contexts/MemoryContext"; import Content from "./content"; import { searchMemoriesAndSpaces } from "@/actions/db"; +import { getMetaData } from "@/server/helpers"; export const runtime = "edge"; @@ -62,18 +64,20 @@ export default async function Home() { console.log(collectedSpaces) // Fetch only first 3 content of each spaces - let contents: (typeof storedContent.$inferSelect)[] = []; + let contents: ChachedSpaceContent[] = []; //console.log(await db.select().from(storedContent).) - + await Promise.all([ collectedSpaces.forEach(async (space) => { console.log("fetching ") - const data = await fetchContentForSpace(space.id, { + const data = (await fetchContentForSpace(space.id, { offset: 0, limit: 3, - }) - console.log(data) + })).map(data => ({ + ...data, + space: space.id + })) contents = [ ...contents, ...data, @@ -85,6 +89,7 @@ export default async function Home() { // freeMemories const freeMemories = await fetchFreeMemories(userData.id); + console.log('free',freeMemories) return ( {spaces.map((space) => ( deleteSpace(space.id)} + onDelete={() => {}} key={space.id} - onClick={() => setExpandedSpace(space.id)} + //onClick={() => setExpandedSpace(space.id)} {...space} /> ))} + {freeMemories.map(m => ( + + ))}
); @@ -145,12 +149,40 @@ const SpaceExitVariant: Variant = { }, }; +export function MemoryItem({ + id, + title, + image +}: StoredContent) { + return ( +
+ + +
+ +
+
+ ) +} + export function SpaceItem({ name, id, onDelete, onClick, }: StoredSpace & { onDelete: () => void; onClick?: () => void }) { + + const { cachedMemories } = useMemory(); + const [itemRef, animateItem] = useAnimate(); const { width } = useViewport(); @@ -162,6 +194,10 @@ export function SpaceItem({ }, }); + const spaceMemories = useMemo(() => { + return cachedMemories.filter(m => m.space === id) + }, [cachedMemories]) + return ( { + 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(); - }; - }; + // 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(); + // }; + // }; }} /> - {/* {content.length > 2 ? ( + {spaceMemories.length > 2 ? ( c.image).reverse() as string[]} + images={spaceMemories.map((c) => c.image).reverse() as string[]} /> - ) : content.length === 1 ? ( + ) : spaceMemories.length === 1 ? ( ) : ( c.image).reverse() as string[]} + images={spaceMemories.map((c) => c.image).reverse() as string[]} /> - )} */} + )} ); } @@ -288,7 +326,7 @@ export function SpaceMoreButton({ setIsOpen?: (open: boolean) => void; }) { return ( - <> + ); } diff --git a/apps/web/src/contexts/MemoryContext.tsx b/apps/web/src/contexts/MemoryContext.tsx index aa9cab81..a6050332 100644 --- a/apps/web/src/contexts/MemoryContext.tsx +++ b/apps/web/src/contexts/MemoryContext.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useCallback } from "react"; import { CollectedSpaces } from "../../types/memory"; -import { StoredContent, storedContent, StoredSpace } from "@/server/db/schema"; +import { ChachedSpaceContent, StoredContent, storedContent, StoredSpace } from "@/server/db/schema"; import { addMemory, searchMemoriesAndSpaces } from "@/actions/db"; import { User } from "next-auth"; @@ -21,7 +21,7 @@ export const MemoryContext = React.createContext<{ memory: typeof storedContent.$inferInsert, spaces?: number[], ) => Promise; - cachedMemories: StoredContent[]; + cachedMemories: ChachedSpaceContent[]; search: (query: string) => Promise; }>({ spaces: [], @@ -37,7 +37,7 @@ export const MemoryProvider: React.FC< { spaces: StoredSpace[]; freeMemories: StoredContent[]; - cachedMemories: StoredContent[]; + cachedMemories: ChachedSpaceContent[]; user: User; } & React.PropsWithChildren > = ({ children, user, spaces: initalSpaces, freeMemories: initialFreeMemories, cachedMemories: initialCachedMemories }) => { @@ -46,7 +46,7 @@ export const MemoryProvider: React.FC< const [freeMemories, setFreeMemories] = React.useState(initialFreeMemories); - const [cachedMemories, setCachedMemories] = React.useState( + const [cachedMemories, setCachedMemories] = React.useState( initialCachedMemories ); diff --git a/apps/web/src/server/db/schema.ts b/apps/web/src/server/db/schema.ts index daac595c..f36e7740 100644 --- a/apps/web/src/server/db/schema.ts +++ b/apps/web/src/server/db/schema.ts @@ -134,3 +134,6 @@ export const space = createTable( export type StoredContent = Omit; export type StoredSpace = typeof space.$inferSelect; +export type ChachedSpaceContent = StoredContent & { + space: number; +} From 070ead455120d6da3d0a6843cb0d842a65359c43 Mon Sep 17 00:00:00 2001 From: yxshv Date: Sat, 13 Apr 2024 15:01:51 +0530 Subject: [PATCH 4/8] search results --- apps/web/src/actions/db.ts | 57 +++++++++------ .../src/components/Sidebar/MemoriesBar.tsx | 70 +++++++++++++++---- apps/web/src/contexts/MemoryContext.tsx | 2 +- apps/web/src/hooks/useDebounce.ts | 27 +++++++ 4 files changed, 120 insertions(+), 36 deletions(-) create mode 100644 apps/web/src/hooks/useDebounce.ts diff --git a/apps/web/src/actions/db.ts b/apps/web/src/actions/db.ts index 46e3ddf6..db301e01 100644 --- a/apps/web/src/actions/db.ts +++ b/apps/web/src/actions/db.ts @@ -6,36 +6,51 @@ import { sessions, StoredContent, storedContent, + StoredSpace, users, space } from "@/server/db/schema"; +import { SearchResult } from "@/contexts/MemoryContext"; import { like, eq, and, sql } from "drizzle-orm"; import { union } from "drizzle-orm/sqlite-core" -import { auth as authOptions } from "@/server/auth"; // @todo: (future) pagination not yet needed -export async function searchMemoriesAndSpaces(userId: string, query: string) { - const searchMemoriesQuery = db.select({ - type: sql`'memory'`, - space: sql`NULL`, - memory: storedContent as any - }).from(storedContent).where(and( - eq(storedContent.user, userId), - like(storedContent.title, `%${query}%`) - )) +export async function searchMemoriesAndSpaces(query: string): Promise { - const searchSpacesQuery = db.select({ - type: sql`'space'`, - space: space as any, - memory: sql`NULL`, - }).from(space).where( - and( - eq(space.user, userId), - like(space.name, `%${query}%`) - ) - ) + const user = await getUser() + + if (!user) { + return [] + } + + try { + const searchMemoriesQuery = db.select({ + type: sql`'memory'`, + space: sql`NULL`, + memory: storedContent as any + }).from(storedContent).where(and( + eq(storedContent.user, user.id), + like(storedContent.title, `%${query}%`) + )).all() + + const searchSpacesQuery = db.select({ + type: sql`'space'`, + space: space as any, + memory: sql`NULL`, + }).from(space).where( + and( + eq(space.user, user.id), + like(space.name, `%${query}%`) + ) + ).all() + + const data = await Promise.all([searchSpacesQuery, searchMemoriesQuery]) + + return data.reduce((acc, i) => [...acc, ...i]) as SearchResult[] + } catch { + return [] + } - return await union(searchMemoriesQuery, searchSpacesQuery) } async function getUser() { diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index 1b3c718e..213667c8 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -10,6 +10,7 @@ import { Input, InputWithIcon } from "../ui/input"; import { ArrowUpRight, Edit3, + Loader, MoreHorizontal, Plus, Search, @@ -25,7 +26,7 @@ import { } from "../ui/dropdown-menu"; import { useEffect, useMemo, useRef, useState } from "react"; import { Variant, useAnimate, motion } from "framer-motion"; -import { useMemory } from "@/contexts/MemoryContext"; +import { SearchResult, useMemory } from "@/contexts/MemoryContext"; import { SpaceIcon } from "@/assets/Memories"; import { Dialog, @@ -44,10 +45,12 @@ import { AddMemoryPage, NoteAddPage, SpaceAddPage } from "./AddMemoryDialog"; import { ExpandedSpace } from "./ExpandedSpace"; import { StoredContent, StoredSpace } from "@/server/db/schema"; import Image from "next/image" +import { useDebounce } from "@/hooks/useDebounce"; +import { searchMemoriesAndSpaces } from "@/actions/db"; export function MemoriesBar() { const [parent, enableAnimations] = useAutoAnimate(); - const { spaces, deleteSpace, freeMemories } = useMemory(); + const { spaces, deleteSpace, freeMemories, search } = useMemory(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [addMemoryState, setAddMemoryState] = useState< @@ -55,6 +58,11 @@ export function MemoriesBar() { >(null); const [expandedSpace, setExpandedSpace] = useState(null); + const [searchQuery, setSearcyQuery] = useState(""); + const [searchLoading, setSearchLoading] = useState(false) + const query = useDebounce(searchQuery, 1000) + + const [searchResults, setSearchResults] = useState([]) if (expandedSpace) { return ( @@ -65,14 +73,31 @@ export function MemoriesBar() { ); } + useEffect(() => { + const q = query.trim() + if (q.length < 1) { + setSearchResults([]) + return + } + + setSearchLoading(true); + + (async () => { + setSearchResults(await search(q)) + setSearchLoading(false) + })(); + }, [query]) + return (

Your Memories

} + icon={searchLoading ? : } className="bg-rgray-4 mt-2 w-full" + value={searchQuery} + onChange={(e) => setSearcyQuery(e.target.value)} />
@@ -123,17 +148,34 @@ export function MemoriesBar() { ref={parent} className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5" > - {spaces.map((space) => ( - {}} - key={space.id} - //onClick={() => setExpandedSpace(space.id)} - {...space} - /> - ))} - {freeMemories.map(m => ( - - ))} + {searchQuery.trim().length > 0 ? ( + <> + {searchResults.map(({ type, space, memory }, i) => ( + <> + {type === "memory" && ( + + )} + {type === "space" && ( + {}} /> + )} + + ))} + + ): ( + <> + {spaces.map((space) => ( + {}} + key={space.id} + //onClick={() => setExpandedSpace(space.id)} + {...space} + /> + ))} + {freeMemories.map(m => ( + + ))} + + )}
); diff --git a/apps/web/src/contexts/MemoryContext.tsx b/apps/web/src/contexts/MemoryContext.tsx index a6050332..b805d41e 100644 --- a/apps/web/src/contexts/MemoryContext.tsx +++ b/apps/web/src/contexts/MemoryContext.tsx @@ -62,7 +62,7 @@ export const MemoryProvider: React.FC< if (!user.id) { throw new Error('user id is not define') } - const data = await searchMemoriesAndSpaces(user.id, query) + const data = await searchMemoriesAndSpaces(query) return data as SearchResult[] } diff --git a/apps/web/src/hooks/useDebounce.ts b/apps/web/src/hooks/useDebounce.ts new file mode 100644 index 00000000..4a47de72 --- /dev/null +++ b/apps/web/src/hooks/useDebounce.ts @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react'; + +/** + * Use this hook when you need to debounce a value. + * @param value + * @param delay in milliseconds + */ +export const useDebounce = (value: T, delay: number) => { + // State and setters for debounced value + const [debouncedValue, setDebouncedValue] = useState(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; +}; From 5b340071245cfe9906fd3534ec5176de1e3fd3bd Mon Sep 17 00:00:00 2001 From: yxshv Date: Sat, 13 Apr 2024 18:11:14 +0530 Subject: [PATCH 5/8] spaces dialog --- apps/web/src/actions/db.ts | 19 ++- .../components/Sidebar/AddMemoryDialog.tsx | 66 +++++-- .../src/components/Sidebar/FilterCombobox.tsx | 161 ++++++++++-------- .../src/components/Sidebar/MemoriesBar.tsx | 4 +- apps/web/src/components/ui/command.tsx | 9 +- apps/web/src/contexts/MemoryContext.tsx | 14 +- 6 files changed, 167 insertions(+), 106 deletions(-) diff --git a/apps/web/src/actions/db.ts b/apps/web/src/actions/db.ts index db301e01..b12ed13b 100644 --- a/apps/web/src/actions/db.ts +++ b/apps/web/src/actions/db.ts @@ -15,7 +15,7 @@ import { like, eq, and, sql } from "drizzle-orm"; import { union } from "drizzle-orm/sqlite-core" // @todo: (future) pagination not yet needed -export async function searchMemoriesAndSpaces(query: string): Promise { +export async function searchMemoriesAndSpaces(query: string, opts?: { filter?: { memories?: boolean, spaces?: boolean }, range?: { offset: number, limit: number } }): Promise { const user = await getUser() @@ -31,7 +31,7 @@ export async function searchMemoriesAndSpaces(query: string): Promise`'space'`, @@ -42,9 +42,20 @@ export async function searchMemoriesAndSpaces(query: string): Promise q.offset(opts.range!.offset).limit(opts.range!.limit)) + } else { + queries = queries.map(q => q.all()) + } + + const data = await Promise.all(queries) return data.reduce((acc, i) => [...acc, ...i]) as SearchResult[] } catch { diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx index 886507ff..4f8ef734 100644 --- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx +++ b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx @@ -10,8 +10,11 @@ import { Input } from "../ui/input"; import { Label } from "../ui/label"; import { Markdown } from "tiptap-markdown"; import { useEffect, useRef, useState } from "react"; -import { FilterSpaces } from "./FilterCombobox"; +import { FilterMemories, FilterSpaces } from "./FilterCombobox"; import { useMemory } from "@/contexts/MemoryContext"; +import { Command, Plus, X } from "lucide-react"; +import { StoredContent } from "@/server/db/schema"; +import { cleanUrl } from "@/lib/utils"; export function AddMemoryPage() { const { addMemory } = useMemory(); @@ -153,29 +156,28 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { } export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { - const [selectedSpacesId, setSelectedSpacesId] = useState([]); const inputRef = useRef(null); const [name, setName] = useState(""); - const [content, setContent] = useState(""); const [loading, setLoading] = useState(false); + const [selected, setSelected] = useState([]); + + function check(): boolean { const data = { name: name.trim(), - content, }; - console.log(name); 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.placeholder = "Please enter a title for the space"; inputRef.current.dataset["error"] = "true"; setTimeout(() => { - inputRef.current!.placeholder = "Title of the note"; + inputRef.current!.placeholder = "Enter the name of the space"; inputRef.current!.dataset["error"] = "false"; }, 500); inputRef.current.focus(); @@ -191,19 +193,48 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { setName(e.target.value)} + className="bg-rgray-4 mt-2 w-full focus-visible:data-[error=true]:ring-red-500/10 data-[error=true]:placeholder:text-red-400 placeholder:transition placeholder:duration-500" /> - + {selected.length > 0 && ( + <> + +
+ {selected.map(i => ( + setSelected(prev => prev.filter(p => p.id !== i.id))} + {...i} + /> + ))} +
+ + )} - + + Memory + + Cancel @@ -211,3 +242,16 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { ); } + +export function MemorySelectedItem({ id, title, url, image, onRemove }: StoredContent & { onRemove: () => void; }) { + return ( +
+ + + {title} + {cleanUrl(url)} +
+ ) +} diff --git a/apps/web/src/components/Sidebar/FilterCombobox.tsx b/apps/web/src/components/Sidebar/FilterCombobox.tsx index bd432215..30463672 100644 --- a/apps/web/src/components/Sidebar/FilterCombobox.tsx +++ b/apps/web/src/components/Sidebar/FilterCombobox.tsx @@ -20,9 +20,11 @@ import { } from "@/components/ui/popover"; import { SpaceIcon } from "@/assets/Memories"; import { AnimatePresence, LayoutGroup, motion } from "framer-motion"; -import { useMemory } from "@/contexts/MemoryContext"; +import { SearchResult, useMemory } from "@/contexts/MemoryContext"; +import { useDebounce } from "@/hooks/useDebounce"; +import { StoredContent } from "@/server/db/schema"; -export interface Props extends React.ButtonHTMLAttributes { +export interface FilterSpacesProps extends React.ButtonHTMLAttributes { side?: "top" | "bottom"; align?: "end" | "start" | "center"; onClose?: () => void; @@ -42,7 +44,7 @@ export function FilterSpaces({ setSelectedSpaces, name, ...props -}: Props) { +}: FilterSpacesProps) { const { spaces } = useMemory(); const [open, setOpen] = React.useState(false); @@ -150,26 +152,58 @@ export function FilterSpaces({ ); } +export type FilterMemoriesProps = { + side?: "top" | "bottom"; + align?: "end" | "start" | "center"; + onClose?: () => void; + selected: StoredContent[]; + setSelected: React.Dispatch>; +} & React.ButtonHTMLAttributes + export function FilterMemories({ className, side = "bottom", align = "center", onClose, - selectedSpaces, - setSelectedSpaces, - name, + selected, + setSelected, ...props -}: Props) { - const { spaces } = useMemory(); - const [open, setOpen] = React.useState(false); +}: FilterMemoriesProps) { - const sortedSpaces = spaces.sort(({ id: a }, { id: b }) => - selectedSpaces.includes(a) && !selectedSpaces.includes(b) - ? -1 - : selectedSpaces.includes(b) && !selectedSpaces.includes(a) - ? 1 - : 0, - ); + const { search } = useMemory(); + + const [open, setOpen] = React.useState(false); + const [searchQuery, setSearchQuery] = React.useState(""); + const query = useDebounce(searchQuery, 500) + + const [searchResults, setSearchResults] = React.useState([]); + const [isSearching, setIsSearching] = React.useState(false) + + const results = React.useMemo(() => { + console.log("use memo") + 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 + } + }) + setSearchResults(results) + setIsSearching(false) + })(); + } else { + setSearchResults([]) + } + }, [query]) React.useEffect(() => { if (!open) { @@ -177,6 +211,7 @@ export function FilterMemories({ } }, [open]); + console.log(searchResults); return ( @@ -191,15 +226,7 @@ export function FilterMemories({ )} {...props} > - - {name} - -
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} -
+ {props.children} - spaces - .find((s) => s.id.toString() === val) - ?.name.toLowerCase() - .includes(search.toLowerCase().trim()) - ? 1 - : 0 - } + shouldFilter={false} > - - - - Nothing found - - {sortedSpaces.map((space) => ( - { - setSelectedSpaces((prev: number[]) => - prev.includes(parseInt(val)) - ? prev.filter((v) => v !== parseInt(val)) - : [...prev, parseInt(val)], - ); - }} - asChild - > - - - {space.name} - {selectedSpaces.includes(space.id)} - - - - ))} - - - + + + + {isSearching ? "Searching..." : query.trim().length > 0 ? "Nothing Found" : "Search something"} + {results.map((m) => ( + { + setSelected((prev) => + prev.find(p => p.id === parseInt(val)) + ? prev.filter((v) => v.id !== parseInt(val)) + : [...prev, m], + ); + }} + asChild + > +
+ + {m.title} + i.id === m.id) !== undefined} + className={cn( + "on:opacity-100 ml-auto h-4 w-4 opacity-0", + )} + /> +
+
+ ))} + +
+
diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index 213667c8..1c9e7143 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -60,7 +60,7 @@ export function MemoriesBar() { const [expandedSpace, setExpandedSpace] = useState(null); const [searchQuery, setSearcyQuery] = useState(""); const [searchLoading, setSearchLoading] = useState(false) - const query = useDebounce(searchQuery, 1000) + const query = useDebounce(searchQuery, 500) const [searchResults, setSearchResults] = useState([]) @@ -148,7 +148,7 @@ export function MemoriesBar() { ref={parent} className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5" > - {searchQuery.trim().length > 0 ? ( + {query.trim().length > 0 ? ( <> {searchResults.map(({ type, space, memory }, i) => ( <> diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx index 74b7f2e8..f3534b55 100644 --- a/apps/web/src/components/ui/command.tsx +++ b/apps/web/src/components/ui/command.tsx @@ -3,10 +3,11 @@ import * as React from "react"; import { type DialogProps } from "@radix-ui/react-dialog"; import { Command as CommandPrimitive } from "cmdk"; -import { Search } from "lucide-react"; +import { Loader, Search } from "lucide-react"; import { cn } from "@/lib/utils"; import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { isSea } from "node:sea"; const Command = React.forwardRef< React.ElementRef, @@ -39,13 +40,13 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => { const CommandInput = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { isSearching?: boolean } +>(({ className, isSearching = false ,...props }, ref) => (
- + {isSearching ? : } Promise; cachedMemories: ChachedSpaceContent[]; - search: (query: string) => Promise; + search: typeof searchMemoriesAndSpaces; }>({ spaces: [], freeMemories: [], @@ -57,15 +57,7 @@ export const MemoryProvider: React.FC< const deleteSpace = async (id: number) => { setSpaces((prev) => prev.filter((s) => s.id !== id)); } - - const search = async (query: string) => { - if (!user.id) { - throw new Error('user id is not define') - } - const data = await searchMemoriesAndSpaces(query) - return data as SearchResult[] - } - + // const fetchMemories = useCallback(async (query: string) => { // const response = await fetch(`/api/memories?${query}`); // }, []); @@ -80,7 +72,7 @@ export const MemoryProvider: React.FC< return ( Date: Sat, 13 Apr 2024 20:04:20 +0530 Subject: [PATCH 6/8] space add --- apps/web/src/actions/db.ts | 82 ++++++++++++++++++- .../components/Sidebar/AddMemoryDialog.tsx | 32 ++++++-- .../src/components/Sidebar/MemoriesBar.tsx | 27 ++++-- apps/web/src/contexts/MemoryContext.tsx | 29 +++++-- 4 files changed, 144 insertions(+), 26 deletions(-) diff --git a/apps/web/src/actions/db.ts b/apps/web/src/actions/db.ts index b12ed13b..c5481392 100644 --- a/apps/web/src/actions/db.ts +++ b/apps/web/src/actions/db.ts @@ -8,10 +8,10 @@ import { storedContent, StoredSpace, users, - space + space, } from "@/server/db/schema"; import { SearchResult } from "@/contexts/MemoryContext"; -import { like, eq, and, sql } from "drizzle-orm"; +import { like, eq, and, sql, exists, asc, notExists } from "drizzle-orm"; import { union } from "drizzle-orm/sqlite-core" // @todo: (future) pagination not yet needed @@ -137,3 +137,81 @@ export async function addMemory( ); return _content; } + +export async function addSpace(name: string, memories: number[]) { + + const user = await getUser(); + + if (!user) { + return null + } + + const [addedSpace] = await db + .insert(space) + .values({ + name: name, + user: user.id + }).returning(); + + const addedMemories = memories.length > 0 ? await db.insert(contentToSpace) + .values(memories.map(m => ({ + contentId: m, + spaceId: addedSpace.id + }))).returning() : [] + + return { + space: addedSpace, + addedMemories + } +} + + +export async function fetchContentForSpace( + spaceId: number, + range?: { + offset: number; + limit: number; + }, +) { + + const query = db + .select() + .from(storedContent) + .where( + exists( + db.select().from(contentToSpace).where(and(eq(contentToSpace.spaceId, spaceId), eq(contentToSpace.contentId, storedContent.id))), + ), + ).orderBy(asc(storedContent.title)) + + return range ? await query.limit(range.limit).offset(range.offset) : await query.all() +} + +export async function fetchFreeMemories( + range?: { + offset: number; + limit: number; + } +) { + + const user = await getUser() + + if (!user) { + return [] + } + + const query = db + .select() + .from(storedContent) + .where( + and( + notExists( + db.select().from(contentToSpace).where(eq(contentToSpace.contentId, storedContent.id)), + ), + eq(storedContent.user, user.id), + ) + + ).orderBy(asc(storedContent.title)) + + return range ? await query.limit(range.limit).offset(range.offset) : await query.all() + +} diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx index 4f8ef734..1482774e 100644 --- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx +++ b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx @@ -12,9 +12,10 @@ import { Markdown } from "tiptap-markdown"; import { useEffect, useRef, useState } from "react"; import { FilterMemories, FilterSpaces } from "./FilterCombobox"; import { useMemory } from "@/contexts/MemoryContext"; -import { Command, Plus, X } from "lucide-react"; +import { Loader, Plus, X } from "lucide-react"; import { StoredContent } from "@/server/db/schema"; import { cleanUrl } from "@/lib/utils"; +import { motion } from "framer-motion" export function AddMemoryPage() { const { addMemory } = useMemory(); @@ -157,8 +158,11 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { + const { addSpace } = useMemory() + const inputRef = useRef(null); const [name, setName] = useState(""); + const [loading, setLoading] = useState(false); const [selected, setSelected] = useState([]); @@ -198,6 +202,7 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { type="url" data-modal-autofocus value={name} + disabled={loading} onChange={e => setName(e.target.value)} className="bg-rgray-4 mt-2 w-full focus-visible:data-[error=true]:ring-red-500/10 data-[error=true]:placeholder:text-red-400 placeholder:transition placeholder:duration-500" /> @@ -219,7 +224,8 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { Memory @@ -228,14 +234,28 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { type={undefined} onClick={() => { if (check()) { - + setLoading(true) + addSpace(name, selected.map(s => s.id)).then(() => closeDialog()) } }} - className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" + disabled={loading} + className="relative disabled:opacity-70 disabled:cursor-not-allowed bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" > - Add + + + + + Add + - + Cancel diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index 1c9e7143..88262f4e 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -196,13 +196,16 @@ export function MemoryItem({ title, image }: StoredContent) { + + const name = title ? title.length > 10 ? title.slice(0, 10) + "..." : title : ''; + return (
@@ -240,6 +243,8 @@ export function SpaceItem({ return cachedMemories.filter(m => m.space === id) }, [cachedMemories]) + const _name = name.length > 10 ? name.slice(0, 10) + "..." : name + return ( c.image).reverse() as string[]} /> - ) : spaceMemories.length === 1 ? ( + ) : spaceMemories.length > 1 ? ( + c.image).reverse() as string[]} + /> + ) : spaceMemories.length === 1 ? ( ) : ( - c.image).reverse() as string[]} - /> - )} +
+ +
+ )}
); } diff --git a/apps/web/src/contexts/MemoryContext.tsx b/apps/web/src/contexts/MemoryContext.tsx index f2f09e80..08d166a6 100644 --- a/apps/web/src/contexts/MemoryContext.tsx +++ b/apps/web/src/contexts/MemoryContext.tsx @@ -1,8 +1,7 @@ "use client"; import React, { useCallback } from "react"; -import { CollectedSpaces } from "../../types/memory"; import { ChachedSpaceContent, StoredContent, storedContent, StoredSpace } from "@/server/db/schema"; -import { addMemory, searchMemoriesAndSpaces } from "@/actions/db"; +import { addMemory, searchMemoriesAndSpaces, addSpace, fetchContentForSpace } from "@/actions/db"; import { User } from "next-auth"; export type SearchResult = { @@ -16,7 +15,7 @@ export const MemoryContext = React.createContext<{ spaces: StoredSpace[]; deleteSpace: (id: number) => Promise; freeMemories: StoredContent[]; - addSpace: (space: StoredSpace) => Promise; + addSpace: typeof addSpace; addMemory: ( memory: typeof storedContent.$inferInsert, spaces?: number[], @@ -27,7 +26,7 @@ export const MemoryContext = React.createContext<{ spaces: [], freeMemories: [], addMemory: async () => {}, - addSpace: async () => {}, + addSpace: (async () => {}) as unknown as (typeof addSpace), deleteSpace: async () => {}, cachedMemories: [], search: async () => [] @@ -49,10 +48,6 @@ export const MemoryProvider: React.FC< const [cachedMemories, setCachedMemories] = React.useState( initialCachedMemories ); - - const addSpace = async (space: StoredSpace) => { - setSpaces((prev) => [...prev, space]); - } const deleteSpace = async (id: number) => { setSpaces((prev) => prev.filter((s) => s.id !== id)); @@ -68,13 +63,29 @@ export const MemoryProvider: React.FC< ) => { const content = await addMemory(memory, spaces); } + + const _addSpace: typeof addSpace = async (...params) => { + const { space: addedSpace, addedMemories } = (await addSpace(...params))!; + + setSpaces(prev => [...prev, addedSpace]) + const cachedMemories = (await fetchContentForSpace(addedSpace.id, { + offset: 0, + limit: 3 + })).map(m => ({ ...m, space: addedSpace.id })) + + setCachedMemories(prev => [...prev, ...cachedMemories]) + + return { + space: addedSpace, addedMemories + } + } return ( Date: Sat, 13 Apr 2024 20:49:56 +0530 Subject: [PATCH 7/8] fix favicons --- apps/web/package.json | 1 + apps/web/src/actions/db.ts | 51 +++++++++++-------- apps/web/src/assets/Note.tsx | 0 .../components/Sidebar/AddMemoryDialog.tsx | 35 +++++++++++-- .../src/components/Sidebar/MemoriesBar.tsx | 21 +++++--- apps/web/src/contexts/MemoryContext.tsx | 30 +++++++---- apps/web/src/server/helpers.ts | 28 +++++----- 7 files changed, 107 insertions(+), 59 deletions(-) create mode 100644 apps/web/src/assets/Note.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 4fd421db..8dccd4d6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-slot": "^1.0.2", + "cheerio": "^1.0.0-rc.12", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "cmdk": "latest", diff --git a/apps/web/src/actions/db.ts b/apps/web/src/actions/db.ts index c5481392..41789689 100644 --- a/apps/web/src/actions/db.ts +++ b/apps/web/src/actions/db.ts @@ -115,28 +115,6 @@ export async function getMemory(title: string) { ); } -export async function addMemory( - content: typeof storedContent.$inferInsert, - spaces: number[], -) { - - const user = await getUser(); - - if (!user) { - return null - } - content.user = user.id; - - const _content = ( - await db.insert(storedContent).values(content).returning() - )[0]; - await Promise.all( - spaces.map((spaceId) => - db.insert(contentToSpace).values({ contentId: _content.id, spaceId }), - ), - ); - return _content; -} export async function addSpace(name: string, memories: number[]) { @@ -215,3 +193,32 @@ export async function fetchFreeMemories( return range ? await query.limit(range.limit).offset(range.offset) : await query.all() } + +export async function addMemory(content: typeof storedContent.$inferInsert, spaces: number[]) { + + const user = await getUser() + + if (!user) { + return null + } + + const [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() : []; + + return { + memory: addedMemory, + addedToSpaces + } + +} diff --git a/apps/web/src/assets/Note.tsx b/apps/web/src/assets/Note.tsx new file mode 100644 index 00000000..e69de29b diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx index 1482774e..b9ab1d86 100644 --- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx +++ b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx @@ -77,6 +77,9 @@ export function AddMemoryPage() { } export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { + + const { addMemory } = useMemory() + const [selectedSpacesId, setSelectedSpacesId] = useState([]); const inputRef = useRef(null); @@ -116,6 +119,7 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { placeholder="Title of the note" data-modal-autofocus value={name} + disabled={loading} onChange={(e) => setName(e.target.value)} /> void }) { Cancel diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index 88262f4e..eb26827c 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -194,7 +194,8 @@ const SpaceExitVariant: Variant = { export function MemoryItem({ id, title, - image + image, + type }: StoredContent) { const name = title ? title.length > 10 ? title.slice(0, 10) + "..." : title : ''; @@ -209,11 +210,19 @@ export function MemoryItem({
- + {type === "page" ? ( + + ): type === "note" ? ( +
+ +
+ ) : ( + <> + )}
) diff --git a/apps/web/src/contexts/MemoryContext.tsx b/apps/web/src/contexts/MemoryContext.tsx index 08d166a6..44fc9799 100644 --- a/apps/web/src/contexts/MemoryContext.tsx +++ b/apps/web/src/contexts/MemoryContext.tsx @@ -16,16 +16,13 @@ export const MemoryContext = React.createContext<{ deleteSpace: (id: number) => Promise; freeMemories: StoredContent[]; addSpace: typeof addSpace; - addMemory: ( - memory: typeof storedContent.$inferInsert, - spaces?: number[], - ) => Promise; + addMemory: typeof addMemory; cachedMemories: ChachedSpaceContent[]; search: typeof searchMemoriesAndSpaces; }>({ spaces: [], freeMemories: [], - addMemory: async () => {}, + addMemory: (() => {}) as unknown as (typeof addMemory), addSpace: (async () => {}) as unknown as (typeof addSpace), deleteSpace: async () => {}, cachedMemories: [], @@ -57,12 +54,6 @@ export const MemoryProvider: React.FC< // const response = await fetch(`/api/memories?${query}`); // }, []); - const _addMemory = async ( - memory: typeof storedContent.$inferInsert, - spaces: number[] = [], - ) => { - const content = await addMemory(memory, spaces); - } const _addSpace: typeof addSpace = async (...params) => { const { space: addedSpace, addedMemories } = (await addSpace(...params))!; @@ -80,6 +71,23 @@ export const MemoryProvider: React.FC< } } + 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 + } + } + return ( (.*?)<\/title>/); - const title = titleMatch ? titleMatch[1] : "Title not found"; + const title = $('title').text().trim() - // Extract meta description - const descriptionMatch = html.match( - //, - ); - const description = descriptionMatch - ? descriptionMatch[1] - : "Description not found"; + const description = $('meta[name=description]').attr('content') ?? '' - // Extract favicon - const faviconMatch = html.match( - //, - ); - const favicon = faviconMatch - ? faviconMatch[1] - : "https://supermemory.dhr.wtf/web.svg"; + 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 + } // Prepare the metadata object const metadata = { From 2b926ba610f3dcd0dfa8b7f81e3f4c76db14d198 Mon Sep 17 00:00:00 2001 From: yxshv Date: Sat, 13 Apr 2024 20:53:36 +0530 Subject: [PATCH 8/8] fix edge case for getting metadata --- apps/web/src/server/helpers.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/src/server/helpers.ts b/apps/web/src/server/helpers.ts index a20474b2..c7455a60 100644 --- a/apps/web/src/server/helpers.ts +++ b/apps/web/src/server/helpers.ts @@ -19,6 +19,8 @@ export async function getMetaData(url: string) { 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