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-10772: Extend linkedevents app #1131

Merged
merged 6 commits into from
Dec 12, 2024
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/styles.min.css

Large diffs are not rendered by default.

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/ploughing-schedule.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.

28 changes: 19 additions & 9 deletions src/js/react/apps/linkedevents/components/ResultCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ function ResultCard({ end_time, id, location, name, keywords=[], start_time, ima
const { baseUrl, imagePlaceholder } = drupalSettings.helfi_events;
const url = `${baseUrl}/${currentLanguage}/events/${id}`;

// Bail if no current language
if (!name[currentLanguage]) {
return null;
}
const resolvedName = name?.[currentLanguage] || name?.fi || Object.values(name)[0] || '';

const getDate = () => {
let startDate;
Expand All @@ -43,7 +40,7 @@ function ResultCard({ end_time, id, location, name, keywords=[], start_time, ima
endDate = new Date(end_time);
isMultiDate = end_time ? overDayApart(startDate, endDate) : false;
} catch (e) {
throw new Error('DATE ERROR');
throw new Error(`DATE ERROR ${e}`);
}

if (isMultiDate) {
Expand Down Expand Up @@ -100,16 +97,29 @@ function ResultCard({ end_time, id, location, name, keywords=[], start_time, ima
return <img alt='' {...imageProps} />;
};

const image = images?.find(img => img.url);
const getImage = () => {
const image = images?.find(img => img.url);

if (image) {
return imageToElement(image);
}
if (imagePlaceholder) {
return parse(imagePlaceholder);
}

return (
<div className='image-placeholder'></div>
);
};

const isRemote = location && location.id === INTERNET_EXCEPTION;
const title = name[currentLanguage] || '';
const cardTags = getCardTags({keywords, currentLanguage});

return (
<CardItem
cardUrl={url}
cardTitle={title}
cardImage={image ? imageToElement(image) : parse(imagePlaceholder) }
cardTitle={resolvedName}
cardImage={getImage()}
cardTags={cardTags}
cardUrlExternal
location={isRemote ? 'Internet' : getLocation()}
Expand Down
3 changes: 2 additions & 1 deletion src/js/react/apps/linkedevents/components/SeeAllButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import ExternalLink from '../../../common/ExternalLink';

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

return (
<div className="event-list__see-all-button">
<ExternalLink
data-hds-component="button"
data-hds-variant="secondary"
href={eventsUrl}
title={Drupal.t('Refine search in tapahtumat.hel.fi', {}, { context: 'Events search' })} />
title={seeAllButtonOverride || Drupal.t('Refine search in tapahtumat.hel.fi', {}, { context: 'Events search' })} />
</div>
);
}
Expand Down
54 changes: 36 additions & 18 deletions src/js/react/apps/linkedevents/containers/ResultsContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { createRef } from 'react';
import { createRef, useEffect, useState } from 'react';
import { useAtomValue } from 'jotai';

import ResultsError from '@/react/common/ResultsError';
import LoadingOverlay from '@/react/common/LoadingOverlay';
import useScrollToResults from '@/react/common/hooks/useScrollToResults';
import Pagination from '../components/Pagination';
import ResultCard from '../components/ResultCard';
import CardGhost from '@/react/common/CardGhost';
import SeeAllButton from '../components/SeeAllButton';
import { settingsAtom, urlAtom } from '../store';
import type Event from '../types/Event';
import ResultsHeader from '@/react/common/ResultsHeader';
import ResultsEmpty from '@/react/common/ResultsEmpty';
import LoadingOverlay from '@/react/common/LoadingOverlay';

type ResultsContainerProps = {
countNumber: number;
Expand All @@ -20,20 +21,20 @@ type ResultsContainerProps = {
};

function ResultsContainer({ countNumber, events, loading, error }: ResultsContainerProps) {
const { useExperimentalGhosts } = drupalSettings.helfi_events;
const settings = useAtomValue(settingsAtom);
const scrollTarget = createRef<HTMLDivElement>();
const url = useAtomValue(urlAtom);
// Checks when user makes the first search and api url is set.
const choices = Boolean(url);
useScrollToResults(scrollTarget, choices);
const [initialized, setInitialized] = useState(false);
useScrollToResults(scrollTarget, initialized && choices && !loading);

if (loading) {
return (
<div className='hdbt__loading-wrapper'>
<LoadingOverlay />
</div>
);
}
useEffect(() => {
if (!initialized && !loading) {
setInitialized(true);
}
}, [initialized, setInitialized, loading]);

if (error) {
return (
Expand All @@ -44,29 +45,46 @@ function ResultsContainer({ countNumber, events, loading, error }: ResultsContai
);
}

if (loading && !useExperimentalGhosts) {
return (
<div className='hdbt__loading-wrapper'>
<LoadingOverlay />
</div>
);
}

const size = settings.eventCount;
const pages = Math.floor(countNumber / size);
const addLastPage = countNumber > size && countNumber % size;
const count = countNumber.toString();

return (
<div className='react-search__list-container'>
{events?.length > 0 ?
const getContent = () => {
if (loading && !events.length) {
return Array.from({ length: size }, (_, i) => <CardGhost key={i} />);
}
if (events.length > 0) {
return (
<>
<ResultsHeader
resultText={
<>
{ Drupal.formatPlural(count, '1 event', '@count events',{},{context: 'Events search: result count'}) }
{Drupal.formatPlural(count, '1 event', '@count events', {}, {context: 'Events search: result count'})}
</>
}
ref={scrollTarget}
/>
{events.map(event => <ResultCard key={event.id} {...event} />)}
{events.map(event => loading ? <CardGhost key={event.id} /> : <ResultCard key={event.id} {...event} />)}
<Pagination pages={5} totalPages={addLastPage ? pages + 1 : pages} />
</>
:
<ResultsEmpty wrapperClass='event-list__no-results' ref={scrollTarget} />
}
);
}

return <ResultsEmpty wrapperClass='event-list__no-results' ref={scrollTarget} />;
};

return (
<div className={`react-search__list-container${loading ? ' loading' : ''}`}>
{getContent()}
<SeeAllButton />
</div>
);
Expand Down
7 changes: 4 additions & 3 deletions src/js/react/apps/linkedevents/containers/SearchContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import useSWR from 'swr';
import { useAtomValue, useAtom } from 'jotai';

import { useEffect, useState } from 'react';
import ResultsContainer from './ResultsContainer';
import FormContainer from './FormContainer';
import type Event from '../types/Event';
Expand All @@ -26,6 +27,7 @@ const SWR_REFRESH_OPTIONS = {
};

const SearchContainer = () => {
const { useExperimentalGhosts } = drupalSettings.helfi_events;
const initialUrl = useAtomValue(initialUrlAtom);
const initialParams = useAtomValue(initialParamsAtom);
const [params, setParams] = useAtom(paramsAtom);
Expand Down Expand Up @@ -59,13 +61,12 @@ const SearchContainer = () => {

throw new Error('Failed to get data from the API');
};
const { data, error } = useSWR(url, getEvents, SWR_REFRESH_OPTIONS);
const loading = !error && !data;
const { data, error, isLoading } = useSWR(url, getEvents, {...SWR_REFRESH_OPTIONS, keepPreviousData: useExperimentalGhosts});

return (
<>
<FormContainer />
<ResultsContainer error={error} countNumber={data?.meta.count || 0} loading={loading} events={data?.data || []} />
<ResultsContainer error={error} countNumber={data?.meta.count || 0} loading={isLoading} events={data?.data || []} />
</>
);
};
Expand Down
7 changes: 3 additions & 4 deletions src/js/react/apps/linkedevents/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import OptionType from './types/OptionType';
import FormErrors from './types/FormErrors';
import ApiKeys from './enum/ApiKeys';
import Topic from './types/Topic';
import type Event from './types/Event';

interface Options {
[key: string]: string
Expand Down Expand Up @@ -50,18 +49,18 @@ const createBaseAtom = () => {
const useFixtures = settings?.use_fixtures;
const eventsApiUrl = settings?.events_api_url;
const eventListTitle = settings?.field_event_list_title;
const eventsPublicUrl = settings?.events_public_url;
const eventsPublicUrl = settings?.events_public_url || 'https://tapahtumat.hel.fi';

const filterSettings: FilterSettings = {
showLocation: settings?.field_event_location,
showTimeFilter: settings?.field_event_time,
showFreeFilter: settings?.field_free_events,
showRemoteFilter: settings?.field_remote_events,
showTopicsFilter: settings?.field_filter_keywords.length > 0,
showTopicsFilter: settings?.field_filter_keywords?.length > 0,
eventCount: Number(settings?.field_event_count)
};
const locations = transformLocations(settings?.places);
const topics: Topic[] = settings?.field_filter_keywords.map(topic => ({
const topics: Topic[] = settings?.field_filter_keywords?.map(topic => ({
value: topic.id,
label: topic.name.charAt(0).toUpperCase() + topic.name.slice(1),
}));
Expand Down
6 changes: 5 additions & 1 deletion src/js/react/common/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ function CardItem({
languageEducation,
registrationRequired,
}: CardItemProps): JSX.Element {
const cardClass = `card${cardModifierClass ? ` ${cardModifierClass}` : ''}${cardUrlExternal ? ' card--external' : ''}`;
const cardClass = `
card
${cardModifierClass ? ` ${cardModifierClass}` : ''}
${cardUrlExternal ? ' card--external' : ''}
`;
const HeadingTag = cardTitleLevel ? `h${cardTitleLevel}` as keyof JSX.IntrinsicElements : 'h3';

return (
Expand Down
9 changes: 9 additions & 0 deletions src/js/react/common/CardGhost.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default function CardGhost() {
return (
<div className='card card--ghost'>
<div className='card__image'>
<div className="image-placeholder"></div>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion src/js/react/common/hooks/useScrollToResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const useScrollToResults = (ref: RefObject<HTMLElement>, shouldScrollOnRender: b

if (current && shouldScrollOnRender) {
current.setAttribute('tabindex', '-1');
current.focus();
current.focus({preventScroll: true});
current.scrollIntoView({behavior: 'smooth', block: 'center'});
}
}, [ref, shouldScrollOnRender]);
Expand Down
4 changes: 3 additions & 1 deletion src/js/types/drupalSettings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ declare namespace drupalSettings {
},
use_fixtures: boolean
}
}
},
seeAllButtonOverride: string,
useExperimentalGhosts: boolean,
};
const helfi_react_search: {
// @todo UHF-10862 Remove cookie_privacy_url once the HDBT cookie banner module is in use.
Expand Down
4 changes: 4 additions & 0 deletions src/scss/05_objects/_card.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ $item-gap: $spacing-half;
}
}

.card--ghost {
background: $color-black-20;
}

.card__image {
flex-basis: 30%;
flex-shrink: 0;
Expand Down
9 changes: 9 additions & 0 deletions src/scss/06_components/paragraphs/_event-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ $tag-vertical-padding: 5px;
@include component-side-padding;
}

.component--react-search.component--coordinates-based-event-list {
background-color: $color-white;
}

// Use experimental card border styles for this paragraph.
.component--coordinates-based-event-list .card:not(.card--ghost) {
border: 2px solid $color-black-20;
}

.events-list__empty-subtext {
margin-bottom: $spacing-and-half;
}
Expand Down
30 changes: 30 additions & 0 deletions src/scss/06_components/paragraphs/_react-search.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,36 @@
margin-bottom: $spacing-double;
}

.react-search__list-container.loading .card {
overflow: hidden;
position: relative;
}

@keyframes loading {
0% {
transform: skewX(-10deg) translateX(-150%);
}

100% {
transform: skewX(-10deg) translateX(250%);
}
}

.react-search__list-container.loading .card::after {
animation: loading 1.2s infinite;
background: linear-gradient(
90deg,
transparent,
$color-white,
transparent,
);
bottom: 0;
content: "";
position: absolute;
top: 0;
width: 50%;
};

.react-search__results-stats {
margin-bottom: $spacing;
}
Expand Down
Loading