Skip to content

Commit

Permalink
feat: implement PubKey Profile Image generator
Browse files Browse the repository at this point in the history
  • Loading branch information
beeman committed Mar 11, 2024
1 parent 049b63b commit fee5adb
Show file tree
Hide file tree
Showing 31 changed files with 935 additions and 1,041 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# PubKey Dynamic Images
# PubKey Profile Images

Experiment for generating dynamic images.

Expand All @@ -23,7 +23,7 @@ Experiment for generating dynamic images.
1. Clone the repository:
```sh
git clone https://github.com/pubkeyapp/pubkey-dynamic-images my-app
git clone https://github.com/pubkeyapp/pubkey-profile-images my-app
cd my-app
pnpm install
```
Expand Down
2 changes: 1 addition & 1 deletion api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This file is generated by Nx.
#
# Build the docker image with `npx nx docker-build api`.
# Build the docker pubkey-profile with `npx nx docker-build api`.
# Tip: Modify "docker-build" options in project.json to change docker build args.
#
# Run the container with `docker run -p 3000:3000 -t api`.
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
128 changes: 128 additions & 0 deletions api/src/lib/features/generate-pub-key-profile-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/** @jsx JSX.createElement */
/** @jsxFrag JSX.Fragment */
import { Builder, JSX } from 'canvacord'

interface Props {
message: string
username: string
footer: string
header: string
avatarUrl: string
logoUrl: string
}

export interface GenerateDynamicImageOptions {
width: number
height: number
message?: string
footer?: string
header?: string
username?: string
avatarUrl?: string
logoUrl?: string
}
export class GeneratePubKeyProfileImage extends Builder<Props> {
constructor(props: GenerateDynamicImageOptions) {
// set width and height
super(props.width, props.height)
const issuedAt = new Date().toISOString().split('T')[0]
// 30 days from now
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
// initialize props
this.bootstrap({
message: props.message ?? `256 Points`,
avatarUrl: props.avatarUrl ?? 'https://avatars.githubusercontent.com/u/36491?v=4',
logoUrl:
props.logoUrl ?? 'https://raw.githubusercontent.com/pubkeyapp/pubkey-brand/main/logo/logo-white-txt400h.png',
username: props.username ?? 'beeman',
footer: props.footer ?? `Issued at: ${issuedAt} Expires at: ${expiresAt}`,
header: props.header ?? `PubKey Profile`,
})
}

async render(): Promise<JSX.Element> {
return (
<TemplateRender
avatarUrl={this.options.get('avatarUrl')}
logoUrl={this.options.get('logoUrl')}
footer={this.options.get('footer')}
header={this.options.get('header')}
message={this.options.get('message')}
username={this.options.get('username')}
/>
)
}
}

function TemplateRender({
avatarUrl,
logoUrl,
footer,
header,
message,
username,
}: {
avatarUrl: string
logoUrl: string
footer: string
header: string
message: string
username: string
}) {
return (
<div
className="h-full w-full flex flex-col justify-between bg-gray-900 rounded-[4] text-white text-2xl"
style={{ fontFamily: 'BalooBhai2 Regular' }}
>
<TemplateHeader logoUrl={logoUrl} header={header} />
<TemplateMain avatarUrl={avatarUrl} message={message} username={username} />

<TemplateFooter footer={footer} />
</div>
)
}

function TemplateMain({ avatarUrl, message, username }: { avatarUrl: string; message: string; username: string }) {
const usernameSize = username.length > 12 ? 'text-[64px]' : 'text-[92px]'
return (
<div className="flex flex-col flex-grow px-16">
<div className="flex flex-col justify-between flex-grow bg-gray-800 rounded-[4]">
<div className="flex flex-col items-center justify-center pt-24 ">
<img src={avatarUrl} alt="" className="flex h-[50] w-[50] rounded-xl" />
<div className={`mt-8 flex ${usernameSize}`} style={{ fontFamily: 'BalooBhai2 ExtraBold' }}>
{username}
</div>
</div>
<div
style={{ fontFamily: 'BalooBhai2 SemiBold' }}
className="flex flex-grow text-[92px] w-full items-center justify-center text-gray-400 mb-4"
>
{message}
</div>
</div>
</div>
)
}

function TemplateHeader({ logoUrl, header }: { logoUrl: string; header: string }) {
return (
<div className="px-16 text-4xl flex h-[32]" style={{ fontFamily: 'BalooBhai2 ExtraBold' }}>
<div className="flex w-full items-center justify-between">
<div>
<img src={logoUrl} alt="" className="flex h-[16]" />
</div>
<div>{header}</div>
</div>
</div>
)
}

