ente/src/components/SearchBar.tsx

368 lines
11 KiB
TypeScript
Raw Normal View History

2021-09-22 18:51:55 +00:00
import { Search, SearchStats } from 'pages/gallery';
import React, { useEffect, useState } from 'react';
2021-05-18 04:32:37 +00:00
import styled from 'styled-components';
import AsyncSelect from 'react-select/async';
2021-05-30 16:56:48 +00:00
import { components } from 'react-select';
2021-05-24 12:04:37 +00:00
import debounce from 'debounce-promise';
2021-05-28 16:25:19 +00:00
import {
Bbox,
getHolidaySuggestion,
getYearSuggestion,
parseHumanDate,
2021-09-22 18:51:55 +00:00
searchCollection,
2021-10-06 05:54:05 +00:00
searchFiles,
2021-05-28 16:25:19 +00:00
searchLocation,
} from 'services/searchService';
2021-05-30 16:56:48 +00:00
import { getFormattedDate } from 'utils/search';
import constants from 'utils/strings/constants';
import LocationIcon from './icons/LocationIcon';
import DateIcon from './icons/DateIcon';
import SearchIcon from './icons/SearchIcon';
import CrossIcon from './icons/CrossIcon';
2021-09-22 18:51:55 +00:00
import { Collection } from 'services/collectionService';
import CollectionIcon from './icons/CollectionIcon';
2021-10-06 05:54:05 +00:00
import { File, FILE_TYPE } from 'services/fileService';
import ImageIcon from './icons/ImageIcon';
import VideoIcon from './icons/VideoIcon';
2021-05-18 04:32:37 +00:00
const Wrapper = styled.div<{ isDisabled: boolean; isOpen: boolean }>`
2021-05-26 06:21:50 +00:00
position: fixed;
top: 0;
z-index: 1000;
display: ${({ isOpen }) => (isOpen ? 'flex' : 'none')};
2021-05-26 06:21:50 +00:00
width: 100%;
background: #111;
@media (min-width: 625px) {
display: flex;
width: calc(100vw - 140px);
margin: 0 70px;
}
2021-05-26 06:21:50 +00:00
align-items: center;
2021-05-18 04:32:37 +00:00
min-height: 64px;
transition: opacity 1s ease;
opacity: ${(props) => (props.isDisabled ? 0 : 1)};
2021-05-18 04:32:37 +00:00
margin-bottom: 10px;
2021-05-26 06:21:50 +00:00
`;
const SearchButton = styled.div<{ isOpen: boolean }>`
display: none;
@media (max-width: 624px) {
display: ${({ isOpen }) => (!isOpen ? 'flex' : 'none')};
right: 80px;
cursor: pointer;
position: fixed;
top: 0;
z-index: 1000;
align-items: center;
min-height: 64px;
}
2021-05-18 04:32:37 +00:00
`;
2021-05-26 07:10:36 +00:00
2021-05-27 08:27:32 +00:00
const SearchStatsContainer = styled.div`
2021-05-26 07:10:36 +00:00
display: flex;
2021-05-26 07:50:52 +00:00
justify-content: center;
2021-05-26 07:10:36 +00:00
align-items: center;
2021-05-26 07:50:52 +00:00
color: #979797;
2021-05-27 12:38:32 +00:00
margin-bottom: 8px;
2021-05-26 07:10:36 +00:00
`;
const SearchInput = styled.div`
width: 100%;
display: flex;
align-items: center;
max-width: 484px;
margin: auto;
`;
2021-05-26 07:47:19 +00:00
export enum SuggestionType {
DATE,
LOCATION,
2021-09-22 18:51:55 +00:00
COLLECTION,
2021-10-06 05:54:05 +00:00
IMAGE,
VIDEO,
}
2021-05-28 16:25:19 +00:00
export interface DateValue {
date?: number;
month?: number;
year?: number;
}
2021-05-26 07:47:19 +00:00
export interface Suggestion {
2021-05-24 12:19:18 +00:00
type: SuggestionType;
label: string;
2021-09-22 18:51:55 +00:00
value: Bbox | DateValue | number;
2021-05-18 07:47:03 +00:00
}
interface Props {
2021-05-18 14:20:15 +00:00
isOpen: boolean;
2021-05-26 16:48:29 +00:00
isFirstFetch: boolean;
2021-09-24 08:43:59 +00:00
setOpen: (value: boolean) => void;
2021-05-18 14:20:15 +00:00
loadingBar: any;
2021-05-27 08:27:32 +00:00
setSearch: (search: Search) => void;
searchStats: SearchStats;
2021-09-22 18:51:55 +00:00
collections: Collection[];
setActiveCollection: (id: number) => void;
2021-10-06 05:54:05 +00:00
files: File[];
2021-05-26 07:10:36 +00:00
}
2021-05-18 07:47:03 +00:00
export default function SearchBar(props: Props) {
const [value, setValue] = useState<Suggestion>(null);
const handleChange = (value) => {
setValue(value);
};
useEffect(() => search(value), [value]);
2021-05-18 14:20:15 +00:00
2021-05-29 06:27:52 +00:00
// = =========================
// Functionality
2021-05-29 06:27:52 +00:00
// = =========================
2021-05-26 11:37:01 +00:00
const getAutoCompleteSuggestions = async (searchPhrase: string) => {
searchPhrase = searchPhrase.trim().toLowerCase();
2021-05-24 12:22:05 +00:00
if (!searchPhrase?.length) {
2021-05-28 16:25:19 +00:00
return [];
2021-05-24 12:22:05 +00:00
}
2021-10-06 05:54:05 +00:00
const options = [
2021-05-28 16:25:19 +00:00
...getHolidaySuggestion(searchPhrase),
...getYearSuggestion(searchPhrase),
];
const searchedDates = parseHumanDate(searchPhrase);
2021-10-06 05:54:05 +00:00
options.push(
2021-05-28 16:25:19 +00:00
...searchedDates.map((searchedDate) => ({
2021-05-24 12:19:18 +00:00
type: SuggestionType.DATE,
value: searchedDate,
2021-05-24 12:19:18 +00:00
label: getFormattedDate(searchedDate),
2021-08-13 02:38:38 +00:00
}))
2021-05-28 16:25:19 +00:00
);
2021-09-22 18:51:55 +00:00
const collectionResults = searchCollection(
searchPhrase,
props.collections
);
2021-10-06 05:54:05 +00:00
options.push(
2021-09-22 18:51:55 +00:00
...collectionResults.map(
(searchResult) =>
({
type: SuggestionType.COLLECTION,
value: searchResult.id,
label: searchResult.name,
} as Suggestion)
)
);
2021-10-06 05:54:05 +00:00
const fileResults = searchFiles(searchPhrase, props.files);
options.push(
...fileResults.map((file) => ({
type:
file.type === FILE_TYPE.IMAGE
? SuggestionType.IMAGE
: SuggestionType.VIDEO,
value: file.index,
label: file.title,
}))
);
2021-09-22 18:51:55 +00:00
const locationResults = await searchLocation(searchPhrase);
2021-10-06 05:54:05 +00:00
options.push(
2021-09-22 18:51:55 +00:00
...locationResults.map(
2021-08-13 02:38:38 +00:00
(searchResult) =>
({
type: SuggestionType.LOCATION,
value: searchResult.bbox,
label: searchResult.place,
} as Suggestion)
)
);
2021-10-06 05:54:05 +00:00
return options;
};
2021-05-20 08:19:37 +00:00
2021-05-26 11:37:01 +00:00
const getOptions = debounce(getAutoCompleteSuggestions, 250);
2021-05-24 12:04:37 +00:00
const search = (selectedOption: Suggestion) => {
if (!selectedOption) {
return;
}
switch (selectedOption.type) {
2021-06-03 08:46:54 +00:00
case SuggestionType.DATE:
props.setSearch({
date: selectedOption.value as DateValue,
});
props.setOpen(true);
2021-06-03 08:46:54 +00:00
break;
case SuggestionType.LOCATION:
props.setSearch({
location: selectedOption.value as Bbox,
});
props.setOpen(true);
2021-06-03 08:46:54 +00:00
break;
2021-09-22 18:51:55 +00:00
case SuggestionType.COLLECTION:
props.setActiveCollection(selectedOption.value as number);
setValue(null);
2021-09-22 18:51:55 +00:00
break;
2021-10-06 05:54:05 +00:00
case SuggestionType.IMAGE:
case SuggestionType.VIDEO:
props.setSearch({ fileIndex: selectedOption.value as number });
setValue(null);
break;
2021-05-20 08:00:02 +00:00
}
2021-05-18 14:20:15 +00:00
};
const resetSearch = () => {
if (props.isOpen) {
2021-05-25 08:49:20 +00:00
props.loadingBar.current?.continuousStart();
2021-05-26 20:36:02 +00:00
props.setSearch({});
2021-05-25 08:49:20 +00:00
setTimeout(() => {
props.loadingBar.current?.complete();
2021-05-25 09:16:20 +00:00
}, 10);
2021-05-25 08:49:20 +00:00
props.setOpen(false);
setValue(null);
2021-05-25 08:49:20 +00:00
}
2021-05-18 14:20:15 +00:00
};
2021-05-29 06:27:52 +00:00
// = =========================
// UI
2021-05-29 06:27:52 +00:00
// = =========================
2021-09-22 18:51:55 +00:00
const getIconByType = (type: SuggestionType) => {
switch (type) {
case SuggestionType.DATE:
return <DateIcon />;
case SuggestionType.LOCATION:
return <LocationIcon />;
case SuggestionType.COLLECTION:
return <CollectionIcon />;
2021-10-06 05:54:05 +00:00
case SuggestionType.IMAGE:
return <ImageIcon />;
case SuggestionType.VIDEO:
return <VideoIcon />;
2021-09-22 18:51:55 +00:00
default:
2021-09-24 07:02:29 +00:00
return <SearchIcon />;
2021-09-22 18:51:55 +00:00
}
};
2021-05-21 13:29:06 +00:00
2021-05-24 12:19:18 +00:00
const LabelWithIcon = (props: { type: SuggestionType; label: string }) => (
2021-05-30 16:56:48 +00:00
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ paddingRight: '10px', paddingBottom: '4px' }}>
2021-05-21 13:29:06 +00:00
{getIconByType(props.type)}
</span>
<span>{props.label}</span>
</div>
);
2021-05-30 16:56:48 +00:00
const { Option, Control } = components;
const OptionWithIcon = (props) => (
<Option {...props}>
2021-05-21 13:29:06 +00:00
<LabelWithIcon type={props.data.type} label={props.data.label} />
</Option>
);
const ControlWithIcon = (props) => (
<Control {...props}>
2021-05-25 10:29:10 +00:00
<span
2021-05-29 06:27:52 +00:00
className="icon"
2021-05-25 10:29:10 +00:00
style={{
paddingLeft: '10px',
2021-05-26 15:31:36 +00:00
paddingBottom: '4px',
2021-08-13 02:38:38 +00:00
}}>
2021-09-24 07:02:29 +00:00
{getIconByType(props.getValue()[0]?.type)}
</span>
{props.children}
</Control>
);
const customStyles = {
2021-05-30 16:56:48 +00:00
control: (style, { isFocused }) => ({
...style,
2021-08-13 02:38:38 +00:00
backgroundColor: '#282828',
color: '#d1d1d1',
2021-08-27 08:52:14 +00:00
borderColor: isFocused ? '#51cd7c' : '#444',
2021-08-13 02:38:38 +00:00
boxShadow: 'none',
':hover': {
2021-08-27 08:52:14 +00:00
borderColor: '#51cd7c',
2021-08-13 02:38:38 +00:00
cursor: 'text',
2021-08-27 08:52:14 +00:00
'&>.icon': { color: '#51cd7c' },
},
}),
input: (style) => ({
...style,
color: '#d1d1d1',
}),
menu: (style) => ({
...style,
2021-05-21 13:29:06 +00:00
marginTop: '10px',
backgroundColor: '#282828',
}),
2021-05-30 16:56:48 +00:00
option: (style, { isFocused }) => ({
...style,
backgroundColor: isFocused && '#343434',
}),
dropdownIndicator: (style) => ({
...style,
display: 'none',
}),
indicatorSeparator: (style) => ({
...style,
display: 'none',
}),
clearIndicator: (style) => ({
...style,
2021-05-25 10:29:10 +00:00
display: 'none',
}),
2021-05-25 11:06:14 +00:00
singleValue: (style, state) => ({
...style,
backgroundColor: '#282828',
color: '#d1d1d1',
2021-05-25 11:06:14 +00:00
display: state.selectProps.menuIsOpen ? 'none' : 'block',
}),
2021-05-25 09:47:57 +00:00
placeholder: (style) => ({
...style,
color: '#686868',
wordSpacing: '2px',
2021-05-27 10:36:45 +00:00
whiteSpace: 'nowrap',
2021-05-25 11:06:14 +00:00
}),
};
2021-05-26 07:10:36 +00:00
return (
2021-05-28 14:02:48 +00:00
<>
2021-05-27 08:27:32 +00:00
{props.searchStats && (
<SearchStatsContainer>
{constants.SEARCH_STATS(props.searchStats)}
</SearchStatsContainer>
)}
<Wrapper isDisabled={props.isFirstFetch} isOpen={props.isOpen}>
<SearchInput>
2021-05-26 16:48:29 +00:00
<div
style={{
flex: 1,
margin: '10px',
2021-08-13 02:38:38 +00:00
}}>
2021-05-26 16:48:29 +00:00
<AsyncSelect
value={value}
2021-05-26 16:48:29 +00:00
components={{
Option: OptionWithIcon,
Control: ControlWithIcon,
2021-05-26 07:10:36 +00:00
}}
2021-05-26 16:48:29 +00:00
placeholder={constants.SEARCH_HINT()}
loadOptions={getOptions}
onChange={handleChange}
2021-05-26 16:48:29 +00:00
isClearable
escapeClearsValue
styles={customStyles}
noOptionsMessage={() => null}
/>
</div>
2021-05-30 16:56:48 +00:00
<div style={{ width: '24px' }}>
2021-05-26 16:48:29 +00:00
{props.isOpen && (
<div
2021-05-30 16:56:48 +00:00
style={{ cursor: 'pointer' }}
onClick={() => resetSearch()}>
2021-05-26 16:48:29 +00:00
<CrossIcon />
</div>
)}
</div>
</SearchInput>
</Wrapper>
<SearchButton
isOpen={props.isOpen}
onClick={() => !props.isFirstFetch && props.setOpen(true)}>
<SearchIcon />
</SearchButton>
2021-05-28 14:02:48 +00:00
</>
2021-05-18 07:47:03 +00:00
);
2021-05-18 04:32:37 +00:00
}