Commit 96712dca authored by Wohlgemuth, Jason's avatar Wohlgemuth, Jason
Browse files

feat: Improve sorting and search results

parent 6e7a843f
Loading
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -54,6 +54,7 @@ const { items } = Astro.props;
    }
    .card-description {
        -webkit-line-clamp: 2;
        line-clamp: 2;
        -webkit-box-orient: vertical;
        line-height: 1.5rem;
        text-overflow: ellipsis;
@@ -61,7 +62,6 @@ const { items } = Astro.props;
        height: 50px;
        max-height: 50px;
        overflow: hidden;
        line-clamp: 2;
    }
    .item-container {
        transition: background-color 0.5s ease;
@@ -140,6 +140,9 @@ const { items } = Astro.props;
            color: var(--primary);
            margin: 0 5px;
        }
        & > button:disabled {
            color: #CCC;
        }
    }
    .search-score {
        color: #666;
@@ -198,6 +201,7 @@ const { items } = Astro.props;
        }
    }
    #catalog-search-input {
        border: 2px solid #CCC;
        display: inline;
        outline: none;
        position: relative;
+32 −19
Original line number Diff line number Diff line
/* eslint-disable sort-imports */
import { alphabetically, getParameters, negate, updateLocation } from '@/lib/utils';
import { ArrowDownAZ, ArrowDownZA, ChartNoAxesCombined, LayoutGrid, List, SquareX } from 'lucide-react';
import { ArrowDownAZ, ArrowDownZA, Blend, ChartNoAxesCombined, LayoutGrid, List, SquareX } from 'lucide-react';
import {
    Card,
    CardDescription,
    CardHeader,
    CardTitle,
} from '@/components/ui/card';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { type CSSProperties } from 'react';
import Fuse from 'fuse.js';

@@ -29,6 +29,12 @@ const SEARCH_KEYS = [
    'contact.organization',
    'contact.affiliation',
];
const hasParameters = type => getParameters(type).length > 0;
const hasSelectedKeywords = item => {
    const keywords = item.meta?.keywords;
    const isSelectedKeyword = value => keywords.map(x => x.toLowerCase()).includes(value.toLowerCase());
    return getParameters('q').every(isSelectedKeyword);
};
const CardItem = ({ item }) => {
    const id = item.meta.identifier;
    const src = `/data/${id}/${item.meta.media[0].contentUrl}`;
@@ -108,10 +114,10 @@ const X = () => {
    return <SquareX style={style} />;
};
export default ({ items }) => {
    const [isCard, setIsCard] = useState(true); // Card view is default
    const [sortUp, setSortUp] = useState(true);
    const [visibleItems, setVisibleItems] = useState([]);
    const search = items => {
    const [isCard, setIsCard] = useState(!(hasParameters('search') || hasParameters('q'))); // Card view is default when search and query parameters are not present
    const [sort, setSort] = useState(hasParameters('search') ? 'score' : 'alphabet'); // Sorting by relevance score is default when search parameters are present
    const [visibleItems, setVisibleItems] = useState(items);
    const search = (items, cutoff = 0.1) => {
        const searchParameters = getParameters('search');
        const options = {
            includeScore: true,
@@ -120,30 +126,37 @@ export default ({ items }) => {
            keys: SEARCH_KEYS,
        };
        const fuse = new Fuse(items, options);
        const results = fuse.search(searchParameters.join(' '));
        const results = fuse.search(searchParameters.join(' ')).filter(({ score = 0 }) => score < cutoff);
        return results;
    };
    const hasParameters = type => getParameters(type).length > 0;
    const hasSelectedKeywords = item => {
        const keywords = item.meta?.keywords;
        const isSelectedKeyword = value => keywords.map(x => x.toLowerCase()).includes(value.toLowerCase());
        return getParameters('q').every(isSelectedKeyword);
    };
    useEffect(() => {
    useMemo(() => {
        const _items = hasParameters('search')
            // @ts-expect-error Only objects can use spread
            ? search(items).map(({ item, score }) => ({ ...item, score }))
            : hasParameters('q') ? items.filter(hasSelectedKeywords) : items;
        _items.sort((a, b) => sortUp ? alphabetically(b.title, a.title) : alphabetically(a.title, b.title));
            ? search(visibleItems).map(({ item, score }) => ({ ...item, score }))
            : hasParameters('q') ? visibleItems.filter(hasSelectedKeywords) : visibleItems;
        switch (sort) {
            case 'alphabet':
                _items.sort((a, b) => alphabetically(a.title, b.title));
                break;
            case 'reverse':
                _items.sort((a, b) => alphabetically(b.title, a.title));
                break;
            case 'score':
                _items.sort((a, b) => a.score - b.score);
                break;
            default:
                _items.sort((a, b) => alphabetically(b.title, a.title));
        }
        setVisibleItems(_items);
    }, [sortUp]);
    }, [sort]);
    return (
        <>
            <Details items={items} />
            <Labels />
            <div className="list-controls">
                <button onClick={() => setSortUp(x => !x)} title={sortUp ? 'Sort Z-A' : 'Sort A-Z'}>{sortUp ? <ArrowDownZA /> : <ArrowDownAZ />}</button>
                <button onClick={() => setSort(sort === 'alphabet' ? 'reverse' : 'alphabet')} title={sort === 'alphabet' ? 'Sort Z-A' : 'Sort A-Z'}>{sort === 'alphabet' ? <ArrowDownZA /> : <ArrowDownAZ />}</button>
                <button onClick={() => setIsCard(x => !x)} title={isCard ? 'View list layout' : 'View card layout'}>{isCard ? <List /> : <LayoutGrid />}</button>
                <button onClick={() => setSort('score')} title="Sort by relevance" disabled={!hasParameters('search')}><Blend /></button>
            </div>
            <div className="items-container">
                {visibleItems.map(item => isCard ? CardItem({ item }) : ListItem({ item }))}