From 054021f425c6940faa2738c1d082db1ec9703536 Mon Sep 17 00:00:00 2001 From: Osama Sayed Date: Mon, 23 Dec 2024 17:40:27 +0800 Subject: [PATCH] Search UI updates --- locales/en/common.json | 4 +- public/icons/search/arabic.svg | 5 + public/icons/search/ayah-range.svg | 12 + public/icons/search/juz.svg | 5 + public/icons/search/page.svg | 5 + public/icons/search/surah.svg | 9 + public/icons/search/translation.svg | 8 + public/icons/search/transliteration.svg | 5 + .../CommandBar/CommandBar.module.scss | 8 - .../CommandBarBase/CommandBarBase.module.scss | 109 -------- .../CommandBarBase/CommandBarBase.tsx | 24 -- .../CommandBarTrigger.module.scss | 60 ---- .../CommandBar/CommandBarTrigger/index.tsx | 51 ---- src/components/CommandBar/index.tsx | 77 ------ src/components/GlobalKeyboardListeners.tsx | 42 ++- src/components/HomePage/HomePageHero.tsx | 8 +- src/components/HomePage/PlayRadioButton.tsx | 3 +- src/components/HomePage/QuickLinks/index.tsx | 3 +- .../SearchDrawer/Footer/Footer.module.scss | 5 +- .../Navbar/SearchDrawer/Footer/index.tsx | 12 +- .../Navbar/SearchDrawer/SearchDrawer.tsx | 1 + .../CommandsList/CommandControl.tsx | 2 - .../CommandsList/CommandList.module.scss | 6 +- .../CommandPrefix/CommandPrefix.module.scss | 5 +- .../CommandsList/CommandPrefix/index.tsx | 26 +- .../CommandBar/CommandsList/index.tsx | 39 ++- .../ExpandedSearchInputSection.module.scss} | 16 +- .../ExpandedSearchInputSection}/index.tsx | 99 ++----- .../Search/NavigationItem/index.tsx | 81 ------ .../PreInput/SearchQuerySuggestion/index.tsx | 7 +- src/components/Search/PreInput/index.tsx | 54 ++-- src/components/Search/SearchBodyContainer.tsx | 9 +- src/components/Search/SearchHistory/index.tsx | 13 +- .../SearchInput/SearchInput.module.scss | 49 ++++ src/components/Search/SearchInput/index.tsx | 100 +++++++ .../KalimatNavigationSearchResultItem.tsx | 52 ---- .../SearchResultItem.module.scss | 33 --- .../Search/SearchResults/SearchResultItem.tsx | 69 ----- .../SearchResultItem.module.scss | 32 +++ .../SearchResults/SearchResultItem/index.tsx | 58 ++++ .../SearchResultItemIcon/index.tsx | 40 +++ .../SearchResults/SearchResults.module.scss | 28 -- .../SearchResultsHeader.module.scss | 27 ++ .../SearchResultsHeader/index.tsx | 56 ++++ src/components/Search/SearchResults/index.tsx | 105 +++---- .../BodyContainer/SearchResults.tsx | 19 +- .../dls/Forms/Input/Input.module.scss | 10 - .../dls/Forms/Input/Suffix/Suffix.module.scss | 9 + .../dls/Forms/Input/Suffix/index.tsx | 36 +++ src/components/dls/Forms/Input/index.tsx | 40 ++- src/pages/search.module.scss | 77 +----- src/pages/search.tsx | 258 +++++++----------- src/redux/slices/CommandBar/persistConfig.ts | 2 +- src/redux/slices/CommandBar/state.ts | 42 ++- src/redux/slices/navbar.ts | 6 + src/redux/slices/voiceSearch.ts | 5 - src/styles/theme.scss | 1 + src/styles/themes/_dark.scss | 1 + src/styles/themes/_light.scss | 4 +- src/styles/themes/_sepia.scss | 3 +- src/utils/search.ts | 185 +++++++------ types/QueryParam.ts | 2 + types/Search/SearchNavigationResult.ts | 7 +- types/Search/SearchRequestParams.ts | 2 +- 64 files changed, 954 insertions(+), 1217 deletions(-) create mode 100644 public/icons/search/arabic.svg create mode 100644 public/icons/search/ayah-range.svg create mode 100644 public/icons/search/juz.svg create mode 100644 public/icons/search/page.svg create mode 100644 public/icons/search/surah.svg create mode 100644 public/icons/search/translation.svg create mode 100644 public/icons/search/transliteration.svg delete mode 100644 src/components/CommandBar/CommandBar.module.scss delete mode 100644 src/components/CommandBar/CommandBarBase/CommandBarBase.module.scss delete mode 100644 src/components/CommandBar/CommandBarBase/CommandBarBase.tsx delete mode 100644 src/components/CommandBar/CommandBarTrigger/CommandBarTrigger.module.scss delete mode 100644 src/components/CommandBar/CommandBarTrigger/index.tsx delete mode 100644 src/components/CommandBar/index.tsx rename src/components/{ => Search}/CommandBar/CommandsList/CommandControl.tsx (88%) rename src/components/{ => Search}/CommandBar/CommandsList/CommandList.module.scss (89%) rename src/components/{ => Search}/CommandBar/CommandsList/CommandPrefix/CommandPrefix.module.scss (81%) rename src/components/{ => Search}/CommandBar/CommandsList/CommandPrefix/index.tsx (72%) rename src/components/{ => Search}/CommandBar/CommandsList/index.tsx (84%) rename src/components/{CommandBar/CommandBarBody/CommandBarBody.module.scss => Search/CommandBar/ExpandedSearchInputSection/ExpandedSearchInputSection.module.scss} (79%) rename src/components/{CommandBar/CommandBarBody => Search/CommandBar/ExpandedSearchInputSection}/index.tsx (61%) delete mode 100644 src/components/Search/NavigationItem/index.tsx create mode 100644 src/components/Search/SearchInput/SearchInput.module.scss create mode 100644 src/components/Search/SearchInput/index.tsx delete mode 100644 src/components/Search/SearchResults/KalimatNavigationSearchResultItem.tsx delete mode 100644 src/components/Search/SearchResults/SearchResultItem.module.scss delete mode 100644 src/components/Search/SearchResults/SearchResultItem.tsx create mode 100644 src/components/Search/SearchResults/SearchResultItem/SearchResultItem.module.scss create mode 100644 src/components/Search/SearchResults/SearchResultItem/index.tsx create mode 100644 src/components/Search/SearchResults/SearchResultItemIcon/index.tsx delete mode 100644 src/components/Search/SearchResults/SearchResults.module.scss create mode 100644 src/components/Search/SearchResults/SearchResultsHeader/SearchResultsHeader.module.scss create mode 100644 src/components/Search/SearchResults/SearchResultsHeader/index.tsx create mode 100644 src/components/dls/Forms/Input/Suffix/Suffix.module.scss create mode 100644 src/components/dls/Forms/Input/Suffix/index.tsx diff --git a/locales/en/common.json b/locales/en/common.json index c4a8d56e5b..4c9ef4487f 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", 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/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/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..e3fb8093f5 100644 --- a/src/components/HomePage/HomePageHero.tsx +++ b/src/components/HomePage/HomePageHero.tsx @@ -1,24 +1,26 @@ 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/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 3d6047b254..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; @@ -23,6 +23,7 @@ interface Props { pageSize?: number; onPageChange?: (page: number) => void; shouldSuggestFullSearchWhenNoResults?: boolean; + source: SearchQuerySource; } const SearchBodyContainer: React.FC = ({ @@ -32,11 +33,11 @@ const SearchBodyContainer: React.FC = ({ searchResult, onSearchKeywordClicked, onSearchResultClicked, - isSearchDrawer = true, currentPage, pageSize, onPageChange, shouldSuggestFullSearchWhenNoResults = false, + source, }) => { const { t } = useTranslation('common'); const isEmptyResponse = @@ -52,7 +53,7 @@ const SearchBodyContainer: React.FC = ({ })} > {!searchQuery ? ( - + ) : ( <> {isSearching ? ( @@ -72,7 +73,7 @@ const SearchBodyContainer: React.FC = ({ onSearchResultClicked={onSearchResultClicked} searchResult={searchResult} searchQuery={searchQuery} - isSearchDrawer={isSearchDrawer} + source={source} currentPage={currentPage} onPageChange={onPageChange} pageSize={pageSize} diff --git a/src/components/Search/SearchHistory/index.tsx b/src/components/Search/SearchHistory/index.tsx index eb0c208764..cd9fc233c5 100644 --- a/src/components/Search/SearchHistory/index.tsx +++ b/src/components/Search/SearchHistory/index.tsx @@ -8,15 +8,17 @@ import styles from './SearchHistory.module.scss'; import Header from '@/components/Search/PreInput/Header'; import SearchQuerySuggestion from '@/components/Search/PreInput/SearchQuerySuggestion'; import { removeSearchHistoryRecord, selectSearchHistory } from '@/redux/slices/Search/search'; +import { SearchNavigationType } from '@/types/Search/SearchNavigationResult'; +import SearchQuerySource from '@/types/SearchQuerySource'; import { areArraysEqual } from '@/utils/array'; import { logButtonClick } from '@/utils/eventLogger'; interface Props { onSearchKeywordClicked: (searchQuery: string) => 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..1b441b7de2 --- /dev/null +++ b/src/components/Search/SearchInput/SearchInput.module.scss @@ -0,0 +1,49 @@ +@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; + } +} + +.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); +} diff --git a/src/components/Search/SearchInput/index.tsx b/src/components/Search/SearchInput/index.tsx new file mode 100644 index 0000000000..357fa8bbbe --- /dev/null +++ b/src/components/Search/SearchInput/index.tsx @@ -0,0 +1,100 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +import classNames from 'classnames'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { 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 { logButtonClick } from '@/utils/eventLogger'; + +type Props = { + placeholder?: string; + initialSearchQuery?: string; +}; + +const SearchInput: React.FC = ({ placeholder, initialSearchQuery }) => { + 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 onTarteelTriggerClicked = (startFlow: boolean) => { + dispatch({ type: setIsExpanded.type, payload: true }); + logButtonClick( + // eslint-disable-next-line i18next/no-literal-string + `search_input_voice_search_${startFlow ? 'start' : 'stop'}_flow`, + ); + }; + + 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/KalimatNavigationSearchResultItem.tsx b/src/components/Search/SearchResults/KalimatNavigationSearchResultItem.tsx deleted file mode 100644 index ffaf9e3325..0000000000 --- a/src/components/Search/SearchResults/KalimatNavigationSearchResultItem.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-disable react/no-danger */ - -import React, { useContext } from 'react'; - -import useTranslation from 'next-translate/useTranslation'; - -import styles from './SearchResultItem.module.scss'; - -import DataContext from '@/contexts/DataContext'; -import Link from '@/dls/Link/Link'; -import { SearchNavigationType } from '@/types/Search/SearchNavigationResult'; -import SearchService from '@/types/Search/SearchService'; -import SearchQuerySource from '@/types/SearchQuerySource'; -import { getChapterData } from '@/utils/chapter'; -import { logButtonClick } from '@/utils/eventLogger'; -import { toLocalizedVerseKey } from '@/utils/locale'; -import { resolveUrlBySearchNavigationType } from '@/utils/navigation'; -import { getVerseAndChapterNumbersFromKey } from '@/utils/verse'; - -interface Props { - name: string; - resultKey: string | number; - source: SearchQuerySource; -} - -const KalimatNavigationSearchResultItem: React.FC = ({ name, source, resultKey }) => { - const { t, lang } = useTranslation(); - const chaptersData = useContext(DataContext); - const [surahNumber] = getVerseAndChapterNumbersFromKey(resultKey as string); - const onResultItemClicked = () => { - logButtonClick(`search_result_item`, { - service: SearchService.KALIMAT, - source, - }); - }; - - const url = resolveUrlBySearchNavigationType(SearchNavigationType.AYAH, resultKey, true); - - return ( - <> - - {`${t('common:surah')} ${ - getChapterData(chaptersData, `${surahNumber}`).transliteratedName - } (${toLocalizedVerseKey(resultKey as string, lang)})`} - -
-
-
- - ); -}; -export default KalimatNavigationSearchResultItem; diff --git a/src/components/Search/SearchResults/SearchResultItem.module.scss b/src/components/Search/SearchResults/SearchResultItem.module.scss deleted file mode 100644 index 1d4071a515..0000000000 --- a/src/components/Search/SearchResults/SearchResultItem.module.scss +++ /dev/null @@ -1,33 +0,0 @@ -.container { - text-decoration: none; - border-block-end: 1px solid var(--color-borders-hairline); - padding-block: var(--spacing-medium); - margin-block-end: var(--spacing-xsmall); -} - -.itemContainer { - border-radius: var(--border-radius-default); - margin-inline-start: auto; - margin-inline-end: auto; -} - -.quranTextResult { - font-size: var(--font-size-xlarge); - line-height: var(--line-height-large); - padding-block-start: var(--spacing-micro); - padding-block-end: var(--spacing-micro); - padding-inline-start: var(--spacing-micro); - padding-inline-end: var(--spacing-micro); - - em { - font-weight: var(--font-weight-semibold); - text-decoration: underline; - } -} - -.verseKey { - color: var(--color-success-medium); - display: block; - margin-block-end: var(--spacing-medium); - font-weight: var(--font-weight-bold); -} diff --git a/src/components/Search/SearchResults/SearchResultItem.tsx b/src/components/Search/SearchResults/SearchResultItem.tsx deleted file mode 100644 index db51c3190e..0000000000 --- a/src/components/Search/SearchResults/SearchResultItem.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; - -import useTranslation from 'next-translate/useTranslation'; - -import KalimatNavigationSearchResultItem from './KalimatNavigationSearchResultItem'; -import styles from './SearchResultItem.module.scss'; - -import Button from '@/dls/Button/Button'; -import { SearchNavigationType } from '@/types/Search/SearchNavigationResult'; -import SearchService from '@/types/Search/SearchService'; -import SearchVerseItem from '@/types/Search/SearchVerseItem'; -import SearchQuerySource from '@/types/SearchQuerySource'; -import { logButtonClick } from '@/utils/eventLogger'; -import { toLocalizedNumber, toLocalizedVerseKey } from '@/utils/locale'; -import { resolveUrlBySearchNavigationType } from '@/utils/navigation'; - -interface Props { - result: SearchVerseItem; - source: SearchQuerySource; - service?: SearchService; -} - -const SearchResultItem: React.FC = ({ result, source, service = SearchService.KALIMAT }) => { - const { lang } = useTranslation('quran-reader'); - const url = resolveUrlBySearchNavigationType(result.resultType, result.key, true); - - const getKalimatResultSuffix = () => { - if (result.resultType === SearchNavigationType.SURAH) { - return `(${toLocalizedNumber(Number(result.key), lang)})`; - } - - if (result.resultType === SearchNavigationType.AYAH) { - return `(${toLocalizedVerseKey(result.key as string, lang)})`; - } - - return undefined; - }; - - const suffix = getKalimatResultSuffix(); - - const onResultItemClicked = () => { - logButtonClick(`search_result_item`, { - service, - source, - }); - }; - - return ( -
-
-
- {result.resultType === SearchNavigationType.AYAH ? ( - - ) : ( - - )} -
-
-
- ); -}; -export default SearchResultItem; 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..bc0d9d0515 --- /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-small); + &: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..0d62b75c01 --- /dev/null +++ b/src/components/Search/SearchResults/SearchResultItem/index.tsx @@ -0,0 +1,58 @@ +/* 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 { getResultSuffix, getResultType } from '@/utils/search'; + +interface Props { + source: SearchQuerySource; + service: SearchService; + result: SearchNavigationResult; +} + +const SearchResultItem: React.FC = ({ source, service, result }) => { + const { name, key: resultKey, isArabic } = result; + const type = getResultType(result); + const { lang } = useTranslation(); + const chaptersData = useContext(DataContext); + const onResultItemClicked = () => { + logButtonClick(`search_result_item`, { + service, + source, + }); + }; + + const suffix = getResultSuffix(type, resultKey as string, lang, chaptersData); + 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 1aec0724e7..0000000000 --- a/src/components/Search/SearchResults/SearchResults.module.scss +++ /dev/null @@ -1,28 +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); - em { - font-weight: var(--font-weight-semibold); - text-decoration: underline; - } -} 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..75b53c3065 --- /dev/null +++ b/src/components/Search/SearchResults/SearchResultsHeader/SearchResultsHeader.module.scss @@ -0,0 +1,27 @@ +.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 { + color: var(--color-text-default); + display: flex; + cursor: pointer; + align-items: center; +} + +.resultsSummaryContainer { + display: flex; + align-items: center; + justify-content: space-between; + color: var(--color-text-faded); +} diff --git a/src/components/Search/SearchResults/SearchResultsHeader/index.tsx b/src/components/Search/SearchResults/SearchResultsHeader/index.tsx new file mode 100644 index 0000000000..41893c6366 --- /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 3016fd8c6f..34b61a775b 100644 --- a/src/components/Search/SearchResults/index.tsx +++ b/src/components/Search/SearchResults/index.tsx @@ -3,104 +3,71 @@ 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) => ( + + ))} + +
); }; diff --git a/src/components/TarteelVoiceSearch/BodyContainer/SearchResults.tsx b/src/components/TarteelVoiceSearch/BodyContainer/SearchResults.tsx index 94abdd993d..ca44c9c8a9 100644 --- a/src/components/TarteelVoiceSearch/BodyContainer/SearchResults.tsx +++ b/src/components/TarteelVoiceSearch/BodyContainer/SearchResults.tsx @@ -1,19 +1,19 @@ -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 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/Search/SearchNavigationResult'; import SearchResult from 'types/Tarteel/SearchResult'; @@ -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,9 +53,11 @@ 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'), @@ -86,7 +89,7 @@ const SearchResults: React.FC = ({ searchResult, isCommandBar }) => { ); }, - [isCommandBar, lang, t], + [chaptersData, isCommandBar, lang, t], ); return ; 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..8338f7228c 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'; @@ -56,6 +57,8 @@ interface Props { htmlType?: React.HTMLInputTypeAttribute; isRequired?: boolean; inputRef?: RefObject; + prefixSuffixContainerClassName?: string; + shouldUseDefaultStyles?: boolean; } const Input: React.FC = ({ @@ -78,9 +81,11 @@ const Input: React.FC = ({ 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 +101,19 @@ const Input: React.FC = ({ } }; + // eslint-disable-next-line react/no-multi-comp + const Suffix = () => ( + <> + {suffix && ( + + )} + + ); + return ( <> {label &&

{label}

} @@ -113,7 +131,15 @@ 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 40a22eda36..89084dc6fb 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -1,179 +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 } from '@/api'; +import { getAvailableLanguages, getNewSearchResults } from '@/api'; +import DataFetcher from '@/components/DataFetcher'; import NextSeoWrapper from '@/components/NextSeoWrapper'; import SearchBodyContainer from '@/components/Search/SearchBodyContainer'; -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 SearchIcon from '@/icons/search.svg'; -import { setInitialSearchQuery, setIsOpen } from '@/redux/slices/CommandBar/state'; +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 } from '@/utils/eventLogger'; +import { + logEvent, + logTextSearchQuery, + logSearchResults, + logEmptySearchResults, +} from '@/utils/eventLogger'; import { getLanguageAlternates } from '@/utils/locale'; import { getCanonicalUrl } from '@/utils/navigation'; -import { addToSearchHistory, searchGetResults } from '@/utils/search'; -import { SearchResponse } from 'types/ApiResponses'; +import { getAdvancedSearchQuery } from '@/utils/search'; import AvailableLanguage from 'types/AvailableLanguage'; import ChaptersData from 'types/ChaptersData'; const PAGE_SIZE = 10; -const DEBOUNCING_PERIOD_MS = 1000; -type SearchProps = { +type SearchPageProps = { languages: AvailableLanguage[]; chaptersData: ChaptersData; }; -const Search: NextPage = (): 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 [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, - }), - [currentPage, debouncedSearchQuery, selectedLanguages], - ); - 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) { - focusInput(); + 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) || ''; } - }, [focusInput, router]); - - 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); + return ''; + }); + const [currentPage, setCurrentPage] = useState(() => { + if (typeof window !== 'undefined') { + const params = new URLSearchParams(window.location.search); + return Number(params.get(QueryParam.PAGE)) || 1; } + return 1; + }); - if (router.query.page) { - setCurrentPage(Number(router.query.page)); - } - if (router.query.languages) { - setSelectedLanguages(router.query.languages as string); - } - }, [router.query.q, router.query.query, router.query.page, router.query.languages]); - - /** - * Handle when the search query is changed. - * - * @param {string} newSearchQuery - * @returns {void} - */ - const onSearchQueryChange = (newSearchQuery: string): void => { - dispatch({ type: setIsOpen.type, payload: true }); - dispatch({ type: setInitialSearchQuery.type, payload: 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} language - */ - const getResults = useCallback((query: string, page: number, language: string) => { - searchGetResults( - SearchQuerySource.SearchPage, - query, - page, - PAGE_SIZE, - setIsSearching, - setHasError, - setSearchResult, - language, - ); - }, []); - - // 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. + // Handle URL changes (both initial load and navigation) 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); + if (!router.isReady) return; - getResults( - debouncedSearchQuery, - // if it is the initial search request, use the page number in the url, otherwise, reset it - isInitialSearch.current ? currentPage : 1, - selectedLanguages, - ); + const query = router.query[QueryParam.QUERY] || router.query[QueryParam.QUERY_OLD]; + const page = Number(router.query[QueryParam.PAGE]) || 1; - // if it was the initial request, update the ref - if (isInitialSearch.current) { - isInitialSearch.current = false; - } + if (query) { + setSearchQuery(query as string); + setCurrentPage(page); } - // 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]); + }, [router.isReady, router.query]); const onPageChange = (page: number) => { logEvent('search_page_number_change', { page }); setCurrentPage(page); - getResults(debouncedSearchQuery, page, selectedLanguages); }; - const onSearchKeywordClicked = useCallback((keyword: string) => { - setSearchQuery(keyword); - }, []); + 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, + }); + } - const navigationUrl = '/search'; + return finalResponse; + } catch (error) { + throw new Error('Search failed'); + } + }; return ( <> = (): 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} - /> -
+
+
- { + return ( + + ); + }} + fetcher={fetcher} />
@@ -245,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 a86f006323..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', 'initialSearchQuery'], + blacklist: ['isExpanded'], }; export default commandBarPersistConfig; diff --git a/src/redux/slices/CommandBar/state.ts b/src/redux/slices/CommandBar/state.ts index e38751d576..596109ac7e 100644 --- a/src/redux/slices/CommandBar/state.ts +++ b/src/redux/slices/CommandBar/state.ts @@ -5,28 +5,18 @@ import SliceName from '@/redux/types/SliceName'; import { SearchNavigationResult } from 'types/Search/SearchNavigationResult'; export type CommandBar = { - isOpen: boolean; recentNavigations: SearchNavigationResult[]; - initialSearchQuery: string; + isExpanded: boolean; }; const MAXIMUM_RECENT_NAVIGATIONS = 5; -const initialState: CommandBar = { isOpen: false, recentNavigations: [], initialSearchQuery: '' }; +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, - initialSearchQuery: '', // we reset the initial search query when the command bar is toggled - }), addRecentNavigation: (state: CommandBar, action: PayloadAction) => { let newRecentNavigations = [...state.recentNavigations]; const newRecentNavigation = action.payload; @@ -55,22 +45,24 @@ export const commandBarSlice = createSlice({ recentNavigations: newRecentNavigations, }; }, - setInitialSearchQuery: (state: CommandBar, action: PayloadAction) => ({ - ...state, - initialSearchQuery: action.payload, - }), + toggleIsExpanded: (state: CommandBar) => { + return { + ...state, + isExpanded: !state.isExpanded, + }; + }, + setIsExpanded: (state: CommandBar, action: PayloadAction) => { + return { + ...state, + isExpanded: action.payload, + }; + }, }, }); -export const { - setIsOpen, - toggleIsOpen, - addRecentNavigation, - removeRecentNavigation, - setInitialSearchQuery, -} = commandBarSlice.actions; +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 selectInitialSearchQuery = (state: RootState) => state.commandBar.initialSearchQuery; +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..541a059004 100644 --- a/src/redux/slices/navbar.ts +++ b/src/redux/slices/navbar.ts @@ -43,6 +43,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, @@ -60,8 +64,10 @@ export const { setIsSearchDrawerOpen, setIsSettingsDrawerOpen, setSettingsView, + toggleSearchDrawerIsOpen, } = 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..0f74219778 100644 --- a/src/redux/slices/voiceSearch.ts +++ b/src/redux/slices/voiceSearch.ts @@ -26,10 +26,6 @@ export const voiceSearchSlice = createSlice({ ...state, isSearchDrawerVoiceFlowStarted: false, }), - stopCommandBarVoiceFlow: (state) => ({ - ...state, - isCommandBardVoiceFlowStarted: false, - }), toggleIsCommandBarVoiceFlowStarted: (state) => ({ ...state, isCommandBardVoiceFlowStarted: !state.isCommandBardVoiceFlowStarted, @@ -45,6 +41,5 @@ export const { toggleIsSearchDrawerVoiceFlowStarted, toggleIsCommandBarVoiceFlowStarted, 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/search.ts b/src/utils/search.ts index ceaed36148..ed22989db1 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -6,18 +6,15 @@ 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 } 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, SearchRequestParams } from '@/types/Search/SearchRequestParams'; -import SearchService from '@/types/Search/SearchService'; 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/Search/SearchNavigationResult'; @@ -115,14 +112,15 @@ export const getSearchNavigationResult = ( result: SearchNavigationResult, t: Translate, locale: string, - shouldUseOriginalAyahName = false, ): SearchNavigationResult & { name: string } => { const { key, resultType } = result; + const resultSuffix = getResultSuffix(resultType, key as string, locale, chaptersData); + let returnedResult = null; if (resultType === SearchNavigationType.JUZ) { const juzNumber = idToJuzNumber(key as string); - return { + returnedResult = { name: `${t('common:juz')} ${toLocalizedNumber(Number(juzNumber), locale)}`, key: juzNumber, resultType: SearchNavigationType.JUZ, @@ -132,16 +130,32 @@ export const getSearchNavigationResult = ( if (resultType === SearchNavigationType.PAGE) { const pageNumber = idToPageNumber(key as string); - return { + returnedResult = { name: `${t('common:page')} ${toLocalizedNumber(Number(pageNumber), locale)}`, key: pageNumber, resultType: SearchNavigationType.PAGE, }; } + if (resultType === SearchNavigationType.RUB_EL_HIZB) { + returnedResult = { + name: `${t('common:rub')} ${toLocalizedNumber(Number(key), locale)}`, + key, + resultType: SearchNavigationType.RUB_EL_HIZB, + }; + } + + if (resultType === SearchNavigationType.HIZB) { + returnedResult = { + name: `${t('common:hizb')} ${toLocalizedNumber(Number(key), locale)}`, + key, + resultType: SearchNavigationType.HIZB, + }; + } + if (resultType === SearchNavigationType.RANGE) { const { surah, from, to } = getVerseNumberRangeFromKey(key as string); - return { + returnedResult = { name: `${t('common:surah')} ${ getChapterData(chaptersData, `${surah}`).transliteratedName } ${t('common:ayah')} ${toLocalizedNumber(from, locale)} - ${toLocalizedNumber(to, locale)}`, @@ -151,91 +165,24 @@ export const getSearchNavigationResult = ( } if (resultType === SearchNavigationType.AYAH) { - if (shouldUseOriginalAyahName) { - return { - name: `${result.name} - (${key})`, - key, - resultType: SearchNavigationType.AYAH, - }; - } - const [surahNumber, ayahNumber] = getVerseAndChapterNumbersFromKey(key as string); - return { - name: `${t('common:surah')} ${ - getChapterData(chaptersData, `${surahNumber}`).transliteratedName - }, ${t('common:ayah')} ${toLocalizedNumber(Number(ayahNumber), locale)}`, + returnedResult = { + name: result.name, key, resultType: SearchNavigationType.AYAH, }; } - // when it's a chapter - return { - name: `${t('common:surah')} ${getChapterData(chaptersData, key as string).transliteratedName}`, - key, - resultType: SearchNavigationType.SURAH, - }; -}; - -/** - * Call Kalimat API to fetch the search results using the passed filters. - * - * @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 - */ -export const searchGetResults = ( - source: SearchQuerySource, - query: string, - page: number, - pageSize: number, - setIsSearching: (arg: boolean) => void, - setHasError: (arg: boolean) => void, - setSearchResult: (data: SearchResponse) => void, - languages?: string, -) => { - setIsSearching(true); - logTextSearchQuery(query, source); - getNewSearchResults({ - mode: SearchMode.Advanced, - query, - size: pageSize, - filterLanguages: languages, - page, - exactMatchesOnly: 0, - getText: 1, - highlight: 1, - }) - .then(async (kalimatResponse) => { - setSearchResult({ - ...kalimatResponse, - service: SearchService.KALIMAT, - }); + if (resultType === SearchNavigationType.SURAH) { + returnedResult = { + name: `${t('common:surah')} ${ + getChapterData(chaptersData, key as string).transliteratedName + }`, + key, + resultType: SearchNavigationType.SURAH, + }; + } - if (kalimatResponse.pagination.totalRecords === 0) { - logEmptySearchResults({ - query, - source, - service: SearchService.KALIMAT, - }); - } else { - logSearchResults({ - query, - source, - service: SearchService.KALIMAT, - }); - } - }) - .catch(() => { - setHasError(true); - }) - .finally(() => { - setIsSearching(false); - }); + return { ...returnedResult, name: `${returnedResult.name} ${resultSuffix}` }; }; /** @@ -268,3 +215,65 @@ export const getQuickSearchQuery = (query: string): SearchRequestParams} + */ +export const getAdvancedSearchQuery = ( + query: string, + page: number, + pageSize: number, +): SearchRequestParams => { + return { + mode: SearchMode.Advanced, + query, + size: pageSize, + page, + exactMatchesOnly: 0, + getText: 1, + highlight: 1, + }; +}; + +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; +}; + +export const getResultSuffix = ( + type: SearchNavigationType, + resultKey: string, + lang: string, + chaptersData: ChaptersData, +) => { + 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/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/Search/SearchNavigationResult.ts b/types/Search/SearchNavigationResult.ts index bd3faa3100..50eb5d0107 100644 --- a/types/Search/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 1d2daa92b9..61704edd6a 100644 --- a/types/Search/SearchRequestParams.ts +++ b/types/Search/SearchRequestParams.ts @@ -11,7 +11,6 @@ interface AdvancedSearchRequestParams { interface QuickSearchRequestParams { indexes?: string; - highlight?: SearchBoolean; } export type SearchRequestParams = { @@ -24,4 +23,5 @@ export type SearchRequestParams = { fields?: string; translationFields?: string; words?: boolean; + highlight?: SearchBoolean; } & (Mode extends SearchMode.Advanced ? AdvancedSearchRequestParams : QuickSearchRequestParams);