Skip to content

Commit d5fd862

Browse files
committed
fix: normalize submission tags and stats
1 parent 609a7e5 commit d5fd862

10 files changed

Lines changed: 118 additions & 91 deletions

.github/workflows/create-theme-submission.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ jobs:
6666
ISSUE_TITLE: ${{ steps.issue.outputs.title }}
6767
ISSUE_AUTHOR: ${{ steps.issue.outputs.author }}
6868
ISSUE_BODY_FILE: issue-body.md
69+
GITHUB_TOKEN: ${{ github.token }}
6970
run: node scripts/create-theme-submission-from-issue.mjs
7071

7172
- run: npm test

.github/workflows/publish-approved-theme-submission.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ jobs:
3939
- name: Publish candidate theme
4040
env:
4141
PR_NUMBER: ${{ github.event.pull_request.number }}
42+
GITHUB_TOKEN: ${{ github.token }}
4243
run: node scripts/publish-approved-theme-submission.mjs
4344

4445
- run: npm test

MEMORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ This file stores durable project context so future conversations can resume work
6767
- Each published theme gets a `/themes/[slug]/` detail page and the site also generates `/themes.json`
6868
- Archived-but-existing repositories are preserved under `/archive/` with unsupported messaging; unavailable/deleted repositories are proposed for removal by PR
6969
- New theme submissions come through the `Submit a theme` GitHub Issue Form; automation turns them into `candidate` PRs, and they become public only after human review
70+
- Theme submission and approval automation populates basic repository stats for submitted repositories, reusing the same provider logic as `npm run refresh:themes`
7071

7172
## Known Technical Risks
7273

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Community submissions use the `Submit a theme` GitHub Issue Form. When a complet
3939

4040
- create a candidate JSON entry in `src/content/themes/`;
4141
- download attached or linked screenshots into `public/assets/img/themes/`;
42+
- populate basic repository metadata such as stars, last update date, and owner avatar;
4243
- validate the catalog with `npm test` and `npm run build`;
4344
- open or update a review pull request from a `submissions/theme-<issue-number>` branch.
4445

scripts/create-theme-submission-from-issue.mjs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'node:fs'
22
import path from 'node:path'
33
import { Buffer } from 'node:buffer'
44
import { fileURLToPath } from 'node:url'
5+
import { fetchRepositoryStats } from './repository-stats.mjs'
56

