Skip to content

Commit

Permalink
Add search filters (#952)
Browse files Browse the repository at this point in the history
Add basic filters/facets to /find

Summary of changes
* Add new components <SearchMapping>, <FacetSidebar> and <FacetGroup>
* Translate facet labels in the backend by providing search.ts with the translator function
* The filters now has some core functionality (levels of expansion, very dumb filtering etc), there will be extensions/variations to this that we can solve with slots etc later on.
* To test OR filters (checkboxes), use libris-qa as api and use query q=*&@type=ChangeObservation&_limit=20
* Facets that are both a checkbox and a link (I think we want the link to use preloading + js independency) is tricky a11y-wise. Not sure the current solution is the best one...
  • Loading branch information
jesperengstrom authored Feb 5, 2024
1 parent c307dd1 commit d3d8a97
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 70 deletions.
9 changes: 6 additions & 3 deletions lxl-web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@
}

/* Base styles for elements */
/* html {
@apply bg-background text-primary;
html {
@apply font-sans text-base antialiased;
} */
}

a {
@apply text-accent-dark underline visited:text-[purple];
}

input {
@apply rounded-sm border border-[#000] p-1;
}
}
9 changes: 8 additions & 1 deletion lxl-web/src/lib/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { interpolate } from './interpolate';
import { defaultLocale, type LocaleCode } from './locales';
import sv from './locales/sv.js';

export type translateFn = {
(key: string, values?: { [key: string]: string }): string;
};

