tercul-frontend/server/routes/filter.ts
google-labs-jules[bot] a50e42094a Implement advanced search filters UI and backend support
- Add Author, Language, Work Type, Date Range filters
- Implement Save Search Preferences
- Add backend routes for search and filtering with client-side pagination support
2025-12-01 09:22:40 +00:00

119 lines
3.5 KiB
TypeScript

import { Router } from "express";
import type { Request } from "express";
import { graphqlClient } from "../lib/graphqlClient";
import { respondWithError } from "../lib/error";
import { gql } from "graphql-request";
import type { Work } from "../../shared/generated/graphql";
const router = Router();
interface GqlRequest extends Request {
gql?: typeof graphqlClient;
}
// Define a type that includes fields we need for filtering
interface WorkWithFilters extends Pick<Work, "id" | "name" | "language" | "createdAt"> {
tags?: { id: string; name: string }[];
authors?: { id: string; name: string }[];
}
const WORKS_WITH_FILTERS_QUERY = gql`
query WorksWithFilters($search: String, $limit: Int) {
works(search: $search, limit: $limit) {
id
name
language
createdAt
tags {
id
name
}
authors {
id
name
}
}
}
`;
// GET /api/filter
router.get("/", async (req: GqlRequest, res) => {
try {
const q = req.query.q as string | undefined;
const language = req.query.language as string | undefined;
const type = req.query.type as string | undefined;
const yearStart = req.query.yearStart ? parseInt(req.query.yearStart as string) : undefined;
const yearEnd = req.query.yearEnd ? parseInt(req.query.yearEnd as string) : undefined;
const tags = req.query.tags ? (req.query.tags as string).split(",") : undefined;
const authorIds = req.query.authorIds ? (req.query.authorIds as string).split(",") : undefined;
// We ignore page param because we are doing client-side pagination for now,
// returning all matching results (up to limit).
// const page = req.query.page ? parseInt(req.query.page as string) : 1;
const client = req.gql || graphqlClient;
// Fetch a larger set to filter in memory.
const variables = {
search: q,
limit: 1000,
};
const response = await client.request<{ works: WorkWithFilters[] }>(
WORKS_WITH_FILTERS_QUERY,
variables
);
let filteredWorks = response.works;
// Filter by language
if (language) {
filteredWorks = filteredWorks.filter(w => w.language === language);
}
// Filter by type
if (type) {
filteredWorks = filteredWorks.filter(w => {
const typeLower = type.toLowerCase();
const hasTag = w.tags?.some(t => t.name.toLowerCase().includes(typeLower));
return hasTag;
});
}
// Filter by year
if (yearStart !== undefined || yearEnd !== undefined) {
filteredWorks = filteredWorks.filter(w => {
const year = new Date(w.createdAt).getFullYear();
if (yearStart !== undefined && year < yearStart) return false;
if (yearEnd !== undefined && year > yearEnd) return false;
return true;
});
}
// Filter by tags
if (tags && tags.length > 0) {
filteredWorks = filteredWorks.filter(w => {
if (!w.tags) return false;
const workTagIds = w.tags.map(t => t.id);
return tags.every(tagId => workTagIds.includes(tagId));
});
}
// Filter by authors
if (authorIds && authorIds.length > 0) {
filteredWorks = filteredWorks.filter(w => {
if (!w.authors) return false;
const workAuthorIds = w.authors.map(a => a.id);
return authorIds.some(authorId => workAuthorIds.includes(authorId));
});
}
// Return all results for client-side pagination
res.json(filteredWorks);
} catch (error) {
respondWithError(res, error, "Failed to filter works");
}
});
export default router;