From ac540f9f736a77b578ab132f424273324dd87511 Mon Sep 17 00:00:00 2001 From: Osama Sayed Date: Tue, 15 Oct 2024 13:07:54 +0800 Subject: [PATCH 01/14] Search as you type POC --- .../CommandBar/CommandBarBody/index.tsx | 35 ++++++++++--- .../CommandsList/CommandList.module.scss | 7 +-- .../CommandPrefix/CommandPrefix.module.scss | 10 ++-- .../CommandsList/CommandPrefix/index.tsx | 44 ++++++++++++---- .../CommandBar/CommandsList/index.tsx | 20 ++++--- .../Navbar/SearchDrawer/SearchDrawer.tsx | 40 ++++++++------ .../Search/NavigationItem/index.tsx | 16 +++++- .../KalimatNavigationSearchResultItem.tsx | 52 +++++++++++++++++++ .../SearchResultItem.module.scss | 7 +-- .../BodyContainer/SearchResults.tsx | 1 + src/utils/search.ts | 8 +++ types/Search/SearchRequestParams.ts | 2 +- 12 files changed, 189 insertions(+), 53 deletions(-) create mode 100644 src/components/Search/SearchResults/KalimatNavigationSearchResultItem.tsx diff --git a/src/components/CommandBar/CommandBarBody/index.tsx b/src/components/CommandBar/CommandBarBody/index.tsx index 0d8c3d24d2..9b9fbfca89 100644 --- a/src/components/CommandBar/CommandBarBody/index.tsx +++ b/src/components/CommandBar/CommandBarBody/index.tsx @@ -10,6 +10,7 @@ import CommandsList, { Command } from '../CommandsList'; import styles from './CommandBarBody.module.scss'; +import { getNewSearchResults } from '@/api'; import DataFetcher from '@/components/DataFetcher'; import TarteelAttribution from '@/components/TarteelAttribution/TarteelAttribution'; import VoiceSearchBodyContainer from '@/components/TarteelVoiceSearch/BodyContainer'; @@ -18,8 +19,9 @@ import useDebounce from '@/hooks/useDebounce'; import IconSearch from '@/icons/search.svg'; import { selectRecentNavigations } from '@/redux/slices/CommandBar/state'; import { selectIsCommandBarVoiceFlowStarted } from '@/redux/slices/voiceSearch'; +import { SearchMode } from '@/types/Search/SearchRequestParams'; import SearchQuerySource from '@/types/SearchQuerySource'; -import { makeSearchResultsUrl } from '@/utils/apiPaths'; +import { makeNewSearchResultsUrl } from '@/utils/apiPaths'; import { areArraysEqual } from '@/utils/array'; import { logButtonClick, logTextSearchQuery } from '@/utils/eventLogger'; import { SearchResponse } from 'types/ApiResponses'; @@ -28,27 +30,27 @@ import { SearchNavigationType } from 'types/SearchNavigationResult'; const NAVIGATE_TO = [ { name: 'Juz 1', - key: 1, + key: '1', resultType: SearchNavigationType.JUZ, }, { name: 'Hizb 1', - key: 1, + key: '1', resultType: SearchNavigationType.HIZB, }, { name: 'Rub el Hizb 1', - key: 1, + key: '1', resultType: SearchNavigationType.RUB_EL_HIZB, }, { name: 'Page 1', - key: 1, + key: '1', resultType: SearchNavigationType.PAGE, }, { name: 'Surah Yasin', - key: 36, + key: '36', resultType: SearchNavigationType.SURAH, }, { @@ -112,6 +114,15 @@ const CommandBarBody: React.FC = () => { [recentNavigations, t], ); + const quickSearchFetcher = useCallback(() => { + return getNewSearchResults({ + mode: SearchMode.Quick, + query: searchQuery, + getText: 1, + highlight: 1, + }); + }, [searchQuery]); + /** * This function will be used by DataFetcher and will run only when there is no API error * or the connections is offline. When we receive the response from DataFetcher, @@ -198,8 +209,18 @@ const CommandBarBody: React.FC = () => { ) : ( )} diff --git a/src/components/CommandBar/CommandsList/CommandList.module.scss b/src/components/CommandBar/CommandsList/CommandList.module.scss index 80a2a81327..89099c982b 100644 --- a/src/components/CommandBar/CommandsList/CommandList.module.scss +++ b/src/components/CommandBar/CommandsList/CommandList.module.scss @@ -24,7 +24,7 @@ $itemHeight: calc(1.5 * var(--spacing-mega)); display: flex; align-items: center; justify-content: space-between; - height: $itemHeight; + min-height: $itemHeight; font-size: var(--font-size-normal); padding-block-start: 0; padding-block-end: 0; @@ -32,17 +32,18 @@ $itemHeight: calc(1.5 * var(--spacing-mega)); padding-inline-end: var(--spacing-xsmall); cursor: pointer; color: var(--color-text-faded); - white-space: nowrap; + // white-space: nowrap; position: relative; z-index: var(--z-index-default); &.selected { color: var(--color-text-default); + background: var(--color-background-alternative-faint); } } .highlight { transition: transform var(--transition-fast); - height: $itemHeight; + min-height: $itemHeight; width: 100%; position: absolute; border-radius: var(--border-radius-rounded); diff --git a/src/components/CommandBar/CommandsList/CommandPrefix/CommandPrefix.module.scss b/src/components/CommandBar/CommandsList/CommandPrefix/CommandPrefix.module.scss index 33e6ae3695..0281d202b8 100644 --- a/src/components/CommandBar/CommandsList/CommandPrefix/CommandPrefix.module.scss +++ b/src/components/CommandBar/CommandsList/CommandPrefix/CommandPrefix.module.scss @@ -15,7 +15,11 @@ } .name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + // white-space: nowrap; + // overflow: hidden; + // text-overflow: ellipsis; + em { + font-weight: var(--font-weight-bold); + text-decoration: underline; + } } diff --git a/src/components/CommandBar/CommandsList/CommandPrefix/index.tsx b/src/components/CommandBar/CommandsList/CommandPrefix/index.tsx index e85697e3e8..9375fee93e 100644 --- a/src/components/CommandBar/CommandsList/CommandPrefix/index.tsx +++ b/src/components/CommandBar/CommandsList/CommandPrefix/index.tsx @@ -1,31 +1,55 @@ -import React from 'react'; +/* eslint-disable react/no-danger */ +import React, { useContext } from 'react'; import useTranslation from 'next-translate/useTranslation'; import styles from './CommandPrefix.module.scss'; +import DataContext from '@/contexts/DataContext'; import NavigateIcon from '@/icons/east.svg'; +import { getSearchNavigationResult } from '@/utils/search'; import { SearchNavigationType } from 'types/SearchNavigationResult'; interface Props { name: string; type: SearchNavigationType; + isVoiceSearch: boolean; + navigationKey: string | number; } -const CommandPrefix: React.FC = ({ name, type }) => { - const { t } = useTranslation('common'); +const CommandPrefix: React.FC = ({ name, type, isVoiceSearch, navigationKey }) => { + const { t, lang } = useTranslation('common'); + const chapterData = useContext(DataContext); + const getName = () => { + if (isVoiceSearch) return name; + + if (type === SearchNavigationType.SEARCH_PAGE) { + return t('search-for', { + searchQuery: name, + }); + } + + const navigation = getSearchNavigationResult( + chapterData, + { resultType: type, key: navigationKey, name }, + t, + lang, + true, + ); + return navigation?.name; + }; + return (
-

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

+

); }; diff --git a/src/components/CommandBar/CommandsList/index.tsx b/src/components/CommandBar/CommandsList/index.tsx index 523b0e5ad6..832b19f84c 100644 --- a/src/components/CommandBar/CommandsList/index.tsx +++ b/src/components/CommandBar/CommandsList/index.tsx @@ -26,8 +26,10 @@ import { SearchNavigationResult } from 'types/SearchNavigationResult'; export interface Command extends SearchNavigationResult { group: string; + name: string; index?: number; isClearable?: boolean; + isVoiceSearch?: boolean; } interface Props { @@ -133,7 +135,6 @@ const CommandsList: React.FC = ({ commandGroups: { groups, numberOfComman return (
    = ({ commandGroups: { groups, numberOfComman
      {groups[commandGroup].map((command) => { - const { name, resultType } = command; - const isSelected = selectedCommandIndex === command.index; + const { name, resultType, key, index, isVoiceSearch } = command; + const isSelected = selectedCommandIndex === index; return (
    • navigateToLink(command)} - onMouseOver={() => setSelectedCommandIndex(command.index)} + onMouseOver={() => setSelectedCommandIndex(index)} > - +
      diff --git a/src/components/Navbar/SearchDrawer/SearchDrawer.tsx b/src/components/Navbar/SearchDrawer/SearchDrawer.tsx index 196f628d08..311ccb30d0 100644 --- a/src/components/Navbar/SearchDrawer/SearchDrawer.tsx +++ b/src/components/Navbar/SearchDrawer/SearchDrawer.tsx @@ -8,6 +8,7 @@ import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import SearchDrawerHeader from './Header'; +import { getNewSearchResults } from '@/api'; import Drawer, { DrawerType } from '@/components/Navbar/Drawer'; import Spinner from '@/dls/Spinner/Spinner'; import useDebounce from '@/hooks/useDebounce'; @@ -15,10 +16,12 @@ import useFocus from '@/hooks/useFocusElement'; import { selectNavbar } from '@/redux/slices/navbar'; import { selectSelectedTranslations } from '@/redux/slices/QuranReader/translations'; import { selectIsSearchDrawerVoiceFlowStarted } from '@/redux/slices/voiceSearch'; +import { SearchMode } from '@/types/Search/SearchRequestParams'; +import SearchService from '@/types/Search/SearchService'; import SearchQuerySource from '@/types/SearchQuerySource'; import { areArraysEqual } from '@/utils/array'; -import { logButtonClick } from '@/utils/eventLogger'; -import { addToSearchHistory, searchGetResults } from '@/utils/search'; +import { logButtonClick, logTextSearchQuery } from '@/utils/eventLogger'; +import { addToSearchHistory } from '@/utils/search'; import { SearchResponse } from 'types/ApiResponses'; const SearchBodyContainer = dynamic(() => import('@/components/Search/SearchBodyContainer'), { @@ -33,8 +36,6 @@ const VoiceSearchBodyContainer = dynamic( }, ); -const FIRST_PAGE_NUMBER = 1; -const PAGE_SIZE = 10; const DEBOUNCING_PERIOD_MS = 1000; const SearchDrawer: React.FC = () => { @@ -60,17 +61,26 @@ const SearchDrawer: React.FC = () => { // only when the search query has a value we call the API. if (debouncedSearchQuery) { addToSearchHistory(dispatch, debouncedSearchQuery, SearchQuerySource.SearchDrawer); - searchGetResults( - SearchQuerySource.SearchDrawer, - debouncedSearchQuery, - FIRST_PAGE_NUMBER, - PAGE_SIZE, - setIsSearching, - setHasError, - setSearchResult, - null, - selectedTranslations?.length && selectedTranslations.join(','), - ); + setIsSearching(true); + logTextSearchQuery(debouncedSearchQuery, SearchQuerySource.SearchDrawer); + getNewSearchResults({ + mode: SearchMode.Quick, + query: debouncedSearchQuery, + getText: 1, + highlight: 1, + }) + .then((response) => { + setSearchResult({ + ...response, + service: SearchService.KALIMAT, + }); + }) + .catch(() => { + setHasError(true); + }) + .finally(() => { + setIsSearching(false); + }); } }, [debouncedSearchQuery, selectedTranslations, dispatch]); diff --git a/src/components/Search/NavigationItem/index.tsx b/src/components/Search/NavigationItem/index.tsx index d8a320dd71..f81dd37f0b 100644 --- a/src/components/Search/NavigationItem/index.tsx +++ b/src/components/Search/NavigationItem/index.tsx @@ -1,7 +1,10 @@ +/* eslint-disable react/no-danger */ import React, { useContext } from 'react'; import useTranslation from 'next-translate/useTranslation'; +import KalimatNavigationSearchResultItem from '../SearchResults/KalimatNavigationSearchResultItem'; + import DataContext from '@/contexts/DataContext'; import Button from '@/dls/Button/Button'; import SearchService from '@/types/Search/SearchService'; @@ -33,7 +36,7 @@ const NavigationItem: React.FC = ({ ); const result = isKalimatService - ? getSearchNavigationResult(chaptersData, navigation, t, lang) + ? getSearchNavigationResult(chaptersData, navigation, t, lang, true) : navigation; const getKalimatResultSuffix = () => { if (navigation.resultType === SearchNavigationType.SURAH) { @@ -57,6 +60,17 @@ const NavigationItem: React.FC = ({ }); }; + if (isKalimatService && result.resultType === SearchNavigationType.AYAH) { + return ( + + ); + } + return ( + )} - {result.translations?.map((translation) => ( -
      -
      - {/* eslint-disable-next-line i18next/no-literal-string */} -

      - {translation.resourceName}

      -
      - ))}
      ); diff --git a/src/components/Search/SearchResults/SearchResults.module.scss b/src/components/Search/SearchResults/SearchResults.module.scss index 7075c92f5a..1aec0724e7 100644 --- a/src/components/Search/SearchResults/SearchResults.module.scss +++ b/src/components/Search/SearchResults/SearchResults.module.scss @@ -21,4 +21,8 @@ } .navigationItemContainer { margin-inline-end: var(--spacing-small); + em { + font-weight: var(--font-weight-semibold); + text-decoration: underline; + } } diff --git a/src/components/Search/SearchResults/index.tsx b/src/components/Search/SearchResults/index.tsx index 88650d699d..3016fd8c6f 100644 --- a/src/components/Search/SearchResults/index.tsx +++ b/src/components/Search/SearchResults/index.tsx @@ -57,7 +57,7 @@ const SearchResults: React.FC = ({ <> {searchResult.result.verses.map((result) => ( = ({ searchResult, isCommandBar }) => { return ( <> {data.verses.map((verse) => ( - = ({ + result, + source, + service = SearchService.Tarteel, +}) => { + const { lang } = useTranslation('quran-reader'); + const localizedVerseKey = useMemo( + () => toLocalizedVerseKey(result.verseKey, lang), + [lang, result.verseKey], + ); + + const chaptersData = useGetChaptersData(lang); + + if (!chaptersData) return null; + + const chapterNumber = getChapterNumberFromKey(result.verseKey); + const chapterData = getChapterData(chaptersData, chapterNumber.toString()); + + const onResultItemClicked = () => { + logButtonClick(`search_result_item`, { + service, + source, + }); + }; + + return ( +
      +
      + + {chapterData.transliteratedName} {localizedVerseKey} + +
      +
      + {result.words.map((word, index) => { + return ( + + ); + })} +
      +
      + {result.translations?.map((translation) => ( +
      +
      + {/* eslint-disable-next-line i18next/no-literal-string */} +

      - {translation.resourceName}

      +
      + ))} +
      +
      + ); +}; +export default TarteelSearchResultItem; diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 8b9ac7172e..40a22eda36 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -9,32 +9,23 @@ import { useDispatch } from 'react-redux'; import styles from './search.module.scss'; -import { getAvailableLanguages, getAvailableTranslations } from '@/api'; +import { getAvailableLanguages } from '@/api'; 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 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 { setInitialSearchQuery, setIsOpen } from '@/redux/slices/CommandBar/state'; import SearchQuerySource from '@/types/SearchQuerySource'; 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 { logButtonClick, logEvent } from '@/utils/eventLogger'; +import { getLanguageAlternates } from '@/utils/locale'; import { getCanonicalUrl } from '@/utils/navigation'; -import { - addToSearchHistory, - getDefaultTranslationIdsByLang, - searchGetResults, -} from '@/utils/search'; +import { addToSearchHistory, searchGetResults } from '@/utils/search'; import { SearchResponse } from 'types/ApiResponses'; import AvailableLanguage from 'types/AvailableLanguage'; -import AvailableTranslation from 'types/AvailableTranslation'; import ChaptersData from 'types/ChaptersData'; const PAGE_SIZE = 10; @@ -42,22 +33,16 @@ const DEBOUNCING_PERIOD_MS = 1000; type SearchProps = { languages: AvailableLanguage[]; - translations: AvailableTranslation[]; chaptersData: ChaptersData; }; -const Search: NextPage = ({ translations }): JSX.Element => { +const Search: 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 [translationSearchQuery, setTranslationSearchQuery] = useState(''); - const [isContentModalOpen, setIsContentModalOpen] = useState(false); const [isSearching, setIsSearching] = useState(false); const [hasError, setHasError] = useState(false); const [searchResult, setSearchResult] = useState(null); @@ -70,9 +55,8 @@ const Search: NextPage = ({ translations }): JSX.Element => { page: currentPage, languages: selectedLanguages, q: debouncedSearchQuery, - translations: selectedTranslations, }), - [currentPage, debouncedSearchQuery, selectedLanguages, selectedTranslations], + [currentPage, debouncedSearchQuery, selectedLanguages], ); useAddQueryParamsToUrl('/search', queryParams); @@ -82,15 +66,10 @@ const Search: NextPage = ({ translations }): JSX.Element => { // 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) { + if (router.isReady) { focusInput(); } - }, [focusInput, router, isContentModalOpen]); - - // handle when language changes - useEffect(() => { - setSelectedTranslations(getDefaultTranslationIdsByLang(translations, lang) as string); - }, [lang, translations]); + }, [focusInput, router]); useEffect(() => { if (router.query.q || router.query.query) { @@ -107,16 +86,7 @@ const Search: NextPage = ({ translations }): JSX.Element => { if (router.query.languages) { setSelectedLanguages(router.query.languages as string); } - 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, - ]); + }, [router.query.q, router.query.query, router.query.page, router.query.languages]); /** * Handle when the search query is changed. @@ -125,7 +95,8 @@ const Search: NextPage = ({ translations }): JSX.Element => { * @returns {void} */ const onSearchQueryChange = (newSearchQuery: string): void => { - setSearchQuery(newSearchQuery || ''); + dispatch({ type: setIsOpen.type, payload: true }); + dispatch({ type: setInitialSearchQuery.type, payload: newSearchQuery }); }; const onClearClicked = () => { @@ -138,25 +109,20 @@ const Search: NextPage = ({ translations }): JSX.Element => { * * @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, - ); - }, - [], - ); + 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); @@ -176,7 +142,6 @@ const Search: NextPage = ({ translations }): JSX.Element => { debouncedSearchQuery, // if it is the initial search request, use the page number in the url, otherwise, reset it isInitialSearch.current ? currentPage : 1, - selectedTranslations, selectedLanguages, ); @@ -188,97 +153,20 @@ const Search: NextPage = ({ translations }): JSX.Element => { // 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]); + }, [debouncedSearchQuery, getResults, selectedLanguages]); const onPageChange = (page: number) => { logEvent('search_page_number_change', { page }); setCurrentPage(page); - getResults(debouncedSearchQuery, page, selectedTranslations, selectedLanguages); + getResults(debouncedSearchQuery, page, 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'); - - if (selectedTranslationsArray.length === 1) { - selectedValueString = firstSelectedTranslation.name; - } - 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 => { 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} -
      -
      @@ -385,32 +221,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, diff --git a/src/redux/slices/CommandBar/persistConfig.ts b/src/redux/slices/CommandBar/persistConfig.ts index d9b070ed23..a86f006323 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: ['isOpen', 'initialSearchQuery'], }; export default commandBarPersistConfig; diff --git a/src/redux/slices/CommandBar/state.ts b/src/redux/slices/CommandBar/state.ts index 659967cd5c..e38751d576 100644 --- a/src/redux/slices/CommandBar/state.ts +++ b/src/redux/slices/CommandBar/state.ts @@ -2,16 +2,17 @@ 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[]; + initialSearchQuery: string; }; const MAXIMUM_RECENT_NAVIGATIONS = 5; -const initialState: CommandBar = { isOpen: false, recentNavigations: [] }; +const initialState: CommandBar = { isOpen: false, recentNavigations: [], initialSearchQuery: '' }; export const commandBarSlice = createSlice({ name: SliceName.COMMAND_BAR, @@ -24,6 +25,7 @@ export const commandBarSlice = createSlice({ 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]; @@ -53,13 +55,22 @@ export const commandBarSlice = createSlice({ recentNavigations: newRecentNavigations, }; }, + setInitialSearchQuery: (state: CommandBar, action: PayloadAction) => ({ + ...state, + initialSearchQuery: action.payload, + }), }, }); -export const { setIsOpen, toggleIsOpen, addRecentNavigation, removeRecentNavigation } = - commandBarSlice.actions; +export const { + setIsOpen, + toggleIsOpen, + addRecentNavigation, + removeRecentNavigation, + setInitialSearchQuery, +} = 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 default commandBarSlice.reducer; diff --git a/src/utils/apiPaths.ts b/src/utils/apiPaths.ts index 39f9ffa5f0..b1703e1ecb 100644 --- a/src/utils/apiPaths.ts +++ b/src/utils/apiPaths.ts @@ -10,7 +10,7 @@ import { getTranslationsInitialState, } from '@/redux/defaultSettings/util'; import { SearchRequestParams, SearchMode } from '@/types/Search/SearchRequestParams'; -import { AdvancedCopyRequest, PagesLookUpRequest, SearchRequest } from 'types/ApiRequests'; +import { AdvancedCopyRequest, PagesLookUpRequest } from 'types/ApiRequests'; import { MushafLines, QuranFont } from 'types/QuranReader'; export const DEFAULT_VERSES_PARAMS = { @@ -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..32807effe0 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. diff --git a/src/utils/search.ts b/src/utils/search.ts index 3dc8784256..ceaed36148 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -8,18 +8,18 @@ import { AnyAction } from 'redux'; import { logEmptySearchResults, logSearchResults, logTextSearchQuery } from './eventLogger'; -import { getNewSearchResults, getSearchResults } from '@/api'; +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 } from '@/types/Search/SearchRequestParams'; +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 { 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', @@ -177,8 +177,7 @@ export const getSearchNavigationResult = ( }; /** - * Call BE to fetch the search results using the passed filters - * and if there are no results call Kalimat API. + * Call Kalimat API to fetch the search results using the passed filters. * * @param {SearchQuerySource} source * @param {string} query @@ -188,7 +187,6 @@ export const getSearchNavigationResult = ( * @param {(arg: boolean) => void} setHasError * @param {(data: SearchResponse) => void} setSearchResult * @param {string} languages - * @param {string} translations */ export const searchGetResults = ( source: SearchQuerySource, @@ -199,65 +197,37 @@ export const searchGetResults = ( setHasError: (arg: boolean) => void, setSearchResult: (data: SearchResponse) => void, languages?: string, - translations?: string, ) => { setIsSearching(true); logTextSearchQuery(query, source); - getSearchResults({ + getNewSearchResults({ + mode: SearchMode.Advanced, query, - ...(languages && { filterLanguages: languages }), // languages will be included only when there is a selected language size: pageSize, + filterLanguages: languages, page, - ...(translations && { filterTranslations: translations }), // translations will be included only when there is a selected translation + exactMatchesOnly: 0, + getText: 1, + highlight: 1, }) - .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', - }), - }); + .then(async (kalimatResponse) => { + setSearchResult({ + ...kalimatResponse, + service: SearchService.KALIMAT, + }); - setSearchResult({ - ...kalimatResponse, - service: SearchService.KALIMAT, - }); - - if (kalimatResponse.pagination.totalRecords === 0) { - logEmptySearchResults({ - query, - source, - service: SearchService.KALIMAT, - }); - } else { - logSearchResults({ - query, - source, - service: SearchService.KALIMAT, - }); - } - } + if (kalimatResponse.pagination.totalRecords === 0) { + logEmptySearchResults({ + query, + source, + service: SearchService.KALIMAT, + }); + } else { + logSearchResults({ + query, + source, + service: SearchService.KALIMAT, + }); } }) .catch(() => { @@ -283,3 +253,18 @@ export const addToSearchHistory = ( 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 { + mode: SearchMode.Quick, + query, + getText: 1, + highlight: 1, + }; +}; 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/SearchNavigationResult.ts b/types/Search/SearchNavigationResult.ts similarity index 100% rename from types/SearchNavigationResult.ts rename to types/Search/SearchNavigationResult.ts diff --git a/types/Search/SearchRequestParams.ts b/types/Search/SearchRequestParams.ts index 167515b826..1d2daa92b9 100644 --- a/types/Search/SearchRequestParams.ts +++ b/types/Search/SearchRequestParams.ts @@ -20,7 +20,6 @@ export type SearchRequestParams = { size?: number; page?: number; getText?: SearchBoolean; - filterTranslations?: string; filterLanguages?: string; fields?: string; translationFields?: string; 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; From 6204f4e88bd56cfc67d396472daa8ed162eb1519 Mon Sep 17 00:00:00 2001 From: Osama Sayed Date: Fri, 20 Dec 2024 16:23:06 +0800 Subject: [PATCH 04/14] Fix merge issue --- src/components/CommandBar/CommandBarBody/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/CommandBar/CommandBarBody/index.tsx b/src/components/CommandBar/CommandBarBody/index.tsx index 2658427061..e9811f75da 100644 --- a/src/components/CommandBar/CommandBarBody/index.tsx +++ b/src/components/CommandBar/CommandBarBody/index.tsx @@ -19,7 +19,10 @@ import useDebounce from '@/hooks/useDebounce'; import IconSearch from '@/icons/search.svg'; import { selectInitialSearchQuery, selectRecentNavigations } from '@/redux/slices/CommandBar/state'; import { selectIsCommandBarVoiceFlowStarted } from '@/redux/slices/voiceSearch'; -import { SearchNavigationResult, SearchNavigationType } from '@/types/SearchNavigationResult'; +import { + SearchNavigationResult, + SearchNavigationType, +} from '@/types/Search/SearchNavigationResult'; import SearchQuerySource from '@/types/SearchQuerySource'; import { makeNewSearchResultsUrl } from '@/utils/apiPaths'; import { areArraysEqual } from '@/utils/array'; From 45c77cdb4d512bf2b2d26de2877857d6db232dad Mon Sep 17 00:00:00 2001 From: Osama Sayed Date: Mon, 23 Dec 2024 19:09:43 +0800 Subject: [PATCH 05/14] Search UI Updates (#2268) 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); From 555f002a3ea613c964c0a8ac42d00cdb4cb23a67 Mon Sep 17 00:00:00 2001 From: Osama Sayed Date: Sat, 28 Dec 2024 19:05:34 +0500 Subject: [PATCH 06/14] Updates --- locales/en/common.json | 6 +++--- .../Navbar/SearchDrawer/Footer/index.tsx | 20 ++++++++++++++++++- .../CommandsList/CommandList.module.scss | 10 ++++------ .../CommandPrefix/CommandPrefix.module.scss | 8 ++++---- .../ExpandedSearchInputSection.module.scss | 2 +- .../ExpandedSearchInputSection/index.tsx | 2 ++ .../SearchInput/SearchInput.module.scss | 4 ++++ .../SearchResultsHeader.module.scss | 14 ++++++++++++- .../SearchResultsHeader/index.tsx | 2 +- .../TarteelAttribution.module.scss | 5 +++++ .../TarteelAttribution/TarteelAttribution.tsx | 20 ++++++++++--------- .../BodyContainer/Error/index.tsx | 18 +---------------- .../BodyContainer/index.tsx | 6 +----- 13 files changed, 69 insertions(+), 48 deletions(-) diff --git a/locales/en/common.json b/locales/en/common.json index 4c9ef4487f..c2186f621c 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -398,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/src/components/Navbar/SearchDrawer/Footer/index.tsx b/src/components/Navbar/SearchDrawer/Footer/index.tsx index 4d4964b0ca..c97a5a8b46 100644 --- a/src/components/Navbar/SearchDrawer/Footer/index.tsx +++ b/src/components/Navbar/SearchDrawer/Footer/index.tsx @@ -1,9 +1,27 @@ import React from 'react'; +import { useSelector, shallowEqual } from 'react-redux'; + import styles from './Footer.module.scss'; +import TarteelAttribution from '@/components/TarteelAttribution/TarteelAttribution'; +import Separator from '@/dls/Separator/Separator'; +import { selectIsSearchDrawerVoiceFlowStarted } from '@/redux/slices/voiceSearch'; + const Footer: React.FC = () => { - return
      ; + const isVoiceSearchFlowStarted = useSelector(selectIsSearchDrawerVoiceFlowStarted, shallowEqual); + return ( + <> + {isVoiceSearchFlowStarted ? ( +
      + + +
      + ) : ( +
      + )} + + ); }; export default Footer; diff --git a/src/components/Search/CommandBar/CommandsList/CommandList.module.scss b/src/components/Search/CommandBar/CommandsList/CommandList.module.scss index 139f945d24..9f7197020c 100644 --- a/src/components/Search/CommandBar/CommandsList/CommandList.module.scss +++ b/src/components/Search/CommandBar/CommandsList/CommandList.module.scss @@ -1,6 +1,6 @@ @use "src/styles/breakpoints"; -$itemHeight: calc(1.4 * var(--spacing-mega)); +$itemHeight: calc(1.2 * var(--spacing-mega)); .noResult { text-align: center; font-size: var(--font-size-large); @@ -24,17 +24,15 @@ $itemHeight: calc(1.4 * var(--spacing-mega)); display: flex; align-items: center; justify-content: space-between; - min-height: $itemHeight; font-size: var(--font-size-normal); - padding-block-start: 0; - padding-block-end: 0; + padding-block-start: var(--spacing-xxsmall); + padding-block-end: var(--spacing-xxsmall); padding-inline-start: var(--spacing-xsmall); padding-inline-end: var(--spacing-xsmall); cursor: pointer; position: relative; z-index: var(--z-index-default); &.selected { - color: var(--color-text-default); background: var(--color-background-alternative-medium); border-radius: var(--border-radius-rounded); } @@ -42,7 +40,7 @@ $itemHeight: calc(1.4 * var(--spacing-mega)); .highlight { transition: transform var(--transition-fast); - min-height: $itemHeight; + // min-height: $itemHeight; width: 100%; position: absolute; border-radius: var(--border-radius-rounded); diff --git a/src/components/Search/CommandBar/CommandsList/CommandPrefix/CommandPrefix.module.scss b/src/components/Search/CommandBar/CommandsList/CommandPrefix/CommandPrefix.module.scss index f2ec8462c7..89584dca90 100644 --- a/src/components/Search/CommandBar/CommandsList/CommandPrefix/CommandPrefix.module.scss +++ b/src/components/Search/CommandBar/CommandsList/CommandPrefix/CommandPrefix.module.scss @@ -1,17 +1,17 @@ .commandPrefix { margin-inline-end: var(--spacing-small); + margin-block-start: var(--spacing-micro); display: flex; align-items: center; > svg { - width: var(--spacing-large); - height: var(--spacing-large); + width: var(--spacing-medium); + height: var(--spacing-medium); } } .container { display: flex; - align-items: center; - max-width: 90%; + align-items: flex-start; } .name { diff --git a/src/components/Search/CommandBar/ExpandedSearchInputSection/ExpandedSearchInputSection.module.scss b/src/components/Search/CommandBar/ExpandedSearchInputSection/ExpandedSearchInputSection.module.scss index fd2b252050..77ec8a275d 100644 --- a/src/components/Search/CommandBar/ExpandedSearchInputSection/ExpandedSearchInputSection.module.scss +++ b/src/components/Search/CommandBar/ExpandedSearchInputSection/ExpandedSearchInputSection.module.scss @@ -1,5 +1,5 @@ @use "src/styles/breakpoints"; -$height: calc(9 * var(--spacing-mega)); +$height: calc(16 * var(--spacing-mega)); .container { padding: 0 var(--spacing-large); diff --git a/src/components/Search/CommandBar/ExpandedSearchInputSection/index.tsx b/src/components/Search/CommandBar/ExpandedSearchInputSection/index.tsx index 2622054651..d27ee29e14 100644 --- a/src/components/Search/CommandBar/ExpandedSearchInputSection/index.tsx +++ b/src/components/Search/CommandBar/ExpandedSearchInputSection/index.tsx @@ -11,6 +11,7 @@ import styles from './ExpandedSearchInputSection.module.scss'; import { getNewSearchResults } from '@/api'; import DataFetcher from '@/components/DataFetcher'; +import TarteelAttribution from '@/components/TarteelAttribution/TarteelAttribution'; import VoiceSearchBodyContainer from '@/components/TarteelVoiceSearch/BodyContainer'; import { selectRecentNavigations } from '@/redux/slices/CommandBar/state'; import { selectIsCommandBarVoiceFlowStarted } from '@/redux/slices/voiceSearch'; @@ -160,6 +161,7 @@ const ExpandedSearchInputSection: React.FC = ({ searchQuery }) => { /> )}
      + {isVoiceSearchFlowStarted && }
      ); }; diff --git a/src/components/Search/SearchInput/SearchInput.module.scss b/src/components/Search/SearchInput/SearchInput.module.scss index 1b441b7de2..eadd8ee062 100644 --- a/src/components/Search/SearchInput/SearchInput.module.scss +++ b/src/components/Search/SearchInput/SearchInput.module.scss @@ -46,4 +46,8 @@ 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/SearchResults/SearchResultsHeader/SearchResultsHeader.module.scss b/src/components/Search/SearchResults/SearchResultsHeader/SearchResultsHeader.module.scss index 75b53c3065..3452968afe 100644 --- a/src/components/Search/SearchResults/SearchResultsHeader/SearchResultsHeader.module.scss +++ b/src/components/Search/SearchResults/SearchResultsHeader/SearchResultsHeader.module.scss @@ -13,10 +13,17 @@ } .moreResultsContainer { - color: var(--color-text-default); 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 { @@ -24,4 +31,9 @@ align-items: center; justify-content: space-between; color: var(--color-text-faded); + 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 index 41893c6366..f13e8dd3dc 100644 --- a/src/components/Search/SearchResults/SearchResultsHeader/index.tsx +++ b/src/components/Search/SearchResults/SearchResultsHeader/index.tsx @@ -35,7 +35,7 @@ const SearchResultsHeader: React.FC = ({ searchQuery, onSearchResultClick }; return (
      -

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

      +

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

      = ({ 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/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, })} > - +
      ); } From 5ef3dc0d6908a1860e8d9d125097f1379fa0d579 Mon Sep 17 00:00:00 2001 From: Osama Sayed Date: Mon, 30 Dec 2024 22:18:08 +0500 Subject: [PATCH 07/14] Updates --- src/components/HomePage/HomePageHero.tsx | 2 +- .../Search/CommandBar/CommandsList/index.tsx | 5 +++-- src/components/Search/SearchInput/index.tsx | 19 +++++++++++++++++-- .../SearchResultsHeader.module.scss | 1 - src/components/dls/Forms/Input/index.tsx | 11 ++++++++++- src/utils/search.ts | 8 ++++++-- 6 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/components/HomePage/HomePageHero.tsx b/src/components/HomePage/HomePageHero.tsx index e3fb8093f5..dd86a1f21d 100644 --- a/src/components/HomePage/HomePageHero.tsx +++ b/src/components/HomePage/HomePageHero.tsx @@ -20,7 +20,7 @@ const HomePageHero = () => {
      - +
      diff --git a/src/components/Search/CommandBar/CommandsList/index.tsx b/src/components/Search/CommandBar/CommandsList/index.tsx index ce810a9cee..66aa9de0af 100644 --- a/src/components/Search/CommandBar/CommandsList/index.tsx +++ b/src/components/Search/CommandBar/CommandsList/index.tsx @@ -24,6 +24,7 @@ import { import SearchQuerySource from '@/types/SearchQuerySource'; import { logButtonClick } from '@/utils/eventLogger'; import { resolveUrlBySearchNavigationType } from '@/utils/navigation'; +import { getResultType } from '@/utils/search'; import { SearchNavigationResult, SearchNavigationType } from 'types/Search/SearchNavigationResult'; export interface Command extends SearchNavigationResult { @@ -173,7 +174,7 @@ const CommandsList: React.FC = ({ )}
        {groups[commandGroup].map((command) => { - const { name, resultType, key, index, isVoiceSearch } = command; + const { name, key, index, isVoiceSearch } = command; const isSelected = selectedCommandIndex === index; return (
      • = ({ navigationKey={key} isVoiceSearch={isVoiceSearch} name={name} - type={resultType} + type={getResultType(command)} />
        = ({ placeholder, initialSearchQuery }) => { +const SearchInput: React.FC = ({ + placeholder, + initialSearchQuery, + shouldExpandOnClick = false, +}) => { + const isVoiceSearchFlowStarted = useSelector(selectIsCommandBarVoiceFlowStarted, shallowEqual); const [searchQuery, setSearchQuery] = useState(initialSearchQuery || ''); const isExpanded = useSelector(selectIsExpanded); const dispatch = useDispatch(); @@ -61,6 +68,12 @@ const SearchInput: React.FC = ({ placeholder, initialSearchQuery }) => { ); }; + const onInputClick = () => { + if (shouldExpandOnClick) { + dispatch({ type: setIsExpanded.type, payload: true }); + } + }; + return (
        = ({ placeholder, initialSearchQuery }) => { >
        void; onChange?: (value: string) => void; + onClick?: () => void; onKeyDown?: (event: KeyboardEvent) => void; inputMode?: HTMLAttributes['inputMode']; value?: string; @@ -77,6 +78,7 @@ const Input: React.FC = ({ onClearClicked, onChange, onKeyDown, + onClick, inputMode, value = '', shouldFlipOnRTL = true, @@ -101,6 +103,12 @@ const Input: React.FC = ({ } }; + const handleClick = () => { + if (onClick) { + onClick(); + } + }; + // eslint-disable-next-line react/no-multi-comp const Suffix = () => ( <> @@ -123,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, @@ -142,11 +149,13 @@ const Input: React.FC = ({
        )} Date: Mon, 30 Dec 2024 22:42:39 +0500 Subject: [PATCH 08/14] Updates --- .../ExpandedSearchInputSection.module.scss | 6 +++++- .../Search/CommandBar/ExpandedSearchInputSection/index.tsx | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/Search/CommandBar/ExpandedSearchInputSection/ExpandedSearchInputSection.module.scss b/src/components/Search/CommandBar/ExpandedSearchInputSection/ExpandedSearchInputSection.module.scss index 77ec8a275d..84554671c1 100644 --- a/src/components/Search/CommandBar/ExpandedSearchInputSection/ExpandedSearchInputSection.module.scss +++ b/src/components/Search/CommandBar/ExpandedSearchInputSection/ExpandedSearchInputSection.module.scss @@ -65,6 +65,10 @@ $height: calc(16 * var(--spacing-mega)); padding-inline-end: var(--spacing-xxsmall); overflow-y: auto; position: relative; - height: $height; box-sizing: border-box; + max-height: $height; +} + +.height { + height: $height; } diff --git a/src/components/Search/CommandBar/ExpandedSearchInputSection/index.tsx b/src/components/Search/CommandBar/ExpandedSearchInputSection/index.tsx index d27ee29e14..34c7e664d2 100644 --- a/src/components/Search/CommandBar/ExpandedSearchInputSection/index.tsx +++ b/src/components/Search/CommandBar/ExpandedSearchInputSection/index.tsx @@ -1,6 +1,7 @@ /* eslint-disable max-lines */ import React, { useCallback } from 'react'; +import classNames from 'classnames'; import groupBy from 'lodash/groupBy'; import useTranslation from 'next-translate/useTranslation'; import { shallowEqual, useSelector } from 'react-redux'; @@ -148,7 +149,7 @@ const ExpandedSearchInputSection: React.FC = ({ searchQuery }) => { return (
        -
        +
        {isVoiceSearchFlowStarted ? ( ) : ( From 52bcdd9c1788e12c36be7ca5bb1613fb07319c2b Mon Sep 17 00:00:00 2001 From: Osama Sayed Date: Tue, 31 Dec 2024 09:40:16 +0500 Subject: [PATCH 09/14] Move navigation to the bottom --- src/components/Search/SearchResults/index.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/Search/SearchResults/index.tsx b/src/components/Search/SearchResults/index.tsx index 34b61a775b..ac5ff25a50 100644 --- a/src/components/Search/SearchResults/index.tsx +++ b/src/components/Search/SearchResults/index.tsx @@ -47,12 +47,6 @@ const SearchResults: React.FC = ({ {t('search-results', { count: toLocalizedNumber(searchResult.pagination.totalRecords, lang), })} - )} @@ -67,6 +61,14 @@ const SearchResults: React.FC = ({ /> ))} + {!isSearchDrawer && !!searchQuery && ( + + )}
        ); }; From 2e389725d70803a640cb71f84907da89acaa816d Mon Sep 17 00:00:00 2001 From: Osama Sayed Date: Tue, 31 Dec 2024 10:29:39 +0500 Subject: [PATCH 10/14] Handle autofill state --- .../Search/SearchInput/SearchInput.module.scss | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/Search/SearchInput/SearchInput.module.scss b/src/components/Search/SearchInput/SearchInput.module.scss index eadd8ee062..c62e9daa7e 100644 --- a/src/components/Search/SearchInput/SearchInput.module.scss +++ b/src/components/Search/SearchInput/SearchInput.module.scss @@ -35,6 +35,18 @@ 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: inherit !important; + transition: background-color 5000s ease-in-out 0s; + background-color: transparent !important; + } + } } .dropdownContainer { From 076d95f1625b002af4d6b3f003054fdc15266c3f Mon Sep 17 00:00:00 2001 From: Osama Sayed Date: Tue, 31 Dec 2024 23:50:09 +0500 Subject: [PATCH 11/14] Fix navigation --- .../CommandsList/CommandPrefix/index.tsx | 18 ++++-------------- .../Search/CommandBar/CommandsList/index.tsx | 1 - .../ExpandedSearchInputSection/index.tsx | 12 +++++++----- .../SearchResults/SearchResultItem/index.tsx | 16 ++++++++++------ src/utils/navigation.ts | 6 +++++- 5 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/components/Search/CommandBar/CommandsList/CommandPrefix/index.tsx b/src/components/Search/CommandBar/CommandsList/CommandPrefix/index.tsx index 3c8c1fe70c..39d43c2e5b 100644 --- a/src/components/Search/CommandBar/CommandsList/CommandPrefix/index.tsx +++ b/src/components/Search/CommandBar/CommandsList/CommandPrefix/index.tsx @@ -1,26 +1,22 @@ /* eslint-disable react/no-danger */ -import React, { useContext } from 'react'; +import React from 'react'; import useTranslation from 'next-translate/useTranslation'; import styles from './CommandPrefix.module.scss'; import SearchResultItemIcon from '@/components/Search/SearchResults/SearchResultItemIcon'; -import DataContext from '@/contexts/DataContext'; import { Direction } from '@/utils/locale'; -import { getSearchNavigationResult } from '@/utils/search'; import { SearchNavigationType } from 'types/Search/SearchNavigationResult'; interface Props { name: string; type: SearchNavigationType; isVoiceSearch: boolean; - navigationKey: string | number; } -const CommandPrefix: React.FC = ({ name, type, isVoiceSearch, navigationKey }) => { - const { t, lang } = useTranslation('common'); - const chapterData = useContext(DataContext); +const CommandPrefix: React.FC = ({ name, type, isVoiceSearch }) => { + const { t } = useTranslation('common'); const getContent = () => { if (type === SearchNavigationType.SEARCH_PAGE) { return t('search-for', { @@ -28,13 +24,7 @@ const CommandPrefix: React.FC = ({ name, type, isVoiceSearch, navigationK }); } - const navigation = getSearchNavigationResult( - chapterData, - { resultType: type, key: navigationKey, name }, - t, - lang, - ); - return navigation?.name; + return name; }; return ( diff --git a/src/components/Search/CommandBar/CommandsList/index.tsx b/src/components/Search/CommandBar/CommandsList/index.tsx index 66aa9de0af..afa707d555 100644 --- a/src/components/Search/CommandBar/CommandsList/index.tsx +++ b/src/components/Search/CommandBar/CommandsList/index.tsx @@ -187,7 +187,6 @@ const CommandsList: React.FC = ({ onMouseOver={() => setSelectedCommandIndex(index)} > = ({ searchQuery }) => { - const { t } = useTranslation('common'); + const { t, lang } = useTranslation('common'); const recentNavigations = useSelector( selectRecentNavigations, areArraysEqual, ) as SearchNavigationResult[]; + const chaptersData = useContext(DataContext); const isVoiceSearchFlowStarted = useSelector(selectIsCommandBarVoiceFlowStarted, shallowEqual); /** @@ -114,7 +116,7 @@ const ExpandedSearchInputSection: React.FC = ({ searchQuery }) => { } else if (data.result.navigation.length) { toBeGroupedCommands = [ ...data.result.navigation.map((navigationItem) => ({ - ...navigationItem, + ...getSearchNavigationResult(chaptersData, navigationItem, t, lang), group: RESULTS_GROUP, })), ]; @@ -144,7 +146,7 @@ const ExpandedSearchInputSection: React.FC = ({ searchQuery }) => { /> ); }, - [getPreInputCommands, recentNavigations.length, searchQuery, t], + [chaptersData, getPreInputCommands, lang, recentNavigations.length, searchQuery, t], ); return ( diff --git a/src/components/Search/SearchResults/SearchResultItem/index.tsx b/src/components/Search/SearchResults/SearchResultItem/index.tsx index 0d62b75c01..5938994ad0 100644 --- a/src/components/Search/SearchResults/SearchResultItem/index.tsx +++ b/src/components/Search/SearchResults/SearchResultItem/index.tsx @@ -15,7 +15,7 @@ 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'; +import { getResultType, getSearchNavigationResult } from '@/utils/search'; interface Props { source: SearchQuerySource; @@ -24,10 +24,9 @@ interface Props { } const SearchResultItem: React.FC = ({ source, service, result }) => { - const { name, key: resultKey, isArabic } = result; - const type = getResultType(result); - const { lang } = useTranslation(); + const { t, lang } = useTranslation(); const chaptersData = useContext(DataContext); + const type = getResultType(result); const onResultItemClicked = () => { logButtonClick(`search_result_item`, { service, @@ -35,7 +34,12 @@ const SearchResultItem: React.FC = ({ source, service, result }) => { }); }; - const suffix = getResultSuffix(type, resultKey as string, lang, chaptersData); + const { + name, + key: resultKey, + isArabic, + } = getSearchNavigationResult(chaptersData, result, t, lang); + const url = resolveUrlBySearchNavigationType(type, resultKey, true); return (
        @@ -48,7 +52,7 @@ const SearchResultItem: React.FC = ({ source, service, result }) => { [styles.arabic]: isArabic, })} dangerouslySetInnerHTML={{ - __html: `${name} ${suffix}`, + __html: `${name}`, }} /> diff --git a/src/utils/navigation.ts b/src/utils/navigation.ts index 32807effe0..ef992be775 100644 --- a/src/utils/navigation.ts +++ b/src/utils/navigation.ts @@ -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) { From af92c08386606c589f3c62ebdb5606d638b1d70d Mon Sep 17 00:00:00 2001 From: Osama Sayed Date: Wed, 1 Jan 2025 23:23:25 +0500 Subject: [PATCH 12/14] Fix wrong icons --- src/utils/search.ts | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/utils/search.ts b/src/utils/search.ts index ce7f86506e..06117ffcad 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -112,18 +112,24 @@ 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 = null; + let returnedResult = { + isTransliteration, + isArabic, + resultType, + key, + } as SearchNavigationResult; if (resultType === SearchNavigationType.JUZ) { const juzNumber = idToJuzNumber(key as string); returnedResult = { + ...returnedResult, name: `${t('common:juz')} ${toLocalizedNumber(Number(juzNumber), locale)}`, key: juzNumber, - resultType: SearchNavigationType.JUZ, }; } @@ -131,36 +137,33 @@ export const getSearchNavigationResult = ( const pageNumber = idToPageNumber(key as string); 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)}`, - key, - resultType: SearchNavigationType.RUB_EL_HIZB, }; } if (resultType === SearchNavigationType.HIZB) { returnedResult = { + ...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); returnedResult = { + ...returnedResult, name: `${t('common:surah')} ${ getChapterData(chaptersData, `${surah}`).transliteratedName } ${t('common:ayah')} ${toLocalizedNumber(from, locale)} - ${toLocalizedNumber(to, locale)}`, - key, - resultType: SearchNavigationType.RANGE, }; } @@ -170,19 +173,17 @@ export const getSearchNavigationResult = ( resultType === SearchNavigationType.TRANSLATION ) { returnedResult = { + ...returnedResult, name: result.name, - key, - resultType, }; } if (resultType === SearchNavigationType.SURAH) { returnedResult = { + ...returnedResult, name: `${t('common:surah')} ${ getChapterData(chaptersData, key as string).transliteratedName }`, - key, - resultType: SearchNavigationType.SURAH, }; } From 83ff402c8221ea71453b6904ea38b2c869e52578 Mon Sep 17 00:00:00 2001 From: Osama Sayed Date: Wed, 8 Jan 2025 08:17:16 +0500 Subject: [PATCH 13/14] Fix autofill color --- src/components/Search/SearchInput/SearchInput.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/SearchInput/SearchInput.module.scss b/src/components/Search/SearchInput/SearchInput.module.scss index c62e9daa7e..66c0da88ec 100644 --- a/src/components/Search/SearchInput/SearchInput.module.scss +++ b/src/components/Search/SearchInput/SearchInput.module.scss @@ -42,7 +42,7 @@ &:-webkit-autofill:focus, &:-webkit-autofill:active { -webkit-box-shadow: 0 0 0 30px transparent inset !important; - -webkit-text-fill-color: inherit !important; + -webkit-text-fill-color: var(--color-text-default) !important; transition: background-color 5000s ease-in-out 0s; background-color: transparent !important; } From e0d945abb14520347c70907fac17df5f6851580d Mon Sep 17 00:00:00 2001 From: Osama Sayed Date: Thu, 9 Jan 2025 09:13:52 +0500 Subject: [PATCH 14/14] Open search drawer when on mobile --- src/components/HomePage/HomePageHero.tsx | 6 ++++- .../Navbar/Drawer/Drawer.module.scss | 3 +++ src/components/Navbar/Drawer/index.tsx | 2 ++ src/components/Navbar/NavbarBody/index.tsx | 3 +++ src/components/Search/SearchInput/index.tsx | 23 ++++++++++++++++--- .../SearchResultItem.module.scss | 2 +- src/redux/slices/navbar.ts | 7 ++++++ src/redux/slices/voiceSearch.ts | 5 ++++ 8 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/components/HomePage/HomePageHero.tsx b/src/components/HomePage/HomePageHero.tsx index dd86a1f21d..2d5bc8db38 100644 --- a/src/components/HomePage/HomePageHero.tsx +++ b/src/components/HomePage/HomePageHero.tsx @@ -20,7 +20,11 @@ const HomePageHero = () => {
        - +
        diff --git a/src/components/Navbar/Drawer/Drawer.module.scss b/src/components/Navbar/Drawer/Drawer.module.scss index dd172033b4..37784d8081 100644 --- a/src/components/Navbar/Drawer/Drawer.module.scss +++ b/src/components/Navbar/Drawer/Drawer.module.scss @@ -30,6 +30,9 @@ inset-inline-end: calc(-1 * constants.$side-menu-desktop-width); } } + &.noTransition { + transition: none; + } } .containerOpen { diff --git a/src/components/Navbar/Drawer/index.tsx b/src/components/Navbar/Drawer/index.tsx index cb388392c4..1895d73472 100644 --- a/src/components/Navbar/Drawer/index.tsx +++ b/src/components/Navbar/Drawer/index.tsx @@ -149,6 +149,7 @@ const Drawer: React.FC = ({ }, isOpen, ); + const isSearchDrawer = type === DrawerType.Search; return (
        = ({ [styles.containerOpen]: isOpen, [styles.left]: side === DrawerSide.Left, [styles.right]: side === DrawerSide.Right, + [styles.noTransition]: type === DrawerType.Search && navbar.disableSearchDrawerTransition, })} ref={drawerRef} id={type === DrawerType.Settings ? 'settings-drawer-container' : undefined} diff --git a/src/components/Navbar/NavbarBody/index.tsx b/src/components/Navbar/NavbarBody/index.tsx index 235ee077f9..9a2dff420d 100644 --- a/src/components/Navbar/NavbarBody/index.tsx +++ b/src/components/Navbar/NavbarBody/index.tsx @@ -20,6 +20,7 @@ import { setIsSearchDrawerOpen, setIsNavigationDrawerOpen, setIsSettingsDrawerOpen, + setDisableSearchDrawerTransition, } from '@/redux/slices/navbar'; import { logEvent } from '@/utils/eventLogger'; @@ -44,6 +45,8 @@ const NavbarBody: React.FC = () => { const openSearchDrawer = () => { logDrawerOpenEvent('search'); dispatch({ type: setIsSearchDrawerOpen.type, payload: true }); + // reset the disable transition state + dispatch({ type: setDisableSearchDrawerTransition.type, payload: false }); }; const openSettingsDrawer = () => { diff --git a/src/components/Search/SearchInput/index.tsx b/src/components/Search/SearchInput/index.tsx index e8fdaba96d..bd975950c5 100644 --- a/src/components/Search/SearchInput/index.tsx +++ b/src/components/Search/SearchInput/index.tsx @@ -13,19 +13,26 @@ 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 { selectIsCommandBarVoiceFlowStarted } from '@/redux/slices/voiceSearch'; +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 || ''); @@ -60,16 +67,26 @@ const SearchInput: React.FC = ({ setSearchQuery(''); }; + const shouldSearchBeInSearchDrawer = shouldOpenDrawerOnMobile && isMobile(); 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`, ); + 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 (shouldExpandOnClick) { + if (shouldSearchBeInSearchDrawer) { + dispatch({ type: setDisableSearchDrawerTransition.type, payload: true }); + dispatch({ type: setIsSearchDrawerOpen.type, payload: true }); + } else if (shouldExpandOnClick) { dispatch({ type: setIsExpanded.type, payload: true }); } }; diff --git a/src/components/Search/SearchResults/SearchResultItem/SearchResultItem.module.scss b/src/components/Search/SearchResults/SearchResultItem/SearchResultItem.module.scss index bc0d9d0515..556877bb02 100644 --- a/src/components/Search/SearchResults/SearchResultItem/SearchResultItem.module.scss +++ b/src/components/Search/SearchResults/SearchResultItem/SearchResultItem.module.scss @@ -5,7 +5,7 @@ .container { padding-block: var(--spacing-xsmall); - padding-inline: var(--spacing-small); + padding-inline: var(--spacing-xsmall); &:hover { background-color: var(--color-background-alternative-faded); border-radius: var(--border-radius-default); diff --git a/src/redux/slices/navbar.ts b/src/redux/slices/navbar.ts index 541a059004..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({ @@ -55,6 +57,10 @@ export const navbarSlice = createSlice({ ...state, settingsView: action.payload, }), + setDisableSearchDrawerTransition: (state: Navbar, action: PayloadAction) => ({ + ...state, + disableSearchDrawerTransition: action.payload, + }), }, }); @@ -65,6 +71,7 @@ export const { setIsSettingsDrawerOpen, setSettingsView, toggleSearchDrawerIsOpen, + setDisableSearchDrawerTransition, } = navbarSlice.actions; export const selectNavbar = (state: RootState) => state.navbar; diff --git a/src/redux/slices/voiceSearch.ts b/src/redux/slices/voiceSearch.ts index 0f74219778..8e8a674f4f 100644 --- a/src/redux/slices/voiceSearch.ts +++ b/src/redux/slices/voiceSearch.ts @@ -22,6 +22,10 @@ export const voiceSearchSlice = createSlice({ ...state, isSearchDrawerVoiceFlowStarted: !state.isSearchDrawerVoiceFlowStarted, }), + startSearchDrawerVoiceFlow: (state) => ({ + ...state, + isSearchDrawerVoiceFlowStarted: true, + }), stopSearchDrawerVoiceFlow: (state) => ({ ...state, isSearchDrawerVoiceFlowStarted: false, @@ -40,6 +44,7 @@ export const selectIsCommandBarVoiceFlowStarted = (state: RootState) => export const { toggleIsSearchDrawerVoiceFlowStarted, toggleIsCommandBarVoiceFlowStarted, + startSearchDrawerVoiceFlow, stopSearchDrawerVoiceFlow, } = voiceSearchSlice.actions; export default voiceSearchSlice.reducer;