zyros
Version:
A developer-friendly static site generator built with Next.js and Tailwind CSS. Transform a simple JSON file into a beautiful, fast static website.
212 lines (181 loc) • 5.34 kB
text/typescript
export interface AnalyticsEvent {
type: 'page_view' | 'scroll_depth' | 'reading_time' | 'search' | 'theme_change' | 'share';
page?: string;
data?: Record<string, unknown>;
timestamp: number;
}
export interface PageAnalytics {
slug: string;
title: string;
views: number;
averageReadingTime: number;
scrollDepth: number;
shares: number;
lastViewed: number;
}
class Analytics {
private events: AnalyticsEvent[] = [];
private isClient = typeof window !== 'undefined';
constructor() {
if (this.isClient) {
this.loadStoredEvents();
this.setupBeforeUnload();
}
}
private loadStoredEvents() {
try {
const stored = localStorage.getItem('zyros-analytics');
if (stored) {
this.events = JSON.parse(stored);
}
} catch (error) {
console.warn('Failed to load analytics data:', error);
}
}
private saveEvents() {
if (!this.isClient) return;
try {
// Keep only last 1000 events to prevent storage bloat
const recentEvents = this.events.slice(-1000);
localStorage.setItem('zyros-analytics', JSON.stringify(recentEvents));
} catch (error) {
console.warn('Failed to save analytics data:', error);
}
}
private setupBeforeUnload() {
window.addEventListener('beforeunload', () => {
this.saveEvents();
});
}
track(event: Omit<AnalyticsEvent, 'timestamp'>) {
const fullEvent: AnalyticsEvent = {
...event,
timestamp: Date.now()
};
this.events.push(fullEvent);
this.saveEvents();
}
trackPageView(page: string) {
this.track({
type: 'page_view',
page,
data: {
referrer: document.referrer,
userAgent: navigator.userAgent,
viewport: {
width: window.innerWidth,
height: window.innerHeight
}
}
});
}
trackScrollDepth(page: string, depth: number) {
this.track({
type: 'scroll_depth',
page,
data: { depth }
});
}
trackReadingTime(page: string, timeSpent: number) {
this.track({
type: 'reading_time',
page,
data: { timeSpent }
});
}
trackSearch(query: string, results: number) {
this.track({
type: 'search',
data: { query, results }
});
}
trackThemeChange(theme: string) {
this.track({
type: 'theme_change',
data: { theme }
});
}
trackShare(page: string, platform: string) {
this.track({
type: 'share',
page,
data: { platform }
});
}
getPageAnalytics(): PageAnalytics[] {
const pageStats = new Map<string, PageAnalytics>();
this.events.forEach(event => {
if (!event.page) return;
if (!pageStats.has(event.page)) {
pageStats.set(event.page, {
slug: event.page,
title: event.page,
views: 0,
averageReadingTime: 0,
scrollDepth: 0,
shares: 0,
lastViewed: 0
});
}
const stats = pageStats.get(event.page)!;
switch (event.type) {
case 'page_view':
stats.views++;
stats.lastViewed = Math.max(stats.lastViewed, event.timestamp);
break;
case 'reading_time':
const timeSpent = (event.data?.timeSpent as number) || 0;
stats.averageReadingTime = (stats.averageReadingTime + timeSpent) / 2;
break;
case 'scroll_depth':
const depth = (event.data?.depth as number) || 0;
stats.scrollDepth = Math.max(stats.scrollDepth, depth);
break;
case 'share':
stats.shares++;
break;
}
});
return Array.from(pageStats.values()).sort((a, b) => b.views - a.views);
}
getSearchAnalytics() {
const searches = this.events.filter(e => e.type === 'search');
const queries = new Map<string, number>();
searches.forEach(search => {
const query = (search.data?.query as string)?.toLowerCase() || '';
queries.set(query, (queries.get(query) || 0) + 1);
});
return Array.from(queries.entries())
.map(([query, count]) => ({ query, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 20);
}
getThemeAnalytics() {
const themeChanges = this.events.filter(e => e.type === 'theme_change');
const themes = new Map<string, number>();
themeChanges.forEach(change => {
const theme = (change.data?.theme as string) || '';
themes.set(theme, (themes.get(theme) || 0) + 1);
});
return Array.from(themes.entries())
.map(([theme, count]) => ({ theme, count }))
.sort((a, b) => b.count - a.count);
}
getTotalViews(): number {
return this.events.filter(e => e.type === 'page_view').length;
}
getUniquePages(): number {
const pages = new Set(this.events.filter(e => e.page).map(e => e.page));
return pages.size;
}
exportData(): string {
return JSON.stringify(this.events, null, 2);
}
clearData() {
this.events = [];
if (this.isClient) {
localStorage.removeItem('zyros-analytics');
}
}
}
export const analytics = new Analytics();