From 7c970953cb77c8a12c7e6f2fd9f50de9f7e58c51 Mon Sep 17 00:00:00 2001 From: Yohan Date: Tue, 8 Oct 2024 01:26:52 +0200 Subject: [PATCH] feat: implemented collection sharing image (#188) --- .../src/app/api/og/collection/route.tsx | 236 ++++++++++++++++++ .../collection/[collectionAddress]/page.tsx | 41 +-- apps/arkmarket/src/components/media.tsx | 20 +- apps/arkmarket/src/lib/getCollectionTokens.ts | 18 ++ 4 files changed, 279 insertions(+), 36 deletions(-) create mode 100644 apps/arkmarket/src/app/api/og/collection/route.tsx diff --git a/apps/arkmarket/src/app/api/og/collection/route.tsx b/apps/arkmarket/src/app/api/og/collection/route.tsx new file mode 100644 index 00000000..90983b0e --- /dev/null +++ b/apps/arkmarket/src/app/api/og/collection/route.tsx @@ -0,0 +1,236 @@ +import { ImageResponse } from "next/og"; + +import { formatNumber, formatUnits } from "@ark-market/ui"; + +import CustomFonts from "~/components/custom-fonts"; +import { env } from "~/env"; +import getCollection from "~/lib/getCollection"; +import { getCollectionTokens, getMediaSrc } from "~/lib/getCollectionTokens"; + +const IMAGE_GLOBAL_MARGIN = "50px"; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + + const hasCollection = searchParams.has("collection_address"); + if (!hasCollection) { + throw new Error("Missing collection address"); + } + const collectionAddress = searchParams.get("collection_address") ?? ""; + + const collection = await getCollection({ collectionAddress }); + + if (collection === null) { + throw new Error("Failed to fetch collection data"); + } + const collectionTokens = await getCollectionTokens({ collectionAddress }); + + if (collectionTokens.data.length === 0) { + throw new Error("Failed to fetch collection tokens data"); + } + const token = collectionTokens.data[0]; + const tokenMediaSrc = + !token?.metadata?.image.includes("data:image/") && + token?.metadata !== undefined + ? getMediaSrc( + token.metadata.image, + token.metadata.image_key, + undefined, + 300, + 300, + ) + : collection.image; + + return new ImageResponse( + ( +
+ + {tokenMediaSrc !== undefined && ( + + )} +
+
+
+
+ {collection.image !== undefined && ( + + )} +

+ {collection.name} +

+
+ +
+ +
+
+ ), + { + width: 1500, + height: 844, + }, + ); + } catch { + return new Response("Failed to generate og collection image", { + status: 500, + }); + } +} + +function Divider() { + return
; +} + +interface CollectionDataProps { + tokenCount: number; + ownerCount: number; + floor: string; + totalVolume: number; +} + +function CollectionData({ + ownerCount, + tokenCount, + floor, + totalVolume, +}: CollectionDataProps) { + return ( +
+
+

Floor

+
+ +

{formatUnits(floor, 18)}

+
+
+ +
+

Total volume

+
+ +

{formatNumber(totalVolume)}

+
+
+ +
+

Items

+

{tokenCount}

+
+ +
+

Owners

+

{ownerCount}

+
+
+ ); +} + +function Logo() { + if (env.NEXT_PUBLIC_THEME !== "unframed") { + return null; + } + return ( + + + + ); +} + +function EtherLogo() { + return ( + + + + ); +} diff --git a/apps/arkmarket/src/app/collection/[collectionAddress]/page.tsx b/apps/arkmarket/src/app/collection/[collectionAddress]/page.tsx index 6ba023d4..816a59a8 100644 --- a/apps/arkmarket/src/app/collection/[collectionAddress]/page.tsx +++ b/apps/arkmarket/src/app/collection/[collectionAddress]/page.tsx @@ -21,23 +21,30 @@ export default async function CollectionPage({ params }: CollectionPageProps) { } return ( -
- + - - - -
+ +
+ + + + +
+ ); } diff --git a/apps/arkmarket/src/components/media.tsx b/apps/arkmarket/src/components/media.tsx index 5909e120..ec0bd2bb 100644 --- a/apps/arkmarket/src/components/media.tsx +++ b/apps/arkmarket/src/components/media.tsx @@ -8,7 +8,7 @@ import { cn } from "@ark-market/ui"; import { NoImage } from "@ark-market/ui/icons"; import { Skeleton } from "@ark-market/ui/skeleton"; -import { env } from "~/env"; +import { getMediaSrc } from "~/lib/getCollectionTokens"; interface MediaProps { // key used to access the image proxy / CDN @@ -21,24 +21,6 @@ interface MediaProps { priority?: boolean; } -function getMediaSrc( - src?: string | null, - mediaKey?: string | null, - thumbnailKey?: string | null, - width?: number, - height?: number, -) { - if (thumbnailKey) { - return `${env.NEXT_PUBLIC_IMAGE_CDN_URL}/${thumbnailKey}`; - } - - if (mediaKey && width && height) { - const resolutionParam = `:${width}:${height}`; - return `${env.NEXT_PUBLIC_IMAGE_PROXY_URL}/_/rs:fit${resolutionParam}/plain/${env.NEXT_PUBLIC_IMAGE_CDN_URL}/${mediaKey}`; - } - return src?.replace("ipfs://", env.NEXT_PUBLIC_IPFS_GATEWAY); -} - function MediaPlaceholder({ className }: PropsWithClassName) { return (