Skip to content

Commit f6d8d48

Browse files
Merge pull request #7 from hamdymohamedak/main
docs: hero npm downloads from API only, silent on failure
2 parents 998defb + 50ea942 commit f6d8d48

3 files changed

Lines changed: 265 additions & 0 deletions

File tree

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
<script setup lang="ts">
2+
import { computed, onMounted, onUnmounted, ref } from "vue";
3+
import { OPENFETCH_NPM_PACKAGE } from "../openfetch-version";
4+
5+
const NPM_API = `https://api.npmjs.org/downloads/point/last-year/${encodeURIComponent(OPENFETCH_NPM_PACKAGE)}`;
6+
const NPM_PAGE = `https://www.npmjs.com/package/${encodeURIComponent(OPENFETCH_NPM_PACKAGE)}`;
7+
8+
const downloads = ref<number | null>(null);
9+
const periodStart = ref<string | null>(null);
10+
const periodEnd = ref<string | null>(null);
11+
12+
let cancelled = false;
13+
14+
onUnmounted(() => {
15+
cancelled = true;
16+
});
17+
18+
onMounted(() => {
19+
(async () => {
20+
try {
21+
const res = await fetch(NPM_API);
22+
if (!res.ok) return;
23+
const data = (await res.json()) as {
24+
downloads?: unknown;
25+
start?: unknown;
26+
end?: unknown;
27+
};
28+
if (typeof data.downloads !== "number" || !Number.isFinite(data.downloads)) return;
29+
if (cancelled) return;
30+
downloads.value = data.downloads;
31+
if (typeof data.start === "string") periodStart.value = data.start;
32+
if (typeof data.end === "string") periodEnd.value = data.end;
33+
} catch {
34+
/* silent: leave downloads null — render nothing */
35+
}
36+
})();
37+
});
38+
39+
function formatCount(n: number) {
40+
return new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(n);
41+
}
42+
43+
const periodTitle = computed(() => {
44+
if (!periodStart.value || !periodEnd.value) return undefined;
45+
return `npm downloads for ${periodStart.value} → ${periodEnd.value} (registry window)`;
46+
});
47+
</script>
48+
49+
<template>
50+
<div v-if="downloads !== null" class="of-hero-npm-dl">
51+
<a
52+
class="of-hero-npm-dl-link"
53+
:href="NPM_PAGE"
54+
target="_blank"
55+
rel="noopener noreferrer"
56+
:title="periodTitle"
57+
>
58+
<span class="of-hero-npm-dl-accent" aria-hidden="true" />
59+
<span class="of-hero-npm-dl-inner">
60+
<span class="of-hero-npm-dl-kicker">
61+
<span class="of-hero-npm-dl-mark">npm</span>
62+
<span class="of-hero-npm-dl-kicker-text">Package installs</span>
63+
</span>
64+
<span class="of-hero-npm-dl-stat">
65+
<strong class="of-hero-npm-dl-num">{{ formatCount(downloads) }}</strong>
66+
<span class="of-hero-npm-dl-label">last 12 months · registry</span>
67+
</span>
68+
</span>
69+
<span class="of-hero-npm-dl-chevron" aria-hidden="true">→</span>
70+
</a>
71+
</div>
72+
</template>
73+
74+
<style scoped>
75+
.of-hero-npm-dl {
76+
display: flex;
77+
justify-content: center;
78+
width: 100%;
79+
margin: 0.5rem auto 0;
80+
}
81+
82+
.of-hero-npm-dl-link {
83+
position: relative;
84+
display: inline-flex;
85+
align-items: stretch;
86+
max-width: min(100%, 22rem);
87+
padding: 0;
88+
border-radius: 14px;
89+
text-decoration: none;
90+
color: var(--vp-c-text-2);
91+
background: linear-gradient(
92+
145deg,
93+
color-mix(in srgb, var(--vp-c-bg-soft) 65%, var(--vp-c-bg)) 0%,
94+
color-mix(in srgb, var(--vp-c-bg) 94%, var(--vp-c-bg-alt)) 100%
95+
);
96+
border: 1px solid color-mix(in srgb, var(--vp-c-divider) 78%, var(--vp-c-brand-1) 12%);
97+
box-shadow:
98+
0 0 0 1px color-mix(in srgb, var(--vp-c-bg) 72%, transparent),
99+
0 10px 28px rgba(15, 23, 42, 0.06),
100+
0 2px 8px rgba(37, 99, 235, 0.04);
101+
backdrop-filter: blur(12px);
102+
overflow: hidden;
103+
transition:
104+
transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
105+
border-color 0.22s ease,
106+
box-shadow 0.22s ease;
107+
}
108+
109+
.of-hero-npm-dl-link:hover {
110+
transform: translateY(-2px);
111+
border-color: color-mix(in srgb, var(--vp-c-divider) 62%, var(--vp-c-brand-1) 26%);
112+
box-shadow:
113+
0 0 0 1px color-mix(in srgb, var(--vp-c-brand-1) 12%, transparent),
114+
0 16px 40px rgba(37, 99, 235, 0.1),
115+
0 4px 12px rgba(15, 23, 42, 0.06);
116+
}
117+
118+
.of-hero-npm-dl-link:focus-visible {
119+
outline: none;
120+
box-shadow:
121+
0 0 0 2px var(--vp-c-bg),
122+
0 0 0 4px color-mix(in srgb, var(--vp-c-brand-1) 55%, transparent),
123+
0 10px 28px rgba(15, 23, 42, 0.06);
124+
}
125+
126+
:root.dark .of-hero-npm-dl-link {
127+
box-shadow:
128+
0 0 0 1px rgba(255, 255, 255, 0.06),
129+
0 14px 40px rgba(0, 0, 0, 0.35);
130+
}
131+
132+
:root.dark .of-hero-npm-dl-link:hover {
133+
box-shadow:
134+
0 0 0 1px color-mix(in srgb, var(--vp-c-brand-1) 18%, transparent),
135+
0 18px 48px rgba(0, 0, 0, 0.45);
136+
}
137+
138+
.of-hero-npm-dl-accent {
139+
width: 4px;
140+
flex-shrink: 0;
141+
background: linear-gradient(
142+
180deg,
143+
#cb3837 0%,
144+
color-mix(in srgb, #cb3837 70%, var(--vp-c-brand-1)) 55%,
145+
var(--vp-c-brand-3) 100%
146+
);
147+
opacity: 0.95;
148+
}
149+
150+
.of-hero-npm-dl-inner {
151+
flex: 1;
152+
min-width: 0;
153+
display: flex;
154+
flex-direction: column;
155+
gap: 0.35rem;
156+
padding: 11px 10px 11px 12px;
157+
text-align: start;
158+
}
159+
160+
.of-hero-npm-dl-kicker {
161+
display: flex;
162+
align-items: center;
163+
gap: 8px;
164+
flex-wrap: wrap;
165+
}
166+
167+
.of-hero-npm-dl-mark {
168+
font-family: var(--of-font-mono, var(--vp-font-family-mono));
169+
font-size: 9px;
170+
font-weight: 800;
171+
letter-spacing: 0.1em;
172+
text-transform: uppercase;
173+
padding: 3px 7px;
174+
border-radius: 6px;
175+
color: #fff;
176+
background: linear-gradient(135deg, #cb3837 0%, #c92a2a 100%);
177+
box-shadow: 0 1px 2px rgba(203, 56, 55, 0.35);
178+
}
179+
180+
.of-hero-npm-dl-kicker-text {
181+
font-family: var(--of-font-display, var(--vp-font-family-base));
182+
font-size: 11px;
183+
font-weight: 600;
184+
letter-spacing: 0.02em;
185+
color: var(--vp-c-text-3);
186+
text-transform: none;
187+
}
188+
189+
.of-hero-npm-dl-stat {
190+
display: flex;
191+
flex-wrap: wrap;
192+
align-items: baseline;
193+
gap: 0.4rem 0.65rem;
194+
min-width: 0;
195+
}
196+
197+
.of-hero-npm-dl-num {
198+
font-family: var(--of-font-mono, var(--vp-font-family-mono));
199+
font-size: clamp(1.05rem, 2.8vw, 1.35rem);
200+
font-weight: 700;
201+
letter-spacing: -0.03em;
202+
font-variant-numeric: tabular-nums;
203+
line-height: 1.1;
204+
background: linear-gradient(
205+
100deg,
206+
var(--vp-c-text-1) 0%,
207+
color-mix(in srgb, var(--vp-c-text-1) 88%, var(--vp-c-brand-1)) 100%
208+
);
209+
-webkit-background-clip: text;
210+
background-clip: text;
211+
color: transparent;
212+
}
213+
214+
:root.dark .of-hero-npm-dl-num {
215+
background: linear-gradient(
216+
100deg,
217+
var(--vp-c-text-1) 0%,
218+
color-mix(in srgb, var(--vp-c-text-1) 82%, var(--vp-c-brand-3)) 100%
219+
);
220+
-webkit-background-clip: text;
221+
background-clip: text;
222+
color: transparent;
223+
}
224+
225+
.of-hero-npm-dl-label {
226+
font-family: var(--of-font-display, var(--vp-font-family-base));
227+
font-size: 11px;
228+
font-weight: 500;
229+
line-height: 1.25;
230+
color: var(--vp-c-text-3);
231+
letter-spacing: 0.01em;
232+
}
233+
234+
.of-hero-npm-dl-chevron {
235+
flex-shrink: 0;
236+
display: flex;
237+
align-items: center;
238+
padding: 0 12px 0 4px;
239+
font-size: 1rem;
240+
font-weight: 500;
241+
color: color-mix(in srgb, var(--vp-c-text-3) 70%, var(--vp-c-brand-1));
242+
opacity: 0.85;
243+
transition: transform 0.2s ease, opacity 0.2s ease;
244+
}
245+
246+
.of-hero-npm-dl-link:hover .of-hero-npm-dl-chevron {
247+
transform: translateX(3px);
248+
opacity: 1;
249+
}
250+
251+
@media (prefers-reduced-motion: reduce) {
252+
.of-hero-npm-dl-link,
253+
.of-hero-npm-dl-link:hover,
254+
.of-hero-npm-dl-chevron,
255+
.of-hero-npm-dl-link:hover .of-hero-npm-dl-chevron {
256+
transition: none;
257+
transform: none;
258+
}
259+
}
260+
</style>

‎.vitepress/openfetch-version.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
*
55
* Bump this when you publish a new `@hamdymohamedak/openfetch` release.
66
*/
7+
/** Published package name on npm (scoped). */
8+
export const OPENFETCH_NPM_PACKAGE = "@hamdymohamedak/openfetch";
9+
710
export const OPENFETCH_VERSION = "0.2.9";
811

912
/** Display tag, e.g. `v0.2.9` */

‎.vitepress/theme/Layout.vue‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import DefaultTheme from "vitepress/theme";
33
import { useData } from "vitepress";
44
import { computed, onUnmounted, watch } from "vue";
55
import HeroCodeTyper from "../components/HeroCodeTyper.vue";
6+
import HeroNpmDownloads from "../components/HeroNpmDownloads.vue";
67
import HomeAskAiButtons from "../components/HomeAskAiButtons.vue";
78
89
const { Layout } = DefaultTheme;
@@ -38,6 +39,7 @@ onUnmounted(() => {
3839
</template>
3940
<template v-if="isLocaleHome" #home-hero-actions-after>
4041
<HomeAskAiButtons />
42+
<HeroNpmDownloads />
4143
</template>
4244
</Layout>
4345
</template>

0 commit comments

Comments
 (0)