From 4f38bc40adbadf033591373d71b34959bf52dcaa Mon Sep 17 00:00:00 2001 From: canisminor1990 Date: Wed, 6 Dec 2023 15:25:53 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20SponsorKit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/sponsor.tsx | 59 ++++++++++ package.json | 3 +- src/Sponsor/Avatar.tsx | 53 +++++++++ src/Sponsor/demos/index.tsx | 37 ++++++ src/Sponsor/index.md | 8 ++ src/Sponsor/index.tsx | 221 ++++++++++++++++++++++++++++++++++++ src/Sponsor/style.ts | 24 ++++ src/index.ts | 1 + 8 files changed, 405 insertions(+), 1 deletion(-) create mode 100644 api/sponsor.tsx create mode 100644 src/Sponsor/Avatar.tsx create mode 100644 src/Sponsor/demos/index.tsx create mode 100644 src/Sponsor/index.md create mode 100644 src/Sponsor/index.tsx create mode 100644 src/Sponsor/style.ts diff --git a/api/sponsor.tsx b/api/sponsor.tsx new file mode 100644 index 0000000..9579bef --- /dev/null +++ b/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 { + 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( + ( + + ), + { + 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, + }); + } +} diff --git a/package.json b/package.json index 1832c49..16fa2bc 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "dependencies": { "@babel/runtime": "^7", "@lobehub/ui": "latest", + "@vercel/og": "^0.5.20", "ahooks": "^3", "antd": "^5", "antd-style": "^3", @@ -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", diff --git a/src/Sponsor/Avatar.tsx b/src/Sponsor/Avatar.tsx new file mode 100644 index 0000000..7f1c75e --- /dev/null +++ b/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 = ({ + src, + name, + size = 64, + style, + + themeMode, +}) => { + const styles = theme(themeMode); + return ( +
+ {src ? ( + {name} + ) : ( +
+ {name.slice(0, 2).toUpperCase()} +
+ )} +
+ ); +}; diff --git a/src/Sponsor/demos/index.tsx b/src/Sponsor/demos/index.tsx new file mode 100644 index 0000000..aeb0b3d --- /dev/null +++ b/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 ( + + {isLoading || !data ? ( +
loading...
+ ) : ( + + )} +
+ ); +}; diff --git a/src/Sponsor/index.md b/src/Sponsor/index.md new file mode 100644 index 0000000..51a8f72 --- /dev/null +++ b/src/Sponsor/index.md @@ -0,0 +1,8 @@ +--- +nav: components +group: sponsor +title: SponsorKit +order: 1 +--- + + diff --git a/src/Sponsor/index.tsx b/src/Sponsor/index.tsx new file mode 100644 index 0000000..cf7bf9b --- /dev/null +++ b/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 = ({ + 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 ( +
+ {sortedData().map((item) => { + const tierConfig = getTier(item.tier); + return ( +
+
+ +
+
+ {`${tierConfig.emoji} ${ + item.name.length > 9 ? item.name.slice(0, 9) + '...' : item.name + }`} +
+
+ ); + })} +
+
{texts[0]}
+
+ {`${texts[1]} `} + S + p + o + n + s + o + r +
+
+
+ ); +}; + +export default Sponsor; diff --git a/src/Sponsor/style.ts b/src/Sponsor/style.ts new file mode 100644 index 0000000..8af62c2 --- /dev/null +++ b/src/Sponsor/style.ts @@ -0,0 +1,24 @@ +interface Style { + avatarBackgroundColor: string; + avatarFontColor: string; + backgroundColor: string; + borderColor: string; + fontColor: string; +} + +export const theme = (themeMode?: 'light' | 'dark'): Style => + themeMode === 'dark' + ? { + avatarBackgroundColor: '#111', + avatarFontColor: '#eee', + backgroundColor: '#000', + borderColor: '#333', + fontColor: '#eee', + } + : { + avatarBackgroundColor: '#f5f5f5', + avatarFontColor: '#666', + backgroundColor: '#fff', + borderColor: '#e4e9ec', + fontColor: '#333', + }; diff --git a/src/index.ts b/src/index.ts index d58694b..a5c1493 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ export { default as ReadmeHero } from './ReadmeHero'; +export { default as Sponsor } from './Sponsor';