Skip to content

Commit 2758f20

Browse files
Adebesin-Cellclaude
andcommitted
feat: show live download bars and versions in compare OG image
Fetches real weekly downloads and latest version for each package, renders a horizontal bar chart with gradient bars sized relative to the highest-downloaded package. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fdd8983 commit 2758f20

File tree

1 file changed

+107
-27
lines changed

1 file changed

+107
-27
lines changed

app/components/OgImage/Compare.vue

Lines changed: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
2-
import { computed } from 'vue'
2+
import { computed, ref } from 'vue'
3+
import { encodePackageName } from '#shared/utils/npm'
34
45
const props = withDefaults(
56
defineProps<{
@@ -25,23 +26,79 @@ const displayPackages = computed(() => {
2526
: raw
2627
return list.slice(0, 4)
2728
})
29+
30+
interface PkgStats {
31+
name: string
32+
downloads: number
33+
version: string
34+
color: string
35+
}
36+
37+
const stats = ref<PkgStats[]>([])
38+
39+
try {
40+
const results = await Promise.all(
41+
displayPackages.value.map(async (name, index) => {
42+
const encoded = encodePackageName(name)
43+
const [dlData, pkgData] = await Promise.all([
44+
$fetch<{ downloads: number }>(
45+
`https://api.npmjs.org/downloads/point/last-week/${encoded}`,
46+
).catch(() => null),
47+
$fetch<{ 'dist-tags'?: { latest?: string } }>(
48+
`https://registry.npmjs.org/${encoded}`,
49+
{ headers: { Accept: 'application/vnd.npm.install-v1+json' } },
50+
).catch(() => null),
51+
])
52+
return {
53+
name,
54+
downloads: dlData?.downloads ?? 0,
55+
version: pkgData?.['dist-tags']?.latest ?? '',
56+
color: ACCENT_COLORS[index % ACCENT_COLORS.length]!,
57+
}
58+
}),
59+
)
60+
stats.value = results
61+
} catch {
62+
stats.value = displayPackages.value.map((name, index) => ({
63+
name,
64+
downloads: 0,
65+
version: '',
66+
color: ACCENT_COLORS[index % ACCENT_COLORS.length]!,
67+
}))
68+
}
69+
70+
const maxDownloads = computed(() => Math.max(...stats.value.map(s => s.downloads), 1))
71+
72+
function formatDownloads(n: number): string {
73+
if (n === 0) return ''
74+
return Intl.NumberFormat('en', {
75+
notation: 'compact',
76+
maximumFractionDigits: 1,
77+
}).format(n)
78+
}
79+
80+
// Bar width as percentage string (max 100%)
81+
function barPct(downloads: number): string {
82+
const pct = (downloads / maxDownloads.value) * 100
83+
return `${Math.max(pct, 5)}%`
84+
}
2885
</script>
2986

3087
<template>
3188
<div
3289
class="h-full w-full flex flex-col justify-center px-20 bg-[#050505] text-[#fafafa] relative overflow-hidden"
3390
style="font-family: 'Geist Mono', sans-serif"
3491
>
35-
<div class="relative z-10 flex flex-col gap-6">
36-
<!-- Icon + title row (same pattern as Default/Package) -->
92+
<div class="relative z-10 flex flex-col gap-5">
93+
<!-- Icon + title row -->
3794
<div class="flex items-start gap-4">
3895
<div
39-
class="flex items-center justify-center w-16 h-16 p-3.5 rounded-xl shadow-lg bg-gradient-to-tr from-[#3b82f6]"
96+
class="flex items-center justify-center w-14 h-14 p-3 rounded-xl shadow-lg bg-gradient-to-tr from-[#3b82f6]"
4097
:style="{ backgroundColor: primaryColor }"
4198
>
4299
<svg
43-
width="36"
44-
height="36"
100+
width="32"
101+
height="32"
45102
viewBox="0 0 24 24"
46103
fill="none"
47104
stroke="white"
@@ -56,35 +113,58 @@ const displayPackages = computed(() => {
56113
</svg>
57114
</div>
58115

59-
<h1 class="text-8xl font-bold">
116+
<h1 class="text-7xl font-bold tracking-tight">
60117
<span
61118
class="opacity-80 tracking-[-0.1em]"
62119
:style="{ color: primaryColor }"
63-
style="margin-left: -1rem; margin-right: 0.5rem"
64-
>./</span
65-
>compare
120+
style="margin-right: 0.25rem"
121+
>./</span>compare
66122
</h1>
67123
</div>
68124

69-
<!-- Package names as badges (same badge style as Default/Package) -->
70-
<div
71-
class="flex flex-wrap items-center gap-x-3 gap-y-3 text-4xl text-[#a3a3a3]"
72-
style="font-family: 'Geist', sans-serif"
73-
>
74-
<template v-for="(pkg, index) in displayPackages" :key="pkg">
75-
<span
76-
class="px-3 py-1 rounded-lg border font-normal"
125+
<!-- Bar chart rows -->
126+
<div class="flex flex-col gap-2">
127+
<div
128+
v-for="pkg in stats"
129+
:key="pkg.name"
130+
class="flex flex-col gap-1"
131+
>
132+
<!-- Label row: name + downloads + version -->
133+
<div
134+
class="flex items-center gap-3"
135+
style="font-family: 'Geist', sans-serif"
136+
>
137+
<span
138+
class="text-2xl font-semibold tracking-tight"
139+
:style="{ color: pkg.color }"
140+
>
141+
{{ pkg.name }}
142+
</span>
143+
<span class="text-xl text-[#737373]">
144+
{{ formatDownloads(pkg.downloads) }}/wk
145+
</span>
146+
<span
147+
v-if="pkg.version"
148+
class="text-lg px-2 py-0.5 rounded-md border"
149+
:style="{
150+
color: pkg.color,
151+
backgroundColor: pkg.color + '10',
152+
borderColor: pkg.color + '30',
153+
}"
154+
>
155+
{{ pkg.version }}
156+
</span>
157+
</div>
158+
159+
<!-- Bar -->
160+
<div
161+
class="h-6 rounded-md"
77162
:style="{
78-
color: ACCENT_COLORS[index % ACCENT_COLORS.length],
79-
backgroundColor: ACCENT_COLORS[index % ACCENT_COLORS.length] + '10',
80-
borderColor: ACCENT_COLORS[index % ACCENT_COLORS.length] + '30',
81-
boxShadow: `0 0 20px ${ACCENT_COLORS[index % ACCENT_COLORS.length]}25`,
163+
width: barPct(pkg.downloads),
164+
background: `linear-gradient(90deg, ${pkg.color}50, ${pkg.color}20)`,
82165
}"
83-
>
84-
{{ pkg }}
85-
</span>
86-
<span v-if="index < displayPackages.length - 1"> vs </span>
87-
</template>
166+
/>
167+
</div>
88168
</div>
89169
</div>
90170

0 commit comments

Comments
 (0)