Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/ckeditor-icons.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/css/hdbt-icons.css

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/css/styles.min.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/icons.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
["zoom-text","zoom-out","zoom-in","youth","wifi","wifi-crossed","virus","vaccine","user","upload","upload-cloud","traveler","trash","ticket","star","star-fill","speechbubble","speechbubble-text","sort","sort-descending","sort-ascending","sort-alphabetical-descending","sort-alphabetical-ascending","signout","signin","share","senior","search","refresh","question-circle","question-circle-fill","printer","podcast","plus","plus-circle","plus-circle-fill","photo","phone","person-wheelchair","person-male","person-female","paperclip","occupation","mover","mobile","minus","minus-circle","minus-circle-fill","menu-hamburger","menu-dots","map","location","locate","link","layers","key","info-circle","info-circle-fill","home","home-smoke","heart","heart-fill","group","glyph-euro","glyph-at","globe","family","face-smile","face-sad","face-neutral","eye","eye-crossed","error","error-fill","envelope","entrepreneur","download","download-cloud","document","display","customer-bot-neutral","cross","cross-circle","cross-circle-fill","company","cogwheel","clock","children","check","check-circle","check-circle-fill","camera","calendar","calendar-clock","arrow-up","arrow-right","arrow-left","angle-up","angle-right","angle-left","angle-down","alert-circle","alert-circle-fill","youtube","yle","whatsapp","vimeo","twitter","twitch","tiktok","snapchat","rss","linkedin","instagram","google","facebook","discord","tel","printableform","mailto","link-external","link-external-part2","link-external-part1","info","hero-arrow-mobile","hero-arrow-desktop","abstract-6","abstract-5","abstract-4","abstract-3","abstract-2","abstract-1","email","helsinki","helsinki-sv","helsinki-ru"]
["zoom-text","zoom-out","zoom-in","youth","wifi","wifi-crossed","virus","vaccine","user","upload","upload-cloud","traveler","trash","ticket","star","star-fill","speechbubble","speechbubble-text","sort","sort-descending","sort-ascending","sort-alphabetical-descending","sort-alphabetical-ascending","signout","signin","share","senior","search","refresh","question-circle","question-circle-fill","printer","podcast","plus","plus-circle","plus-circle-fill","photo","phone","person-wheelchair","person-male","person-female","paperclip","occupation","mover","mobile","minus","minus-circle","minus-circle-fill","menu-hamburger","menu-dots","map","location","locate","link","layers","key","info-circle","info-circle-fill","home","home-smoke","heart","heart-fill","group","glyph-euro","glyph-at","globe","family","face-smile","face-sad","face-neutral","eye","eye-crossed","error","error-fill","envelope","entrepreneur","download","download-cloud","document","display","customer-bot-neutral","cross","cross-circle","cross-circle-fill","company","cogwheel","clock","children","check","check-circle","check-circle-fill","camera","calendar","calendar-clock","bell","arrow-up","arrow-right","arrow-left","angle-up","angle-right","angle-left","angle-down","alert-circle","alert-circle-fill","youtube","yle","whatsapp","vimeo","twitter","twitch","tiktok","snapchat","rss","linkedin","instagram","google","facebook","discord","tel","printableform","mailto","link-external","link-external-part2","link-external-part1","info","hero-arrow-mobile","hero-arrow-desktop","abstract-6","abstract-5","abstract-4","abstract-3","abstract-2","abstract-1","email","helsinki","helsinki-sv","helsinki-ru"]
1 change: 0 additions & 1 deletion dist/icons/sprite-68e0d.svg

This file was deleted.

1 change: 1 addition & 0 deletions dist/icons/sprite-d61ec.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion dist/js/district-and-project-search.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/js/health-station-search.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/js/job-search.min.js

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 dist/js/maternity-and-child-health-clinic-search.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/js/news-archive.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/js/school-search.min.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions hdbt.theme
Original file line number Diff line number Diff line change
Expand Up @@ -1636,6 +1636,7 @@ function hdbt_preprocess_paragraph__event_list(&$variables): void {

$settings['events_public_url'] = $paragraph->getEventsPublicUrl();
$settings['events_api_url'] = $paragraph->getApiUrl();
$settings['event_list_type'] = $paragraph->getEventListType();

/** @var \Drupal\helfi_react_search\LinkedEvents $linkedEvents */
$linkedEvents = \Drupal::service(LinkedEvents::class);
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.

File renamed without changes
38 changes: 38 additions & 0 deletions src/js/react/apps/linkedevents/components/EventTypeFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Checkbox } from 'hds-react';
import { useAtom, useSetAtom } from 'jotai';
import { eventTypeAtom, updateParamsAtom } from '../store';
import ApiKeys from '../enum/ApiKeys';
import { typeSelectionsToString } from '../helpers/TypeSelectionsToString';
import { EventTypeOption } from '../types/EventTypeOption';