// always import default translation?
const loadedTranslations: Record<string, typeof sv> = {
sv
Expand All @@ -19,7 +23,10 @@ export async function getTranslator(locale: LocaleCode) {
// do we require nested keys?
throw new Error('Incorrect i11n key');
}
const [section, item] = key.split('.') as [string, string];

// split key at the first '.'
const [section, ...rest] = key.split('.') as [string, string[]];
const item = rest.join('.');

// @ts-expect-error - how to typecheck??
const localeResult = loadedTranslations[locale][section]?.[item];
Expand Down
28 changes: 26 additions & 2 deletions lxl-web/src/lib/i18n/locales/en.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
/** @type {typeof import('./sv.js').default} */
export default {
home: {},
search: {
result_info: 'Search result for: {q}'
facet: {
q: 'Free text search',
'@reverse.itemOf.heldBy.@id': 'Has holding',
'instanceOf.@type': 'Type of work',
issuanceType: 'Issuance type',
'publication.year': 'Publication year',
'instanceOf.language.@id': 'Language of work',
'@type': 'Type',
'inScheme.@id': 'Term System',
'inCollection.@id': 'Term Collection',
'nationality.@id': 'Nationality',
'language.@id': 'Language',
'genreForm.@id': 'Genre/form',
'instanceOf.genreForm.@id': 'Genre/form of work',
'contribution.agent.@id': 'Contribution or primary contribution',
'contentType.@id': 'Content type',
'carrierType.@id': 'Carrier type',
'instanceOf.subject.@id': 'Subject',
'subject.@id': 'Subject',
'intendedAudience.@id': 'Intended audience',
'meta.bibliography.@id': 'In bibliography',
'category.@id': 'Change category',
'[email protected].@id': 'Has holding',
'@reverse': 'Relation',
'meta.encodingLevel': 'Encoding level'
},
search: {},
errors: {},
general: {}
};
28 changes: 26 additions & 2 deletions lxl-web/src/lib/i18n/locales/sv.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
export default {
home: {},
search: {
result_info: 'Sökresultat för: {q}'
facet: {
q: 'Fritextsökning',
'@reverse.itemOf.heldBy.@id': 'Har bestånd',
'instanceOf.@type': 'Verkstyp',
issuanceType: 'Utgivningssätt',
'publication.year': 'Utgivningsår',
'instanceOf.language.@id': 'Verksspråk',
'@type': 'Typ',
'inScheme.@id': 'Termsystem',
'inCollection.@id': 'Termsamling',
'nationality.@id': 'Nationalitet',
'language.@id': 'Språk',
'genreForm.@id': 'Genre/form',
'instanceOf.genreForm.@id': 'Genre/form på verket',
'contribution.agent.@id': 'Medverkan eller primär medverkan',
'contentType.@id': 'Innehållstyp',
'carrierType.@id': 'Bärartyp',
'instanceOf.subject.@id': 'Ämne',
'subject.@id': 'Ämne',
'intendedAudience.@id': 'Målgrupp',
'meta.bibliography.@id': 'Ingår i bibliografi',
'category.@id': 'Ändringskategori',
'[email protected].@id': 'Har bestånd',
'@reverse': 'Relation',
'meta.encodingLevel': 'Beskrivningsnivå'
},
search: {},
errors: {},
general: {
// shared stuff go here
Expand Down
6 changes: 4 additions & 2 deletions lxl-web/src/routes/(app)/[[lang=lang]]/find/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import { redirect } from '@sveltejs/kit';
import { DisplayUtil } from '$lib/utils/xl';
import { getSupportedLocale } from '$lib/i18n/locales';
import { asResult, type PartialCollectionView } from './search';
import { getTranslator } from '$lib/i18n';

export const load = async ({ params, locals, fetch, url }) => {
if (!url.searchParams.size) {
redirect(303, `/`); // redirect to home page if no search params are given
}

const recordsRes = await fetch(`${env.API_URL}/find.jsonld?${url.searchParams.toString()}`);
const result = (await recordsRes.json()) as PartialCollectionView;
const displayUtil: DisplayUtil = locals.display;
const searchResult = asResult(result, displayUtil, getSupportedLocale(params?.lang));
const locale = getSupportedLocale(params?.lang);
const translator = await getTranslator(locale);
const searchResult = await asResult(result, displayUtil, locale, translator);

return {
searchResult
Expand Down
69 changes: 21 additions & 48 deletions lxl-web/src/routes/(app)/[[lang=lang]]/find/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,58 +1,31 @@
<script lang="ts">
import { page } from '$app/stores';
import SeachMapping from './SeachMapping.svelte';
import FacetSidebar from './FacetSidebar.svelte';
import DecoratedData from '$lib/components/DecoratedData.svelte';
import { LxlLens } from '$lib/utils/display.types';
import { relativize } from '$lib/utils/http';
export let data;
$: q = $page.url.searchParams.get('q');
</script>

<div class="m-3">
<h1 class="text-6-cond-extrabold">{$page.data.t('search.result_info', { q: `${q}` })}</h1>
<ul>
{#each data.searchResult.mapping as mapping}
<li>
<DecoratedData data={mapping.display} />
{#if 'up' in mapping}
<a class="underline" href={mapping.up['@id']}>x</a>
{/if}
</li>
{/each}
</ul>
<br />
<br />
<ul>
{#each data.searchResult.items as item (item['@id'])}
<li>
<a href={relativize(item['@id'])}
><h2 class="text-4-cond-extrabold">
<DecoratedData data={item[LxlLens.CardHeading]} />
</h2></a
>
<DecoratedData data={item[LxlLens.CardBody]} />
</li>
<br />
{/each}
</ul>
<br />
<br />
<ul>
{#each data.searchResult.facetGroups as group (group.dimension)}
<h2 class="text-3-cond-extrabold">{group.label}</h2>
<br />

{#each group.facets as facet (facet.view)}
<a href={facet.view['@id']}>
<DecoratedData data={facet.object} />
</a>
<pre>{JSON.stringify(facet)}</pre>
<br />
{/each}

<br />
<br />
{/each}
</ul>
<SeachMapping mapping={data.searchResult.mapping} />
<div class="flex">
<FacetSidebar facets={data.searchResult.facetGroups} />
<main class="w-10/12">
<ul>
{#each data.searchResult.items as item (item['@id'])}
<li>
<a href={relativize(item['@id'])}
><h2 class="text-4-cond-extrabold">
<DecoratedData data={item[LxlLens.CardHeading]} />
</h2></a
>
<DecoratedData data={item[LxlLens.CardBody]} />
</li>
<br />
{/each}
</ul>
</main>
</div>
</div>
78 changes: 78 additions & 0 deletions lxl-web/src/routes/(app)/[[lang=lang]]/find/FacetGroup.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<script lang="ts">
import type { LocaleCode } from '$lib/i18n/locales';
import { type FacetGroup } from './search';
export let group: FacetGroup;
export let locale: LocaleCode;
const defaultFacetsShown = 5;
let facetsShown = defaultFacetsShown;
let expanded = false;
let searchPhrase = '';
$: numfacets = group.facets.length;
$: filteredFacets = group.facets.filter((facet) =>
facet.str.toLowerCase().startsWith(searchPhrase.toLowerCase())
);
$: shownFacets = filteredFacets.filter((facet, index) => index < facetsShown);
$: canShowMoreFacets = filteredFacets.length > facetsShown;
$: canShowLessFacets = !canShowMoreFacets && filteredFacets.length > defaultFacetsShown;
</script>

<li class="my-4 border-b-[1px] pb-2 text-2-regular">
<button
id={'toggle-' + group.dimension}
type="button"
on:click={() => (expanded = !expanded)}
aria-expanded={!!expanded}
aria-controls={'group-' + group.dimension}
class="w-full text-left text-2-cond-bold"
>
{expanded ? '' : ''} {group.label}</button
>
<div
id={'group-' + group.dimension}
aria-labelledby={'toggle-' + group.dimension}
class:hidden={!expanded}
>
{#if numfacets > defaultFacetsShown}
<input
bind:value={searchPhrase}
class="mt-2"
placeholder="Sök {group.label.toLowerCase()}"
title="Sök {group.label.toLowerCase()}"
/>
{/if}
<ol class="mt-2">
{#each shownFacets as facet (facet.view['@id'])}
<li>
<a class="flex justify-between no-underline text-2-regular" href={facet.view['@id']}>
<span class="flex items-baseline">
{#if 'selected' in facet}
<!-- howto A11y?! -->
<span class="sr-only">{facet.selected ? 'Valt filter' : ''}</span>
<span class="mr-1 text-3-regular" aria-hidden="true"
>{facet.selected ? '' : ''}</span
>
{/if}
<span>{facet.str}</span>
</span>
<span>({facet.totalItems.toLocaleString(locale)})</span>
</a>
</li>
{/each}
</ol>
{#if canShowMoreFacets || canShowLessFacets}
<button
class="my-2"
on:click={() =>
canShowMoreFacets ? (facetsShown = numfacets) : (facetsShown = defaultFacetsShown)}
>
{canShowMoreFacets ? 'Visa fler' : 'Visa färre'}</button
>
{/if}
{#if searchPhrase && filteredFacets.length === 0}
<span role="status" aria-atomic="true">Inget resultat</span>
{/if}
</div>
</li>
15 changes: 15 additions & 0 deletions lxl-web/src/routes/(app)/[[lang=lang]]/find/FacetSidebar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script lang="ts">
import { page } from '$app/stores';
import { type FacetGroup as Group } from './search';
import FacetGroup from './FacetGroup.svelte';
export let facets: Group[];
</script>

<nav class="mr-4 w-2/12" aria-labelledby="facet-sidebar-header">
<header id="facet-sidebar-header" class="text-2-cond-bold">Filter</header>
<ol>
{#each facets as group (group.dimension)}
<FacetGroup {group} locale={$page.data.locale} />
{/each}
</ol>
</nav>
21 changes: 21 additions & 0 deletions lxl-web/src/routes/(app)/[[lang=lang]]/find/SeachMapping.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts">
import DecoratedData from '$lib/components/DecoratedData.svelte';
import type { DisplayMapping } from './search';
export let mapping: DisplayMapping[];
$: filteredMapping = mapping.filter((m) => m);
</script>

<section>
<ul class="my-4 flex flex-wrap">
{#each filteredMapping as filter}
<li class="my-2 mr-2 justify-center rounded-md bg-accent-light px-4 py-2">
<span class="text-2-regular">{filter.label}:</span>
<span class="text-2-cond-bold"><DecoratedData data={filter.display} /></span>
{#if 'up' in filter}
<a class="underline" href={filter.up?.['@id']}>x</a>
{/if}
</li>
{/each}
</ul>
</section>
Loading

0 comments on commit d3d8a97

Please sign in to comment.