Skip to content

Commit 4717e86

Browse files
committed
shared comonent
1 parent cf1e786 commit 4717e86

4 files changed

Lines changed: 222 additions & 115 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<script>
2+
(() => {
3+
const countEls = document.querySelectorAll('[data-ops-signups-count]');
4+
if (!countEls.length) return;
5+
6+
const parseCount = (value) => {
7+
if (value === null || value === undefined) return NaN;
8+
const normalized = String(value).replace(/,/g, '').trim();
9+
return Number.parseInt(normalized, 10);
10+
};
11+
12+
const formatCount = (value) => value.toLocaleString('en-US');
13+
14+
const animateSignups = (targetValue) => {
15+
const target = parseCount(targetValue);
16+
if (!Number.isFinite(target)) return;
17+
18+
const initialValue = parseCount(countEls[0].textContent);
19+
const start = Number.isFinite(initialValue) ? initialValue : target;
20+
if (target <= start) {
21+
const finalText = formatCount(target);
22+
countEls.forEach((el) => {
23+
el.textContent = finalText;
24+
});
25+
return;
26+
}
27+
28+
const durationMs = 2500;
29+
const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
30+
const startAt = performance.now();
31+
32+
const tick = (now) => {
33+
const elapsed = now - startAt;
34+
const progress = Math.min(1, elapsed / durationMs);
35+
const eased = easeOutCubic(progress);
36+
const nextValue = Math.round(start + (target - start) * eased);
37+
const nextText = formatCount(nextValue);
38+
39+
countEls.forEach((el) => {
40+
el.textContent = nextText;
41+
});
42+
43+
if (progress < 1) {
44+
requestAnimationFrame(tick);
45+
}
46+
};
47+
48+
requestAnimationFrame(tick);
49+
};
50+
51+
const updateSignups = (value) => {
52+
if (!value) return;
53+
animateSignups(value);
54+
};
55+
56+
const fallbackText = countEls[0].textContent;
57+
if (fallbackText) {
58+
const fallbackValue = parseCount(fallbackText);
59+
const normalizedFallback = Number.isFinite(fallbackValue) ? fallbackValue : 426;
60+
countEls.forEach((el) => {
61+
el.textContent = formatCount(normalizedFallback);
62+
});
63+
}
64+
65+
const start = () => {
66+
fetch('https://ops.dotenvx.com/public/stats', { method: 'GET' })
67+
.then((response) => {
68+
if (!response.ok) throw new Error(`stats request failed: ${response.status}`);
69+
return response.json();
70+
})
71+
.then((data) => {
72+
updateSignups(data && data.signups);
73+
})
74+
.catch(() => {
75+
// Keep fallback count from markup.
76+
});
77+
};
78+
79+
const triggerEl = document.querySelector('[data-ops-signups-trigger]');
80+
if (!triggerEl) {
81+
start();
82+
return;
83+
}
84+
85+
if (!('IntersectionObserver' in window)) {
86+
start();
87+
return;
88+
}
89+
90+
let started = false;
91+
const observer = new IntersectionObserver((entries) => {
92+
if (started) return;
93+
entries.forEach((entry) => {
94+
if (!started && entry.isIntersecting) {
95+
started = true;
96+
observer.disconnect();
97+
start();
98+
}
99+
});
100+
}, { threshold: 0.35 });
101+
102+
observer.observe(triggerEl);
103+
})();
104+
</script>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<section class="{{ include.section_classes | default: 'w-full max-w-7xl mx-auto px-1 md:px-6 mt-24 md:mt-32 mb-8' }}" data-ops-signups-trigger>
2+
<div class="relative w-full overflow-hidden rounded-t-[0.9rem] md:rounded-t-[1.1rem] bg-black pt-14 md:pt-20 pb-14 md:pb-20">
3+
<div class="pointer-events-none absolute left-1/2 top-1/2 h-44 md:h-56 w-[72%] -translate-x-1/2 -translate-y-1/2 bg-[radial-gradient(ellipse_at_center,rgba(112,154,210,0.18)_0%,rgba(112,154,210,0.08)_38%,rgba(0,0,0,0)_74%)] blur-[2px]" aria-hidden="true"></div>
4+
<div class="relative z-10 text-center">
5+
<div class="mb-4 md:mb-5 text-[0.68rem] md:text-xs font-semibold tracking-[0.1em] uppercase text-zinc-500 dark:text-zinc-400">{{ include.label | default: 'TRUSTED BY DEVELOPERS' }}</div>
6+
<p class="mx-auto max-w-[36ch] text-center text-3xl md:text-4xl leading-tight font-medium tracking-[-0.01em] text-zinc-200">
7+
<span class="inline-block text-right tabular-nums" data-ops-signups-count>{{ include.fallback_count | default: 426 }}</span> {{ include.copy | default: 'developers signed up last month.' }}
8+
</p>
9+
</div>
10+
</div>
11+
</section>

ops/index.md

Lines changed: 2 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,7 @@ title: "Dotenvx Ops"
3333
</div>
3434
</section>
3535

36-
<section class="w-full max-w-7xl mx-auto px-1 md:px-6 mt-44 md:mt-64 lg:mt-[18rem] mb-32 sm:mb-48 md:mb-64 lg:mb-[18rem]" data-ops-signups-trigger>
37-
<div class="relative w-full overflow-hidden rounded-t-[0.9rem] md:rounded-t-[1.1rem] bg-black pt-14 md:pt-20 pb-14 md:pb-20">
38-
<div class="pointer-events-none absolute left-1/2 top-1/2 h-44 md:h-56 w-[72%] -translate-x-1/2 -translate-y-1/2 bg-[radial-gradient(ellipse_at_center,rgba(112,154,210,0.18)_0%,rgba(112,154,210,0.08)_38%,rgba(0,0,0,0)_74%)] blur-[2px]" aria-hidden="true"></div>
39-
<div class="relative z-10 text-center">
40-
<div class="mb-4 md:mb-5 text-[0.68rem] md:text-xs font-semibold tracking-[0.1em] uppercase text-zinc-500 dark:text-zinc-400">TRUSTED BY DEVELOPERS</div>
41-
<p class="mx-auto max-w-[36ch] text-center text-3xl md:text-4xl leading-tight font-medium tracking-[-0.01em] text-zinc-200">
42-
<span class="inline-block text-right tabular-nums" data-ops-signups-count>426</span> developers signed up last month.
43-
</p>
44-
</div>
45-
</div>
46-
</section>
36+
{% include components/trust-signups.html section_classes="w-full max-w-7xl mx-auto px-1 md:px-6 mt-44 md:mt-64 lg:mt-[18rem] mb-32 sm:mb-48 md:mb-64 lg:mb-[18rem]" %}
4737

4838
<section class="w-full max-w-5xl mx-auto px-6 mt-8 md:mt-16 lg:mt-20 mb-44 md:mb-64 lg:mb-[18rem]">
4939
<div class="text-center max-w-3xl mx-auto">
@@ -347,107 +337,4 @@ title: "Dotenvx Ops"
347337
})();
348338
</script>
349339

350-
<script>
351-
(() => {
352-
const countEls = document.querySelectorAll('[data-ops-signups-count]');
353-
if (!countEls.length) return;
354-
355-
const parseCount = (value) => {
356-
if (value === null || value === undefined) return NaN;
357-
const normalized = String(value).replace(/,/g, '').trim();
358-
return Number.parseInt(normalized, 10);
359-
};
360-
361-
const formatCount = (value) => value.toLocaleString('en-US');
362-
363-
const animateSignups = (targetValue) => {
364-
const target = parseCount(targetValue);
365-
if (!Number.isFinite(target)) return;
366-
367-
const initialValue = parseCount(countEls[0].textContent);
368-
const start = Number.isFinite(initialValue) ? initialValue : target;
369-
if (target <= start) {
370-
const finalText = formatCount(target);
371-
countEls.forEach((el) => {
372-
el.textContent = finalText;
373-
});
374-
return;
375-
}
376-
377-
const durationMs = 2500;
378-
const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
379-
const startAt = performance.now();
380-
381-
const tick = (now) => {
382-
const elapsed = now - startAt;
383-
const progress = Math.min(1, elapsed / durationMs);
384-
const eased = easeOutCubic(progress);
385-
const nextValue = Math.round(start + (target - start) * eased);
386-
const nextText = formatCount(nextValue);
387-
388-
countEls.forEach((el) => {
389-
el.textContent = nextText;
390-
});
391-
392-
if (progress < 1) {
393-
requestAnimationFrame(tick);
394-
}
395-
};
396-
397-
requestAnimationFrame(tick);
398-
};
399-
400-
const updateSignups = (value) => {
401-
if (!value) return;
402-
animateSignups(value);
403-
};
404-
405-
const fallbackText = countEls[0].textContent;
406-
if (fallbackText) {
407-
const fallbackValue = parseCount(fallbackText);
408-
const normalizedFallback = Number.isFinite(fallbackValue) ? fallbackValue : 426;
409-
countEls.forEach((el) => {
410-
el.textContent = formatCount(normalizedFallback);
411-
});
412-
}
413-
414-
const start = () => {
415-
fetch('https://ops.dotenvx.com/public/stats', { method: 'GET' })
416-
.then((response) => {
417-
if (!response.ok) throw new Error(`stats request failed: ${response.status}`);
418-
return response.json();
419-
})
420-
.then((data) => {
421-
updateSignups(data && data.signups);
422-
})
423-
.catch(() => {
424-
// Keep fallback count from markup.
425-
});
426-
};
427-
428-
const triggerEl = document.querySelector('[data-ops-signups-trigger]');
429-
if (!triggerEl) {
430-
start();
431-
return;
432-
}
433-
434-
if (!('IntersectionObserver' in window)) {
435-
start();
436-
return;
437-
}
438-
439-
let started = false;
440-
const observer = new IntersectionObserver((entries) => {
441-
if (started) return;
442-
entries.forEach((entry) => {
443-
if (!started && entry.isIntersecting) {
444-
started = true;
445-
observer.disconnect();
446-
start();
447-
}
448-
});
449-
}, { threshold: 0.35 });
450-
451-
observer.observe(triggerEl);
452-
})();
453-
</script>
340+
{% include components/trust-signups-script.html %}

pricing/index.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,108 @@ title: "Pricing"
267267
</div>
268268
</section>
269269
</section>
270+
271+
<script>
272+
(() => {
273+
const countEls = document.querySelectorAll('[data-ops-signups-count]');
274+
if (!countEls.length) return;
275+
276+
const parseCount = (value) => {
277+
if (value === null || value === undefined) return NaN;
278+
const normalized = String(value).replace(/,/g, '').trim();
279+
return Number.parseInt(normalized, 10);
280+
};
281+
282+
const formatCount = (value) => value.toLocaleString('en-US');
283+
284+
const animateSignups = (targetValue) => {
285+
const target = parseCount(targetValue);
286+
if (!Number.isFinite(target)) return;
287+
288+
const initialValue = parseCount(countEls[0].textContent);
289+
const start = Number.isFinite(initialValue) ? initialValue : target;
290+
if (target <= start) {
291+
const finalText = formatCount(target);
292+
countEls.forEach((el) => {
293+
el.textContent = finalText;
294+
});
295+
return;
296+
}
297+
298+
const durationMs = 2500;
299+
const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
300+
const startAt = performance.now();
301+
302+
const tick = (now) => {
303+
const elapsed = now - startAt;
304+
const progress = Math.min(1, elapsed / durationMs);
305+
const eased = easeOutCubic(progress);
306+
const nextValue = Math.round(start + (target - start) * eased);
307+
const nextText = formatCount(nextValue);
308+
309+
countEls.forEach((el) => {
310+
el.textContent = nextText;
311+
});
312+
313+
if (progress < 1) {
314+
requestAnimationFrame(tick);
315+
}
316+
};
317+
318+
requestAnimationFrame(tick);
319+
};
320+
321+
const updateSignups = (value) => {
322+
if (!value) return;
323+
animateSignups(value);
324+
};
325+
326+
const fallbackText = countEls[0].textContent;
327+
if (fallbackText) {
328+
const fallbackValue = parseCount(fallbackText);
329+
const normalizedFallback = Number.isFinite(fallbackValue) ? fallbackValue : 426;
330+
countEls.forEach((el) => {
331+
el.textContent = formatCount(normalizedFallback);
332+
});
333+
}
334+
335+
const start = () => {
336+
fetch('https://ops.dotenvx.com/public/stats', { method: 'GET' })
337+
.then((response) => {
338+
if (!response.ok) throw new Error(`stats request failed: ${response.status}`);
339+
return response.json();
340+
})
341+
.then((data) => {
342+
updateSignups(data && data.signups);
343+
})
344+
.catch(() => {
345+
// Keep fallback count from markup.
346+
});
347+
};
348+
349+
const triggerEl = document.querySelector('[data-ops-signups-trigger]');
350+
if (!triggerEl) {
351+
start();
352+
return;
353+
}
354+
355+
if (!('IntersectionObserver' in window)) {
356+
start();
357+
return;
358+
}
359+
360+
let started = false;
361+
const observer = new IntersectionObserver((entries) => {
362+
if (started) return;
363+
entries.forEach((entry) => {
364+
if (!started && entry.isIntersecting) {
365+
started = true;
366+
observer.disconnect();
367+
start();
368+
}
369+
});
370+
}, { threshold: 0.35 });
371+
372+
observer.observe(triggerEl);
373+
})();
374+
</script>

0 commit comments

Comments
 (0)