export const EventTypeFilter = () => {
const [typeSelections, setTypes] = useAtom(eventTypeAtom);
const updateParams = useSetAtom(updateParamsAtom);

const toggleValue = (event: React.ChangeEvent<HTMLInputElement>) => {
const checked = event?.target?.checked;
const value = event.target.id === 'event-type-toggle' ? 'General' : 'Course';
const newTypeSelections: EventTypeOption[] = checked ? [...typeSelections, value] : typeSelections.filter((type) => type !== value);
setTypes(newTypeSelections);
updateParams({ [ApiKeys.EVENT_TYPE]: typeSelectionsToString(newTypeSelections) });
};

return (
<>
<Checkbox
checked={typeSelections.includes('General')}
className='hdbt-search--react__checkbox'
id='event-type-toggle'
label={Drupal.t('Events', {}, { context: 'Event search: events type' })}
onChange={toggleValue}
/>
<Checkbox
checked={typeSelections.includes('Course')}
className='hdbt-search--react__checkbox'
id='hobby-type-toggle'
label={Drupal.t('Hobbies', {}, { context: 'Event search: hobbies type' })}
onChange={toggleValue}
/>
</>
);
};
90 changes: 78 additions & 12 deletions src/js/react/apps/linkedevents/components/ResultCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import parse from 'html-react-parser';

import type { Event, EventImage, EventKeyword } from '../types/Event';
import type { Event, EventImage } from '../types/Event';
import CardItem from '@/react/common/Card';
import type TagType from '@/types/TagType';
import { hobbiesPublicUrl } from '../store';

const INTERNET_EXCEPTION = 'helsinki:internet';
const overDayApart = (start: Date, end: Date) => start.toDateString() !== end.toDateString();
Expand All @@ -20,19 +20,29 @@ const formatStartDate = (start: Date, end: Date) => {
return start.toLocaleDateString('fi-FI');
};

interface KeywordsForLanguage { keywords: EventKeyword[], currentLanguage: string }
const getCardTags = ({ keywords, currentLanguage }: KeywordsForLanguage ) => keywords?.map((item: any) => ({ tag: item.name[currentLanguage], color: 'silver' })).filter((keyword: any) => keyword.tag !== undefined) as TagType[];

interface ResultCardProps extends Event {
cardModifierClass?: string;
}

