2021-09-22 18:51:55 +00:00
|
|
|
import { Search, SearchStats } from 'pages/gallery';
|
2021-09-24 10:16:49 +00:00
|
|
|
import React, { useEffect, useState } from 'react';
|
2021-05-18 04:32:37 +00:00
|
|
|
import styled from 'styled-components';
|
2021-05-21 09:46:56 +00:00
|
|
|
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-05-28 16:25:19 +00:00
|
|
|
searchLocation,
|
|
|
|
} from 'services/searchService';
|
2021-05-30 16:56:48 +00:00
|
|
|
import { getFormattedDate } from 'utils/search';
|
2021-05-21 09:46:56 +00:00
|
|
|
import constants from 'utils/strings/constants';
|
2021-06-14 15:05:43 +00:00
|
|
|
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-05-18 04:32:37 +00:00
|
|
|
|
2021-08-24 07:57:43 +00:00
|
|
|
const Wrapper = styled.div<{ isDisabled: boolean; isOpen: boolean }>`
|
2021-05-26 06:21:50 +00:00
|
|
|
position: fixed;
|
|
|
|
top: 0;
|
2021-08-24 07:57:43 +00:00
|
|
|
z-index: 1000;
|
|
|
|
display: ${({ isOpen }) => (isOpen ? 'flex' : 'none')};
|
2021-05-26 06:21:50 +00:00
|
|
|
width: 100%;
|
2021-08-24 07:57:43 +00:00
|
|
|
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;
|
2021-05-28 14:26:35 +00:00
|
|
|
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
|
|
|
`;
|
|
|
|
|
2021-08-24 07:57:43 +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
|
|
|
`;
|
|
|
|
|
2021-08-24 07:57:43 +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 {
|
2021-05-21 09:46:56 +00:00
|
|
|
DATE,
|
|
|
|
LOCATION,
|
2021-09-22 18:51:55 +00:00
|
|
|
COLLECTION,
|
2021-05-21 09:46:56 +00:00
|
|
|
}
|
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;
|
2021-05-21 09:46:56 +00:00
|
|
|
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-05-26 07:10:36 +00:00
|
|
|
}
|
2021-05-18 07:47:03 +00:00
|
|
|
export default function SearchBar(props: Props) {
|
2021-09-24 10:16:49 +00:00
|
|
|
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
|
|
|
// = =========================
|
2021-05-25 08:08:57 +00:00
|
|
|
// Functionality
|
2021-05-29 06:27:52 +00:00
|
|
|
// = =========================
|
2021-05-26 11:37:01 +00:00
|
|
|
const getAutoCompleteSuggestions = async (searchPhrase: string) => {
|
2021-09-24 14:11:01 +00:00
|
|
|
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-05-29 06:27:52 +00:00
|
|
|
const option = [
|
2021-05-28 16:25:19 +00:00
|
|
|
...getHolidaySuggestion(searchPhrase),
|
|
|
|
...getYearSuggestion(searchPhrase),
|
|
|
|
];
|
|
|
|
|
|
|
|
const searchedDates = parseHumanDate(searchPhrase);
|
|
|
|
|
|
|
|
option.push(
|
|
|
|
...searchedDates.map((searchedDate) => ({
|
2021-05-24 12:19:18 +00:00
|
|
|
type: SuggestionType.DATE,
|
2021-05-21 09:46:56 +00:00
|
|
|
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
|
|
|
|
);
|
|
|
|
option.push(
|
|
|
|
...collectionResults.map(
|
|
|
|
(searchResult) =>
|
|
|
|
({
|
|
|
|
type: SuggestionType.COLLECTION,
|
|
|
|
value: searchResult.id,
|
|
|
|
label: searchResult.name,
|
|
|
|
} as Suggestion)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
const locationResults = await searchLocation(searchPhrase);
|
2021-05-21 09:46:56 +00:00
|
|
|
option.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-05-21 09:46:56 +00:00
|
|
|
);
|
|
|
|
return option;
|
|
|
|
};
|
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
|
|
|
|
2021-09-24 10:16:49 +00:00
|
|
|
const search = (selectedOption: Suggestion) => {
|
2021-05-21 09:46:56 +00:00
|
|
|
if (!selectedOption) {
|
|
|
|
return;
|
|
|
|
}
|
2021-05-20 08:19:37 +00:00
|
|
|
|
2021-05-21 09:46:56 +00:00
|
|
|
switch (selectedOption.type) {
|
2021-06-03 08:46:54 +00:00
|
|
|
case SuggestionType.DATE:
|
|
|
|
props.setSearch({
|
|
|
|
date: selectedOption.value as DateValue,
|
|
|
|
});
|
2021-09-24 07:20:47 +00:00
|
|
|
props.setOpen(true);
|
2021-06-03 08:46:54 +00:00
|
|
|
break;
|
|
|
|
case SuggestionType.LOCATION:
|
|
|
|
props.setSearch({
|
|
|
|
location: selectedOption.value as Bbox,
|
|
|
|
});
|
2021-09-24 07:20:47 +00:00
|
|
|
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);
|
2021-10-07 05:54:21 +00:00
|
|
|
setValue(null);
|
2021-09-22 18:51:55 +00:00
|
|
|
break;
|
2021-05-20 08:00:02 +00:00
|
|
|
}
|
2021-05-18 14:20:15 +00:00
|
|
|
};
|
2021-10-07 05:54:21 +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);
|
2021-09-24 10:16:49 +00:00
|
|
|
setValue(null);
|
2021-05-25 08:49:20 +00:00
|
|
|
}
|
2021-05-18 14:20:15 +00:00
|
|
|
};
|
2021-05-21 09:46:56 +00:00
|
|
|
|
2021-05-29 06:27:52 +00:00
|
|
|
// = =========================
|
2021-05-25 08:08:57 +00:00
|
|
|
// UI
|
2021-05-29 06:27:52 +00:00
|
|
|
// = =========================
|
2021-05-25 08:08:57 +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 />;
|
|
|
|
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-21 10:41:22 +00:00
|
|
|
);
|
2021-05-30 16:56:48 +00:00
|
|
|
const { Option, Control } = components;
|
2021-05-26 15:22:01 +00:00
|
|
|
|
2021-05-21 10:41:22 +00:00
|
|
|
const OptionWithIcon = (props) => (
|
|
|
|
<Option {...props}>
|
2021-05-21 13:29:06 +00:00
|
|
|
<LabelWithIcon type={props.data.type} label={props.data.label} />
|
2021-05-21 10:41:22 +00:00
|
|
|
</Option>
|
|
|
|
);
|
2021-05-24 15:52:07 +00:00
|
|
|
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)}
|
2021-05-24 15:52:07 +00:00
|
|
|
</span>
|
|
|
|
{props.children}
|
|
|
|
</Control>
|
|
|
|
);
|
2021-05-21 10:41:22 +00:00
|
|
|
|
2021-05-21 09:46:56 +00:00
|
|
|
const customStyles = {
|
2021-05-30 16:56:48 +00:00
|
|
|
control: (style, { isFocused }) => ({
|
2021-05-25 06:20:18 +00:00
|
|
|
...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',
|
2021-05-21 09:46:56 +00:00
|
|
|
':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' },
|
2021-05-21 09:46:56 +00:00
|
|
|
},
|
|
|
|
}),
|
2021-05-25 06:20:18 +00:00
|
|
|
input: (style) => ({
|
|
|
|
...style,
|
2021-05-21 09:46:56 +00:00
|
|
|
color: '#d1d1d1',
|
|
|
|
}),
|
2021-05-25 06:20:18 +00:00
|
|
|
menu: (style) => ({
|
|
|
|
...style,
|
2021-05-21 13:29:06 +00:00
|
|
|
marginTop: '10px',
|
2021-05-21 09:46:56 +00:00
|
|
|
backgroundColor: '#282828',
|
|
|
|
}),
|
2021-05-30 16:56:48 +00:00
|
|
|
option: (style, { isFocused }) => ({
|
2021-05-25 06:20:18 +00:00
|
|
|
...style,
|
2021-05-21 09:46:56 +00:00
|
|
|
backgroundColor: isFocused && '#343434',
|
|
|
|
}),
|
2021-05-25 06:20:18 +00:00
|
|
|
dropdownIndicator: (style) => ({
|
|
|
|
...style,
|
2021-05-21 09:46:56 +00:00
|
|
|
display: 'none',
|
|
|
|
}),
|
2021-05-25 06:20:18 +00:00
|
|
|
indicatorSeparator: (style) => ({
|
|
|
|
...style,
|
2021-05-21 09:46:56 +00:00
|
|
|
display: 'none',
|
|
|
|
}),
|
2021-05-25 06:20:18 +00:00
|
|
|
clearIndicator: (style) => ({
|
|
|
|
...style,
|
2021-05-25 10:29:10 +00:00
|
|
|
display: 'none',
|
2021-05-21 09:46:56 +00:00
|
|
|
}),
|
2021-05-25 11:06:14 +00:00
|
|
|
singleValue: (style, state) => ({
|
2021-05-25 06:20:18 +00:00
|
|
|
...style,
|
2021-05-21 09:46:56 +00:00
|
|
|
backgroundColor: '#282828',
|
|
|
|
color: '#d1d1d1',
|
2021-05-25 11:06:14 +00:00
|
|
|
display: state.selectProps.menuIsOpen ? 'none' : 'block',
|
2021-05-21 09:46:56 +00:00
|
|
|
}),
|
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-21 09:46:56 +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>
|
|
|
|
)}
|
2021-08-24 07:57:43 +00:00
|
|
|
<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
|
2021-09-24 10:16:49 +00:00
|
|
|
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}
|
2021-09-24 10:16:49 +00:00
|
|
|
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' }}
|
2021-09-24 07:20:47 +00:00
|
|
|
onClick={() => resetSearch()}>
|
2021-05-26 16:48:29 +00:00
|
|
|
<CrossIcon />
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
2021-08-24 07:57:43 +00:00
|
|
|
</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
|
|
|
}
|