Add API endpoints and frontend components for tool management and analytics

Implement CRUD operations for tools and ratings, introduce analytics endpoints, and develop frontend components for displaying tools, ratings, and analytics data.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 776963d0-f75d-42e2-a57b-cc36bdff8495
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: feaa4ce1-5aed-4cc0-bcea-47855b615b48
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0683fb79-a27c-485c-9333-5f4b288c4567/776963d0-f75d-42e2-a57b-cc36bdff8495/rx9K7bW
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
cheffe01
2026-05-25 13:04:53 +00:00
parent 1de22a6979
commit 036973ea32
111 changed files with 11006 additions and 96 deletions
@@ -0,0 +1,181 @@
import { Router, type IRouter } from "express";
import { eq, sql, desc } from "drizzle-orm";
import { db, toolsTable, ratingsTable } from "@workspace/db";
import {
GetTopToolsQueryParams,
GetRatingDistributionQueryParams,
} from "@workspace/api-zod";
const router: IRouter = Router();
router.get("/analytics/summary", async (_req, res): Promise<void> => {
const [toolStats] = await db
.select({ totalTools: sql<number>`count(*)::int` })
.from(toolsTable);
const [ratingStats] = await db
.select({
totalRatings: sql<number>`count(*)::int`,
avgUsefulness: sql<number | null>`avg(${ratingsTable.usefulness})`,
avgUsability: sql<number | null>`avg(${ratingsTable.usability})`,
})
.from(ratingsTable);
const [catStats] = await db
.select({ categoriesCount: sql<number>`count(distinct ${toolsTable.category})::int` })
.from(toolsTable);
const avgCombined = ratingStats.avgUsefulness != null && ratingStats.avgUsability != null
? (Number(ratingStats.avgUsefulness) + Number(ratingStats.avgUsability)) / 2
: null;
// Most-rated tool
const [mostRatedRow] = await db
.select({
toolId: ratingsTable.toolId,
ratingCount: sql<number>`count(*)::int`,
})
.from(ratingsTable)
.groupBy(ratingsTable.toolId)
.orderBy(desc(sql`count(*)`))
.limit(1);
let mostRatedTool = null;
if (mostRatedRow) {
const [tool] = await db.select().from(toolsTable).where(eq(toolsTable.id, mostRatedRow.toolId));
if (tool) {
const ratingRows = await db
.select({ usefulness: ratingsTable.usefulness, usability: ratingsTable.usability })
.from(ratingsTable)
.where(eq(ratingsTable.toolId, tool.id));
const rc = ratingRows.length;
const au = rc > 0 ? ratingRows.reduce((s, r) => s + r.usefulness, 0) / rc : null;
const aus = rc > 0 ? ratingRows.reduce((s, r) => s + r.usability, 0) / rc : null;
const ac = au != null && aus != null ? (au + aus) / 2 : null;
mostRatedTool = { ...tool, ratingCount: rc, avgUsefulness: au, avgUsability: aus, avgCombined: ac };
}
}
res.json({
totalTools: toolStats?.totalTools ?? 0,
totalRatings: ratingStats?.totalRatings ?? 0,
avgUsefulness: ratingStats?.avgUsefulness != null ? Number(ratingStats.avgUsefulness) : null,
avgUsability: ratingStats?.avgUsability != null ? Number(ratingStats.avgUsability) : null,
avgCombined,
categoriesCount: catStats?.categoriesCount ?? 0,
mostRatedTool,
});
});
router.get("/analytics/top-tools", async (req, res): Promise<void> => {
const parsed = GetTopToolsQueryParams.safeParse(req.query);
if (!parsed.success) {
res.status(400).json({ error: parsed.error.message });
return;
}
const limit = parsed.data.limit ?? 10;
const metric = parsed.data.metric ?? "combined";
const tools = await db.select().from(toolsTable);
const allRatings = await db
.select({ toolId: ratingsTable.toolId, usefulness: ratingsTable.usefulness, usability: ratingsTable.usability })
.from(ratingsTable);
const ratingsByTool = new Map<number, { usefulness: number; usability: number }[]>();
for (const r of allRatings) {
const arr = ratingsByTool.get(r.toolId) ?? [];
arr.push({ usefulness: r.usefulness, usability: r.usability });
ratingsByTool.set(r.toolId, arr);
}
const toolsWithStats = tools.map((t) => {
const ratings = ratingsByTool.get(t.id) ?? [];
const rc = ratings.length;
const au = rc > 0 ? ratings.reduce((s, r) => s + r.usefulness, 0) / rc : null;
const aus = rc > 0 ? ratings.reduce((s, r) => s + r.usability, 0) / rc : null;
const ac = au != null && aus != null ? (au + aus) / 2 : null;
return { ...t, ratingCount: rc, avgUsefulness: au, avgUsability: aus, avgCombined: ac };
});
const scored = toolsWithStats
.filter((t) => t.ratingCount > 0)
.map((t) => {
let score = 0;
if (metric === "usefulness") score = t.avgUsefulness ?? 0;
else if (metric === "usability") score = t.avgUsability ?? 0;
else score = t.avgCombined ?? 0;
return { tool: t, score, ratingCount: t.ratingCount };
})
.sort((a, b) => b.score - a.score)
.slice(0, limit);
res.json(scored);
});
router.get("/analytics/by-category", async (_req, res): Promise<void> => {
const categories = await db
.selectDistinct({ category: toolsTable.category })
.from(toolsTable);
const result = await Promise.all(
categories.map(async ({ category }) => {
const [toolCount] = await db
.select({ count: sql<number>`count(*)::int` })
.from(toolsTable)
.where(eq(toolsTable.category, category));
const [ratingStats] = await db
.select({
totalRatings: sql<number>`count(*)::int`,
avgUsefulness: sql<number | null>`avg(${ratingsTable.usefulness})`,
avgUsability: sql<number | null>`avg(${ratingsTable.usability})`,
})
.from(ratingsTable)
.innerJoin(toolsTable, eq(ratingsTable.toolId, toolsTable.id))
.where(eq(toolsTable.category, category));
return {
category,
toolCount: toolCount?.count ?? 0,
totalRatings: ratingStats?.totalRatings ?? 0,
avgUsefulness: ratingStats?.avgUsefulness != null ? Number(ratingStats.avgUsefulness) : null,
avgUsability: ratingStats?.avgUsability != null ? Number(ratingStats.avgUsability) : null,
};
})
);
res.json(result);
});
router.get("/analytics/rating-distribution", async (req, res): Promise<void> => {
const parsed = GetRatingDistributionQueryParams.safeParse(req.query);
if (!parsed.success) {
res.status(400).json({ error: parsed.error.message });
return;
}
const toolIdFilter = parsed.data.toolId;
const baseQuery = toolIdFilter
? db.select({ usefulness: ratingsTable.usefulness, usability: ratingsTable.usability })
.from(ratingsTable)
.where(eq(ratingsTable.toolId, toolIdFilter))
: db.select({ usefulness: ratingsTable.usefulness, usability: ratingsTable.usability })
.from(ratingsTable);
const allRatings = await baseQuery;
const usefulnessBuckets = [1, 2, 3, 4, 5].map((score) => ({
score,
count: allRatings.filter((r) => r.usefulness === score).length,
}));
const usabilityBuckets = [1, 2, 3, 4, 5].map((score) => ({
score,
count: allRatings.filter((r) => r.usability === score).length,
}));
res.json({ usefulness: usefulnessBuckets, usability: usabilityBuckets });
});
export default router;