diff --git a/src/components/Projects/StyledProjects.tsx b/src/components/Projects/StyledProjects.tsx index fa3c8c0b..8d0920ad 100644 --- a/src/components/Projects/StyledProjects.tsx +++ b/src/components/Projects/StyledProjects.tsx @@ -40,6 +40,10 @@ export const ProjectsHeader = styled.div` } } } + .projectCount { + display: flex; + gap: 0.5rem; + } `; export const SearchInput = styled.input` diff --git a/src/components/Projects/index.js b/src/components/Projects/index.js index c9fd19d2..a48d007b 100644 --- a/src/components/Projects/index.js +++ b/src/components/Projects/index.js @@ -1,8 +1,11 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import Highlighter from 'react-highlight-words'; +import { LoadingOutlined } from '@ant-design/icons'; +import { Spin } from 'antd'; import Box from 'components/Box'; import ProjectLink from 'components/link/Project'; +import { debounce } from 'lib/util'; import { ProjectsHeader, @@ -18,6 +21,8 @@ import { */ const Projects = ({ projects = [], initialSearch }) => { const [searchInput, setSearchInput] = useState(initialSearch); + const [isFiltering, setIsFiltering] = useState(false); + const [filteredProjects, setFilteredProjects] = useState(projects); const searchInputRef = useRef(null); @@ -26,32 +31,94 @@ const Projects = ({ projects = [], initialSearch }) => { searchInputRef.current.focus(); } }, []); - const filteredProjects = projects.filter(key => { - const sortByName = key.name.toLowerCase().includes(searchInput.trim().toLowerCase()); - let sortByUrl = ''; - if (key.environments[0] !== void 0) { - if (key.environments[0].route !== null) { - sortByUrl = key.environments[0].route.toLowerCase().includes(searchInput.trim().toLowerCase()); - } - } - return ['name', 'environments', '__typename'].includes(key) ? false : (true && sortByName) || sortByUrl; - }); + + const timerLengthPercentage = useMemo( + () => Math.min(1000, Math.max(40, Math.floor(projects.length * 0.0725))), + [projects.length] + ); + + const debouncedSearch = useCallback( + debounce(searchVal => { + setSearchInput(searchVal); + }, timerLengthPercentage), + [] + ); + + const handleSearch = searchVal => { + setIsFiltering(true); + debouncedSearch(searchVal); + }; + + useEffect(() => { + const filterProjects = async () => { + return new Promise(resolve => { + const filtered = projects.filter(key => { + const sortByName = key.name.toLowerCase().includes(searchInput.trim().toLowerCase()); + let sortByUrl = ''; + if (key.environments[0] !== void 0) { + if (key.environments[0].route !== null) { + sortByUrl = key.environments[0].route.toLowerCase().includes(searchInput.trim().toLowerCase()); + } + } + return ['name', 'environments', '__typename'].includes(key) ? false : (true && sortByName) || sortByUrl; + }); + + resolve(filtered); + }); + }; + + filterProjects() + .then(filtered => setFilteredProjects(filtered)) + .finally(() => setIsFiltering(false)); + }, [searchInput, projects]); + + const MemoizedHighlighter = React.memo(Highlighter); + + const mappedProjects = useMemo(() => { + return filteredProjects.map(project => ( + + + +

+ +

+ + {project.environments.map((environment, index) => ( + + ))} + +
+ +
+
+ )); + }, [filteredProjects]); return ( - ); }; diff --git a/src/lib/util.js b/src/lib/util.js index ba246da5..c42fb188 100644 --- a/src/lib/util.js +++ b/src/lib/util.js @@ -9,3 +9,14 @@ export const queryStringToObject = R.pipe( ); export const makeSafe = string => string.toLocaleLowerCase().replace(/[^0-9a-z-]/g, '-'); + +export const debounce = (fn, delay) => { + let timeoutId; + + return function (val) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + fn.call(null, val); + }, delay); + }; +};