diff --git a/apps/web/app/(dash)/memories/content.tsx b/apps/web/app/(dash)/(memories)/content.tsx
similarity index 52%
rename from apps/web/app/(dash)/memories/content.tsx
rename to apps/web/app/(dash)/(memories)/content.tsx
index f34e9523..26aed6a5 100644
--- a/apps/web/app/(dash)/memories/content.tsx
+++ b/apps/web/app/(dash)/(memories)/content.tsx
@@ -3,18 +3,44 @@
import { getAllUserMemoriesAndSpaces } from "@/app/actions/fetchers";
import { Content, StoredSpace } from "@/server/db/schema";
import { MemoriesIcon, NextIcon, SearchIcon, UrlIcon } from "@repo/ui/icons";
-import { NotebookIcon, PaperclipIcon } from "lucide-react";
+import {
+ ArrowLeftIcon,
+ MenuIcon,
+ MoveIcon,
+ NotebookIcon,
+ PaperclipIcon,
+ TrashIcon,
+} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import React, { useEffect, useMemo, useState } from "react";
import Masonry from "react-layout-masonry";
import { getRawTweet } from "@repo/shared-types/utils";
-import { MyTweet } from "./render-tweet";
+import { MyTweet } from "../../../components/twitter/render-tweet";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuPortal,
+ DropdownMenuSeparator,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+} from "@repo/ui/shadcn/dropdown-menu";
+import { Button } from "@repo/ui/shadcn/button";
+import { deleteItem, moveItem } from "@/app/actions/doers";
+import { toast } from "sonner";
export function MemoriesPage({
memoriesAndSpaces,
+ title = "Your Memories",
+ currentSpace,
}: {
memoriesAndSpaces: { memories: Content[]; spaces: StoredSpace[] };
+ title?: string;
+ currentSpace?: StoredSpace;
}) {
const [filter, setFilter] = useState("All");
@@ -69,9 +95,24 @@ export function MemoriesPage({
key={`${memoriesAndSpaces.memories.length + memoriesAndSpaces.spaces.length}`}
className="px-2 md:px-32 py-36 h-full flex mx-auto w-full flex-col gap-6"
>
-
- My Memories
+ {currentSpace && (
+
+ Back to all memories
+
+ )}
+
+
+ {title}
+ {currentSpace && (
+
+ Space
+
+
+ {currentSpace.name}
+
+
+ )}
@@ -99,6 +140,8 @@ export function MemoriesPage({
"/placeholder-image.svg" // TODO: add this placeholder
}
description={(item.data as Content).description ?? ""}
+ spaces={memoriesAndSpaces.spaces}
+ id={(item.data as Content).id}
/>
);
}
@@ -108,6 +151,7 @@ export function MemoriesPage({
);
}
@@ -122,19 +166,24 @@ export function MemoriesPage({
function TabComponent({
title,
description,
+ id,
}: {
title: string;
description: string;
+ id: number;
}) {
return (
-
+
Space
- {title.slice(0, 2).toUpperCase()}
+ {title.slice(0, 2).toUpperCase()} {id}
@@ -145,7 +194,7 @@ function TabComponent({
-
+
);
}
@@ -156,6 +205,8 @@ function LinkComponent({
url,
image,
description,
+ spaces,
+ id,
}: {
type: string;
content: string;
@@ -163,34 +214,95 @@ function LinkComponent({
url: string;
image?: string;
description: string;
+ spaces: StoredSpace[];
+ id: number;
}) {
return (
-
- {type === "page" ? (
- <>
-
-
{title}
-
- {url.replace("https://supermemory.ai", "").split("#")[0] ?? "/"}
-
- >
- ) : type === "note" ? (
- <>
-
- Note
-
- {title}
- {content.replace(title, "")}
- >
- ) : type === "tweet" ? (
-
- ) : null}
-
+
+ {type === "page" ? (
+ <>
+
+ {title}
+
+ {url.replace("https://supermemory.ai", "").split("#")[0] ?? "/"}
+
+ >
+ ) : type === "note" ? (
+ <>
+
+ Note
+
+ {title}
+
+ {content.replace(title, "")}
+
+ >
+ ) : type === "tweet" ? (
+
+ ) : null}
+
+
+
+
+
+
+ {spaces.length > 0 && (
+
+
+
+ Add to space
+
+
+
+ {spaces.map((space) => (
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
+
);
}
diff --git a/apps/web/app/(dash)/memories/page.tsx b/apps/web/app/(dash)/(memories)/memories/page.tsx
similarity index 89%
rename from apps/web/app/(dash)/memories/page.tsx
rename to apps/web/app/(dash)/(memories)/memories/page.tsx
index 1856161f..d1aa999a 100644
--- a/apps/web/app/(dash)/memories/page.tsx
+++ b/apps/web/app/(dash)/(memories)/memories/page.tsx
@@ -1,6 +1,6 @@
import { getAllUserMemoriesAndSpaces } from "@/app/actions/fetchers";
import { redirect } from "next/navigation";
-import MemoriesPage from "./content";
+import MemoriesPage from "../content";
async function Page() {
const { success, data } = await getAllUserMemoriesAndSpaces();
diff --git a/apps/web/app/(dash)/(memories)/space/[spaceid]/page.tsx b/apps/web/app/(dash)/(memories)/space/[spaceid]/page.tsx
new file mode 100644
index 00000000..723fb29e
--- /dev/null
+++ b/apps/web/app/(dash)/(memories)/space/[spaceid]/page.tsx
@@ -0,0 +1,17 @@
+import { getMemoriesInsideSpace } from "@/app/actions/fetchers";
+import { redirect } from "next/navigation";
+import MemoriesPage from "../../content";
+
+async function Page({ params: { spaceid } }: { params: { spaceid: number } }) {
+ const { success, data } = await getMemoriesInsideSpace(spaceid);
+ if (!success ?? !data) return redirect("/home");
+ return (
+
+ );
+}
+
+export default Page;
diff --git a/apps/web/app/(dash)/memories/render-tweet.tsx b/apps/web/app/(dash)/memories/render-tweet.tsx
deleted file mode 100644
index 3e1e3746..00000000
--- a/apps/web/app/(dash)/memories/render-tweet.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import type { Tweet } from "react-tweet/api";
-import {
- type TwitterComponents,
- TweetContainer,
- TweetHeader,
- TweetInReplyTo,
- TweetBody,
- TweetMedia,
- TweetInfo,
- QuotedTweet,
- enrichTweet,
-} from "react-tweet";
-
-type Props = {
- tweet: Tweet;
- components?: TwitterComponents;
-};
-
-export const MyTweet = ({ tweet: t, components }: Props) => {
- const tweet = enrichTweet(t);
- return (
-
-
- {tweet.in_reply_to_status_id_str && }
-
- {tweet.mediaDetails?.length ? (
-
- ) : null}
- {tweet.quoted_tweet && }
-
-
- );
-};
diff --git a/apps/web/app/actions/doers.ts b/apps/web/app/actions/doers.ts
index 035b85ec..90103092 100644
--- a/apps/web/app/actions/doers.ts
+++ b/apps/web/app/actions/doers.ts
@@ -191,7 +191,8 @@ export const createMemory = async (input: {
storeToSpaces = [];
}
- console.log(storeToSpaces);
+ console.log("SAVING URL: ", metadata.baseUrl);
+
const vectorSaveResponse = await fetch(
`${process.env.BACKEND_BASE_URL}/api/add`,
{
@@ -298,10 +299,12 @@ export const createMemory = async (input: {
.all();
await Promise.all(
- spaceData.map(async (space) => {
+ spaceData.map(async (s) => {
await db
.insert(contentToSpace)
- .values({ contentId: contentId, spaceId: space.id });
+ .values({ contentId: contentId, spaceId: s.id });
+
+ db.update(space).set({ numItems: s.numItems + 1 });
}),
);
}
@@ -437,6 +440,124 @@ export const linkTelegramToUser = async (
};
};
+export const deleteItem = async (id: number) => {
+ const data = await auth();
+
+ if (!data || !data.user || !data.user.id) {
+ redirect("/signin");
+ return { error: "Not authenticated", success: false };
+ }
+
+ try {
+ const deletedItem = await db
+ .delete(storedContent)
+ .where(eq(storedContent.id, id))
+ .returning();
+
+ if (!deletedItem) {
+ return {
+ success: false,
+ error: "Failed to delete item",
+ };
+ }
+
+ const actualUrl = deletedItem[0]?.url.split("#supermemory-user-")[0];
+
+ console.log(
+ "ACTUAL URL BADBAL;KFJDLKASJFLKDSJFLKDSJFKD LSFJSLKDJF :",
+ actualUrl,
+ );
+
+ await fetch(`${process.env.BACKEND_BASE_URL}/api/delete`, {
+ method: "POST",
+ body: JSON.stringify({
+ websiteUrl: actualUrl,
+ user: data.user.id,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: "Bearer " + process.env.BACKEND_SECURITY_KEY,
+ },
+ });
+
+ revalidatePath("/memories");
+
+ return {
+ success: true,
+ message: "in-sync",
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error,
+ message: "An error occured while saving your canvas",
+ };
+ }
+};
+
+// TODO: also move in vectorize
+export const moveItem = async (
+ id: number,
+ spaces: number[],
+ fromSpace?: number | undefined,
+): ServerActionReturnType => {
+ const data = await auth();
+
+ if (!data || !data.user || !data.user.id) {
+ redirect("/signin");
+ return { error: "Not authenticated", success: false };
+ }
+
+ try {
+ if (fromSpace) {
+ await db
+ .delete(contentToSpace)
+ .where(
+ and(
+ eq(contentToSpace.contentId, id),
+ eq(contentToSpace.spaceId, fromSpace),
+ ),
+ );
+ }
+
+ const addedItem = await db
+ .insert(contentToSpace)
+ .values(spaces.map((spaceId) => ({ contentId: id, spaceId })))
+ .returning();
+
+ if (!(addedItem.length > 0)) {
+ return {
+ success: false,
+ error: "Failed to move item",
+ };
+ }
+
+ await db
+ .update(space)
+ .set({ numItems: sql`numItems + 1` })
+ .where(eq(space.id, addedItem[0]?.spaceId!));
+
+ if (!addedItem) {
+ return {
+ success: false,
+ error: "Failed to move item",
+ };
+ }
+
+ revalidatePath("/memories");
+
+ return {
+ success: true,
+ data: true,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: (error as Error).message,
+ };
+ }
+};
+
export const createCanvas = async () => {
const data = await auth();
diff --git a/apps/web/app/actions/fetchers.ts b/apps/web/app/actions/fetchers.ts
index a4cc9e95..c33f90d1 100644
--- a/apps/web/app/actions/fetchers.ts
+++ b/apps/web/app/actions/fetchers.ts
@@ -9,6 +9,7 @@ import {
chatThreads,
Content,
contentToSpace,
+ space,
storedContent,
StoredSpace,
users,
@@ -75,6 +76,43 @@ export const getAllMemories = async (
return { success: true, data: contentNotInAnySpace };
};
+export const getMemoriesInsideSpace = async (
+ spaceId: number,
+): ServerActionReturnType<{ memories: Content[]; spaces: StoredSpace[] }> => {
+ const data = await auth();
+
+ if (!data || !data.user) {
+ redirect("/signin");
+ return { error: "Not authenticated", success: false };
+ }
+
+ const memories = await db
+ .select()
+ .from(storedContent)
+ .where(
+ inArray(
+ storedContent.id,
+ db
+ .select({ contentId: contentToSpace.contentId })
+ .from(contentToSpace)
+ .where(eq(contentToSpace.spaceId, spaceId)),
+ ),
+ )
+ .execute();
+
+ const queriedSpace = await db.query.space.findFirst({
+ where: and(eq(users, data.user.id), eq(space.id, spaceId)),
+ });
+
+ return {
+ success: true,
+ data: {
+ memories: memories,
+ spaces: queriedSpace ? [queriedSpace] : [],
+ },
+ };
+};
+
export const getAllUserMemoriesAndSpaces = async (): ServerActionReturnType<{
spaces: StoredSpace[];
memories: Content[];
diff --git a/apps/web/app/api/store/route.ts b/apps/web/app/api/store/route.ts
index a67787c4..d9f99277 100644
--- a/apps/web/app/api/store/route.ts
+++ b/apps/web/app/api/store/route.ts
@@ -128,10 +128,12 @@ const createMemoryFromAPI = async (input: {
.all();
await Promise.all(
- spaceData.map(async (space) => {
+ spaceData.map(async (s) => {
await db
.insert(contentToSpace)
- .values({ contentId: contentId, spaceId: space.id });
+ .values({ contentId: contentId, spaceId: s.id });
+
+ await db.update(space).set({ numItems: s.numItems + 1 });
}),
);
}
diff --git a/apps/web/components/twitter/icons/icons.module.css b/apps/web/components/twitter/icons/icons.module.css
new file mode 100644
index 00000000..aca493e6
--- /dev/null
+++ b/apps/web/components/twitter/icons/icons.module.css
@@ -0,0 +1,9 @@
+.verified {
+ margin-left: 0.125rem;
+ max-width: 20px;
+ max-height: 20px;
+ height: 1.25em;
+ fill: currentColor;
+ user-select: none;
+ vertical-align: text-bottom;
+}
diff --git a/apps/web/components/twitter/icons/index.ts b/apps/web/components/twitter/icons/index.ts
new file mode 100644
index 00000000..f90ec616
--- /dev/null
+++ b/apps/web/components/twitter/icons/index.ts
@@ -0,0 +1,3 @@
+export * from "./verified";
+export * from "./verified-business";
+export * from "./verified-government";
diff --git a/apps/web/components/twitter/icons/verified-business.tsx b/apps/web/components/twitter/icons/verified-business.tsx
new file mode 100644
index 00000000..06d574bd
--- /dev/null
+++ b/apps/web/components/twitter/icons/verified-business.tsx
@@ -0,0 +1,53 @@
+import s from "./icons.module.css";
+
+export const VerifiedBusiness = () => (
+
+);
diff --git a/apps/web/components/twitter/icons/verified-government.tsx b/apps/web/components/twitter/icons/verified-government.tsx
new file mode 100644
index 00000000..601a6910
--- /dev/null
+++ b/apps/web/components/twitter/icons/verified-government.tsx
@@ -0,0 +1,18 @@
+import s from "./icons.module.css";
+
+export const VerifiedGovernment = () => (
+
+);
diff --git a/apps/web/components/twitter/icons/verified.tsx b/apps/web/components/twitter/icons/verified.tsx
new file mode 100644
index 00000000..81c9fc25
--- /dev/null
+++ b/apps/web/components/twitter/icons/verified.tsx
@@ -0,0 +1,14 @@
+import s from "./icons.module.css";
+
+export const Verified = () => (
+
+);
diff --git a/apps/web/components/twitter/render-tweet.tsx b/apps/web/components/twitter/render-tweet.tsx
new file mode 100644
index 00000000..9d6d1a8a
--- /dev/null
+++ b/apps/web/components/twitter/render-tweet.tsx
@@ -0,0 +1,117 @@
+import type { Tweet } from "react-tweet/api";
+import {
+ type TwitterComponents,
+ TweetContainer,
+ TweetInReplyTo,
+ TweetBody,
+ TweetMedia,
+ TweetInfo,
+ QuotedTweet,
+ enrichTweet,
+ EnrichedTweet,
+} from "react-tweet";
+import clsx from "clsx";
+import s from "./tweet-header.module.css";
+import { VerifiedBadge } from "./verified-badge";
+
+type Props = {
+ tweet: Tweet;
+ components?: TwitterComponents;
+};
+
+type AvatarImgProps = {
+ src: string;
+ alt: string;
+ width: number;
+ height: number;
+};
+const AvatarImg = (props: AvatarImgProps) =>
;
+
+const TweetHeader = ({
+ tweet,
+ components,
+}: {
+ tweet: EnrichedTweet;
+ components?: TwitterComponents;
+}) => {
+ const Img = components?.AvatarImg ?? AvatarImg;
+ const { user } = tweet;
+
+ return (
+
+ );
+};
+
+export const MyTweet = ({ tweet: t, components }: Props) => {
+ const tweet = enrichTweet(t);
+ return (
+
+
+ {tweet.in_reply_to_status_id_str && }
+
+ {tweet.mediaDetails?.length ? (
+
+ ) : null}
+ {tweet.quoted_tweet && }
+
+
+ );
+};
diff --git a/apps/web/components/twitter/tweet-header.module.css b/apps/web/components/twitter/tweet-header.module.css
new file mode 100644
index 00000000..2ce994e2
--- /dev/null
+++ b/apps/web/components/twitter/tweet-header.module.css
@@ -0,0 +1,96 @@
+.header {
+ display: flex;
+ padding-bottom: 0.75rem;
+ line-height: var(--tweet-header-line-height);
+ font-size: var(--tweet-header-font-size);
+ white-space: nowrap;
+ overflow-wrap: break-word;
+ overflow: hidden;
+}
+
+.avatar {
+ position: relative;
+ height: 48px;
+ width: 48px;
+}
+.avatarOverflow {
+ height: 100%;
+ width: 100%;
+ position: absolute;
+ overflow: hidden;
+ border-radius: 9999px;
+}
+.avatarSquare {
+ border-radius: 4px;
+}
+.avatarShadow {
+ height: 100%;
+ width: 100%;
+ transition-property: background-color;
+ transition-duration: 0.2s;
+ box-shadow: rgb(0 0 0 / 3%) 0px 0px 2px inset;
+}
+.avatarShadow:hover {
+ background-color: rgba(26, 26, 26, 0.15);
+}
+
+.author {
+ max-width: calc(100% - 84px);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin: 0 0.5rem;
+}
+.authorLink {
+ text-decoration: none;
+ color: inherit;
+ display: flex;
+ align-items: center;
+}
+.authorLink:hover {
+ text-decoration-line: underline;
+}
+.authorVerified {
+ display: inline-flex;
+}
+.authorLinkText {
+ font-weight: 700;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.authorMeta {
+ display: flex;
+}
+.authorFollow {
+ display: flex;
+}
+.username {
+ color: var(--tweet-font-color-secondary);
+ text-decoration: none;
+ text-overflow: ellipsis;
+}
+.follow {
+ color: var(--tweet-color-blue-secondary);
+ text-decoration: none;
+ font-weight: 700;
+}
+.follow:hover {
+ text-decoration-line: underline;
+}
+.separator {
+ padding: 0 0.25rem;
+}
+
+.brand {
+ margin-inline-start: auto;
+}
+
+.twitterIcon {
+ width: 23.75px;
+ height: 23.75px;
+ color: var(--tweet-twitter-icon-color);
+ fill: currentColor;
+ user-select: none;
+}
diff --git a/apps/web/components/twitter/verified-badge.module.css b/apps/web/components/twitter/verified-badge.module.css
new file mode 100644
index 00000000..c16e77ec
--- /dev/null
+++ b/apps/web/components/twitter/verified-badge.module.css
@@ -0,0 +1,10 @@
+.verifiedOld {
+ color: var(--tweet-verified-old-color);
+}
+.verifiedBlue {
+ color: var(--tweet-verified-blue-color);
+}
+.verifiedGovernment {
+ /* color: var(--tweet-verified-government-color); */
+ color: rgb(130, 154, 171);
+}
diff --git a/apps/web/components/twitter/verified-badge.tsx b/apps/web/components/twitter/verified-badge.tsx
new file mode 100644
index 00000000..daa11852
--- /dev/null
+++ b/apps/web/components/twitter/verified-badge.tsx
@@ -0,0 +1,34 @@
+import clsx from "clsx";
+import { Verified, VerifiedBusiness, VerifiedGovernment } from "./icons/index";
+import s from "./verified-badge.module.css";
+
+type Props = {
+ user: any;
+ className?: string;
+};
+
+export const VerifiedBadge = ({ user, className }: Props) => {
+ const verified = user.verified || user.is_blue_verified || user.verified_type;
+ let icon = ;
+ let iconClassName: string | null = s.verifiedBlue ?? null;
+
+ if (verified) {
+ if (!user.is_blue_verified) {
+ iconClassName = s.verifiedOld!;
+ }
+ switch (user.verified_type) {
+ case "Government":
+ icon = ;
+ iconClassName = s.verifiedGovernment!;
+ break;
+ case "Business":
+ icon = ;
+ iconClassName = null;
+ break;
+ }
+ }
+
+ return verified ? (
+ {icon}
+ ) : null;
+};
diff --git a/package.json b/package.json
index e4e5cc86..79b1f113 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
"@million/lint": "^1.0.0-rc.18",
"@mozilla/readability": "^0.5.0",
"@radix-ui/react-accordion": "^1.1.2",
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.1.1",