diff --git a/.gitignore b/.gitignore
index 529cfe1c88..1221725994 100644
--- a/.gitignore
+++ b/.gitignore
@@ -119,3 +119,7 @@ dist
!.yarn/releases
!.yarn/sdks
!.yarn/versions
+
+
+# IDEs
+.idea/
diff --git a/packages/libs/web-common/src/App/Header/HeaderNav.jsx b/packages/libs/web-common/src/App/Header/HeaderNav.jsx
index f2eb3cd22a..3247b84f1e 100755
--- a/packages/libs/web-common/src/App/Header/HeaderNav.jsx
+++ b/packages/libs/web-common/src/App/Header/HeaderNav.jsx
@@ -92,7 +92,7 @@ class HeaderNav extends React.Component {
maxWidth: '35em',
}}
>
-
+
)}
@@ -212,7 +212,7 @@ class HeaderNav extends React.Component {
fontSize: '1.2em',
}}
>
-
+
)}
diff --git a/packages/libs/web-common/src/components/SiteSearch/SiteSearch.scss b/packages/libs/web-common/src/components/SiteSearch/SiteSearch.scss
index 6872244c56..31d457fe41 100644
--- a/packages/libs/web-common/src/components/SiteSearch/SiteSearch.scss
+++ b/packages/libs/web-common/src/components/SiteSearch/SiteSearch.scss
@@ -94,23 +94,100 @@
display: flex;
justify-content: space-between;
border-radius: 1.5em;
- overflow: hidden;
+ position: relative;
+
+ > :first-child {
+ border-radius: 1.5em 0 0 1.5em;
+ overflow: hidden;
+ }
+
+ > :last-child {
+ border-radius: 0 1.5em 1.5em 0;
+ overflow: hidden;
+ }
&:focus-within {
box-shadow: 0 0 3pt 2pt #0067f4;
}
- input {
- border: none !important;
- border-radius: 0;
- padding: 0.4em 0.9em !important;
+ div.type-ahead {
width: 100%;
- &:focus {
- outline: none;
+
+ div.type-ahead-input {
+ position: relative;
+
+ input {
+ border: none !important;
+ padding: 0.4em 0.9em !important;
+ width: 100%;
+
+ &:focus {
+ outline: none;
+ }
+ }
+
+ input:first-child {
+ position: relative;
+ background: #00000000 !important;
+ z-index: 1;
+ }
+
+ input:nth-child(2) {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 0;
+ color: gray;
+ }
+ }
+
+ ul.type-ahead-hints {
+ margin: 0.2em 0 0 0;
+ padding: 0;
+ list-style: none;
+ position: absolute;
+ background: #ededed;
+ overflow: hidden;
+ border-radius: 1em;
+ box-shadow: 0 0 1em rgba(0, 48, 76, 0.5);
+ left: 0;
+ right: 0;
+
+ li.type-ahead-hint {
+ padding: 0.2em 0 0.2em 1em;
+ text-align: left;
+
+ &[tabindex] {
+ cursor: pointer;
+ &:focus,
+ &:active,
+ &:hover {
+ background: #dedede;
+ }
+ }
+
+ &:first-child {
+ padding-top: 0.4em;
+ }
+
+ &:last-child {
+ padding-bottom: 0.4em;
+ }
+ }
}
}
- button {
+ &__with-filters {
+ div.type-ahead {
+ ul.type-ahead-hints {
+ li.type-ahead-hint {
+ padding-left: 13ex;
+ }
+ }
+ }
+ }
+
+ > button {
border: none;
border-radius: 0;
background: #6c757d;
diff --git a/packages/libs/web-common/src/components/SiteSearch/SiteSearchHooks.ts b/packages/libs/web-common/src/components/SiteSearch/SiteSearchHooks.ts
new file mode 100644
index 0000000000..7b3381e70f
--- /dev/null
+++ b/packages/libs/web-common/src/components/SiteSearch/SiteSearchHooks.ts
@@ -0,0 +1,16 @@
+import { useStorageBackedState } from '@veupathdb/wdk-client/lib/Hooks/StorageBackedState';
+import {
+ arrayOf,
+ decodeOrElse,
+ string,
+} from '@veupathdb/wdk-client/lib/Utils/Json';
+
+export function useRecentSearches() {
+ return useStorageBackedState(
+ window.localStorage,
+ [],
+ 'site-search/history',
+ JSON.stringify,
+ (value) => decodeOrElse(arrayOf(string), [], value)
+ );
+}
diff --git a/packages/libs/web-common/src/components/SiteSearch/SiteSearchInput.tsx b/packages/libs/web-common/src/components/SiteSearch/SiteSearchInput.tsx
index 4af2d92e4c..d922e8526a 100644
--- a/packages/libs/web-common/src/components/SiteSearch/SiteSearchInput.tsx
+++ b/packages/libs/web-common/src/components/SiteSearch/SiteSearchInput.tsx
@@ -1,4 +1,4 @@
-import { isEmpty } from 'lodash';
+import { isEmpty, uniq } from 'lodash';
import React, { useCallback, useEffect, useRef } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { Tooltip } from '@veupathdb/wdk-client/lib/Components';
@@ -14,6 +14,8 @@ import {
ORGANISM_PARAM,
FILTERS_PARAM,
} from './SiteSearchConstants';
+import { TypeAheadInput } from './TypeAheadInput';
+import { useRecentSearches } from './SiteSearchHooks';
import './SiteSearch.scss';
@@ -26,9 +28,13 @@ const preventEventWith = (callback: () => void) => (event: React.FormEvent) => {
export interface Props {
placeholderText?: string;
+ siteSearchURL: string;
}
-export const SiteSearchInput = wrappable(function ({ placeholderText }: Props) {
+export const SiteSearchInput = wrappable(function ({
+ placeholderText,
+ siteSearchURL,
+}: Props) {
const location = useLocation();
const history = useHistory();
const inputRef = useRef
(null);
@@ -53,6 +59,8 @@ export const SiteSearchInput = wrappable(function ({ placeholderText }: Props) {
const hasFilters =
!isEmpty(docType) || !isEmpty(organisms) || !isEmpty(fields);
+ const [recentSearches, setRecentSearches] = useRecentSearches();
+
const onSearch = useCallback(
(queryString: string) => {
history.push(`${SITE_SEARCH_ROUTE}?${queryString}`);
@@ -60,20 +68,42 @@ export const SiteSearchInput = wrappable(function ({ placeholderText }: Props) {
[history]
);
+ const saveSearchString = useCallback(() => {
+ if (inputRef.current?.value) {
+ setRecentSearches(
+ uniq([inputRef.current.value].concat(recentSearches)).slice(0, 10)
+ );
+ }
+ }, [setRecentSearches, recentSearches]);
+
const handleSubmitWithFilters = useCallback(() => {
const { current } = formRef;
if (current == null) return;
const formData = new FormData(current);
const queryString = new URLSearchParams(formData as any).toString();
onSearch(queryString);
- }, [onSearch]);
+ saveSearchString();
+ }, [onSearch, saveSearchString]);
const handleSubmitWithoutFilters = useCallback(() => {
const queryString = `q=${encodeURIComponent(
inputRef.current?.value || ''
)}`;
onSearch(queryString);
- }, [onSearch]);
+ saveSearchString();
+ }, [onSearch, saveSearchString]);
+
+ const handleSubmitWithRecentSearch = useCallback(
+ (searchString: string) => {
+ const queryString = `q=${encodeURIComponent(searchString)}`;
+ onSearch(queryString);
+ },
+ [onSearch]
+ );
+
+ const clearRecentSearches = useCallback(() => {
+ setRecentSearches([]);
+ }, [setRecentSearches]);
const [lastSearchQueryString, setLastSearchQueryString] =
useSessionBackedState(
@@ -93,9 +123,21 @@ export const SiteSearchInput = wrappable(function ({ placeholderText }: Props) {
-