Last active
September 19, 2024 22:21
-
-
Save souporserious/84d6e757f2c8ac1c041dbec83aa99f60 to your computer and use it in GitHub Desktop.
Display a list of GitHub sponsors by tier in React.
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
import React from 'react' | |
interface SponsorEntity { | |
username: string | |
avatarUrl: string | |
} | |
interface Sponsor { | |
sponsorEntity: SponsorEntity | |
tier: { | |
monthlyPriceInCents: number | |
name: string | |
} | |
} | |
interface Tier { | |
title: string | |
[key: string]: any | |
} | |
/** Fetches GitHub sponsors for the authenticated user. */ | |
export async function fetchSponsors(amount: number) { | |
const token = process.env.GITHUB_TOKEN | |
if (!token) { | |
throw new Error( | |
'[renoun] GITHUB_TOKEN must be set when using the <GitHubSponsors /> component.' | |
) | |
} | |
const query = ` | |
query { | |
viewer { | |
sponsorshipsAsMaintainer(first: ${amount}) { | |
nodes { | |
sponsorEntity { | |
... on User { | |
username: login | |
avatarUrl | |
} | |
} | |
tier { | |
monthlyPriceInCents | |
name | |
} | |
} | |
} | |
} | |
} | |
` | |
const response = await fetch('https://api.github.com/graphql', { | |
method: 'POST', | |
headers: { | |
Authorization: `Bearer ${token}`, | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ query }), | |
}) | |
const json = await response.json() | |
return json.data.viewer.sponsorshipsAsMaintainer.nodes | |
} | |
interface SponsorTier extends Tier { | |
sponsors: SponsorEntity[] | |
} | |
/** Fetches sponsors and groups them by tier. */ | |
export async function fetchSponsorTiers( | |
tiers: Record<number, Tier>, | |
amount: number = 100 | |
): Promise<SponsorTier[]> { | |
const sponsors = await fetchSponsors(amount) | |
// Convert the tiers record to an array of [price, tier] and sort by minAmount ascending | |
const tierArray = Object.entries(tiers) | |
.map(([price, tier]) => ({ | |
...tier, | |
minAmount: parseFloat(price), // Treat the key as the minimum amount for this tier | |
})) | |
.sort((a, b) => a.minAmount - b.minAmount) // Sort by minAmount ascending | |
// Group sponsors by their custom tiers based on monthlyPriceInCents | |
const sponsorsByTier = sponsors.reduce( | |
(allTiers: Record<string, SponsorEntity[]>, sponsor: Sponsor) => { | |
const amount = sponsor.tier.monthlyPriceInCents / 100 // Convert cents to dollars | |
// Find the correct tier by checking the range, starting from the lowest minAmount | |
const matchedTier = tierArray.find((tier, index) => { | |
const nextTier = tierArray[index + 1] | |
// Match current tier if amount is >= current minAmount and either: | |
// - there's no next tier, or | |
// - the amount is less than the next tier's minAmount | |
return ( | |
amount >= tier.minAmount && (!nextTier || amount < nextTier.minAmount) | |
) | |
}) | |
if (matchedTier) { | |
const tierTitle = matchedTier.title | |
if (!allTiers[tierTitle]) { | |
allTiers[tierTitle] = [] | |
} | |
allTiers[tierTitle].push(sponsor.sponsorEntity) | |
} | |
return allTiers | |
}, | |
{} | |
) as Record<string, SponsorEntity[]> | |
return Object.keys(tiers) | |
.reverse() | |
.map((amount) => { | |
const tier = tiers[amount as any] as Tier | |
return { | |
...tier, | |
sponsors: sponsorsByTier[tier.title] || [], | |
} | |
}) | |
} | |
interface GitHubSponsorsProps { | |
/** A record of tiers with the minimum amount as the key and an object with at least the title as the value. */ | |
tiers: Record<number, Tier> | |
/** A function that receives the grouped sponsors by tier. */ | |
children: ( | |
teirs: Awaited<ReturnType<typeof fetchSponsorTiers>> | |
) => React.ReactNode | |
} | |
/** Renders a list of GitHub sponsors grouped by tier. */ | |
export async function GitHubSponsors({ tiers, children }: GitHubSponsorsProps) { | |
const resolvedTiers = await fetchSponsorTiers(tiers) | |
return children(resolvedTiers) | |
} |
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
import { GitHubSponsors } from './GitHubSponsors' | |
export function GitHubSponsorTiers() { | |
return ( | |
<GitHubSponsors | |
tiers={{ | |
100: { | |
title: 'Bronze', | |
icon: 'π₯', | |
}, | |
250: { | |
title: 'Silver', | |
icon: 'π₯', | |
}, | |
500: { | |
title: 'Gold', | |
icon: 'π₯', | |
}, | |
1000: { | |
title: 'Diamond', | |
icon: 'π', | |
}, | |
}} | |
> | |
{(tiers) => { | |
return ( | |
<div | |
css={{ | |
display: 'flex', | |
flexDirection: 'column', | |
gap: '3rem', | |
}} | |
> | |
{tiers.map((tier) => { | |
if (tier.sponsors.length === 0) { | |
return ( | |
<section | |
key={tier.title} | |
css={{ | |
display: 'flex', | |
flexDirection: 'column', | |
gap: '1rem', | |
}} | |
> | |
<h3> | |
{tier.icon} {tier.title} | |
</h3> | |
<div | |
css={{ | |
display: 'flex', | |
flexDirection: 'column', | |
alignItems: 'center', | |
justifyContent: 'center', | |
gap: '1rem', | |
minHeight: '16rem', | |
backgroundColor: 'var(--color-surface-secondary)', | |
}} | |
> | |
<p> | |
Become the first <strong>{tier.title}</strong> sponsor | |
</p> | |
<SponsorLink tier={tier.title} /> | |
</div> | |
</section> | |
) | |
} | |
return ( | |
<section | |
key={tier.title} | |
css={{ | |
display: 'flex', | |
flexDirection: 'column', | |
gap: '1rem', | |
}} | |
> | |
<div | |
css={{ | |
display: 'flex', | |
gap: '1rem', | |
justifyContent: 'space-between', | |
}} | |
> | |
<h3> | |
{tier.icon} {tier.title} | |
</h3> | |
<SponsorLink tier={tier.title} variant="small" /> | |
</div> | |
<ul | |
css={{ | |
listStyle: 'none', | |
display: 'flex', | |
flexWrap: 'wrap', | |
minHeight: '16rem', | |
padding: '1rem', | |
margin: 0, | |
gap: '1rem', | |
backgroundColor: 'var(--color-surface-secondary)', | |
}} | |
> | |
{tier.sponsors.map((sponsor, index) => ( | |
<li key={index}> | |
<a href={`https://github.com/${sponsor.username}`}> | |
<img | |
src={sponsor.avatarUrl} | |
alt={`${sponsor.username}'s avatar`} | |
title={sponsor.username} | |
css={{ width: '4rem', borderRadius: '100%' }} | |
/> | |
</a> | |
</li> | |
))} | |
</ul> | |
</section> | |
) | |
})} | |
</div> | |
) | |
}} | |
</GitHubSponsors> | |
) | |
} | |
function SponsorLink({ | |
tier, | |
variant, | |
}: { | |
tier: string | |
variant: 'small' | 'medium' | |
}) { | |
return ( | |
<a | |
href="https://github.com/sponsors/souporserious" | |
css={{ | |
fontSize: | |
variant === 'small' | |
? 'var(--font-size-body-3)' | |
: 'var(--font-size-body-2)', | |
fontWeight: 'var(--font-weight-button)', | |
display: 'inline-flex', | |
padding: variant === 'small' ? '0.25rem 0.75rem' : '0.5rem 1rem', | |
borderRadius: '0.25rem', | |
backgroundColor: '#db61a2', | |
color: 'white', | |
}} | |
> | |
Sponsor {tier} | |
</a> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment