refactor by review comments
This commit is contained in:
parent
c78d5b3337
commit
6752d35491
|
@ -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",
|
||||
|
|
|
@ -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) => {
|
|||
<a href={`/${props.article.slug || ''}`}>
|
||||
<div class={styles.shoutCardTitle}>
|
||||
<span class={styles.shoutCardLinkWrapper}>
|
||||
<span class={styles.shoutCardLinkContainer} innerHTML={title} />
|
||||
<span class={styles.shoutCardLinkContainer} innerHTML={sanitizeString(title)} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Show when={!props.settings?.nosubtitle && subtitle}>
|
||||
<div class={styles.shoutCardSubtitle}>
|
||||
<span class={styles.shoutCardLinkContainer} innerHTML={subtitle} />
|
||||
<span class={styles.shoutCardLinkContainer} innerHTML={sanitizeString(subtitle)} />
|
||||
</div>
|
||||
</Show>
|
||||
</a>
|
||||
|
@ -191,7 +201,10 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
</div>
|
||||
</Show>
|
||||
<Show when={props.article.description}>
|
||||
<section class={styles.shoutCardDescription} innerHTML={props.article.description} />
|
||||
<section
|
||||
class={styles.shoutCardDescription}
|
||||
innerHTML={sanitizeString(props.article.description)}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={props.settings?.isFeedMode}>
|
||||
<Show when={!props.settings?.noimage && props.article.cover}>
|
||||
|
|
|
@ -124,10 +124,10 @@ export const HeaderAuth = (props: Props) => {
|
|||
|
||||
<Show when={!isSaveButtonVisible()}>
|
||||
<div class={styles.userControlItem}>
|
||||
<button onClick={() => showModal('search')}>
|
||||
<a href="?modal=search">
|
||||
<Icon name="search" class={styles.icon} />
|
||||
<Icon name="search" class={clsx(styles.icon, styles.iconHover)} />
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<div class={styles.searchContainer}>
|
||||
<input
|
||||
type="search"
|
||||
placeholder={t('Site search')}
|
||||
ref={(el) => (searchInputRef.current = el)}
|
||||
class={styles.searchInput}
|
||||
onInput={handleSearch}
|
||||
onInput={(event) => {
|
||||
setInputValue(event.target.value)
|
||||
|
||||
handleSearch()
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
@ -116,8 +105,8 @@ export const SearchModal = () => {
|
|||
<Show when={searchResultsList()}>
|
||||
<For each={searchResultsList()}>
|
||||
{(article: Shout) => (
|
||||
<div onClick={handleArticleClick}>
|
||||
<ArticleCard
|
||||
<div>
|
||||
<SearchResultItem
|
||||
article={article}
|
||||
settings={{
|
||||
noimage: true, // @@TODO remove flag after cover support
|
||||
|
|
33
src/components/Nav/SearchModal/SearchResultItem.tsx
Normal file
33
src/components/Nav/SearchModal/SearchResultItem.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { ArticleCard } from '../../Feed/ArticleCard'
|
||||
|
||||
import type { Shout } from '../../../graphql/types.gen'
|
||||
|
||||
interface SearchCardProps {
|
||||
settings?: {
|
||||
noicon?: boolean
|
||||
noimage?: boolean
|
||||
nosubtitle?: boolean
|
||||
noauthor?: boolean
|
||||
nodate?: boolean
|
||||
isGroup?: boolean
|
||||
photoBottom?: boolean
|
||||
additionalClass?: string
|
||||
isFeedMode?: boolean
|
||||
isFloorImportant?: boolean
|
||||
isWithCover?: boolean
|
||||
isBigTitle?: boolean
|
||||
isVertical?: boolean
|
||||
isShort?: boolean
|
||||
withBorder?: boolean
|
||||
isCompact?: boolean
|
||||
isSingle?: boolean
|
||||
isBeside?: boolean
|
||||
withViewed?: boolean
|
||||
noAuthorLink?: boolean
|
||||
}
|
||||
article: Shout
|
||||
}
|
||||
|
||||
export const SearchResultItem = (props: SearchCardProps) => {
|
||||
return <ArticleCard article={props.article} settings={props.settings} />
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user