function TemplateFooter({ footer }: { footer?: string }) {
return (
<div className="px-16 flex text-2xl h-[32]" style={{ fontFamily: 'BalooBhai2 Medium' }}>
<div className="flex w-full items-center justify-center">
<div className="flex text-gray-400">{footer}</div>
</div>
</div>
)
}
46 changes: 46 additions & 0 deletions api/src/lib/features/pubkey-profile-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Font } from 'canvacord'
import { Request, Response } from 'express'
import { join } from 'node:path'
import { GeneratePubKeyProfileImage } from './generate-pub-key-profile-image'

export function pubkeyProfileRoute({ cwd }: { cwd: string }) {
const fontRoot = join(cwd, 'assets/fonts')

Font.loadDefault()
Font.fromFileSync(join(fontRoot, 'BalooBhai2/BalooBhai2-ExtraBold.ttf'), 'BalooBhai2 ExtraBold')
Font.fromFileSync(join(fontRoot, 'BalooBhai2/BalooBhai2-Bold.ttf'), 'BalooBhai2 Bold')
Font.fromFileSync(join(fontRoot, 'BalooBhai2/BalooBhai2-Medium.ttf'), 'BalooBhai2 Medium')
Font.fromFileSync(join(fontRoot, 'BalooBhai2/BalooBhai2-Regular.ttf'), 'BalooBhai2 Regular')
Font.fromFileSync(join(fontRoot, 'BalooBhai2/BalooBhai2-SemiBold.ttf'), 'BalooBhai2 SemiBold')

return async (req: Request, res: Response) => {
const query = req.query as {
avatarUrl?: string
footer?: string
header?: string
logoUrl?: string
message?: string
username?: string
}

const imageBuilder = new GeneratePubKeyProfileImage({
width: 1024,
height: 1024,
message: query.message,
avatarUrl: query.avatarUrl,
footer: query.footer,
header: query.header,
logoUrl: query.logoUrl,
username: query.username,
})

const image = await imageBuilder.build({ format: 'png' })

// set headers
res.setHeader('Content-Type', 'pubkey-profile/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(image)
}
}
6 changes: 6 additions & 0 deletions api/src/lib/server-config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { dirname } from 'node:path'

export interface ServerConfig {
apiUrl: string
cwd: string
host: string
port: string
}

const cwd = dirname(import.meta.url).replace('file://', '')

export function getServerConfig(): ServerConfig {
const requiredEnvVars = [
// Place any required environment variables here
Expand All @@ -22,6 +27,7 @@ export function getServerConfig(): ServerConfig {

return {
apiUrl,
cwd,
host,
port,
}
Expand Down
6 changes: 4 additions & 2 deletions api/src/lib/server-router.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import express, { Request, Response } from 'express'

import { pubkeyProfileRoute } from './features/pubkey-profile-route'
import { uptimeRoute } from './features/uptime.route'
import { ServerConfig } from './server-config'

export function serverRouter(): express.Router {
export function serverRouter(config: ServerConfig): express.Router {
const router = express.Router()

router.use('/pubkey-profile', pubkeyProfileRoute({ cwd: config.cwd }))
router.use('/uptime', uptimeRoute())
router.use('/', (req: Request, res: Response) => res.send('PubKey API'))
router.use('*', (req: Request, res: Response) => res.status(404).send('Not Found'))
Expand Down
10 changes: 4 additions & 6 deletions api/src/lib/server.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import express from 'express'
import { existsSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { join } from 'node:path'
import favicon from 'serve-favicon'
import { ServerConfig } from './server-config'
import { serverRouter } from './server-router'

const dir = dirname(import.meta.url).replace('file://', '')

export async function server(config: ServerConfig) {
// Set up Express server
const app = express()
// Set up favicon
app.use(favicon(join(dir, 'assets', 'favicon.ico')))
app.use(favicon(join(config.cwd, 'assets', 'favicon.ico')))
// Parse JSON
app.use(express.json())
// Set base path to /api
app.use('/api', serverRouter())
app.use('/api', serverRouter(config))
// Serve static files
const staticPath = setupAssets(app, dir)
const staticPath = setupAssets(app, config.cwd)

// Start server
app.listen(Number(config.port), config.host).on('listening', async () => {
Expand Down
6 changes: 5 additions & 1 deletion api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
}
],
"compilerOptions": {
"esModuleInterop": true
"esModuleInterop": true,
"jsx": "preserve",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"target": "ESNext",
"module": "ESNext"
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@pubkey-dynamic-images/source",
"name": "@pubkey-profile-images/source",
"version": "0.0.0",
"license": "MIT",
"scripts": {
Expand Down Expand Up @@ -77,6 +77,7 @@
"@solana/web3.js": "^1.91.0",
"@tabler/icons-react": "^2.47.0",
"@tanstack/react-query": "^5.25.0",
"canvacord": "^6.0.2",
"clsx": "^2.1.0",
"dayjs": "^1.11.10",
"dotenv": "^16.4.5",
Expand Down
Loading

0 comments on commit fee5adb

Please sign in to comment.