Skip to content

Commit

Permalink
feat: make faceted listing use instantsearch with single refinement list
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinstadler committed Oct 7, 2024
1 parent 9392750 commit 03d55bb
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 67 deletions.
2 changes: 1 addition & 1 deletion app/languages/[language]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FacetedListing } from "@/components/faceted-listing";

export default function LanguagesPage() {
return <FacetedListing queryArgToRefinementFields={{ language: "language" }} />;
return <FacetedListing queryArgsToRefinementFields={{ language: "language" }} />;
}
18 changes: 14 additions & 4 deletions app/works/[category]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import WorksPage from "./[work]/page";
import { getTranslations } from "next-intl/server";

interface BlankWorksPageProps {
import { FacetedListing } from "@/components/faceted-listing";

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

export default function BlankWorksPage(props: BlankWorksPageProps) {
return <WorksPage params={props.params} />;
export default async function WorksPage(props: WorksPageProps) {
const t = await getTranslations("BernhardCategories");
return (
<FacetedListing
// 'category' values in the database are stored as the english category strings, not the URL slugs
// eslint-disable-next-line @typescript-eslint/no-explicit-any
filters={{ "contains.work.category": t(props.params.category as any) }}
queryArgsToRefinementFields={{ work: "contains.work.title" }}
/>
);
}
18 changes: 11 additions & 7 deletions components/faceted-listing.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { MainContent } from "@/components/main-content";

import { InstantSearch } from "./instantsearch";
import { SingleRefinementList } from "./single-refinement-list";

export interface FacetedListingProps {
queryArgToRefinementFields: Record<string, string>;
queryArgsToRefinementFields: Record<string, string>;
filters?: Record<string, string>;
}

export function FacetedListing(props: FacetedListingProps) {
// const data = await getFaceted(props.facet, safeParams.facetValue);
// const publications = data.hits?.map((h) => {
// return h.document;
// });

return (
<MainContent>
<InstantSearch queryArgToRefinementFields={props.queryArgToRefinementFields}></InstantSearch>
<InstantSearch
filters={props.filters}
queryArgsToRefinementFields={props.queryArgsToRefinementFields}
>
{Object.values(props.queryArgsToRefinementFields).map((attribute) => {
return <SingleRefinementList key={attribute} attribute={attribute} />;
})}
</InstantSearch>
</MainContent>
);
}
85 changes: 30 additions & 55 deletions components/instantsearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@
import type { UiState } from "instantsearch.js";
import { type MessageKeys, useTranslations } from "next-intl";
import { type ReactNode, useEffect, useRef } from "react";
import { Configure, RefinementList, SearchBox, SortBy, useInfiniteHits } from "react-instantsearch";
import { Configure, SearchBox, SortBy, useInfiniteHits } from "react-instantsearch";
import { InstantSearchNext } from "react-instantsearch-nextjs";
import TypesenseInstantSearchAdapter, { type SearchClient } from "typesense-instantsearch-adapter";

import { ClickablePublicationThumbnail } from "@/components/publication-cover";
import { collectionName } from "@/lib/data";
import type { Publication } from "@/lib/model";

import { InstantSearchStats } from "./instantsearch-stats";
import { PublicationGrid } from "./publication-grid";

// TODO put into props
const sortOptions = ["year:desc", "year:asc", "title:asc"];

interface InstantSearchProps {
queryArgToRefinementFields: Record<string, string>;
queryArgsToRefinementFields: Record<string, string>;
children?: ReactNode;
filters?: Record<string, string>; // ugly
}

const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
Expand Down Expand Up @@ -53,7 +55,8 @@ function InfiniteScroll(): ReactNode {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !isLastPage) {
showMore();
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
showMore && showMore();
}
});
});
Expand All @@ -79,37 +82,11 @@ function InfiniteScroll(): ReactNode {
);
}

