feat: auth foundation, similar tools, costs, redundancy, anonymous voting
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { db, toolsTable, ratingsTable } from "@workspace/db";
|
||||
import { requireAuth } from "../middleware/auth";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
router.get("/admin/redundancy", requireAuth, async (_req, res): Promise<void> => {
|
||||
const tools = await db.select().from(toolsTable).orderBy(toolsTable.category, toolsTable.name);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function buildStats(t: typeof toolsTable.$inferSelect) {
|
||||
const ratings = ratingsByTool.get(t.id) ?? [];
|
||||
const count = ratings.length;
|
||||
const avgU = count > 0 ? ratings.reduce((s, r) => s + r.usefulness, 0) / count : null;
|
||||
const avgUs = count > 0 ? ratings.reduce((s, r) => s + r.usability, 0) / count : null;
|
||||
return {
|
||||
id: t.id, name: t.name, category: t.category, features: t.features ?? [], tags: t.tags ?? [],
|
||||
ratingCount: count,
|
||||
avgUsefulness: avgU, avgUsability: avgUs,
|
||||
avgCombined: avgU != null && avgUs != null ? (avgU + avgUs) / 2 : null,
|
||||
};
|
||||
}
|
||||
|
||||
const grouped: Record<string, any[]> = {};
|
||||
for (const t of tools) {
|
||||
const g = grouped[t.category] ?? [];
|
||||
g.push(buildStats(t));
|
||||
grouped[t.category] = g;
|
||||
}
|
||||
|
||||
const result = Object.entries(grouped).map(([category, items]) => {
|
||||
const pairs: { a: any; b: any; overlap: number; scoreDiff: number }[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
for (let j = i + 1; j < items.length; j++) {
|
||||
const aFeat = new Set(items[i].features);
|
||||
const bFeat = new Set(items[j].features);
|
||||
const shared = [...aFeat].filter((f) => bFeat.has(f)).length;
|
||||
const total = new Set([...aFeat, ...bFeat]).size;
|
||||
const overlap = total > 0 ? Math.round((shared / total) * 100) : 0;
|
||||
const scoreDiff = (items[j].avgCombined ?? 0) - (items[i].avgCombined ?? 0);
|
||||
pairs.push({ a: items[i], b: items[j], overlap, scoreDiff });
|
||||
}
|
||||
}
|
||||
pairs.sort((a, b) => b.overlap - a.overlap || Math.abs(b.scoreDiff) - Math.abs(a.scoreDiff));
|
||||
return { category, tools: items, pairs: pairs.slice(0, 5) };
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user