Skip to content

Commit

Permalink
✨ feat: Add SponsorKit
Browse files Browse the repository at this point in the history
  • Loading branch information
canisminor1990 committed Dec 6, 2023
1 parent 6cbec02 commit 4f38bc4
Show file tree
Hide file tree
Showing 8 changed files with 405 additions and 1 deletion.
59 changes: 59 additions & 0 deletions api/sponsor.tsx
@@ -0,0 +1,59 @@
import { ImageResponse } from '@vercel/og';

import Sponsor from '../src/Sponsor';

export const config = {
runtime: 'edge',
};

const getNumber = (value: string | null, defaultValue?: number) => {
if (!value || value === null) return defaultValue;
const parsed = Number.parseInt(value, 10);
if (Number.isNaN(parsed)) return defaultValue;
return parsed;
};

const getData = async (id: string) => {
const res = await fetch(`https://opencollective.com/${id}/members/all.json`);
const json = await res.json();
return json;
};
export default async function handler(request: Request): Promise<any> {
try {
const { searchParams } = new URL(request.url);

const avatarSize = getNumber(searchParams.get('avatarSize'));
const width = getNumber(searchParams.get('width'), 800);
const height = getNumber(searchParams.get('height'));
const padding = getNumber(searchParams.get('padding'));
const themeMode = searchParams.get('themeMode') === 'dark' ? 'dark' : 'light';
const id = searchParams.get('id') || 'lobehub';
const data = (await getData(id)) as any;

return new ImageResponse(
(
<Sponsor
avatarSize={avatarSize}
data={data}
padding={padding}
themeMode={themeMode}
width={width}
/>
),
{
headers: {
'CDN-Cache-Control': 'public, s-maxage=60',
'Cache-Control': 'public, s-maxage=1',
'Vercel-CDN-Cache-Control': 'public, s-maxage=3600',
},
height,
width,
},
);
} catch (error: any) {
console.log(`${error.message}`);
return new Response(`Failed to generate the image`, {
status: 500,
});
}
}
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -56,6 +56,7 @@
"dependencies": {
"@babel/runtime": "^7",
"@lobehub/ui": "latest",
"@vercel/og": "^0.5.20",
"ahooks": "^3",
"antd": "^5",
"antd-style": "^3",
Expand All @@ -75,7 +76,7 @@
"remark-slug": "^7",
"remark-toc": "^8",
"simple-icons": "^9",
"swr": "^2",
"swr": "^2.2.4",
"url-join": "^5",
"use-merge-value": "^1",
"utility-types": "^3",
Expand Down
53 changes: 53 additions & 0 deletions src/Sponsor/Avatar.tsx
@@ -0,0 +1,53 @@
import { CSSProperties, FC } from 'react';

import { theme } from '@/Sponsor/style';

interface AvatarProps {
name: string;
size?: number;
src?: string;
style?: CSSProperties;
themeMode?: 'light' | 'dark';
}

export const Avatar: FC<AvatarProps> = ({
src,
name,
size = 64,
style,

themeMode,
}) => {
const styles = theme(themeMode);
return (
<div
style={{
alignItems: 'center',
backgroundColor: styles.avatarBackgroundColor,
border: `${Math.floor(size / 64)}px solid ${styles.borderColor}`,
borderRadius: '50%',
color: styles.avatarFontColor,
display: 'flex',
height: size,
justifyContent: 'center',
overflow: 'hidden',
width: size,
...style,
}}
>
{src ? (
<img alt={name} height={'100%'} src={src} width={'100%'} />
) : (
<div
style={{
fontSize: size / 2,
fontWeight: 'bold',
lineHeight: 1,
}}
>
{name.slice(0, 2).toUpperCase()}
</div>
)}
</div>
);
};
37 changes: 37 additions & 0 deletions src/Sponsor/demos/index.tsx
@@ -0,0 +1,37 @@
import { Sponsor } from '@lobehub/readme-wizard';
import { StoryBook, useControls, useCreateStore } from '@lobehub/ui';
import { useThemeMode } from 'antd-style';
import useSWR from 'swr';

export default () => {
const { isDarkMode } = useThemeMode();
const store = useCreateStore();
const { id, ...config } = useControls(
{
avatarSize: 64,
id: 'lobehub',
padding: 0,
width: 800,
},
{ store },
);
const { data, isLoading } = useSWR(
id,
async () => {
const res = await fetch(`https://opencollective.com/${id}/members/all.json`);
const json = await res.json();
return json;
},
{ revalidateOnFocus: false },
);

return (
<StoryBook levaStore={store}>
{isLoading || !data ? (
<div>loading...</div>
) : (
<Sponsor data={data} themeMode={isDarkMode ? 'dark' : 'light'} {...config} />
)}
</StoryBook>
);
};
8 changes: 8 additions & 0 deletions src/Sponsor/index.md
@@ -0,0 +1,8 @@
---
nav: components
group: sponsor
title: SponsorKit
order: 1
---

<code src="./demos/index.tsx" nopadding></code>
221 changes: 221 additions & 0 deletions src/Sponsor/index.tsx
@@ -0,0 +1,221 @@
import { CSSProperties, FC } from 'react';

import { Avatar } from '@/Sponsor/Avatar';
import { theme } from '@/Sponsor/style';

interface MemberProfile {
MemberId: number;
company?: string;
createdAt: string;
currency?: string;
description?: string;
email?: string;
github?: string;
image?: string;
isActive: boolean;
lastTransactionAmount: number;
lastTransactionAt: string;
name: string;
profile: string;
role: 'ADMIN' | 'HOST' | 'BACKER';
tier?: string;
totalAmountDonated: number;
twitter?: string;
type: 'USER' | 'ORGANIZATION';
website?: string;
}

interface TierItem {
emoji: string;
preset: 'backer' | 'sponsor';
sort: number;
style?: CSSProperties;
title: string;
}

const DEFAULT_GROUP: TierItem[] = [
{
emoji: '🥇',
preset: 'sponsor',
sort: 22,
style: {
backgroundImage: `linear-gradient(45deg, #F5E729 0%, #DC9A01 33%, #DC9A01 66%, #F5E729 100%)`,
},
title: '🥇 Gold Sponsor',
},
{
emoji: '🥈',
preset: 'sponsor',
sort: 21,
style: {
backgroundImage: `linear-gradient(45deg, #D8D8D8 0%, #888888 33%, #888888 66%, #D8D8D8 100%)`,
},
title: '🥈 Silver Sponsor',
},
{
emoji: '🥉',
preset: 'sponsor',
sort: 20,
style: {
backgroundImage: `linear-gradient(45deg, #D8974D 0%, #833204 33%, #833204 66%, #D8974D 100%)`,
},
title: '🥉 Bronze Sponsor',
},
{
emoji: '💖',
preset: 'backer',
sort: 11,
title: '💖 Generous Backer',
},
{
emoji: '☕',
preset: 'backer',
sort: 10,
title: '☕ Backer',
},
{
emoji: '🌟',
preset: 'backer',
sort: 1,
title: '🌟 One Time',
},
];

export interface SponsorProps {
avatarSize?: number;
data: MemberProfile[];
fallbackTier?: string;
groupBy?: TierItem[];
height?: number;
padding?: number;
texts?: [string, string];
themeMode?: 'light' | 'dark';
width?: number;
}

const Sponsor: FC<SponsorProps> = ({
padding,
width = 800,
height,
data,
groupBy = DEFAULT_GROUP,
fallbackTier = '🌟 One Time',
avatarSize = 64,
texts = ['Become ❤️', 'LobeHub'],
themeMode,
}) => {
const styles = theme(themeMode);
const sortedData = () => {
const filteredData = data.filter((item) => item.totalAmountDonated > 0);
const tierSortMap = new Map(groupBy.map((item) => [item.title, item.sort]));
return [...filteredData].sort((a, b) => {
const sortA = tierSortMap.get(a.tier || fallbackTier) || 0;
const sortB = tierSortMap.get(b.tier || fallbackTier) || 0;
if (sortA !== sortB) {
return sortB - sortA;
}
return b.totalAmountDonated - a.totalAmountDonated;
});
};

const getTier = (tier = fallbackTier): TierItem => {
return (
groupBy.find((item) => item.title.toLowerCase() === tier.toLowerCase()) ||
(groupBy.at(-1) as TierItem)
);
};

return (
<div
style={{
alignItems: 'center',
display: 'flex',
flexWrap: 'wrap',
gap: avatarSize / 8,
height,
padding,
width,
}}
>
{sortedData().map((item) => {
const tierConfig = getTier(item.tier);
return (
<div
key={item.MemberId}
style={{
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
lineHeight: 1,
position: 'relative',
}}
>
<div
style={{
alignItems: 'center',
borderRadius: '50%',
display: 'flex',
height: avatarSize * 1.15,
justifyContent: 'center',
position: 'relative',
width: avatarSize * 1.15,
...tierConfig.style,
}}
>
<Avatar name={item.name} size={avatarSize} src={item.image} themeMode={themeMode} />
</div>
<div
style={{
alignItems: 'center',
background: styles.backgroundColor,
border: `${Math.floor(avatarSize / 64)}px solid ${styles.borderColor}`,
borderRadius: avatarSize / 16,
color: styles.fontColor,
display: 'flex',
fontSize: avatarSize / 6,
justifyContent: 'center',
marginTop: -avatarSize / 6,
padding: avatarSize / 16,
zIndex: 2,
}}
>
{`${tierConfig.emoji} ${
item.name.length > 9 ? item.name.slice(0, 9) + '...' : item.name
}`}
</div>
</div>
);
})}
<div
style={{
alignItems: 'flex-start',
backgroundColor: styles.avatarBackgroundColor,
border: `${Math.floor(avatarSize / 64)}px solid ${styles.borderColor}`,
borderRadius: avatarSize / 6,
color: styles.fontColor,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
lineHeight: 1.3,
overflow: 'hidden',
padding: `${avatarSize / 8}px ${avatarSize / 6}px`,
}}
>
<div style={{ fontSize: avatarSize / 6 }}>{texts[0]}</div>
<div style={{ fontSize: avatarSize / 4, fontWeight: 'bold' }}>
{`${texts[1]} `}
<span style={{ color: '#DC485F' }}>S</span>
<span style={{ color: '#AB4ABB' }}>p</span>
<span style={{ color: '#3BBCFF' }}>o</span>
<span style={{ color: '#76DAC1' }}>n</span>
<span style={{ color: '#99D52C' }}>s</span>
<span style={{ color: '#F2C314' }}>o</span>
<span style={{ color: '#E65E41' }}>r</span>
</div>
</div>
</div>
);
};

export default Sponsor;

0 comments on commit 4f38bc4

Please sign in to comment.