diff --git a/locales/en/common.json b/locales/en/common.json index c4a8d56e5b..c2186f621c 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -253,6 +253,7 @@ "pagination-summary": "{{currentResultNumber}}-{{endOfResultNumber}} of {{totalNumberOfResults}} search results", "pbuh": "Blessings of Allah be upon him", "popular-links": "Popular Links", + "search-results-no-count": "Search Results", "popup": { "footnote": "Monthly donations allow us to focus less on fundraising", "text-1": "We are committed to serving the world Quranic knowledge and technology, always for free.", @@ -303,7 +304,8 @@ "results": "results", "show-all": "Show all results", "switch-mode": "Switch to Advanced Search", - "title": "Search" + "title": "Search", + "more-results": "More results" }, "seconds": "Seconds", "see-new": "See What's New", @@ -396,9 +398,9 @@ "view": "View", "voice": { "ask-permission": "Please enable microphone permission to start using Voice Search", - "error": "An error has occurred, please try again later. Or download the ", - "no-permission": "It looks like you do not have the microphone permissions enabled. Please enable the microphone permissions and try again or download the ", - "not-supported": "It looks like your browser does not support microphone. Please try a different browser or download the ", + "error": "An error has occurred, please try again later.", + "no-permission": "It looks like you do not have the microphone permissions enabled. Please enable the microphone permissions and try again.", + "not-supported": "It looks like your browser does not support microphone. Please try a different browser.", "suggest": "Please begin reciting and your verse will appear.", "suggest-subtitle": "Recite any verse in Arabic, and the verse will appear", "suggest-title": "Recite now", diff --git a/public/icons/search/arabic.svg b/public/icons/search/arabic.svg new file mode 100644 index 0000000000..ecdc4e375f --- /dev/null +++ b/public/icons/search/arabic.svg @@ -0,0 +1,5 @@ + + + diff --git a/public/icons/search/ayah-range.svg b/public/icons/search/ayah-range.svg new file mode 100644 index 0000000000..18e4ccaf4d --- /dev/null +++ b/public/icons/search/ayah-range.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/public/icons/search/juz.svg b/public/icons/search/juz.svg new file mode 100644 index 0000000000..706b46de0f --- /dev/null +++ b/public/icons/search/juz.svg @@ -0,0 +1,5 @@ + + + diff --git a/public/icons/search/page.svg b/public/icons/search/page.svg new file mode 100644 index 0000000000..9d78539173 --- /dev/null +++ b/public/icons/search/page.svg @@ -0,0 +1,5 @@ + + + diff --git a/public/icons/search/surah.svg b/public/icons/search/surah.svg new file mode 100644 index 0000000000..1be7fb12d1 --- /dev/null +++ b/public/icons/search/surah.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/public/icons/search/translation.svg b/public/icons/search/translation.svg new file mode 100644 index 0000000000..37ed847dbe --- /dev/null +++ b/public/icons/search/translation.svg @@ -0,0 +1,8 @@ + + + + diff --git a/public/icons/search/transliteration.svg b/public/icons/search/transliteration.svg new file mode 100644 index 0000000000..b1c37fec4e --- /dev/null +++ b/public/icons/search/transliteration.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/api.ts b/src/api.ts index f70a27c84c..83bf3ba1c5 100644 --- a/src/api.ts +++ b/src/api.ts @@ -12,7 +12,6 @@ import { makeAudioTimestampsUrl, makeChapterAudioDataUrl, makeAvailableRecitersUrl, - makeSearchResultsUrl, makeTranslationsInfoUrl, makeTranslationsUrl, makeVersesUrl, @@ -32,10 +31,9 @@ import { } from '@/utils/apiPaths'; import generateSignature from '@/utils/auth/signature'; import { isStaticBuild } from '@/utils/build'; -import { SearchRequest, AdvancedCopyRequest, PagesLookUpRequest } from 'types/ApiRequests'; +import { AdvancedCopyRequest, PagesLookUpRequest } from 'types/ApiRequests'; import { TranslationsResponse, - SearchResponse, AdvancedCopyRawResultResponse, LanguagesResponse, RecitersResponse, @@ -244,15 +242,6 @@ export const getAdvancedCopyRawResult = async ( params: AdvancedCopyRequest, ): Promise => fetcher(makeAdvancedCopyUrl(params)); -/** - * Get the search results of a query. - * - * @param {SearchRequest} params - * @returns {Promise} - */ -export const getSearchResults = async (params: SearchRequest): Promise => - fetcher(makeSearchResultsUrl(params)); - /** * Get the search results of a query. * diff --git a/src/components/CommandBar/CommandBar.module.scss b/src/components/CommandBar/CommandBar.module.scss deleted file mode 100644 index 2b737bcfdd..0000000000 --- a/src/components/CommandBar/CommandBar.module.scss +++ /dev/null @@ -1,8 +0,0 @@ -@use "src/styles/breakpoints"; - -.loadingContainer { - flex: 1; - display: flex; - justify-content: center; - align-items: center; -} diff --git a/src/components/CommandBar/CommandBarBase/CommandBarBase.module.scss b/src/components/CommandBar/CommandBarBase/CommandBarBase.module.scss deleted file mode 100644 index d56db90499..0000000000 --- a/src/components/CommandBar/CommandBarBase/CommandBarBase.module.scss +++ /dev/null @@ -1,109 +0,0 @@ -@use "src/styles/theme"; - -$content-animation-easing: cubic-bezier(0.16, 1, 0.3, 1); -$overlay-background-light: hsla(0, 0%, 100%, 0.8); -$overlay-background-dark: rgba(0, 0, 0, 0.8); -$overlay-background-sepia: rgba(239, 226, 205, 0.7); -$shadow: 0 16px 70px rgb(0 0 0 / 20%); // using custom shadow for now until we fix our token and design in dark theme; -$width: 95vw; -$max-width: calc(20 * var(--spacing-mega)); -$max-height: 85vh; -$min-height: calc(9 * var(--spacing-mega)); - -@keyframes contentShow { - 0% { - opacity: 0; - transform: var(--content-translate-position) scale(0.96); - } - 100% { - opacity: 1; - transform: var(--content-translate-position) scale(1); - } -} - -@keyframes contentHide { - 0% { - opacity: 1; - transform: var(--content-translate-position) scale(1); - } - 100% { - opacity: 0; - transform: var(--content-translate-position) scale(0.96); - } -} - -.content { - --content-translate-position: translate(-50%, -50%); - [dir="rtl"] & { - --content-translate-position: translate(50%, -50%); - } - - background-color: var(--color-background-default); - border-radius: var(--border-radius-rounded); - box-shadow: $shadow; - position: fixed; - inset-block-start: 50%; - inset-inline-start: 50%; - transform: var(--content-translate-position); - width: $width; - max-width: $max-width; - max-height: $max-height; - min-height: $min-height; - display: flex; - flex-direction: column; - @media (prefers-reduced-motion: no-preference) { - &[data-state="open"] { - animation: contentShow var(--transition-fast) $content-animation-easing; - } - &[data-state="closed"] { - animation: contentHide var(--transition-fast) $content-animation-easing; - } - } - &:focus { - outline: none; - } - z-index: var(--z-index-modal); -} - -@keyframes overlayShow { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes overlayHide { - from { - opacity: 1; - } - to { - opacity: 0; - } -} - -.overlay { - @include theme.light { - background-color: $overlay-background-light; - } - @include theme.dark { - background-color: $overlay-background-dark; - } - @include theme.sepia { - background-color: $overlay-background-sepia; - } - backdrop-filter: blur(6px); - position: fixed; - inset: 0; - - @media (prefers-reduced-motion: no-preference) { - &[data-state="open"] { - animation: overlayShow var(--transition-fast) ease; - } - &[data-state="closed"] { - animation: overlayHide var(--transition-fast) ease; - } - } - z-index: var(--z-index-overlay); -} diff --git a/src/components/CommandBar/CommandBarBase/CommandBarBase.tsx b/src/components/CommandBar/CommandBarBase/CommandBarBase.tsx deleted file mode 100644 index b4a7f720e4..0000000000 --- a/src/components/CommandBar/CommandBarBase/CommandBarBase.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as Dialog from '@radix-ui/react-dialog'; - -import styles from './CommandBarBase.module.scss'; - -type CommandBarBaseProps = { - onClickOutside: () => void; - children: React.ReactNode; - isOpen: boolean; -}; - -const CommandBarBase = ({ onClickOutside, children, isOpen }: CommandBarBaseProps) => { - return ( - - - - - {children} - - - - ); -}; - -export default CommandBarBase; diff --git a/src/components/CommandBar/CommandBarTrigger/CommandBarTrigger.module.scss b/src/components/CommandBar/CommandBarTrigger/CommandBarTrigger.module.scss deleted file mode 100644 index 6200496df3..0000000000 --- a/src/components/CommandBar/CommandBarTrigger/CommandBarTrigger.module.scss +++ /dev/null @@ -1,60 +0,0 @@ -@use "src/styles/breakpoints"; -@use "src/styles/theme"; - -$width: calc(11 * var(--spacing-mega)); - -.leftSection { - display: flex; - align-items: center; - - > svg { - fill: var(--color-text-faded); - opacity: var(--opacity-50); - width: calc(1.4 * var(--spacing-medium)); - } - - .placeholder { - margin-inline-start: var(--spacing-small); - } -} - -.container { - position: relative; - transition: - box-shadow var(--transition-fast) ease, - top var(--transition-fast) ease; - box-sizing: border-box; - - background: var(--color-background-elevated); - - width: 100%; - display: flex; - align-items: center; - padding: 0 calc(1.5 * var(--spacing-medium)); - color: var(--color-text-faded); - min-height: calc(3 * var(--spacing-large)); - border-radius: var(--border-radius-pill); - cursor: pointer; - outline: inherit; - font-size: var(--font-size-large); - inset-block-start: 0; - @include theme.light { - box-shadow: var(--shadow-small); - &:hover { - color: var(--color-text-default); - box-shadow: var(--shadow-large); - } - } - justify-content: space-between; -} - -.actionsContainer { - display: flex; - justify-content: center; - align-items: center; -} - -.searchButtonWrapper { - margin-inline-end: calc(-1 * var(--spacing-xsmall)); - margin-inline-start: calc(var(--spacing-xsmall)); -} diff --git a/src/components/CommandBar/CommandBarTrigger/index.tsx b/src/components/CommandBar/CommandBarTrigger/index.tsx deleted file mode 100644 index 39c405007a..0000000000 --- a/src/components/CommandBar/CommandBarTrigger/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { useCallback } from 'react'; - -import useTranslation from 'next-translate/useTranslation'; -import { useDispatch } from 'react-redux'; - -import styles from './CommandBarTrigger.module.scss'; - -import TarteelVoiceSearchTrigger from '@/components/TarteelVoiceSearch/Trigger'; -import KeyboardInput from '@/dls/KeyboardInput'; -import IconSearch from '@/icons/search.svg'; -import { toggleIsOpen } from '@/redux/slices/CommandBar/state'; -import { logButtonClick } from '@/utils/eventLogger'; - -const CommandBarTrigger: React.FC = () => { - const { t } = useTranslation('common'); - const dispatch = useDispatch(); - const onClick = useCallback(() => { - logButtonClick('command_bar_homepage_trigger'); - dispatch({ type: toggleIsOpen.type }); - }, [dispatch]); - - return ( -
-
- - {t('command-bar.placeholder')} -
-
- -
- { - logButtonClick('command_bar_homepage_voice_search_trigger'); - }} - /> -
-
-
- ); -}; - -export default CommandBarTrigger; diff --git a/src/components/CommandBar/CommandsList/CommandPrefix/CommandPrefix.module.scss b/src/components/CommandBar/CommandsList/CommandPrefix/CommandPrefix.module.scss deleted file mode 100644 index 33e6ae3695..0000000000 --- a/src/components/CommandBar/CommandsList/CommandPrefix/CommandPrefix.module.scss +++ /dev/null @@ -1,21 +0,0 @@ -.commandPrefix { - margin-inline-end: var(--spacing-small); - display: flex; - align-items: center; - > svg { - width: var(--spacing-large); - height: var(--spacing-large); - } -} - -.container { - display: flex; - align-items: center; - max-width: 90%; -} - -.name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} diff --git a/src/components/CommandBar/CommandsList/CommandPrefix/index.tsx b/src/components/CommandBar/CommandsList/CommandPrefix/index.tsx deleted file mode 100644 index e85697e3e8..0000000000 --- a/src/components/CommandBar/CommandsList/CommandPrefix/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import useTranslation from 'next-translate/useTranslation'; - -import styles from './CommandPrefix.module.scss'; - -import NavigateIcon from '@/icons/east.svg'; -import { SearchNavigationType } from 'types/SearchNavigationResult'; - -interface Props { - name: string; - type: SearchNavigationType; -} - -const CommandPrefix: React.FC = ({ name, type }) => { - const { t } = useTranslation('common'); - return ( -
- - - -

- {type === SearchNavigationType.SEARCH_PAGE - ? t('search-for', { - searchQuery: name, - }) - : name} -

-
- ); -}; - -export default CommandPrefix; diff --git a/src/components/CommandBar/index.tsx b/src/components/CommandBar/index.tsx deleted file mode 100644 index 83354c9f3e..0000000000 --- a/src/components/CommandBar/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* eslint-disable react/no-multi-comp */ -import React, { useCallback } from 'react'; - -import dynamic from 'next/dynamic'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; - -import styles from './CommandBar.module.scss'; -import CommandBarBase from './CommandBarBase/CommandBarBase'; - -import Spinner from '@/dls/Spinner/Spinner'; -import { selectCommandBarIsOpen, setIsOpen, toggleIsOpen } from '@/redux/slices/CommandBar/state'; -import { stopCommandBarVoiceFlow } from '@/redux/slices/voiceSearch'; -import { logEvent } from '@/utils/eventLogger'; - -const CommandBarBody = dynamic(() => import('./CommandBarBody'), { - ssr: false, - loading: () => ( -
- -
- ), -}); - -const getPressedShortcut = (event: KeyboardEvent): string => { - let shortcut = ''; - if (event.metaKey) { - shortcut = 'cmd'; - } else if (event.ctrlKey) { - shortcut = 'ctrl'; - } - return `${shortcut}_${event.key}`; -}; - -const CommandBar: React.FC = () => { - const dispatch = useDispatch(); - const isOpen = useSelector(selectCommandBarIsOpen, shallowEqual); - const toggleShowCommandBar = useCallback( - (event: KeyboardEvent) => { - // eslint-disable-next-line i18next/no-literal-string - logEvent(`command_bar_${isOpen ? 'close' : 'open'}`, { - // eslint-disable-next-line @typescript-eslint/naming-convention - keyboard_shortcut: getPressedShortcut(event), - }); - event.preventDefault(); - dispatch({ type: toggleIsOpen.type }); - }, - [dispatch, isOpen], - ); - const closeCommandBar = useCallback( - (event?: KeyboardEvent) => { - const isClickedOutside = !event; - // eslint-disable-next-line i18next/no-literal-string - logEvent(`command_bar_close_${isClickedOutside ? 'outside_click' : 'esc_key'}`); - dispatch({ type: setIsOpen.type, payload: false }); - dispatch({ type: stopCommandBarVoiceFlow.type }); - }, - [dispatch], - ); - useHotkeys( - 'meta+k, ctrl+k, meta+p, ctrl+p', - toggleShowCommandBar, - { enableOnFormTags: ['INPUT'] }, - [dispatch], - ); - useHotkeys('Escape', closeCommandBar, { enabled: isOpen, enableOnFormTags: ['INPUT'] }, [ - dispatch, - ]); - - return ( - closeCommandBar()}> - - - ); -}; - -export default CommandBar; diff --git a/src/components/GlobalKeyboardListeners.tsx b/src/components/GlobalKeyboardListeners.tsx index 7a77a979fb..c491dde7c0 100644 --- a/src/components/GlobalKeyboardListeners.tsx +++ b/src/components/GlobalKeyboardListeners.tsx @@ -1,13 +1,43 @@ -import React from 'react'; +import React, { useCallback } from 'react'; -import CommandBar from '@/components/CommandBar'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { shallowEqual, useSelector, useDispatch } from 'react-redux'; + +import { selectIsSearchDrawerOpen, toggleSearchDrawerIsOpen } from '@/redux/slices/navbar'; +import { logEvent } from '@/utils/eventLogger'; + +const getPressedShortcut = (event: KeyboardEvent): string => { + let shortcut = ''; + if (event.metaKey) { + shortcut = 'cmd'; + } else if (event.ctrlKey) { + shortcut = 'ctrl'; + } + return `${shortcut}_${event.key}`; +}; const GlobalKeyboardListeners: React.FC = () => { - return ( - <> - - + const dispatch = useDispatch(); + const isOpen = useSelector(selectIsSearchDrawerOpen, shallowEqual); + const toggleShowCommandBar = useCallback( + (event: KeyboardEvent) => { + // eslint-disable-next-line i18next/no-literal-string + logEvent(`search_drawer_${isOpen ? 'close' : 'open'}`, { + // eslint-disable-next-line @typescript-eslint/naming-convention + keyboard_shortcut: getPressedShortcut(event), + }); + event.preventDefault(); + dispatch({ type: toggleSearchDrawerIsOpen.type }); + }, + [dispatch, isOpen], + ); + useHotkeys( + 'meta+k, ctrl+k, meta+p, ctrl+p', + toggleShowCommandBar, + { enableOnFormTags: ['INPUT'] }, + [dispatch], ); + return <>; }; export default GlobalKeyboardListeners; diff --git a/src/components/HomePage/HomePageHero.tsx b/src/components/HomePage/HomePageHero.tsx index 5c1b115407..2d5bc8db38 100644 --- a/src/components/HomePage/HomePageHero.tsx +++ b/src/components/HomePage/HomePageHero.tsx @@ -1,24 +1,30 @@ import dynamic from 'next/dynamic'; import Head from 'next/head'; +import useTranslation from 'next-translate/useTranslation'; import styles from './HomePageHero.module.scss'; import QuickLinks from './QuickLinks'; -import CommandBarTrigger from '@/components/CommandBar/CommandBarTrigger'; +import SearchInput from '@/components/Search/SearchInput'; const PlayRadioButton = dynamic(() => import('./PlayRadioButton')); const HomePageHero = () => { + const { t } = useTranslation('common'); return (
-
+
- +
diff --git a/src/components/HomePage/PlayRadioButton.tsx b/src/components/HomePage/PlayRadioButton.tsx index 5d9e2224ab..a2c5f00fc1 100644 --- a/src/components/HomePage/PlayRadioButton.tsx +++ b/src/components/HomePage/PlayRadioButton.tsx @@ -14,6 +14,7 @@ import Button, { ButtonType, ButtonSize } from '@/dls/Button/Button'; import Spinner from '@/dls/Spinner/Spinner'; import PauseIcon from '@/icons/pause.svg'; import PlayIcon from '@/icons/play-arrow.svg'; +import ThemeType from '@/redux/types/ThemeType'; import { logButtonClick } from '@/utils/eventLogger'; import { selectIsLoading } from 'src/xstate/actors/audioPlayer/selectors'; import { AudioPlayerMachineContext } from 'src/xstate/AudioPlayerMachineContext'; @@ -54,7 +55,7 @@ const PlayRadioButton = () => { const { radioActor } = audioService.getSnapshot().context; return ( -
+
{isAudioPlaying && isRadioMode ? ( - ); -}; - -export default NavigationItem; diff --git a/src/components/Search/NoResults/index.tsx b/src/components/Search/NoResults/index.tsx index 418fdbcf70..e160fe9631 100644 --- a/src/components/Search/NoResults/index.tsx +++ b/src/components/Search/NoResults/index.tsx @@ -4,14 +4,37 @@ import useTranslation from 'next-translate/useTranslation'; import styles from './NoResults.module.scss'; +import Link, { LinkVariant } from '@/dls/Link/Link'; import IconSearch from '@/icons/search.svg'; +import { logButtonClick } from '@/utils/eventLogger'; +import { getSearchQueryNavigationUrl } from '@/utils/navigation'; interface Props { searchQuery: string; + shouldSuggestFullSearchWhenNoResults?: boolean; } -const NoResults: React.FC = ({ searchQuery }) => { +const NoResults: React.FC = ({ + searchQuery, + shouldSuggestFullSearchWhenNoResults = false, +}) => { const { t } = useTranslation('common'); + if (shouldSuggestFullSearchWhenNoResults) { + return ( + { + logButtonClick('no_results_advanced_search_link'); + }} + variant={LinkVariant.Blend} + > + {t('search-for', { + searchQuery, + })} + + ); + } return ( <>
diff --git a/src/components/Search/PreInput/SearchQuerySuggestion/index.tsx b/src/components/Search/PreInput/SearchQuerySuggestion/index.tsx index 181f6c6486..52ac5c5bc1 100644 --- a/src/components/Search/PreInput/SearchQuerySuggestion/index.tsx +++ b/src/components/Search/PreInput/SearchQuerySuggestion/index.tsx @@ -1,23 +1,26 @@ import React, { MouseEvent, KeyboardEvent } from 'react'; +import SearchResultItemIcon from '../../SearchResults/SearchResultItemIcon'; import SearchItem from '../SearchItem'; import styles from './SearchQuerySuggestion.module.scss'; import Button, { ButtonShape, ButtonSize, ButtonVariant } from '@/dls/Button/Button'; import CloseIcon from '@/icons/close.svg'; -import SearchIcon from '@/icons/search.svg'; +import { SearchNavigationType } from '@/types/Search/SearchNavigationResult'; interface Props { searchQuery: string; onSearchKeywordClicked: (searchQuery: string) => void; onRemoveSearchQueryClicked?: (searchQuery: string) => void; + type: SearchNavigationType; } const SearchQuerySuggestion: React.FC = ({ searchQuery, onSearchKeywordClicked, onRemoveSearchQueryClicked, + type, }) => { const onRemoveClicked = ( event: MouseEvent | KeyboardEvent, @@ -31,7 +34,7 @@ const SearchQuerySuggestion: React.FC = ({
} + prefix={} onClick={() => onSearchKeywordClicked(searchQuery)} suffix={ onRemoveSearchQueryClicked && ( diff --git a/src/components/Search/PreInput/index.tsx b/src/components/Search/PreInput/index.tsx index 6cf57d01a5..2b457ea2b4 100644 --- a/src/components/Search/PreInput/index.tsx +++ b/src/components/Search/PreInput/index.tsx @@ -11,6 +11,8 @@ import SearchHistory from '@/components/Search/SearchHistory'; import Link from '@/dls/Link/Link'; import useGetChaptersData from '@/hooks/useGetChaptersData'; import TrendUpIcon from '@/icons/trend-up.svg'; +import { SearchNavigationType } from '@/types/Search/SearchNavigationResult'; +import SearchQuerySource from '@/types/SearchQuerySource'; import { getChapterData } from '@/utils/chapter'; import { logButtonClick } from '@/utils/eventLogger'; import { toLocalizedNumber, toLocalizedVerseKey } from '@/utils/locale'; @@ -18,23 +20,39 @@ import { getSurahNavigationUrl } from '@/utils/navigation'; interface Props { onSearchKeywordClicked: (searchQuery: string) => void; - isSearchDrawer: boolean; + source: SearchQuerySource; } const POPULAR_SEARCH_QUERIES = { Mulk: 67, Noah: 71, Kahf: 18, Yaseen: 36 }; -const PreInput: React.FC = ({ onSearchKeywordClicked, isSearchDrawer }) => { +const PreInput: React.FC = ({ onSearchKeywordClicked, source }) => { const { t, lang } = useTranslation('common'); const chaptersData = useGetChaptersData(lang); if (!chaptersData) { return <>; } - const SEARCH_FOR_KEYWORDS = [ - `${t('juz')} ${toLocalizedNumber(1, lang)}`, - `${t('page')} ${toLocalizedNumber(1, lang)}`, - getChapterData(chaptersData, '36').transliteratedName, - toLocalizedNumber(36, lang), - toLocalizedVerseKey('2:255', lang), + + const SEARCH_FOR_KEYWORD = [ + { + type: SearchNavigationType.JUZ, + value: `${t('juz')} ${toLocalizedNumber(1, lang)}`, + }, + { + type: SearchNavigationType.PAGE, + value: `${t('page')} ${toLocalizedNumber(1, lang)}`, + }, + { + type: SearchNavigationType.SURAH, + value: getChapterData(chaptersData, '36').transliteratedName, + }, + { + type: SearchNavigationType.SURAH, + value: toLocalizedNumber(36, lang), + }, + { + type: SearchNavigationType.AYAH, + value: toLocalizedVerseKey('2:255', lang), + }, ]; return (
@@ -52,29 +70,23 @@ const PreInput: React.FC = ({ onSearchKeywordClicked, isSearchDrawer }) = title={chapterData.transliteratedName} key={url} onClick={() => { - logButtonClick( - `search_${ - isSearchDrawer ? 'drawer' : 'page' - }_popular_search_${popularSearchQuery}`, - ); + logButtonClick(`${source}_popular_search_${popularSearchQuery}`); }} /> ); })}
- +
- {SEARCH_FOR_KEYWORDS.map((keyword, index) => { + {SEARCH_FOR_KEYWORD.map((keyword, index) => { return ( { - logButtonClick(`search_${isSearchDrawer ? 'drawer' : 'page'}_search_hint_${index}`); + logButtonClick(`${source}_search_hint_${index}`); onSearchKeywordClicked(searchQuery); }} /> diff --git a/src/components/Search/SearchBodyContainer.tsx b/src/components/Search/SearchBodyContainer.tsx index de71b89026..9625687f63 100644 --- a/src/components/Search/SearchBodyContainer.tsx +++ b/src/components/Search/SearchBodyContainer.tsx @@ -9,12 +9,12 @@ import styles from './SearchBodyContainer.module.scss'; import SearchResults from '@/components/Search/SearchResults'; import Spinner, { SpinnerSize } from '@/dls/Spinner/Spinner'; +import SearchQuerySource from '@/types/SearchQuerySource'; import { SearchResponse } from 'types/ApiResponses'; interface Props { searchQuery: string; isSearching: boolean; - isSearchDrawer?: boolean; hasError: boolean; searchResult: SearchResponse; onSearchKeywordClicked: (keyword: string) => void; @@ -22,6 +22,8 @@ interface Props { currentPage?: number; pageSize?: number; onPageChange?: (page: number) => void; + shouldSuggestFullSearchWhenNoResults?: boolean; + source: SearchQuerySource; } const SearchBodyContainer: React.FC = ({ @@ -31,10 +33,11 @@ const SearchBodyContainer: React.FC = ({ searchResult, onSearchKeywordClicked, onSearchResultClicked, - isSearchDrawer = true, currentPage, pageSize, onPageChange, + shouldSuggestFullSearchWhenNoResults = false, + source, }) => { const { t } = useTranslation('common'); const isEmptyResponse = @@ -50,7 +53,7 @@ const SearchBodyContainer: React.FC = ({ })} > {!searchQuery ? ( - + ) : ( <> {isSearching ? ( @@ -61,13 +64,16 @@ const SearchBodyContainer: React.FC = ({ {!hasError && searchResult && ( <> {isEmptyResponse ? ( - + ) : ( void; - isSearchDrawer: boolean; + source: SearchQuerySource; } -const SearchHistory: React.FC = ({ onSearchKeywordClicked, isSearchDrawer }) => { +const SearchHistory: React.FC = ({ onSearchKeywordClicked, source }) => { const { t } = useTranslation('common'); const searchHistory = useSelector(selectSearchHistory, areArraysEqual) as string[]; const dispatch = useDispatch(); @@ -24,10 +26,10 @@ const SearchHistory: React.FC = ({ onSearchKeywordClicked, isSearchDrawer const onRemoveSearchQueryClicked = useCallback( (searchQuery: string) => { // eslint-disable-next-line i18next/no-literal-string - logButtonClick(`search_${isSearchDrawer ? 'drawer' : 'page'}_remove_query`); + logButtonClick(`${source}_remove_query`); dispatch({ type: removeSearchHistoryRecord.type, payload: searchQuery }); }, - [dispatch, isSearchDrawer], + [dispatch, source], ); // if there are no recent search queries. @@ -39,10 +41,11 @@ const SearchHistory: React.FC = ({ onSearchKeywordClicked, isSearchDrawer
{searchHistory.map((recentSearchQuery) => ( { - logButtonClick(`search_${isSearchDrawer ? 'drawer' : 'page'}_history_item`); + logButtonClick(`${source}_history_item`); onSearchKeywordClicked(searchQuery); }} onRemoveSearchQueryClicked={onRemoveSearchQueryClicked} diff --git a/src/components/Search/SearchInput/SearchInput.module.scss b/src/components/Search/SearchInput/SearchInput.module.scss new file mode 100644 index 0000000000..66c0da88ec --- /dev/null +++ b/src/components/Search/SearchInput/SearchInput.module.scss @@ -0,0 +1,65 @@ +@use "src/styles/breakpoints"; + +.headerOuterContainer { + background: var(--color-background-elevated); + position: relative; + border-radius: var(--border-radius-pill); + box-shadow: var(--shadow-strong); + width: 100%; + margin: 0 auto; + z-index: 1; + max-width: calc(25 * var(--spacing-mega)); +} + +.prefixSuffixContainer { + color: var(--color-text-default) !important; +} + +.expanded { + z-index: 100; + border-radius: var(--border-radius-circle-small); + border-radius: var(--border-radius-circle-small) var(--border-radius-circle-small) 0 0; +} + +.inputContainer { + display: flex; + align-items: center; + justify-content: center; +} + +.input { + border: none !important; + width: 100%; + + input::placeholder { + color: var(--color-text-default); + opacity: 1; + } + + input { + &:-webkit-autofill, + &:-webkit-autofill:hover, + &:-webkit-autofill:focus, + &:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 30px transparent inset !important; + -webkit-text-fill-color: var(--color-text-default) !important; + transition: background-color 5000s ease-in-out 0s; + background-color: transparent !important; + } + } +} + +.dropdownContainer { + position: absolute; + top: 100%; + left: 0; + right: 0; + width: 100%; + z-index: 100; + border-radius: 0 0 var(--border-radius-circle-small) var(--border-radius-circle-small); + box-shadow: var(--shadow-strong); + ::-webkit-scrollbar { + width: 7px; + height: 7px; + } +} diff --git a/src/components/Search/SearchInput/index.tsx b/src/components/Search/SearchInput/index.tsx new file mode 100644 index 0000000000..bd975950c5 --- /dev/null +++ b/src/components/Search/SearchInput/index.tsx @@ -0,0 +1,132 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +import classNames from 'classnames'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; + +import styles from './SearchInput.module.scss'; + +import ExpandedSearchInputSection from '@/components/Search/CommandBar/ExpandedSearchInputSection'; +import TarteelVoiceSearchTrigger from '@/components/TarteelVoiceSearch/Trigger'; +import Input, { InputSize } from '@/dls/Forms/Input'; +import KeyboardInput from '@/dls/KeyboardInput'; +import useOutsideClickDetector from '@/hooks/useOutsideClickDetector'; +import SearchIcon from '@/icons/search.svg'; +import { selectIsExpanded, setIsExpanded } from '@/redux/slices/CommandBar/state'; +import { setIsSearchDrawerOpen, setDisableSearchDrawerTransition } from '@/redux/slices/navbar'; +import { + selectIsCommandBarVoiceFlowStarted, + startSearchDrawerVoiceFlow, +} from '@/redux/slices/voiceSearch'; +import { logButtonClick } from '@/utils/eventLogger'; +import { isMobile } from '@/utils/responsive'; + +type Props = { + placeholder?: string; + initialSearchQuery?: string; + shouldExpandOnClick?: boolean; + shouldOpenDrawerOnMobile?: boolean; +}; + +const SearchInput: React.FC = ({ + placeholder, + initialSearchQuery, + shouldExpandOnClick = false, + shouldOpenDrawerOnMobile = false, +}) => { + const isVoiceSearchFlowStarted = useSelector(selectIsCommandBarVoiceFlowStarted, shallowEqual); + const [searchQuery, setSearchQuery] = useState(initialSearchQuery || ''); + const isExpanded = useSelector(selectIsExpanded); + const dispatch = useDispatch(); + const containerRef = useRef(null); + const collapseContainer = useCallback(() => { + dispatch({ type: setIsExpanded.type, payload: false }); + }, [dispatch]); + useOutsideClickDetector(containerRef, collapseContainer, isExpanded); + useHotkeys('Escape', collapseContainer, { enabled: isExpanded, enableOnFormTags: ['INPUT'] }); + + useEffect(() => { + if (initialSearchQuery) { + setSearchQuery(initialSearchQuery); + } + }, [initialSearchQuery]); + + /** + * Handle when the search query is changed. + * + * @param {string} newSearchQuery + * @returns {void} + */ + const onSearchQueryChange = (newSearchQuery: string): void => { + setSearchQuery(newSearchQuery); + dispatch({ type: setIsExpanded.type, payload: !!newSearchQuery }); + }; + + const onClearClicked = () => { + logButtonClick('search_input_clear_query'); + setSearchQuery(''); + }; + + const shouldSearchBeInSearchDrawer = shouldOpenDrawerOnMobile && isMobile(); + const onTarteelTriggerClicked = (startFlow: boolean) => { + logButtonClick( + // eslint-disable-next-line i18next/no-literal-string + `search_input_voice_search_${startFlow ? 'start' : 'stop'}_flow`, + ); + if (shouldSearchBeInSearchDrawer) { + dispatch({ type: setDisableSearchDrawerTransition.type, payload: true }); + dispatch({ type: setIsSearchDrawerOpen.type, payload: true }); + dispatch({ type: startSearchDrawerVoiceFlow.type, payload: false }); + } else { + dispatch({ type: setIsExpanded.type, payload: true }); + } + }; + + const onInputClick = () => { + if (shouldSearchBeInSearchDrawer) { + dispatch({ type: setDisableSearchDrawerTransition.type, payload: true }); + dispatch({ type: setIsSearchDrawerOpen.type, payload: true }); + } else if (shouldExpandOnClick) { + dispatch({ type: setIsExpanded.type, payload: true }); + } + }; + + return ( +
+
+ } + prefixSuffixContainerClassName={styles.prefixSuffixContainer} + containerClassName={styles.input} + suffix={ + <> + + + + } + shouldUseDefaultStyles={false} + fixedWidth={false} + size={InputSize.Large} + /> +
+ {isExpanded && ( +
+ +
+ )} +
+ ); +}; + +export default SearchInput; diff --git a/src/components/Search/SearchResults/SearchResultItem/SearchResultItem.module.scss b/src/components/Search/SearchResults/SearchResultItem/SearchResultItem.module.scss new file mode 100644 index 0000000000..556877bb02 --- /dev/null +++ b/src/components/Search/SearchResults/SearchResultItem/SearchResultItem.module.scss @@ -0,0 +1,32 @@ +.iconContainer { + padding-block-start: var(--spacing-micro); + padding-inline-end: var(--spacing-small); +} + +.container { + padding-block: var(--spacing-xsmall); + padding-inline: var(--spacing-xsmall); + &:hover { + background-color: var(--color-background-alternative-faded); + border-radius: var(--border-radius-default); + } +} + +.linkContainer { + display: flex; + flex-direction: row; +} + +.arabic { + direction: rtl; +} + +.resultText { + em { + font-weight: var(--font-weight-bold); + color: var(--color-highlight-dark); + } + sup { + display: none; + } +} diff --git a/src/components/Search/SearchResults/SearchResultItem/index.tsx b/src/components/Search/SearchResults/SearchResultItem/index.tsx new file mode 100644 index 0000000000..5938994ad0 --- /dev/null +++ b/src/components/Search/SearchResults/SearchResultItem/index.tsx @@ -0,0 +1,62 @@ +/* eslint-disable react/no-danger */ +import React, { useContext } from 'react'; + +import classNames from 'classnames'; +import useTranslation from 'next-translate/useTranslation'; + +import SearchResultItemIcon from '../SearchResultItemIcon'; + +import styles from './SearchResultItem.module.scss'; + +import DataContext from '@/contexts/DataContext'; +import Link from '@/dls/Link/Link'; +import { SearchNavigationResult } from '@/types/Search/SearchNavigationResult'; +import SearchService from '@/types/Search/SearchService'; +import SearchQuerySource from '@/types/SearchQuerySource'; +import { logButtonClick } from '@/utils/eventLogger'; +import { resolveUrlBySearchNavigationType } from '@/utils/navigation'; +import { getResultType, getSearchNavigationResult } from '@/utils/search'; + +interface Props { + source: SearchQuerySource; + service: SearchService; + result: SearchNavigationResult; +} + +const SearchResultItem: React.FC = ({ source, service, result }) => { + const { t, lang } = useTranslation(); + const chaptersData = useContext(DataContext); + const type = getResultType(result); + const onResultItemClicked = () => { + logButtonClick(`search_result_item`, { + service, + source, + }); + }; + + const { + name, + key: resultKey, + isArabic, + } = getSearchNavigationResult(chaptersData, result, t, lang); + + const url = resolveUrlBySearchNavigationType(type, resultKey, true); + return ( +
+ +
+ +
+
+ +
+ ); +}; +export default SearchResultItem; diff --git a/src/components/Search/SearchResults/SearchResultItemIcon/index.tsx b/src/components/Search/SearchResults/SearchResultItemIcon/index.tsx new file mode 100644 index 0000000000..47fe482ce0 --- /dev/null +++ b/src/components/Search/SearchResults/SearchResultItemIcon/index.tsx @@ -0,0 +1,40 @@ +import NavigateToIcon from '@/icons/east.svg'; +import ArabicIcon from '@/icons/search/arabic.svg'; +import AyahRangeIcon from '@/icons/search/ayah-range.svg'; +import JuzIcon from '@/icons/search/juz.svg'; +import PageIcon from '@/icons/search/page.svg'; +import SurahIcon from '@/icons/search/surah.svg'; +import TranslationIcon from '@/icons/search/translation.svg'; +import TransliterationIcon from '@/icons/search/transliteration.svg'; +import SearchIcon from '@/icons/search.svg'; +import { SearchNavigationType } from '@/types/Search/SearchNavigationResult'; + +const TYPE_ICON_MAP = { + [SearchNavigationType.AYAH]: ArabicIcon, + [SearchNavigationType.SURAH]: SurahIcon, + [SearchNavigationType.JUZ]: JuzIcon, + [SearchNavigationType.PAGE]: PageIcon, + [SearchNavigationType.RANGE]: AyahRangeIcon, + // TODO: change this after it's ready + [SearchNavigationType.RUB_EL_HIZB]: ArabicIcon, + // TODO: change this after it's ready + [SearchNavigationType.HIZB]: ArabicIcon, + [SearchNavigationType.SEARCH_PAGE]: SearchIcon, + [SearchNavigationType.TRANSLITERATION]: TransliterationIcon, + [SearchNavigationType.TRANSLATION]: TranslationIcon, +}; + +interface Props { + type: SearchNavigationType; +} + +const SearchResultItemIcon = ({ type }: Props) => { + const Icon = TYPE_ICON_MAP[type]; + if (!type) { + return <>; + } + + return Icon ? : ; +}; + +export default SearchResultItemIcon; diff --git a/src/components/Search/SearchResults/SearchResults.module.scss b/src/components/Search/SearchResults/SearchResults.module.scss deleted file mode 100644 index 7075c92f5a..0000000000 --- a/src/components/Search/SearchResults/SearchResults.module.scss +++ /dev/null @@ -1,24 +0,0 @@ -.resultsSummaryContainer { - margin-block-start: var(--spacing-medium); - display: flex; - align-items: center; - justify-content: space-between; - padding-block-end: var(--spacing-medium); -} - -.header { - margin-block-start: var(--spacing-large); - text-transform: capitalize; - color: var(--color-text-faded); -} - -.showAll { - text-decoration: underline; -} - -.navigationItemsListContainer { - margin-block-start: var(--spacing-large); -} -.navigationItemContainer { - margin-inline-end: var(--spacing-small); -} diff --git a/src/components/Search/SearchResults/SearchResultsHeader/SearchResultsHeader.module.scss b/src/components/Search/SearchResults/SearchResultsHeader/SearchResultsHeader.module.scss new file mode 100644 index 0000000000..9b9311db94 --- /dev/null +++ b/src/components/Search/SearchResults/SearchResultsHeader/SearchResultsHeader.module.scss @@ -0,0 +1,38 @@ +.showAll { + font-weight: var(--font-weight-bold); +} + +.commandPrefix { + margin-inline-start: var(--spacing-xxsmall); + display: flex; + align-items: center; + > svg { + width: var(--spacing-large); + height: var(--spacing-large); + } +} + +.moreResultsContainer { + display: flex; + cursor: pointer; + align-items: center; + font-size: var(--font-size-small); + svg { + width: var(--spacing-medium) !important; + height: var(--spacing-medium) !important; + path { + fill: var(--color-text-faded) !important; + } + } +} + +.resultsSummaryContainer { + display: flex; + align-items: center; + justify-content: space-between; + padding-block-end: var(--spacing-small); +} + +.resultsSummary { + font-size: var(--font-size-small); +} diff --git a/src/components/Search/SearchResults/SearchResultsHeader/index.tsx b/src/components/Search/SearchResults/SearchResultsHeader/index.tsx new file mode 100644 index 0000000000..f13e8dd3dc --- /dev/null +++ b/src/components/Search/SearchResults/SearchResultsHeader/index.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { useRouter } from 'next/router'; +import useTranslation from 'next-translate/useTranslation'; +import { useDispatch } from 'react-redux'; + +import styles from './SearchResultsHeader.module.scss'; + +import IconContainer from '@/dls/IconContainer/IconContainer'; +import NavigateIcon from '@/icons/east.svg'; +import { setIsExpanded } from '@/redux/slices/CommandBar/state'; +import SearchQuerySource from '@/types/SearchQuerySource'; +import { logButtonClick } from '@/utils/eventLogger'; +import { getSearchQueryNavigationUrl } from '@/utils/navigation'; + +type Props = { + searchQuery: string; + onSearchResultClicked?: () => void; + source: SearchQuerySource; +}; + +const SearchResultsHeader: React.FC = ({ searchQuery, onSearchResultClicked, source }) => { + const { t } = useTranslation(); + const router = useRouter(); + const dispatch = useDispatch(); + + const onNavigationLinkClicked = () => { + router.push(getSearchQueryNavigationUrl(searchQuery)).then(() => { + dispatch({ type: setIsExpanded.type, payload: false }); + if (onSearchResultClicked) { + onSearchResultClicked(); + } + logButtonClick(`${source}_show_all`); + }); + }; + return ( +
+

{t('common:search-results-no-count')}

+
+
+

{t('common:search.more-results')}

+ + } /> + +
+
+
+ ); +}; + +export default SearchResultsHeader; diff --git a/src/components/Search/SearchResults/index.tsx b/src/components/Search/SearchResults/index.tsx index 88650d699d..ac5ff25a50 100644 --- a/src/components/Search/SearchResults/index.tsx +++ b/src/components/Search/SearchResults/index.tsx @@ -3,104 +3,73 @@ import React from 'react'; import useTranslation from 'next-translate/useTranslation'; import SearchResultItem from './SearchResultItem'; -import styles from './SearchResults.module.scss'; +import SearchResultsHeader from './SearchResultsHeader'; -import NavigationItem from '@/components/Search/NavigationItem'; -import Link from '@/dls/Link/Link'; import Pagination from '@/dls/Pagination/Pagination'; import SearchQuerySource from '@/types/SearchQuerySource'; -import { logButtonClick } from '@/utils/eventLogger'; import { toLocalizedNumber } from '@/utils/locale'; import { SearchResponse } from 'types/ApiResponses'; interface Props { searchResult: SearchResponse; searchQuery: string; - isSearchDrawer?: boolean; currentPage?: number; pageSize?: number; onPageChange?: (page: number) => void; onSearchResultClicked?: () => void; + source: SearchQuerySource; } const SearchResults: React.FC = ({ searchResult, searchQuery, - isSearchDrawer = true, + source, currentPage, onPageChange, pageSize, onSearchResultClicked, }) => { - const { t, lang } = useTranslation(); + const results = searchResult.result.navigation.concat(searchResult.result.verses); + const isSearchDrawer = source === SearchQuerySource.SearchDrawer; + const { t, lang } = useTranslation('common'); return ( - <> -
- {!!searchResult.result.navigation?.length && ( -
- {searchResult.result.navigation.map((navigationResult) => ( - - - - ))} -
- )} -

- {t('common:search-results', { - count: toLocalizedNumber(searchResult.pagination.totalRecords, lang), - })} -

+
+ {isSearchDrawer ? ( + + ) : ( <> - {searchResult.result.verses.map((result) => ( - - ))} - {isSearchDrawer ? ( -
-

- {toLocalizedNumber(searchResult.pagination.totalRecords, lang)}{' '} - {t('common:search.results')} -

- {searchResult.pagination.totalRecords > 0 && ( - { - if (onSearchResultClicked) onSearchResultClicked(); - logButtonClick('search_drawer_show_all'); - }} - > - -

{t('common:search.show-all')}

-
- - )} -
- ) : ( + {searchQuery && ( <> - {searchQuery && ( - - )} + {t('search-results', { + count: toLocalizedNumber(searchResult.pagination.totalRecords, lang), + })} )} -
- + )} + <> + {results.map((result) => ( + + ))} + + {!isSearchDrawer && !!searchQuery && ( + + )} +
); }; diff --git a/src/components/TarteelAttribution/TarteelAttribution.module.scss b/src/components/TarteelAttribution/TarteelAttribution.module.scss index be6f63411b..a012cdfb9d 100644 --- a/src/components/TarteelAttribution/TarteelAttribution.module.scss +++ b/src/components/TarteelAttribution/TarteelAttribution.module.scss @@ -19,3 +19,8 @@ fill: var(--color-text-default); } } + +.attribution { + display: flex; + flex-direction: row-reverse; +} diff --git a/src/components/TarteelAttribution/TarteelAttribution.tsx b/src/components/TarteelAttribution/TarteelAttribution.tsx index c10b86ecc3..621f394a85 100644 --- a/src/components/TarteelAttribution/TarteelAttribution.tsx +++ b/src/components/TarteelAttribution/TarteelAttribution.tsx @@ -17,15 +17,17 @@ const TarteelAttribution: React.FC = ({ isCommandBar = false }) => { logTarteelLinkClick(isCommandBar ? 'command_bar' : 'search_drawer'); }; return ( - -
- {t('voice.voice-search-powered-by')} - - - - -
- +
+ +
+ {t('voice.voice-search-powered-by')} + + + + +
+ +
); }; diff --git a/src/components/TarteelVoiceSearch/BodyContainer/Error/index.tsx b/src/components/TarteelVoiceSearch/BodyContainer/Error/index.tsx index faf991c5ee..cddec2b124 100644 --- a/src/components/TarteelVoiceSearch/BodyContainer/Error/index.tsx +++ b/src/components/TarteelVoiceSearch/BodyContainer/Error/index.tsx @@ -4,26 +4,18 @@ import useTranslation from 'next-translate/useTranslation'; import styles from './Error.module.scss'; -import Link, { LinkVariant } from '@/dls/Link/Link'; import ErrorIcon from '@/icons/info.svg'; import MicrophoneIcon from '@/icons/microphone.svg'; import NoMicrophoneIcon from '@/icons/no-mic.svg'; -import { logTarteelLinkClick } from '@/utils/eventLogger'; import VoiceError from 'types/Tarteel/VoiceError'; interface Props { error: VoiceError; isWaitingForPermission: boolean; - isCommandBar: boolean; } -const Error: React.FC = ({ error, isWaitingForPermission, isCommandBar }) => { +const Error: React.FC = ({ error, isWaitingForPermission }) => { const { t } = useTranslation('common'); - - const onTarteelLinkClicked = () => { - // eslint-disable-next-line i18next/no-literal-string - logTarteelLinkClick(`${isCommandBar ? 'command_bar' : 'search_drawer'}_error`); - }; let icon = null; let errorBody = null; if (isWaitingForPermission) { @@ -48,14 +40,6 @@ const Error: React.FC = ({ error, isWaitingForPermission, isCommandBar }) errorBody = (
{errorText} - - {t('tarteel.app')} -
); } diff --git a/src/components/TarteelVoiceSearch/BodyContainer/SearchResults.tsx b/src/components/TarteelVoiceSearch/BodyContainer/SearchResults.tsx index 060d438106..ca44c9c8a9 100644 --- a/src/components/TarteelVoiceSearch/BodyContainer/SearchResults.tsx +++ b/src/components/TarteelVoiceSearch/BodyContainer/SearchResults.tsx @@ -1,21 +1,21 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useContext } from 'react'; import groupBy from 'lodash/groupBy'; import useTranslation from 'next-translate/useTranslation'; import { useSelector } from 'react-redux'; -import CommandsList from '@/components/CommandBar/CommandsList'; import DataFetcher from '@/components/DataFetcher'; -import SearchResultItem from '@/components/Search/SearchResults/SearchResultItem'; +import CommandsList from '@/components/Search/CommandBar/CommandsList'; +import TarteelSearchResultItem from '@/components/TarteelVoiceSearch/TarteelSearchResultItem'; +import DataContext from '@/contexts/DataContext'; import { selectSelectedTranslations } from '@/redux/slices/QuranReader/translations'; import SearchService from '@/types/Search/SearchService'; import SearchQuerySource from '@/types/SearchQuerySource'; import { makeVersesFilterUrl } from '@/utils/apiPaths'; import { areArraysEqual } from '@/utils/array'; -import { toLocalizedVerseKey } from '@/utils/locale'; -import { truncateString } from '@/utils/string'; +import { getResultSuffix } from '@/utils/search'; import { VersesResponse } from 'types/ApiResponses'; -import { SearchNavigationType } from 'types/SearchNavigationResult'; +import { SearchNavigationType } from 'types/Search/SearchNavigationResult'; import SearchResult from 'types/Tarteel/SearchResult'; interface Props { @@ -26,6 +26,7 @@ interface Props { const SearchResults: React.FC = ({ searchResult, isCommandBar }) => { const selectedTranslations = useSelector(selectSelectedTranslations, areArraysEqual); const { t, lang } = useTranslation('common'); + const chaptersData = useContext(DataContext); const params = { // only get the first 10 results @@ -52,10 +53,13 @@ const SearchResults: React.FC = ({ searchResult, isCommandBar }) => { return { key: verse.verseKey, resultType: SearchNavigationType.AYAH, - name: `[${toLocalizedVerseKey(verse.verseKey, lang)}] ${truncateString( - verse.textUthmani, - 80, + name: `${verse.textUthmani} ${getResultSuffix( + SearchNavigationType.AYAH, + verse.verseKey, + lang, + chaptersData, )}`, + isVoiceSearch: true, group: t('command-bar.navigations'), }; }); @@ -75,7 +79,7 @@ const SearchResults: React.FC = ({ searchResult, isCommandBar }) => { return ( <> {data.verses.map((verse) => ( - = ({ searchResult, isCommandBar }) => { ); }, - [isCommandBar, lang, t], + [chaptersData, isCommandBar, lang, t], ); return ; diff --git a/src/components/TarteelVoiceSearch/BodyContainer/index.tsx b/src/components/TarteelVoiceSearch/BodyContainer/index.tsx index f39360cd99..a0f1599985 100644 --- a/src/components/TarteelVoiceSearch/BodyContainer/index.tsx +++ b/src/components/TarteelVoiceSearch/BodyContainer/index.tsx @@ -54,11 +54,7 @@ const VoiceSearchBodyContainer: React.FC = ({ isCommandBar = false }) => [styles.container]: !isCommandBar, })} > - +
); } diff --git a/src/components/Search/SearchResults/SearchResultItem.module.scss b/src/components/TarteelVoiceSearch/TarteelSearchResultItem/TarteelSearchResultItem.module.scss similarity index 96% rename from src/components/Search/SearchResults/SearchResultItem.module.scss rename to src/components/TarteelVoiceSearch/TarteelSearchResultItem/TarteelSearchResultItem.module.scss index 6df1512eae..f81fefdef3 100644 --- a/src/components/Search/SearchResults/SearchResultItem.module.scss +++ b/src/components/TarteelVoiceSearch/TarteelSearchResultItem/TarteelSearchResultItem.module.scss @@ -10,6 +10,7 @@ margin-inline-end: 0; em { font-weight: var(--font-weight-semibold); + text-decoration: underline; } } @@ -26,12 +27,6 @@ margin-inline-end: auto; } -.quranTextContainer { -} - -.verseLink { -} - .quranTextResult { font-size: var(--font-size-xlarge); line-height: var(--line-height-large); diff --git a/src/components/Search/SearchResults/SearchResultItem.tsx b/src/components/TarteelVoiceSearch/TarteelSearchResultItem/index.tsx similarity index 92% rename from src/components/Search/SearchResults/SearchResultItem.tsx rename to src/components/TarteelVoiceSearch/TarteelSearchResultItem/index.tsx index 50360d057e..bfe05c8195 100644 --- a/src/components/Search/SearchResults/SearchResultItem.tsx +++ b/src/components/TarteelVoiceSearch/TarteelSearchResultItem/index.tsx @@ -1,10 +1,9 @@ /* eslint-disable react/no-danger */ - import React, { useMemo } from 'react'; import useTranslation from 'next-translate/useTranslation'; -import styles from './SearchResultItem.module.scss'; +import styles from './TarteelSearchResultItem.module.scss'; import Link from '@/dls/Link/Link'; import QuranWord from '@/dls/QuranWord/QuranWord'; @@ -24,7 +23,11 @@ interface Props { service?: SearchService; } -const SearchResultItem: React.FC = ({ result, source, service = SearchService.QDC }) => { +const TarteelSearchResultItem: React.FC = ({ + result, + source, + service = SearchService.Tarteel, +}) => { const { lang } = useTranslation('quran-reader'); const localizedVerseKey = useMemo( () => toLocalizedVerseKey(result.verseKey, lang), @@ -81,4 +84,4 @@ const SearchResultItem: React.FC = ({ result, source, service = SearchSer
); }; -export default SearchResultItem; +export default TarteelSearchResultItem; diff --git a/src/components/dls/Forms/Input/Input.module.scss b/src/components/dls/Forms/Input/Input.module.scss index cd5d82a4ab..c0cf5e8028 100644 --- a/src/components/dls/Forms/Input/Input.module.scss +++ b/src/components/dls/Forms/Input/Input.module.scss @@ -126,13 +126,3 @@ padding-inline-start: var(--spacing-xsmall); padding-inline-end: var(--spacing-xxsmall); } - -.suffix { - border-inline-start: 1px solid var(--color-background-alternative-deep); - border-start-end-radius: var(--border-radius-default); - border-end-end-radius: var(--border-radius-default); - background: var(--color-background-alternative-medium); - - padding-inline-start: var(--spacing-xxsmall); - padding-inline-end: var(--spacing-xsmall); -} diff --git a/src/components/dls/Forms/Input/Suffix/Suffix.module.scss b/src/components/dls/Forms/Input/Suffix/Suffix.module.scss new file mode 100644 index 0000000000..523e8d6712 --- /dev/null +++ b/src/components/dls/Forms/Input/Suffix/Suffix.module.scss @@ -0,0 +1,9 @@ +.suffix { + border-inline-start: 1px solid var(--color-background-alternative-deep); + border-start-end-radius: var(--border-radius-default); + border-end-end-radius: var(--border-radius-default); + background: var(--color-background-alternative-medium); + + padding-inline-start: var(--spacing-xxsmall); + padding-inline-end: var(--spacing-xsmall); +} diff --git a/src/components/dls/Forms/Input/Suffix/index.tsx b/src/components/dls/Forms/Input/Suffix/index.tsx new file mode 100644 index 0000000000..321f42a12e --- /dev/null +++ b/src/components/dls/Forms/Input/Suffix/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import classNames from 'classnames'; + +import styles from './Suffix.module.scss'; + +type Props = { + suffix: React.ReactNode; + suffixContainerClassName?: string; + shouldUseDefaultStyles?: boolean; +}; + +const InputSuffix: React.FC = ({ + suffix, + suffixContainerClassName, + shouldUseDefaultStyles = true, +}) => { + return ( + <> + {suffix && ( +
+ {suffix} +
+ )} + + ); +}; + +export default InputSuffix; diff --git a/src/components/dls/Forms/Input/index.tsx b/src/components/dls/Forms/Input/index.tsx index e6be0b4e3c..b6ec3671b6 100644 --- a/src/components/dls/Forms/Input/index.tsx +++ b/src/components/dls/Forms/Input/index.tsx @@ -14,6 +14,7 @@ import classNames from 'classnames'; import Button, { ButtonShape, ButtonSize, ButtonVariant } from '../../Button/Button'; import styles from './Input.module.scss'; +import InputSuffix from './Suffix'; import ClearIcon from '@/icons/close.svg'; @@ -45,6 +46,7 @@ interface Props { suffix?: ReactNode; onClearClicked?: () => void; onChange?: (value: string) => void; + onClick?: () => void; onKeyDown?: (event: KeyboardEvent) => void; inputMode?: HTMLAttributes['inputMode']; value?: string; @@ -56,6 +58,8 @@ interface Props { htmlType?: React.HTMLInputTypeAttribute; isRequired?: boolean; inputRef?: RefObject; + prefixSuffixContainerClassName?: string; + shouldUseDefaultStyles?: boolean; } const Input: React.FC = ({ @@ -74,13 +78,16 @@ const Input: React.FC = ({ onClearClicked, onChange, onKeyDown, + onClick, inputMode, value = '', shouldFlipOnRTL = true, containerClassName, + prefixSuffixContainerClassName, htmlType, isRequired, inputRef, + shouldUseDefaultStyles = true, }) => { const [inputValue, setInputValue] = useState(value); // listen to any change in value in-case the value gets populated after and API call. @@ -96,6 +103,25 @@ const Input: React.FC = ({ } }; + const handleClick = () => { + if (onClick) { + onClick(); + } + }; + + // eslint-disable-next-line react/no-multi-comp + const Suffix = () => ( + <> + {suffix && ( + + )} + + ); + return ( <> {label &&

{label}

} @@ -105,7 +131,6 @@ const Input: React.FC = ({ [styles.mediumContainer]: size === InputSize.Medium, [styles.largeContainer]: size === InputSize.Large, [styles.fixedWidth]: fixedWidth, - [styles.disabled]: disabled, [styles.error]: type === InputType.Error, [styles.success]: type === InputType.Success, [styles.warning]: type === InputType.Warning, @@ -113,14 +138,24 @@ const Input: React.FC = ({ })} > {prefix && ( -
{prefix}
+
+ {prefix} +
)} = ({ /> {clearable ? ( <> - {inputValue && ( + {inputValue ? (
+ ) : ( + )} ) : ( - <> - {suffix && ( -
- {suffix} -
- )} - + )}
diff --git a/src/pages/search.module.scss b/src/pages/search.module.scss index 7150556c3d..fe07c5d191 100644 --- a/src/pages/search.module.scss +++ b/src/pages/search.module.scss @@ -7,38 +7,19 @@ $tablet-max-width: 50rem; // TODO: remove this when we remove banner @include breakpoints.smallerThanTablet { - padding-block-start: calc(var(--spacing-medium) + var(--banner-height)); + padding-block-start: calc(var(--spacing-micro) + var(--banner-height)); } margin-block-end: calc(2 * var(--spacing-mega)); } -.paginationContainer { - min-height: calc(2 * var(--spacing-mega)); -} - .searchInputContainer { - border: 1px solid var(--color-borders-hairline); - border-radius: var(--border-radius-pill); - padding-block-start: var(--spacing-xxsmall); - padding-block-end: var(--spacing-xxsmall); padding-inline-start: var(--spacing-xsmall); padding-inline-end: var(--spacing-xsmall); - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - height: var(--spacing-large); - margin-block-start: var(--spacing-medium); - margin-block-end: var(--spacing-medium); margin-inline-start: auto; margin-inline-end: auto; } -.rtlFlexDirection { - flex-direction: row-reverse; -} - .searchInput { border: 0; width: 90%; @@ -62,63 +43,9 @@ $tablet-max-width: 50rem; } .searchBodyContainer { - box-sizing: border-box; - padding-inline: var(--spacing-small); - max-width: 50rem; -} - -.filtersContainer { - display: flex; - align-items: center; - justify-content: space-between; - @include breakpoints.tablet { - margin-inline: var(--spacing-large); - } - margin-block: var(--spacing-xsmall); -} - -.headerInnerContainer { - padding-inline: calc(1.5 * var(--spacing-medium)); - @include breakpoints.tablet { - padding-inline: calc(2 * var(--spacing-mega)); - } - max-width: $tablet-max-width; - margin-inline: auto; -} - -.headerOuterContainer { - border-block-end: 1px solid var(--color-borders-hairline); - margin-block-end: var(--spacing-medium); -} - -.filterButton { - margin-inline-end: var(--spacing-medium); + padding-block-start: var(--spacing-small); } .searching { font-weight: var(--font-weight-bold); } - -.languagePopover, -.translationPopover { - display: inline-block; -} - -.translationFilterContainer { - min-width: calc(10 * var(--spacing-mega)); -} -.filterButton, -.resetButton { - text-transform: uppercase; -} - -.modalContainer { - display: flex; - align-items: center; - justify-content: space-between; -} - -.translationSearchContainer { - flex: 1; - margin-inline-end: var(--spacing-small); -} diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 8b9ac7172e..89084dc6fb 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -1,291 +1,129 @@ -/* eslint-disable react-func/max-lines-per-function */ /* eslint-disable max-lines */ -import { RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { GetStaticProps, NextPage } from 'next'; import { useRouter } from 'next/router'; import useTranslation from 'next-translate/useTranslation'; -import { useDispatch } from 'react-redux'; import styles from './search.module.scss'; -import { getAvailableLanguages, getAvailableTranslations } from '@/api'; +import { getAvailableLanguages, getNewSearchResults } from '@/api'; +import DataFetcher from '@/components/DataFetcher'; import NextSeoWrapper from '@/components/NextSeoWrapper'; -import TranslationsFilter from '@/components/Search/Filters/TranslationsFilter'; import SearchBodyContainer from '@/components/Search/SearchBodyContainer'; -import Button, { ButtonSize, ButtonVariant } from '@/dls/Button/Button'; -import ContentModal, { ContentModalSize } from '@/dls/ContentModal/ContentModal'; -import Input, { InputVariant } from '@/dls/Forms/Input'; +import SearchInput from '@/components/Search/SearchInput'; import useAddQueryParamsToUrl from '@/hooks/useAddQueryParamsToUrl'; -import useDebounce from '@/hooks/useDebounce'; -import useFocus from '@/hooks/useFocusElement'; -import FilterIcon from '@/icons/filter.svg'; -import SearchIcon from '@/icons/search.svg'; +import QueryParam from '@/types/QueryParam'; +import SearchResponse from '@/types/Search/SearchResponse'; +import SearchService from '@/types/Search/SearchService'; import SearchQuerySource from '@/types/SearchQuerySource'; +import { makeNewSearchResultsUrl } from '@/utils/apiPaths'; import { getAllChaptersData } from '@/utils/chapter'; -import { logButtonClick, logEvent, logValueChange } from '@/utils/eventLogger'; -import filterTranslations from '@/utils/filter-translations'; -import { getLanguageAlternates, toLocalizedNumber } from '@/utils/locale'; -import { getCanonicalUrl } from '@/utils/navigation'; import { - addToSearchHistory, - getDefaultTranslationIdsByLang, - searchGetResults, -} from '@/utils/search'; -import { SearchResponse } from 'types/ApiResponses'; + logEvent, + logTextSearchQuery, + logSearchResults, + logEmptySearchResults, +} from '@/utils/eventLogger'; +import { getLanguageAlternates } from '@/utils/locale'; +import { getCanonicalUrl } from '@/utils/navigation'; +import { getAdvancedSearchQuery } from '@/utils/search'; import AvailableLanguage from 'types/AvailableLanguage'; -import AvailableTranslation from 'types/AvailableTranslation'; import ChaptersData from 'types/ChaptersData'; const PAGE_SIZE = 10; -const DEBOUNCING_PERIOD_MS = 1000; -type SearchProps = { +type SearchPageProps = { languages: AvailableLanguage[]; - translations: AvailableTranslation[]; chaptersData: ChaptersData; }; -const Search: NextPage = ({ translations }): JSX.Element => { +const navigationUrl = '/search'; +const source = SearchQuerySource.SearchPage; + +const SearchPage: NextPage = (): JSX.Element => { const { t, lang } = useTranslation('common'); const router = useRouter(); - const [searchQuery, setSearchQuery] = useState(''); - const [focusInput, searchInputRef]: [() => void, RefObject] = useFocus(); - const [currentPage, setCurrentPage] = useState(1); - const [selectedLanguages, setSelectedLanguages] = useState(''); - const [selectedTranslations, setSelectedTranslations] = useState(() => { - return getDefaultTranslationIdsByLang(translations, lang) as string; + const [searchQuery, setSearchQuery] = useState(() => { + if (typeof window !== 'undefined') { + const params = new URLSearchParams(window.location.search); + return params.get(QueryParam.QUERY) || params.get(QueryParam.QUERY_OLD) || ''; + } + return ''; }); - const [translationSearchQuery, setTranslationSearchQuery] = useState(''); - const [isContentModalOpen, setIsContentModalOpen] = useState(false); - const [isSearching, setIsSearching] = useState(false); - const [hasError, setHasError] = useState(false); - const [searchResult, setSearchResult] = useState(null); - // Debounce search query to avoid having to call the API on every type. The API will be called once the user stops typing. - const debouncedSearchQuery = useDebounce(searchQuery, DEBOUNCING_PERIOD_MS); - const dispatch = useDispatch(); - // the query params that we want added to the url - const queryParams = useMemo( - () => ({ - page: currentPage, - languages: selectedLanguages, - q: debouncedSearchQuery, - translations: selectedTranslations, - }), - [currentPage, debouncedSearchQuery, selectedLanguages, selectedTranslations], - ); - useAddQueryParamsToUrl('/search', queryParams); - - // We need this since pages that are statically optimized will be hydrated - // without their route parameters provided, i.e query will be an empty object ({}). - // After hydration, Next.js will trigger an update to provide the route parameters - // in the query object. @see https://nextjs.org/docs/routing/dynamic-routes#caveats - useEffect(() => { - // we don't want to focus the main search input when the translation filter modal is open. - if (router.isReady && !isContentModalOpen) { - focusInput(); + const [currentPage, setCurrentPage] = useState(() => { + if (typeof window !== 'undefined') { + const params = new URLSearchParams(window.location.search); + return Number(params.get(QueryParam.PAGE)) || 1; } - }, [focusInput, router, isContentModalOpen]); + return 1; + }); - // handle when language changes + // Handle URL changes (both initial load and navigation) useEffect(() => { - setSelectedTranslations(getDefaultTranslationIdsByLang(translations, lang) as string); - }, [lang, translations]); + if (!router.isReady) return; - useEffect(() => { - if (router.query.q || router.query.query) { - let query = router.query.q as string; - if (router.query.query) { - query = router.query.query as string; - } - setSearchQuery(query); - } + const query = router.query[QueryParam.QUERY] || router.query[QueryParam.QUERY_OLD]; + const page = Number(router.query[QueryParam.PAGE]) || 1; - if (router.query.page) { - setCurrentPage(Number(router.query.page)); - } - if (router.query.languages) { - setSelectedLanguages(router.query.languages as string); + if (query) { + setSearchQuery(query as string); + setCurrentPage(page); } - if (router.query.translations) { - setSelectedTranslations(router.query.translations as string); - } - }, [ - router.query.q, - router.query.query, - router.query.page, - router.query.languages, - router.query.translations, - ]); - - /** - * Handle when the search query is changed. - * - * @param {string} newSearchQuery - * @returns {void} - */ - const onSearchQueryChange = (newSearchQuery: string): void => { - setSearchQuery(newSearchQuery || ''); - }; - - const onClearClicked = () => { - logButtonClick('search_page_clear_query'); - setSearchQuery(''); - }; - - /** - * Call BE to fetch the results using the passed filters. - * - * @param {string} query - * @param {number} page - * @param {string} translation - * @param {string} language - */ - const getResults = useCallback( - (query: string, page: number, translation: string, language: string) => { - searchGetResults( - SearchQuerySource.SearchPage, - query, - page, - PAGE_SIZE, - setIsSearching, - setHasError, - setSearchResult, - language, - translation, - ); - }, - [], - ); - - // a ref to know whether this is the initial search request made when the user loads the page or not - const isInitialSearch = useRef(true); - - // listen to any changes in the API params and call BE on change. - useEffect(() => { - // only when the search query has a value we call the API. - if (debouncedSearchQuery) { - // we don't want to reset pagination when the user reloads the page with a ?page={number} in the url query - if (!isInitialSearch.current) { - setCurrentPage(1); - } - - addToSearchHistory(dispatch, debouncedSearchQuery, SearchQuerySource.SearchPage); - - getResults( - debouncedSearchQuery, - // if it is the initial search request, use the page number in the url, otherwise, reset it - isInitialSearch.current ? currentPage : 1, - selectedTranslations, - selectedLanguages, - ); - - // if it was the initial request, update the ref - if (isInitialSearch.current) { - isInitialSearch.current = false; - } - } - // we don't want to run this effect when currentPage is changed - // because we are already handeling this in onPageChange - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedSearchQuery, getResults, selectedLanguages, selectedTranslations]); + }, [router.isReady, router.query]); const onPageChange = (page: number) => { logEvent('search_page_number_change', { page }); setCurrentPage(page); - getResults(debouncedSearchQuery, page, selectedTranslations, selectedLanguages); }; - const onTranslationChange = useCallback((translationIds: string[]) => { - // convert the array into a string - setSelectedTranslations((prevTranslationIds) => { - // filter out the empty strings - const newTranslationIds = translationIds.filter((id) => id !== '').join(','); - logValueChange('search_page_selected_translations', prevTranslationIds, newTranslationIds); - return newTranslationIds; - }); - // reset the current page since most probable the results will change. - setCurrentPage(1); - }, []); - - const onSearchKeywordClicked = useCallback((keyword: string) => { - setSearchQuery(keyword); - }, []); - - const navigationUrl = '/search'; - - const formattedSelectedTranslations = useMemo(() => { - if (!selectedTranslations) return t('search:default-translations'); - - let selectedValueString; - - const selectedTranslationsArray = selectedTranslations.split(','); - - const firstSelectedTranslation = translations.find( - (translation) => translation.id.toString() === selectedTranslationsArray[0], - ); - - if (!firstSelectedTranslation) return t('search:all-translations'); + const queryParams = useMemo( + () => ({ + [QueryParam.PAGE]: currentPage, + [QueryParam.QUERY]: searchQuery || undefined, + }), + [currentPage, searchQuery], + ); + useAddQueryParamsToUrl(navigationUrl, queryParams); + + const REQUEST_PARAMS = getAdvancedSearchQuery(searchQuery, currentPage, PAGE_SIZE); + const fetcher = async () => { + logTextSearchQuery(REQUEST_PARAMS.query, source); + + try { + const response = await getNewSearchResults(REQUEST_PARAMS); + const finalResponse = { + ...response, + service: SearchService.KALIMAT, + }; + + if (response.pagination.totalRecords === 0) { + logEmptySearchResults({ + query: searchQuery, + source, + service: SearchService.KALIMAT, + }); + } else { + logSearchResults({ + query: searchQuery, + source, + service: SearchService.KALIMAT, + }); + } - if (selectedTranslationsArray.length === 1) { - selectedValueString = firstSelectedTranslation.name; + return finalResponse; + } catch (error) { + throw new Error('Search failed'); } - if (selectedTranslationsArray.length === 2) { - selectedValueString = t('settings.value-and-other', { - value: firstSelectedTranslation?.name, - othersCount: toLocalizedNumber(selectedTranslationsArray.length - 1, lang), - }); - } - if (selectedTranslationsArray.length > 2) { - selectedValueString = t('settings.value-and-others', { - value: firstSelectedTranslation?.name, - othersCount: toLocalizedNumber(selectedTranslationsArray.length - 1, lang), - }); - } - - return selectedValueString; - }, [lang, selectedTranslations, t, translations]); - - const filteredTranslations = translationSearchQuery - ? filterTranslations(translations, translationSearchQuery) - : translations; - - const onResetButtonClicked = () => { - logButtonClick('search_page_reset_button'); - const defaultLangTranslationIds = getDefaultTranslationIdsByLang( - translations, - lang, - false, - ) as string[]; - onTranslationChange(defaultLangTranslationIds); - }; - - const onTranslationSearchQueryChange = (newTranslationSearchQuery: string) => { - logValueChange( - 'search_page_translation_search_query', - translationSearchQuery, - newTranslationSearchQuery, - ); - setTranslationSearchQuery(newTranslationSearchQuery); - }; - - const onTranslationSearchClearClicked = () => { - logButtonClick('search_page_translation_search_clear'); - setTranslationSearchQuery(''); - }; - - const onTranslationsFiltersClicked = () => { - logButtonClick('search_page_translation_filter'); - setIsContentModalOpen(true); }; return ( <> = ({ translations }): JSX.Element => { languageAlternates={getLanguageAlternates(navigationUrl)} />
-
-
- } - onChange={onSearchQueryChange} - onClearClicked={onClearClicked} - inputRef={searchInputRef} - clearable - value={searchQuery} - disabled={isSearching} - placeholder={t('search.title')} - fixedWidth={false} - variant={InputVariant.Main} - /> - -
- } - onChange={onTranslationSearchQueryChange} - onClearClicked={onTranslationSearchClearClicked} - clearable - value={translationSearchQuery} - placeholder={t('settings.search-translations')} - fixedWidth={false} - variant={InputVariant.Main} - /> -
- -
- } - isOpen={isContentModalOpen} - onClose={() => setIsContentModalOpen(false)} - > - - -
- -
- {/* eslint-disable-next-line i18next/no-literal-string */} - {t('search:searching-translations')}: - {formattedSelectedTranslations} -
-
-
+
+
- { + return ( + + ); + }} + fetcher={fetcher} />
@@ -385,32 +165,22 @@ const Search: NextPage = ({ translations }): JSX.Element => { export const getStaticProps: GetStaticProps = async ({ locale }) => { try { - const [availableLanguagesResponse, availableTranslationsResponse] = await Promise.all([ - getAvailableLanguages(locale), - getAvailableTranslations(locale), - ]); + const availableLanguagesResponse = await getAvailableLanguages(locale); - let translations = []; let languages = []; if (availableLanguagesResponse.status !== 500) { const { languages: responseLanguages } = availableLanguagesResponse; languages = responseLanguages; } - if (availableTranslationsResponse.status !== 500) { - const { translations: responseTranslations } = availableTranslationsResponse; - translations = responseTranslations; - } const chaptersData = await getAllChaptersData(locale); return { props: { chaptersData, languages, - translations, }, }; } catch (e) { - console.log(e); return { props: { hasError: true, @@ -419,4 +189,4 @@ export const getStaticProps: GetStaticProps = async ({ locale }) => { } }; -export default Search; +export default SearchPage; diff --git a/src/redux/slices/CommandBar/persistConfig.ts b/src/redux/slices/CommandBar/persistConfig.ts index d9b070ed23..f6fc7a4c05 100644 --- a/src/redux/slices/CommandBar/persistConfig.ts +++ b/src/redux/slices/CommandBar/persistConfig.ts @@ -6,7 +6,7 @@ const commandBarPersistConfig = { key: SliceName.COMMAND_BAR, storage, version: 1, - blacklist: ['isOpen'], + blacklist: ['isExpanded'], }; export default commandBarPersistConfig; diff --git a/src/redux/slices/CommandBar/state.ts b/src/redux/slices/CommandBar/state.ts index 659967cd5c..596109ac7e 100644 --- a/src/redux/slices/CommandBar/state.ts +++ b/src/redux/slices/CommandBar/state.ts @@ -2,29 +2,21 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { RootState } from '@/redux/RootState'; import SliceName from '@/redux/types/SliceName'; -import { SearchNavigationResult } from 'types/SearchNavigationResult'; +import { SearchNavigationResult } from 'types/Search/SearchNavigationResult'; export type CommandBar = { - isOpen: boolean; recentNavigations: SearchNavigationResult[]; + isExpanded: boolean; }; const MAXIMUM_RECENT_NAVIGATIONS = 5; -const initialState: CommandBar = { isOpen: false, recentNavigations: [] }; +const initialState: CommandBar = { recentNavigations: [], isExpanded: false }; export const commandBarSlice = createSlice({ name: SliceName.COMMAND_BAR, initialState, reducers: { - setIsOpen: (state: CommandBar, action: PayloadAction) => ({ - ...state, - isOpen: action.payload, - }), - toggleIsOpen: (state: CommandBar) => ({ - ...state, - isOpen: !state.isOpen, - }), addRecentNavigation: (state: CommandBar, action: PayloadAction) => { let newRecentNavigations = [...state.recentNavigations]; const newRecentNavigation = action.payload; @@ -53,13 +45,24 @@ export const commandBarSlice = createSlice({ recentNavigations: newRecentNavigations, }; }, + toggleIsExpanded: (state: CommandBar) => { + return { + ...state, + isExpanded: !state.isExpanded, + }; + }, + setIsExpanded: (state: CommandBar, action: PayloadAction) => { + return { + ...state, + isExpanded: action.payload, + }; + }, }, }); -export const { setIsOpen, toggleIsOpen, addRecentNavigation, removeRecentNavigation } = +export const { addRecentNavigation, removeRecentNavigation, toggleIsExpanded, setIsExpanded } = commandBarSlice.actions; -export const selectCommandBarIsOpen = (state: RootState) => state.commandBar.isOpen; export const selectRecentNavigations = (state: RootState) => state.commandBar.recentNavigations; - +export const selectIsExpanded = (state: RootState) => state.commandBar.isExpanded; export default commandBarSlice.reducer; diff --git a/src/redux/slices/navbar.ts b/src/redux/slices/navbar.ts index 21544ba6f9..caf8c752dd 100644 --- a/src/redux/slices/navbar.ts +++ b/src/redux/slices/navbar.ts @@ -17,6 +17,7 @@ export type Navbar = { isSearchDrawerOpen: boolean; isSettingsDrawerOpen: boolean; settingsView: SettingsView; + disableSearchDrawerTransition: boolean; }; const initialState: Navbar = { @@ -25,6 +26,7 @@ const initialState: Navbar = { isSearchDrawerOpen: false, isSettingsDrawerOpen: false, settingsView: SettingsView.Body, + disableSearchDrawerTransition: false, }; export const navbarSlice = createSlice({ @@ -43,6 +45,10 @@ export const navbarSlice = createSlice({ ...state, isSearchDrawerOpen: action.payload, }), + toggleSearchDrawerIsOpen: (state: Navbar) => ({ + ...state, + isSearchDrawerOpen: !state.isSearchDrawerOpen, + }), setIsSettingsDrawerOpen: (state: Navbar, action: PayloadAction) => ({ ...state, isSettingsDrawerOpen: action.payload, @@ -51,6 +57,10 @@ export const navbarSlice = createSlice({ ...state, settingsView: action.payload, }), + setDisableSearchDrawerTransition: (state: Navbar, action: PayloadAction) => ({ + ...state, + disableSearchDrawerTransition: action.payload, + }), }, }); @@ -60,8 +70,11 @@ export const { setIsSearchDrawerOpen, setIsSettingsDrawerOpen, setSettingsView, + toggleSearchDrawerIsOpen, + setDisableSearchDrawerTransition, } = navbarSlice.actions; export const selectNavbar = (state: RootState) => state.navbar; +export const selectIsSearchDrawerOpen = (state: RootState) => state.navbar.isSearchDrawerOpen; export default navbarSlice.reducer; diff --git a/src/redux/slices/voiceSearch.ts b/src/redux/slices/voiceSearch.ts index fc33a69ad0..8e8a674f4f 100644 --- a/src/redux/slices/voiceSearch.ts +++ b/src/redux/slices/voiceSearch.ts @@ -22,13 +22,13 @@ export const voiceSearchSlice = createSlice({ ...state, isSearchDrawerVoiceFlowStarted: !state.isSearchDrawerVoiceFlowStarted, }), - stopSearchDrawerVoiceFlow: (state) => ({ + startSearchDrawerVoiceFlow: (state) => ({ ...state, - isSearchDrawerVoiceFlowStarted: false, + isSearchDrawerVoiceFlowStarted: true, }), - stopCommandBarVoiceFlow: (state) => ({ + stopSearchDrawerVoiceFlow: (state) => ({ ...state, - isCommandBardVoiceFlowStarted: false, + isSearchDrawerVoiceFlowStarted: false, }), toggleIsCommandBarVoiceFlowStarted: (state) => ({ ...state, @@ -44,7 +44,7 @@ export const selectIsCommandBarVoiceFlowStarted = (state: RootState) => export const { toggleIsSearchDrawerVoiceFlowStarted, toggleIsCommandBarVoiceFlowStarted, + startSearchDrawerVoiceFlow, stopSearchDrawerVoiceFlow, - stopCommandBarVoiceFlow, } = voiceSearchSlice.actions; export default voiceSearchSlice.reducer; diff --git a/src/styles/theme.scss b/src/styles/theme.scss index 0617782309..a374613897 100644 --- a/src/styles/theme.scss +++ b/src/styles/theme.scss @@ -38,6 +38,7 @@ --border-radius-default: 0.25rem; --border-radius-rounded: 0.5rem; --border-radius-circle: 50%; + --border-radius-circle-small: 30px; --opacity-10: 10%; --opacity-30: 30%; diff --git a/src/styles/themes/_dark.scss b/src/styles/themes/_dark.scss index e723347e08..0dcf2cdce9 100644 --- a/src/styles/themes/_dark.scss +++ b/src/styles/themes/_dark.scss @@ -57,6 +57,7 @@ --shadow-jumbo: 0 30px 60px var(--shade-9); --shadow-hover: 0 30px 60px var(--shade-9); --shadow-sticky: 0 12px 10px -10px var(--shade-9); + --shadow-strong: 0px 4px 4px 0px rgba(0, 0, 0, 0.5); font-palette: --Dark; // scrollbar styles diff --git a/src/styles/themes/_light.scss b/src/styles/themes/_light.scss index 15d4000dea..9c2c0a3383 100644 --- a/src/styles/themes/_light.scss +++ b/src/styles/themes/_light.scss @@ -49,6 +49,7 @@ --color-borders-hairline: rgb(235, 238, 240); --color-highlight: #79ffe1; + --color-highlight-dark: #22a5ad; --shadow-small: 0px 2px 4px rgba(0, 0, 0, 0.1); --shadow-normal: 0px 4px 8px rgba(0, 0, 0, 0.12); @@ -57,6 +58,7 @@ --shadow-jumbo: 0 30px 60px rgba(0, 0, 0, 0.12); --shadow-hover: 0 30px 60px rgba(0, 0, 0, 0.12); --shadow-sticky: 0 12px 10px -10px rgba(0, 0, 0, 0.12); + --shadow-strong: 0px 4px 4px 0px rgba(0, 0, 0, 0.5); font-palette: --Light; // scrollbar styles @@ -64,4 +66,4 @@ --scrollbar-thumb: var(--shade-4); --scrollbar-thumb-hover: var(--shade-6); --scrollbar-thumb-active: var(--shade-7); -} \ No newline at end of file +} diff --git a/src/styles/themes/_sepia.scss b/src/styles/themes/_sepia.scss index 222864d62a..43dd9bdba5 100644 --- a/src/styles/themes/_sepia.scss +++ b/src/styles/themes/_sepia.scss @@ -57,6 +57,7 @@ --shadow-jumbo: 0 30px 60px rgba(0, 0, 0, 0.12); --shadow-hover: 0 30px 60px rgba(0, 0, 0, 0.12); --shadow-sticky: 0 12px 10px -10px rgba(0, 0, 0, 0.12); + --shadow-strong: 0px 4px 4px 0px rgba(0, 0, 0, 0.5); font-palette: --Sepia; // scrollbar styles @@ -64,4 +65,4 @@ --scrollbar-thumb: #f0cd8c; --scrollbar-thumb-hover: #d4b478; --scrollbar-thumb-active: #8d774e; -} \ No newline at end of file +} diff --git a/src/utils/apiPaths.ts b/src/utils/apiPaths.ts index 5e971c1468..4632539033 100644 --- a/src/utils/apiPaths.ts +++ b/src/utils/apiPaths.ts @@ -11,7 +11,7 @@ import { } from '@/redux/defaultSettings/util'; import { MushafLines, QuranFont } from '@/types/QuranReader'; import { SearchRequestParams, SearchMode } from '@/types/Search/SearchRequestParams'; -import { AdvancedCopyRequest, PagesLookUpRequest, SearchRequest } from 'types/ApiRequests'; +import { AdvancedCopyRequest, PagesLookUpRequest } from 'types/ApiRequests'; export const DEFAULT_VERSES_PARAMS = { words: true, @@ -146,14 +146,6 @@ export const makeTranslationsInfoUrl = (locale: string, translations: number[]): export const makeAdvancedCopyUrl = (params: AdvancedCopyRequest): string => makeUrl('/verses/advanced_copy', params as Record); -/** - * Compose the url for search API. - * - * @param {SearchRequest} params the request params. - * @returns {string} - */ -export const makeSearchResultsUrl = (params: SearchRequest): string => makeUrl('/search', params); - export const makeNewSearchApiUrl = (params: Record) => { const baseUrl = process.env.NEXT_PUBLIC_SEARCH_BASE_URL; diff --git a/src/utils/navigation.ts b/src/utils/navigation.ts index a2b5d3056d..ef992be775 100644 --- a/src/utils/navigation.ts +++ b/src/utils/navigation.ts @@ -8,7 +8,7 @@ import { getVerseAndChapterNumbersFromKey, getVerseNumberRangeFromKey } from './ import QueryParam from '@/types/QueryParam'; import { QuranReaderFlow } from '@/types/QuranReader'; -import { SearchNavigationType } from 'types/SearchNavigationResult'; +import { SearchNavigationType } from 'types/Search/SearchNavigationResult'; /** * Get the href link to a verse. @@ -226,7 +226,11 @@ export const resolveUrlBySearchNavigationType = ( isKalimatSearch = false, ): string => { const stringKey = isKalimatSearch ? searchIdToNavigationKey(type, String(key)) : String(key); - if (type === SearchNavigationType.AYAH) { + if ( + type === SearchNavigationType.AYAH || + type === SearchNavigationType.TRANSLITERATION || + type === SearchNavigationType.TRANSLATION + ) { return getChapterWithStartingVerseUrl(stringKey); } if (type === SearchNavigationType.JUZ) { diff --git a/src/utils/search.ts b/src/utils/search.ts index 205651a662..06117ffcad 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -6,20 +6,17 @@ import groupBy from 'lodash/groupBy'; import { Translate } from 'next-translate'; import { AnyAction } from 'redux'; -import { logEmptySearchResults, logSearchResults, logTextSearchQuery } from './eventLogger'; +import { logTextSearchQuery } from './eventLogger'; -import { getNewSearchResults, getSearchResults } from '@/api'; import { addSearchHistoryRecord } from '@/redux/slices/Search/search'; -import { SearchResponse } from '@/types/ApiResponses'; import AvailableTranslation from '@/types/AvailableTranslation'; import ChaptersData from '@/types/ChaptersData'; -import { SearchMode } from '@/types/Search/SearchRequestParams'; -import SearchService from '@/types/Search/SearchService'; +import { SearchMode, SearchRequestParams } from '@/types/Search/SearchRequestParams'; import SearchQuerySource from '@/types/SearchQuerySource'; import { getChapterData } from '@/utils/chapter'; -import { toLocalizedNumber } from '@/utils/locale'; +import { toLocalizedNumber, toLocalizedVerseKey } from '@/utils/locale'; import { getVerseAndChapterNumbersFromKey, getVerseNumberRangeFromKey } from '@/utils/verse'; -import { SearchNavigationResult, SearchNavigationType } from 'types/SearchNavigationResult'; +import { SearchNavigationResult, SearchNavigationType } from 'types/Search/SearchNavigationResult'; export const LOCALE_TO_TRANSLATION_LANGUAGE = { en: 'english', @@ -115,163 +112,173 @@ export const getSearchNavigationResult = ( result: SearchNavigationResult, t: Translate, locale: string, -): SearchNavigationResult & { name: string } => { - const { key, resultType } = result; +): SearchNavigationResult => { + const { key, isArabic, isTransliteration } = result; + const resultType = getResultType(result); + const resultSuffix = getResultSuffix(resultType, key as string, locale, chaptersData); + let returnedResult = { + isTransliteration, + isArabic, + resultType, + key, + } as SearchNavigationResult; if (resultType === SearchNavigationType.JUZ) { const juzNumber = idToJuzNumber(key as string); - return { + returnedResult = { + ...returnedResult, name: `${t('common:juz')} ${toLocalizedNumber(Number(juzNumber), locale)}`, key: juzNumber, - resultType: SearchNavigationType.JUZ, }; } if (resultType === SearchNavigationType.PAGE) { const pageNumber = idToPageNumber(key as string); - return { + returnedResult = { + ...returnedResult, name: `${t('common:page')} ${toLocalizedNumber(Number(pageNumber), locale)}`, key: pageNumber, - resultType: SearchNavigationType.PAGE, + }; + } + + if (resultType === SearchNavigationType.RUB_EL_HIZB) { + returnedResult = { + ...returnedResult, + name: `${t('common:rub')} ${toLocalizedNumber(Number(key), locale)}`, + }; + } + + if (resultType === SearchNavigationType.HIZB) { + returnedResult = { + ...returnedResult, + name: `${t('common:hizb')} ${toLocalizedNumber(Number(key), locale)}`, }; } if (resultType === SearchNavigationType.RANGE) { const { surah, from, to } = getVerseNumberRangeFromKey(key as string); - return { + returnedResult = { + ...returnedResult, name: `${t('common:surah')} ${ getChapterData(chaptersData, `${surah}`).transliteratedName } ${t('common:ayah')} ${toLocalizedNumber(from, locale)} - ${toLocalizedNumber(to, locale)}`, - key, - resultType: SearchNavigationType.RANGE, }; } - if (resultType === SearchNavigationType.AYAH) { - const [surahNumber, ayahNumber] = getVerseAndChapterNumbersFromKey(key as string); - return { + if ( + resultType === SearchNavigationType.AYAH || + resultType === SearchNavigationType.TRANSLITERATION || + resultType === SearchNavigationType.TRANSLATION + ) { + returnedResult = { + ...returnedResult, + name: result.name, + }; + } + + if (resultType === SearchNavigationType.SURAH) { + returnedResult = { + ...returnedResult, name: `${t('common:surah')} ${ - getChapterData(chaptersData, `${surahNumber}`).transliteratedName - }, ${t('common:ayah')} ${toLocalizedNumber(Number(ayahNumber), locale)}`, - key, - resultType: SearchNavigationType.AYAH, + getChapterData(chaptersData, key as string).transliteratedName + }`, }; } - // when it's a chapter + return { ...returnedResult, name: `${returnedResult.name} ${resultSuffix}` }; +}; + +/** + * Adds the searched text to the recent history. + * + * @param {Dispatch} dispatch + * @param {string} debouncedSearchQuery + * @param {SearchQuerySource} source + */ +export const addToSearchHistory = ( + dispatch: Dispatch, + debouncedSearchQuery: string, + source: SearchQuerySource, +) => { + dispatch({ type: addSearchHistoryRecord.type, payload: debouncedSearchQuery }); + logTextSearchQuery(debouncedSearchQuery, source); +}; + +/** + * Get the quick search query. + * + * @param {string} query + * @returns {SearchRequestParams} + */ +export const getQuickSearchQuery = (query: string): SearchRequestParams => { return { - name: `${t('common:surah')} ${getChapterData(chaptersData, key as string).transliteratedName}`, - key, - resultType: SearchNavigationType.SURAH, + mode: SearchMode.Quick, + query, + getText: 1, + highlight: 1, }; }; /** - * Call BE to fetch the search results using the passed filters - * and if there are no results call Kalimat API. + * Get the advanced search query. * - * @param {SearchQuerySource} source * @param {string} query * @param {number} page * @param {number} pageSize - * @param {(arg: boolean) => void} setIsSearching - * @param {(arg: boolean) => void} setHasError - * @param {(data: SearchResponse) => void} setSearchResult - * @param {string} languages - * @param {string} translations + * @returns {SearchRequestParams} */ -export const searchGetResults = ( - source: SearchQuerySource, +export const getAdvancedSearchQuery = ( query: string, page: number, pageSize: number, - setIsSearching: (arg: boolean) => void, - setHasError: (arg: boolean) => void, - setSearchResult: (data: SearchResponse) => void, - languages?: string, - translations?: string, -) => { - setIsSearching(true); - logTextSearchQuery(query, source); - getSearchResults({ +): SearchRequestParams => { + return { + mode: SearchMode.Advanced, query, - ...(languages && { filterLanguages: languages }), // languages will be included only when there is a selected language size: pageSize, page, - ...(translations && { filterTranslations: translations }), // translations will be included only when there is a selected translation - }) - .then(async (response) => { - if (response.status === 500) { - setHasError(true); - } else { - setSearchResult({ ...response, service: SearchService.QDC }); - const noQdcResults = - response.pagination.totalRecords === 0 && !response.result.navigation.length; - // if there is no navigations nor verses in the response - if (noQdcResults) { - logEmptySearchResults({ - query, - source, - service: SearchService.QDC, - }); - - const kalimatResponse = await getNewSearchResults({ - mode: SearchMode.Advanced, - query, - size: pageSize, - filterLanguages: languages, - page, - exactMatchesOnly: 0, - // translations will be included only when there is a selected translation - ...(translations && { - filterTranslations: translations, - translationFields: 'resource_name', - }), - }); - - setSearchResult({ - ...kalimatResponse, - service: SearchService.KALIMAT, - }); + exactMatchesOnly: 0, + getText: 1, + highlight: 1, + }; +}; - if (kalimatResponse.pagination.totalRecords === 0) { - logEmptySearchResults({ - query, - source, - service: SearchService.KALIMAT, - }); - } else { - logSearchResults({ - query, - source, - service: SearchService.KALIMAT, - }); - } - } - } - }) - .catch(() => { - setHasError(true); - }) - .finally(() => { - setIsSearching(false); - }); +export const getResultType = (result: SearchNavigationResult) => { + const { resultType, isArabic, isTransliteration } = result; + if (resultType === SearchNavigationType.AYAH) { + if (isArabic) { + return SearchNavigationType.AYAH; + } + if (isTransliteration) { + return SearchNavigationType.TRANSLITERATION; + } + return SearchNavigationType.TRANSLATION; + } + return resultType; }; -/** - * Adds the searched text to the recent history. - * - * @param {Dispatch} dispatch - * @param {string} debouncedSearchQuery - * @param {SearchQuerySource} source - */ -export const addToSearchHistory = ( - dispatch: Dispatch, - debouncedSearchQuery: string, - source: SearchQuerySource, +export const getResultSuffix = ( + type: SearchNavigationType, + resultKey: string, + lang: string, + chaptersData: ChaptersData, ) => { - dispatch({ type: addSearchHistoryRecord.type, payload: debouncedSearchQuery }); - logTextSearchQuery(debouncedSearchQuery, source); + const [surahNumber] = getVerseAndChapterNumbersFromKey(resultKey as string); + if (type === SearchNavigationType.SURAH) { + return `- ${toLocalizedNumber(Number(surahNumber), lang)}`; + } + + if ( + type === SearchNavigationType.AYAH || + type === SearchNavigationType.TRANSLITERATION || + type === SearchNavigationType.TRANSLATION + ) { + return `(${ + getChapterData(chaptersData, `${surahNumber}`).transliteratedName + } ${toLocalizedVerseKey(resultKey as string, lang)})`; + } + + return ''; }; diff --git a/types/ApiRequests.ts b/types/ApiRequests.ts index cb44fb7f94..e7da06d773 100644 --- a/types/ApiRequests.ts +++ b/types/ApiRequests.ts @@ -1,11 +1,3 @@ -export type SearchRequest = { - query: string; - filterLanguages?: string; - filterTranslations?: string; - size?: number; - page?: number; -}; - export type AdvancedCopyRequest = { from: string; to: string; diff --git a/types/ApiResponses.ts b/types/ApiResponses.ts index cb55c05c9c..4f1f46e505 100644 --- a/types/ApiResponses.ts +++ b/types/ApiResponses.ts @@ -9,8 +9,9 @@ import LookupRange from './LookupRange'; import LookupRecord from './LookupRecord'; import MetaData from './MetaData'; import Reciter from './Reciter'; +import { SearchNavigationResult } from './Search/SearchNavigationResult'; import SearchService from './Search/SearchService'; -import { SearchNavigationResult } from './SearchNavigationResult'; +import SearchVerseItem from './Search/SearchVerseItem'; import TafsirInfo from './TafsirInfo'; import Verse from './Verse'; @@ -19,7 +20,7 @@ export interface BaseResponse { error?: string; } -interface Pagination { +export interface Pagination { perPage: number; currentPage: number; nextPage: number | null; @@ -84,7 +85,7 @@ export interface SearchResponse extends BaseResponse { pagination: Pagination; result?: { navigation: SearchNavigationResult[]; - verses: Verse[]; + verses: SearchVerseItem[]; }; } diff --git a/types/QueryParam.ts b/types/QueryParam.ts index b3b2f5a562..f93fbe49f1 100644 --- a/types/QueryParam.ts +++ b/types/QueryParam.ts @@ -7,6 +7,7 @@ enum QueryParam { FLOW = 'flow', STARTING_VERSE = 'startingVerse', QUERY = 'query', + QUERY_OLD = 'q', REDIRECT_TO = 'r', VERSE_TO = 'verseTo', VERSE_FROM = 'verseFrom', @@ -23,6 +24,7 @@ enum QueryParam { ORIENTATION = 'orientation', VIDEO_ID = 'videoId', SURAH = 'surah', + PAGE = 'page', } export default QueryParam; diff --git a/types/SearchNavigationResult.ts b/types/Search/SearchNavigationResult.ts similarity index 68% rename from types/SearchNavigationResult.ts rename to types/Search/SearchNavigationResult.ts index bd3faa3100..50eb5d0107 100644 --- a/types/SearchNavigationResult.ts +++ b/types/Search/SearchNavigationResult.ts @@ -2,15 +2,20 @@ export enum SearchNavigationType { SURAH = 'surah', JUZ = 'juz', HIZB = 'hizb', - AYAH = 'ayah', RUB_EL_HIZB = 'rub_el_hizb', SEARCH_PAGE = 'search_page', PAGE = 'page', RANGE = 'range', + HISTORY = 'history', + AYAH = 'ayah', + TRANSLITERATION = 'transliteration', + TRANSLATION = 'translation', } export interface SearchNavigationResult { resultType: SearchNavigationType; name: string; key: number | string; + isArabic?: boolean; + isTransliteration?: boolean; } diff --git a/types/Search/SearchRequestParams.ts b/types/Search/SearchRequestParams.ts index 17029ccd85..61704edd6a 100644 --- a/types/Search/SearchRequestParams.ts +++ b/types/Search/SearchRequestParams.ts @@ -11,7 +11,6 @@ interface AdvancedSearchRequestParams { interface QuickSearchRequestParams { indexes?: string; - disableHighlighting?: SearchBoolean; } export type SearchRequestParams = { @@ -20,9 +19,9 @@ export type SearchRequestParams = { size?: number; page?: number; getText?: SearchBoolean; - filterTranslations?: string; filterLanguages?: string; fields?: string; translationFields?: string; words?: boolean; + highlight?: SearchBoolean; } & (Mode extends SearchMode.Advanced ? AdvancedSearchRequestParams : QuickSearchRequestParams); diff --git a/types/Search/SearchResponse.ts b/types/Search/SearchResponse.ts index 7bb72ad810..8ce399109b 100644 --- a/types/Search/SearchResponse.ts +++ b/types/Search/SearchResponse.ts @@ -1,6 +1,6 @@ -import { BaseResponse } from '../ApiResponses'; -import { SearchNavigationResult } from '../SearchNavigationResult'; +import { BaseResponse, Pagination } from '../ApiResponses'; +import { SearchNavigationResult } from './SearchNavigationResult'; import SearchVerseItem from './SearchVerseItem'; interface SearchResponse extends BaseResponse { @@ -8,13 +8,7 @@ interface SearchResponse extends BaseResponse { navigation: SearchNavigationResult[]; verses: SearchVerseItem[]; }; - pagination: { - perPage: number; - currentPage: number; - nextPage: number | null; - totalRecords: number; - totalPages: number; - }; + pagination: Pagination; } export default SearchResponse; diff --git a/types/Search/SearchVerseItem.ts b/types/Search/SearchVerseItem.ts index 1d85a42a3b..98cded5ea3 100644 --- a/types/Search/SearchVerseItem.ts +++ b/types/Search/SearchVerseItem.ts @@ -1,11 +1,4 @@ -import Verse from '../Verse'; -import Word from '../Word'; +import { SearchNavigationResult } from './SearchNavigationResult'; -type SearchVerseItem = Verse & { - words: Word[]; -} & { - kalimatData: { - matches?: string; - }; -}; +type SearchVerseItem = SearchNavigationResult; export default SearchVerseItem;