Update package dependencies, enhance README for clarity, and implement new features in the admin panel and script detail pages. Added support for collections, improved script submission previews, and refactored comment handling in the script detail view.

This commit is contained in:
2025-08-15 20:29:02 +01:00
parent 5fdfe3e790
commit ef211ebe0a
27 changed files with 3457 additions and 353 deletions

274
src/lib/api/analytics.ts Normal file
View File

@ -0,0 +1,274 @@
import { db } from '@/lib/db';
import { scriptAnalytics, scripts } from '@/lib/db/schema';
import { eq, and, gte, lte, desc, count, sql } from 'drizzle-orm';
import { generateId, ApiError } from './index';
export interface TrackEventData {
scriptId: string;
eventType: 'view' | 'download' | 'share';
userId?: string;
userAgent?: string;
ipAddress?: string;
referrer?: string;
}
export interface AnalyticsFilters {
scriptId?: string;
eventType?: string;
startDate?: Date;
endDate?: Date;
userId?: string;
}
// Track an analytics event
export async function trackEvent(data: TrackEventData) {
try {
const eventRecord = await db.insert(scriptAnalytics).values({
id: generateId(),
scriptId: data.scriptId,
eventType: data.eventType,
userId: data.userId,
userAgent: data.userAgent,
ipAddress: data.ipAddress,
referrer: data.referrer,
createdAt: new Date(),
});
// Update script counters based on event type
if (data.eventType === 'view') {
await db
.update(scripts)
.set({
viewCount: sql`${scripts.viewCount} + 1`,
})
.where(eq(scripts.id, data.scriptId));
} else if (data.eventType === 'download') {
await db
.update(scripts)
.set({
downloadCount: sql`${scripts.downloadCount} + 1`,
})
.where(eq(scripts.id, data.scriptId));
}
return { success: true };
} catch (error) {
throw new ApiError(`Failed to track event: ${error}`, 500);
}
}
// Get analytics events with filters
export async function getAnalyticsEvents(filters: AnalyticsFilters = {}) {
try {
let query = db.select().from(scriptAnalytics);
let conditions: any[] = [];
if (filters.scriptId) {
conditions.push(eq(scriptAnalytics.scriptId, filters.scriptId));
}
if (filters.eventType) {
conditions.push(eq(scriptAnalytics.eventType, filters.eventType));
}
if (filters.userId) {
conditions.push(eq(scriptAnalytics.userId, filters.userId));
}
if (filters.startDate) {
conditions.push(gte(scriptAnalytics.createdAt, filters.startDate));
}
if (filters.endDate) {
conditions.push(lte(scriptAnalytics.createdAt, filters.endDate));
}
if (conditions.length > 0) {
query = query.where(and(...conditions)) as any;
}
const events = await query.orderBy(desc(scriptAnalytics.createdAt));
return events;
} catch (error) {
throw new ApiError(`Failed to get analytics events: ${error}`, 500);
}
}
// Get analytics summary for a script
export async function getScriptAnalytics(scriptId: string, days: number = 30) {
try {
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
// Get event counts by type
const eventCounts = await db
.select({
eventType: scriptAnalytics.eventType,
count: count(scriptAnalytics.id),
})
.from(scriptAnalytics)
.where(
and(
eq(scriptAnalytics.scriptId, scriptId),
gte(scriptAnalytics.createdAt, startDate)
)
)
.groupBy(scriptAnalytics.eventType);
// Get daily activity
const dailyActivity = await db
.select({
date: sql<string>`DATE(${scriptAnalytics.createdAt})`,
eventType: scriptAnalytics.eventType,
count: count(scriptAnalytics.id),
})
.from(scriptAnalytics)
.where(
and(
eq(scriptAnalytics.scriptId, scriptId),
gte(scriptAnalytics.createdAt, startDate)
)
)
.groupBy(sql`DATE(${scriptAnalytics.createdAt})`, scriptAnalytics.eventType);
// Get referrer statistics
const referrers = await db
.select({
referrer: scriptAnalytics.referrer,
count: count(scriptAnalytics.id),
})
.from(scriptAnalytics)
.where(
and(
eq(scriptAnalytics.scriptId, scriptId),
gte(scriptAnalytics.createdAt, startDate)
)
)
.groupBy(scriptAnalytics.referrer)
.orderBy(desc(count(scriptAnalytics.id)))
.limit(10);
return {
eventCounts,
dailyActivity,
referrers,
periodDays: days,
};
} catch (error) {
throw new ApiError(`Failed to get script analytics: ${error}`, 500);
}
}
// Get platform-wide analytics (admin only)
export async function getPlatformAnalytics(days: number = 30) {
try {
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
// Total scripts and activity
const [totals] = await db
.select({
totalScripts: count(scripts.id),
approvedScripts: sql<number>`SUM(CASE WHEN ${scripts.isApproved} = 1 THEN 1 ELSE 0 END)`,
pendingScripts: sql<number>`SUM(CASE WHEN ${scripts.isApproved} = 0 THEN 1 ELSE 0 END)`,
})
.from(scripts);
// Activity by event type
const activityByType = await db
.select({
eventType: scriptAnalytics.eventType,
count: count(scriptAnalytics.id),
})
.from(scriptAnalytics)
.where(gte(scriptAnalytics.createdAt, startDate))
.groupBy(scriptAnalytics.eventType);
// Most popular scripts
const popularScripts = await db
.select({
scriptId: scriptAnalytics.scriptId,
scriptName: scripts.name,
views: count(scriptAnalytics.id),
})
.from(scriptAnalytics)
.innerJoin(scripts, eq(scriptAnalytics.scriptId, scripts.id))
.where(
and(
eq(scriptAnalytics.eventType, 'view'),
gte(scriptAnalytics.createdAt, startDate)
)
)
.groupBy(scriptAnalytics.scriptId, scripts.name)
.orderBy(desc(count(scriptAnalytics.id)))
.limit(10);
// Daily activity trends
const dailyTrends = await db
.select({
date: sql<string>`DATE(${scriptAnalytics.createdAt})`,
views: sql<number>`SUM(CASE WHEN ${scriptAnalytics.eventType} = 'view' THEN 1 ELSE 0 END)`,
downloads: sql<number>`SUM(CASE WHEN ${scriptAnalytics.eventType} = 'download' THEN 1 ELSE 0 END)`,
})
.from(scriptAnalytics)
.where(gte(scriptAnalytics.createdAt, startDate))
.groupBy(sql`DATE(${scriptAnalytics.createdAt})`)
.orderBy(sql`DATE(${scriptAnalytics.createdAt})`);
return {
totals,
activityByType,
popularScripts,
dailyTrends,
periodDays: days,
};
} catch (error) {
throw new ApiError(`Failed to get platform analytics: ${error}`, 500);
}
}
// Get user analytics
export async function getUserAnalytics(userId: string, days: number = 30) {
try {
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
// User's scripts performance
const userScriptsAnalytics = await db
.select({
scriptId: scripts.id,
scriptName: scripts.name,
views: scripts.viewCount,
downloads: scripts.downloadCount,
rating: scripts.rating,
ratingCount: scripts.ratingCount,
})
.from(scripts)
.where(eq(scripts.authorId, userId))
.orderBy(desc(scripts.viewCount));
// Recent activity on user's scripts
const recentActivity = await db
.select({
eventType: scriptAnalytics.eventType,
count: count(scriptAnalytics.id),
})
.from(scriptAnalytics)
.innerJoin(scripts, eq(scriptAnalytics.scriptId, scripts.id))
.where(
and(
eq(scripts.authorId, userId),
gte(scriptAnalytics.createdAt, startDate)
)
)
.groupBy(scriptAnalytics.eventType);
return {
userScripts: userScriptsAnalytics,
recentActivity,
periodDays: days,
};
} catch (error) {
throw new ApiError(`Failed to get user analytics: ${error}`, 500);
}
}