Loading src/components/Catalog.astro +1 −0 Original line number Diff line number Diff line Loading @@ -113,6 +113,7 @@ const { items } = Astro.props; font-size: 0.9rem; margin: 0 2px; padding: 1px 5px 0px 5px; white-space: nowrap; &:hover { color: red; } Loading src/components/Listing.tsx +62 −67 Original line number Diff line number Diff line import { ChartNoAxesCombined, SquareX } from 'lucide-react'; import { alphabetically, negate, removeItem } from '@/lib/utils'; import { useEffect, useState } from 'react'; import { type CSSProperties } from 'react'; import Fuse from 'fuse.js'; // // Utility functions // const alphabetically = (a, b) => a.localeCompare(b); const formatScoreText = value => value ? `Relevancy = ${(1.0 - value).toFixed(4)}` : ''; const getParameters = key => { const search = window.location.search; const params = new URLSearchParams(search); return params?.getAll(key) || []; }; const negate = fn => { const inner = (...args) => !fn.apply(this, args); return inner; }; const removeItem = (arr, value) => { const index = arr.indexOf(value); if (index > -1) { arr.splice(index, 1); } return arr; }; const search = items => { const searchParameters = getParameters('search'); const options = { includeScore: true, isCaseSensitive: true, useExtendedSearch: false, keys: [ const SEARCH_KEYS = [ 'meta.identifier', 'meta.media.caption', 'meta.technology', Loading @@ -45,11 +21,16 @@ const search = items => { 'contact.email', 'contact.organization', 'contact.affiliation', ], ]; const getParameters = key => { const search = window.location.search; const params = new URLSearchParams(search); return params?.getAll(key) || []; }; const fuse = new Fuse(items, options); const results = fuse.search(searchParameters.join(' ')); return results; 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 updateLocation = (value, key) => { return () => { Loading @@ -64,7 +45,7 @@ const updateLocation = (value, key) => { window.location.href = location; }; }; const Details = ({ items }) => { const Details = ({ items = [] }) => { const count = items.length; const [visibleItems, setVisibleItems] = useState(count); useEffect(() => { Loading @@ -77,17 +58,23 @@ const Details = ({ items }) => { </div> ); }; const Labels = () => { const Labels = ({ item = { meta: { identifier: '', keywords: [] } } }) => { const isItemLabel = Boolean(item?.meta?.identifier.length > 0); const queryParameters = getParameters('q'); const searchParameters = getParameters('search'); const labels = [...searchParameters, ...queryParameters]; const margin = isItemLabel ? '20px 0px' : '10px 10px 10px 20px'; const style : CSSProperties = { margin, display: 'flex', flexDirection: 'row' }; const onClick = keyword => updateLocation(keyword, searchParameters.length > 0 ? 'search' : 'q'); const isSelectedKeyword = value => queryParameters.map(x => x.toLowerCase()).includes(value.toLowerCase()); const labels = isItemLabel ? item?.meta?.keywords.filter(negate(isSelectedKeyword)).sort(alphabetically) : [...searchParameters, ...queryParameters]; return ( <div className="label-container" style={{ display: 'flex', flexDirection: 'row', margin: '20px 0px' }}> <div className="label-container" style={style}> { labels.map(keyword => ( <div className="label" onClick={updateLocation(keyword, searchParameters.length > 0 ? 'search' : 'q')}> <SquareX style={{ display: 'inline-block', marginTop: '-2px' }} /> {' '} <div className="label" onClick={onClick(keyword)}> {isItemLabel ? '' : <X />} {keyword} </div> )) Loading @@ -95,15 +82,28 @@ const Labels = () => { </div> ); }; const X = () => { const style = { display: 'inline-block', marginTop: '-2px', marginRight: '4px', }; return <SquareX style={style} />; }; export default ({ items }) => { const queryParameters = getParameters('q'); const search = items => { const searchParameters = getParameters('search'); const isSelectedKeyword = value => queryParameters.map(x => x.toLowerCase()).includes(value.toLowerCase()); 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 options = { includeScore: true, isCaseSensitive: true, useExtendedSearch: false, keys: SEARCH_KEYS, }; const fuse = new Fuse(items, options); const results = fuse.search(searchParameters.join(' ')); return results; }; const formatScoreText = value => value ? `Relevancy = ${(1.0 - value).toFixed(4)}` : ''; const html = item => { return ( <div className="item-container"> Loading @@ -113,13 +113,7 @@ export default ({ items }) => { </h4> {item?.score && <span className="search-score">{formatScoreText(item?.score)}</span>} </div> <div className="label-container" style={{ display: 'flex', flexDirection: 'row', margin: '10px 10px 10px 20px' }}> { item.meta?.keywords.filter(negate(isSelectedKeyword)).sort(alphabetically).map(keyword => ( <div className="label" onClick={updateLocation(keyword, 'q')}>{keyword}</div> )) } </div> <Labels item={item} /> <p>{ item?.sections?.mission }</p> </div> ); Loading @@ -130,9 +124,10 @@ export default ({ items }) => { <Labels /> <div style={{ height: '10px' }}></div> { searchParameters.length > 0 getParameters('search').length > 0 // @ts-expect-error Only objects can use spread ? search(items).map(({ item, score }) => ({ ...item, score })).map(html) : queryParameters.length > 0 : getParameters('q').length > 0 ? items.filter(hasSelectedKeywords).map(html) : items.map(html) } Loading src/lib/utils.ts +39 −0 Original line number Diff line number Diff line import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; /** * Compares two strings and returns a negative, zero, or positive value * indicating the result of the comparison. This is the same as the * `String.prototype.localeCompare()` method. * * @param {string} a The first string * @param {string} b The second string * @returns {number} A negative, zero, or positive value indicating the result * of the comparison. */ export function alphabetically(a, b) { return a.localeCompare(b); } // Used by shadcn/ui components export function cn(...inputs : ClassValue[]) { return twMerge(clsx(inputs)); } /** * Returns a new function that negates the result of the given function. * * @param {Function} fn - The function whose result is to be negated. * @returns {Function} A new function that returns the negated result of `fn`. */ export function negate(fn) { const inner = (...args) => !fn.apply(this, args); return inner; }; /** * Removes the given value from the given array in-place * * @param {any[]} arr The array to modify * @param {any} value The value to remove * @returns The modified array */ export function removeItem(arr, value) { const index = arr.indexOf(value); if (index > -1) { arr.splice(index, 1); } return arr; }; src/pages/index.astro +1 −1 Original line number Diff line number Diff line Loading @@ -17,7 +17,7 @@ const items = [...projects]; <Layout title="Research Activity Index"> <Header title="Research Activity Index" subtitle="A catalog of research projects and organizations at ORNL" subtitle="A catalog of research at ORNL" /> <main> <article> Loading Loading
src/components/Catalog.astro +1 −0 Original line number Diff line number Diff line Loading @@ -113,6 +113,7 @@ const { items } = Astro.props; font-size: 0.9rem; margin: 0 2px; padding: 1px 5px 0px 5px; white-space: nowrap; &:hover { color: red; } Loading
src/components/Listing.tsx +62 −67 Original line number Diff line number Diff line import { ChartNoAxesCombined, SquareX } from 'lucide-react'; import { alphabetically, negate, removeItem } from '@/lib/utils'; import { useEffect, useState } from 'react'; import { type CSSProperties } from 'react'; import Fuse from 'fuse.js'; // // Utility functions // const alphabetically = (a, b) => a.localeCompare(b); const formatScoreText = value => value ? `Relevancy = ${(1.0 - value).toFixed(4)}` : ''; const getParameters = key => { const search = window.location.search; const params = new URLSearchParams(search); return params?.getAll(key) || []; }; const negate = fn => { const inner = (...args) => !fn.apply(this, args); return inner; }; const removeItem = (arr, value) => { const index = arr.indexOf(value); if (index > -1) { arr.splice(index, 1); } return arr; }; const search = items => { const searchParameters = getParameters('search'); const options = { includeScore: true, isCaseSensitive: true, useExtendedSearch: false, keys: [ const SEARCH_KEYS = [ 'meta.identifier', 'meta.media.caption', 'meta.technology', Loading @@ -45,11 +21,16 @@ const search = items => { 'contact.email', 'contact.organization', 'contact.affiliation', ], ]; const getParameters = key => { const search = window.location.search; const params = new URLSearchParams(search); return params?.getAll(key) || []; }; const fuse = new Fuse(items, options); const results = fuse.search(searchParameters.join(' ')); return results; 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 updateLocation = (value, key) => { return () => { Loading @@ -64,7 +45,7 @@ const updateLocation = (value, key) => { window.location.href = location; }; }; const Details = ({ items }) => { const Details = ({ items = [] }) => { const count = items.length; const [visibleItems, setVisibleItems] = useState(count); useEffect(() => { Loading @@ -77,17 +58,23 @@ const Details = ({ items }) => { </div> ); }; const Labels = () => { const Labels = ({ item = { meta: { identifier: '', keywords: [] } } }) => { const isItemLabel = Boolean(item?.meta?.identifier.length > 0); const queryParameters = getParameters('q'); const searchParameters = getParameters('search'); const labels = [...searchParameters, ...queryParameters]; const margin = isItemLabel ? '20px 0px' : '10px 10px 10px 20px'; const style : CSSProperties = { margin, display: 'flex', flexDirection: 'row' }; const onClick = keyword => updateLocation(keyword, searchParameters.length > 0 ? 'search' : 'q'); const isSelectedKeyword = value => queryParameters.map(x => x.toLowerCase()).includes(value.toLowerCase()); const labels = isItemLabel ? item?.meta?.keywords.filter(negate(isSelectedKeyword)).sort(alphabetically) : [...searchParameters, ...queryParameters]; return ( <div className="label-container" style={{ display: 'flex', flexDirection: 'row', margin: '20px 0px' }}> <div className="label-container" style={style}> { labels.map(keyword => ( <div className="label" onClick={updateLocation(keyword, searchParameters.length > 0 ? 'search' : 'q')}> <SquareX style={{ display: 'inline-block', marginTop: '-2px' }} /> {' '} <div className="label" onClick={onClick(keyword)}> {isItemLabel ? '' : <X />} {keyword} </div> )) Loading @@ -95,15 +82,28 @@ const Labels = () => { </div> ); }; const X = () => { const style = { display: 'inline-block', marginTop: '-2px', marginRight: '4px', }; return <SquareX style={style} />; }; export default ({ items }) => { const queryParameters = getParameters('q'); const search = items => { const searchParameters = getParameters('search'); const isSelectedKeyword = value => queryParameters.map(x => x.toLowerCase()).includes(value.toLowerCase()); 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 options = { includeScore: true, isCaseSensitive: true, useExtendedSearch: false, keys: SEARCH_KEYS, }; const fuse = new Fuse(items, options); const results = fuse.search(searchParameters.join(' ')); return results; }; const formatScoreText = value => value ? `Relevancy = ${(1.0 - value).toFixed(4)}` : ''; const html = item => { return ( <div className="item-container"> Loading @@ -113,13 +113,7 @@ export default ({ items }) => { </h4> {item?.score && <span className="search-score">{formatScoreText(item?.score)}</span>} </div> <div className="label-container" style={{ display: 'flex', flexDirection: 'row', margin: '10px 10px 10px 20px' }}> { item.meta?.keywords.filter(negate(isSelectedKeyword)).sort(alphabetically).map(keyword => ( <div className="label" onClick={updateLocation(keyword, 'q')}>{keyword}</div> )) } </div> <Labels item={item} /> <p>{ item?.sections?.mission }</p> </div> ); Loading @@ -130,9 +124,10 @@ export default ({ items }) => { <Labels /> <div style={{ height: '10px' }}></div> { searchParameters.length > 0 getParameters('search').length > 0 // @ts-expect-error Only objects can use spread ? search(items).map(({ item, score }) => ({ ...item, score })).map(html) : queryParameters.length > 0 : getParameters('q').length > 0 ? items.filter(hasSelectedKeywords).map(html) : items.map(html) } Loading
src/lib/utils.ts +39 −0 Original line number Diff line number Diff line import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; /** * Compares two strings and returns a negative, zero, or positive value * indicating the result of the comparison. This is the same as the * `String.prototype.localeCompare()` method. * * @param {string} a The first string * @param {string} b The second string * @returns {number} A negative, zero, or positive value indicating the result * of the comparison. */ export function alphabetically(a, b) { return a.localeCompare(b); } // Used by shadcn/ui components export function cn(...inputs : ClassValue[]) { return twMerge(clsx(inputs)); } /** * Returns a new function that negates the result of the given function. * * @param {Function} fn - The function whose result is to be negated. * @returns {Function} A new function that returns the negated result of `fn`. */ export function negate(fn) { const inner = (...args) => !fn.apply(this, args); return inner; }; /** * Removes the given value from the given array in-place * * @param {any[]} arr The array to modify * @param {any} value The value to remove * @returns The modified array */ export function removeItem(arr, value) { const index = arr.indexOf(value); if (index > -1) { arr.splice(index, 1); } return arr; };
src/pages/index.astro +1 −1 Original line number Diff line number Diff line Loading @@ -17,7 +17,7 @@ const items = [...projects]; <Layout title="Research Activity Index"> <Header title="Research Activity Index" subtitle="A catalog of research projects and organizations at ORNL" subtitle="A catalog of research at ORNL" /> <main> <article> Loading