diff --git a/apps/api/src/assets/images/brand/brand-discord.png b/apps/api/src/assets/images/brand/brand-discord.png new file mode 100644 index 0000000..9130ad0 Binary files /dev/null and b/apps/api/src/assets/images/brand/brand-discord.png differ diff --git a/apps/api/src/assets/images/brand/brand-github.png b/apps/api/src/assets/images/brand/brand-github.png new file mode 100644 index 0000000..a841523 Binary files /dev/null and b/apps/api/src/assets/images/brand/brand-github.png differ diff --git a/apps/api/src/assets/images/brand/brand-google.png b/apps/api/src/assets/images/brand/brand-google.png new file mode 100644 index 0000000..0c7d94d Binary files /dev/null and b/apps/api/src/assets/images/brand/brand-google.png differ diff --git a/apps/api/src/assets/images/brand/brand-x.png b/apps/api/src/assets/images/brand/brand-x.png new file mode 100644 index 0000000..2929edb Binary files /dev/null and b/apps/api/src/assets/images/brand/brand-x.png differ diff --git a/libs/api/metadata/data-access/src/lib/api-metadata-image.service.ts b/libs/api/metadata/data-access/src/lib/api-metadata-image.service.ts index cbc2eff..63fb326 100644 --- a/libs/api/metadata/data-access/src/lib/api-metadata-image.service.ts +++ b/libs/api/metadata/data-access/src/lib/api-metadata-image.service.ts @@ -8,7 +8,7 @@ import { LRUCache } from 'lru-cache' import * as fs from 'node:fs/promises' import { join } from 'node:path' import { ExtensionTokenMetadata } from './api-metadata.service' -import { GenerateBusinessVisaImage } from './generators/generate-business-visa-image' +import { BusinessVisaSocials, GenerateBusinessVisaImage } from './generators/generate-business-visa-image' type IdentityWithProfile = Omit & { profile: { username: string } } type UserWithIdentities = User & { identities: IdentityWithProfile[] } @@ -199,14 +199,23 @@ export class ApiMetadataImageService implements OnModuleInit { const poweredBy = await this.renderPoweredBy() const icon = await this.renderIcon(community) const logo = await this.renderLogo(community) - - if (!icon || !logo) { - throw new Error('Missing icon or logo. Expected in brand folder') + const brands = await this.getBrands() + if (!icon || !logo || !brands) { + throw new Error('Missing brands, icon or logo. Expected in brand folder') } + const discordIdentity = user.identities.find((i) => i.provider === IdentityProvider.Discord) const githubIdentity = user.identities.find((i) => i.provider === IdentityProvider.GitHub) + const googleIdentity = user.identities.find((i) => i.provider === IdentityProvider.Google) const twitterIdentity = user.identities.find((i) => i.provider === IdentityProvider.Twitter) + const socials: BusinessVisaSocials = { + discord: discordIdentity ? discordIdentity?.profile?.username : '', + github: githubIdentity ? githubIdentity?.profile?.username : '', + google: googleIdentity ? googleIdentity?.profile?.username : '', + x: twitterIdentity ? twitterIdentity?.profile?.username : '', + } + const templateData = { left: { username: user.username, @@ -234,10 +243,21 @@ export class ApiMetadataImageService implements OnModuleInit { poweredBy, icon, logo, + brands, + socials, }) const image: Buffer = (await imageBuilder.build({ format: 'png' })) as Buffer return image } + + private async getBrands() { + return { + discord: this.brandMap.get('brand-discord.png') as Buffer, + github: this.brandMap.get('brand-github.png') as Buffer, + google: this.brandMap.get('brand-google.png') as Buffer, + x: this.brandMap.get('brand-x.png') as Buffer, + } + } } diff --git a/libs/api/metadata/data-access/src/lib/api-metadata.service.ts b/libs/api/metadata/data-access/src/lib/api-metadata.service.ts index e8a909f..56d5ea3 100644 --- a/libs/api/metadata/data-access/src/lib/api-metadata.service.ts +++ b/libs/api/metadata/data-access/src/lib/api-metadata.service.ts @@ -35,6 +35,13 @@ export interface LocalMetadata { @Injectable() export class ApiMetadataService { private readonly logger = new Logger(ApiMetadataService.name) + + constructor( + private readonly core: ApiCoreService, + private readonly image: ApiMetadataImageService, + private readonly solana: ApiSolanaService, + ) {} + private readonly externalMetadataCache = new LRUCache({ max: 1000, ttl: TEN_MINUTES, @@ -102,7 +109,7 @@ export class ApiMetadataService { if (metadata) { // The metadata is external, fetch it and return - if (!metadata.state.uri.startsWith(this.core.config.apiUrl)) { + if (!metadata.state.uri.includes('/api/metadata/')) { // We have external metadata const externalMetadata = await this.externalMetadataCache.fetch(metadata.state.uri) @@ -141,29 +148,31 @@ export class ApiMetadataService { }, }) - constructor( - private readonly core: ApiCoreService, - private readonly image: ApiMetadataImageService, - private readonly solana: ApiSolanaService, - ) {} + private readonly imageCache = new LRUCache({ + max: 1000, + ttl: TEN_MINUTES, + fetchMethod: async (account: string) => { + const accountMetadata = await this.accountMetadataCache.fetch(account) + if (!accountMetadata) { + throw new Error(`Failed to fetch metadata for ${account}`) + } - async getImage(account: string): Promise { - this.logger.verbose(`Fetching metadata image for ${account}`) - const accountMetadata = await this.accountMetadataCache.fetch(account) - if (!accountMetadata) { - throw new Error(`Failed to fetch metadata for ${account}`) - } + if (!accountMetadata.state.uri?.includes('/api/metadata/')) { + const externalMetadata = await this.externalMetadataCache.fetch(accountMetadata.state.uri) + if (!externalMetadata) { + throw new Error(`Failed to fetch external metadata for ${accountMetadata.state.uri}`) + } - if (!accountMetadata.state.uri?.startsWith(this.core.config.apiUrl)) { - const externalMetadata = await this.externalMetadataCache.fetch(accountMetadata.state.uri) - if (!externalMetadata) { - throw new Error(`Failed to fetch external metadata for ${accountMetadata.state.uri}`) + return externalMetadata.image } - return externalMetadata.image - } + return this.image.generate(account, accountMetadata) + }, + }) - return this.image.generate(account, accountMetadata) + async getImage(account: string): Promise { + this.logger.verbose(`Fetching metadata image for ${account}`) + return this.imageCache.fetch(account) } async getJson(account: string) { diff --git a/libs/api/metadata/data-access/src/lib/generators/generate-business-visa-image.tsx b/libs/api/metadata/data-access/src/lib/generators/generate-business-visa-image.tsx index d276240..ccef299 100644 --- a/libs/api/metadata/data-access/src/lib/generators/generate-business-visa-image.tsx +++ b/libs/api/metadata/data-access/src/lib/generators/generate-business-visa-image.tsx @@ -3,6 +3,20 @@ import { Builder, JSX } from 'canvacord' import { CSSProperties } from 'react' +export interface BusinessVisaBrands { + discord: Buffer + github: Buffer + google: Buffer + x: Buffer +} + +export interface BusinessVisaSocials { + discord?: string + github?: string + google?: string + x?: string +} + interface Props { status: string earnings: string @@ -13,6 +27,8 @@ interface Props { poweredBy: Buffer icon: Buffer logo: Buffer + brands: BusinessVisaBrands + socials: BusinessVisaSocials } export interface GenerateDynamicImageOptions { @@ -27,6 +43,8 @@ export interface GenerateDynamicImageOptions { poweredBy: Buffer icon: Buffer logo: Buffer + brands: BusinessVisaBrands + socials: BusinessVisaSocials } export class GenerateBusinessVisaImage extends Builder { constructor(props: GenerateDynamicImageOptions) { @@ -44,6 +62,8 @@ export class GenerateBusinessVisaImage extends Builder { poweredBy: props.poweredBy, icon: props.icon, logo: props.logo, + brands: props.brands, + socials: props.socials, }) } @@ -59,12 +79,16 @@ export class GenerateBusinessVisaImage extends Builder { icon={this.options.get('icon')} logo={this.options.get('logo')} avatar={this.options.get('avatar')} + brands={this.options.get('brands')} + socials={this.options.get('socials')} /> ) } } function TemplateRender({ + socials, + brands, icon, logo, poweredBy, @@ -75,6 +99,8 @@ function TemplateRender({ avatar, community, }: { + socials: BusinessVisaSocials + brands: BusinessVisaBrands icon: Buffer logo: Buffer poweredBy: Buffer @@ -89,6 +115,11 @@ function TemplateRender({ const base64PoweredBy = Buffer.from(poweredBy).toString('base64') const base64Icon = Buffer.from(icon).toString('base64') const base64Logo = Buffer.from(logo).toString('base64') + const base64Discord = Buffer.from(brands.discord).toString('base64') + const base64Github = Buffer.from(brands.github).toString('base64') + const base64Google = Buffer.from(brands.google).toString('base64') + const base64X = Buffer.from(brands.x).toString('base64') + const gray = '#383838' const brand = '#8743FF' const brandLight = '#D6C6FF' @@ -108,6 +139,14 @@ function TemplateRender({ width: '401px', left: '541px', } + + const socialMap = Object.keys(socials) + .filter((key) => (socials[key as keyof BusinessVisaSocials] ?? '').length > 0) + .map((key) => ({ + username: socials[key as keyof BusinessVisaSocials], + icon: brands[key as keyof BusinessVisaBrands], + })) + return (
{name}
{community}
-
-
x.com/beeman_nl
-
-
-
github.com/beeman
-
- {/* RIGHT CARD */} + {socialMap + .filter((social) => (social.username ?? '')?.length > 0) + .map((social, index) => { + const top = 640 + index * 40 + return ( +
+ +
+ ) + })}
{status}
@@ -210,3 +252,12 @@ function TemplateRender({
) } + +function SocialIcon({ username, icon }: { username: string; icon: string }) { + return ( +
+ + {username} +
+ ) +} diff --git a/libs/api/metadata/feature/src/lib/api-metadata.controller.ts b/libs/api/metadata/feature/src/lib/api-metadata.controller.ts index 3846d0e..33c3ac9 100644 --- a/libs/api/metadata/feature/src/lib/api-metadata.controller.ts +++ b/libs/api/metadata/feature/src/lib/api-metadata.controller.ts @@ -18,15 +18,10 @@ export class ApiMetadataController { return res.redirect(result) } - // set headers res.setHeader('Content-Type', 'image/png') res.setHeader('Cache-Control', 'no-store no-cache must-revalidate private max-age=0 s-maxage=0 proxy-revalidate') - // send pubkey-profile res.send(result) - // - // res.writeHead(200, { 'Content-Type': 'image/png', 'Content-Length': result.length }) - // res.end(result) } @Get('json/:account') diff --git a/libs/web/asset/feature/src/lib/user-asset-detail-feature.tsx b/libs/web/asset/feature/src/lib/user-asset-detail-feature.tsx index 954b7eb..d0939dc 100644 --- a/libs/web/asset/feature/src/lib/user-asset-detail-feature.tsx +++ b/libs/web/asset/feature/src/lib/user-asset-detail-feature.tsx @@ -1,10 +1,11 @@ -import { Accordion, SimpleGrid, Text } from '@mantine/core' +import { Accordion, Group, SimpleGrid, Text } from '@mantine/core' import { UiCard, UiDebugModal, UiGroup, UiInfo, UiLoader, UiPage, UiWarning } from '@pubkey-ui/core' import { useQuery } from '@tanstack/react-query' import { Asset, AssetActivityType } from '@tokengator/sdk' import { useGetAsset, useGetAssetActivity } from '@tokengator/web-asset-data-access' import { AssetActivityUiEntryList, AssetActivityUiPoints, AssetUiItem } from '@tokengator/web-asset-ui' import { useSdk } from '@tokengator/web-core-data-access' +import { SolanaExplorerIcon } from '@tokengator/web-solana-ui' import { useParams } from 'react-router-dom' export function useMetadataAll(account: string) { @@ -28,7 +29,14 @@ export function UserAssetDetailFeature() { return loading ? ( ) : asset ? ( - }> + + + + + } + > diff --git a/libs/web/community/ui/src/lib/minter-ui-assets.tsx b/libs/web/community/ui/src/lib/minter-ui-assets.tsx index 3e869d2..d7930a2 100644 --- a/libs/web/community/ui/src/lib/minter-ui-assets.tsx +++ b/libs/web/community/ui/src/lib/minter-ui-assets.tsx @@ -1,6 +1,7 @@ -import { AspectRatio, Image, SimpleGrid, useMantineTheme } from '@mantine/core' +import { AspectRatio, Group, Image, SimpleGrid, useMantineTheme } from '@mantine/core' import { UiAnchor, UiCard, UiCardTitle, UiDebugModal, UiGroup } from '@pubkey-ui/core' import { AccountInfo, ParsedAccountData } from '@solana/web3.js' +import { SolanaExplorerIcon } from '@tokengator/web-solana-ui' export function MinterUiAssets({ items }: { items: AccountInfo[] }) { const { colors } = useMantineTheme() @@ -18,7 +19,10 @@ export function MinterUiAssets({ items }: { items: AccountInfo Asset {index} - + + + + } key={index}