mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-09 22:31:10 +00:00
conflicts
This commit is contained in:
commit
57e699a6ee
13 changed files with 3345 additions and 207 deletions
2713
apps/extension/pnpm-lock.yaml
generated
Normal file
2713
apps/extension/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -52,13 +52,12 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||
const spaces = request.spaces(
|
||||
// eslint-disable-next-line no-unexpected-multiline
|
||||
async () => {
|
||||
chrome.storage.local.get(["jwt"], ({ jwt }) => {
|
||||
chrome.storage.local.get(["jwt"], async ({ jwt }) => {
|
||||
if (!jwt) {
|
||||
console.error("No JWT found");
|
||||
return;
|
||||
}
|
||||
fetch(`${backendUrl}/api/store`, {
|
||||
method: "POST",
|
||||
await fetch(`${backendUrl}/api/spaces`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
|
|
@ -67,7 +66,6 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||
});
|
||||
},
|
||||
)();
|
||||
return true;
|
||||
} else if (request.type === "fetchSpaces") {
|
||||
const run = () =>
|
||||
chrome.storage.local.get(["jwt"], async ({ jwt }) => {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@ export function FilterSpaces({
|
|||
<SpaceItem
|
||||
{...space}
|
||||
key={space.id}
|
||||
// onRemove={() => setSelectedSpaces(prev => prev.filter(s => s !== space.id))}
|
||||
onRemove={() =>
|
||||
setSelectedSpaces((prev) => prev.filter((s) => s !== space.id))
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{leftSpaces.length > 0 && (
|
||||
|
|
@ -76,14 +78,11 @@ export function FilterSpaces({
|
|||
);
|
||||
}
|
||||
|
||||
function SpaceItem({ name }: Space) {
|
||||
function SpaceItem({ name, onRemove }: Space & { onRemove: () => void }) {
|
||||
return (
|
||||
<div className="anycontext-flex anycontext-justify-center anycontext-items-center anycontext-gap-2 anycontext-p-1 anycontext-pl-2 anycontext-pr-3 anycontext-rounded-full anycontext-bg-black/5 dark:anycontext-bg-white/5 anycontext-border-white/20 dark:anycontext-border-black/20 border">
|
||||
<button
|
||||
onClick={
|
||||
// onRemove
|
||||
(e) => e.stopPropagation()
|
||||
}
|
||||
onClick={onRemove}
|
||||
className="anycontext-flex hover:anycontext-bg-transparent anycontext-justify-center anycontext-scale-110 anycontext-items-center focus-visible:anycontext-outline-none anycontext-rounded-full anycontext-w-3 anycontext-bg-black/5 dark:anycontext-bg-white/5 anycontext-h-3 anycontext-text-transparent hover:anycontext-text-black dark:hover:anycontext-text-white"
|
||||
>
|
||||
<X className="anycontext-w-3 anycontext-h-3" />
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -6,41 +6,73 @@ import {
|
|||
sessions,
|
||||
StoredContent,
|
||||
storedContent,
|
||||
StoredSpace,
|
||||
users,
|
||||
space,
|
||||
} from "@/server/db/schema";
|
||||
import { like, eq, and, sql } from "drizzle-orm";
|
||||
import { SearchResult } from "@/contexts/MemoryContext";
|
||||
import { like, eq, and, sql, exists, asc, notExists } from "drizzle-orm";
|
||||
import { union } from "drizzle-orm/sqlite-core";
|
||||
import { auth as authOptions } from "@/server/auth";
|
||||
import { FormEvent } from "react";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
// @todo: (future) pagination not yet needed
|
||||
export async function searchMemoriesAndSpaces(userId: string, query: string) {
|
||||
const searchMemoriesQuery = db
|
||||
.select({
|
||||
type: sql<string>`'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,
|
||||
opts?: {
|
||||
filter?: { memories?: boolean; spaces?: boolean };
|
||||
range?: { offset: number; limit: number };
|
||||
},
|
||||
): Promise<SearchResult[]> {
|
||||
const user = await getUser();
|
||||
|
||||
const searchSpacesQuery = db
|
||||
.select({
|
||||
type: sql<string>`'space'`,
|
||||
space: space as any,
|
||||
memory: sql`NULL`,
|
||||
})
|
||||
.from(space)
|
||||
.where(and(eq(space.user, userId), like(space.name, `%${query}%`)));
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await union(searchMemoriesQuery, searchSpacesQuery);
|
||||
try {
|
||||
const searchMemoriesQuery = db
|
||||
.select({
|
||||
type: sql<string>`'memory'`,
|
||||
space: sql`NULL`,
|
||||
memory: storedContent as any,
|
||||
})
|
||||
.from(storedContent)
|
||||
.where(
|
||||
and(
|
||||
eq(storedContent.user, user.id),
|
||||
like(storedContent.title, `%${query}%`),
|
||||
),
|
||||
);
|
||||
|
||||
const 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}%`)));
|
||||
|
||||
let queries = [];
|
||||
|
||||
[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);
|
||||
|
||||
return data.reduce((acc, i) => [...acc, ...i]) as SearchResult[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getUser() {
|
||||
|
|
@ -94,6 +126,101 @@ export async function getMemory(title: string) {
|
|||
);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
export async function addMemory(
|
||||
content: typeof storedContent.$inferInsert,
|
||||
spaces: number[],
|
||||
|
|
@ -103,15 +230,30 @@ export async function addMemory(
|
|||
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;
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
0
apps/web/src/assets/Note.tsx
Normal file
0
apps/web/src/assets/Note.tsx
Normal file
|
|
@ -10,8 +10,12 @@ 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 { 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();
|
||||
|
|
@ -73,6 +77,8 @@ export function AddMemoryPage() {
|
|||
}
|
||||
|
||||
export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) {
|
||||
const { addMemory } = useMemory();
|
||||
|
||||
const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -112,6 +118,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)}
|
||||
/>
|
||||
<Editor
|
||||
|
|
@ -134,16 +141,41 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) {
|
|||
<button
|
||||
onClick={() => {
|
||||
if (check()) {
|
||||
closeDialog();
|
||||
setLoading(true);
|
||||
addMemory(
|
||||
{
|
||||
content,
|
||||
title: name,
|
||||
type: "note",
|
||||
url: "https://notes.supermemory.dhr.wtf/",
|
||||
image: "",
|
||||
savedAt: new Date(),
|
||||
},
|
||||
selectedSpacesId,
|
||||
).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="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"
|
||||
>
|
||||
Add
|
||||
<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}
|
||||
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={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>
|
||||
|
|
@ -153,6 +185,37 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) {
|
|||
}
|
||||
|
||||
export function SpaceAddPage({ closeDialog }: { closeDialog: () => 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="md:w-[40vw]">
|
||||
<DialogHeader>
|
||||
|
|
@ -160,23 +223,99 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) {
|
|||
</DialogHeader>
|
||||
<Label className="mt-5 block">Name</Label>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder="Enter the name of the space"
|
||||
type="url"
|
||||
data-modal-autofocus
|
||||
className="bg-rgray-4 mt-2 w-full"
|
||||
value={name}
|
||||
disabled={loading}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="bg-rgray-4 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"
|
||||
/>
|
||||
<Label className="mt-5 block">Memories</Label>
|
||||
{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>
|
||||
<DialogClose
|
||||
type={undefined}
|
||||
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"
|
||||
<FilterMemories
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
disabled={loading}
|
||||
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={() => {
|
||||
if (check()) {
|
||||
setLoading(true);
|
||||
addSpace(
|
||||
name,
|
||||
selected.map((s) => s.id),
|
||||
).then(() => 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="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"
|
||||
>
|
||||
Add
|
||||
</DialogClose>
|
||||
<DialogClose 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">
|
||||
Cancel
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MemorySelectedItem({
|
||||
id,
|
||||
title,
|
||||
url,
|
||||
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-1 px-2 text-sm [&:hover>[data-icon]]:block [&:hover>img]:hidden">
|
||||
<img src={image ?? "/icons/logo_without_bg.png"} className="h-5 w-5" />
|
||||
<button
|
||||
onClick={onRemove}
|
||||
data-icon
|
||||
className="m-0 hidden h-5 w-5 p-0 focus-visible:outline-none"
|
||||
>
|
||||
<X className="h-5 w-5 scale-90" />
|
||||
</button>
|
||||
<span>{title}</span>
|
||||
<span className="ml-auto block opacity-50">{cleanUrl(url)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,12 @@ 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<HTMLButtonElement> {
|
||||
export interface FilterSpacesProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
side?: "top" | "bottom";
|
||||
align?: "end" | "start" | "center";
|
||||
onClose?: () => void;
|
||||
|
|
@ -42,7 +45,7 @@ export function FilterSpaces({
|
|||
setSelectedSpaces,
|
||||
name,
|
||||
...props
|
||||
}: Props) {
|
||||
}: FilterSpacesProps) {
|
||||
const { spaces } = useMemory();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
|
|
@ -150,26 +153,57 @@ export function FilterSpaces({
|
|||
);
|
||||
}
|
||||
|
||||
export type FilterMemoriesProps = {
|
||||
side?: "top" | "bottom";
|
||||
align?: "end" | "start" | "center";
|
||||
onClose?: () => void;
|
||||
selected: StoredContent[];
|
||||
setSelected: React.Dispatch<React.SetStateAction<StoredContent[]>>;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
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 { search } = useMemory();
|
||||
|
||||
const sortedSpaces = spaces.sort(({ id: a }, { id: b }) =>
|
||||
selectedSpaces.includes(a) && !selectedSpaces.includes(b)
|
||||
? -1
|
||||
: selectedSpaces.includes(b) && !selectedSpaces.includes(a)
|
||||
? 1
|
||||
: 0,
|
||||
);
|
||||
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(() => {
|
||||
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 (
|
||||
<AnimatePresence mode="popLayout">
|
||||
<LayoutGroup>
|
||||
|
|
@ -191,15 +226,7 @@ export function FilterMemories({
|
|||
)}
|
||||
{...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>
|
||||
{props.children}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
|
|
@ -208,56 +235,53 @@ export function FilterMemories({
|
|||
side={side}
|
||||
className="w-[200px] p-0"
|
||||
>
|
||||
<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}
|
||||
{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>
|
||||
<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.image ?? "/icons/logo_without_bg.png"}
|
||||
className="mr-2 h-4 w-4"
|
||||
/>
|
||||
{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>
|
||||
|
|
|
|||
|
|
@ -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<number | null>(null);
|
||||
const [searchQuery, setSearcyQuery] = useState("");
|
||||
const [searchLoading, setSearchLoading] = useState(false);
|
||||
const query = useDebounce(searchQuery, 500);
|
||||
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
||||
|
||||
if (expandedSpace) {
|
||||
return (
|
||||
|
|
@ -65,14 +73,37 @@ 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 (
|
||||
<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={<Search className="text-rgray-11 h-5 w-5 opacity-50" />}
|
||||
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">
|
||||
|
|
@ -123,17 +154,32 @@ export function MemoriesBar() {
|
|||
ref={parent}
|
||||
className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5"
|
||||
>
|
||||
{spaces.map((space) => (
|
||||
<SpaceItem
|
||||
onDelete={() => {}}
|
||||
key={space.id}
|
||||
//onClick={() => setExpandedSpace(space.id)}
|
||||
{...space}
|
||||
/>
|
||||
))}
|
||||
{freeMemories.map((m) => (
|
||||
<MemoryItem {...m} key={m.id} />
|
||||
))}
|
||||
{query.trim().length > 0 ? (
|
||||
<>
|
||||
{searchResults.map(({ type, space, memory }, i) => (
|
||||
<>
|
||||
{type === "memory" && <MemoryItem {...memory!} key={i} />}
|
||||
{type === "space" && (
|
||||
<SpaceItem {...space!} key={i} onDelete={() => {}} />
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{spaces.map((space) => (
|
||||
<SpaceItem
|
||||
onDelete={() => {}}
|
||||
key={space.id}
|
||||
//onClick={() => setExpandedSpace(space.id)}
|
||||
{...space}
|
||||
/>
|
||||
))}
|
||||
{freeMemories.map((m) => (
|
||||
<MemoryItem {...m} key={m.id} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -149,15 +195,29 @@ const SpaceExitVariant: Variant = {
|
|||
},
|
||||
};
|
||||
|
||||
export function MemoryItem({ id, title, image }: StoredContent) {
|
||||
export function MemoryItem({ id, title, image, type }: StoredContent) {
|
||||
const name = title
|
||||
? title.length > 10
|
||||
? title.slice(0, 10) + "..."
|
||||
: title
|
||||
: "<no title>";
|
||||
|
||||
return (
|
||||
<div 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 data-space-text className="focus-visible:outline-none">
|
||||
{title}
|
||||
{name}
|
||||
</button>
|
||||
|
||||
<div className="flex h-24 w-24 items-center justify-center">
|
||||
<img className="h-16 w-16" id={id.toString()} src={image!} />
|
||||
{type === "page" ? (
|
||||
<img className="h-16 w-16" id={id.toString()} src={image!} />
|
||||
) : type === "note" ? (
|
||||
<div className="bg-rgray-4 flex items-center justify-center rounded-md p-2 shadow-md">
|
||||
<Text className="h-10 w-10" />
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -186,6 +246,8 @@ export function SpaceItem({
|
|||
return cachedMemories.filter((m) => m.space === id);
|
||||
}, [cachedMemories]);
|
||||
|
||||
const _name = name.length > 10 ? name.slice(0, 10) + "..." : name;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={itemRef}
|
||||
|
|
@ -194,7 +256,7 @@ export function SpaceItem({
|
|||
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 data-space-text className="focus-visible:outline-none">
|
||||
{name}
|
||||
{_name}
|
||||
</button>
|
||||
<SpaceMoreButton
|
||||
isOpen={moreDropdownOpen}
|
||||
|
|
@ -287,6 +349,12 @@ export function SpaceItem({
|
|||
id={id.toString()}
|
||||
images={spaceMemories.map((c) => c.image).reverse() as string[]}
|
||||
/>
|
||||
) : spaceMemories.length > 1 ? (
|
||||
<MemoryWithImages2
|
||||
className="h-24 w-24"
|
||||
id={id.toString()}
|
||||
images={spaceMemories.map((c) => c.image).reverse() as string[]}
|
||||
/>
|
||||
) : spaceMemories.length === 1 ? (
|
||||
<MemoryWithImage
|
||||
className="h-24 w-24"
|
||||
|
|
@ -294,11 +362,7 @@ export function SpaceItem({
|
|||
image={spaceMemories[0].image!}
|
||||
/>
|
||||
) : (
|
||||
<MemoryWithImages2
|
||||
className="h-24 w-24"
|
||||
id={id.toString()}
|
||||
images={spaceMemories.map((c) => c.image).reverse() as string[]}
|
||||
/>
|
||||
<div className="bg-rgray-4 shadow- h-24 w-24 scale-50 rounded-full opacity-30"></div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<typeof CommandPrimitive>,
|
||||
|
|
@ -39,13 +40,19 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
|||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
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=""
|
||||
>
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
{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(
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
"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 = {
|
||||
|
|
@ -21,18 +25,15 @@ export const MemoryContext = React.createContext<{
|
|||
spaces: StoredSpace[];
|
||||
deleteSpace: (id: number) => Promise<void>;
|
||||
freeMemories: StoredContent[];
|
||||
addSpace: (space: StoredSpace) => Promise<void>;
|
||||
addMemory: (
|
||||
memory: typeof storedContent.$inferInsert,
|
||||
spaces?: number[],
|
||||
) => Promise<void>;
|
||||
addSpace: typeof addSpace;
|
||||
addMemory: typeof addMemory;
|
||||
cachedMemories: ChachedSpaceContent[];
|
||||
search: (query: string) => Promise<SearchResult[]>;
|
||||
search: typeof searchMemoriesAndSpaces;
|
||||
}>({
|
||||
spaces: [],
|
||||
freeMemories: [],
|
||||
addMemory: async () => {},
|
||||
addSpace: async () => {},
|
||||
addMemory: (() => {}) as unknown as typeof addMemory,
|
||||
addSpace: (async () => {}) as unknown as typeof addSpace,
|
||||
deleteSpace: async () => {},
|
||||
cachedMemories: [],
|
||||
search: async () => [],
|
||||
|
|
@ -60,39 +61,60 @@ export const MemoryProvider: React.FC<
|
|||
ChachedSpaceContent[]
|
||||
>(initialCachedMemories);
|
||||
|
||||
const addSpace = async (space: StoredSpace) => {
|
||||
setSpaces((prev) => [...prev, space]);
|
||||
};
|
||||
|
||||
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(user.id, query);
|
||||
return data as SearchResult[];
|
||||
};
|
||||
|
||||
// const fetchMemories = useCallback(async (query: string) => {
|
||||
// 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))!;
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
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 (
|
||||
<MemoryContext.Provider
|
||||
value={{
|
||||
search,
|
||||
search: searchMemoriesAndSpaces,
|
||||
spaces,
|
||||
addSpace,
|
||||
addSpace: _addSpace,
|
||||
deleteSpace,
|
||||
freeMemories,
|
||||
cachedMemories,
|
||||
|
|
|
|||
27
apps/web/src/hooks/useDebounce.ts
Normal file
27
apps/web/src/hooks/useDebounce.ts
Normal file
|
|
@ -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 = <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;
|
||||
};
|
||||
|
|
@ -1,29 +1,31 @@
|
|||
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 titleMatch = html.match(/<title>(.*?)<\/title>/);
|
||||
const title = titleMatch ? titleMatch[1] : "Title not found";
|
||||
const title = $("title").text().trim();
|
||||
|
||||
// Extract meta description
|
||||
const descriptionMatch = html.match(
|
||||
/<meta name="description" content="(.*?)"\s*\/?>/,
|
||||
);
|
||||
const description = descriptionMatch
|
||||
? descriptionMatch[1]
|
||||
: "Description not found";
|
||||
const description = $("meta[name=description]").attr("content") ?? "";
|
||||
|
||||
// Extract favicon
|
||||
const faviconMatch = html.match(
|
||||
/<link rel="(?:icon|shortcut icon)" href="(.*?)"\s*\/?>/,
|
||||
);
|
||||
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;
|
||||
} else if (favicon.startsWith("./")) {
|
||||
favicon = baseUrl + favicon.slice(1);
|
||||
}
|
||||
|
||||
// Prepare the metadata object
|
||||
const metadata = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue