mirror of
https://github.com/samber/awesome-prometheus-alerts.git
synced 2026-06-21 00:47:18 +08:00
feat: add GitHub star nudges across the site
- Prepend attribution comment to "Copy all" exporter clipboard
- Show inline ⭐ Star nudge on individual rule copy (3s, dismisses automatically)
- Change StatsBar stars label to "engineers starred" for social proof
- Add milestone progress bar toward 10k stars in StatsBar
- Fix header/StatsBar showing "0" when SSR GitHub API fetch fails (use "—" placeholder)
This commit is contained in:
parent
d38511d7cb
commit
bb055773b4
5 changed files with 112 additions and 29 deletions
|
|
@ -1,31 +1,53 @@
|
||||||
---
|
---
|
||||||
|
import { GITHUB_URL } from '../data/site';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
targetId: string;
|
targetId: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
variant?: 'icon' | 'text';
|
variant?: 'icon' | 'text';
|
||||||
class?: string;
|
class?: string;
|
||||||
|
withAttribution?: boolean;
|
||||||
|
nudge?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { targetId, label = 'Copy', variant = 'icon', class: extraClass = '' } = Astro.props;
|
const { targetId, label = 'Copy', variant = 'icon', class: extraClass = '', withAttribution = false, nudge = false } = Astro.props;
|
||||||
const btnId = `copy-btn-${targetId}`;
|
const btnId = `copy-btn-${targetId}`;
|
||||||
|
const nudgeId = `star-nudge-${btnId}`;
|
||||||
---
|
---
|
||||||
|
|
||||||
{variant === 'icon' ? (
|
{variant === 'icon' ? (
|
||||||
<button
|
<span class={`inline-flex items-center gap-1 ${extraClass}`}>
|
||||||
id={btnId}
|
<button
|
||||||
data-copy-target={targetId}
|
id={btnId}
|
||||||
aria-label="Copy to clipboard"
|
data-copy-target={targetId}
|
||||||
class={`copy-btn inline-flex items-center gap-1.5 px-2 py-1 text-xs rounded text-slate-400 dark:text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors ${extraClass}`}
|
aria-label="Copy to clipboard"
|
||||||
>
|
class="copy-btn inline-flex items-center gap-1.5 px-2 py-1 text-xs rounded text-slate-400 dark:text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
|
||||||
<svg class="copy-icon w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
<svg class="copy-icon w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
<svg class="check-icon w-3.5 h-3.5 hidden text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
</svg>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
<svg class="check-icon w-3.5 h-3.5 hidden text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
<span class="copy-label sr-only">Copy</span>
|
</svg>
|
||||||
<span class="copied-label hidden text-green-500 not-sr-only text-xs">Copied!</span>
|
<span class="copy-label sr-only">Copy</span>
|
||||||
</button>
|
<span class="copied-label hidden text-green-500 not-sr-only text-xs">Copied!</span>
|
||||||
|
</button>
|
||||||
|
{nudge && (
|
||||||
|
<a
|
||||||
|
id={nudgeId}
|
||||||
|
href={GITHUB_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="hidden items-center gap-0.5 text-xs text-yellow-500 hover:text-yellow-600 dark:text-yellow-400 dark:hover:text-yellow-300 transition-colors whitespace-nowrap"
|
||||||
|
aria-label="Star on GitHub"
|
||||||
|
>
|
||||||
|
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||||
|
</svg>
|
||||||
|
Star
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
id={btnId}
|
id={btnId}
|
||||||
|
|
@ -43,7 +65,7 @@ const btnId = `copy-btn-${targetId}`;
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<script define:vars={{ btnId }}>
|
<script define:vars={{ btnId, nudgeId, withAttribution, nudge, GITHUB_URL }}>
|
||||||
const btn = document.getElementById(btnId);
|
const btn = document.getElementById(btnId);
|
||||||
if (!(btn instanceof HTMLButtonElement)) return;
|
if (!(btn instanceof HTMLButtonElement)) return;
|
||||||
if (btn.dataset.copyBound === 'true') return;
|
if (btn.dataset.copyBound === 'true') return;
|
||||||
|
|
@ -55,13 +77,17 @@ const btnId = `copy-btn-${targetId}`;
|
||||||
const target = document.getElementById(targetId);
|
const target = document.getElementById(targetId);
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
const text = target.textContent ?? '';
|
let text = target.textContent ?? '';
|
||||||
|
text = withAttribution
|
||||||
|
? `# Source: ${GITHUB_URL}\n${text.trim()}`
|
||||||
|
: text.trim();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text.trim());
|
await navigator.clipboard.writeText(text);
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback for older browsers
|
// Fallback for older browsers
|
||||||
const ta = document.createElement('textarea');
|
const ta = document.createElement('textarea');
|
||||||
ta.value = text.trim();
|
ta.value = text;
|
||||||
ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;';
|
ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;';
|
||||||
document.body.appendChild(ta);
|
document.body.appendChild(ta);
|
||||||
ta.select();
|
ta.select();
|
||||||
|
|
@ -87,6 +113,19 @@ const btnId = `copy-btn-${targetId}`;
|
||||||
copiedLabel?.classList.add('hidden');
|
copiedLabel?.classList.add('hidden');
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
|
// Inline star nudge
|
||||||
|
if (nudge) {
|
||||||
|
const nudgeEl = document.getElementById(nudgeId);
|
||||||
|
if (nudgeEl) {
|
||||||
|
nudgeEl.classList.remove('hidden');
|
||||||
|
nudgeEl.classList.add('inline-flex');
|
||||||
|
setTimeout(() => {
|
||||||
|
nudgeEl.classList.add('hidden');
|
||||||
|
nudgeEl.classList.remove('inline-flex');
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.dispatchEvent(new CustomEvent('copy-success'));
|
window.dispatchEvent(new CustomEvent('copy-success'));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ const exporterPrefix = showExporterNumber
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<CopyButton targetId={allRulesId} label="Copy all" variant="text" />
|
<CopyButton targetId={allRulesId} label="Copy all" variant="text" withAttribution={true} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ try {
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const starsLabel = stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars);
|
const starsLabel = stars > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars)) : '—';
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Main header -->
|
<!-- Main header -->
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ const codeId = `code-${anchorId}`;
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<CopyButton targetId={codeId} variant="icon" />
|
<CopyButton targetId={codeId} variant="icon" nudge={true} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
---
|
---
|
||||||
import { getTotalRuleCount, getTotalExporterCount, data } from '../data/rules';
|
import { getTotalRuleCount, getTotalExporterCount, data } from '../data/rules';
|
||||||
import { GITHUB_API_REPO_URL } from '../data/site';
|
import { GITHUB_API_REPO_URL, GITHUB_URL } from '../data/site';
|
||||||
|
|
||||||
const totalRules = getTotalRuleCount();
|
const totalRules = getTotalRuleCount();
|
||||||
const totalExporters = getTotalExporterCount();
|
const totalExporters = getTotalExporterCount();
|
||||||
const totalGroups = data.groups.length;
|
const totalGroups = data.groups.length;
|
||||||
|
|
||||||
|
const STAR_MILESTONE = 10000;
|
||||||
|
const MILESTONE_LABEL = '10k';
|
||||||
|
|
||||||
let stars = 0;
|
let stars = 0;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(GITHUB_API_REPO_URL, {
|
const res = await fetch(GITHUB_API_REPO_URL, {
|
||||||
|
|
@ -17,7 +20,10 @@ try {
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const starsLabel = stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars);
|
const starsLabel = stars > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars)) : '—';
|
||||||
|
const progressPct = stars > 0 ? Math.min(100, (stars / STAR_MILESTONE) * 100).toFixed(1) : '0';
|
||||||
|
const starsFormatted = stars > 0 ? stars.toLocaleString('en') : '';
|
||||||
|
const milestoneFormatted = STAR_MILESTONE.toLocaleString('en');
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="flex flex-wrap justify-center gap-6 sm:gap-10 py-4 text-center">
|
<div class="flex flex-wrap justify-center gap-6 sm:gap-10 py-4 text-center">
|
||||||
|
|
@ -33,33 +39,70 @@ const starsLabel = stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(star
|
||||||
<div class="text-2xl font-bold text-brand dark:text-brand-dark">{totalGroups}</div>
|
<div class="text-2xl font-bold text-brand dark:text-brand-dark">{totalGroups}</div>
|
||||||
<div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">categories</div>
|
<div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">categories</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="https://github.com/samber/awesome-prometheus-alerts" target="_blank" rel="noopener noreferrer" class="group">
|
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" class="group">
|
||||||
<div class="text-2xl font-bold text-brand dark:text-brand-dark flex items-center justify-center gap-1">
|
<div class="text-2xl font-bold text-brand dark:text-brand-dark flex items-center justify-center gap-1">
|
||||||
<svg class="w-5 h-5 text-yellow-500" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
<svg class="w-5 h-5 text-yellow-500" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span id="statsbar-stars">{starsLabel}</span>
|
<span id="statsbar-stars">{starsLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5 group-hover:text-brand dark:group-hover:text-brand-dark transition-colors">GitHub stars</div>
|
<div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5 group-hover:text-brand dark:group-hover:text-brand-dark transition-colors">engineers starred</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<!-- Milestone progress bar -->
|
||||||
|
<div id="star-milestone-row" class={`mt-2 px-4 ${stars === 0 ? 'opacity-0' : ''}`}>
|
||||||
|
<div class="max-w-xs mx-auto">
|
||||||
|
<div class="flex items-center justify-between text-xs text-slate-400 dark:text-slate-500 mb-1">
|
||||||
|
<span>
|
||||||
|
<span id="statsbar-stars-count">{starsFormatted}</span><span id="statsbar-milestone-suffix">{stars > 0 ? ` / ${milestoneFormatted} — help us reach ${MILESTONE_LABEL}!` : ''}</span>
|
||||||
|
</span>
|
||||||
|
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" class="text-yellow-500 hover:text-yellow-600 dark:text-yellow-400 dark:hover:text-yellow-300 font-medium transition-colors flex items-center gap-0.5 ml-2 flex-shrink-0">
|
||||||
|
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||||
|
</svg>
|
||||||
|
Star it
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-100 dark:bg-slate-800 rounded-full h-1.5">
|
||||||
|
<div
|
||||||
|
id="statsbar-star-bar"
|
||||||
|
class="bg-yellow-400 dark:bg-yellow-500 h-1.5 rounded-full transition-all duration-700"
|
||||||
|
style={`width: ${progressPct}%`}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script define:vars={{ STAR_MILESTONE, MILESTONE_LABEL }}>
|
||||||
const starsEl = document.getElementById('statsbar-stars');
|
const starsEl = document.getElementById('statsbar-stars');
|
||||||
|
const starsCountEl = document.getElementById('statsbar-stars-count');
|
||||||
|
const milestoneSuffixEl = document.getElementById('statsbar-milestone-suffix');
|
||||||
|
const barEl = document.getElementById('statsbar-star-bar');
|
||||||
|
const milestoneRow = document.getElementById('star-milestone-row');
|
||||||
|
|
||||||
if (starsEl) {
|
if (starsEl) {
|
||||||
const CACHE_KEY = 'gh_stars_apa';
|
const CACHE_KEY = 'gh_stars_apa';
|
||||||
const CACHE_TTL = 3600 * 1000;
|
const CACHE_TTL = 3600 * 1000;
|
||||||
|
|
||||||
function fmt(n: number): string {
|
function fmt(n) {
|
||||||
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateMilestone(n) {
|
||||||
|
if (starsCountEl) starsCountEl.textContent = n.toLocaleString('en');
|
||||||
|
if (milestoneSuffixEl) milestoneSuffixEl.textContent = ` / ${STAR_MILESTONE.toLocaleString('en')} — help us reach ${MILESTONE_LABEL}!`;
|
||||||
|
if (barEl) barEl.style.width = `${Math.min(100, (n / STAR_MILESTONE) * 100).toFixed(1)}%`;
|
||||||
|
if (milestoneRow) milestoneRow.classList.remove('opacity-0');
|
||||||
|
}
|
||||||
|
|
||||||
const cached = sessionStorage.getItem(CACHE_KEY);
|
const cached = sessionStorage.getItem(CACHE_KEY);
|
||||||
let isFresh = false;
|
let isFresh = false;
|
||||||
if (cached) {
|
if (cached) {
|
||||||
const { value, ts } = JSON.parse(cached);
|
const { value, ts } = JSON.parse(cached);
|
||||||
if (Date.now() - ts < CACHE_TTL) {
|
if (Date.now() - ts < CACHE_TTL) {
|
||||||
starsEl.textContent = fmt(value);
|
starsEl.textContent = fmt(value);
|
||||||
|
updateMilestone(value);
|
||||||
isFresh = true;
|
isFresh = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +114,7 @@ const starsLabel = stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(star
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data?.stargazers_count) {
|
if (data?.stargazers_count) {
|
||||||
starsEl.textContent = fmt(data.stargazers_count);
|
starsEl.textContent = fmt(data.stargazers_count);
|
||||||
|
updateMilestone(data.stargazers_count);
|
||||||
sessionStorage.setItem(CACHE_KEY, JSON.stringify({ value: data.stargazers_count, ts: Date.now() }));
|
sessionStorage.setItem(CACHE_KEY, JSON.stringify({ value: data.stargazers_count, ts: Date.now() }));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue