Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add image generation for nft claimer #192

Open
wants to merge 1 commit into
base: add-open-graph-image
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,27 @@ router.post('/votes/:id', async (req, res) => {
}
});

router.get('/picsnap/:type(og-space|og-proposal|og-home)/:id?.:ext(png|svg)?', async (req, res) => {
const { type, id = '', ext = 'png' } = req.params;
router.get(
'/picsnap/:type(og-space|og-proposal|og-home|snap-it)/:id?.:ext(png|svg)?',
async (req, res) => {
const { type, id = '', ext = 'png' } = req.params;

try {
const image = new picSnap(type as ImageType, id, storageEngine(process.env.PICSNAP_SUBDIR));
try {
const image = new picSnap(type as ImageType, id, storageEngine(process.env.PICSNAP_SUBDIR));

res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');
res.setHeader('Content-Type', `image/${ext === 'svg' ? 'svg+xml' : 'png'}`);
return res.end(
ext === 'svg' ? await image.getSvg() : (await image.getCache()) || (await image.createCache())
);
} catch (e: any) {
capture(e);
return rpcError(res, e, id || type);
res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');
res.setHeader('Content-Type', `image/${ext === 'svg' ? 'svg+xml' : 'png'}`);
return res.end(
ext === 'svg'
? await image.getSvg()
: (await image.getCache()) || (await image.createCache())
);
} catch (e: any) {
capture(e);
return rpcError(res, e, id || type);
}
}
});
);

router.get('/moderation', async (req, res) => {
const { list } = req.query;
Expand Down
4 changes: 4 additions & 0 deletions src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ export function storageEngine(subDir?: string) {
return new FileStorageEngine(subDir);
}
}

export function shortenAddress(str = '') {
return `${str.slice(0, 6)}...${str.slice(str.length - 4)}`;
}
2 changes: 1 addition & 1 deletion src/lib/picSnap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import render from './templates/index';
import { IStorage } from '../storage/types';
import Cache from '../cache';

export type ImageType = 'og-space' | 'og-proposal' | 'og-home';
export type ImageType = 'og-space' | 'og-proposal' | 'og-home' | 'snap-it';

export default class picSnap extends Cache {
type: ImageType;
Expand Down
23 changes: 19 additions & 4 deletions src/lib/picSnap/templates/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ import satori, { SatoriOptions } from 'satori';
import getProposalSvg from './ogProposal';
import getSpaceSvg from './ogSpace';
import getHomeSvg from './ogHome';
import getSnapItSvg from './snapIt';
import { fontsData, loadDynamicAsset } from '../utils';
import type { ImageType } from '../index';

const OG_DIMENSIONS = {
width: 1200,
height: 600
height: 600,
background: '#fff'
};

const templates: Record<
ImageType,
{ prepare: (id: string) => Promise<JSX.Element>; width: number; height: number }
{
prepare: (id: string) => Promise<JSX.Element>;
width: number;
height: number;
background: string;
}
> = {
'og-home': {
...OG_DIMENSIONS,
Expand All @@ -25,19 +32,27 @@ const templates: Record<
'og-proposal': {
...OG_DIMENSIONS,
prepare: (id: string) => getProposalSvg(id)
},
'snap-it': {
width: 1200,
height: 1200,
background: 'linear-gradient(135deg, #faf5f1 0%, #f2f2fc 100%)',
prepare: (id: string) => getSnapItSvg(id)
}
};

export default async function render(type: ImageType, id: string) {
const { width, height, prepare } = templates[type];
const { width, height, prepare, background } = templates[type];
const content = await prepare(id);

return await satori(
<div
style={{
display: 'flex',
flexDirection: 'column',
background: '#fff',
background,
width,
height,
color: '#57606a',
fontSize: '40px',
fontFamily: 'Calibre',
Expand Down
12 changes: 12 additions & 0 deletions src/lib/picSnap/templates/snapIt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { fetchProposal } from '../../../../helpers/snapshot';
import template from './svg';

export default async function svg(proposalId: string) {
const proposal = await fetchProposal(proposalId);

if (!proposal) {
throw new Error('RECORD_NOT_FOUND');
}

return template(proposal);
}
52 changes: 52 additions & 0 deletions src/lib/picSnap/templates/snapIt/svg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { image, spaceAvatarUrl, spaceAuthorUrl } from '../../utils';
import type { Proposal } from '../../../../helpers/snapshot';
import { shortenAddress } from '../../../../helpers/utils';

export default async function (proposal: Proposal) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column'
}}
>
{await image(spaceAvatarUrl(proposal.space.id), {
borderRadius: '100%',
width: '160px',
height: '160px'
})}

<div
style={{
height: 500
}}
></div>

<div
style={{
color: '#111',
fontSize: '80px',
fontWeight: '700',
margin: '24px 0'
}}
>
{proposal.title}
</div>

<div
style={{
display: 'flex',
flexDirection: 'row'
}}
>
{await image(spaceAuthorUrl(proposal.author), {
borderRadius: '100%',
width: '64px',
height: '64px',
marginRight: '24px'
})}
<div style={{ fontSize: '64px' }}>{shortenAddress(proposal.author)}</div>
</div>
</div>
);
}
4 changes: 4 additions & 0 deletions src/lib/picSnap/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export function spaceAvatarUrl(spaceId: string, size = 160) {
return `https://cdn.stamp.fyi/space/${spaceId}?s=${size}`;
}

export function spaceAuthorUrl(address: string, size = 160) {
return `https://cdn.stamp.fyi/avatar/eth:${address}?s=${size}`;
}

export async function loadDynamicAsset(emojiType: keyof typeof apis, _code: string, text: string) {
if (_code === 'emoji') {
return `data:image/svg+xml;base64,${btoa(await loadEmoji(emojiType, getIconCode(text)))}`;
Expand Down