conflicts

This commit is contained in:
Dhravya 2024-04-13 09:55:29 -07:00
commit 57e699a6ee
13 changed files with 3345 additions and 207 deletions

2713
apps/extension/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -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 }) => {

View file

@ -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" />

View file

@ -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",

View file

@ -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,
};
}

View file

View 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>
);
}

View file

@ -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>

View file

@ -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>
);

View file

@ -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(

View file

@ -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,

View 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;
};

View file

@ -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 = {