diff --git a/src/hooks/useCommonHooks.ts b/src/hooks/useCommonHooks.ts index ba6a3d8..f2fd240 100644 --- a/src/hooks/useCommonHooks.ts +++ b/src/hooks/useCommonHooks.ts @@ -25,6 +25,7 @@ export function useCommonHooks() { const [searchValue, setSearchValue] = useState(''); const [providerLabel, setProviderLabel] = useState('Google Search'); const [providerURL, setProviderURL] = useState('https://www.google.com/search?q='); + const [providerPrefix, setProviderPrefix] = useState('/g'); const [currentUser, setCurrentUser] = useLocalStorage({ key: 'user', defaultValue: { @@ -77,6 +78,8 @@ export function useCommonHooks() { setProviderLabel, providerURL, setProviderURL, + providerPrefix, + setProviderPrefix, currentUser, setCurrentUser, currentLocation, diff --git a/src/pages/homepage/components/Search/Search.tsx b/src/pages/homepage/components/Search/Search.tsx index e695b38..e0ccf59 100644 --- a/src/pages/homepage/components/Search/Search.tsx +++ b/src/pages/homepage/components/Search/Search.tsx @@ -1,9 +1,9 @@ -import { Stack, TextInput } from '@mantine/core'; +import { Group, type OptionsFilter, Select, Stack, Text } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { HelpModal, WeatherModal } from 'components'; -import { useCommonHooks, useQueryAPI } from 'hooks'; +import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useEffect, useState } from 'react'; -import { textInputClasses } from 'styles'; +import { selectClasses } from 'styles'; export default function Search({ current, @@ -21,30 +21,47 @@ export default function Search({ endpoint: 'api/apps', }); const [applicationData, setApplicationData] = useState([]); + const { data: history, isFetched: historyIsFetched } = useQueryAPI({ + endpoint: 'api/searchHistory', + }); + const [searchHistory, setSearchHistory] = useState([]); + const { mutateAsync: createHistoryItem } = useMutationAPI(); const [providerModalOpened, { open: openProviderModal, close: closeProviderModal }] = useDisclosure(false); + const [dropdownOpened, { open: openDropdown, close: closeDropdown }] = useDisclosure(); const { + theme, providerLabel, setProviderLabel, providerURL, setProviderURL, + providerPrefix, + setProviderPrefix, searchValue, setSearchValue, } = useCommonHooks(); + const urlPattern = new RegExp( + '^(https?:\\/\\/)?' + // protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', + 'i', + ); // fragment locator + + const handleSearchHistoryItem = (item: any) => { + const url = + providers.find((provider: any) => provider.prefix === item.option.prefix)?.url || + 'https://www.google.com/search?q='; + open(`${url}${item.option.searchTerm}`, '_self'); + }; + const handleKeyPress = (item: any) => { let option = ''; let subtext = ''; - const urlPattern = new RegExp( - '^(https?:\\/\\/)?' + // protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name - '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path - '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string - '(\\#[-a-z\\d_]*)?$', - 'i', - ); // fragment locator if (item.target.value.startsWith('/')) { option = item.target.value.substr(1, item.target.value.indexOf(' ') - 1) || @@ -61,6 +78,11 @@ export default function Search({ if (providerLabel === 'Show help') { openProviderModal(); } else if (item.target.value.startsWith('/')) { + createHistoryItem({ + method: 'POST', + endpoint: 'api/searchHistory', + body: { prefix: providerPrefix, provider: providerLabel, searchTerm: subtext }, + }); open(`${providerURL}${subtext}`, '_self'); } else if (providerLabel === 'Show weather') { openWeatherModal(); @@ -74,11 +96,28 @@ export default function Search({ window.location = item.target.value; else open(`https://${item.target.value}`, '_self'); } else { + createHistoryItem({ + method: 'POST', + endpoint: 'api/searchHistory', + body: { prefix: providerPrefix, provider: providerLabel, searchTerm: subtext }, + }); open(`${providerURL}${subtext}`, '_self'); } } }; + const optionsFilter: OptionsFilter = ({ options, search }) => { + const splittedSearch = search.toLowerCase().trim().split(' '); + return options + .filter((option: any) => { + const words = option.value.toLowerCase().trim().split(' '); + return splittedSearch.every((searchWord) => + words.some((word: any) => word.includes(searchWord)), + ); + }) + .slice(0, 10); + }; + useEffect(() => { if (providers && providersIsFetched) { setProviderData(providers); @@ -90,6 +129,12 @@ export default function Search({ setApplicationData(apps); } }, [apps, isAppsFetching]); + + useEffect(() => { + if (history && historyIsFetched) { + setSearchHistory(history); + } + }, [history, historyIsFetched]); return ( - ({ + label: `${item.provider}`, + value: `${item.prefix} ${item.searchTerm} ${item.dateTimeStamp}`, + ...item, + }))} + renderOption={(item: any) => { + const m = new Date(item.option.dateTimeStamp); + const dateString = `${m.getUTCFullYear()}/${`0${m.getUTCMonth() + 1}`.slice(-2)}/${`0${m.getUTCDate()}`.slice(-2)} ${`0${m.getUTCHours()}`.slice(-2)}:${`0${m.getUTCMinutes()}`.slice(-2)}:${`0${m.getUTCSeconds()}`.slice(-2)}`; + return ( + { + handleSearchHistoryItem(item); + }} + > + + + {item.option.label} + + + + {item.option.searchTerm} + + {dateString} + + + + ); + }} spellCheck='true' leftSection={providerLabel} + rightSection={} value={searchValue} + searchValue={searchValue} + placeholder={searchValue === '' ? undefined : searchValue} + nothingFoundMessage='Nothing found...' data-active={searchValue !== ''} - onChange={(item: any) => { + dropdownOpened={dropdownOpened} + onBlur={() => closeDropdown()} + onFocus={() => { + if (searchValue !== '') { + openDropdown(); + } + }} + comboboxProps={{ + withinPortal: false, + transitionProps: { transition: 'fade', duration: 200 }, + }} + onInput={(item: any) => { + openDropdown(); setSearchValue(item.currentTarget.value); if ( item.target.value === '/?' || @@ -120,12 +222,17 @@ export default function Search({ item.target.value === '/h' ) { setProviderLabel('Show help'); + } else if (item.target.value.startsWith('/g')) { + setProviderLabel('Google Search'); + setProviderURL('https://www.google.com/search?q='); + setProviderPrefix('/g'); } else if (item.target.value.startsWith('/')) { const provider = providerData?.find( (p: any) => p.prefix === item.target.value.split(' ')[0], ); setProviderLabel(provider?.name || 'Invalid Search'); setProviderURL(provider?.url); + setProviderPrefix(provider?.prefix); } else if (item.target.value.startsWith('!')) { if (item.target.value === '!w' || item.target.value === '!weather') { setProviderLabel('Show weather'); @@ -139,15 +246,19 @@ export default function Search({ } else { setProviderLabel('Invalid function'); } - } else { - setProviderLabel('Google Search'); - setProviderURL('https://www.google.com/search?q='); } }} onKeyUp={(item: any) => { if (item.code === 'Escape') { + if (dropdownOpened) { + closeDropdown(); + } setSearchValue(''); setProviderLabel('Google Search'); + setProviderPrefix('/g'); + } + if (item.code === 'ArrowDown' && !dropdownOpened) { + openDropdown(); } if (!providerLabel.startsWith('Invalid')) { handleKeyPress(item); diff --git a/src/pages/homepage/components/SettingsButton.tsx b/src/pages/homepage/components/SettingsButton.tsx index 85318f0..7251724 100644 --- a/src/pages/homepage/components/SettingsButton.tsx +++ b/src/pages/homepage/components/SettingsButton.tsx @@ -154,8 +154,12 @@ function SettingsButton() { .toSorted((a: any, b: any) => a.label.localeCompare(b.label)) || [] } w='92%' - classNames={selectClasses} - className={selectClasses.default} + classNames={{ + input: selectClasses.menuSelectInput, + label: selectClasses.menuSelectLabel, + options: selectClasses.menuSelectOptions, + option: selectClasses.menuSelectOption, + }} comboboxProps={{ withinPortal: false }} onChange={(_value, option: any) => setSelectedClient(option)} onLoad={(option: any) => setSelectedClient(option)} diff --git a/src/styles/Select.module.css b/src/styles/Select.module.css index cac50f5..93cb00d 100644 --- a/src/styles/Select.module.css +++ b/src/styles/Select.module.css @@ -1,9 +1,9 @@ -.label { +.menuSelectLabel { margin-bottom: 5px !important; color: var(--color-text-pri) !important; font-size: 14px !important; } -.input { +.menuSelectInput { border-color: light-dark(var(--mantine-color-appGray-2), white) !important; background-color: var(--color-background) !important; color: var(--color-text-pri) !important; @@ -18,28 +18,130 @@ } } -.options { - .option { - color: var(--color-text-pri) !important; - font-size: 14px !important; - /* applies styles to selected item */ - &[data-checked] { - background-color: var(--color-text-acc) !important; - color: var(--color-text-pri) !important; - &:hover { - background-color: var(--color-text-acc) !important; - color: var(--color-background) !important; - } - } +.menuSelectOptions { + color: var(--color-text-pri) !important; +} - /* applies styles to hovered item (with mouse or keyboard) */ +.menuSelectOption { + color: var(--color-text-pri) !important; + font-size: 14px !important; + /* applies styles to selected item */ + &[data-checked] { + background-color: var(--color-text-acc) !important; + color: var(--color-text-pri) !important; &:hover { background-color: var(--color-text-acc) !important; - color: var(--color-text-pri) !important; + color: var(--color-background) !important; } } + + /* applies styles to hovered item (with mouse or keyboard) */ + &:hover { + background-color: var(--color-text-acc) !important; + color: var(--color-text-pri) !important; + } } .dropdown { - background-color: var(--color-background); + border: none !important; + background-color: var(--color-search-background) !important; + backdrop-filter: blur(10px) !important; } + +.options { + .option { + transition: all .1s ease !important; + color: var(--color-text-acc); + font-size: 18px; + &[data-combobox-selected="true"] { + background-color: var(--color-background); + } + &:hover { + background-color: var(--color-background); + } + } +} + +.wrapper { + display: flex; + justify-content: center; + .input { + border: 0; + border-bottom: thin solid var(--color-border); + background-color: transparent; + color: var(--color-text-pri); + font-style: italic; + font-size: 0.8em; + height: 3.5em; + transition: all 0.4s ease; + width: 50%; + padding-left: 1em; + padding-right: 1em; + text-align: center; + &[data-active="true"] { + background-color: color-mix( + in srgb, + var(--color-search-background), + transparent 50% + ); + border-color: var(--color-border); + outline: none; + color: var(--color-text-pri); + font-style: normal; + font-weight: 500; + font-size: 1.2em; + letter-spacing: .2em; + width: 100%; + border-top-left-radius: .25em; + border-top-right-radius: .25em; + border-bottom-left-radius: .25em; + border-bottom-right-radius: .25em; + } + } + .section { + transition: opacity 0.1s 0s ease; + color: var(--color-border); + width: auto; + padding-left: .5rem; + opacity: 0; + } +} +.wrapper:focus-within { + .input { + background-color: var(--color-search-background); + border-color: var(--color-border); + outline: none; + color: var(--color-text-pri); + font-style: normal; + font-weight: 500; + font-size: 1.2em; + letter-spacing: .2em; + width: 100%; + border-top-left-radius: .25em; + border-top-right-radius: .25em; + border-bottom-left-radius: .25em; + border-bottom-right-radius: .25em; + &[data-active="true"] { + background-color: var(--color-search-background); + border-color: var(--color-border); + outline: none; + color: var(--color-text-pri); + font-style: normal; + font-weight: 500; + font-size: 1.2em; + letter-spacing: .2em; + width: 100%; + border-top-left-radius: .25em; + border-top-right-radius: .25em; + border-bottom-left-radius: .25em; + border-bottom-right-radius: .25em; + } + } + .section { + transition: opacity 0.4s 0.4s ease; + color: var(--color-border); + width: auto; + padding-left: .5rem; + opacity: 1; + } +} \ No newline at end of file