Skip to content

Commit

Permalink
Merge pull request #77 from naxa-developers/enhancement/projects-more…
Browse files Browse the repository at this point in the history
…-filter

enhancement/projects more filter
  • Loading branch information
royallsilwallz authored Apr 17, 2024
2 parents 352e884 + 982528b commit a9fe9fe
Show file tree
Hide file tree
Showing 14 changed files with 175 additions and 28 deletions.
4 changes: 4 additions & 0 deletions backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ def add_api_endpoints(app):
UsersStatisticsAPI,
UsersStatisticsInterestsAPI,
UsersStatisticsAllAPI,
OhsomeProxyAPI,
)

# System API endpoint
Expand Down Expand Up @@ -900,6 +901,9 @@ def add_api_endpoints(app):
UsersStatisticsAllAPI,
format_url("users/statistics/"),
)
api.add_resource(
OhsomeProxyAPI, format_url("users/statistics/ohsome/"), methods=["GET"]
)
# User RecommendedProjects endpoint
api.add_resource(
UsersRecommendedProjectsAPI,
Expand Down
42 changes: 42 additions & 0 deletions backend/api/users/statistics.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from json import JSONEncoder
from datetime import date, timedelta
from flask_restful import Resource, request
import requests

from backend.services.users.user_service import UserService
from backend.services.stats_service import StatsService
from backend.services.interests_service import InterestService
from backend.services.users.authentication_service import token_auth
from backend.api.utils import validate_date_input
from backend.config import EnvironmentConfig


class UsersStatisticsAPI(Resource, JSONEncoder):
Expand Down Expand Up @@ -138,3 +140,43 @@ def get(self):
return stats.to_primitive(), 200
except (KeyError, ValueError) as e:
return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 400


class OhsomeProxyAPI(Resource):
@token_auth.login_required
def get(self):
"""
Get HomePage Stats
---
tags:
- system
produces:
- application/json
parameters:
- in: header
name: Authorization
description: Base64 encoded session token
required: true
type: string
default: Token sessionTokenHere==
- in: query
name: url
type: string
description: get user stats for osm contributions
responses:
200:
description: User stats
500:
description: Internal Server Error
"""
url = request.args.get("url")
if not url:
return {"Error": "URL is None", "SubCode": "URL not provided"}, 400
try:
headers = {"Authorization": f"Basic {EnvironmentConfig.OHSOME_STATS_TOKEN}"}

# Make the GET request with headers
response = requests.get(url, headers=headers)
return response.json(), 200
except Exception as e:
return {"Error": str(e), "SubCode": "Error fetching data"}, 400
3 changes: 3 additions & 0 deletions backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ class EnvironmentConfig:
# Sentry backend DSN
SENTRY_BACKEND_DSN = os.getenv("TM_SENTRY_BACKEND_DSN", None)

# Ohsome Stats Token
OHSOME_STATS_TOKEN = os.getenv("OHSOME_STATS_TOKEN", None)


class TestEnvironmentConfig(EnvironmentConfig):
POSTGRES_TEST_DB = os.getenv("POSTGRES_TEST_DB", None)
Expand Down
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
15 changes: 10 additions & 5 deletions frontend/src/api/stats.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import { fetchExternalJSONAPI } from '../network/genericJSONRequest';

import { fetchExternalJSONAPI } from '../network/genericJSONRequest';
import api from './apiClient';
import { OHSOME_STATS_BASE_URL } from '../config';

const ohsomeProxyAPI = (url) => {
const token = localStorage.getItem('token');
return api(token).get(`users/statistics/ohsome/?url=${url}`);
};

export const useSystemStatisticsQuery = () => {
const fetchSystemStats = ({ signal }) => {
return api().get(`system/statistics/`, {
Expand Down Expand Up @@ -65,17 +70,17 @@ export const useOsmHashtagStatsQuery = (defaultComment) => {

export const useUserOsmStatsQuery = (id) => {
const fetchUserOsmStats = () => {
return fetchExternalJSONAPI(
return ohsomeProxyAPI(
`${OHSOME_STATS_BASE_URL}/topic/poi,highway,building,waterway/user?userId=${id}`,
true,
);
};

return useQuery({
queryKey: ['user-osm-stats'],
queryFn: fetchUserOsmStats,
useErrorBoundary: true,
select: (data) => data.result,
// userDetail.test.js fails on CI when useErrorBoundary=true
useErrorBoundary: process.env.NODE_ENV !== 'test',
select: (data) => data.data.result,
enabled: !!id,
});
};
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/footer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function Footer() {
'projects/:id/tasks',
'projects/:id/map',
'projects/:id/validate',
'projects/:id/live',
'manage/organisations/new/',
'manage/teams/new',
'manage/campaigns/new',
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 @@ -116,7 +116,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
7 changes: 4 additions & 3 deletions frontend/src/components/projects/projectNav.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@ export const ProjectNav = (props) => {
setQuery(
{
...fullProjectsQuery,
omitMapResults:!isMapShown
omitMapResults: !isMapShown,
},
'pushIn',
);
// eslint-disable-next-line react-hooks/exhaustive-deps
},[isMapShown])
}, [isMapShown]);
const linkCombo = 'link ph3 f6 pv2 ba b--tan br1 ph3 fw5';

const moreFiltersAnyActive =
Expand All @@ -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
4 changes: 1 addition & 3 deletions frontend/src/components/teamsAndOrgs/featureStats.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ export const FeatureStats = () => {
const [stats, setStats] = useState({ edits: 0, buildings: 0, roads: 0, pois: 0, waterways: 0 });
const getStats = async () => {
try {
const response = await axios.get(
`${OHSOME_STATS_BASE_URL}/stats/hotosm-project-%2A`,
);
const response = await axios.get(`${OHSOME_STATS_BASE_URL}/stats/hotosm-project-%2A`);
const { edits, buildings, roads } = response.data.result;
setStats({
edits,
Expand Down
1 change: 0 additions & 1 deletion frontend/src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export const API_URL = process.env.REACT_APP_API_URL
: 'http://127.0.0.1:5000/api/' + API_VERSION + '/';
export const OHSOME_STATS_BASE_URL =
process.env.REACT_APP_OHSOME_STATS_BASE_URL || 'https://stats.now.ohsome.org/api';
export const OHSOME_STATS_TOKEN = process.env.REACT_APP_OHSOME_STATS_TOKEN || '';

// APPLICATION SETTINGS
export const DEFAULT_LOCALE = process.env.REACT_APP_DEFAULT_LOCALE || 'en';
Expand Down
9 changes: 2 additions & 7 deletions frontend/src/network/genericJSONRequest.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { handleErrors } from '../utils/promise';
import { API_URL, OHSOME_STATS_TOKEN } from '../config';
import { API_URL } from '../config';

export function fetchExternalJSONAPI(url, isSetToken = false): Promise<*> {
export function fetchExternalJSONAPI(url): Promise<*> {
const headers = {
'Content-Type': 'application/json',
};

// Passing token only for ohsomeNow stats
if (isSetToken) {
headers['Authorization'] = `Basic ${OHSOME_STATS_TOKEN}`;
}

return fetch(url, {
method: 'GET',
headers: headers,
Expand Down
100 changes: 94 additions & 6 deletions frontend/src/views/project.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Suspense, useEffect } from 'react';
import React, { Suspense, 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,10 @@ 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;

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

Expand All @@ -29,12 +33,20 @@ 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/'),
});

return (
<div className="pull-center">
Expand Down Expand Up @@ -139,22 +151,98 @@ export const ProjectsPageIndex = (props) => {
};

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

useEffect(() => {
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = 'auto';
};
}, []);

// calculate position of more filter button for layout
useLayoutEffect(() => {
if (!filterElement) return;
const { top, left, height, width } = filterElement.getBoundingClientRect();
setPosition({ top, left, height, width });
}, [filterElement, width]);

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) => {
if (e.target.id === 'more-filter-id') 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,
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,
}}
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 h-100 bg-blue-dark o-70 h6"
/>
</>
);
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 @@ -113,6 +113,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 a9fe9fe

Please sign in to comment.