Skip to content

Commit

Permalink
feat: add rank-card example
Browse files Browse the repository at this point in the history
  • Loading branch information
beeman committed Mar 11, 2024
1 parent d399169 commit f5118fe
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 0 deletions.
128 changes: 128 additions & 0 deletions api/src/lib/features/rank-card/generate-rank-card-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 GenerateRankCardImage 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>
)
}
56 changes: 56 additions & 0 deletions api/src/lib/features/rank-card/rank-card-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { BuiltInGraphemeProvider, Font, RankCardBuilder } from 'canvacord'
import { Request, Response } from 'express'
import { join } from 'node:path'

export function rankCardRoute({ 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
name?: string
username?: string
}

const card = new RankCardBuilder()
.setDisplayName(query.name ?? 'Wumpus 😍')
.setUsername(query.username ?? '@wumpus')
.setAvatar(`${query.avatarUrl ?? 'https://cdn.discordapp.com/embed/avatars/0.png?size=256'}`)
// .setFonts({
// progress: { xp: { value: '2' } },
// username: { name: 'BalooBhai2-ExtraBold', handle: 'BalooBhai2-Bold' },
// })
.setCurrentXP(300)
.setRequiredXP(600)
.setProgressCalculator(() => {
return Math.floor(Math.random() * 100)
})
.setLevel(2)
.setRank(5)
.setOverlay(90)
.setBackground('#23272a')
// .setBackground(`${__dirname}/minecraft.png`)
// .setStatus()
.setGraphemeProvider(BuiltInGraphemeProvider.FluentEmojiFlat)

const image = await card.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)
}
}
2 changes: 2 additions & 0 deletions api/src/lib/server-router.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import express, { Request, Response } from 'express'
import { businessVisaRoute } from './features/business-visa/business-visa-route'
import { pubkeyProfileRoute } from './features/pubkey-profile/pubkey-profile-route'
import { rankCardRoute } from './features/rank-card/rank-card-route'
import { uptimeRoute } from './features/uptime.route'
import { ServerConfig } from './server-config'

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

router.use('/business-visa', businessVisaRoute({ cwd: config.cwd }))
router.use('/rank-card', rankCardRoute({ cwd: config.cwd }))
router.use('/pubkey-profile', pubkeyProfileRoute({ cwd: config.cwd }))
router.use('/uptime', uptimeRoute())
router.use('/', (req: Request, res: Response) => res.send('PubKey API'))
Expand Down
3 changes: 3 additions & 0 deletions web/src/app/app-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import { Link, Navigate, RouteObject, useRoutes } from 'react-router-dom'
import { AppLayout } from './app-layout'
import { BusinessVisaFeature } from './features/business-visa/business-visa-feature'
import { PubkeyProfileFeature } from './features/pubkey-profile/pubkey-profile-feature'
import { RankCardFeature } from './features/rank-card/rank-card-feature'

const links: UiHeaderLink[] = [
{ label: 'Profile', link: '/profile' },
{ label: 'Ranking', link: '/rank-card' },
{ label: 'Visa', link: '/visa' },
]
const routes: RouteObject[] = [
{ path: '/', element: <Navigate to="/profile" replace /> },
{ path: '/visa/*', element: <BusinessVisaFeature /> },
{ path: '/rank-card/*', element: <RankCardFeature /> },
{ path: '/profile/*', element: <PubkeyProfileFeature /> },
{ path: '*', element: <UiNotFound /> },
]
Expand Down
28 changes: 28 additions & 0 deletions web/src/app/features/rank-card/rank-card-data-access.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export interface RankCardPayload {
status?: string
name?: string
earnings?: string
}

export const profiles: RankCardPayload[] = [
{
name: 'beeman',
status: 'Expired',
earnings: '42 USD',
},
{
name: 'derlys',
status: 'Active',
earnings: '1000 USD',
},
{
name: 'sundeep',
status: 'Active',
earnings: '512 USD',
},
{
name: 'deanmachine',
status: 'Expired',
earnings: '1024 USD',
},
]
11 changes: 11 additions & 0 deletions web/src/app/features/rank-card/rank-card-feature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useMemo, useState } from 'react'
import { profiles } from './rank-card-data-access'
import { RankCardUiLayout } from './rank-card-ui'

export function RankCardFeature() {
const baseUrl = '/api/rank-card'
const [params, setParams] = useState<string | undefined>()
const url = useMemo(() => `${baseUrl}${params ? `?${params}` : ''}`, [params])

return <RankCardUiLayout url={url} setParams={setParams} profiles={profiles} />
}
110 changes: 110 additions & 0 deletions web/src/app/features/rank-card/rank-card-ui.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { AspectRatio, Button, Flex, Grid, Group, Image, Paper, rem, TextInput } from '@mantine/core'
import { useForm } from '@mantine/form'
import { UiDebugModal, UiStack } from '@pubkey-ui/core'
import { useEffect, useState } from 'react'
import { RankCardPayload } from './rank-card-data-access'

export function RankCardUiLayout({
profiles,
url,
setParams,
}: {
profiles: RankCardPayload[]
url: string
setParams: (params: string) => void
}) {
const [profile, setProfile] = useState<RankCardPayload | undefined>(profiles[0])
return (
<Flex direction="column" align="stretch" justify="center" h="calc(100vh - 100px)" maw={1500} mx="auto">
<Paper withBorder radius="md" shadow="lg">
<Grid h="100%" gutter={0}>
<Grid.Col span={8} p="md">
<RankCardUiImagePreview url={url} />
</Grid.Col>
<Grid.Col span={4} py="md" pr="md">
<UiStack>
<RankCardUiForm profile={profile} setParams={setParams} />
<Group justify="center">
{profiles.map((profile, index) => (
<Button variant="light" key={index} onClick={() => setProfile(profile)}>
{profile.name}
</Button>
))}
<UiDebugModal data={{ url }} />
</Group>
</UiStack>
</Grid.Col>
</Grid>
</Paper>
</Flex>
)
}

export function RankCardUiForm({
profile,
setParams,
}: {
profile?: RankCardPayload
setParams: (params: string) => void
}) {
function submit(profile?: RankCardPayload) {
const values = (profile ?? form.getTransformedValues()) as Record<string, string>
const keys = Object.keys(values).filter((key) => values[key])
if (!keys.length) {
return
}
const valueMap = keys.map((key) => `${key}=${values[key]}`)
setParams(valueMap.join('&'))
}
const form = useForm<RankCardPayload>({
initialValues: {
status: '',
earnings: '',
name: '',
},
})
useEffect(() => {
if (profile) {
form.setValues(profile)
submit(profile)
}
}, [profile])
return (
<form onSubmit={form.onSubmit(submit)}>
<UiStack>
<TextInput
minLength={3}
maxLength={20}
size="lg"
label="Name"
placeholder="Text displayed in the name field"
{...form.getInputProps('name')}
/>
<TextInput
size="lg"
label="Earnings"
placeholder="Text displayed in the earnings field"
{...form.getInputProps('earnings')}
/>

<TextInput
size="lg"
label="Status"
placeholder="Text displayed in the status field"
{...form.getInputProps('status')}
/>
<Button size="xl" type="submit">
Generate
</Button>
</UiStack>
</form>
)
}

export function RankCardUiImagePreview({ url }: { url: string }) {
return (
<AspectRatio ratio={10 / 3} style={{ flex: `0 0 ${rem(100)}`, flexGrow: 1 }}>
<Image src={url} alt="Generated Image" />
</AspectRatio>
)
}

0 comments on commit f5118fe

Please sign in to comment.