UNPKG

@asgerami/zemenay-blog

Version:

Plug-and-play blog system for Next.js - Get a fully functional blog running in minutes with zero configuration

306 lines (305 loc) 11.5 kB
"use strict"; "use client"; Object.defineProperty(exports, "__esModule", { value: true }); exports.trackPostView = trackPostView; exports.calculateReadingTime = calculateReadingTime; exports.getPostAnalytics = getPostAnalytics; exports.getPopularPosts = getPopularPosts; exports.getBlogAnalytics = getBlogAnalytics; exports.trackTimeOnPage = trackTimeOnPage; const supabase_1 = require("./supabase"); /** * Track a page view for a blog post */ async function trackPostView(postId, options = {}) { try { const supabase = (0, supabase_1.getSupabaseClient)(); const { trackUniqueViews = true, sessionDuration = 30 } = options; // Get visitor info const visitorId = getVisitorId(); const userAgent = navigator.userAgent; const referrer = document.referrer || "direct"; const currentTime = new Date().toISOString(); // Check if this is a unique view within the session duration if (trackUniqueViews) { const { data: recentView } = await supabase .from("post_views") .select("id") .eq("post_id", postId) .eq("visitor_id", visitorId) .gte("viewed_at", new Date(Date.now() - sessionDuration * 60 * 1000).toISOString()) .single(); if (recentView) { return { success: true }; // Don't track duplicate views } } // Insert the view record const { error } = await supabase.from("post_views").insert({ post_id: postId, visitor_id: visitorId, user_agent: userAgent, referrer: referrer, viewed_at: currentTime, }); if (error) { console.error("Error tracking view:", error); return { success: false, error: error.message }; } return { success: true }; } catch (error) { console.error("Error tracking post view:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * Calculate reading time for a blog post */ function calculateReadingTime(content) { const wordsPerMinute = 200; // Average reading speed const words = content.replace(/<[^>]*>/g, "").split(/\s+/).length; return Math.ceil(words / wordsPerMinute); } /** * Get analytics data for a specific post */ async function getPostAnalytics(postId) { try { const supabase = (0, supabase_1.getSupabaseClient)(); // Get total views const { data: viewsData, error: viewsError } = await supabase .from("post_views") .select("id, viewed_at, referrer") .eq("post_id", postId); if (viewsError) { return { success: false, error: viewsError.message }; } // Get unique views (by visitor_id) const { data: uniqueViewsData, error: uniqueViewsError } = await supabase .from("post_views") .select("visitor_id") .eq("post_id", postId); if (uniqueViewsError) { return { success: false, error: uniqueViewsError.message }; } // Get post content for reading time const { data: postData, error: postError } = await supabase .from("posts") .select("content") .eq("id", postId) .single(); if (postError) { return { success: false, error: postError.message }; } // Calculate metrics const totalViews = (viewsData === null || viewsData === void 0 ? void 0 : viewsData.length) || 0; const uniqueViews = new Set(uniqueViewsData === null || uniqueViewsData === void 0 ? void 0 : uniqueViewsData.map((v) => v.visitor_id)).size; const readingTime = calculateReadingTime((postData === null || postData === void 0 ? void 0 : postData.content) || ""); // Analyze referrers const referrers = (viewsData === null || viewsData === void 0 ? void 0 : viewsData.reduce((acc, view) => { const referrer = view.referrer || "direct"; acc[referrer] = (acc[referrer] || 0) + 1; return acc; }, {})) || {}; // Get views by date for the last 30 days const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); const viewsByDate = (viewsData === null || viewsData === void 0 ? void 0 : viewsData.filter((view) => new Date(view.viewed_at) >= thirtyDaysAgo).reduce((acc, view) => { const date = new Date(view.viewed_at) .toISOString() .split("T")[0]; acc[date] = (acc[date] || 0) + 1; return acc; }, {})) || {}; const analytics = { post_id: postId, total_views: totalViews, unique_views: uniqueViews, reading_time: readingTime, referrers, views_by_date: viewsByDate, last_updated: new Date().toISOString(), }; return { success: true, data: analytics }; } catch (error) { console.error("Error getting post analytics:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * Get popular posts based on views */ async function getPopularPosts(limit = 10, timeframe = "month") { try { const supabase = (0, supabase_1.getSupabaseClient)(); // Calculate date filter let dateFilter = null; const now = new Date(); switch (timeframe) { case "day": dateFilter = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); break; case "week": dateFilter = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(); break; case "month": dateFilter = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(); break; } // Build query let query = supabase.from("post_views").select(` post_id, posts!inner(id, title, slug, created_at) `); if (dateFilter) { query = query.gte("viewed_at", dateFilter); } const { data: viewsData, error } = await query; if (error) { return { success: false, error: error.message }; } // Count views per post const postViews = (viewsData === null || viewsData === void 0 ? void 0 : viewsData.reduce((acc, view) => { const postId = view.post_id; if (!acc[postId]) { acc[postId] = { post: view.posts, views: 0, }; } acc[postId].views++; return acc; }, {})) || {}; // Sort by views and limit const popularPosts = Object.values(postViews) .sort((a, b) => b.views - a.views) .slice(0, limit) .map((item) => (Object.assign(Object.assign({}, item.post), { view_count: item.views }))); return { success: true, data: popularPosts }; } catch (error) { console.error("Error getting popular posts:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * Get overall blog analytics */ async function getBlogAnalytics() { try { const supabase = (0, supabase_1.getSupabaseClient)(); // Get total posts const { count: totalPosts, error: postsError } = await supabase .from("posts") .select("*", { count: "exact", head: true }); if (postsError) { return { success: false, error: postsError.message }; } // Get total views const { count: totalViews, error: viewsError } = await supabase .from("post_views") .select("*", { count: "exact", head: true }); if (viewsError) { return { success: false, error: viewsError.message }; } // Get unique visitors const { data: uniqueVisitors, error: visitorsError } = await supabase .from("post_views") .select("visitor_id"); if (visitorsError) { return { success: false, error: visitorsError.message }; } const uniqueVisitorCount = new Set(uniqueVisitors === null || uniqueVisitors === void 0 ? void 0 : uniqueVisitors.map((v) => v.visitor_id)) .size; // Get views for the last 30 days const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(); const { count: recentViews, error: recentViewsError } = await supabase .from("post_views") .select("*", { count: "exact", head: true }) .gte("viewed_at", thirtyDaysAgo); if (recentViewsError) { return { success: false, error: recentViewsError.message }; } const metrics = { total_posts: totalPosts || 0, total_views: totalViews || 0, unique_visitors: uniqueVisitorCount, views_last_30_days: recentViews || 0, average_views_per_post: totalPosts ? Math.round((totalViews || 0) / totalPosts) : 0, last_updated: new Date().toISOString(), }; return { success: true, data: metrics }; } catch (error) { console.error("Error getting blog analytics:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * Generate a unique visitor ID (stored in localStorage) */ function getVisitorId() { if (typeof window === "undefined") return "server"; let visitorId = localStorage.getItem("zemenay_visitor_id"); if (!visitorId) { visitorId = "visitor_" + Math.random().toString(36).substr(2, 9) + "_" + Date.now(); localStorage.setItem("zemenay_visitor_id", visitorId); } return visitorId; } /** * Track time spent on page */ function trackTimeOnPage(postId) { const startTime = Date.now(); const cleanup = () => { const timeSpent = Math.round((Date.now() - startTime) / 1000); // in seconds // Only track if user spent more than 10 seconds on page if (timeSpent > 10) { trackEngagement(postId, "time_spent", timeSpent); } }; // Track when user leaves the page window.addEventListener("beforeunload", cleanup); window.addEventListener("pagehide", cleanup); // Return cleanup function return () => { window.removeEventListener("beforeunload", cleanup); window.removeEventListener("pagehide", cleanup); }; } /** * Track engagement events (scroll, time spent, etc.) */ async function trackEngagement(postId, eventType, value) { try { const supabase = (0, supabase_1.getSupabaseClient)(); const visitorId = getVisitorId(); await supabase.from("post_engagement").insert({ post_id: postId, visitor_id: visitorId, event_type: eventType, value: value, created_at: new Date().toISOString(), }); } catch (error) { console.error("Error tracking engagement:", error); } }