From 3c34d8aa740b32d9b4c4280ed6c92a4e6245f049 Mon Sep 17 00:00:00 2001 From: Sev Furneaux Date: Tue, 11 Feb 2025 09:47:50 +0000 Subject: [PATCH] react/kiez-search-profile: add search profile alerts in project overview --- changelog/_8895.md | 3 ++ .../templates/meinberlin_plans/plan_list.html | 1 + meinberlin/apps/plans/views.py | 3 +- .../scss/components_user_facing/_alert.scss | 5 +++ .../use-create-search-profile.jest.js | 5 ++- .../kiezradar/use-create-search-profile.js | 1 + meinberlin/react/plans/react_plans_map.jsx | 4 +- .../react/projects/ProjectsControlBar.jsx | 45 ++++++++++++++++++- .../react/projects/ProjectsListMapBox.jsx | 28 +++++++++++- 9 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 changelog/_8895.md diff --git a/changelog/_8895.md b/changelog/_8895.md new file mode 100644 index 0000000000..fa99d06528 --- /dev/null +++ b/changelog/_8895.md @@ -0,0 +1,3 @@ +### Added + +- Added search profile alerts to plan list page. diff --git a/meinberlin/apps/plans/templates/meinberlin_plans/plan_list.html b/meinberlin/apps/plans/templates/meinberlin_plans/plan_list.html index 6f4d0c08a7..81a68db263 100644 --- a/meinberlin/apps/plans/templates/meinberlin_plans/plan_list.html +++ b/meinberlin/apps/plans/templates/meinberlin_plans/plan_list.html @@ -33,6 +33,7 @@ data-baseurl="{{ baseurl }}" data-bounds="{{ bounds }}" data-search-profile="{{ search_profile|default:"" }}" + data-search-profiles-api-url="{{ search_profiles_api_url }}" data-search-profiles-url="{{ search_profiles_url }}" data-search-profiles-count="{{ search_profiles_count }}" data-is-authenticated="{{ is_authenticated }}" diff --git a/meinberlin/apps/plans/views.py b/meinberlin/apps/plans/views.py index 4ab4a04455..a1051c0476 100644 --- a/meinberlin/apps/plans/views.py +++ b/meinberlin/apps/plans/views.py @@ -194,7 +194,8 @@ def get_context_data(self, **kwargs): context["district"] = self.request.GET.get("district", -1) context["topic"] = self.request.GET.get("topic", -1) context["participation_choices"] = self.get_participation_choices() - context["search_profiles_url"] = reverse("searchprofiles-list") + context["search_profiles_api_url"] = reverse("searchprofiles-list") + context["search_profiles_url"] = reverse("search_profiles") context["search_profiles_count"] = self.get_search_profiles_count() context["is_authenticated"] = json.dumps(self.request.user.is_authenticated) context["project_status"] = self.get_project_status() diff --git a/meinberlin/assets/scss/components_user_facing/_alert.scss b/meinberlin/assets/scss/components_user_facing/_alert.scss index e53ca3530a..598cc0d522 100644 --- a/meinberlin/assets/scss/components_user_facing/_alert.scss +++ b/meinberlin/assets/scss/components_user_facing/_alert.scss @@ -4,6 +4,11 @@ background-color: $message-light-blue; } +.alert a { + color: inherit; + text-decoration: underline; +} + .alert__content { padding: 1.125rem 2rem 1.125rem 1.125rem; margin: 0 auto; diff --git a/meinberlin/react/kiezradar/use-create-search-profile.jest.js b/meinberlin/react/kiezradar/use-create-search-profile.jest.js index 0bb3de6776..05f328aae6 100644 --- a/meinberlin/react/kiezradar/use-create-search-profile.jest.js +++ b/meinberlin/react/kiezradar/use-create-search-profile.jest.js @@ -156,6 +156,8 @@ describe('useCreateSearchProfile', () => { search: '' } + const mockOnSearchProfileCreate = jest.fn() + const { result } = renderHook(() => useCreateSearchProfile({ searchProfilesApiUrl, @@ -166,7 +168,7 @@ describe('useCreateSearchProfile', () => { participationChoices, projectStatus, searchProfilesCount: 10, - onSearchProfileCreate: () => {} + onSearchProfileCreate: mockOnSearchProfileCreate }) ) @@ -175,5 +177,6 @@ describe('useCreateSearchProfile', () => { }) expect(result.current.limitExceeded).toBe(true) + expect(mockOnSearchProfileCreate).toHaveBeenCalledWith(null, true) }) }) diff --git a/meinberlin/react/kiezradar/use-create-search-profile.js b/meinberlin/react/kiezradar/use-create-search-profile.js index 8340dcd021..ebb15a8846 100644 --- a/meinberlin/react/kiezradar/use-create-search-profile.js +++ b/meinberlin/react/kiezradar/use-create-search-profile.js @@ -30,6 +30,7 @@ export function useCreateSearchProfile ({ const createSearchProfile = async () => { if (searchProfilesCount === 10) { setLimitExceeded(true) + onSearchProfileCreate(null, true) return } diff --git a/meinberlin/react/plans/react_plans_map.jsx b/meinberlin/react/plans/react_plans_map.jsx index 06757d26d1..068be5d646 100644 --- a/meinberlin/react/plans/react_plans_map.jsx +++ b/meinberlin/react/plans/react_plans_map.jsx @@ -22,7 +22,8 @@ function init () { const omtToken = el.getAttribute('data-omt-token') const useVectorMap = el.getAttribute('data-use_vector_map') const participationChoices = JSON.parse(el.getAttribute('data-participation-choices')) - const searchProfilesApiUrl = el.getAttribute('data-search-profiles-url') + const searchProfilesApiUrl = el.getAttribute('data-search-profiles-api-url') + const searchProfilesUrl = el.getAttribute('data-search-profiles-url') const searchProfilesCount = JSON.parse(el.getAttribute('data-search-profiles-count')) const isAuthenticated = JSON.parse(el.getAttribute('data-is-authenticated')) const projectStatus = JSON.parse(el.getAttribute('data-project-status')) @@ -48,6 +49,7 @@ function init () { participationChoices={participationChoices} searchProfile={searchProfile} searchProfilesApiUrl={searchProfilesApiUrl} + searchProfilesUrl={searchProfilesUrl} searchProfilesCount={searchProfilesCount} isAuthenticated={isAuthenticated} projectStatus={projectStatus} diff --git a/meinberlin/react/projects/ProjectsControlBar.jsx b/meinberlin/react/projects/ProjectsControlBar.jsx index 495fcc122d..01fd395bb0 100644 --- a/meinberlin/react/projects/ProjectsControlBar.jsx +++ b/meinberlin/react/projects/ProjectsControlBar.jsx @@ -24,7 +24,21 @@ const translated = { plans: django.gettext('Plans'), nav: django.gettext('Search, filter and sort the ideas list'), searchFor: django.gettext('Search for Proposals'), - button: django.gettext('Show projects') + button: django.gettext('Show projects'), + searchProfileCreatedTitle: django.gettext('Search profile created successfully'), + searchProfileCreatedText: (url) => + django.interpolate( + django.gettext('You will be informed about new projects that meet the selected filters. You can manage your search profiles in the User Settings under Search Profiles.'), + { url }, + true + ), + searchProfileLimitExceededTitle: django.gettext('Search profile cannot be saved'), + searchProfileLimitExceededText: (url) => + django.interpolate( + django.gettext('You have saved the maximum number of 10 search profiles. To save a new one, delete an existing profile in the User Settings under Search Profiles.'), + { url }, + true + ) } const statusNames = { @@ -75,9 +89,12 @@ export const ProjectsControlBar = ({ appliedFilters, onFiltered, onResetClick, + onAlert, + onError, hasContainer, searchProfile: initialSearchProfile, searchProfilesApiUrl, + searchProfilesUrl, searchProfilesCount: initialSearchProfilesCount, isAuthenticated, projectStatus @@ -98,9 +115,33 @@ export const ProjectsControlBar = ({ window.history.replaceState({}, '', window.location.pathname) } - const createSearchProfile = (searchProfile) => { + const createSearchProfile = (searchProfile, limitExceeded) => { + if (limitExceeded) { + onError({ + title: translated.searchProfileLimitExceededTitle, + message: ( +
+ ) + }) + return + } + setSearchProfile(searchProfile) setSearchProfilesCount(searchProfilesCount + 1) + onAlert({ + title: translated.searchProfileCreatedTitle, + message: ( +
+ ) + }) window.history.replaceState({}, '', window.location.pathname + '?search-profile=' + searchProfile.id ) } diff --git a/meinberlin/react/projects/ProjectsListMapBox.jsx b/meinberlin/react/projects/ProjectsListMapBox.jsx index 33285c4bc6..db12c964d0 100644 --- a/meinberlin/react/projects/ProjectsListMapBox.jsx +++ b/meinberlin/react/projects/ProjectsListMapBox.jsx @@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import ProjectsList from '../projects/ProjectsList' import { ToggleSwitch } from '../contrib/ToggleSwitch' import { IconSwitch } from '../contrib/IconSwitch' -import { classNames } from 'adhocracy4' +import { alert as Alert, classNames } from 'adhocracy4' import sortProjects from './sort-projects' import ProjectsMap from './ProjectsMap' import Spinner from '../contrib/Spinner' @@ -39,6 +39,7 @@ const ProjectsListMapBox = ({ extprojectApiUrl, privateprojectApiUrl, searchProfilesApiUrl, + searchProfilesUrl, attribution, bounds, baseUrl, @@ -60,6 +61,8 @@ const ProjectsListMapBox = ({ const [items, setItems] = useState([]) const fetchCache = useRef({}) const [appliedFilters, setAppliedFilters] = useState(getDefaultState(searchProfile)) + const [alert, setAlert] = useState(null) + const [error, setError] = useState(null) const fetchItems = useCallback(async () => { setLoading(true) @@ -113,6 +116,26 @@ const ProjectsListMapBox = ({ return (
+ {(error || alert) && ( +
+ {error && ( + setError(null)} + /> + )} + {alert && ( + setAlert(null)} + /> + )} +
+ )}