Files
oh-my-opencode/packages/web/lib/stats.ts
T
YeonGyu-Kim 7e0406f4ec fix(web): UX/a11y polish + middleware metadata route fix
- footer.tsx: Discord link → canonical discord.gg/PUwSMR9XNk
  (was outdated invite that 404'd).
- [locale]/layout.tsx: hooks count 40 → 54 (matches AGENTS.md).
- lib/stats.ts:88: drop '!' non-null assertion; use '??' fallback.
  Removes the only '!' in packages/web (project rule).
- nav-header.tsx: hamburger Button size='icon' → explicit h-11 w-11;
  mobile drawer links min-h-11 px-3 rounded-md. WCAG 2.5.5 tap
  target 44x44 minimum.
- install-command.tsx: copy button h-8 w-8 → h-11 w-11 (was 32px,
  below WCAG). Icon stays 16x16; padding fills to 44.
- middleware.ts: matcher excludes opengraph-image, twitter-image,
  icon, apple-icon, manifest.webmanifest, robots.txt, sitemap.xml.
  Previously next-intl redirected /opengraph-image → /en/opengraph-image
  breaking OG previews for crawlers that hit the unlocalized path.
2026-05-20 14:26:09 +09:00

148 lines
3.7 KiB
TypeScript

const GITHUB_OWNER = "code-yeongyu"
const GITHUB_REPO = "oh-my-openagent"
const NPM_PACKAGES = ["oh-my-opencode", "oh-my-openagent"]
const NPM_FIRST_PUBLISH_YEAR = 2025
const CACHE_TTL_MS = 60 * 60 * 1000
interface StatsCache {
data: StatsData
timestamp: number
}
export interface StatsData {
stars: number
totalDownloads: number
monthlyDownloads: number
weeklyDownloads: number
}
export interface FormattedStatsData {
readonly stars: string
readonly totalDownloads: string
readonly monthlyDownloads: string
readonly weeklyDownloads: string
}
let cache: StatsCache | null = null
function formatCount(num: number): string {
if (num >= 1_000_000) {
const formatted = (num / 1_000_000).toFixed(1)
return `${formatted.replace(/\.0$/, "")}M+`
}
if (num >= 1_000) {
const formatted = (num / 1_000).toFixed(1)
return `${formatted.replace(/\.0$/, "")}k`
}
return String(num)
}
async function fetchGitHubStars(): Promise<number> {
const headers: Record<string, string> = {
Accept: "application/vnd.github.v3+json",
"User-Agent": "oh-my-openagent-web",
}
const token = process.env.GITHUB_TOKEN
if (token) {
headers.Authorization = `Bearer ${token}`
}
const res = await fetch(`https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}`, {
headers,
next: { revalidate: 3600 },
})
if (!res.ok) {
throw new Error(`GitHub API error: ${res.status}`)
}
const data = await res.json()
return data.stargazers_count
}
async function fetchNpmDownloadsForPackage(period: string, pkg: string): Promise<number> {
try {
const res = await fetch(`https://api.npmjs.org/downloads/point/${period}/${pkg}`, {
next: { revalidate: 3600 },
})
if (!res.ok) return 0
const data = await res.json()
return data.downloads ?? 0
} catch {
return 0
}
}
async function fetchNpmDownloads(period: string): Promise<number> {
const results = await Promise.all(
NPM_PACKAGES.map((pkg) => fetchNpmDownloadsForPackage(period, pkg)),
)
return results.reduce((sum, n) => sum + n, 0)
}
async function fetchAllNpmDownloadsForPackage(pkg: string): Promise<number> {
const now = new Date()
let total = 0
let year = NPM_FIRST_PUBLISH_YEAR
while (year <= now.getFullYear()) {
const start = `${year}-01-01`
const endDate = new Date(year, 11, 31)
const end =
endDate > now ? (now.toISOString().split("T")[0] ?? `${year}-12-31`) : `${year}-12-31`
try {
const res = await fetch(`https://api.npmjs.org/downloads/point/${start}:${end}/${pkg}`, {
next: { revalidate: 3600 },
})
if (res.ok) {
const data = await res.json()
total += data.downloads ?? 0
}
} catch {
continue
}
year++
}
return total
}
async function fetchAllNpmDownloads(): Promise<number> {
const results = await Promise.all(NPM_PACKAGES.map((pkg) => fetchAllNpmDownloadsForPackage(pkg)))
return results.reduce((sum, n) => sum + n, 0)
}
export async function getStats(): Promise<StatsData> {
const now = Date.now()
if (cache && now - cache.timestamp < CACHE_TTL_MS) {
return cache.data
}
const [stars, monthlyDownloads, weeklyDownloads, totalDownloads] = await Promise.all([
fetchGitHubStars(),
fetchNpmDownloads("last-month"),
fetchNpmDownloads("last-week"),
fetchAllNpmDownloads(),
])
const data: StatsData = { stars, totalDownloads, monthlyDownloads, weeklyDownloads }
cache = { data, timestamp: now }
return data
}
export function formatStats(stats: StatsData): FormattedStatsData {
return {
stars: formatCount(stats.stars),
totalDownloads: formatCount(stats.totalDownloads),
monthlyDownloads: formatCount(stats.monthlyDownloads),
weeklyDownloads: formatCount(stats.weeklyDownloads),
}
}