Skip to content
9 changes: 6 additions & 3 deletions app/component/Badge.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ const getIcon = variant => {
): {
return <Icon img="icon_info-circled" className="info" />;
}
case variant === AlertSeverityLevelType.Warning: {
return <Icon img="icon_alert-circled" className="warning" />;
case variant === 'success': {
return <Icon img="icon_check" className="success" />;
}
case variant === AlertSeverityLevelType.Severe: {
case [
AlertSeverityLevelType.Warning,
AlertSeverityLevelType.Severe,
].includes(variant): {
return <Icon img="icon_caution_white_exclamation" className="danger" />;
}
default:
Expand Down
59 changes: 55 additions & 4 deletions app/component/trafficnow/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,65 @@ import Link from 'found/Link';
import cx from 'classnames';
import Icon from '../Icon';
import { useBreakpoint } from '../../util/withBreakpoint';
import { useConfigContext } from '../../configurations/ConfigContext';
import { useTranslationsContext } from '../../util/useTranslationsContext';

const AdditionalDescription = () => {
const intl = useTranslationsContext();
const {
URL: { HOLIDAYS_AND_EXCEPTIONS, MAJOR_CHANGES },
language,
} = useConfigContext();

const links = [
{
key: 'link1',
href: HOLIDAYS_AND_EXCEPTIONS[language],
message: {
id: 'traffic-now_description_see-also--link1',
defaultMessage: 'holidays and exceptions',
},
},
...(MAJOR_CHANGES && MAJOR_CHANGES[language]
? [
{
key: 'link2',
href: MAJOR_CHANGES[language],
message: {
id: 'traffic-now_description_see-also--link2',
defaultMessage: 'major changes',
},
},
]
: []),
];

return (
<FormattedMessage
id="traffic-now_description_see-also"
defaultMessage="See also {link1} as well as {link2}, which you will find in detail on their own pages"
values={links.reduce(
(acc, link) => ({
...acc,
[link.key]: (
<a href={link.href}>{intl.formatMessage(link.message)}</a>
),
}),
{ amount: links.length },
)}
/>
);
};

export default function Header() {
const breakpoint = useBreakpoint();
const { CONFIG } = useConfigContext();

const mobile = breakpoint !== 'large';
const desktop = breakpoint === 'large';
return (
<div
className={cx('traffic-now__header', {
'traffic-now__header--mobile': mobile,
'traffic-now__header--mobile': !desktop,
})}
>
<span className="traffic-now__header-breadcrumb">
Expand All @@ -30,9 +80,10 @@ export default function Header() {
<h2>
<FormattedMessage id="traffic-now" />
</h2>
<span className="traffic-now__header-description">
<p className="traffic-now__header-description">
<FormattedMessage id="traffic-now_description" />
</span>
{CONFIG === 'hsl' && <AdditionalDescription />}
</p>
</div>
);
}
Expand Down
45 changes: 24 additions & 21 deletions app/component/trafficnow/RouteBadges.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,31 @@ export default function RouteBadges({ entities: rawEntities }) {
return (
<div className="route-badges">
{Object.entries(entitiesByMode).map(
([key, { mode, isRoute, entities }]) => (
<div key={key} className={`route-badges-mode flex-row ${mode}`}>
<Icon
img={`icon_${mode}`}
height={2}
width={2}
iconScale={isRoute ? NORMAL_ICON_SCALE : STOP_SIGN_ICON_SCALE}
background={
!isRoute && (
<IconBackground shape="stopsign" color="currentcolor" />
)
}
/>
<div className="route-badges-mode-lines flex-row vertically-centered">
{entities.map(({ id, name, url }) => (
<a key={id} onClick={handleRouteBadgeClick(url)} href={url}>
<span className="route-badges-mode-lines--text">{name}</span>
</a>
))}
([key, { mode, isRoute, entities }]) =>
mode && (
<div key={key} className={`route-badges-mode flex-row ${mode}`}>
<Icon
img={`icon_${mode}`}
height={2}
width={2}
iconScale={isRoute ? NORMAL_ICON_SCALE : STOP_SIGN_ICON_SCALE}
background={
!isRoute && (
<IconBackground shape="stopsign" color="currentcolor" />
)
}
/>
<div className="route-badges-mode-lines flex-row vertically-centered">
{entities.map(({ id, name, url }) => (
<a key={id} onClick={handleRouteBadgeClick(url)} href={url}>
<span className="route-badges-mode-lines--text">
{name}
</span>
</a>
))}
</div>
</div>
</div>
),
),
)}
</div>
);
Expand Down
14 changes: 10 additions & 4 deletions app/component/trafficnow/TrafficNow.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ import { useBreakpoint } from '../../util/withBreakpoint';
import Gutterer from '../Gutterer';
import Loading from '../Loading';
import { FilterContextProvider } from './filters/FiltersContext';
import { useTranslationsContext } from '../../util/useTranslationsContext';

export default function TrafficNow() {
const intl = useTranslationsContext();
const breakpoint = useBreakpoint();
const [showFiltersModal, setShowFiltersModal] = useState(false);

const mobile = breakpoint !== 'large';

return (
<div className={cx('traffic-now')}>
<Gutterer maxWidth="1440px">
<Gutterer maxWidth="1440px" contentStyles={{ display: 'flex' }}>
<Header />
</Gutterer>
<div className="separator horizontal" />
Expand All @@ -44,19 +46,23 @@ export default function TrafficNow() {
<Filters />
</div>
) : (
<>
<div className="traffic-now__content__filters-button-container">
<FiltersModal
isOpen={showFiltersModal}
onClose={() => setShowFiltersModal(false)}
/>
<Button
className="traffic-now__content__filters-button"
size="medium"
fullWidth
variant="blue"
value="Suodattimet"
value={intl.formatMessage({
id: 'filters',
defaultMessage: 'Filters',
})}
onClick={() => setShowFiltersModal(true)}
/>
</>
</div>
)}
<Suspense fallback={<Loading />}>
<Alerts />
Expand Down
9 changes: 7 additions & 2 deletions app/component/trafficnow/filters/EntitySearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import { withSearchContext } from '../../WithSearchContext';
import { useConfigContext } from '../../../configurations/ConfigContext';
import { useFilterContext } from './FiltersContext';
import { useTranslationsContext } from '../../../util/useTranslationsContext';

const searchSources = ['Favourite', 'History', 'Datasource'];

Expand All @@ -15,6 +16,7 @@ const EntitySearch = ({ filterId }) => {
iconModeSet,
} = useConfigContext();
const { selectedFilters, setFilter } = useFilterContext();
const intl = useTranslationsContext();

const DTAutoSuggestWithSearchContext = withSearchContext(DTAutoSuggest);

Expand All @@ -30,14 +32,17 @@ const EntitySearch = ({ filterId }) => {
<FormattedMessage
tagName="legend"
id="traffic-now_filters_entity-search"
defaultMessage="Hae yksittäistä linjaa tai pysäkkiä"
defaultMessage="Search for individual route or stop"
/>
<DTAutoSuggestWithSearchContext
appElement="#app"
icon="search"
id="traffic-now_filters_entity-search--input"
className="traffic-now_filters_entity-search--input"
placeholder="Linja, pysäkki tai asema"
placeholder={intl.formatMessage({
id: 'traffic-now_filters_entity-search--placeholder',
defaultMessage: 'Route, stop or station',
})}
geocodingSize={40}
sources={searchSources}
value={selectedFilters[filterId]?.address}
Expand Down
10 changes: 8 additions & 2 deletions app/component/trafficnow/filters/FiltersModal.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import React from 'react';
import Modal from '@hsl-fi/modal';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import Filters from './Filters';
import Icon from '../../Icon';
import IconBackground from '../../icon/IconBackground';
import { useFilterContext } from './FiltersContext';
import { useTranslationsContext } from '../../../util/useTranslationsContext';

const FiltersModal = ({ isOpen, onClose }) => {
const { resetFilters } = useFilterContext();
const intl = useTranslationsContext();

return (
<Modal
appElement="#app"
isOpen={isOpen}
shouldCloseOnEsc
shouldCloseOnOverlayClick
contentLabel="Filters"
contentLabel={intl.formatMessage({
id: 'filters',
defaultMessage: 'Filters',
})}
onRequestClose={onClose}
variant="large"
className="traffic-now__modal-filters"
>
<header>
<h3>Suodata</h3>
<FormattedMessage id="filter" defaultMessage="Filter" tagName="h3" />
<button type="button" onClick={onClose}>
<Icon
height={1.5}
Expand Down
47 changes: 46 additions & 1 deletion app/component/trafficnow/filters/filterUtils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
import { AlertSeverityLevelType } from '../../../constants';

const SEVERITY_ORDER = {
[AlertSeverityLevelType.Severe]: 0,
[AlertSeverityLevelType.Warning]: 1,
[AlertSeverityLevelType.Info]: 2,
DEFAULT: 3,
};

const isActiveWarning = alert => {
const now = Date.now() * 0.001;
const { effectiveStartDate, effectiveEndDate, alertSeverityLevel } = alert;
const isActive = effectiveStartDate <= now && now <= effectiveEndDate;
const isInfo = alertSeverityLevel === AlertSeverityLevelType.Info;
return isActive && !isInfo;
};

const validityPeriodFilter = (alert, { validityPeriod }) => {
const now = Date.now() * 0.001;
switch (validityPeriod) {
Expand All @@ -15,6 +32,9 @@ const validityPeriodFilter = (alert, { validityPeriod }) => {
* Filters alerts by selected vehicle modes. If no modes are selected, include all alerts.
* If any entity matches a selected mode, include the alert.
*
* Active WARNING|SEVERE alerts are preceding INFO alerts. Inactive (e.g. upcoming) alerts
* are sorted asc by date
*
* entities may contain objects with different properties:
* - Stop: entity with a vehicleMode property
* - Route: entity with a mode property
Expand All @@ -40,7 +60,32 @@ const entityFilter = ({ entities }, { entity }) =>

export function filterAndSortAlerts(alerts, selectedFilters) {
const filterFns = [validityPeriodFilter, vehicleModesFilter, entityFilter];

return alerts
.filter(alert => filterFns.every(fn => fn(alert, selectedFilters)))
.sort((a, b) => a.effectiveStartDate - b.effectiveStartDate);
.sort((a, b) => {
const aIsActiveWarning = isActiveWarning(a);
const bIsActiveWarning = isActiveWarning(b);

if (aIsActiveWarning && !bIsActiveWarning) {
return -1;
}
if (!aIsActiveWarning && bIsActiveWarning) {
return 1;
}

if (aIsActiveWarning && bIsActiveWarning) {
const aSeverity =
SEVERITY_ORDER[a.alertSeverityLevel] ?? SEVERITY_ORDER.DEFAULT;
const bSeverity =
SEVERITY_ORDER[b.alertSeverityLevel] ?? SEVERITY_ORDER.DEFAULT;
if (aSeverity !== bSeverity) {
return aSeverity - bSeverity;
}

return a.effectiveStartDate - b.effectiveStartDate;
}

return a.effectiveStartDate - b.effectiveStartDate;
});
}
22 changes: 16 additions & 6 deletions app/component/trafficnow/trafficnow.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
&__header {
display: flex;
flex-direction: column;
height: 216px;
gap: 20px;
padding: 0 50px;
gap: var(--space-m);
padding: var(--space-l) 50px var(--space-xl) 50px;
flex: 0 0 50%;

&-breadcrumb {
display: inline-flex;
Expand All @@ -47,6 +47,7 @@

&--mobile {
padding: 25px 20px 20px 20px;
flex: 0 0 100%;
}
}

Expand All @@ -58,8 +59,7 @@
background-color: $background-color-lighter;

&--mobile {
padding: var(--space-m) var(--space-s) 0 var(--space-s);
gap: var(--space-l);
padding: 0 var(--space-s);
}

&__filters {
Expand All @@ -68,11 +68,21 @@
gap: var(--space-m);
padding-top: var(--space-xl);
padding-right: var(--space-xl);
position: sticky;
top: 0;

&-mobile {
padding: var(--space-xl) var(--space-m);
}

&-button-container {
background-color: $background-color-lighter;
padding: var(--space-m) 0;
position: sticky;
top: 0;
z-index: 10;
}

&-container {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -120,7 +130,7 @@
height: 100%;
flex-direction: column;
min-width: 0;
gap: inherit;
gap: var(--space-l);
min-height: 0;
overflow: auto;

Expand Down
Loading