Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UHF-11235: Events near you #1177

Merged
merged 26 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
15e1420
UHF-11235: Near you events base
jeremysteerio Feb 3, 2025
4a0ae7c
UHF-11235: Make locations filter searchable
jeremysteerio Feb 4, 2025
16a88b8
UHF-11235: Memoize selects
jeremysteerio Feb 5, 2025
91dde95
UHF-11235: Selections work
jeremysteerio Feb 5, 2025
1d79b5e
UHF-11235: Hide pills when using selections with search
jeremysteerio Feb 6, 2025
a689bff
UHF-11235: Workaround rendering issues with select components
jeremysteerio Feb 6, 2025
f4c84b2
UHF-11235: Use searchinput for address suggesting
jeremysteerio Feb 6, 2025
0dacf34
UHF-11235: Add reading initial address from query string
jeremysteerio Feb 6, 2025
88ea1bc
UHF-11235: Bump event list version
jeremysteerio Feb 6, 2025
4526824
UHF-11235: Add override for all events link
jeremysteerio Feb 6, 2025
26d5e81
UHF-11235: Helsinki near you events page tweaks
jeremysteerio Feb 7, 2025
a5d85fd
UHF-11235: Helsinki near you events form styles
jeremysteerio Feb 7, 2025
2950149
UHF-11235: Remove unused hook
jeremysteerio Feb 7, 2025
84e72ce
UHF-11235: Add better message for missing address
jeremysteerio Feb 7, 2025
6facace
UHF-11235: Fix translation context
jeremysteerio Feb 7, 2025
c3c629f
Add consistent bottom margin to hdbt filters
jeremysteerio Feb 7, 2025
66d173a
UHF-11235: Adjusted spacing a to be a bit more consistent
teroelonen Feb 7, 2025
2d7c840
UHF-11235: Add consistent bottom margin to hdbt filters
jeremysteerio Feb 7, 2025
1214c13
UHF-11235: Adjusted spacing a to be a bit more consistent
teroelonen Feb 7, 2025
679821f
UHF-11235: Wrap checkboxes in fieldset, remove legacy styles
jeremysteerio Feb 10, 2025
99b0473
Merge remote-tracking branch 'origin/main' into UHF-11235
jeremysteerio Feb 10, 2025
1d0d486
UHF-11235: Move legend inside fieldset
jeremysteerio Feb 10, 2025
3a65fbc
UHF-11235: Merge branch 'UHF-11235' of https://github.com/City-of-Hel…
teroelonen Feb 10, 2025
269162f
UHF-11235: Hide checkbox container when none are present
jeremysteerio Feb 10, 2025
ac58029
UHF-11235: Merge branch 'UHF-11235' of https://github.com/City-of-Hel…
teroelonen Feb 10, 2025
09259b7
UHF-11235: Reverse conditional for hiding form heading
jeremysteerio Feb 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dist/css/styles.min.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/js/linkedevents.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion hdbt.libraries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ nav-global:
- hdbt/nav-toggle

