refactor by review comments
This commit is contained in:
parent
c78d5b3337
commit
6752d35491
|
@ -34,7 +34,8 @@
|
||||||
"i18next-icu": "2.3.0",
|
"i18next-icu": "2.3.0",
|
||||||
"intl-messageformat": "10.5.3",
|
"intl-messageformat": "10.5.3",
|
||||||
"just-throttle": "4.2.0",
|
"just-throttle": "4.2.0",
|
||||||
"mailgun.js": "8.2.1"
|
"mailgun.js": "8.2.1",
|
||||||
|
"sanitize-html": "2.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.21.8",
|
"@babel/core": "7.21.8",
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { createMemo, createSignal, For, Show } from 'solid-js'
|
import { createMemo, createSignal, For, Show } from 'solid-js'
|
||||||
|
import sanitizeHtml from 'sanitize-html'
|
||||||
|
|
||||||
import type { Shout } from '../../../graphql/types.gen'
|
import type { Shout } from '../../../graphql/types.gen'
|
||||||
import { capitalize } from '../../../utils/capitalize'
|
import { capitalize } from '../../../utils/capitalize'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
|
@ -70,6 +72,14 @@ const getTitleAndSubtitle = (
|
||||||
return { title, subtitle }
|
return { title, subtitle }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sanitizeString = (html) =>
|
||||||
|
sanitizeHtml(html, {
|
||||||
|
allowedTags: ['span'],
|
||||||
|
allowedAttributes: {
|
||||||
|
span: ['class']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export const ArticleCard = (props: ArticleCardProps) => {
|
export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
const { t, lang, formatDate } = useLocalize()
|
const { t, lang, formatDate } = useLocalize()
|
||||||
const { user } = useSession()
|
const { user } = useSession()
|
||||||
|
@ -161,13 +171,13 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
<a href={`/${props.article.slug || ''}`}>
|
<a href={`/${props.article.slug || ''}`}>
|
||||||
<div class={styles.shoutCardTitle}>
|
<div class={styles.shoutCardTitle}>
|
||||||
<span class={styles.shoutCardLinkWrapper}>
|
<span class={styles.shoutCardLinkWrapper}>
|
||||||
<span class={styles.shoutCardLinkContainer} innerHTML={title} />
|
<span class={styles.shoutCardLinkContainer} innerHTML={sanitizeString(title)} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={!props.settings?.nosubtitle && subtitle}>
|
<Show when={!props.settings?.nosubtitle && subtitle}>
|
||||||
<div class={styles.shoutCardSubtitle}>
|
<div class={styles.shoutCardSubtitle}>
|
||||||
<span class={styles.shoutCardLinkContainer} innerHTML={subtitle} />
|
<span class={styles.shoutCardLinkContainer} innerHTML={sanitizeString(subtitle)} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</a>
|
</a>
|
||||||
|
@ -191,7 +201,10 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.article.description}>
|
<Show when={props.article.description}>
|
||||||
<section class={styles.shoutCardDescription} innerHTML={props.article.description} />
|
<section
|
||||||
|
class={styles.shoutCardDescription}
|
||||||
|
innerHTML={sanitizeString(props.article.description)}
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.settings?.isFeedMode}>
|
<Show when={props.settings?.isFeedMode}>
|
||||||
<Show when={!props.settings?.noimage && props.article.cover}>
|
<Show when={!props.settings?.noimage && props.article.cover}>
|
||||||
|
|
|
@ -124,10 +124,10 @@ export const HeaderAuth = (props: Props) => {
|
||||||
|
|
||||||
<Show when={!isSaveButtonVisible()}>
|
<Show when={!isSaveButtonVisible()}>
|
||||||
<div class={styles.userControlItem}>
|
<div class={styles.userControlItem}>
|
||||||
<button onClick={() => showModal('search')}>
|
<a href="?modal=search">
|
||||||
<Icon name="search" class={styles.icon} />
|
<Icon name="search" class={styles.icon} />
|
||||||
<Icon name="search" class={clsx(styles.icon, styles.iconHover)} />
|
<Icon name="search" class={clsx(styles.icon, styles.iconHover)} />
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</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 { Button } from '../../_shared/Button'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
|
import { SearchResultItem } from './SearchResultItem'
|
||||||
|
|
||||||
|
import { apiClient } from '../../../utils/apiClient'
|
||||||
import type { Shout } from '../../../graphql/types.gen'
|
import type { Shout } from '../../../graphql/types.gen'
|
||||||
|
|
||||||
import { searchUrl } from '../../../utils/config'
|
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { hideModal } from '../../../stores/ui'
|
|
||||||
|
|
||||||
import styles from './SearchModal.module.scss'
|
import styles from './SearchModal.module.scss'
|
||||||
|
|
||||||
|
@ -26,29 +24,23 @@ const getSearchCoincidences = ({ str, intersection }: { str: string; intersectio
|
||||||
export const SearchModal = () => {
|
export const SearchModal = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const searchInputRef: { current: HTMLInputElement } = { current: null }
|
const [inputValue, setInputValue] = createSignal('')
|
||||||
|
|
||||||
const [searchResultsList, setSearchResultsList] = createSignal<[] | null>([])
|
const [searchResultsList, setSearchResultsList] = createSignal<[] | null>([])
|
||||||
const [isLoading, setIsLoading] = createSignal(false)
|
const [isLoading, setIsLoading] = createSignal(false)
|
||||||
// const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
// const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
|
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
const searchValue = searchInputRef.current?.value || ''
|
const searchValue = inputValue() || ''
|
||||||
|
|
||||||
if (Boolean(searchValue)) {
|
if (Boolean(searchValue) && searchValue.length > 2) {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
await fetch(`${searchUrl}=${searchValue}`, {
|
try {
|
||||||
method: 'GET',
|
const response = await apiClient.getSearchResults(searchValue)
|
||||||
headers: {
|
const searchResult = await response.json()
|
||||||
accept: 'application/json',
|
|
||||||
'content-type': 'application/json; charset=utf-8'
|
if (searchResult.length) {
|
||||||
}
|
const preparedSearchResultsList = searchResult.map((article, index) => ({
|
||||||
})
|
|
||||||
.then((data) => data.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.length) {
|
|
||||||
const preparedSearchResultsList = data.map((article, index) => ({
|
|
||||||
...article,
|
...article,
|
||||||
body: '',
|
body: '',
|
||||||
cover: '',
|
cover: '',
|
||||||
|
@ -60,13 +52,13 @@ export const SearchModal = () => {
|
||||||
title: article.title
|
title: article.title
|
||||||
? getSearchCoincidences({
|
? getSearchCoincidences({
|
||||||
str: article.title,
|
str: article.title,
|
||||||
intersection: searchInputRef.current?.value || ''
|
intersection: searchValue
|
||||||
})
|
})
|
||||||
: '',
|
: '',
|
||||||
subtitle: article.subtitle
|
subtitle: article.subtitle
|
||||||
? getSearchCoincidences({
|
? getSearchCoincidences({
|
||||||
str: article.subtitle,
|
str: article.subtitle,
|
||||||
intersection: searchInputRef.current?.value || ''
|
intersection: searchValue
|
||||||
})
|
})
|
||||||
: ''
|
: ''
|
||||||
}))
|
}))
|
||||||
|
@ -75,18 +67,12 @@ export const SearchModal = () => {
|
||||||
} else {
|
} else {
|
||||||
setSearchResultsList(null)
|
setSearchResultsList(null)
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch((error) => {
|
|
||||||
console.log('search request failed', error)
|
console.log('search request failed', error)
|
||||||
})
|
} finally {
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleArticleClick = () => {
|
|
||||||
hideModal()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -94,9 +80,12 @@ export const SearchModal = () => {
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
placeholder={t('Site search')}
|
placeholder={t('Site search')}
|
||||||
ref={(el) => (searchInputRef.current = el)}
|
|
||||||
class={styles.searchInput}
|
class={styles.searchInput}
|
||||||
onInput={handleSearch}
|
onInput={(event) => {
|
||||||
|
setInputValue(event.target.value)
|
||||||
|
|
||||||
|
handleSearch()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -116,8 +105,8 @@ export const SearchModal = () => {
|
||||||
<Show when={searchResultsList()}>
|
<Show when={searchResultsList()}>
|
||||||
<For each={searchResultsList()}>
|
<For each={searchResultsList()}>
|
||||||
{(article: Shout) => (
|
{(article: Shout) => (
|
||||||
<div onClick={handleArticleClick}>
|
<div>
|
||||||
<ArticleCard
|
<SearchResultItem
|
||||||
article={article}
|
article={article}
|
||||||
settings={{
|
settings={{
|
||||||
noimage: true, // @@TODO remove flag after cover support
|
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 markNotificationAsRead from '../graphql/mutation/mark-notification-as-read'
|
||||||
import mySubscriptions from '../graphql/query/my-subscriptions'
|
import mySubscriptions from '../graphql/query/my-subscriptions'
|
||||||
|
|
||||||
|
import { searchUrl } from './config'
|
||||||
|
|
||||||
type ApiErrorCode =
|
type ApiErrorCode =
|
||||||
| 'unknown'
|
| 'unknown'
|
||||||
| 'email_not_confirmed'
|
| 'email_not_confirmed'
|
||||||
|
@ -397,5 +399,18 @@ export const apiClient = {
|
||||||
getRecipients: async (options: QueryLoadRecipientsArgs) => {
|
getRecipients: async (options: QueryLoadRecipientsArgs) => {
|
||||||
const resp = await privateGraphQLClient.query(loadRecipients, options).toPromise()
|
const resp = await privateGraphQLClient.query(loadRecipients, options).toPromise()
|
||||||
return resp.data.loadRecipients.members
|
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 || ''
|
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
|
export const searchUrl = import.meta.env.PUBLIC_SEARCH_URL || defaultSearchUrl
|
||||||
|
|
Loading…
Reference in New Issue
Block a user