Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | import { ref, readonly } from "vue";
type Action = "copy" | "download" | "cart";
interface Stats {
copies: number;
downloads: number;
carts: number;
total: number;
}
// Global cache for fetched stats
const statsCache = ref<Record<string, Stats>>({});
const isFetching = ref(false);
// Track event to server (fire-and-forget)
async function trackToServer(template: string, action: Action) {
try {
await fetch("/api/track", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ template, action }),
});
} catch {
// Silent fail - tracking is non-critical
}
}
export function useUsageStats() {
// Initialize stats for template if not in cache
function ensureStats(name: string) {
if (!statsCache.value[name]) {
statsCache.value[name] = { copies: 0, downloads: 0, carts: 0, total: 0 };
}
}
// Track copy event
function trackCopy(name: string) {
trackToServer(name, "copy");
ensureStats(name);
statsCache.value[name].copies++;
statsCache.value[name].total++;
}
// Track download event
function trackDownload(name: string) {
trackToServer(name, "download");
ensureStats(name);
statsCache.value[name].downloads++;
statsCache.value[name].total++;
}
// Track cart add event
function trackCart(name: string) {
trackToServer(name, "cart");
ensureStats(name);
statsCache.value[name].carts++;
statsCache.value[name].total++;
}
// Get stats for a template
function getStats(name: string): Stats {
return (
statsCache.value[name] || { copies: 0, downloads: 0, carts: 0, total: 0 }
);
}
// Fetch stats for multiple templates
async function fetchStats(templates: string[]) {
if (templates.length === 0 || isFetching.value) return;
isFetching.value = true;
try {
const res = await fetch(`/api/stats?templates=${templates.join(",")}`);
if (res.ok) {
const data = await res.json();
for (const [name, stats] of Object.entries(data)) {
statsCache.value[name] = stats as Stats;
}
}
} catch {
// Silent fail - stats are non-critical
} finally {
isFetching.value = false;
}
}
// Fetch all stats (for leaderboard)
async function fetchAllStats() {
if (isFetching.value) return;
isFetching.value = true;
try {
const res = await fetch("/api/stats");
if (res.ok) {
const data = await res.json();
statsCache.value = data;
}
} catch {
// Silent fail - stats are non-critical
} finally {
isFetching.value = false;
}
}
// Format count for display (1.2k, 5.4k, etc.)
function formatCount(count: number): string {
if (count >= 1000) {
return `${(count / 1000).toFixed(1)}k`;
}
return String(count);
}
return {
stats: readonly(statsCache),
trackCopy,
trackDownload,
trackCart,
getStats,
fetchStats,
fetchAllStats,
formatCount,
};
}
|