feat: watchlist (premium), mobile bottom nav
Build & Push Docker Image / build (push) Successful in 2m35s
Build & Push Docker Image / build (push) Successful in 2m35s
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { Router, type IRouter, type Request } from "express";
|
||||
import { Issuer, generators, type Client } from "openid-client";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, and, inArray, isNull } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { db, usersTable } from "@workspace/db";
|
||||
import { db, usersTable, toolsTable, ratingsTable } from "@workspace/db";
|
||||
import { logger } from "../lib/logger";
|
||||
import { getEntitlements } from "../middleware/feature";
|
||||
import { getEntitlements, requireFeature } from "../middleware/feature";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
@@ -301,6 +301,7 @@ async function resolveDbUser(u: SessionUser) {
|
||||
const PreferenceSchema = z.object({
|
||||
view: z.enum(["grid", "table", "rows"]).optional(),
|
||||
density: z.enum(["cozy", "compact"]).optional(),
|
||||
watchlist: z.array(z.number().int().positive()).max(50).optional(),
|
||||
});
|
||||
|
||||
router.get("/auth/me/preferences", async (req, res): Promise<void> => {
|
||||
@@ -316,6 +317,46 @@ router.get("/auth/me/preferences", async (req, res): Promise<void> => {
|
||||
res.json(dbUser.preferences ?? {});
|
||||
});
|
||||
|
||||
router.get("/auth/me/watchlist", requireFeature("watchlist"), async (req, res): Promise<void> => {
|
||||
const dbUser = await resolveDbUser(req.session.user!);
|
||||
if (!dbUser) {
|
||||
res.status(401).json({ error: "User not found" });
|
||||
return;
|
||||
}
|
||||
const ids = (dbUser.preferences?.watchlist ?? []).filter((id) => Number.isInteger(id));
|
||||
if (ids.length === 0) {
|
||||
res.json([]);
|
||||
return;
|
||||
}
|
||||
const tools = await db
|
||||
.select()
|
||||
.from(toolsTable)
|
||||
.where(and(isNull(toolsTable.deletedAt), inArray(toolsTable.id, ids)));
|
||||
const ratings = await db
|
||||
.select({ toolId: ratingsTable.toolId, usefulness: ratingsTable.usefulness, usability: ratingsTable.usability })
|
||||
.from(ratingsTable)
|
||||
.where(inArray(ratingsTable.toolId, tools.map((t) => t.id)));
|
||||
const byTool = new Map<number, { usefulness: number; usability: number }[]>();
|
||||
for (const r of ratings) {
|
||||
const list = byTool.get(r.toolId) ?? [];
|
||||
list.push({ usefulness: r.usefulness, usability: r.usability });
|
||||
byTool.set(r.toolId, list);
|
||||
}
|
||||
const byId = new Map(tools.map((t) => [t.id, t]));
|
||||
const ordered = ids
|
||||
.filter((id) => byId.has(id))
|
||||
.map((id) => {
|
||||
const tool = byId.get(id)!;
|
||||
const toolRatings = byTool.get(id) ?? [];
|
||||
const ratingCount = toolRatings.length;
|
||||
const avgUsefulness = ratingCount > 0 ? toolRatings.reduce((s, r) => s + r.usefulness, 0) / ratingCount : null;
|
||||
const avgUsability = ratingCount > 0 ? toolRatings.reduce((s, r) => s + r.usability, 0) / ratingCount : null;
|
||||
const avgCombined = avgUsefulness != null && avgUsability != null ? (avgUsefulness + avgUsability) / 2 : null;
|
||||
return { ...tool, ratingCount, avgUsefulness, avgUsability, avgCombined };
|
||||
});
|
||||
res.json(ordered);
|
||||
});
|
||||
|
||||
router.put("/auth/me/preferences", async (req, res): Promise<void> => {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ error: "Not authenticated" });
|
||||
|
||||
Reference in New Issue
Block a user