diff options
| author | Dhravya <[email protected]> | 2024-07-01 20:12:56 -0500 |
|---|---|---|
| committer | Dhravya <[email protected]> | 2024-07-01 20:12:56 -0500 |
| commit | 2c6a96e96f49439853f68ff18d1502cac04d618c (patch) | |
| tree | c4b541c4ba21bf1b404722d714d5ab32a2644d90 /apps | |
| parent | fix link (diff) | |
| download | supermemory-2c6a96e96f49439853f68ff18d1502cac04d618c.tar.xz supermemory-2c6a96e96f49439853f68ff18d1502cac04d618c.zip | |
spaces function
Diffstat (limited to 'apps')
16 files changed, 682 insertions, 71 deletions
diff --git a/apps/web/app/(dash)/memories/content.tsx b/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" > - <h2 className="text-white w-full font-medium text-3xl text-left font-semibold"> - My Memories + {currentSpace && ( + <Link href={"/memories"} className="flex gap-2 items-center"> + <ArrowLeftIcon className="w-3 h-3" /> Back to all memories + </Link> + )} + + <h2 className="text-white w-full text-3xl text-left font-semibold"> + {title} </h2> + {currentSpace && ( + <div className="flex gap-4 items-center"> + Space + <div className="flex items-center gap-2 bg-secondary p-2 rounded-xl"> + <Image src={MemoriesIcon} alt="Spaces icon" className="w-3 h-3" /> + <span className="text-[#fff]">{currentSpace.name}</span> + </div> + </div> + )} <Filters setFilter={setFilter} filter={filter} /> @@ -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({ <TabComponent title={(item.data as StoredSpace).name} description={`${(item.data as StoredSpace).numItems} memories`} + id={(item.data as StoredSpace).id} /> ); } @@ -122,19 +166,24 @@ export function MemoriesPage({ function TabComponent({ title, description, + id, }: { title: string; description: string; + id: number; }) { return ( - <div className="flex flex-col gap-4 bg-[#161f2a]/30 backdrop-blur-md border-2 border-border w-full rounded-xl p-4 -z-10"> + <Link + href={`/space/${id}`} + className="flex flex-col gap-4 bg-[#161f2a]/30 backdrop-blur-md border-2 border-border w-full rounded-xl p-4" + > <div className="flex items-center gap-2 text-xs"> <Image alt="Spaces icon" src={MemoriesIcon} className="size-3" /> Space </div> <div className="flex items-center"> <div> <div className="h-12 w-12 flex justify-center items-center rounded-md"> - {title.slice(0, 2).toUpperCase()} + {title.slice(0, 2).toUpperCase()} {id} </div> </div> <div className="grow px-4"> @@ -145,7 +194,7 @@ function TabComponent({ <Image src={NextIcon} alt="Search icon" /> </div> </div> - </div> + </Link> ); } @@ -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 ( - <Link - href={url.replace("https://supermemory.ai", "").split("#")[0] ?? "/"} - className={`bg-secondary border-2 border-border rounded-xl ${type === "tweet" ? "" : "p-4"} hover:scale-105 transition duration-200`} + <div + className={`bg-secondary group relative border-2 border-border rounded-xl ${type === "tweet" ? "" : "p-4"} hover:scale-105 transition duration-200`} > - {type === "page" ? ( - <> - <div className="flex items-center gap-2 text-xs"> - <PaperclipIcon className="w-3 h-3" /> Page - </div> - <div className="text-lg text-[#fff] mt-4 line-clamp-2">{title}</div> - <div> - {url.replace("https://supermemory.ai", "").split("#")[0] ?? "/"} - </div> - </> - ) : type === "note" ? ( - <> - <div className="flex items-center gap-2 text-xs"> - <NotebookIcon className="w-3 h-3" /> Note - </div> - <div className="text-lg text-[#fff] mt-4 line-clamp-2">{title}</div> - <div className="line-clamp-3 mt-2">{content.replace(title, "")}</div> - </> - ) : type === "tweet" ? ( - <MyTweet tweet={JSON.parse(getRawTweet(content) ?? "{}")} /> - ) : null} - </Link> + <Link + href={url.replace("https://supermemory.ai", "").split("#")[0] ?? "/"} + > + {type === "page" ? ( + <> + <div className="flex items-center gap-2 text-xs"> + <PaperclipIcon className="w-3 h-3" /> Page + </div> + <div className="text-lg text-[#fff] mt-4 line-clamp-2">{title}</div> + <div> + {url.replace("https://supermemory.ai", "").split("#")[0] ?? "/"} + </div> + </> + ) : type === "note" ? ( + <> + <div className="flex items-center gap-2 text-xs"> + <NotebookIcon className="w-3 h-3" /> Note + </div> + <div className="text-lg text-[#fff] mt-4 line-clamp-2">{title}</div> + <div className="line-clamp-3 mt-2"> + {content.replace(title, "")} + </div> + </> + ) : type === "tweet" ? ( + <MyTweet tweet={JSON.parse(getRawTweet(content) ?? "{}")} /> + ) : null} + </Link> + <DropdownMenu modal={false}> + <DropdownMenuTrigger className="top-5 right-5 absolute opacity-0 group-focus:opacity-100 group-hover:opacity-100 transition duration-200"> + <MenuIcon /> + </DropdownMenuTrigger> + <DropdownMenuContent> + {spaces.length > 0 && ( + <DropdownMenuSub> + <DropdownMenuSubTrigger> + <MoveIcon className="mr-2 h-4 w-4" /> + <span>Add to space</span> + </DropdownMenuSubTrigger> + <DropdownMenuPortal> + <DropdownMenuSubContent> + {spaces.map((space) => ( + <DropdownMenuItem> + <button + className="w-full h-full" + onClick={async () => { + toast.info("Adding to space..."); + + console.log(id, space.id); + const response = await moveItem(id, [space.id]); + + if (response.success) { + toast.success("Moved to space"); + console.log("Moved to space"); + } else { + toast.error("Failed to move to space"); + console.error("Failed to move to space"); + } + }} + > + {space.name} + </button> + </DropdownMenuItem> + ))} + </DropdownMenuSubContent> + </DropdownMenuPortal> + </DropdownMenuSub> + )} + <DropdownMenuItem asChild> + <Button + onClick={async () => { + await deleteItem(id); + }} + variant="destructive" + className="w-full" + > + <TrashIcon className="mr-2 h-4 w-4" /> + Delete + </Button> + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + </div> ); } diff --git a/apps/web/app/(dash)/memories/page.tsx b/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 ( + <MemoriesPage + memoriesAndSpaces={{ memories: data.memories, spaces: [] }} + title={data.spaces[0]?.name} + currentSpace={data.spaces[0]} + /> + ); +} + +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 ( - <TweetContainer className="bg-transparent !m-0 !p-0 !z-0"> - <TweetHeader tweet={tweet} components={components} /> - {tweet.in_reply_to_status_id_str && <TweetInReplyTo tweet={tweet} />} - <TweetBody tweet={tweet} /> - {tweet.mediaDetails?.length ? ( - <TweetMedia tweet={tweet} components={components} /> - ) : null} - {tweet.quoted_tweet && <QuotedTweet tweet={tweet.quoted_tweet} />} - <TweetInfo tweet={tweet} /> - </TweetContainer> - ); -}; 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<boolean> => { + 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<number>`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 = () => ( + <svg + viewBox="0 0 22 22" + aria-label="Verified account" + role="img" + className={s.verified} + > + <g> + <linearGradient + gradientUnits="userSpaceOnUse" + id="0-a" + x1="4.411" + x2="18.083" + y1="2.495" + y2="21.508" + > + <stop offset="0" stopColor="#f4e72a"></stop> + <stop offset=".539" stopColor="#cd8105"></stop> + <stop offset=".68" stopColor="#cb7b00"></stop> + <stop offset="1" stopColor="#f4ec26"></stop> + <stop offset="1" stopColor="#f4e72a"></stop> + </linearGradient> + <linearGradient + gradientUnits="userSpaceOnUse" + id="0-b" + x1="5.355" + x2="16.361" + y1="3.395" + y2="19.133" + > + <stop offset="0" stopColor="#f9e87f"></stop> + <stop offset=".406" stopColor="#e2b719"></stop> + <stop offset=".989" stopColor="#e2b719"></stop> + </linearGradient> + <g clipRule="evenodd" fillRule="evenodd"> + <path + d="M13.324 3.848L11 1.6 8.676 3.848l-3.201-.453-.559 3.184L2.06 8.095 3.48 11l-1.42 2.904 2.856 1.516.559 3.184 3.201-.452L11 20.4l2.324-2.248 3.201.452.559-3.184 2.856-1.516L18.52 11l1.42-2.905-2.856-1.516-.559-3.184zm-7.09 7.575l3.428 3.428 5.683-6.206-1.347-1.247-4.4 4.795-2.072-2.072z" + fill="url(#0-a)" + ></path> + <path + d="M13.101 4.533L11 2.5 8.899 4.533l-2.895-.41-.505 2.88-2.583 1.37L4.2 11l-1.284 2.627 2.583 1.37.505 2.88 2.895-.41L11 19.5l2.101-2.033 2.895.41.505-2.88 2.583-1.37L17.8 11l1.284-2.627-2.583-1.37-.505-2.88zm-6.868 6.89l3.429 3.428 5.683-6.206-1.347-1.247-4.4 4.795-2.072-2.072z" + fill="url(#0-b)" + ></path> + <path + d="M6.233 11.423l3.429 3.428 5.65-6.17.038-.033-.005 1.398-5.683 6.206-3.429-3.429-.003-1.405.005.003z" + fill="#d18800" + ></path> + </g> + </g> + </svg> +); 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 = () => ( + <svg + viewBox="0 0 22 22" + aria-label="Verified account" + role="img" + className={s.verified} + > + <g> + <path + clipRule="evenodd" + d="M12.05 2.056c-.568-.608-1.532-.608-2.1 0l-1.393 1.49c-.284.303-.685.47-1.1.455L5.42 3.932c-.832-.028-1.514.654-1.486 1.486l.069 2.039c.014.415-.152.816-.456 1.1l-1.49 1.392c-.608.568-.608 1.533 0 2.101l1.49 1.393c.304.284.47.684.456 1.1l-.07 2.038c-.027.832.655 1.514 1.487 1.486l2.038-.069c.415-.014.816.152 1.1.455l1.392 1.49c.569.609 1.533.609 2.102 0l1.393-1.49c.283-.303.684-.47 1.099-.455l2.038.069c.832.028 1.515-.654 1.486-1.486L18 14.542c-.015-.415.152-.815.455-1.099l1.49-1.393c.608-.568.608-1.533 0-2.101l-1.49-1.393c-.303-.283-.47-.684-.455-1.1l.068-2.038c.029-.832-.654-1.514-1.486-1.486l-2.038.07c-.415.013-.816-.153-1.1-.456zm-5.817 9.367l3.429 3.428 5.683-6.206-1.347-1.247-4.4 4.795-2.072-2.072z" + fillRule="evenodd" + ></path> + </g> + </svg> +); 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 = () => ( + <svg + viewBox="0 0 24 24" + aria-label="Verified account" + role="img" + className={s.verified} + > + <g> + <path d="M22.25 12c0-1.43-.88-2.67-2.19-3.34.46-1.39.2-2.9-.81-3.91s-2.52-1.27-3.91-.81c-.66-1.31-1.91-2.19-3.34-2.19s-2.67.88-3.33 2.19c-1.4-.46-2.91-.2-3.92.81s-1.26 2.52-.8 3.91c-1.31.67-2.2 1.91-2.2 3.34s.89 2.67 2.2 3.34c-.46 1.39-.21 2.9.8 3.91s2.52 1.26 3.91.81c.67 1.31 1.91 2.19 3.34 2.19s2.68-.88 3.34-2.19c1.39.45 2.9.2 3.91-.81s1.27-2.52.81-3.91c1.31-.67 2.19-1.91 2.19-3.34zm-11.71 4.2L6.8 12.46l1.41-1.42 2.26 2.26 4.8-5.23 1.47 1.36-6.2 6.77z"></path> + </g> + </svg> +); 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) => <img {...props} />; + +const TweetHeader = ({ + tweet, + components, +}: { + tweet: EnrichedTweet; + components?: TwitterComponents; +}) => { + const Img = components?.AvatarImg ?? AvatarImg; + const { user } = tweet; + + return ( + <div className={s.header}> + <a + href={tweet.url} + className={s.avatar} + target="_blank" + rel="noopener noreferrer" + > + <div + className={clsx( + s.avatarOverflow, + user.profile_image_shape === "Square" && s.avatarSquare, + )} + > + <Img + src={user.profile_image_url_https} + alt={user.name} + width={48} + height={48} + /> + </div> + <div className={s.avatarOverflow}> + <div className={s.avatarShadow}></div> + </div> + </a> + <div className={s.author}> + <a + href={tweet.url} + className={s.authorLink} + target="_blank" + rel="noopener noreferrer" + > + <div className={s.authorLinkText}> + <span title={user.name}>{user.name}</span> + </div> + <VerifiedBadge user={user} className={s.authorVerified} /> + </a> + <div className={s.authorMeta}> + <a + href={tweet.url} + className={s.username} + target="_blank" + rel="noopener noreferrer" + > + <span title={`@${user.screen_name}`}>@{user.screen_name}</span> + </a> + <div className={s.authorFollow}> + <span className={s.separator}>ยท</span> + <a + href={user.follow_url} + className={s.follow} + target="_blank" + rel="noopener noreferrer" + > + Follow + </a> + </div> + </div> + </div> + </div> + ); +}; + +export const MyTweet = ({ tweet: t, components }: Props) => { + const tweet = enrichTweet(t); + return ( + <TweetContainer className="bg-transparent !m-0 !p-0 !z-0"> + <TweetHeader tweet={tweet} components={components} /> + {tweet.in_reply_to_status_id_str && <TweetInReplyTo tweet={tweet} />} + <TweetBody tweet={tweet} /> + {tweet.mediaDetails?.length ? ( + <TweetMedia tweet={tweet} components={components} /> + ) : null} + {tweet.quoted_tweet && <QuotedTweet tweet={tweet.quoted_tweet} />} + <TweetInfo tweet={tweet} /> + </TweetContainer> + ); +}; 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 = <Verified />; + let iconClassName: string | null = s.verifiedBlue ?? null; + + if (verified) { + if (!user.is_blue_verified) { + iconClassName = s.verifiedOld!; + } + switch (user.verified_type) { + case "Government": + icon = <VerifiedGovernment />; + iconClassName = s.verifiedGovernment!; + break; + case "Business": + icon = <VerifiedBusiness />; + iconClassName = null; + break; + } + } + + return verified ? ( + <div className={clsx(className, iconClassName)}>{icon}</div> + ) : null; +}; |