@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
JavaScript
;
"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);
}
}