refactor by review comments

This commit is contained in:
dog 2024-01-21 15:57:03 +03:00
parent c78d5b3337
commit 6752d35491
7 changed files with 118 additions and 67 deletions

View File

@ -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",

View File

@ -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}>

View File

@ -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>

View File

@ -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

View 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} />
}

View File

@ -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
}
}

View File

@ -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