67
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
78
const themesDir = path.join(root, 'src/content/themes')
@@ -32,6 +33,7 @@ const submitterRole = normalizeSubmitterRole(requiredField(fields, 'Your relatio
3233
const notes = optionalField(fields, 'Notes for reviewers')
3334
const slug = uniqueSlug(slugify(title))
3435
const submittedBy = issueAuthor.length >= 2 ? issueAuthor : null
36+
const repositoryStats = await fetchRepositoryStats(repository)
3537

3638
if (title.length < 2 || title.length > 80) {
3739
throw new Error('Theme title must be between 2 and 80 characters.')
@@ -68,12 +70,7 @@ const theme = {
6870
status: 'candidate',
6971
catalogIndex: candidateCatalogIndex(),
7072
...(submittedBy ? { submittedBy } : {}),
71-
stats: {
72-
stars: 0,
73-
updatedAt: null,
74-
ownerAvatar: null,
75-
accessible: true
76-
}
73+
stats: repositoryStats
7774
}
7875

7976
const themePath = path.join(themesDir, `${slug}.json`)

scripts/publish-approved-theme-submission.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'node:fs'
22
import path from 'node:path'
33
import { execFileSync } from 'node:child_process'
44
import { fileURLToPath } from 'node:url'
5+
import { fetchRepositoryStats } from './repository-stats.mjs'
56

67
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
78
const themesDir = path.join(root, 'src/content/themes')
@@ -54,6 +55,7 @@ const candidatePath = path.join(root, candidateFiles[0])
5455
const candidate = readTheme(candidatePath)
5556
candidate.status = 'published'
5657
candidate.catalogIndex = nextCatalogIndex(candidatePath)
58+
candidate.stats = await fetchRepositoryStats(candidate.repository, candidate.stats)
5759

5860
fs.writeFileSync(candidatePath, `${JSON.stringify(candidate, null, 2)}\n`)
5961
console.log(`Published ${candidate.slug} with catalogIndex ${candidate.catalogIndex}.`)

scripts/refresh-theme-stats.mjs

Lines changed: 2 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,15 @@
11
import fs from 'node:fs/promises'
22
import path from 'node:path'
33
import { fileURLToPath } from 'node:url'
4+
import { fetchRepositoryStats } from './repository-stats.mjs'
45

56
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
67
const themesDir = path.join(root, 'src/content/themes')
7-
const githubToken = process.env.GITHUB_TOKEN
8-
const gitlabToken = process.env.GITLAB_TOKEN
9-
const codebergToken = process.env.CODEBERG_TOKEN
10-
11-
async function requestJson(url, headers = {}) {
12-
const response = await fetch(url, { headers })
13-
14-
if (!response.ok) {
15-
throw new Error(`${response.status} ${response.statusText}`)
16-
}
17-
18-
return response.json()
19-
}
20-
21-
function parseRepo(repository) {
22-
const url = new URL(repository)
23-
const [owner, name] = url.pathname.replace(/^\/|\/$/g, '').split('/')
24-
25-
if (!owner || !name) {
26-
throw new Error(`Unsupported repository path: ${repository}`)
27-
}
28-
29-
return { host: url.hostname.replace(/^www\./, ''), owner, name }
30-
}
31-
32-
async function readGithub({ owner, name }) {
33-
const headers = githubToken ? { Authorization: `Bearer ${githubToken}` } : {}
34-
const data = await requestJson(`https://api.github.com/repos/${owner}/${name}`, headers)
35-
36-
return {
37-
stars: data.stargazers_count ?? 0,
38-
updatedAt: data.pushed_at ?? data.updated_at ?? null,
39-
ownerAvatar: data.owner?.avatar_url ?? null,
40-
accessible: true
41-
}
42-
}
43-
44-
async function readGitlab({ owner, name }) {
45-
const headers = gitlabToken ? { Authorization: `Bearer ${gitlabToken}` } : {}
46-
const encoded = encodeURIComponent(`${owner}/${name}`)
47-
const data = await requestJson(`https://gitlab.com/api/v4/projects/${encoded}`, headers)
48-
49-
const avatar = data.namespace?.avatar_url ?? null
50-
51-
return {
52-
stars: data.star_count ?? 0,
53-
updatedAt: data.last_activity_at ?? null,
54-
ownerAvatar: avatar?.startsWith('/') ? `https://gitlab.com${avatar}` : avatar,
55-
accessible: true
56-
}
57-
}
58-
59-
async function readCodeberg({ owner, name }) {
60-
const headers = codebergToken ? { Authorization: `token ${codebergToken}` } : {}
61-
const data = await requestJson(`https://codeberg.org/api/v1/repos/${owner}/${name}`, headers)
62-
63-
return {
64-
stars: data.stars_count ?? 0,
65-
updatedAt: data.updated_at ?? null,
66-
ownerAvatar: data.owner?.avatar_url ?? null,
67-
accessible: true
68-
}
69-
}
708

719
async function refreshTheme(file) {
7210
const filePath = path.join(themesDir, file)
7311
const theme = JSON.parse(await fs.readFile(filePath, 'utf8'))
74-
const repo = parseRepo(theme.repository)
75-
76-
let stats
77-
if (repo.host === 'github.com') {
78-
stats = await readGithub(repo)
79-
} else if (repo.host === 'gitlab.com') {
80-
stats = await readGitlab(repo)
81-
} else if (repo.host === 'codeberg.org') {
82-
stats = await readCodeberg(repo)
83-
} else {
84-
stats = { ...theme.stats, accessible: true }
85-
}
86-
87-
theme.stats = { ...theme.stats, ...stats }
12+
theme.stats = { ...theme.stats, ...await fetchRepositoryStats(theme.repository, theme.stats) }
8813
await fs.writeFile(filePath, `${JSON.stringify(theme, null, 2)}\n`)
8914
console.log(`Refreshed ${theme.slug}`)
9015
}

scripts/repository-stats.mjs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
const githubToken = process.env.GITHUB_TOKEN
2+
const gitlabToken = process.env.GITLAB_TOKEN
3+
const codebergToken = process.env.CODEBERG_TOKEN
4+
5+
export async function fetchRepositoryStats(repository, currentStats = {}) {
6+
const repo = parseRepo(repository)
7+
8+
try {
9+
if (repo.host === 'github.com') {
10+
return await readGithub(repo)
11+
}
12+
13+
if (repo.host === 'gitlab.com') {
14+
return await readGitlab(repo)
15+
}
16+
17+
if (repo.host === 'codeberg.org') {
18+
return await readCodeberg(repo)
19+
}
20+
21+
return { ...currentStats, accessible: true }
22+
} catch {
23+
return {
24+
stars: currentStats.stars ?? 0,
25+
updatedAt: currentStats.updatedAt ?? null,
26+
ownerAvatar: currentStats.ownerAvatar ?? null,
27+
accessible: false
28+
}
29+
}
30+
}
31+
32+
async function requestJson(url, headers = {}) {
33+
const response = await fetch(url, { headers })
34+
35+
if (!response.ok) {
36+
throw new Error(`${response.status} ${response.statusText}`)
37+
}
38+
39+
return response.json()
40+
}
41+
42+
function parseRepo(repository) {
43+
const url = new URL(repository)
44+
const [owner, name] = url.pathname.replace(/^\/|\/$/g, '').split('/')
45+
46+
if (!owner || !name) {
47+
throw new Error(`Unsupported repository path: ${repository}`)
48+
}
49+
50+
return { host: url.hostname.replace(/^www\./, ''), owner, name }
51+
}
52+
53+
async function readGithub({ owner, name }) {
54+
const headers = githubToken ? { Authorization: `Bearer ${githubToken}` } : {}
55+
const data = await requestJson(`https://api.github.com/repos/${owner}/${name}`, headers)
56+
57+
return {
58+
stars: data.stargazers_count ?? 0,
59+
updatedAt: data.pushed_at ?? data.updated_at ?? null,
60+
ownerAvatar: data.owner?.avatar_url ?? null,
61+
accessible: true
62+
}
63+
}
64+
65+
async function readGitlab({ owner, name }) {
66+
const headers = gitlabToken ? { Authorization: `Bearer ${gitlabToken}` } : {}
67+
const encoded = encodeURIComponent(`${owner}/${name}`)
68+
const data = await requestJson(`https://gitlab.com/api/v4/projects/${encoded}`, headers)
69+
const avatar = data.namespace?.avatar_url ?? null
70+
71+
return {
72+
stars: data.star_count ?? 0,
73+
updatedAt: data.last_activity_at ?? null,
74+
ownerAvatar: avatar?.startsWith('/') ? `https://gitlab.com${avatar}` : avatar,
75+
accessible: true
76+
}
77+
}
78+
79+
async function readCodeberg({ owner, name }) {
80+
const headers = codebergToken ? { Authorization: `token ${codebergToken}` } : {}
81+
const data = await requestJson(`https://codeberg.org/api/v1/repos/${owner}/${name}`, headers)
82+
83+
return {
84+
stars: data.stars_count ?? 0,
85+
updatedAt: data.updated_at ?? null,
86+
ownerAvatar: data.owner?.avatar_url ?? null,
87+
accessible: true
88+
}
89+
}

src/content/themes/seal-mint.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
"submittedBy": "Neikon",
2222
"stats": {
2323
"stars": 0,
24-
"updatedAt": null,
25-
"ownerAvatar": null,
24+
"updatedAt": "2026-04-09T11:08:17Z",
25+
"ownerAvatar": "https://avatars.githubusercontent.com/u/273342409?v=4",
2626
"accessible": true
2727
}
2828
}

src/styles/global.css

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,7 @@ select {
243243
overflow-x: auto;
244244
}
245245

246-
.tag-filter,
247-
.theme-tags span {
246+
.tag-filter {
248247
border: 1px solid var(--line);
249248
border-radius: 999px;
250249
background: var(--surface);
@@ -346,13 +345,24 @@ select {
346345
}
347346

348347
.theme-tags span {
349-
padding: 5px 9px;
350-
font-size: 0.78rem;
351-
font-weight: 800;
348+
display: inline-flex;
349+
align-items: center;
350+
min-height: 24px;
351+
padding: 2px 8px;
352+
border: 1px solid var(--line);
353+
border-radius: 999px;
354+
background: color-mix(in srgb, var(--surface) 76%, var(--bg));
355+
color: var(--muted);
356+
font-size: 0.74rem;
357+
font-weight: 650;
358+
line-height: 1.2;
359+
white-space: nowrap;
352360
}
353361

354362
.theme-tags.large span {
355-
font-size: 0.9rem;
363+
min-height: 30px;
364+
padding-inline: 10px;
365+
font-size: 0.84rem;
356366
}
357367

358368
.theme-meta,

0 commit comments

Comments
 (0)