Skip to content

Commit 3221277

Browse files
committed
all my homies are on npmx.social
1 parent 26d967e commit 3221277

2 files changed

Lines changed: 62 additions & 197 deletions

File tree

app/pages/about.vue

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,9 @@ const pmLinks = {
3030
3131
const { data: contributors, status: contributorsStatus } = useLazyFetch('/api/contributors')
3232
33-
const governanceMembers = computed(
34-
() => contributors.value?.filter(c => c.role !== 'contributor') ?? [],
35-
)
33+
const governanceMembers = computed(() => [])
3634
37-
const communityContributors = computed(
38-
() => contributors.value?.filter(c => c.role === 'contributor') ?? [],
39-
)
35+
const communityContributors = computed(() => contributors.value ?? [])
4036
4137
const roleLabels = computed(
4238
() =>
@@ -179,27 +175,27 @@ const roleLabels = computed(
179175
<ul class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 list-none p-0">
180176
<li
181177
v-for="person in governanceMembers"
182-
:key="person.id"
178+
:key="person.did"
183179
class="relative flex items-center gap-3 p-3 border border-border rounded-lg hover:border-border-hover hover:bg-bg-muted transition-[border-color,background-color] duration-200 cursor-pointer focus-within:ring-2 focus-within:ring-offset-bg focus-within:ring-offset-2 focus-within:ring-fg/50"
184180
>
185181
<img
186-
:src="`${person.avatar_url}&s=80`"
187-
:alt="`${person.login}'s avatar`"
182+
:src="`${person.avatar}&s=80`"
183+
:alt="`${person.did}'s avatar`"
188184
class="w-12 h-12 rounded-md ring-1 ring-border shrink-0"
189185
loading="lazy"
190186
/>
191187
<div class="min-w-0 flex-1">
192188
<div class="font-mono text-sm text-fg truncate">
193189
<NuxtLink
194-
:to="person.html_url"
190+
:to="person.displayName"
195191
target="_blank"
196192
class="decoration-none after:content-[''] after:absolute after:inset-0"
197193
:aria-label="$t('about.contributors.view_profile', { name: person.login })"
198194
>
199-
@{{ person.login }}
195+
@{{ person.loghandlein }}
200196
</NuxtLink>
201197
</div>
202-
<div class="text-xs text-fg-muted tracking-tight">
198+
<!-- <div class="text-xs text-fg-muted tracking-tight">
203199
{{ roleLabels[person.role] ?? person.role }}
204200
</div>
205201
<LinkBase
@@ -212,7 +208,7 @@ const roleLabels = computed(
212208
:aria-label="$t('about.team.sponsor_aria', { name: person.login })"
213209
>
214210
{{ $t('about.team.sponsor') }}
215-
</LinkBase>
211+
</LinkBase> -->
216212
</div>
217213
<span
218214
class="i-lucide:external-link rtl-flip w-3.5 h-3.5 text-fg-muted opacity-50 shrink-0 self-start mt-0.5 pointer-events-none"
@@ -228,13 +224,14 @@ const roleLabels = computed(
228224
id="contributors-heading"
229225
class="text-sm text-fg-subtle uppercase tracking-wider mb-4"
230226
>
231-
{{
227+
{{ `${communityContributors.length} people on npmx.social` }}
228+
<!-- {{
232229
$t(
233230
'about.contributors.title',
234231
{ count: $n(communityContributors.length) },
235232
communityContributors.length,
236233
)
237-
}}
234+
}} -->
238235
</h3>
239236

240237
<div
@@ -257,7 +254,7 @@ const roleLabels = computed(
257254
>
258255
<li
259256
v-for="contributor in communityContributors"
260-
:key="contributor.id"
257+
:key="contributor.did"
261258
class="group relative"
262259
>
263260
<LinkBase
@@ -267,8 +264,18 @@ const roleLabels = computed(
267264
:aria-label="$t('about.contributors.view_profile', { name: contributor.login })"
268265
>
269266
<img
270-
:src="`${contributor.avatar_url}&s=64`"
271-
:alt="`${contributor.login}'s avatar`"
267+
v-if="contributor.avatar"
268+
:src="`${contributor.avatar}`"
269+
:alt="`${contributor.handle}'s avatar`"
270+
width="48"
271+
height="48"
272+
class="w-12 h-12 rounded-lg ring-2 ring-transparent group-hover:ring-accent transition-all duration-200 ease-out hover:scale-125 will-change-transform"
273+
loading="lazy"
274+
/>
275+
<img
276+
v-else
277+
src="https://npmx.dev/pwa-64x64.png"
278+
alt="Default avatar"
272279
width="48"
273280
height="48"
274281
class="w-12 h-12 rounded-lg ring-2 ring-transparent group-hover:ring-accent transition-all duration-200 ease-out hover:scale-125 will-change-transform"
@@ -279,7 +286,7 @@ const roleLabels = computed(
279286
dir="ltr"
280287
role="tooltip"
281288
>
282-
@{{ contributor.login }}
289+
@{{ contributor.handle }}
283290
</span>
284291
</LinkBase>
285292
</li>

server/api/contributors.get.ts

Lines changed: 36 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -1,196 +1,54 @@
1-
export type Role = 'steward' | 'maintainer' | 'contributor'
2-
3-
export interface GitHubContributor {
4-
login: string
5-
id: number
6-
avatar_url: string
7-
html_url: string
8-
contributions: number
9-
role: Role
10-
sponsors_url: string | null
11-
}
12-
13-
type GitHubAPIContributor = Omit<GitHubContributor, 'role' | 'sponsors_url'>
14-
15-
// Fallback when no GitHub token is available (e.g. preview environments).
16-
// Only stewards are shown as maintainers; everyone else is a contributor.
17-
const FALLBACK_STEWARDS = new Set(['danielroe', 'patak-dev'])
18-
19-
interface TeamMembers {
20-
steward: Set<string>
21-
maintainer: Set<string>
1+
interface AtprotoProfile {
2+
did: string
3+
handle: string
4+
displayName?: string
5+
avatar?: string
226
}
237

24-
async function fetchTeamMembers(token: string): Promise<TeamMembers | null> {
25-
const teams: Record<keyof TeamMembers, string> = {
26-
steward: 'stewards',
27-
maintainer: 'maintainers',
28-
}
29-
30-
try {
31-
const result: TeamMembers = { steward: new Set(), maintainer: new Set() }
32-
33-
for (const [role, slug] of Object.entries(teams) as [keyof TeamMembers, string][]) {
34-
const response = await fetch(
35-
`https://api.github.com/orgs/npmx-dev/teams/${slug}/members?per_page=100`,
36-
{
37-
headers: {
38-
'Accept': 'application/vnd.github.v3+json',
39-
'Authorization': `Bearer ${token}`,
40-
'User-Agent': 'npmx',
41-
},
42-
},
43-
)
44-
45-
if (!response.ok) {
46-
console.warn(`Failed to fetch ${slug} team members: ${response.status}`)
47-
return null
48-
}
49-
50-
const members = (await response.json()) as { login: string }[]
51-
for (const member of members) {
52-
result[role].add(member.login)
53-
}
54-
}
55-
56-
return result
57-
} catch (error) {
58-
console.warn('Failed to fetch team members from GitHub:', error)
59-
return null
60-
}
61-
}
62-
63-
/**
64-
* Batch-query GitHub GraphQL API to check which users have sponsors enabled.
65-
* Returns a Set of logins that have a sponsors listing.
66-
*/
67-
async function fetchSponsorable(token: string, logins: string[]): Promise<Set<string>> {
68-
if (logins.length === 0) return new Set()
69-
70-
// Build aliased GraphQL query: user0: user(login: "x") { hasSponsorsListing login }
71-
const fragments = logins.map(
72-
(login, i) => `user${i}: user(login: "${login}") { hasSponsorsListing login }`,
73-
)
74-
const query = `{ ${fragments.join('\n')} }`
75-
76-
try {
77-
const response = await fetch('https://api.github.com/graphql', {
78-
method: 'POST',
79-
headers: {
80-
'Authorization': `Bearer ${token}`,
81-
'Content-Type': 'application/json',
82-
'User-Agent': 'npmx',
83-
},
84-
body: JSON.stringify({ query }),
85-
})
86-
8+
export default defineCachedEventHandler(
9+
async (): Promise<AtprotoProfile[]> => {
10+
const response = await fetch('https://npmx.social/xrpc/com.atproto.sync.listRepos?limit=1000')
8711
if (!response.ok) {
88-
console.warn(`Failed to fetch sponsors info: ${response.status}`)
89-
return new Set()
90-
}
91-
92-
const json = (await response.json()) as {
93-
data?: Record<string, { login: string; hasSponsorsListing: boolean } | null>
94-
}
95-
96-
const sponsorable = new Set<string>()
97-
if (json.data) {
98-
for (const user of Object.values(json.data)) {
99-
if (user?.hasSponsorsListing) {
100-
sponsorable.add(user.login)
101-
}
102-
}
12+
throw createError({
13+
statusCode: response.status,
14+
message: 'Failed to fetch PDS repos',
15+
})
10316
}
104-
return sponsorable
105-
} catch (error) {
106-
console.warn('Failed to fetch sponsors info:', error)
107-
return new Set()
108-
}
109-
}
110-
111-
function getRoleInfo(login: string, teams: TeamMembers): { role: Role; order: number } {
112-
if (teams.steward.has(login)) return { role: 'steward', order: 0 }
113-
if (teams.maintainer.has(login)) return { role: 'maintainer', order: 1 }
114-
return { role: 'contributor', order: 2 }
115-
}
116-
117-
export default defineCachedEventHandler(
118-
async (): Promise<GitHubContributor[]> => {
119-
const githubToken = useRuntimeConfig().github.orgToken
12017

121-
// Fetch team members dynamically if token is available, otherwise use fallback
122-
const teams: TeamMembers = await (async () => {
123-
if (githubToken) {
124-
const fetched = await fetchTeamMembers(githubToken)
125-
if (fetched) return fetched
126-
}
127-
return { steward: FALLBACK_STEWARDS, maintainer: new Set<string>() }
128-
})()
129-
130-
const allContributors: GitHubAPIContributor[] = []
131-
let page = 1
132-
const perPage = 100
18+
const listRepos = (await response.json()) as { repos: { did: string; active: boolean }[] }
19+
const activeOnlyDids = listRepos.repos.filter(repo => repo.active).map(repo => repo.did)
13320

134-
while (true) {
135-
const response = await fetch(
136-
`https://api.github.com/repos/npmx-dev/npmx.dev/contributors?per_page=${perPage}&page=${page}`,
137-
{
138-
headers: {
139-
'Accept': 'application/vnd.github.v3+json',
140-
'User-Agent': 'npmx',
141-
...(githubToken && { Authorization: `Bearer ${githubToken}` }),
142-
},
143-
},
144-
)
21+
const getProfilesUrl = 'https://public.api.bsky.app/xrpc/app.bsky.actor.getProfiles'
22+
const allProfiles: AtprotoProfile[] = []
14523

146-
if (!response.ok) {
147-
throw createError({
148-
statusCode: response.status,
149-
message: 'Failed to fetch contributors',
150-
})
24+
// Batch DIDs into groups of 25 (API limit)
25+
for (let i = 0; i < activeOnlyDids.length; i += 25) {
26+
const batch = activeOnlyDids.slice(i, i + 25)
27+
const params = new URLSearchParams()
28+
for (const did of batch) {
29+
params.append('actors', did)
15130
}
15231

153-
const contributors = (await response.json()) as GitHubAPIContributor[]
154-
155-
if (contributors.length === 0) {
156-
break
157-
}
32+
try {
33+
const profilesResponse = await fetch(`${getProfilesUrl}?${params.toString()}`)
34+
if (!profilesResponse.ok) {
35+
console.warn(`Failed to fetch atproto profiles: ${profilesResponse.status}`)
36+
continue
37+
}
15838

159-
allContributors.push(...contributors)
39+
const { profiles } = (await profilesResponse.json()) as { profiles: AtprotoProfile[] }
16040

161-
if (contributors.length < perPage) {
162-
break
41+
allProfiles.push(...profiles)
42+
} catch (error) {
43+
console.warn('Failed to fetch atproto profiles:', error)
16344
}
164-
165-
page++
16645
}
16746

168-
const filtered = allContributors.filter(c => !c.login.includes('[bot]'))
169-
170-
// Identify maintainers (stewards + maintainers) and check their sponsors status
171-
const maintainerLogins = filtered
172-
.filter(c => teams.steward.has(c.login) || teams.maintainer.has(c.login))
173-
.map(c => c.login)
174-
175-
const sponsorable = githubToken
176-
? await fetchSponsorable(githubToken, maintainerLogins)
177-
: new Set<string>()
178-
179-
return filtered
180-
.map(c => {
181-
const { role, order } = getRoleInfo(c.login, teams)
182-
const sponsors_url = sponsorable.has(c.login)
183-
? `https://github.com/sponsors/${c.login}`
184-
: null
185-
Object.assign(c, { role, order, sponsors_url })
186-
return c as GitHubContributor & { order: number; sponsors_url: string | null; role: Role }
187-
})
188-
.sort((a, b) => a.order - b.order || b.contributions - a.contributions)
189-
.map(({ order: _, ...rest }) => rest)
47+
return allProfiles
19048
},
19149
{
192-
maxAge: 3600, // Cache for 1 hour
193-
name: 'github-contributors',
194-
getKey: () => 'contributors',
50+
maxAge: 1,
51+
name: 'pds-contributors',
52+
getKey: () => 'pds-contributors',
19553
},
19654
)

0 commit comments

Comments
 (0)