From 2c6a96e96f49439853f68ff18d1502cac04d618c Mon Sep 17 00:00:00 2001 From: Dhravya Date: Mon, 1 Jul 2024 20:12:56 -0500 Subject: [PATCH] spaces function --- .../{memories => (memories)}/content.tsx | 176 ++++++++++++++---- .../(dash)/{ => (memories)}/memories/page.tsx | 2 +- .../(memories)/space/[spaceid]/page.tsx | 17 ++ apps/web/app/(dash)/memories/render-tweet.tsx | 33 ---- apps/web/app/actions/doers.ts | 127 ++++++++++++- apps/web/app/actions/fetchers.ts | 38 ++++ apps/web/app/api/store/route.ts | 6 +- .../components/twitter/icons/icons.module.css | 9 + apps/web/components/twitter/icons/index.ts | 3 + .../twitter/icons/verified-business.tsx | 53 ++++++ .../twitter/icons/verified-government.tsx | 18 ++ .../web/components/twitter/icons/verified.tsx | 14 ++ apps/web/components/twitter/render-tweet.tsx | 117 ++++++++++++ .../twitter/tweet-header.module.css | 96 ++++++++++ .../twitter/verified-badge.module.css | 10 + .../web/components/twitter/verified-badge.tsx | 34 ++++ package.json | 1 + 17 files changed, 683 insertions(+), 71 deletions(-) rename apps/web/app/(dash)/{memories => (memories)}/content.tsx (52%) rename apps/web/app/(dash)/{ => (memories)}/memories/page.tsx (89%) create mode 100644 apps/web/app/(dash)/(memories)/space/[spaceid]/page.tsx delete mode 100644 apps/web/app/(dash)/memories/render-tweet.tsx create mode 100644 apps/web/components/twitter/icons/icons.module.css create mode 100644 apps/web/components/twitter/icons/index.ts create mode 100644 apps/web/components/twitter/icons/verified-business.tsx create mode 100644 apps/web/components/twitter/icons/verified-government.tsx create mode 100644 apps/web/components/twitter/icons/verified.tsx create mode 100644 apps/web/components/twitter/render-tweet.tsx create mode 100644 apps/web/components/twitter/tweet-header.module.css create mode 100644 apps/web/components/twitter/verified-badge.module.css create mode 100644 apps/web/components/twitter/verified-badge.tsx 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 +
+ Spaces icon + {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 ( -
+
Spaces icon Space
- {title.slice(0, 2).toUpperCase()} + {title.slice(0, 2).toUpperCase()} {id}
@@ -145,7 +194,7 @@ function TabComponent({ Search icon
-
+ ); } @@ -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" ? ( - <> -
- Page -
-
{title}
-
- {url.replace("https://supermemory.ai", "").split("#")[0] ?? "/"} -
- - ) : type === "note" ? ( - <> -
- Note -
-
{title}
-
{content.replace(title, "")}
- - ) : type === "tweet" ? ( - - ) : null} - + + {type === "page" ? ( + <> +
+ 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",