Skip to content

Instantly share code, notes, and snippets.

@souporserious
Last active September 19, 2024 22:21
Show Gist options
  • Save souporserious/84d6e757f2c8ac1c041dbec83aa99f60 to your computer and use it in GitHub Desktop.
Save souporserious/84d6e757f2c8ac1c041dbec83aa99f60 to your computer and use it in GitHub Desktop.
Display a list of GitHub sponsors by tier in React.
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)
}
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