Skip to content

Commit

Permalink
refactor: implement work category faceting through dedicated request …
Browse files Browse the repository at this point in the history
…instead of redundant yeartitle field
  • Loading branch information
kevinstadler committed Jan 8, 2025
1 parent 127dd30 commit 7b2887c
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -1,55 +1,87 @@
"use client";
import type { RefinementListItem } from "instantsearch.js/es/connectors/refinement-list/connectRefinementList";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";

import { Results } from "@/components/instantsearch/results";
import { SingleRefinementDropdown } from "@/components/instantsearch/single-refinement-dropdown";
import { SingleRefinementList } from "@/components/instantsearch/single-refinement-list";
import { ThomasBernhardInstantSearchProvider } from "@/components/instantsearch/thomas-bernhard/thomasbernhard-instantsearchprovider";
import { MainContent } from "@/components/main-content";
import { getWorks } from "@/lib/data";
import type { BernhardWork, Category } from "@/lib/model";

interface WorksPageProps {
params: {
category: string;
id?: string;
};
}

export default function WorksPage(props: WorksPageProps) {
const ct = useTranslations("BernhardCategories");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const categoryLabel = ct(props.params.category as any);
// TODO validate category
const t = useTranslations("InstantSearch");
const ct = useTranslations("BernhardCategories");
const tl = useTranslations("Languages");

// TODO get id -> year+title dictionaries from backend
// TODO validate category
const category = props.params.category;
const categoryLabel = ct(category as Category);

// get id -> work info dictionary once on pageload
const [works, setWorks] = useState({} as Record<string, BernhardWork>);
useEffect(() => {
const get = async () => {
setWorks(await getWorks(category as Category));
};
// eslint-disable-next-line @typescript-eslint/no-floating-promises
get();
}, [category]);

return (
<ThomasBernhardInstantSearchProvider
filters={`contains.work.category:=${props.params.category}`}
// pageName={props.params.category} // hack
// pathnameField="contains.work.yeartitle"
queryArgsToMenuFields={{ work: "contains.work.yeartitle", language: "language" }}
filters={`contains.work.category:=${category}`}
pageName={category} // hack
pathnameField="contains.work.id"
queryArgsToMenuFields={{ language: "language" }}
>
<MainContent>
<div className="grid h-full grid-cols-[25%_75%] gap-6 px-2">
<div className="relative h-full">
<SingleRefinementList
allLabel={categoryLabel}
attribute={"contains.work.yeartitle"}
attribute={"contains.work.id"}
// format as title (year) instead of showing facet count
refinementArgs={{
// workaround like https://github.com/algolia/instantsearch/issues/2568
transformItems: (items: Array<RefinementListItem>) => {
return items
.filter((item) => {
return item.label.startsWith(props.params.category);
})
.map((item) => {
const [_category, year, title] = item.label.split("_");
item.label = Number.isNaN(parseInt(year!)) ? title! : `${title!} (${year!})`;
return item;
});
return (
items
// the refinement may contain out-of-category works which are contained in
// publications which also contain works of this category (and therefore show up
// in the filtered search)
.filter((item) => {
return item.value in works;
})
.sort((a, b) => {
const ya = works[a.value]!.year;
const yb = works[b.value]!.year;
if (!ya) {
return 1;
} else if (!yb) {
return -1;
} else {
return ya - yb;
}
})
.map((item) => {
const work = works[item.value]!;
item.label = work.short_title ?? work.title;
if (work.year) {
item.label += ` (${work.year.toString()})`;
}
return item;
})
);
},
}}
/>
Expand Down
6 changes: 1 addition & 5 deletions components/bernhard-links.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@ interface BernhardWorkProps {

export function BernhardWorkLink(props: BernhardWorkProps) {
return (
<AppNavLink
href={encodeURI(
`/works/${props.work.category!}?work=${props.work.category!}$${props.work.year ? props.work.year.toString() : "None"}$${props.work.short_title!}`,
)}
>
<AppNavLink href={encodeURI(`/works/${props.work.category!}/${props.work.id.toString()}`)}>
{props.display_title ?? props.work.title}
</AppNavLink>
);
Expand Down
10 changes: 8 additions & 2 deletions components/instantsearch-view.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"use client";

import { LayoutGrid, LayoutList } from "lucide-react";
import {
ArrowDownAZ,
CalendarArrowDown,
CalendarArrowUp,
LayoutGrid,
LayoutList,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { type ReactNode, useState } from "react";
import { Switch } from "react-aria-components";
Expand All @@ -21,8 +27,8 @@ interface InstantSearchViewProps extends Partial<ThomasBernhardInstantSearchProv
}

const sortOptions = {
"year:desc": CalendarArrowUp,
"year:asc": CalendarArrowDown,
"year:desc": CalendarArrowUp,
"title:asc": ArrowDownAZ,
};

Expand Down
2 changes: 2 additions & 0 deletions components/instantsearch/instantsearchprovider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export function InstantSearchProvider(props: InstantSearchProviderProps): ReactN
indexName={collectionName}
routing={{
router: {
// FIXME without pageName the url rewriter eats up queryargs?

// "The difference here is that routing.router takes the same options as the historyRouter."
createURL({ location, routeState, qsModule }) {
const parts = location.pathname.split("/");
Expand Down
16 changes: 14 additions & 2 deletions components/instantsearch/results.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"use client";

import { LayoutGrid, LayoutList } from "lucide-react";
import {
ArrowDownAZ,
CalendarArrowDown,
CalendarArrowUp,
LayoutGrid,
LayoutList,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { type ReactNode, useState } from "react";
import { Switch } from "react-aria-components";
Expand All @@ -16,6 +22,12 @@ interface ResultsProps {
children?: ReactNode;
}

const sortOptions = {
"year:asc": CalendarArrowDown,
"year:desc": CalendarArrowUp,
"title:asc": ArrowDownAZ,
};

export function Results(props: ResultsProps): ReactNode {
const t = useTranslations("InstantSearch");

Expand All @@ -38,7 +50,7 @@ export function Results(props: ResultsProps): ReactNode {
<span className="grow">
<InstantSearchStats />
</span>
<InstantSearchSortBy sortOptions={["year:asc", "year:desc", "title:asc"]} />
<InstantSearchSortBy sortOptions={sortOptions} />
{props.children}
<Switch
className="react-aria-Switch"
Expand Down
24 changes: 23 additions & 1 deletion lib/data.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Client } from "typesense";

import { env } from "@/config/env.config";
import type { Publication } from "@/lib/model";
import type { BernhardWork, Category, Publication } from "@/lib/model";

export const client = new Client({
nodes: [
Expand Down Expand Up @@ -77,3 +77,25 @@ export async function getPublications({
}) as unknown as Array<Publication>;
});
}

export async function getWorks(category: Category): Promise<Record<number, BernhardWork>> {
const x = await collection.documents().search({
q: "*",
filter_by: `contains.work.category:${category}`,
per_page: 250, // hard maximum
});
const works = x.hits?.reduce((accumulator: Record<number, BernhardWork>, hit) => {
const pub = hit.document as Publication;
pub.contains
.filter((translation) => {
return translation.work.category === category;
})
.forEach((translation) => {
if (!(translation.work.id in accumulator)) {
accumulator[translation.work.id] = translation.work;
}
});
return accumulator;
}, {});
return works!;
}
4 changes: 0 additions & 4 deletions scripts/3_to_typesense.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,6 @@ def del_empty_strings(o, field_names):

w["category"] = categories[w["category"]] if w["category"] else "fragments"

# helper field for the faceted listing by work
# w["yeartitle"] = f'{w["category"]}_{w["year"]}_{w["short_title"].replace(" ", "_")}'
w["yeartitle"] = f'{w["category"]}_{w["year"]}_{w["short_title"]}'

for t in translations:
t["work"] = works[t["work"] - 1]
t["translators"] = [translators[t_id - 1] for t_id in t["translators"]]
Expand Down

0 comments on commit 7b2887c

Please sign in to comment.