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

View File

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

View File

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

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

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

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