From 793edc1aea1460dd3db6dd2320ec90d7716f31c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emre=20=C3=87ak=C4=B1r?= Date: Fri, 19 Nov 2021 23:42:43 +0300 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20search=20filter=20by?= =?UTF-8?q?=20platform=20has=20implemented?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/App.js | 16 +++++- src/components/NewsletterSubscription.js | 2 - src/components/Results.js | 20 ++++--- src/components/Search.js | 70 ++++++++++++++++++++---- src/hooks/useLocalStorage.js | 34 ++++++++++++ src/styles/Search.css | 15 +++++ 6 files changed, 135 insertions(+), 22 deletions(-) create mode 100644 src/hooks/useLocalStorage.js 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..d2b601d 100644 --- a/src/components/Results.js +++ b/src/components/Results.js @@ -3,11 +3,13 @@ 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], @@ -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..44749a5 100644 --- a/src/components/Search.js +++ b/src/components/Search.js @@ -1,23 +1,45 @@ -import React, { useMemo } from 'react'; -import { Input, Icon } from 'antd'; +import React, { useMemo, useRef, useEffect } from 'react'; +import { Input, Icon, Select } from 'antd'; import { Link } from 'react-router-dom'; // TODO: use styled components instead import '../styles/Search.css'; +import useLocalStorage from '../hooks/useLocalStorage'; -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', []); + + useEffect(() => { + onFilterChange(selectedObjects); + }, [onFilterChange, selectedObjects]); + + const prettifyInput = 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 = ({ target }) => { + onChange(prettifyInput(target.value)); }; const clearInput = () => { onChange(''); }; - return useMemo(() => { + const findServiceObjects = selections => { + return services?.filter(s => selections?.includes(s.service)); + }; + + const onSearchTargetChange = values => { + setFilters(values); + setSelectedObjects(findServiceObjects(values)); + clearInput(); + }; + + const searchContent = useMemo(() => { return ( -
+ <> { @@ -30,6 +52,7 @@ export default function Search({ input, onChange }) {
-
+ ); - // eslint-disable-next-line }, [input]); + + const filterContent = useMemo(() => { + return ( +
+
Limit search targets
+ + +
+ ); + }, [services]); + + return ( +
+ {searchContent} + {filterContent} +
+ ); } 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..9e24336 100644 --- a/src/styles/Search.css +++ b/src/styles/Search.css @@ -16,6 +16,21 @@ margin-right: auto; } +.advancedSearchWrapper { + color: white; + font-size: 1.125rem; + padding: 0.25em; + width: 100%; + display: flex; + align-items: center; + flex-direction: column; +} + +.targetSelector { + width: 100%; + background: transparent; +} + .header { display: flex; flex-direction: row; From 6789c3856584d4b8c8640db13d297351800faf5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emre=20=C3=87ak=C4=B1r?= Date: Tue, 30 Nov 2021 16:39:04 +0300 Subject: [PATCH 2/3] =?UTF-8?q?style:=20=F0=9F=92=84=20advanced=20search?= =?UTF-8?q?=20animate=20on=20open-close?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/components/Search.js | 33 ++++++++++++++++++++++--------- src/styles/Search.css | 42 +++++++++++++++++++++++++++++++--------- 3 files changed, 58 insertions(+), 18 deletions(-) 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/Search.js b/src/components/Search.js index 44749a5..75fa006 100644 --- a/src/components/Search.js +++ b/src/components/Search.js @@ -1,14 +1,16 @@ -import React, { useMemo, useRef, useEffect } from 'react'; -import { Input, Icon, Select } from 'antd'; +import React, { useMemo, useRef, useEffect, useState } 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'; -import useLocalStorage from '../hooks/useLocalStorage'; 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); @@ -37,6 +39,10 @@ export default function Search({ input, onChange, services, onFilterChange }) { clearInput(); }; + const onSearchOptionButtonClick = () => { + setFilterActive(oldState => !oldState); + }; + const searchContent = useMemo(() => { return ( <> @@ -53,6 +59,7 @@ export default function Search({ input, onChange, services, onFilterChange }) { { return ( -
-
Limit search targets
- + <> -
+ + ); }, [services]); + const filterContentWrapper = useMemo(() => { + return ( +
{filterContent}
+ ); + }, [filterContent, isFilterActive]); + return (
{searchContent} - {filterContent} + {filterContentWrapper}
); } diff --git a/src/styles/Search.css b/src/styles/Search.css index 9e24336..6f391bd 100644 --- a/src/styles/Search.css +++ b/src/styles/Search.css @@ -16,14 +16,38 @@ margin-right: auto; } +.searchInput { + z-index: 1; +} + .advancedSearchWrapper { + position: relative; + width: 90%; color: white; font-size: 1.125rem; padding: 0.25em; - width: 100%; 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 { @@ -61,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) { @@ -77,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) { @@ -93,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 { @@ -108,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; - } + } */ } From e6d9305cf184057001949bb54b9cc8020a1d48b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emre=20=C3=87ak=C4=B1r?= Date: Tue, 30 Nov 2021 16:55:01 +0300 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20build=20errors?= =?UTF-8?q?=20resolved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Results.js | 2 +- src/components/Search.js | 47 ++++++++++++++++++++++----------------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/components/Results.js b/src/components/Results.js index d2b601d..3b66ef1 100644 --- a/src/components/Results.js +++ b/src/components/Results.js @@ -12,7 +12,7 @@ export default function Results({ username, services, filteredServices }) { (selectedServices?.length ? selectedServices : services).map(({ service: serviceName }) => ( )), - [services], + [services, selectedServices], ); // cards to render in this component diff --git a/src/components/Search.js b/src/components/Search.js index 75fa006..cf991c0 100644 --- a/src/components/Search.js +++ b/src/components/Search.js @@ -1,4 +1,4 @@ -import React, { useMemo, useRef, useEffect, useState } from 'react'; +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'; @@ -16,28 +16,33 @@ export default function Search({ input, onChange, services, onFilterChange }) { onFilterChange(selectedObjects); }, [onFilterChange, selectedObjects]); - const prettifyInput = input => { + const prettifyInput = useCallback(input => { // niceInput is the url friendly version of the input return input.replace(/[^a-zA-Z0-9-_.]/g, ''); - }; - - const inputChanged = ({ target }) => { - onChange(prettifyInput(target.value)); - }; + }, []); - const clearInput = () => { - onChange(''); - }; + const inputChanged = useCallback( + ({ target }) => { + onChange(prettifyInput(target.value)); + }, + [onChange, prettifyInput], + ); - const findServiceObjects = selections => { - return services?.filter(s => selections?.includes(s.service)); - }; + const findServiceObjects = useCallback( + selections => { + return services?.filter(s => selections?.includes(s.service)); + }, + [services], + ); - const onSearchTargetChange = values => { - setFilters(values); - setSelectedObjects(findServiceObjects(values)); - clearInput(); - }; + const onSearchTargetChange = useCallback( + values => { + setFilters(values); + setSelectedObjects(findServiceObjects(values)); + onChange(''); + }, + [onChange, setSelectedObjects, setFilters, findServiceObjects], + ); const onSearchOptionButtonClick = () => { setFilterActive(oldState => !oldState); @@ -49,7 +54,7 @@ export default function Search({ input, onChange, services, onFilterChange }) { { - clearInput(); + onChange(''); }} >
@@ -69,7 +74,7 @@ export default function Search({ input, onChange, services, onFilterChange }) { /> ); - }, [input]); + }, [input, onChange, inputChanged]); const filterContent = useMemo(() => { return ( @@ -92,7 +97,7 @@ export default function Search({ input, onChange, services, onFilterChange }) { ); - }, [services]); + }, [services, filters, onSearchTargetChange]); const filterContentWrapper = useMemo(() => { return (