+ `
${str.replace(
+ new RegExp(intersection, 'gi'),
+ (casePreservedMatch) => `${casePreservedMatch}`,
+ )}`
+
+const prepareSearchResults = (list, searchValue) =>
+ list.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,
+ })
+ : '',
+ }))
+
+export const SearchModal = () => {
+ const { t } = useLocalize()
+
+ const [inputValue, setInputValue] = createSignal('')
+ const [searchResultsList, setSearchResultsList] = createSignal<[] | null>([])
+ const [isLoading, setIsLoading] = createSignal(false)
+ // const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
+
+ const handleSearch = async () => {
+ const searchValue = inputValue() || ''
+
+ if (Boolean(searchValue) && searchValue.length > 2) {
+ setIsLoading(true)
+
+ try {
+ const response = await apiClient.getSearchResults(searchValue)
+ const searchResult = await response.json()
+
+ if (searchResult.length > 0) {
+ const preparedSearchResultsList = prepareSearchResults(searchResult, searchValue)
+
+ setSearchResultsList(preparedSearchResultsList)
+ } else {
+ setSearchResultsList(null)
+ }
+ } catch (error) {
+ console.log('search request failed', error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+ }
+
+ return (
+
+
{
+ setInputValue(event.target.value)
+
+ handleSearch()
+ }}
+ />
+
+
:
}
+ />
+
+
+
+
+
+
+ {(article: Shout) => (
+
+
+
+ )}
+
+
+ {/*
+
+
+
+ */}
+
+
+
+
+
+
+
+ {/* @@TODO handle filter */}
+ {/*
+
+
+ {(filter) => (
+
+ )}
+
+
+ */}
+
+ {/* @@TODO handle topics */}
+ {/*
+
+
+
+
+ {(topic) => (
+
+ )}
+
+
+
+
+ */}
+
+ )
+}
diff --git a/src/components/Nav/SearchModal/SearchResultItem.tsx b/src/components/Nav/SearchModal/SearchResultItem.tsx
new file mode 100644
index 00000000..06e6dd55
--- /dev/null
+++ b/src/components/Nav/SearchModal/SearchResultItem.tsx
@@ -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
+}
diff --git a/src/stores/ui.ts b/src/stores/ui.ts
index 7fcb4fe5..14982c53 100644
--- a/src/stores/ui.ts
+++ b/src/stores/ui.ts
@@ -23,6 +23,7 @@ export type ModalType =
| 'editorInsertLink'
| 'followers'
| 'following'
+ | 'search'
| 'inviteCoAuthors'
| 'share'
@@ -40,6 +41,7 @@ export const MODALS: Record
= {
editorInsertLink: 'editorInsertLink',
followers: 'followers',
following: 'following',
+ search: 'search',
inviteCoAuthors: 'inviteCoAuthors',
share: 'share',
}
diff --git a/src/styles/app.scss b/src/styles/app.scss
index 0977d95a..3380f5b5 100644
--- a/src/styles/app.scss
+++ b/src/styles/app.scss
@@ -1066,6 +1066,11 @@ iframe {
cursor: pointer;
}
+.blackModeIntersection {
+ color: var(--default-color);
+ background: #fef2f2;
+}
+
.img-align-column {
clear: both;
}
diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts
index eacbd655..362c727d 100644
--- a/src/utils/apiClient.ts
+++ b/src/utils/apiClient.ts
@@ -66,6 +66,8 @@ import topicBySlug from '../graphql/query/topic-by-slug'
import topicsAll from '../graphql/query/topics-all'
import topicsRandomQuery from '../graphql/query/topics-random'
+import { searchUrl } from './config'
+
type ApiErrorCode =
| 'unknown'
| 'email_not_confirmed'
@@ -441,4 +443,15 @@ export const apiClient = {
const resp = await graphQLClient.query(loadRecipients, options).toPromise()
return resp.data.loadRecipients.members
},
+
+ // search
+ getSearchResults: async (searchValue: string) => {
+ return await fetch(`${searchUrl}/search?q=${searchValue}`, {
+ method: 'GET',
+ headers: {
+ accept: 'application/json',
+ 'content-type': 'application/json; charset=utf-8',
+ },
+ })
+ },
}
diff --git a/src/utils/config.ts b/src/utils/config.ts
index 3039e213..ee3e2d25 100644
--- a/src/utils/config.ts
+++ b/src/utils/config.ts
@@ -7,3 +7,6 @@ const defaultThumborUrl = 'https://images.discours.io'
export const thumborUrl = import.meta.env.PUBLIC_THUMBOR_URL || defaultThumborUrl
export const SENTRY_DSN = import.meta.env.PUBLIC_SENTRY_DSN || ''
+
+const defaultSearchUrl = 'https://search.discours.io'
+export const searchUrl = import.meta.env.PUBLIC_SEARCH_URL || defaultSearchUrl