function DefaultRefinementList({
attribute,
placeholder,
}: {
attribute: string;
placeholder: string;
}) {
return (
<RefinementList
attribute={attribute}
classNames={{
count: 'before:content-["("] after:content-[")"]',
disabledShowMore: "hidden",
labelText: "px-1",
root: "p-2",
}}
limit={1000}
searchable={true}
searchablePlaceholder={placeholder}
// showMore={true}
// showMoreLimit={Infinity}
sortBy={["name"]}
/>
);
}

type RouteState = Record<string, string | undefined>;

export function InstantSearch(props: InstantSearchProps): ReactNode {
const t = useTranslations("SearchPage");
const { children, queryArgToRefinementFields } = props;
const { children, filters, queryArgsToRefinementFields } = props;
return (
<InstantSearchNext
indexName={collectionName}
Expand All @@ -123,7 +100,7 @@ export function InstantSearch(props: InstantSearchProps): ReactNode {
route.sort = indexUiState.sortBy?.split("/").at(-1);
if (indexUiState.refinementList) {
for (const [field, values] of Object.entries(indexUiState.refinementList)) {
const queryarg = Object.entries(queryArgToRefinementFields).find(([_k, v]) => {
const queryarg = Object.entries(queryArgsToRefinementFields).find(([_k, v]) => {
return v === field;
})?.[0];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Expand All @@ -141,14 +118,16 @@ export function InstantSearch(props: InstantSearchProps): ReactNode {
sortBy: routeState.sort ? `${collectionName}/sort/${routeState.sort}` : undefined,
},
} as UiState;
Object.entries(queryArgToRefinementFields).forEach(([queryArg, field]) => {
if (routeState[queryArg]) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uiState[collectionName]!.refinementList![field] = routeState[queryArg]
.split(";")
.map(decodeURI);
}
});
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
queryArgsToRefinementFields &&
Object.entries(queryArgsToRefinementFields).forEach(([queryArg, field]) => {
if (routeState[queryArg]) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uiState[collectionName]!.refinementList![field] = routeState[queryArg]
.split(";")
.map(decodeURI);
}
});
return uiState;
},
},
Expand All @@ -157,25 +136,21 @@ export function InstantSearch(props: InstantSearchProps): ReactNode {
searchClient={searchClient}
>
<Configure
// when the facetingValue is undefined the search will return nothing, which is fine
// filters={`${props.faceting.facetingField} := ${props.faceting.facetingValue}`}
hitsPerPage={24}
filters={
filters
? Object.keys(filters)
.map((k) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return `${k}:= ${filters[k]!}`;
})
.join(" and ")
: undefined
}
/>
<div>
{Object.keys(queryArgToRefinementFields).map((attribute) => {
return (
<DefaultRefinementList
key={attribute}
attribute={attribute}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
placeholder={`${t("filter")} ${t(("filter_by." + attribute) as MessageKeys<any, any>)}`}
/>
);
})}
{children}
</div>
<div>{children}</div>
<div>
<div className="flex place-content-between">
<InstantSearchStats />
<SearchBox placeholder={t("query_placeholder")} />
sort by{" "}
<SortBy
Expand Down
27 changes: 27 additions & 0 deletions components/single-refinement-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";
import { useTranslations } from "next-intl";
import { RefinementList } from "react-instantsearch";

// a refinement list that is alphabetically ordered and only allows filtering for one value
export function SingleRefinementList({ attribute }: { attribute: string }) {
const t = useTranslations("SearchPage");
const attributePath = attribute.split(".");
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
const attributeKey = `filter_by.${attributePath[attributePath.length - 1]}`;
return (
<RefinementList
attribute={attribute}
classNames={{
count: 'before:content-["("] after:content-[")"]',
disabledShowMore: "hidden",
labelText: "px-1",
root: "p-2",
}}
limit={1000}
searchable={true}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
searchablePlaceholder={`${t("filter")} ${t(attributeKey as any)}`}
sortBy={["name"]}
/>
);
}
1 change: 1 addition & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"category": "categories",
"language": "languages",
"translator": "translators",
"title": "works",
"work": "works"
},
"query_placeholder": "search publications",
Expand Down

0 comments on commit 03d55bb

Please sign in to comment.