Skip to content

Commit

Permalink
feat: add social icons to business visa
Browse files Browse the repository at this point in the history
  • Loading branch information
beeman committed Apr 7, 2024
1 parent 4ba954b commit b738afe
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 39 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/api/src/assets/images/brand/brand-google.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/api/src/assets/images/brand/brand-x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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<Identity, 'profile'> & { profile: { username: string } }
type UserWithIdentities = User & { identities: IdentityWithProfile[] }
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
}
}
}
47 changes: 28 additions & 19 deletions libs/api/metadata/data-access/src/lib/api-metadata.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ExternalMetadata>({
max: 1000,
ttl: TEN_MINUTES,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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<string, Buffer | string>({
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<Buffer | string> {
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<Buffer | string | undefined> {
this.logger.verbose(`Fetching metadata image for ${account}`)
return this.imageCache.fetch(account)
}

async getJson(account: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,6 +27,8 @@ interface Props {
poweredBy: Buffer
icon: Buffer
logo: Buffer
brands: BusinessVisaBrands
socials: BusinessVisaSocials
}

export interface GenerateDynamicImageOptions {
Expand All @@ -27,6 +43,8 @@ export interface GenerateDynamicImageOptions {
poweredBy: Buffer
icon: Buffer
logo: Buffer
brands: BusinessVisaBrands
socials: BusinessVisaSocials
}
export class GenerateBusinessVisaImage extends Builder<Props> {
constructor(props: GenerateDynamicImageOptions) {
Expand All @@ -44,6 +62,8 @@ export class GenerateBusinessVisaImage extends Builder<Props> {
poweredBy: props.poweredBy,
icon: props.icon,
logo: props.logo,
brands: props.brands,
socials: props.socials,
})
}

Expand All @@ -59,12 +79,16 @@ export class GenerateBusinessVisaImage extends Builder<Props> {
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,
Expand All @@ -75,6 +99,8 @@ function TemplateRender({
avatar,
community,
}: {
socials: BusinessVisaSocials
brands: BusinessVisaBrands
icon: Buffer
logo: Buffer
poweredBy: Buffer
Expand All @@ -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'
Expand All @@ -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 (
<div
style={{
Expand Down Expand Up @@ -151,13 +190,16 @@ function TemplateRender({
</div>
<div style={{ ...lineItem, top: '530px', fontFamily: 'BalooBhai2-SemiBold', fontSize: '48px' }}>{name}</div>
<div style={{ ...lineItem, height: '50px', top: '580px', fontSize: '36px' }}>{community}</div>
<div style={{ ...lineItem, height: '50px', top: '680px', fontSize: '24px' }}>
<div>x.com/beeman_nl</div>
</div>
<div style={{ ...lineItem, height: '50px', top: '730px', fontSize: '24px' }}>
<div>github.com/beeman</div>
</div>
{/* RIGHT CARD */}
{socialMap
.filter((social) => (social.username ?? '')?.length > 0)
.map((social, index) => {
const top = 640 + index * 40
return (
<div key={index} style={{ ...lineItem, height: '50px', top: `${top}px`, fontSize: '24px' }}>
<SocialIcon icon={social.icon.toString('base64')} username={social.username as string} />
</div>
)
})}
<div style={{ ...leftItem, top: '230px', fontFamily: 'BalooBhai2-SemiBold', fontSize: '48px', color: brand }}>
{status}
</div>
Expand Down Expand Up @@ -210,3 +252,12 @@ function TemplateRender({
</div>
)
}

function SocialIcon({ username, icon }: { username: string; icon: string }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<img height={24} src={`data:image/png;base64,${icon}`} alt="" />
<span>{username}</span>
</div>
)
}
5 changes: 0 additions & 5 deletions libs/api/metadata/feature/src/lib/api-metadata.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
12 changes: 10 additions & 2 deletions libs/web/asset/feature/src/lib/user-asset-detail-feature.tsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -28,7 +29,14 @@ export function UserAssetDetailFeature() {
return loading ? (
<UiLoader />
) : asset ? (
<UiPage rightAction={<UiDebugModal data={{ asset, metadata }} />}>
<UiPage
rightAction={
<Group>
<SolanaExplorerIcon path={`account/${account}`} />
<UiDebugModal data={{ asset, metadata }} />
</Group>
}
>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<UiCard title="Asset">
<AssetUiItem asset={asset} />
Expand Down
8 changes: 6 additions & 2 deletions libs/web/community/ui/src/lib/minter-ui-assets.tsx
Original file line number Diff line number Diff line change
@@ -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<ParsedAccountData>[] }) {
const { colors } = useMantineTheme()
Expand All @@ -18,7 +19,10 @@ export function MinterUiAssets({ items }: { items: AccountInfo<ParsedAccountData
<UiAnchor to={`/assets/${mint}`}>
<UiCardTitle>Asset {index}</UiCardTitle>
</UiAnchor>
<UiDebugModal data={item} />
<Group gap="xs">
<SolanaExplorerIcon path={`account/${mint}`} />
<UiDebugModal data={item} />
</Group>
</UiGroup>
}
key={index}
Expand Down

0 comments on commit b738afe

Please sign in to comment.