Skip to content

Commit

Permalink
Merge pull request #6325 from spnayan/enhancement/projects-more-filter
Browse files Browse the repository at this point in the history
Optimize API calls and Redesign `More Filter` section of `Explore projects`
  • Loading branch information
ramyaragupathy committed May 8, 2024
2 parents 8ce70d1 + 1a0a32c commit 90805b8
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 11 deletions.
3 changes: 2 additions & 1 deletion frontend/src/api/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { remapParamsToAPI } from '../utils/remapParamsToAPI';
import api from './apiClient';
import { UNDERPASS_URL } from '../config';

export const useProjectsQuery = (fullProjectsQuery, action) => {
export const useProjectsQuery = (fullProjectsQuery, action, queryOptions) => {
const token = useSelector((state) => state.auth.token);
const locale = useSelector((state) => state.preferences['locale']);

Expand Down Expand Up @@ -35,6 +35,7 @@ export const useProjectsQuery = (fullProjectsQuery, action) => {
queryKey: ['projects', fullProjectsQuery, action],
queryFn: ({ signal, queryKey }) => fetchProjects(signal, queryKey),
keepPreviousData: true,
...queryOptions,
});
};

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const Header = () => {
) : null;

return (
<header className="w-100 bb b--grey-light">
<header id="top-header" className="w-100 bb b--grey-light">
<UpdateDialog />
{checkUserEmail()}
{showOrgBar && (
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/projects/moreFiltersForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const MoreFiltersForm = (props) => {
/>
</fieldset>
)}
<div className="tr w-100 mt3">
<div className="tr w-100 mt3 pb3 ph2">
<Link to="/explore">
<Button className="bg-white blue-dark mr1 f6 pv2">
<FormattedMessage {...messages.clear} />
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/projects/projectNav.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const ProjectNav = (props) => {
// onSelectedItemChange={(changes) => console.log(changes)}
return (
/* mb1 mb2-ns (removed for map, but now small gap for more-filters) */
<header className="bt bb b--tan w-100 ">
<header id="explore-nav" className="bt bb b--tan w-100 ">
<div className="mt2 mb1 ph3 dib lh-copy w-100 cf">
<div className="w-80-l w-90-m w-100 fl dib">
<div className="dib">
Expand All @@ -125,6 +125,7 @@ export const ProjectNav = (props) => {
<ProjectsActionFilter setQuery={setQuery} fullProjectsQuery={fullProjectsQuery} />
<Link
to={filterRouteToggled}
id="more-filter-id"
className={`dn mr3 dib-l lh-title f6 ${linkCombo} ${moreFiltersCurrentActiveStyle} blue-dark`}
>
<FormattedMessage {...messages.moreFilters} />
Expand Down
121 changes: 114 additions & 7 deletions frontend/src/views/project.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Suspense, lazy, useEffect } from 'react';
import React, { Suspense, lazy, useEffect, useState, useLayoutEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import ReactPlaceholder from 'react-placeholder';
import { Outlet, useLocation, useNavigate, useParams } from 'react-router-dom';
Expand All @@ -16,6 +16,17 @@ import { useSetTitleTag } from '../hooks/UseMetaTags';
import { NotFound } from './notFound';
import { ProjectDetailPlaceholder } from '../components/projectDetail/projectDetailPlaceholder';
import { useProjectsQuery, useProjectQuery } from '../api/projects';
import { useWindowSize } from '../hooks/UseWindowSize';
import { useOnClickOutside } from '../hooks/UseOnClickOutside';

const smallScreenSize = 960;

// returns true if the element or one of its parents has the classname
function hasSomeParentClass(element, classname) {
if (typeof element.className === 'string' && element.className.split(' ').indexOf(classname) >= 0)
return true;
return element.parentNode && hasSomeParentClass(element.parentNode, classname);
}

const ProjectCreate = lazy(() => import('../components/projectCreate/index'));

Expand All @@ -29,15 +40,24 @@ export const CreateProject = () => {

export const ProjectsPage = () => {
useSetTitleTag('Explore projects');
const { pathname } = useLocation();
const action = useSelector((state) => state.preferences['action']);
const [fullProjectsQuery, setProjectQuery] = useExploreProjectsQueryParams();
const isMapShown = useSelector((state) => state.preferences['mapShown']);
const searchResultWidth = isMapShown ? 'two-column' : 'one-column';

const { data: projects, status, refetch } = useProjectsQuery(fullProjectsQuery, action);
const {
data: projects,
status,
refetch,
} = useProjectsQuery(fullProjectsQuery, action, {
// prevent api call until the filters are applied
enabled: !pathname.includes('/explore/filters/'),
cacheTime: 0,
});

return (
<div className="pull-center">
<div className="pull-center" id="projects-container">
<ProjectNav>
<Outlet />
</ProjectNav>
Expand Down Expand Up @@ -139,22 +159,109 @@ export const ProjectsPageIndex = (props) => {
};

export const MoreFilters = () => {
const [position, setPosition] = useState({ top: 0, left: 0, height: 0, width: 0 });
const [projectContainerHeight, setProjectContainerHeight] = useState({ height: 0 });
const [scrollHeight, setScrollHeight] = useState(0);
const navigate = useNavigate();
const [fullProjectsQuery] = useExploreProjectsQueryParams();
const [componentHeight, setComponentHeight] = useState(`${window.innerHeight}px`);
const filterElement = document?.getElementById('more-filter-id');
const projectContainerElement = document?.getElementById('projects-container');
const [width] = useWindowSize();

// calculate position of more filter button for layout
useLayoutEffect(() => {
if (!filterElement || !projectContainerElement) return;

const { top, left, height, width } = filterElement.getBoundingClientRect();
const { height: containerHeight } = projectContainerElement.getBoundingClientRect();
const navbarHeight = document.getElementById('explore-nav').offsetHeight;
// calculate difference between explore project page and navbar to set popover overlay height
setProjectContainerHeight(containerHeight - navbarHeight);
setPosition({ top, left, height, width });
setScrollHeight(window.scrollY);
}, [filterElement, width, projectContainerElement]);

useEffect(() => {
const contentHeight =
document.getElementById('explore-nav').offsetHeight +
document.getElementById('top-header').offsetHeight;

const handleResize = () => {
setComponentHeight(window.innerHeight - contentHeight);
};

handleResize();

window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
};
}, []);

const currentUrl = `/explore${
stringify(fullProjectsQuery) ? ['?', stringify(fullProjectsQuery)].join('') : ''
}`;
const moreFilterRef = useRef(null);

useOnClickOutside(moreFilterRef, (e) => {
const clickedElement = e.target;
const isClearSelectButton = hasSomeParentClass(clickedElement, 'react-select__clear-indicator');
if (
e.target.id === 'more-filter-id' ||
isClearSelectButton //prevent popup close on clicking clear button of select component
)
return;
navigate(currentUrl);
});

const isSmallScreen = width < smallScreenSize;

return (
<>
<div className="absolute left-0 z-4 mt1 w-40-l w-100 h-100 bg-white h4 ph1 ph5-l">
<MoreFiltersForm currentUrl={currentUrl} />
<div
ref={moreFilterRef}
className={`absolute z-4 bg-white ${
// compare screen size for two different design in small screen and large screen of filter section
isSmallScreen ? ' left-0 mt1 w-40-l w-100 h4 ph1 ph5-l' : 'pa2 ba b--light-gray'
}`}
style={
isSmallScreen
? { height: `${componentHeight}px` }
: {
// 250 is half the width of filter component to place filter exactly center of more-filter button
left: position.left - 250 + position.width / 2,
top: position.top + position.height + 10 + scrollHeight,
width: '31.25em',
boxShadow: '2px 1px 23px -1px rgba(143,130,130,0.75)',
}
}
>
<div
className={`${
isSmallScreen ? 'scrollable-container h-100 overflow-x-hidden overflow-y-auto' : ''
}`}
>
<MoreFiltersForm currentUrl={currentUrl} />
</div>
</div>
{!isSmallScreen && (
<div
style={{
left: `${position.left + position.width / 2}px`,
top: position.top + position.height + 2 + scrollHeight,
}}
className={`absolute w1 h1 bg-white bl bt b--grey-light rotate-45 z-5`}
/>
)}

<div
onClick={() => navigate(currentUrl)}
role="button"
className="absolute right-0 z-4 br w-60-l w-0 h-100 bg-blue-dark o-70 h6"
className="absolute right-0 z-2 br w-100-l w-0 bg-blue-dark o-70"
style={{
height: `${projectContainerHeight}px`,
}}
/>
</>
);
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/views/tests/project.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ describe('UserProjectsPage Component', () => {
});

test('More Filters should close the more filters container when clicked outside the container', async () => {
jest.spyOn(document, 'getElementById').mockReturnValue({
offsetHeight: 100,
getBoundingClientRect: () => ({
top: 0,
left: 0,
height: 100,
width: 100,
}),
});

const { user, router } = createComponentWithMemoryRouter(
<QueryParamProvider adapter={ReactRouter6Adapter}>
<ReduxIntlProviders>
Expand Down

0 comments on commit 90805b8

Please sign in to comment.