All files useUsageStats.ts

0% Statements 0/42
0% Branches 0/16
0% Functions 0/10
0% Lines 0/40

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,
  };
}