-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
338 additions
and
0 deletions.
There are no files selected for viewing
128 changes: 128 additions & 0 deletions
128
api/src/lib/features/rank-card/generate-rank-card-image.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} /> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |