feat: add server-side pagination, search, and filtering to all collection and admin pages

- Public pages (videos, magazine, models): URL-driven search, sort, category/duration
  filters, and Prev/Next pagination (page size 24)
- Admin tables (videos, articles): search input, toggle filters, and pagination (page size 50)
- Tags page: tag filtering now done server-side via DB arrayContains query instead of
  fetching all items and filtering client-side
- Backend resolvers updated for videos, articles, models with paginated { items, total }
  responses and filter/sort/tag args

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 10:43:26 +01:00
parent c90c09da9a
commit 9c5dba5c90
17 changed files with 1159 additions and 496 deletions

View File

@@ -1,7 +1,7 @@
import { builder } from "../builder";
import { ArticleType } from "../types/index";
import { ArticleType, ArticleListType, AdminArticleListType } from "../types/index";
import { articles, users } from "../../db/schema/index";
import { eq, and, lte, desc } from "drizzle-orm";
import { eq, and, lte, desc, asc, ilike, or, count, arrayContains } from "drizzle-orm";
import { requireAdmin } from "../../lib/acl";
async function enrichArticle(db: any, article: any) {
@@ -24,30 +24,54 @@ async function enrichArticle(db: any, article: any) {
builder.queryField("articles", (t) =>
t.field({
type: [ArticleType],
type: ArticleListType,
args: {
featured: t.arg.boolean(),
limit: t.arg.int(),
search: t.arg.string(),
category: t.arg.string(),
offset: t.arg.int(),
sortBy: t.arg.string(),
tag: t.arg.string(),
},
resolve: async (_root, args, ctx) => {
const dateFilter = lte(articles.publish_date, new Date());
const whereCondition =
args.featured !== null && args.featured !== undefined
? and(dateFilter, eq(articles.featured, args.featured))
: dateFilter;
const pageSize = args.limit ?? 24;
const offset = args.offset ?? 0;
let query = ctx.db
.select()
.from(articles)
.where(whereCondition)
.orderBy(desc(articles.publish_date));
if (args.limit) {
query = (query as any).limit(args.limit);
const conditions: any[] = [lte(articles.publish_date, new Date())];
if (args.featured !== null && args.featured !== undefined) {
conditions.push(eq(articles.featured, args.featured));
}
if (args.category) conditions.push(eq(articles.category, args.category));
if (args.tag) conditions.push(arrayContains(articles.tags, [args.tag]));
if (args.search) {
conditions.push(
or(
ilike(articles.title, `%${args.search}%`),
ilike(articles.excerpt, `%${args.search}%`),
),
);
}
const articleList = await query;
return Promise.all(articleList.map((article: any) => enrichArticle(ctx.db, article)));
const orderArgs =
args.sortBy === "name"
? [asc(articles.title)]
: args.sortBy === "featured"
? [desc(articles.featured), desc(articles.publish_date)]
: [desc(articles.publish_date)];
const where = and(...conditions);
const [articleList, totalRows] = await Promise.all([
(ctx.db.select().from(articles).where(where) as any)
.orderBy(...orderArgs)
.limit(pageSize)
.offset(offset),
ctx.db.select({ total: count() }).from(articles).where(where),
]);
const items = await Promise.all(
articleList.map((article: any) => enrichArticle(ctx.db, article)),
);
return { items, total: totalRows[0]?.total ?? 0 };
},
}),
);
@@ -76,11 +100,47 @@ builder.queryField("article", (t) =>
builder.queryField("adminListArticles", (t) =>
t.field({
type: [ArticleType],
resolve: async (_root, _args, ctx) => {
type: AdminArticleListType,
args: {
search: t.arg.string(),
category: t.arg.string(),
featured: t.arg.boolean(),
limit: t.arg.int(),
offset: t.arg.int(),
},
resolve: async (_root, args, ctx) => {
requireAdmin(ctx);
const articleList = await ctx.db.select().from(articles).orderBy(desc(articles.publish_date));
return Promise.all(articleList.map((article: any) => enrichArticle(ctx.db, article)));
const limit = args.limit ?? 50;
const offset = args.offset ?? 0;
const conditions: any[] = [];
if (args.search) {
conditions.push(
or(
ilike(articles.title, `%${args.search}%`),
ilike(articles.excerpt, `%${args.search}%`),
),
);
}
if (args.category) conditions.push(eq(articles.category, args.category));
if (args.featured !== null && args.featured !== undefined)
conditions.push(eq(articles.featured, args.featured));
const where = conditions.length > 0 ? and(...conditions) : undefined;
const [articleList, totalRows] = await Promise.all([
ctx.db
.select()
.from(articles)
.where(where)
.orderBy(desc(articles.publish_date))
.limit(limit)
.offset(offset),
ctx.db.select({ total: count() }).from(articles).where(where),
]);
const items = await Promise.all(
articleList.map((article: any) => enrichArticle(ctx.db, article)),
);
return { items, total: totalRows[0]?.total ?? 0 };
},
}),
);