diff --git a/package.json b/package.json index 988290eb..7d39da8d 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "i18next-icu": "2.3.0", "intl-messageformat": "10.5.3", "just-throttle": "4.2.0", - "mailgun.js": "8.2.1" + "mailgun.js": "8.2.1", + "sanitize-html": "2.11.0" }, "devDependencies": { "@babel/core": "7.21.8", diff --git a/src/components/Feed/ArticleCard/ArticleCard.tsx b/src/components/Feed/ArticleCard/ArticleCard.tsx index 9c0eda98..7c381e96 100644 --- a/src/components/Feed/ArticleCard/ArticleCard.tsx +++ b/src/components/Feed/ArticleCard/ArticleCard.tsx @@ -1,4 +1,6 @@ import { createMemo, createSignal, For, Show } from 'solid-js' +import sanitizeHtml from 'sanitize-html' + import type { Shout } from '../../../graphql/types.gen' import { capitalize } from '../../../utils/capitalize' import { Icon } from '../../_shared/Icon' @@ -70,6 +72,14 @@ const getTitleAndSubtitle = ( return { title, subtitle } } +const sanitizeString = (html) => + sanitizeHtml(html, { + allowedTags: ['span'], + allowedAttributes: { + span: ['class'] + } + }) + export const ArticleCard = (props: ArticleCardProps) => { const { t, lang, formatDate } = useLocalize() const { user } = useSession() @@ -161,13 +171,13 @@ export const ArticleCard = (props: ArticleCardProps) => { - + - + @@ -191,7 +201,10 @@ export const ArticleCard = (props: ArticleCardProps) => { - + diff --git a/src/components/Nav/HeaderAuth.tsx b/src/components/Nav/HeaderAuth.tsx index 1678391e..49542fb9 100644 --- a/src/components/Nav/HeaderAuth.tsx +++ b/src/components/Nav/HeaderAuth.tsx @@ -124,10 +124,10 @@ export const HeaderAuth = (props: Props) => { - showModal('search')}> + - + diff --git a/src/components/Nav/SearchModal/SearchModal.tsx b/src/components/Nav/SearchModal/SearchModal.tsx index d31ce669..d118dcc0 100644 --- a/src/components/Nav/SearchModal/SearchModal.tsx +++ b/src/components/Nav/SearchModal/SearchModal.tsx @@ -1,15 +1,13 @@ -import { createSignal, Show, For } from 'solid-js' +import { createSignal, Show, For, JSX } from 'solid-js' -import { ArticleCard } from '../../Feed/ArticleCard' import { Button } from '../../_shared/Button' import { Icon } from '../../_shared/Icon' +import { SearchResultItem } from './SearchResultItem' +import { apiClient } from '../../../utils/apiClient' import type { Shout } from '../../../graphql/types.gen' -import { searchUrl } from '../../../utils/config' - import { useLocalize } from '../../../context/localize' -import { hideModal } from '../../../stores/ui' import styles from './SearchModal.module.scss' @@ -26,77 +24,68 @@ const getSearchCoincidences = ({ str, intersection }: { str: string; intersectio export const SearchModal = () => { const { t } = useLocalize() - const searchInputRef: { current: HTMLInputElement } = { current: null } - + const [inputValue, setInputValue] = createSignal('') const [searchResultsList, setSearchResultsList] = createSignal<[] | null>([]) const [isLoading, setIsLoading] = createSignal(false) // const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const handleSearch = async () => { - const searchValue = searchInputRef.current?.value || '' + const searchValue = inputValue() || '' - if (Boolean(searchValue)) { + if (Boolean(searchValue) && searchValue.length > 2) { setIsLoading(true) - await fetch(`${searchUrl}=${searchValue}`, { - method: 'GET', - headers: { - accept: 'application/json', - 'content-type': 'application/json; charset=utf-8' + try { + const response = await apiClient.getSearchResults(searchValue) + const searchResult = await response.json() + + if (searchResult.length) { + const preparedSearchResultsList = searchResult.map((article, index) => ({ + ...article, + body: '', + cover: '', + createdAt: '', + id: index, + slug: article.slug, + authors: [], + topics: [], + title: article.title + ? getSearchCoincidences({ + str: article.title, + intersection: searchValue + }) + : '', + subtitle: article.subtitle + ? getSearchCoincidences({ + str: article.subtitle, + intersection: searchValue + }) + : '' + })) + + setSearchResultsList(preparedSearchResultsList) + } else { + setSearchResultsList(null) } - }) - .then((data) => data.json()) - .then((data) => { - if (data.length) { - const preparedSearchResultsList = data.map((article, index) => ({ - ...article, - body: '', - cover: '', - createdAt: '', - id: index, - slug: article.slug, - authors: [], - topics: [], - title: article.title - ? getSearchCoincidences({ - str: article.title, - intersection: searchInputRef.current?.value || '' - }) - : '', - subtitle: article.subtitle - ? getSearchCoincidences({ - str: article.subtitle, - intersection: searchInputRef.current?.value || '' - }) - : '' - })) - - setSearchResultsList(preparedSearchResultsList) - } else { - setSearchResultsList(null) - } - }) - .catch((error) => { - console.log('search request failed', error) - }) - .finally(() => { - setIsLoading(false) - }) + } catch (error) { + console.log('search request failed', error) + } finally { + setIsLoading(false) + } } } - const handleArticleClick = () => { - hideModal() - } - return ( (searchInputRef.current = el)} class={styles.searchInput} - onInput={handleSearch} + onInput={(event) => { + setInputValue(event.target.value) + + handleSearch() + }} /> { {(article: Shout) => ( - - + { + return +} diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index 134d4658..a22c9f4a 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -61,6 +61,8 @@ import notifications from '../graphql/query/notifications' import markNotificationAsRead from '../graphql/mutation/mark-notification-as-read' import mySubscriptions from '../graphql/query/my-subscriptions' +import { searchUrl } from './config' + type ApiErrorCode = | 'unknown' | 'email_not_confirmed' @@ -397,5 +399,18 @@ export const apiClient = { getRecipients: async (options: QueryLoadRecipientsArgs) => { const resp = await privateGraphQLClient.query(loadRecipients, options).toPromise() return resp.data.loadRecipients.members + }, + + // search + getSearchResults: async (searchValue: string) => { + const resp = await fetch(`${searchUrl}/search?q=${searchValue}`, { + method: 'GET', + headers: { + accept: 'application/json', + 'content-type': 'application/json; charset=utf-8' + } + }) + + return resp } } diff --git a/src/utils/config.ts b/src/utils/config.ts index e83e9735..ee3e2d25 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -8,5 +8,5 @@ export const thumborUrl = import.meta.env.PUBLIC_THUMBOR_URL || defaultThumborUr export const SENTRY_DSN = import.meta.env.PUBLIC_SENTRY_DSN || '' -const defaultSearchUrl = 'https://search.discours.io/search?q' +const defaultSearchUrl = 'https://search.discours.io' export const searchUrl = import.meta.env.PUBLIC_SEARCH_URL || defaultSearchUrl