Minor Search updates

This commit is contained in:
Sean Edmunds
2025-09-05 15:02:57 -05:00
parent 2e05002c63
commit fed970834b
4 changed files with 296 additions and 255 deletions

View File

@@ -8,7 +8,7 @@ import {
Text, Text,
} from '@mantine/core'; } from '@mantine/core';
import { useDisclosure, useElementSize } from '@mantine/hooks'; import { useDisclosure, useElementSize } from '@mantine/hooks';
import { AppItem, HelpModal, WeatherModal } from 'components'; import { HelpModal, WeatherModal } from 'components';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import * as fa6 from 'react-icons/fa6'; import * as fa6 from 'react-icons/fa6';
@@ -23,7 +23,9 @@ import type {
Provider, Provider,
SearchHistoryItem, SearchHistoryItem,
} from 'types'; } from 'types';
import SearchSelectItem from './SearchSelectItem'; import RecommendedApplications from './components/RecommendedApplications';
import RecommendedBookmarks from './components/RecommendedBookmarks';
import SearchSelectItem from './components/SearchSelectItem';
export default function Search({ export default function Search({
current, current,
@@ -90,12 +92,12 @@ export default function Search({
} = useCommonHooks(); } = useCommonHooks();
const [placeholderValue, setPlaceholderValue] = useState<string>(''); const [placeholderValue, setPlaceholderValue] = useState<string>('');
const [value, setValue] = useState(0); const [value, setValue] = useState(0);
const [historyMult, setHistoryMult] = useState(1); const [historyMultiplier, setHistoryMultiplier] = useState(1);
const handleResetSearch = () => { const handleResetSearch = () => {
setSearchValue(''); setSearchValue('');
setListIndex(-1); setListIndex(-1);
setHistoryMult(1); setHistoryMultiplier(1);
setProviderLabel(defaultProviderLabel); setProviderLabel(defaultProviderLabel);
setProviderURL(defaultProviderURL); setProviderURL(defaultProviderURL);
}; };
@@ -310,10 +312,10 @@ export default function Search({
.some((word: string) => word.includes(searchWord)), .some((word: string) => word.includes(searchWord)),
); );
}) })
.slice(0, 10 * historyMult), .slice(0, 10 * historyMultiplier),
]; ];
if (historyOptions.length >= 10 * historyMult) { if (historyOptions.length >= 10 * historyMultiplier) {
return [ return [
...historyOptions, ...historyOptions,
{ {
@@ -330,138 +332,7 @@ export default function Search({
return historyOptions; return historyOptions;
}; };
useEffect(() => { const renderItem: any = (item: any) => {
if (providerData && providerDataIsFetched) {
setProviders(providerData);
}
}, [providerData, providerDataIsFetched]);
useEffect(() => {
if (applicationData && !isApplicationDataFetching) {
setApplications(applicationData);
}
}, [applicationData, isApplicationDataFetching]);
useEffect(() => {
if (applications) {
setFilteredApplications(
applications.filter((app: Application) =>
app.name.toLowerCase().includes(searchValue.toLowerCase()),
),
);
}
}, [applications, searchValue]);
useEffect(() => {
if (bookmarkData && bookmarksIsFetched) {
setBookmarks(
bookmarkData.filter(
(bookmark: Bookmark) =>
bookmark.enabled === 1 &&
bookmark.name.toLowerCase().includes(searchValue.toLowerCase()),
),
);
}
}, [bookmarksIsFetched, bookmarkData, searchValue]);
useEffect(() => {
if (historyData && historyDataIsFetched) {
setSearchHistory(historyData);
if (filteredHistory.length === 0) {
setFilteredHistory(historyData.slice(0, 10 * historyMult));
}
}
}, [historyData, historyDataIsFetched]);
useEffect(() => {
if (dropdownOpened && listIndex >= 0) {
setPlaceholderValue(
searchHistory.length > 0
? `${searchHistory[listIndex].prefix} ${searchHistory[listIndex].searchTerm}`
: '',
);
}
}, [listIndex, dropdownOpened]);
useEffect(() => {
const splittedSearch = searchValue.toLowerCase().trim().split(' ');
const filtered = searchHistory.filter((item: SearchHistoryItem) => {
const option = `${item.prefix} ${item.searchTerm}`;
const words = option.toLowerCase().trim().split(' ');
return splittedSearch.every((searchWord) =>
words.some((word: string) => word.includes(searchWord)),
);
});
setFilteredHistory(filtered);
}, [searchValue]);
useEffect(() => {
if (defaultProviderData && defaultProviderDataIsFetched) {
setDefaultProviderURL(
defaultProviderData[0]?.url ?? 'https://www.google.com/search?q=',
);
setDefaultProviderPrefix(defaultProviderData[0]?.prefix ?? '/g');
setDefaultProviderLabel(defaultProviderData[0]?.name ?? 'Google Search');
}
}, [defaultProviderData, defaultProviderDataIsFetched]);
return (
<>
{dropdownOpened && <Overlay blur={5} onClick={closeDropdown} />}
<Stack
style={{
height: position === 'center' ? '100%' : '100px',
backgroundColor: 'transparent',
textAlign: 'center',
position: 'relative',
padding: '.3rem',
display: position === 'center' ? 'inline-flex' : 'inline-block',
transition: 'margin-top .5s ease',
}}
align='center'
justify='center'
w={'100%'}
>
<Select
classNames={selectClasses}
data={searchHistory.map((item: SearchHistoryItem) => ({
label: `${item.provider}`,
value: `${item.prefix} ${item.searchTerm} ${item.dateTimeStamp}`,
...item,
}))}
filter={optionsFilter}
value={searchValue}
searchValue={searchValue}
placeholder={placeholderValue}
nothingFoundMessage={
<Text c={'var(--mantine-color-primary-0)'}>Nothing found...</Text>
}
data-active={dropdownOpened}
dropdownOpened={dropdownOpened}
comboboxProps={{
transitionProps: { transition: 'fade', duration: 200 },
}}
styles={{
wrapper: {
zIndex: 1000,
},
root: {
width: '100%',
},
}}
spellCheck={true}
searchable
autoFocus={!isMobile}
leftSection={providerLabel}
rightSection={searchValue === '' ? <div /> : undefined}
clearable
onClear={handleResetSearch}
clearButtonProps={{
'aria-label': 'Clear search',
style: { color: 'var(--mantine-color-accent-0)', marginRight: '.5rem' },
}}
scrollAreaProps={{ viewportRef: ref }}
renderOption={(item: any) => {
const Icon = allIcons[item.option.icon]; const Icon = allIcons[item.option.icon];
return !searchValue.startsWith('!') ? ( return !searchValue.startsWith('!') ? (
item.option.value === 'load' ? ( item.option.value === 'load' ? (
@@ -475,7 +346,7 @@ export default function Search({
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setHistoryMult((prev: number) => prev + 1); setHistoryMultiplier((prev: number) => prev + 1);
}} }}
> >
<Text <Text
@@ -525,7 +396,140 @@ export default function Search({
</Text> </Text>
</Group> </Group>
); );
};
useEffect(() => {
if (providerData && providerDataIsFetched) {
setProviders(providerData);
}
}, [providerData, providerDataIsFetched]);
useEffect(() => {
if (applicationData && !isApplicationDataFetching) {
setApplications(applicationData);
}
}, [applicationData, isApplicationDataFetching]);
useEffect(() => {
if (applications) {
setFilteredApplications(
applications.filter((app: Application) =>
app.name.toLowerCase().includes(searchValue.toLowerCase()),
),
);
}
}, [applications, searchValue]);
useEffect(() => {
if (bookmarkData && bookmarksIsFetched) {
setBookmarks(
bookmarkData.filter(
(bookmark: Bookmark) =>
bookmark.enabled === 1 &&
bookmark.name.toLowerCase().includes(searchValue.toLowerCase()),
),
);
}
}, [bookmarksIsFetched, bookmarkData, searchValue]);
useEffect(() => {
if (historyData && historyDataIsFetched) {
setSearchHistory(historyData);
if (filteredHistory.length === 0) {
setFilteredHistory(historyData.slice(0, 10 * historyMultiplier));
}
}
}, [historyData, historyDataIsFetched]);
useEffect(() => {
if (dropdownOpened && listIndex >= 0) {
setPlaceholderValue(
searchHistory.length > 0
? `${searchHistory[listIndex].prefix} ${searchHistory[listIndex].searchTerm}`
: '',
);
}
}, [listIndex, dropdownOpened]);
useEffect(() => {
const splittedSearch = searchValue.toLowerCase().trim().split(' ');
const filtered = searchHistory.filter((item: SearchHistoryItem) => {
const option = `${item.prefix} ${item.searchTerm}`;
const words = option.toLowerCase().trim().split(' ');
return splittedSearch.every((searchWord) =>
words.some((word: string) => word.includes(searchWord)),
);
});
setFilteredHistory(filtered);
}, [searchValue]);
useEffect(() => {
if (defaultProviderData && defaultProviderDataIsFetched) {
setDefaultProviderURL(
defaultProviderData[0]?.url ?? 'https://www.google.com/search?q=',
);
setDefaultProviderPrefix(defaultProviderData[0]?.prefix ?? '/g');
setDefaultProviderLabel(defaultProviderData[0]?.name ?? 'Google Search');
}
}, [defaultProviderData, defaultProviderDataIsFetched]);
return (
<>
{dropdownOpened && <Overlay blur={5} onClick={closeDropdown} />}
<Stack
style={{
height: position === 'center' ? '100%' : '100px',
backgroundColor: 'transparent',
textAlign: 'center',
position: 'relative',
padding: '.3rem',
display: position === 'center' ? 'inline-flex' : 'inline-block',
transition: 'margin-top .5s ease',
width: '100%',
}} }}
align='center'
justify='center'
>
<Select
classNames={selectClasses}
data={searchHistory.map((item: SearchHistoryItem) => ({
label: `${item.provider}`,
value: `${item.prefix} ${item.searchTerm} ${item.dateTimeStamp}`,
...item,
}))}
filter={optionsFilter}
value={searchValue}
searchValue={searchValue}
placeholder={placeholderValue}
nothingFoundMessage={
<Text c={'var(--mantine-color-primary-0)'}>Nothing found...</Text>
}
data-active={dropdownOpened}
dropdownOpened={dropdownOpened}
comboboxProps={{
transitionProps: { transition: 'fade', duration: 200 },
}}
styles={{
wrapper: {
zIndex: 1000,
},
root: {
width: '100%',
},
}}
spellCheck={true}
searchable
autoFocus={!isMobile}
leftSection={providerLabel}
rightSection={searchValue === '' ? <div /> : undefined}
clearable
onClear={handleResetSearch}
clearButtonProps={{
'aria-label': 'Clear search',
style: { color: 'var(--mantine-color-accent-0)', marginRight: '.5rem' },
}}
scrollAreaProps={{ viewportRef: ref }}
renderOption={(item: any) => renderItem(item)}
onBlur={() => { onBlur={() => {
setPlaceholderValue(''); setPlaceholderValue('');
setListIndex(-1); setListIndex(-1);
@@ -546,8 +550,7 @@ export default function Search({
onInput={onInput} onInput={onInput}
onKeyUp={handleKeyUp} onKeyUp={handleKeyUp}
/> />
{dropdownOpened && searchValue !== '' && ( {dropdownOpened && (
<Group w={'100%'} justify='center'>
<Card <Card
w={'100%'} w={'100%'}
mt={height > 0 ? `calc(${height}px + 1.5rem)` : '3.75rem'} mt={height > 0 ? `calc(${height}px + 1.5rem)` : '3.75rem'}
@@ -566,155 +569,35 @@ export default function Search({
}} }}
> >
<Stack gap={8} w={'100%'}> <Stack gap={8} w={'100%'}>
<Group <Text
w={'100%'} w={'100%'}
p={'1rem'} p={'1rem'}
c={'var(--mantine-color-primary-0)'}
fz={'1.5rem'}
style={{ style={{
backgroundColor: 'var(--mantine-color-search-0)', backgroundColor: 'var(--mantine-color-search-0)',
backdropFilter: 'blur(15px) brightness(1.75)', backdropFilter: 'blur(15px) brightness(1.75)',
borderRadius: '4px', borderRadius: '4px',
}} }}
> >
<Text c={'var(--mantine-color-primary-0)'} fz={'1.5rem'}>
Recommended items: Recommended items:
</Text> </Text>
</Group>
<Group align='start' justify='space-between'> <Group align='start' justify='space-between'>
<Group <RecommendedBookmarks
w={'49%'} bookmarks={bookmarks}
p={'1rem'} searchValue={searchValue}
justify='center' dropdownOpened={dropdownOpened}
style={{ />
backgroundColor: 'var(--mantine-color-search-0)', <RecommendedApplications
backdropFilter: 'blur(15px) brightness(1.75)', searchValue={searchValue}
borderRadius: '4px', dropdownOpened={dropdownOpened}
opacity: filteredApplications={filteredApplications}
searchValue && dropdownOpened && bookmarks.length > 0 ? 1 : 0,
}}
>
<Stack w={'100%'} mah={'636px'}>
<Text
c={'var(--mantine-color-primary-0)'}
fz={'1.25rem'}
style={{ fontWeight: 'bold', textDecoration: 'underline' }}
>
Bookmarks
</Text>
<Group
w={'100%'}
gap={8}
justify='space-between'
style={{ overflow: 'hidden', whiteSpace: 'nowrap' }}
>
<Stack
w={bookmarks.length <= 5 ? '98%' : '48%'}
style={{ transition: 'all .4s ease-in-out' }}
>
{bookmarks
.map((bookmark: Bookmark) => (
<Text
key={bookmark.name}
component='a'
href={bookmark.url}
c={'var(--mantine-color-primary-0)'}
style={{
backgroundColor: 'var(--mantine-color-search-0)',
padding: '.5rem',
borderRadius: '10px',
overflow: 'ellipsis',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{bookmark.name}
</Text>
))
.toSorted((a: any, b: any) => (a.key > b.key ? 1 : -1))
.slice(0, 5)}
</Stack>
<Stack
w={bookmarks.length <= 5 ? '0%' : '48%'}
style={{ transition: 'all .4s ease-in-out' }}
>
{bookmarks.length > 5 &&
bookmarks
.map((bookmark: Bookmark) => (
<Text
key={bookmark.name}
component='a'
href={bookmark.url}
c={'var(--mantine-color-primary-0)'}
style={{
backgroundColor: 'var(--mantine-color-search-0)',
padding: '.5rem',
borderRadius: '10px',
overflow: 'ellipsis',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{bookmark.name}
</Text>
))
.toSorted((a: any, b: any) => (a.key > b.key ? 1 : -1))
.slice(5, 10)}
</Stack>
</Group>
</Stack>
</Group>
<Group
w={'49%'}
p={'1rem'}
justify='center'
style={{
backgroundColor: 'var(--mantine-color-search-0)',
backdropFilter: 'blur(15px) brightness(1.75)',
borderRadius: '4px',
opacity:
searchValue && dropdownOpened && filteredApplications.length > 0
? 1
: 0,
}}
>
<Stack w={'100%'} mah={'636px'}>
<Text
c={'var(--mantine-color-primary-0)'}
fz={'1.25rem'}
style={{ fontWeight: 'bold', textDecoration: 'underline' }}
>
Applications
</Text>
<Group
w={'100%'}
gap={8}
justify='space-between'
style={{ overflow: 'hidden', whiteSpace: 'nowrap' }}
>
<Stack w={'98%'} style={{ transition: 'all .4s ease-in-out' }}>
{filteredApplications
.map((application: Application) => (
<AppItem
key={application.name}
url={application.url}
name={application.name}
icon={application.icon}
enabled={application.enabled}
scale={1}
linksActive={true}
/> />
))
.toSorted((a: any, b: any) => (a.key > b.key ? 1 : -1))
.slice(0, 5)}
</Stack>
</Group>
</Stack>
</Group>
</Group> </Group>
</Stack> </Stack>
</Card> </Card>
</Group>
)} )}
</Stack>
<HelpModal <HelpModal
handleResetSearch={handleResetSearch} handleResetSearch={handleResetSearch}
applicationData={applications} applicationData={applications}
@@ -729,7 +612,6 @@ export default function Search({
handleResetSearch={handleResetSearch} handleResetSearch={handleResetSearch}
closeWeatherModal={closeWeatherModal} closeWeatherModal={closeWeatherModal}
/> />
</Stack>
</> </>
); );
} }

View File

@@ -0,0 +1,61 @@
import { Group, Stack, Text } from '@mantine/core';
import { AppItem } from 'components';
import type { Application } from 'types';
export default function RecommendedApplications({
searchValue,
dropdownOpened,
filteredApplications,
}: {
searchValue: string;
dropdownOpened: boolean;
filteredApplications: Application[];
}) {
return (
<Group
w={'49%'}
p={'1rem'}
justify='center'
style={{
backgroundColor: 'var(--mantine-color-search-0)',
backdropFilter: 'blur(15px) brightness(1.75)',
borderRadius: '4px',
opacity: searchValue && dropdownOpened && filteredApplications.length > 0 ? 1 : 0,
}}
>
<Stack w={'100%'} mah={'636px'}>
<Text
c={'var(--mantine-color-primary-0)'}
fz={'1.25rem'}
style={{ fontWeight: 'bold', textDecoration: 'underline' }}
>
Applications
</Text>
<Group
w={'100%'}
gap={8}
justify='space-between'
style={{ overflow: 'hidden', whiteSpace: 'nowrap' }}
>
<Stack w={'98%'} style={{ transition: 'all .4s ease-in-out' }}>
{filteredApplications
.map((application: Application) => (
<AppItem
key={application.name}
url={application.url}
name={application.name}
icon={application.icon}
enabled={application.enabled}
scale={1}
linksActive={true}
/>
))
.toSorted((a: any, b: any) => (a.key > b.key ? 1 : -1))
.slice(0, 5)}
</Stack>
</Group>
</Stack>
</Group>
);
}

View File

@@ -0,0 +1,98 @@
import { Group, Stack, Text } from '@mantine/core';
import type { Bookmark } from 'types';
export default function RecommendedBookmarks({
bookmarks,
searchValue,
dropdownOpened,
}: {
bookmarks: Bookmark[];
searchValue: string;
dropdownOpened: boolean;
}) {
return (
<Group
w={'49%'}
p={'1rem'}
justify='center'
style={{
backgroundColor: 'var(--mantine-color-search-0)',
backdropFilter: 'blur(15px) brightness(1.75)',
borderRadius: '4px',
opacity: searchValue && dropdownOpened && bookmarks.length > 0 ? 1 : 0,
}}
>
<Stack w={'100%'} mah={'636px'}>
<Text
c={'var(--mantine-color-primary-0)'}
fz={'1.25rem'}
style={{ fontWeight: 'bold', textDecoration: 'underline' }}
>
Bookmarks
</Text>
<Group
w={'100%'}
h={'100%'}
gap={8}
justify='space-between'
align='start'
style={{ overflow: 'hidden', whiteSpace: 'nowrap' }}
>
<Stack
w={bookmarks.length <= 5 ? '98%' : '48%'}
style={{ transition: 'all .4s ease-in-out' }}
>
{bookmarks
.map((bookmark: Bookmark) => (
<Text
key={bookmark.name}
component='a'
href={bookmark.url}
c={'var(--mantine-color-primary-0)'}
style={{
backgroundColor: 'var(--mantine-color-search-0)',
padding: '.5rem',
borderRadius: '10px',
overflow: 'ellipsis',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{bookmark.name}
</Text>
))
.toSorted((a: any, b: any) => (a.key > b.key ? 1 : -1))
.slice(0, 5)}
</Stack>
<Stack
w={bookmarks.length <= 5 ? '0%' : '48%'}
style={{ transition: 'all .4s ease-in-out' }}
>
{bookmarks.length > 5 &&
bookmarks
.map((bookmark: Bookmark) => (
<Text
key={bookmark.name}
component='a'
href={bookmark.url}
c={'var(--mantine-color-primary-0)'}
style={{
backgroundColor: 'var(--mantine-color-search-0)',
padding: '.5rem',
borderRadius: '10px',
overflow: 'ellipsis',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{bookmark.name}
</Text>
))
.toSorted((a: any, b: any) => (a.key > b.key ? 1 : -1))
.slice(5, 10)}
</Stack>
</Group>
</Stack>
</Group>
);
}