function ResultCard({ end_time, id, location, name, keywords=[], start_time, images, offers, cardModifierClass, }: ResultCardProps) {
function ResultCard({
cardModifierClass,
end_time,
enrolment_end_time,
enrolment_start_time,
id,
images,
keywords=[],
location,
name,
offers,
start_time,
type_id,
}: ResultCardProps) {
const { currentLanguage } = drupalSettings.path;
const { baseUrl, imagePlaceholder } = drupalSettings.helfi_events;
const url = `${baseUrl}/${currentLanguage}/events/${id}`;

const resolvedName = name?.[currentLanguage] || name?.fi || Object.values(name)[0] || '';

const formatTime = (date: Date) => date.toLocaleTimeString('fi-FI', { hour: '2-digit', minute: '2-digit' });

const getDate = () => {
let startDate;
Expand All @@ -51,7 +61,7 @@ function ResultCard({ end_time, id, location, name, keywords=[], start_time, ima
return `${formatStartDate(startDate, endDate)} - ${endDate.toLocaleDateString('fi-FI')}`;
}

return `${startDate.toLocaleDateString('fi-FI')}, ${Drupal.t('at', {}, { context: 'Indication that events take place in a certain timeframe' })} ${startDate.toLocaleTimeString('fi-FI', { hour: '2-digit', minute: '2-digit' })} - ${endDate.toLocaleTimeString('fi-FI', { hour: '2-digit', minute: '2-digit' })}`;
return `${startDate.toLocaleDateString('fi-FI')}, ${Drupal.t('at', {}, { context: 'Indication that events take place in a certain timeframe' })} ${formatTime(startDate)} - ${formatTime(endDate)}`;
};

const getLocation = () => {
Expand Down Expand Up @@ -110,20 +120,76 @@ function ResultCard({ end_time, id, location, name, keywords=[], start_time, ima
);
};

const getCardCategoryTag = () => {
if (!type_id || type_id === 'Volunteering') {
return;
}

return type_id === 'Course' ?
{tag: Drupal.t('Hobby', {}, {context: 'Event search: hobby tag'}), color: 'gold'} :
{tag: Drupal.t('Event', {}, {context: 'Event search: event tag'}), color: 'fog-medium-light'};
};

const isRemote = location && location.id === INTERNET_EXCEPTION;
const cardTags = getCardTags({keywords, currentLanguage});
const isFree = offers?.some(({ is_free }) => is_free);
const getCardTags = () => {
const tags = [];

if (isRemote) {
tags.push({
tag: Drupal.t('Remote participation', {}, { context: 'Label for remote events' }),
color: 'silver',
});
}

if (isFree) {
tags.push({
tag: Drupal.t('Free', {}, { context: 'Label for free events' }),
color: 'silver',
});
}

return tags;
};

const getSignUp = () => {
if (!enrolment_end_time && !enrolment_start_time) {
return;
}

const startDate = new Date(enrolment_start_time);
const startString = `${startDate.toLocaleDateString('fi-FI')} ${Drupal.t('at', {}, {context: 'Indication that events take place in a certain timeframe' })} ${formatTime(startDate)}`;

// There should never be a case where we have end date but no start date.
if (!enrolment_end_time) {
return startString;
}

const endDate = new Date(enrolment_end_time);
return `${startString} - ${endDate.toLocaleDateString('fi-FI')} ${Drupal.t('at', {}, {context: 'Indication that events take place in a certain timeframe' })} ${formatTime(endDate)}`;
};

const getUrl = () => {
if (type_id && type_id === 'Course') {
return `${hobbiesPublicUrl}/${currentLanguage}/courses/${id}`;
}

return `${baseUrl}/${currentLanguage}/events/${id}`;
};

return (
<CardItem
cardCategoryTag={getCardCategoryTag()}
cardModifierClass={cardModifierClass}
cardUrl={url}
cardUrl={getUrl()}
cardTitle={resolvedName}
cardImage={getImage()}
cardTags={cardTags}
cardTags={getCardTags()}
cardUrlExternal
location={isRemote ? 'Internet' : getLocation()}
time={getDate()}
registrationRequired={getOffers()}
signUp={getSignUp()}
/>
);
}
Expand Down
31 changes: 24 additions & 7 deletions src/js/react/apps/linkedevents/components/SeeAllButton.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
import { useAtomValue} from 'jotai';
import { eventsPublicUrl } from '../store';
import { eventsPublicUrl, hobbiesPublicUrl, settingsAtom } from '../store';
import ExternalLink from '../../../common/ExternalLink';

function SeeAllButton() {
const filterSettings = useAtomValue(settingsAtom);
const eventsUrl = useAtomValue(eventsPublicUrl) || '';
const { seeAllButtonOverride } = drupalSettings?.helfi_events || null;
const { eventListType } = filterSettings;

return (
<div className="event-list__see-all-button">
<ExternalLink
data-hds-component="button"
data-hds-variant="secondary"
href={eventsUrl}
title={seeAllButtonOverride || Drupal.t('Search for more events on the Events website', {}, { context: 'Events search' })} />
<div className='event-list__see-all-container'>
{['events', 'events_and_hobbies'].includes(eventListType) &&
<div className="event-list__see-all-button">
<ExternalLink
data-hds-component="button"
data-hds-variant="secondary"
href={eventsUrl}
title={seeAllButtonOverride || Drupal.t('Search for more events on the Events website', {}, { context: 'Events search' })}
/>
</div>
}
{['hobbies', 'events_and_hobbies'].includes(eventListType) &&
<div className="event-list__see-all-button">
<ExternalLink
data-hds-component='button'
data-hds-variant="secondary"
href={hobbiesPublicUrl}
title={Drupal.t('Search for more events on the Hobbies website', {}, { context: 'Events search' })}
/>
</div>
}
</div>
);
}
Expand Down
15 changes: 14 additions & 1 deletion src/js/react/apps/linkedevents/containers/FormContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import AddressSearch from '../components/AddressSearch';
import FullTopicsFilter from '../components/FullTopicsFilter';
import FullLocationFilter from '../components/FullLocationFilter';
import { LanguageFilter } from '../components/LanguageFilter';
import { EventTypeFilter } from '../components/EventTypeFilter';


function FormContainer() {
Expand All @@ -30,6 +31,7 @@ function FormContainer() {
const url = useAtomValue(urlAtom);
const updateUrl = useSetAtom(updateUrlAtom);
const {
eventListType,
showFreeFilter,
hideHeading,
showLanguageFilter,
Expand Down Expand Up @@ -59,7 +61,7 @@ function FormContainer() {
const freeLabel = bothCheckboxes ? freeTranslation : `${showOnlyLabel} ${freeTranslation.toLowerCase()}`;
const remoteLabel = bothCheckboxes ? remoteTranslation : `${showOnlyLabel} ${remoteTranslation.toLowerCase()}`;

const showForm = showLocation || showFreeFilter || showTimeFilter || showRemoteFilter || showTopicsFilter;
const showForm = showLocation || showFreeFilter || showTimeFilter || showRemoteFilter || showTopicsFilter || eventListType === 'events_and_hobbies';
const HeadingTag = eventListTitle ? 'h3' : 'h2';

if (!showForm) {
Expand Down Expand Up @@ -101,6 +103,17 @@ function FormContainer() {
<LanguageFilter />
}
</div>
{
eventListType === 'events_and_hobbies' &&
<div className='hdbt-search--react__checkbox-filter-container'>
<fieldset className='hdbt-search--react__fieldset'>
<legend className='hdbt-search--react__legend'>
{Drupal.t('Type', {}, { context: 'Event search: type filter label' })}
</legend>
<EventTypeFilter />
</fieldset>
</div>
}
{
(showFreeFilter || showRemoteFilter) &&
<div className='hdbt-search--react__checkbox-filter-container'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ import {
updateParamsAtom,
updateUrlAtom,
languageAtom,
eventTypeAtom,
} from '../store';
import OptionType from '../types/OptionType';
import ApiKeys from '../enum/ApiKeys';
import getDateString from '../helpers/GetDate';
import { EventTypeOption } from '../types/EventTypeOption';
import { typeSelectionsToString } from '../helpers/TypeSelectionsToString';

type SelectionsContainerProps = {
url: string | undefined;
Expand All @@ -33,9 +36,18 @@ const SelectionsContainer = ({ url }: SelectionsContainerProps) => {
const [locationSelection, setLocationSelection] = useAtom(locationSelectionAtom);
const [topicsSelection, setTopicsSelection] = useAtom(topicSelectionAtom);
const [languageSelection, setLanguageSelection] = useAtom(languageAtom);
const eventTypeSelection = useAtomValue(eventTypeAtom);
const resetForm = useSetAtom(resetFormAtom);

const showClearButton = locationSelection.length || topicsSelection.length || languageSelection.length || startDate || endDate || freeFilter || remoteFilter;
const showClearButton =
locationSelection.length ||
topicsSelection.length ||
languageSelection.length ||
eventTypeSelection.length ||
startDate ||
endDate ||
freeFilter ||
remoteFilter;

if (!url) {
return null;
Expand Down Expand Up @@ -80,6 +92,7 @@ const SelectionsContainer = ({ url }: SelectionsContainerProps) => {
url={url}
value={freeFilter}
/>
<TypeFilterPills {...{eventTypeSelection, url}}/>
</FilterBulletsWrapper>
);
};
Expand Down Expand Up @@ -172,6 +185,42 @@ type DateFilterBulletProps = {
url: string | null;
};

const TypeFilterBullets = ({
eventTypeSelection,
url,
}: {
eventTypeSelection: EventTypeOption[];
url: string|null;
}) => {
const setEventTypeSelection = useSetAtom(eventTypeAtom);
const updateParams = useSetAtom(updateParamsAtom);
const updateUrl = useSetAtom(updateUrlAtom);

if (!eventTypeSelection.length) {
return null;
}

return (
<>
{eventTypeSelection.map((selection: EventTypeOption) => (
<FilterButton
clearSelection={() => {
const value = eventTypeSelection.filter((type) => type !== selection);
setEventTypeSelection(value);
updateParams({[ApiKeys.EVENT_TYPE]: typeSelectionsToString(value)});
updateUrl();
}}
key={selection}
value={selection === 'General' ?
Drupal.t('Events', {}, {context: 'Event search: events type'}) :
Drupal.t('Hobbies', {}, {context: 'Event search: hobbies type'})
}
/>
))}
</>
);
};

const DateFilterBullet = ({ startDate, endDate, url}: DateFilterBulletProps) => {
const setStartDate = useSetAtom(startDateAtom);
const setEndDate = useSetAtom(endDateAtom);
Expand Down Expand Up @@ -208,4 +257,5 @@ const FilterBulletsWrapper = memo(FilterBullets, updateSelections);
const ListFilterPills = memo(ListFilterBullets, updateSelections);
const CheckboxFilterPill = memo(CheckboxFilterBullet, updateSelections);
const DateFilterPill = memo(DateFilterBullet, updateSelections);
const TypeFilterPills = memo(TypeFilterBullets, updateSelections);
export default memo(SelectionsContainer, updateSelections);
1 change: 1 addition & 0 deletions src/js/react/apps/linkedevents/enum/ApiKeys.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const ApiKeys = {
COORDINATES: 'dwithin_origin',
END: 'end',
EVENT_TYPE: 'event_type',
FREE: 'is_free',
KEYWORDS: 'keyword_OR',
LANGUAGE: 'in_language',
Expand Down
Loading
Loading