webapp/src/components/Nav/SearchModal/SearchModal.tsx

197 lines
5.9 KiB
TypeScript
Raw Normal View History

2024-01-25 18:19:59 +00:00
import type { Shout } from '../../../graphql/schema/core.gen'
2024-02-04 11:25:21 +00:00
import { For, Show, createResource, createSignal, onCleanup } from 'solid-js'
2024-01-28 05:57:05 +00:00
import { debounce } from 'throttle-debounce'
2023-12-26 10:05:15 +00:00
2024-01-25 18:19:59 +00:00
import { useLocalize } from '../../../context/localize'
2024-01-28 05:57:05 +00:00
import { loadShoutsSearch } from '../../../stores/zine/articles'
2024-01-29 09:10:30 +00:00
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
import { byScore } from '../../../utils/sortby'
2024-02-04 11:25:21 +00:00
import { FEED_PAGE_SIZE } from '../../Views/Feed/Feed'
2024-01-25 15:06:26 +00:00
import { Button } from '../../_shared/Button'
2023-11-02 19:21:51 +00:00
import { Icon } from '../../_shared/Icon'
2024-01-25 15:06:26 +00:00
2024-01-25 18:19:59 +00:00
import { SearchResultItem } from './SearchResultItem'
2023-11-30 08:50:29 +00:00
import styles from './SearchModal.module.scss'
2023-11-02 19:21:51 +00:00
2024-01-25 15:06:26 +00:00
// @@TODO handle empty article options after backend support (subtitle, cover, etc.)
// @@TODO implement load more
// @@TODO implement FILTERS & TOPICS
2024-01-28 05:57:05 +00:00
// @@TODO use save/restoreScrollPosition if needed
2024-01-25 15:06:26 +00:00
const getSearchCoincidences = ({ str, intersection }: { str: string; intersection: string }) =>
2024-01-25 18:19:59 +00:00
`<span>${str.replaceAll(
2024-01-25 15:06:26 +00:00
new RegExp(intersection, 'gi'),
(casePreservedMatch) => `<span class="blackModeIntersection">${casePreservedMatch}</span>`,
)}</span>`
2024-01-28 05:57:05 +00:00
const prepareSearchResults = (list: Shout[], searchValue: string) =>
list.sort(byScore()).map((article, index) => ({
2024-01-25 15:06:26 +00:00
...article,
id: index,
title: article.title
? getSearchCoincidences({
str: article.title,
intersection: searchValue,
})
: '',
subtitle: article.subtitle
? getSearchCoincidences({
str: article.subtitle,
intersection: searchValue,
})
: '',
}))
2023-11-02 19:21:51 +00:00
export const SearchModal = () => {
const { t } = useLocalize()
2024-01-25 19:16:38 +00:00
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
2024-01-25 15:06:26 +00:00
const [inputValue, setInputValue] = createSignal('')
const [isLoading, setIsLoading] = createSignal(false)
2024-01-28 07:06:31 +00:00
const [offset, setOffset] = createSignal<number>(0)
2024-01-28 05:57:05 +00:00
const [searchResultsList, { refetch: loadSearchResults, mutate: setSearchResultsList }] = createResource<
2024-01-28 08:19:04 +00:00
Shout[] | null
2024-01-28 05:57:05 +00:00
>(
async () => {
setIsLoading(true)
2024-02-03 08:30:01 +00:00
saveScrollPosition()
2024-01-28 05:57:05 +00:00
const { hasMore, newShouts } = await loadShoutsSearch({
2024-01-25 19:16:38 +00:00
limit: FEED_PAGE_SIZE,
2024-01-28 05:57:05 +00:00
text: inputValue(),
2024-01-28 07:06:31 +00:00
offset: offset(),
2024-01-25 19:16:38 +00:00
})
2024-01-28 07:06:31 +00:00
setIsLoading(false)
setOffset(newShouts.length)
2024-01-25 19:16:38 +00:00
setIsLoadMoreButtonVisible(hasMore)
2024-01-28 05:57:05 +00:00
return newShouts
},
{
ssrLoadFrom: 'initial',
2024-01-28 08:19:04 +00:00
initialValue: null,
2024-01-28 05:57:05 +00:00
},
)
2024-01-25 15:06:26 +00:00
2024-01-25 19:16:38 +00:00
let searchEl: HTMLInputElement
2024-01-28 05:57:05 +00:00
const debouncedLoadMore = debounce(500, loadSearchResults)
2024-01-25 15:06:26 +00:00
2024-01-28 07:06:31 +00:00
const handleQueryInput = async () => {
setInputValue(searchEl.value)
if (searchEl.value?.length > 2) {
await debouncedLoadMore()
} else {
setIsLoading(false)
2024-01-28 08:19:04 +00:00
setSearchResultsList(null)
2024-01-28 07:06:31 +00:00
}
2024-01-25 19:16:38 +00:00
}
2024-01-25 15:06:26 +00:00
2024-01-28 07:06:31 +00:00
const enterQuery = async (ev: KeyboardEvent) => {
2024-01-25 19:16:38 +00:00
setIsLoading(true)
2024-01-28 07:06:31 +00:00
if (ev.key === 'Enter' && inputValue().length > 2) {
await debouncedLoadMore()
2024-01-25 19:16:38 +00:00
} else {
2024-01-28 07:06:31 +00:00
setIsLoading(false)
2024-01-28 08:19:04 +00:00
setSearchResultsList(null)
2024-01-25 15:06:26 +00:00
}
2024-01-25 19:16:38 +00:00
restoreScrollPosition()
setIsLoading(false)
2023-11-02 19:21:51 +00:00
}
2024-01-25 15:06:26 +00:00
2024-01-28 07:06:31 +00:00
// Cleanup the debounce timer when the component unmounts
onCleanup(() => {
debouncedLoadMore.cancel()
2024-01-28 08:19:04 +00:00
// console.debug('[SearchModal] cleanup debouncing search')
2024-01-28 07:06:31 +00:00
})
2023-11-02 19:21:51 +00:00
return (
2024-01-25 15:06:26 +00:00
<div class={styles.searchContainer}>
2023-11-02 19:21:51 +00:00
<input
2024-01-25 15:06:26 +00:00
type="search"
2023-11-02 19:21:51 +00:00
placeholder={t('Site search')}
2024-01-25 15:06:26 +00:00
class={styles.searchInput}
2024-01-28 05:57:05 +00:00
onInput={handleQueryInput}
2024-01-28 07:06:31 +00:00
onKeyDown={enterQuery}
2024-01-25 19:16:38 +00:00
ref={searchEl}
2024-01-25 15:06:26 +00:00
/>
<Button
class={styles.searchButton}
2024-01-28 07:06:31 +00:00
onClick={debouncedLoadMore}
2024-01-25 15:06:26 +00:00
value={isLoading() ? <div class={styles.searchLoader} /> : <Icon name="search" />}
/>
<p
class={styles.searchDescription}
innerHTML={t(
'To find publications, art, comments, authors and topics of interest to you, just start typing your query',
)}
2023-11-02 19:21:51 +00:00
/>
2024-01-25 15:06:26 +00:00
<Show when={!isLoading()}>
2024-01-28 05:57:05 +00:00
<Show when={searchResultsList()}>
<For each={prepareSearchResults(searchResultsList(), inputValue())}>
2024-01-25 15:06:26 +00:00
{(article: Shout) => (
<div>
<SearchResultItem
article={article}
settings={{
isFloorImportant: true,
isSingle: true,
nodate: true,
}}
/>
</div>
)}
</For>
2024-01-25 19:16:38 +00:00
<Show when={isLoadMoreButtonVisible()}>
2024-01-25 15:06:26 +00:00
<p class="load-more-container">
2024-01-28 05:57:05 +00:00
<button class="button" onClick={loadSearchResults}>
2024-01-25 15:06:26 +00:00
{t('Load more')}
</button>
</p>
2024-01-25 19:16:38 +00:00
</Show>
2024-01-25 15:06:26 +00:00
</Show>
2024-01-28 08:19:04 +00:00
<Show when={Array.isArray(searchResultsList()) && searchResultsList().length === 0}>
2024-01-25 15:06:26 +00:00
<p class={styles.searchDescription} innerHTML={t("We couldn't find anything for your request")} />
</Show>
</Show>
{/* @@TODO handle filter */}
{/* <Show when={FILTERS.length}>
<div class={styles.filterResults}>
<For each={FILTERS}>
{(filter) => (
<button
type="button"
class={styles.filterResultsControl}
onClick={() => setActiveFilter(filter)}
>
{filter.name}
</button>
)}
</For>
</div>
</Show> */}
{/* @@TODO handle topics */}
{/* <Show when={TOPICS.length}>
<div class="container-xl">
<div class="row">
<div class={clsx('col-md-18 offset-md-2', styles.topicsList)}>
<For each={TOPICS}>
{(topic) => (
<button type="button" class={styles.topTopic} onClick={() => setActiveTopic(topic)}>
{topic.name}
</button>
)}
</For>
</div>
2023-11-02 19:21:51 +00:00
</div>
</div>
2024-01-25 15:06:26 +00:00
</Show> */}
</div>
2023-11-02 19:21:51 +00:00
)
}