v4: a profile's lists of dumps can be narrowed to a category — /@name/dumps and /@name/upvoted showed every dump, with no way to see only those filed under one category, as the index can. The title row of each list now has the index's category menu, and the category picked goes in the URL as ?category=<slug>, which the other list keeps and every other tab drops (a path segment would collide with /@name/dumps/<slug>, a dump's own page). GET /api/users/:key/dumps and /upvoted take ?category= as the followed feed does, an unknown slug answering an empty list; resolveCategoryFilter moves to category-service.ts for both. useUserDumpFeed passes everything the socket changes through the category, so a dump edited out of it leaves and an upvote outside it doesn't come in. CategorySwitcher now reports the category picked to its owner (onSelect) instead of navigating to the index's path itself, and the divider after it in the index's tab row is drawn by CSS, so the profile gets none. useTabParam's setter takes the query string to go to, the current one by default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -96,7 +96,6 @@ export function getCategoryBySlug(slug: string): Category {
|
||||
|
||||
/**
|
||||
* Returns the id of the category at `slug`, or undefined when there is none.
|
||||
* The follows routes use it for their `?category=` feed filter.
|
||||
*/
|
||||
export function getCategoryIdBySlug(slug: string): string | undefined {
|
||||
const row = db.prepare(`SELECT id FROM categories WHERE slug = ?;`).get(slug);
|
||||
@@ -105,6 +104,20 @@ export function getCategoryIdBySlug(slug: string): string | undefined {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of the category the `?category=` slug of `params` names,
|
||||
* undefined without one, or "missing" when it names none, so that the list
|
||||
* comes back empty rather than unfiltered. The follows routes filter the
|
||||
* index's "followed" tab with it, and the users routes a profile's lists.
|
||||
*/
|
||||
export function resolveCategoryFilter(
|
||||
params: URLSearchParams,
|
||||
): string | "missing" | undefined {
|
||||
const slug = params.get("category");
|
||||
if (!slug) return undefined;
|
||||
return getCategoryIdBySlug(slug) ?? "missing";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns every category, by position then slug, for `GET /api/categories`.
|
||||
*/
|
||||
|
||||
@@ -690,10 +690,24 @@ export async function replaceFileDump(
|
||||
return updatedDump;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the join that keeps only the dumps `d` filed under `categoryId`,
|
||||
* with its parameters, or nothing without one. The lists of a user's dumps
|
||||
* filter by it.
|
||||
*/
|
||||
function categoryJoin(categoryId?: string): [string, string[]] {
|
||||
return categoryId
|
||||
? [
|
||||
"INNER JOIN dump_categories dc ON dc.dump_id = d.id AND dc.category_id = ?",
|
||||
[categoryId],
|
||||
]
|
||||
: ["", []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a page of `userId`'s dumps, newest first, for
|
||||
* `GET /api/users/:key/dumps`. The route sets `includePrivate` when users
|
||||
* ask for their own.
|
||||
* `GET /api/users/:key/dumps`, optionally only those in `categoryId`. The
|
||||
* route sets `includePrivate` when users ask for their own.
|
||||
*
|
||||
* @throws {APIException} 500 when a row doesn't parse as a dump.
|
||||
*/
|
||||
@@ -702,12 +716,16 @@ export function getDumpsByUser(
|
||||
page: number,
|
||||
limit: number,
|
||||
includePrivate: boolean,
|
||||
categoryId?: string,
|
||||
): Page<Dump> {
|
||||
const offset = (page - 1) * limit;
|
||||
const privacyFilter = includePrivate ? "" : " AND is_private = 0";
|
||||
const privacyFilter = includePrivate ? "" : " AND d.is_private = 0";
|
||||
const [catJoin, catParams] = categoryJoin(categoryId);
|
||||
const rows = db.prepare(
|
||||
`SELECT ${SELECT_COLS} FROM dumps WHERE user_id = ?${privacyFilter} ORDER BY created_at DESC LIMIT ? OFFSET ?;`,
|
||||
).all(userId, limit + 1, offset);
|
||||
`SELECT ${SELECT_COLS_ALIASED} FROM dumps d ${catJoin}
|
||||
WHERE d.user_id = ?${privacyFilter}
|
||||
ORDER BY d.created_at DESC LIMIT ? OFFSET ?;`,
|
||||
).all(...catParams, userId, limit + 1, offset);
|
||||
if (!rows.every(isDumpRow)) {
|
||||
throw new APIException(500, "Malformed dump data");
|
||||
}
|
||||
@@ -718,8 +736,8 @@ export function getDumpsByUser(
|
||||
|
||||
/**
|
||||
* Returns a page of the dumps `userId` upvoted, latest vote first, for
|
||||
* `GET /api/users/:key/upvoted`. Their own private dumps are included only
|
||||
* when they are the one asking.
|
||||
* `GET /api/users/:key/upvoted`, optionally only those in `categoryId`.
|
||||
* Their own private dumps are included only when they are the one asking.
|
||||
*
|
||||
* @throws {APIException} 500 when a row doesn't parse as a dump.
|
||||
*/
|
||||
@@ -728,28 +746,28 @@ export function getVotedDumpsByUser(
|
||||
page: number,
|
||||
limit: number,
|
||||
requestingUserId: string | null,
|
||||
categoryId?: string,
|
||||
): Page<Dump> {
|
||||
const offset = (page - 1) * limit;
|
||||
const dumpCols = SELECT_COLS_ALIASED;
|
||||
let rows: unknown[];
|
||||
|
||||
if (requestingUserId === userId) {
|
||||
rows = db.prepare(
|
||||
`SELECT ${dumpCols}
|
||||
FROM dumps d
|
||||
INNER JOIN votes v ON d.id = v.dump_id
|
||||
WHERE v.user_id = ? AND (d.is_private = 0 OR d.user_id = ?)
|
||||
ORDER BY v.created_at DESC LIMIT ? OFFSET ?;`,
|
||||
).all(userId, userId, limit + 1, offset);
|
||||
} else {
|
||||
rows = db.prepare(
|
||||
`SELECT ${dumpCols}
|
||||
FROM dumps d
|
||||
INNER JOIN votes v ON d.id = v.dump_id
|
||||
WHERE v.user_id = ? AND d.is_private = 0
|
||||
ORDER BY v.created_at DESC LIMIT ? OFFSET ?;`,
|
||||
).all(userId, limit + 1, offset);
|
||||
}
|
||||
const isOwn = requestingUserId === userId;
|
||||
const privacyFilter = isOwn
|
||||
? "(d.is_private = 0 OR d.user_id = ?)"
|
||||
: "d.is_private = 0";
|
||||
const [catJoin, catParams] = categoryJoin(categoryId);
|
||||
const rows = db.prepare(
|
||||
`SELECT ${SELECT_COLS_ALIASED}
|
||||
FROM dumps d
|
||||
INNER JOIN votes v ON d.id = v.dump_id
|
||||
${catJoin}
|
||||
WHERE v.user_id = ? AND ${privacyFilter}
|
||||
ORDER BY v.created_at DESC LIMIT ? OFFSET ?;`,
|
||||
).all(
|
||||
...catParams,
|
||||
userId,
|
||||
...(isOwn ? [userId] : []),
|
||||
limit + 1,
|
||||
offset,
|
||||
);
|
||||
|
||||
if (!rows.every(isDumpRow)) {
|
||||
throw new APIException(500, "Malformed dump data");
|
||||
|
||||
Reference in New Issue
Block a user