event-list:
version: 1.3.1
version: 1.4.0
js:
dist/js/linkedevents.min.js: {
preprocess: false,
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 70 additions & 0 deletions src/js/react/apps/linkedevents/components/AddressSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { SearchInput } from 'hds-react';
import { useAtom } from 'jotai';
import { addressAtom } from '../store';
import getNameTranslation from '@/react/common/helpers/ServiceMap';
import { ServiceMapAddress, ServiceMapResponse } from '@/types/ServiceMap';
import ServiceMap from '@/react/common/enum/ServiceMap';

const AddressSearch = () => {
const [address, updateAddress] = useAtom(addressAtom);

const onChange = (value: string) => {
updateAddress(value);
};

const getSuggestions = async(searchTerm: string|undefined) => {
if (!searchTerm || searchTerm === '') {
return [];
}

const fetchSuggestions = (param: URLSearchParams) => {
const url = new URL(ServiceMap.EVENTS_URL);
url.search = param.toString();

return fetch(url.toString()).then(response => response.json());
};

const params = ['fi', 'sv'].map(lang => new URLSearchParams({
format: 'json',
language: lang,
municipality: 'helsinki',
page_size: '10',
q: searchTerm,
type: 'address',
}));

const [fiParams, svParams] = params;
const results = Promise.all([
fetchSuggestions(fiParams),
fetchSuggestions(svParams)
]);

const parseResults = (result: ServiceMapResponse<ServiceMapAddress>, langKey: string) => result.results.map(addressResult => ({
value: getNameTranslation(addressResult.name, langKey) as string
}));

const [fiResults, svResults] = await results;

return [...parseResults(fiResults, 'fi'), ...parseResults(svResults, 'sv')].slice(0, 10);
};

return (
<div className='hdbt-search__filter'>
<SearchInput
className='hdbt-search__input hdbt-search__input--address'
getSuggestions={getSuggestions}
hideSearchButton
id='location'
label={Drupal.t('Address', {}, {context: 'React search: location label'})}
onChange={onChange}
onSubmit={onChange}
placeholder={Drupal.t('For example, Kotikatu 1', {}, {context: 'Helsinki near you events search'})}
suggestionLabelField='value'
visibleSuggestions={5}
value={address || ''}
/>
</div>
);
};

export default AddressSearch;
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function CheckboxFilter({ id, label, atom, valueKey }: CheckboxFilterProps) {
return (
<Checkbox
checked={checked}
className="hdbt-search__filter hdbt-search__checkbox"
className="hdbt-search--react__checkbox"
id={id}
label={label}
onChange={(event) => toggleValue(event)}
Expand Down
128 changes: 128 additions & 0 deletions src/js/react/apps/linkedevents/components/FullLocationFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Select, SelectData, useSelectStorage } from 'hds-react';
import { useSetAtom } from 'jotai';
import { useAtomCallback } from 'jotai/utils';
import { memo, useCallback, useEffect } from 'react';
import type OptionType from '../types/OptionType';

import { locationSelectionAtom, updateParamsAtom } from '../store';
import SearchComponents from '../enum/SearchComponents';
import ApiKeys from '../enum/ApiKeys';
import { ServiceMapPlace } from '@/types/ServiceMap';
import useTimeoutFetch from '@/react/common/hooks/useTimeoutFetch';
import { getNameTranslation } from '@/react/common/helpers/ServiceMap';
import LinkedEvents from '@/react/common/enum/LinkedEvents';
import { clearAllSelectionsFromStorage, updateSelectionsInStorage } from '@/react/common/helpers/HDS';

const FullLocationFilter = memo(() => {
const setLocationFilter = useSetAtom(locationSelectionAtom);
const updateParams = useSetAtom(updateParamsAtom);

const getLocationParamValue = useAtomCallback(
useCallback((get) => get(locationSelectionAtom), [])
);

const getLocations = async (
searchTerm: string,
selectedOptions: OptionType[],
data: SelectData,
) => {
const url = new URL(LinkedEvents.PLACES_URL);
const locationParams = new URLSearchParams({
has_upcoming_events: 'true',
municipality: 'helsinki',
text: searchTerm,
});
url.search = locationParams.toString();
const result = {
options: [],
};

const response = await useTimeoutFetch(url.toString());

if (response.status !== 200) {
return result;
}

const body = await response.json();

if (body.data && body.data.length) {
const places = body.data.map((place: ServiceMapPlace) => ({
value: place.id,
label: getNameTranslation(place.name, drupalSettings.path.currentLanguage)
}));

result.options = places;
return result;
}

return result;
};

const onChange = (value: OptionType[], clickedOption: OptionType, data: SelectData) => {
setLocationFilter(value.map(option => ({
label: option.label,
value: option.value,
})));
updateParams({ [ApiKeys.LOCATION]: value.map((location: any) => location.value).join(',') });

storage.updateAllOptions((option, group, groupindex) => ({
...option,
selected: value.some(selection => selection.value === option.value),
}));

if (clickedOption) {
storage.setOpen(true);
}
};

const selectVenueLabel: string = Drupal.t('Select a venue', {}, {context: 'Events search'});

const storage = useSelectStorage({
id: SearchComponents.LOCATION,
multiSelect: true,
noTags: true,
onChange,
open: false,
onSearch: getLocations
});

const clearAllSelections = () => {
clearAllSelectionsFromStorage(storage);
};

const updateSelections = () => {
updateSelectionsInStorage(storage, getLocationParamValue());
};

useEffect(() => {
window.addEventListener('eventsearch-clear', clearAllSelections);
window.addEventListener(`eventsearch-clear-${ApiKeys.LOCATION}`, updateSelections);

return () => {
window.addEventListener('eventsearch-clear', clearAllSelections);
window.removeEventListener(`eventsearch-clear-${ApiKeys.LOCATION}`, updateSelections);
};
});

return (
<div className='hdbt-search__filter event-form__filter--location'>
<Select
className='hdbt-search__dropdown'
texts={{
clearButtonAriaLabel_one: Drupal.t('Clear @label selection', {'@label': selectVenueLabel}, { context: 'React search clear selection label' }),
clearButtonAriaLabel_multiple: Drupal.t('Clear @label selection', {'@label': selectVenueLabel}, { context: 'React search clear selection label' }),
label: selectVenueLabel,
placeholder: Drupal.t('All', {}, { context: 'React search: all available options' }),
}}
theme={{
'--checkbox-background-selected': 'var(--hdbt-color-black)',
'--focus-outline-color': 'var(--hdbt-color-black)',
'--placeholder-color': 'var(--hdbt-color-black)',
}}
{...storage.getProps()}
/>
</div>
);
});

export default FullLocationFilter;
123 changes: 123 additions & 0 deletions src/js/react/apps/linkedevents/components/FullTopicsFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Select, SelectData, useSelectStorage } from 'hds-react';
import { useSetAtom } from 'jotai';
import { memo, useCallback, useEffect, useState } from 'react';
import { useAtomCallback } from 'jotai/utils';
import type OptionType from '../types/OptionType';

import { topicSelectionAtom, updateParamsAtom} from '../store';
import SearchComponents from '../enum/SearchComponents';
import ApiKeys from '../enum/ApiKeys';
import getNameTranslation from '@/react/common/helpers/ServiceMap';
import { LinkedEventsTopic } from '@/types/LinkedEvents';
import useTimeoutFetch from '@/react/common/hooks/useTimeoutFetch';
import LinkedEvents from '@/react/common/enum/LinkedEvents';
import { clearAllSelectionsFromStorage, updateSelectionsInStorage } from '@/react/common/helpers/HDS';

const FullTopicsFilter = memo(() => {
const setTopicsFilter = useSetAtom(topicSelectionAtom);
const updateParams = useSetAtom(updateParamsAtom);

const getTopicsParamValue = useAtomCallback(
useCallback((get) => get(topicSelectionAtom), [])
);

const getTopics = async (
searchTerm: string,
selectedOptions: OptionType[],
data: SelectData,
) => {
const url = new URL(LinkedEvents.KEYWORDS_URL);
const locationParams = new URLSearchParams({
has_upcoming_events: 'true',
text: searchTerm,
});
url.search = locationParams.toString();
const result = {
options: [],
};

const response = await useTimeoutFetch(url.toString());

if (response.status !== 200) {
return result;
}

const body = await response.json();

if (body.data && body.data.length) {
const places = body.data.map((place: LinkedEventsTopic) => ({
value: place.id,
label: getNameTranslation(place.name, drupalSettings.path.currentLanguage)
}));

result.options = places;
return result;
}

return result;
};

const onChange = (selectedOptions: OptionType[], clickedOption?: OptionType) => {
setTopicsFilter(selectedOptions);
updateParams({ [ApiKeys.KEYWORDS]: selectedOptions.map((topic: any) => topic.value).join(',') });

storage.updateAllOptions((option, group, groupindex) => ({
...option,
selected: selectedOptions.some(selection => selection.value === option.value),
}));

if (clickedOption) {
storage.setOpen(true);
}
};

const selectLabel: string = Drupal.t('Event topic', {}, { context: 'React search: topics filter' });

const storage = useSelectStorage({
id: SearchComponents.TOPICS,
multiSelect: true,
noTags: true,
onChange,
onSearch: getTopics,
});

const clearAllSelections = () => {
clearAllSelectionsFromStorage(storage);
};

const updateSelections = () => {
updateSelectionsInStorage(storage, getTopicsParamValue());
};

useEffect(() => {
window.addEventListener('eventsearch-clear', clearAllSelections);
window.addEventListener(`eventsearch-clear-${ApiKeys.KEYWORDS}`, updateSelections);

return () => {
window.addEventListener('eventsearch-clear', clearAllSelections);
window.removeEventListener(`eventsearch-clear-${ApiKeys.KEYWORDS}`, updateSelections);
};
});

return (
<div className='hdbt-search__filter event-form__filter--topics'>
<Select
className='hdbt-search__dropdown'
texts={{
clearButtonAriaLabel_multiple: Drupal.t('Clear @label selection', {'@label': selectLabel}, { context: 'React search clear selection label' }),
clearButtonAriaLabel_one: Drupal.t('Clear @label selection', {'@label': selectLabel}, { context: 'React search clear selection label' }),
label: selectLabel,
placeholder: Drupal.t('All topics', {}, { context: 'React search: topics filter' }),
}}
theme={{
'--checkbox-background-selected': 'var(--hdbt-color-black)',
'--focus-outline-color': 'var(--hdbt-color-black)',
'--placeholder-color': 'var(--hdbt-color-black)',
}}
{...storage.getProps()}
/>
</div>
);
});

export default FullTopicsFilter;
Loading
Loading