awesome-prometheus-alerts/site/src/components/CopyButton.astro
Samuel Berthe 1c5f626046
feat: add first-party copy event pipe to Tinybird
Sends rule_copy, wget_copy events on clipboard interactions,
bypassing ad blockers. Tracks user_id (localStorage apa_uid),
session_id (sessionStorage apa_sid), session/lifetime copy counts,
full rule coordinates (group/service/exporter/rule slugs + indices),
page context, and browser environment. Event name is the Tinybird
data source name, scoped to "rule" or "exporter" per copy type.
2026-04-15 11:34:12 +02:00

135 lines
5.3 KiB
Text

---
import { GITHUB_URL } from '../data/site';
interface Props {
targetId: string;
label?: string;
variant?: 'icon' | 'text';
class?: string;
withAttribution?: boolean;
nudge?: boolean;
copyData?: Record<string, unknown> | null;
}
const { targetId, label = 'Copy', variant = 'icon', class: extraClass = '', withAttribution = false, nudge = false, copyData = null } = Astro.props;
const btnId = `copy-btn-${targetId}`;
const nudgeId = `star-nudge-${btnId}`;
---
{variant === 'icon' ? (
<span class={`inline-flex items-center gap-1 ${extraClass}`}>
<button
id={btnId}
data-copy-target={targetId}
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>
<svg class="check-icon w-3.5 h-3.5 hidden text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<span class="copy-label sr-only">Copy</span>
<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
id={btnId}
data-copy-target={targetId}
class={`copy-btn inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:border-brand dark:hover:border-brand-dark hover:text-brand dark:hover:text-brand-dark transition-colors ${extraClass}`}
>
<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>
<svg class="check-icon w-3.5 h-3.5 hidden text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<span class="copy-label">{label}</span>
<span class="copied-label hidden text-green-600 dark:text-green-400">Copied!</span>
</button>
)}
<script define:vars={{ btnId, nudgeId, withAttribution, nudge, GITHUB_URL, copyData }}>
const btn = document.getElementById(btnId);
if (!(btn instanceof HTMLButtonElement)) return;
if (btn.dataset.copyBound === 'true') return;
btn.dataset.copyBound = 'true';
btn.addEventListener('click', async () => {
const targetId = btn.dataset.copyTarget;
if (!targetId) return;
const target = document.getElementById(targetId);
if (!target) return;
let text = target.textContent ?? '';
text = withAttribution
? `# Source: ${GITHUB_URL}\n${text.trim()}`
: text.trim();
try {
await navigator.clipboard.writeText(text);
} catch {
// Fallback for older browsers
const ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
// Visual feedback
const copyIcon = btn.querySelector('.copy-icon');
const checkIcon = btn.querySelector('.check-icon');
const copyLabel = btn.querySelector('.copy-label');
const copiedLabel = btn.querySelector('.copied-label');
copyIcon?.classList.add('hidden');
checkIcon?.classList.remove('hidden');
copyLabel?.classList.add('hidden');
copiedLabel?.classList.remove('hidden');
setTimeout(() => {
copyIcon?.classList.remove('hidden');
checkIcon?.classList.add('hidden');
copyLabel?.classList.remove('hidden');
copiedLabel?.classList.add('hidden');
}, 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);
}
}
if (copyData) {
window.dispatchEvent(new CustomEvent('apa-copy', { detail: copyData }));
}
window.dispatchEvent(new CustomEvent('copy-success'));
});
</script>