Skip to content

Commit 7821938

Browse files
committed
chore: progress
1 parent 5f17b6d commit 7821938

File tree

4 files changed

+77
-99
lines changed

4 files changed

+77
-99
lines changed

app/components/OgImage/Package.takumi.vue

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ const { repoRef, stars, refresh: refreshRepoMeta } = useRepoMeta(repositoryUrl)
5858
5959
const formattedStars = computed(() => (stars.value > 0 ? compactFormat.format(stars.value) : ''))
6060
61+
const weeklyDownloads = shallowRef(0)
62+
const formattedDownloads = computed(() =>
63+
weeklyDownloads.value ? compactFormat.format(weeklyDownloads.value) : '',
64+
)
65+
6166
const totalLikes = shallowRef(0)
6267
const formattedLikes = computed(() =>
6368
totalLikes.value ? compactFormat.format(totalLikes.value) : '',
@@ -184,8 +189,23 @@ const fetchLikes = $fetch<{ totalLikes: number }>(`/api/social/likes/${name}`)
184189
})
185190
.catch(() => {})
186191
192+
const downloadUrl = `https://api.npmjs.org/downloads/point/last-week/${encodePackageName(name)}`
193+
const fetchDownloads = $fetch<{ downloads: number }>(downloadUrl)
194+
.then(d => {
195+
console.error('[og-image-package] downloads response:', JSON.stringify(d))
196+
weeklyDownloads.value = d?.downloads ?? 0
197+
})
198+
.catch(err => {
199+
console.error('[og-image-package] downloads fetch failed:', downloadUrl, err?.message || err)
200+
})
201+
187202
try {
188-
await Promise.all([refreshPkg().then(() => refreshRepoMeta()), fetchVariantData(), fetchLikes])
203+
await Promise.all([
204+
refreshPkg().then(() => refreshRepoMeta()),
205+
fetchVariantData(),
206+
fetchLikes,
207+
fetchDownloads,
208+
])
189209
} catch (err) {
190210
console.warn('[og-image-package] Failed to load data server-side:', err)
191211
throw createError({
@@ -273,6 +293,14 @@ const sparklineSrc = computed(() => {
273293
<span v-else>{{ $t('package.links.repo') }}</span>
274294
</div>
275295

296+
<span v-if="formattedDownloads" class="flex items-center gap-2" data-testid="downloads">
297+
<div
298+
class="i-lucide:download shrink-0 text-fg-muted"
299+
:style="{ width: '32px', height: '32px' }"
300+
/>
301+
<span>{{ formattedDownloads }}/wk</span>
302+
</span>
303+
276304
<span v-if="formattedStars" class="flex items-center gap-2" data-testid="stars">
277305
<div
278306
class="i-lucide:star shrink-0 text-fg-muted"

app/composables/useCharts.ts

Lines changed: 0 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -66,65 +66,6 @@ function mergeDailyPoints(points: DailyRawPoint[]): DailyRawPoint[] {
6666
.map(([day, value]) => ({ day, value }))
6767
}
6868

69-
export function buildDailyEvolutionFromDaily(daily: DailyRawPoint[]): DailyDataPoint[] {
70-
return daily
71-
.slice()
72-
.sort((a, b) => a.day.localeCompare(b.day))
73-
.map(item => {
74-
const dayDate = parseIsoDate(item.day)
75-
const timestamp = dayDate.getTime()
76-
77-
return { day: item.day, value: item.value, timestamp }
78-
})
79-
}
80-
81-
export function buildRollingWeeklyEvolutionFromDaily(
82-
daily: DailyRawPoint[],
83-
rangeStartIso: string,
84-
rangeEndIso: string,
85-
): WeeklyDataPoint[] {
86-
const sorted = daily.slice().sort((a, b) => a.day.localeCompare(b.day))
87-
const rangeStartDate = parseIsoDate(rangeStartIso)
88-
const rangeEndDate = parseIsoDate(rangeEndIso)
89-
90-
const groupedByIndex = new Map<number, number>()
91-
92-
for (const item of sorted) {
93-
const itemDate = parseIsoDate(item.day)
94-
const dayOffset = Math.floor((itemDate.getTime() - rangeStartDate.getTime()) / 86400000)
95-
if (dayOffset < 0) continue
96-
97-
const weekIndex = Math.floor(dayOffset / 7)
98-
groupedByIndex.set(weekIndex, (groupedByIndex.get(weekIndex) ?? 0) + item.value)
99-
}
100-
101-
return Array.from(groupedByIndex.entries())
102-
.sort(([a], [b]) => a - b)
103-
.map(([weekIndex, value]) => {
104-
const weekStartDate = addDays(rangeStartDate, weekIndex * 7)
105-
const weekEndDate = addDays(weekStartDate, 6)
106-
107-
// Clamp weekEnd to the actual data range end date
108-
const clampedWeekEndDate =
109-
weekEndDate.getTime() > rangeEndDate.getTime() ? rangeEndDate : weekEndDate
110-
111-
const weekStartIso = toIsoDateString(weekStartDate)
112-
const weekEndIso = toIsoDateString(clampedWeekEndDate)
113-
114-
const timestampStart = weekStartDate.getTime()
115-
const timestampEnd = clampedWeekEndDate.getTime()
116-
117-
return {
118-
value,
119-
weekKey: `${weekStartIso}_${weekEndIso}`,
120-
weekStart: weekStartIso,
121-
weekEnd: weekEndIso,
122-
timestampStart,
123-
timestampEnd,
124-
}
125-
})
126-
}
127-
12869
/** Catmull-Rom monotone cubic spline — same algorithm as vue-data-ui's smoothPath for OG Images */
12970
export function smoothPath(pts: { x: number; y: number }[]): string {
13071
if (pts.length < 2) return '0,0'
@@ -162,42 +103,6 @@ export function smoothPath(pts: { x: number; y: number }[]): string {
162103
return out.join(' ')
163104
}
164105

165-
export function buildMonthlyEvolutionFromDaily(daily: DailyRawPoint[]): MonthlyDataPoint[] {
166-
const sorted = daily.slice().sort((a, b) => a.day.localeCompare(b.day))
167-
const valuesByMonth = new Map<string, number>()
168-
169-
for (const item of sorted) {
170-
const month = item.day.slice(0, 7)
171-
valuesByMonth.set(month, (valuesByMonth.get(month) ?? 0) + item.value)
172-
}
173-
174-
return Array.from(valuesByMonth.entries())
175-
.sort(([a], [b]) => a.localeCompare(b))
176-
.map(([month, value]) => {
177-
const monthStartDate = parseIsoDate(`${month}-01`)
178-
const timestamp = monthStartDate.getTime()
179-
return { month, value, timestamp }
180-
})
181-
}
182-
183-
export function buildYearlyEvolutionFromDaily(daily: DailyRawPoint[]): YearlyDataPoint[] {
184-
const sorted = daily.slice().sort((a, b) => a.day.localeCompare(b.day))
185-
const valuesByYear = new Map<string, number>()
186-
187-
for (const item of sorted) {
188-
const year = item.day.slice(0, 4)
189-
valuesByYear.set(year, (valuesByYear.get(year) ?? 0) + item.value)
190-
}
191-
192-
return Array.from(valuesByYear.entries())
193-
.sort(([a], [b]) => a.localeCompare(b))
194-
.map(([year, value]) => {
195-
const yearStartDate = parseIsoDate(`${year}-01-01`)
196-
const timestamp = yearStartDate.getTime()
197-
return { year, value, timestamp }
198-
})
199-
}
200-
201106
const npmDailyRangeCache = import.meta.client ? new Map<string, Promise<DailyRawPoint[]>>() : null
202107
const likesEvolutionCache = import.meta.client ? new Map<string, Promise<DailyRawPoint[]>>() : null
203108
const contributorsEvolutionCache = import.meta.client

nuxt.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ export default defineNuxtConfig({
127127
'/api/registry/package-meta/**': { isr: 300 },
128128
'/:pkg/.well-known/skills/**': { isr: 3600 },
129129
'/:scope/:pkg/.well-known/skills/**': { isr: 3600 },
130-
'/_og/d/**': getISRConfig(60 * 60 * 24), // 1 day
131130
'/_avatar/**': { isr: 3600, proxy: 'https://www.gravatar.com/avatar/**' },
132131
'/opensearch.xml': { isr: true },
133132
'/oauth-client-metadata.json': { prerender: true },

test/nuxt/components/OgImagePackage.spec.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ vi.mock('~/composables/useCharts', () => ({
2020
useCharts: vi.fn().mockReturnValue({
2121
fetchPackageDownloadEvolution: vi.fn().mockResolvedValue([]),
2222
}),
23-
buildRollingWeeklyEvolutionFromDaily: vi.fn().mockReturnValue([]),
2423
smoothPath: vi.fn().mockReturnValue(''),
2524
}))
2625

@@ -38,9 +37,31 @@ describe('OgImagePackage', () => {
3837
stars?: number
3938
license?: string | null
4039
packageName?: string
40+
downloads?: number
4141
} = {},
4242
) {
43-
const { stars = 0, license = 'MIT', packageName = 'test-package' } = overrides
43+
const {
44+
stars = 0,
45+
license = 'MIT',
46+
packageName = 'test-package',
47+
downloads = 12500,
48+
} = overrides
49+
50+
// Mock $fetch for downloads endpoint
51+
vi.spyOn(globalThis, '$fetch').mockImplementation((url: string) => {
52+
if (typeof url === 'string' && url.includes('api.npmjs.org/downloads/point')) {
53+
return Promise.resolve({
54+
downloads,
55+
start: '2026-04-03',
56+
end: '2026-04-09',
57+
package: packageName,
58+
})
59+
}
60+
if (typeof url === 'string' && url.includes('/api/social/likes/')) {
61+
return Promise.resolve({ totalLikes: 0, userHasLiked: false })
62+
}
63+
return Promise.resolve(null)
64+
})
4465

4566
mockUseResolvedVersion.mockReturnValue({
4667
data: ref('1.0.0'),
@@ -79,6 +100,7 @@ describe('OgImagePackage', () => {
79100
mockUseResolvedVersion.mockReset()
80101
mockUsePackage.mockReset()
81102
mockUseRepoMeta.mockReset()
103+
vi.restoreAllMocks()
82104
})
83105

84106
it('renders the package name', async () => {
@@ -135,6 +157,30 @@ describe('OgImagePackage', () => {
135157
})
136158
})
137159

160+
describe('downloads', () => {
161+
it('shows formatted downloads when present', async () => {
162+
setupMocks({ downloads: 12500 })
163+
164+
const component = await mountSuspended(OgImagePackage, {
165+
props: baseProps,
166+
})
167+
168+
expect(component.find('[data-testid="downloads"]').exists()).toBe(true)
169+
expect(component.text()).toContain('12.5K')
170+
expect(component.text()).toContain('/wk')
171+
})
172+
173+
it('hides downloads when count is 0', async () => {
174+
setupMocks({ downloads: 0 })
175+
176+
const component = await mountSuspended(OgImagePackage, {
177+
props: baseProps,
178+
})
179+
180+
expect(component.find('[data-testid="downloads"]').exists()).toBe(false)
181+
})
182+
})
183+
138184
it('renders repo info', async () => {
139185
setupMocks()
140186

0 commit comments

Comments
 (0)