diff --git a/package.json b/package.json index 2942599..13858cf 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.6.3", "private": true, "dependencies": { + "@ant-design/icons": "^4.7.0", "antd": "^3.12.1", "debounce": "^1.2.0", "react": "^16.7.0", diff --git a/src/components/App.js b/src/components/App.js index 24949b2..58e763d 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -16,6 +16,7 @@ window.apiUrl = process.env.REACT_APP_API_URL; export default function App({ match }) { const { page } = match.params; const [services, setServices] = useState([]); + const [filteredServices, setFilteredServices] = useState(); const [username, setUsername] = useState(''); const fetchServices = async () => { @@ -33,6 +34,12 @@ export default function App({ match }) { setUsername(text); }; + // Can not listen localstorage changes on the same window + // Had to use callback + const onFilterChange = filters => { + setFilteredServices(filters); + }; + return useMemo(() => { // main content of page let content; @@ -40,7 +47,7 @@ export default function App({ match }) { if (username.length > 0) { content = (
- +
); } else { @@ -71,7 +78,12 @@ export default function App({ match }) { <>
- +
{content} diff --git a/src/components/NewsletterSubscription.js b/src/components/NewsletterSubscription.js index 572b201..56b38e1 100644 --- a/src/components/NewsletterSubscription.js +++ b/src/components/NewsletterSubscription.js @@ -72,8 +72,6 @@ export default function NewsletterSubscription({ illustrationEnabled = false }) } }, [subscriptionInfo]); - console.log(); - return (
{illustrationEnabled && ( diff --git a/src/components/Results.js b/src/components/Results.js index 28bb795..3b66ef1 100644 --- a/src/components/Results.js +++ b/src/components/Results.js @@ -3,14 +3,16 @@ import debounce from 'debounce'; import '../styles/Results.css'; import ResultCard from './ResultCard'; -export default function Results({ username, services }) { +export default function Results({ username, services, filteredServices }) { + const [selectedServices] = useState(filteredServices || []); + // these are cards with loading state enabled. Create once, use many times const spinningCards = useMemo( () => - services.map(({ service: serviceName }) => ( + (selectedServices?.length ? selectedServices : services).map(({ service: serviceName }) => ( )), - [services], + [services, selectedServices], ); // cards to render in this component @@ -18,12 +20,14 @@ export default function Results({ username, services }) { // returns real functional cards const createCards = (username, services) => - services.map(({ service: serviceName, endpoint }) => { - const checkEndpoint = endpoint.replace('{username}', username); - return ( - - ); - }); + (selectedServices?.length ? selectedServices : services).map( + ({ service: serviceName, endpoint }) => { + const checkEndpoint = endpoint.replace('{username}', username); + return ( + + ); + }, + ); // sets the cards with a debounce. This allows us not to calculate real cards while user keeps typing const debouncedSetCards = useCallback( diff --git a/src/components/Search.js b/src/components/Search.js index 31015b1..cf991c0 100644 --- a/src/components/Search.js +++ b/src/components/Search.js @@ -1,27 +1,60 @@ -import React, { useMemo } from 'react'; -import { Input, Icon } from 'antd'; +import React, { useMemo, useRef, useEffect, useState, useCallback } from 'react'; +import { Input, Icon, Select, Button } from 'antd'; import { Link } from 'react-router-dom'; +import { SettingOutlined } from '@ant-design/icons'; +import useLocalStorage from '../hooks/useLocalStorage'; // TODO: use styled components instead import '../styles/Search.css'; -export default function Search({ input, onChange }) { - const inputChanged = ({ target }) => { +export default function Search({ input, onChange, services, onFilterChange }) { + const inputRef = useRef(); + const [filters, setFilters] = useLocalStorage('filters', []); + const [selectedObjects, setSelectedObjects] = useLocalStorage('filterObjects', []); + const [isFilterActive, setFilterActive] = useState(Boolean(selectedObjects.length)); + + useEffect(() => { + onFilterChange(selectedObjects); + }, [onFilterChange, selectedObjects]); + + const prettifyInput = useCallback(input => { // niceInput is the url friendly version of the input - let niceInput = target.value.replace(/[^a-zA-Z0-9-_.]/g, ''); - onChange(niceInput); - }; + return input.replace(/[^a-zA-Z0-9-_.]/g, ''); + }, []); + + const inputChanged = useCallback( + ({ target }) => { + onChange(prettifyInput(target.value)); + }, + [onChange, prettifyInput], + ); - const clearInput = () => { - onChange(''); + const findServiceObjects = useCallback( + selections => { + return services?.filter(s => selections?.includes(s.service)); + }, + [services], + ); + + const onSearchTargetChange = useCallback( + values => { + setFilters(values); + setSelectedObjects(findServiceObjects(values)); + onChange(''); + }, + [onChange, setSelectedObjects, setFilters, findServiceObjects], + ); + + const onSearchOptionButtonClick = () => { + setFilterActive(oldState => !oldState); }; - return useMemo(() => { + const searchContent = useMemo(() => { return ( -
+ <> { - clearInput(); + onChange(''); }} >
@@ -30,6 +63,8 @@ export default function Search({ input, onChange }) {
-
+ + ); + }, [input, onChange, inputChanged]); + + const filterContent = useMemo(() => { + return ( + <> + + + ); - // eslint-disable-next-line - }, [input]); + }, [services, filters, onSearchTargetChange]); + + const filterContentWrapper = useMemo(() => { + return ( +
{filterContent}
+ ); + }, [filterContent, isFilterActive]); + + return ( +
+ {searchContent} + {filterContentWrapper} +
+ ); } diff --git a/src/hooks/useLocalStorage.js b/src/hooks/useLocalStorage.js new file mode 100644 index 0000000..bbbe942 --- /dev/null +++ b/src/hooks/useLocalStorage.js @@ -0,0 +1,34 @@ +import { useState } from 'react'; + +function useLocalStorage(key, initialValue) { + const [storedValue, setStoredValue] = useState(() => { + try { + // Get from local storage by key + const item = window.localStorage.getItem(key); + // Parse stored json or if none return initialValue + return item ? JSON.parse(item) : initialValue; + } catch (error) { + // If error also return initialValue + console.log(error); + return initialValue; + } + }); + // Return a wrapped version of useState's setter function that ... + // ... persists the new value to localStorage. + const setValue = value => { + try { + // Allow value to be a function so we have same API as useState + const valueToStore = value instanceof Function ? value(storedValue) : value; + // Save state + setStoredValue(valueToStore); + // Save to local storage + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } catch (error) { + // A more advanced implementation would handle the error case + console.log(error); + } + }; + return [storedValue, setValue]; +} + +export default useLocalStorage; diff --git a/src/styles/Search.css b/src/styles/Search.css index 9435f30..6f391bd 100644 --- a/src/styles/Search.css +++ b/src/styles/Search.css @@ -16,6 +16,45 @@ margin-right: auto; } +.searchInput { + z-index: 1; +} + +.advancedSearchWrapper { + position: relative; + width: 90%; + color: white; + font-size: 1.125rem; + padding: 0.25em; + display: flex; + align-items: center; + flex-direction: column; + top: -45px; + transition: 0.3s; +} + +.advancedSearchWrapper > div.ant-select { + opacity: 0; + transition: 0.3s; +} + +.advancedSearchWrapper.active > div.ant-select { + opacity: 1; +} + +.advancedSearchWrapper.active { + top: 0; +} + +.searchOptionButton { + margin-top: 4.5px; +} + +.targetSelector { + width: 100%; + background: transparent; +} + .header { display: flex; flex-direction: row; @@ -46,10 +85,10 @@ .anticon-thunderbolt { font-size: 2.4rem !important; } - .ant-input.ant-input-lg { + /* .ant-input.ant-input-lg { font-size: 0.9rem; height: 1.8rem !important; - } + } */ } @media (min-width: 360px) { @@ -62,10 +101,10 @@ .anticon-thunderbolt { font-size: 3rem !important; } - .ant-input.ant-input-lg { + /* .ant-input.ant-input-lg { font-size: 1.05rem; height: 2.1rem !important; - } + } */ } @media (min-width: 768px) { @@ -78,10 +117,10 @@ .anticon-thunderbolt { font-size: 3.7rem !important; } - .ant-input.ant-input-lg { + /* .ant-input.ant-input-lg { font-size: 1.3rem; height: 2.6rem !important; - } + } */ } @media (min-width: 992px) { .header { @@ -93,8 +132,8 @@ .anticon-thunderbolt { font-size: 3.8rem !important; } - .ant-input.ant-input-lg { + /* .ant-input.ant-input-lg { font-size: 1.25rem; height: 2.7rem !important; - } + } */ }