@geops/rvf-mobility-web-component 0.1.115 → 0.1.117
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/index.js +145 -139
- package/jest-setup.js +8 -0
- package/package.json +17 -17
- package/src/FeaturesInfosListener/FeaturesInfosListener.tsx +16 -2
- package/src/LayoutState/LayoutState.tsx +2 -2
- package/src/NotificationDetails/NotificationDetails.tsx +4 -0
- package/src/Overlay/Overlay.tsx +15 -19
- package/src/RealtimeLayer/RealtimeLayer.tsx +5 -1
- package/src/RouteFollowingButton/RouteFollowingButton.tsx +30 -0
- package/src/RouteFollowingButton/index.tsx +1 -0
- package/src/RouteSchedule/RouteSchedule.tsx +6 -25
- package/src/RouteScheduleHeader/RouteScheduleHeader.tsx +2 -13
- package/src/RouteStop/RouteStop.tsx +6 -1
- package/src/ShadowOverflow/ShadowOverflow.tsx +16 -8
- package/src/SituationDetails/SituationDetails.tsx +4 -1
- package/src/StopsSearch/StopsSearch.tsx +1 -1
- package/src/utils/hooks/useLnp.tsx +1 -1
- package/src/utils/hooks/useScrollTo.tsx +36 -0
- package/src/utils/mergePublications.test.ts +91 -0
- package/src/utils/mergePublications.ts +52 -0
package/jest-setup.js
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@geops/rvf-mobility-web-component",
|
|
3
3
|
"license": "UNLICENSED",
|
|
4
4
|
"description": "Web components for rvf in the domains of mobility and logistics.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.117",
|
|
6
6
|
"homepage": "https://rvf-mobility-web-component.vercel.app/",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "index.js",
|
|
@@ -12,32 +12,32 @@
|
|
|
12
12
|
"jspdf": "^4.1.0",
|
|
13
13
|
"lodash.debounce": "^4.0.8",
|
|
14
14
|
"maplibre-gl": "5.12.0",
|
|
15
|
-
"mobility-toolbox-js": "3.6.
|
|
16
|
-
"ol": "^10.
|
|
17
|
-
"preact": "^10.28.
|
|
15
|
+
"mobility-toolbox-js": "3.6.7",
|
|
16
|
+
"ol": "^10.8.0",
|
|
17
|
+
"preact": "^10.28.4",
|
|
18
18
|
"preact-custom-element": "^4.6.0",
|
|
19
19
|
"react": "npm:@preact/compat@^18.3.1",
|
|
20
20
|
"react-dom": "npm:@preact/compat@^18.3.1",
|
|
21
21
|
"react-icons": "^5.5.0",
|
|
22
|
-
"react-spatial": "^2.0.
|
|
22
|
+
"react-spatial": "^2.0.3",
|
|
23
23
|
"rosetta": "^1.1.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@commitlint/cli": "^20.4.
|
|
27
|
-
"@commitlint/config-conventional": "^20.4.
|
|
28
|
-
"@eslint/js": "^9
|
|
29
|
-
"@geops/eslint-config-react": "^1.6.0
|
|
30
|
-
"@tailwindcss/cli": "^4.1
|
|
26
|
+
"@commitlint/cli": "^20.4.2",
|
|
27
|
+
"@commitlint/config-conventional": "^20.4.2",
|
|
28
|
+
"@eslint/js": "^9",
|
|
29
|
+
"@geops/eslint-config-react": "^1.6.0",
|
|
30
|
+
"@tailwindcss/cli": "^4.2.1",
|
|
31
31
|
"@tailwindcss/container-queries": "^0.1.1",
|
|
32
32
|
"@testing-library/preact": "^3.2.4",
|
|
33
33
|
"@types/geojson": "^7946.0.16",
|
|
34
34
|
"@types/jest": "^30.0.0",
|
|
35
|
-
"@types/lodash": "^4.17.
|
|
35
|
+
"@types/lodash": "^4.17.24",
|
|
36
36
|
"@types/preact-custom-element": "^4.0.4",
|
|
37
37
|
"concurrently": "^9.2.1",
|
|
38
|
-
"esbuild": "^0.27.
|
|
38
|
+
"esbuild": "^0.27.3",
|
|
39
39
|
"esbuild-sass-plugin": "^3.6.0",
|
|
40
|
-
"eslint": "^9
|
|
40
|
+
"eslint": "^9",
|
|
41
41
|
"eslint-plugin-tailwindcss": "^4.0.0-beta.0",
|
|
42
42
|
"fixpack": "^4.0.0",
|
|
43
43
|
"generact": "^0.4.0",
|
|
@@ -47,15 +47,15 @@
|
|
|
47
47
|
"jest-environment-jsdom": "^30.2.0",
|
|
48
48
|
"jest-preset-preact": "^4.1.1",
|
|
49
49
|
"next": "15.5.7",
|
|
50
|
-
"preact-render-to-string": "^6.6.
|
|
50
|
+
"preact-render-to-string": "^6.6.6",
|
|
51
51
|
"prettier": "^3.8.1",
|
|
52
52
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
53
53
|
"standard-version": "^9.5.0",
|
|
54
|
-
"tailwind-merge": "^3.
|
|
55
|
-
"tailwindcss": "^4.1
|
|
54
|
+
"tailwind-merge": "^3.5.0",
|
|
55
|
+
"tailwindcss": "^4.2.1",
|
|
56
56
|
"ts-jest": "^29.4.6",
|
|
57
57
|
"typescript": "^5.9.3",
|
|
58
|
-
"typescript-eslint": "^8.
|
|
58
|
+
"typescript-eslint": "^8.56.1"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "yarn build:css && yarn build:js && cp index*.js* doc/public/",
|
|
@@ -24,6 +24,11 @@ function FeaturesInfosListener() {
|
|
|
24
24
|
} = useMapContext();
|
|
25
25
|
|
|
26
26
|
useEffect(() => {
|
|
27
|
+
// We want to call this use effect only when a hover happened
|
|
28
|
+
// and a query has been done not when the component is mounted
|
|
29
|
+
if (!featuresInfosHovered?.length) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
27
32
|
const [realtimeFeature] =
|
|
28
33
|
featuresInfosHovered?.find((info) => {
|
|
29
34
|
return info.layer === realtimeLayer;
|
|
@@ -34,7 +39,7 @@ function FeaturesInfosListener() {
|
|
|
34
39
|
return info.layer === notificationsLayer;
|
|
35
40
|
})?.features || [];
|
|
36
41
|
|
|
37
|
-
// We prioritize only symbol notifications,
|
|
42
|
+
// We prioritize only symbol notifications, because we want to be able to click on trains on lines
|
|
38
43
|
const isSymbolNotification =
|
|
39
44
|
notificationFeature?.getGeometry()?.getType() === "Point";
|
|
40
45
|
|
|
@@ -50,6 +55,15 @@ function FeaturesInfosListener() {
|
|
|
50
55
|
}, [featuresInfosHovered, notificationsLayer, realtimeLayer]);
|
|
51
56
|
|
|
52
57
|
useEffect(() => {
|
|
58
|
+
// We want to call this use effect only when a click happened
|
|
59
|
+
// and a query has been done not when the component is mounted
|
|
60
|
+
if (!featuresInfos?.length) {
|
|
61
|
+
// Reinitialize selected features
|
|
62
|
+
setSelectedFeature(null);
|
|
63
|
+
setSelectedFeatures([]);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
const [realtimeFeature] =
|
|
54
68
|
featuresInfos?.find((info) => {
|
|
55
69
|
return info.layer === realtimeLayer;
|
|
@@ -84,7 +98,7 @@ function FeaturesInfosListener() {
|
|
|
84
98
|
return info.features;
|
|
85
99
|
}) || [];
|
|
86
100
|
|
|
87
|
-
// We prioritize only symbol notifications,
|
|
101
|
+
// We prioritize only symbol notifications, because we want to be able to click on trains on lines
|
|
88
102
|
const isSymbolNotification =
|
|
89
103
|
notificationFeature?.getGeometry()?.getType() === "Point";
|
|
90
104
|
|
|
@@ -345,10 +345,10 @@ function LayoutState() {
|
|
|
345
345
|
|
|
346
346
|
useEffect(() => {
|
|
347
347
|
setIsOverlayOpen(
|
|
348
|
-
(
|
|
349
|
-
(hasPrint && isExportMenuOpen) ||
|
|
348
|
+
(hasPrint && isExportMenuOpen) ||
|
|
350
349
|
(hasLayerTree && isLayerTreeOpen) ||
|
|
351
350
|
(hasShare && isShareMenuOpen) ||
|
|
351
|
+
(hasDetails && !!selectedFeature) || // For custom layers details.
|
|
352
352
|
(hasDetails && hasRealtime && !!trainId) ||
|
|
353
353
|
(hasDetails && tenant && !!stationId) ||
|
|
354
354
|
(hasDetails && hasLnp && !!linesIds) ||
|
|
@@ -11,6 +11,7 @@ import Link from "../ui/Link";
|
|
|
11
11
|
import useI18n from "../utils/hooks/useI18n";
|
|
12
12
|
import useMapContext from "../utils/hooks/useMapContext";
|
|
13
13
|
import useMocoSituation from "../utils/hooks/useMocoSituation";
|
|
14
|
+
import mergePublications from "../utils/mergePublications";
|
|
14
15
|
|
|
15
16
|
import type {
|
|
16
17
|
MultilingualTextualContentType,
|
|
@@ -88,6 +89,9 @@ function NotificationDetails({
|
|
|
88
89
|
});
|
|
89
90
|
}) || [];
|
|
90
91
|
|
|
92
|
+
// Merge similar publications
|
|
93
|
+
publicationsToDisplay = mergePublications(publicationsToDisplay);
|
|
94
|
+
|
|
91
95
|
// Display the current and next affected time intervals not the one in the past
|
|
92
96
|
timeIntervalsToDisplay =
|
|
93
97
|
(situationParsed?.affectedTimeIntervals || []).filter(
|
package/src/Overlay/Overlay.tsx
CHANGED
|
@@ -39,25 +39,21 @@ function Overlay({
|
|
|
39
39
|
className)
|
|
40
40
|
}
|
|
41
41
|
>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
</ScrollableHandler>
|
|
58
|
-
{children}
|
|
59
|
-
</>
|
|
60
|
-
)}
|
|
42
|
+
<ScrollableHandler
|
|
43
|
+
className="absolute inset-0 flex h-[65px] touch-none justify-center @lg/main:hidden"
|
|
44
|
+
style={{ width: "100%" }}
|
|
45
|
+
{...ScrollableHandlerProps}
|
|
46
|
+
>
|
|
47
|
+
<div
|
|
48
|
+
className="m-2 mr-[-60px] bg-gray-300"
|
|
49
|
+
style={{
|
|
50
|
+
borderRadius: 2,
|
|
51
|
+
height: 4,
|
|
52
|
+
width: 32,
|
|
53
|
+
}}
|
|
54
|
+
/>
|
|
55
|
+
</ScrollableHandler>
|
|
56
|
+
{children}
|
|
61
57
|
</div>
|
|
62
58
|
);
|
|
63
59
|
}
|
|
@@ -94,13 +94,14 @@ function RealtimeLayer(props: Partial<RealtimeLayerOptions>) {
|
|
|
94
94
|
}, [apikey, realtimeurl, realtimebboxparameters, mots, tenant, props]);
|
|
95
95
|
|
|
96
96
|
useEffect(() => {
|
|
97
|
+
let key;
|
|
97
98
|
if (!map || !layer) {
|
|
98
99
|
return;
|
|
99
100
|
}
|
|
100
101
|
if (map.getView()?.getCenter()) {
|
|
101
102
|
map.addLayer(layer);
|
|
102
103
|
} else {
|
|
103
|
-
map.once("moveend", () => {
|
|
104
|
+
key = map.once("moveend", () => {
|
|
104
105
|
map.addLayer(layer);
|
|
105
106
|
});
|
|
106
107
|
}
|
|
@@ -108,6 +109,9 @@ function RealtimeLayer(props: Partial<RealtimeLayerOptions>) {
|
|
|
108
109
|
setRealtimeLayer(layer);
|
|
109
110
|
|
|
110
111
|
return () => {
|
|
112
|
+
if (key) {
|
|
113
|
+
unByKey(key);
|
|
114
|
+
}
|
|
111
115
|
map.removeLayer(layer);
|
|
112
116
|
setRealtimeLayer(null);
|
|
113
117
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { memo } from "preact/compat";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
import Tracking from "../icons/Tracking";
|
|
5
|
+
import IconButton from "../ui/IconButton";
|
|
6
|
+
import useMapContext from "../utils/hooks/useMapContext";
|
|
7
|
+
|
|
8
|
+
import type { IconButtonProps } from "../ui/IconButton/IconButton";
|
|
9
|
+
|
|
10
|
+
function RouteFollowingButton({
|
|
11
|
+
children,
|
|
12
|
+
className,
|
|
13
|
+
...props
|
|
14
|
+
}: IconButtonProps) {
|
|
15
|
+
const { isFollowing, setIsFollowing } = useMapContext();
|
|
16
|
+
return (
|
|
17
|
+
<IconButton
|
|
18
|
+
className={twMerge(`${isFollowing ? "animate-pulse" : ""}`, className)}
|
|
19
|
+
onClick={() => {
|
|
20
|
+
setIsFollowing(!isFollowing);
|
|
21
|
+
}}
|
|
22
|
+
selected={isFollowing}
|
|
23
|
+
{...props}
|
|
24
|
+
>
|
|
25
|
+
{children || <Tracking />}
|
|
26
|
+
</IconButton>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default memo(RouteFollowingButton);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./RouteFollowingButton";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { memo } from "preact/compat";
|
|
2
|
-
import {
|
|
2
|
+
import { useRef } from "preact/hooks";
|
|
3
3
|
import { twMerge } from "tailwind-merge";
|
|
4
4
|
|
|
5
5
|
import RouteScheduleFooter from "../RouteScheduleFooter";
|
|
@@ -8,6 +8,7 @@ import RouteStop from "../RouteStop";
|
|
|
8
8
|
import ShadowOverflow from "../ShadowOverflow";
|
|
9
9
|
import useMapContext from "../utils/hooks/useMapContext";
|
|
10
10
|
import useRealtimeStopSequences from "../utils/hooks/useRealtimeStopSequences";
|
|
11
|
+
import useScrollTo from "../utils/hooks/useScrollTo";
|
|
11
12
|
|
|
12
13
|
import type { RealtimeStop } from "mobility-toolbox-js/types";
|
|
13
14
|
import type { HTMLAttributes, PreactDOMAttributes } from "preact";
|
|
@@ -20,29 +21,9 @@ export type RouteScheduleProps = {
|
|
|
20
21
|
function RouteSchedule({ className }: RouteScheduleProps) {
|
|
21
22
|
const { trainId } = useMapContext();
|
|
22
23
|
const stopSequences = useRealtimeStopSequences(trainId);
|
|
23
|
-
const ref = useRef();
|
|
24
|
+
const ref = useRef<HTMLDivElement>();
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
const interval = window.setInterval(() => {
|
|
27
|
-
const elt = ref.current as HTMLDivElement;
|
|
28
|
-
if (!elt) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
const nextStation = elt.querySelector("[data-station-passed=false]");
|
|
32
|
-
if (nextStation) {
|
|
33
|
-
// We use scrollTo avoid scrolling the entire window.
|
|
34
|
-
(nextStation.parentNode.parentNode as Element).scrollTo({
|
|
35
|
-
behavior: "smooth",
|
|
36
|
-
top: (nextStation as HTMLElement).offsetTop || 0,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
clearInterval(interval);
|
|
40
|
-
}, 300);
|
|
41
|
-
return () => {
|
|
42
|
-
clearTimeout(interval);
|
|
43
|
-
};
|
|
44
|
-
// Scroll automatically when a new scroll infos is set.
|
|
45
|
-
}, [stopSequences]);
|
|
26
|
+
useScrollTo(ref, "[data-station-passed=false]", [stopSequences]);
|
|
46
27
|
|
|
47
28
|
if (!stopSequences?.[0]) {
|
|
48
29
|
return null;
|
|
@@ -51,8 +32,8 @@ function RouteSchedule({ className }: RouteScheduleProps) {
|
|
|
51
32
|
return (
|
|
52
33
|
<>
|
|
53
34
|
<RouteScheduleHeader stopSequence={stopSequence} />
|
|
54
|
-
<ShadowOverflow>
|
|
55
|
-
<div className={twMerge("text-base", className)}
|
|
35
|
+
<ShadowOverflow ref={ref}>
|
|
36
|
+
<div className={twMerge("text-base", className)}>
|
|
56
37
|
{stopSequence.stations.map((stop: RealtimeStop, index: number) => {
|
|
57
38
|
const { arrivalTime, departureTime, stationId, stationName } = stop;
|
|
58
39
|
return (
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { memo } from "preact/compat";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import RouteFollowingButton from "../RouteFollowingButton";
|
|
4
4
|
import RouteIcon from "../RouteIcon";
|
|
5
5
|
import RouteInfos from "../RouteInfos";
|
|
6
|
-
import IconButton from "../ui/IconButton";
|
|
7
|
-
import useMapContext from "../utils/hooks/useMapContext";
|
|
8
6
|
|
|
9
7
|
import type { RealtimeStopSequence } from "mobility-toolbox-js/types";
|
|
10
8
|
|
|
@@ -13,20 +11,11 @@ function RouteScheduleHeader({
|
|
|
13
11
|
}: {
|
|
14
12
|
stopSequence: RealtimeStopSequence;
|
|
15
13
|
}) {
|
|
16
|
-
const { isFollowing, setIsFollowing } = useMapContext();
|
|
17
14
|
return (
|
|
18
15
|
<div className="flex items-center gap-x-4 bg-slate-100 p-4 py-2">
|
|
19
16
|
<RouteIcon stopSequence={stopSequence} />
|
|
20
17
|
<RouteInfos className="flex grow flex-col" stopSequence={stopSequence} />
|
|
21
|
-
<
|
|
22
|
-
className={`${isFollowing ? "animate-pulse" : ""}`}
|
|
23
|
-
onClick={() => {
|
|
24
|
-
setIsFollowing(!isFollowing);
|
|
25
|
-
}}
|
|
26
|
-
selected={isFollowing}
|
|
27
|
-
>
|
|
28
|
-
<Tracking />
|
|
29
|
-
</IconButton>
|
|
18
|
+
<RouteFollowingButton />
|
|
30
19
|
</div>
|
|
31
20
|
);
|
|
32
21
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { memo } from "preact/compat";
|
|
2
2
|
import { useEffect, useMemo, useState } from "preact/hooks";
|
|
3
|
+
import { twMerge } from "tailwind-merge";
|
|
3
4
|
|
|
4
5
|
import DebugStop from "../DebugStop/DebugStop";
|
|
5
6
|
import RouteStopDelay from "../RouteStopDelay";
|
|
@@ -30,6 +31,7 @@ export type RouteScheduleStopProps = {
|
|
|
30
31
|
|
|
31
32
|
function RouteStop({
|
|
32
33
|
children,
|
|
34
|
+
className,
|
|
33
35
|
classNameGreyOut = "text-gray-600",
|
|
34
36
|
index,
|
|
35
37
|
invertColor = false,
|
|
@@ -74,7 +76,10 @@ function RouteStop({
|
|
|
74
76
|
<RouteStopContext.Provider value={routeStopState}>
|
|
75
77
|
<button
|
|
76
78
|
// max-h-[58px] because the svg showing the progress is 58px height.
|
|
77
|
-
className={
|
|
79
|
+
className={twMerge(
|
|
80
|
+
`flex max-h-[58px] w-full scroll-mt-[50px] items-stretch rounded text-left hover:bg-slate-100 ${colorScheme}`,
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
78
83
|
data-station-passed={status.isPassed} // Use for auto scroll
|
|
79
84
|
onClick={() => {
|
|
80
85
|
if (stop.coordinate) {
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import { memo } from "preact/compat";
|
|
1
|
+
import { forwardRef, memo } from "preact/compat";
|
|
2
2
|
import { twMerge } from "tailwind-merge";
|
|
3
3
|
|
|
4
4
|
import type { PreactDOMAttributes } from "preact";
|
|
5
|
+
import type { MutableRef } from "preact/hooks";
|
|
5
6
|
|
|
6
|
-
function ShadowOverflow(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
function ShadowOverflow(
|
|
8
|
+
{
|
|
9
|
+
children,
|
|
10
|
+
className,
|
|
11
|
+
...props
|
|
12
|
+
}: { className?: string } & PreactDOMAttributes,
|
|
13
|
+
ref: MutableRef<HTMLDivElement>,
|
|
14
|
+
) {
|
|
11
15
|
return (
|
|
12
|
-
<div
|
|
16
|
+
<div
|
|
17
|
+
{...props}
|
|
18
|
+
className={twMerge("relative overflow-y-auto", className)}
|
|
19
|
+
ref={ref}
|
|
20
|
+
>
|
|
13
21
|
<div className="pointer-events-none sticky top-0 right-0 left-0 h-4 bg-gradient-to-t from-transparent to-white"></div>
|
|
14
22
|
{children}
|
|
15
23
|
<div className="pointer-events-none sticky right-0 bottom-[-1px] left-0 h-4 bg-gradient-to-b from-transparent to-white"></div>
|
|
@@ -17,4 +25,4 @@ function ShadowOverflow({
|
|
|
17
25
|
);
|
|
18
26
|
}
|
|
19
27
|
|
|
20
|
-
export default memo(ShadowOverflow);
|
|
28
|
+
export default memo(forwardRef(ShadowOverflow));
|
|
@@ -13,6 +13,7 @@ import ArrowUp from "../icons/ArrowUp";
|
|
|
13
13
|
import Warning from "../icons/Warning";
|
|
14
14
|
import Link from "../ui/Link";
|
|
15
15
|
import useI18n from "../utils/hooks/useI18n";
|
|
16
|
+
import mergePublications from "../utils/mergePublications";
|
|
16
17
|
|
|
17
18
|
import type { MultilingualTextualContentType } from "mobility-toolbox-js/types";
|
|
18
19
|
|
|
@@ -99,6 +100,8 @@ function SituationDetails({
|
|
|
99
100
|
});
|
|
100
101
|
}) || [];
|
|
101
102
|
|
|
103
|
+
publicationsToDisplay = mergePublications(publicationsToDisplay);
|
|
104
|
+
|
|
102
105
|
// Display the current and next affected time intervals not the one in the past
|
|
103
106
|
timeIntervalsToDisplay =
|
|
104
107
|
(situation?.affectedTimeIntervals || []).filter(
|
|
@@ -112,7 +115,7 @@ function SituationDetails({
|
|
|
112
115
|
|
|
113
116
|
// Display the reasons
|
|
114
117
|
reasonsToDisplay = (situation?.reasons || []).map(({ name }) => {
|
|
115
|
-
return name;
|
|
118
|
+
return name && t(name.replace(".", ""));
|
|
116
119
|
});
|
|
117
120
|
|
|
118
121
|
return (
|
|
@@ -108,7 +108,7 @@ function useLnpLineInfo(text: string): LnpLineInfo {
|
|
|
108
108
|
|
|
109
109
|
return Object.values(linesInfos).find((info) => {
|
|
110
110
|
return ["id", "external_id", "short_name", "long_name"].find((key) => {
|
|
111
|
-
return !!info[key] && info[key] === text;
|
|
111
|
+
return !!info[key] && info[key].toLowerCase() === text.toLowerCase();
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
114
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useLayoutEffect } from "react";
|
|
2
|
+
|
|
3
|
+
import type { MutableRef } from "react";
|
|
4
|
+
|
|
5
|
+
const defaultScrollOptions: ScrollIntoViewOptions = { behavior: "smooth" };
|
|
6
|
+
|
|
7
|
+
function useScrollTo(
|
|
8
|
+
ref: MutableRef<HTMLElement>,
|
|
9
|
+
targetSelector: string,
|
|
10
|
+
dependenciesArray: unknown[] = [],
|
|
11
|
+
scrollToOptions: ScrollIntoViewOptions = defaultScrollOptions,
|
|
12
|
+
) {
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
const elt = ref.current;
|
|
15
|
+
if (!elt) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const scrollToElt = elt.querySelector(targetSelector);
|
|
19
|
+
|
|
20
|
+
if (scrollToElt) {
|
|
21
|
+
try {
|
|
22
|
+
// We use scrollTo avoid scrolling the entire window.
|
|
23
|
+
elt.scrollTo({
|
|
24
|
+
...scrollToOptions,
|
|
25
|
+
top: (scrollToElt as HTMLElement).offsetTop || 0,
|
|
26
|
+
});
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
console.error("Error while scrolling to element", err);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
|
+
}, [...dependenciesArray]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default useScrollTo;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import mergePublications from "./mergePublications";
|
|
2
|
+
|
|
3
|
+
import type { PublicationType } from "mobility-toolbox-js/types";
|
|
4
|
+
|
|
5
|
+
describe("mergePublications", () => {
|
|
6
|
+
it("should merge publications with same publication windows and textual contents", () => {
|
|
7
|
+
const publications = [
|
|
8
|
+
{
|
|
9
|
+
publicationLines: [
|
|
10
|
+
{
|
|
11
|
+
category: "DISRUPTION",
|
|
12
|
+
hasIcon: true,
|
|
13
|
+
lines: [
|
|
14
|
+
{
|
|
15
|
+
name: "510",
|
|
16
|
+
operatorRef: "SBG",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "510",
|
|
20
|
+
operatorRef: "SBG",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "510",
|
|
24
|
+
operatorRef: "SBG",
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
mot: "BUS",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
publicationStops: [],
|
|
31
|
+
publicationWindows: [],
|
|
32
|
+
serviceCondition: "DIVERTED",
|
|
33
|
+
serviceConditionGroup: "CHANGES",
|
|
34
|
+
severity: "NORMAL",
|
|
35
|
+
severityGroup: "NORMAL",
|
|
36
|
+
textualContentLarge: null,
|
|
37
|
+
textualContentMedium: {
|
|
38
|
+
de: {
|
|
39
|
+
consequence: "",
|
|
40
|
+
description:
|
|
41
|
+
"Zwischen <b>Denzlingen Mattenbühl</b> und <b>Denzlingen Bahnhof</b> kommt es vom <b>26. Februar 2026 ab 08:00 Uhr bis auf Weiteres</b> zu <b>einer Umleitung und Haltausfällen</b> wegen einer Baumaßnahme. Betroffen <b>sind die Linien 510 und 512</b>. Es kommt zu folgenden Änderungen:<br>\n <b>Umleitung Linie 510</b>: Die Haltestellen Denzlingen Gewerbegebiet bis Denzlingen Mattenbühl werden nicht bedient. <br><br>Die Änderungen sind in der elektronischen Fahrplanauskunft EFA hinterlegt.",
|
|
42
|
+
durationText: "",
|
|
43
|
+
reason: "",
|
|
44
|
+
recommendation: "",
|
|
45
|
+
summary:
|
|
46
|
+
"Linien 510 und 512: Umleitung und Haltentfall wegen einer Baumaßnahme",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
publicationLines: [
|
|
52
|
+
{
|
|
53
|
+
category: "DISRUPTION",
|
|
54
|
+
hasIcon: true,
|
|
55
|
+
lines: [
|
|
56
|
+
{
|
|
57
|
+
name: "512",
|
|
58
|
+
operatorRef: "SBG",
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
mot: "BUS",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
publicationStops: [],
|
|
65
|
+
publicationWindows: [],
|
|
66
|
+
serviceCondition: "DIVERTED",
|
|
67
|
+
serviceConditionGroup: "CHANGES",
|
|
68
|
+
severity: "NORMAL",
|
|
69
|
+
severityGroup: "NORMAL",
|
|
70
|
+
textualContentLarge: null,
|
|
71
|
+
textualContentMedium: {
|
|
72
|
+
de: {
|
|
73
|
+
consequence: "",
|
|
74
|
+
description:
|
|
75
|
+
"Zwischen <b>Denzlingen Mattenbühl</b> und <b>Denzlingen Bahnhof</b> kommt es vom <b>26. Februar 2026 ab 08:00 Uhr bis auf Weiteres</b> zu <b>einer Umleitung und Haltausfällen</b> wegen einer Baumaßnahme. Betroffen <b>sind die Linien 510 und 512</b>. Es kommt zu folgenden Änderungen:<br>\n <b>Umleitung Linie 510</b>: Die Haltestellen Denzlingen Gewerbegebiet bis Denzlingen Mattenbühl werden nicht bedient. <br><br>Die Änderungen sind in der elektronischen Fahrplanauskunft EFA hinterlegt.",
|
|
76
|
+
durationText: "",
|
|
77
|
+
reason: "",
|
|
78
|
+
recommendation: "",
|
|
79
|
+
summary:
|
|
80
|
+
"Linien 510 und 512: Umleitung und Haltentfall wegen einer Baumaßnahme",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
] as PublicationType[];
|
|
85
|
+
const merged = mergePublications(publications);
|
|
86
|
+
expect(merged).toHaveLength(1);
|
|
87
|
+
expect(merged[0].publicationLines).toHaveLength(2);
|
|
88
|
+
expect(merged[0].publicationLines?.[0].lines[0].name).toBe("510");
|
|
89
|
+
expect(merged[0].publicationLines?.[1].lines[0].name).toBe("512");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { PublicationType } from "mobility-toolbox-js/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* When imported from external source, some publication have the same publication
|
|
5
|
+
* windows and the same textualContents but only differs the lines or stations.
|
|
6
|
+
* In that case we merge them (lines and stops) to avoid duplicates in the display.
|
|
7
|
+
* TODO fixes this in backend oder import
|
|
8
|
+
*/
|
|
9
|
+
const mergePublications = (
|
|
10
|
+
publications: PublicationType[] = [],
|
|
11
|
+
): PublicationType[] => {
|
|
12
|
+
const mergedPublications: PublicationType[] = [];
|
|
13
|
+
try {
|
|
14
|
+
publications.forEach((publication) => {
|
|
15
|
+
const publicationsWindowsStr = JSON.stringify(
|
|
16
|
+
publication.publicationWindows,
|
|
17
|
+
);
|
|
18
|
+
const textualContentStr = JSON.stringify(publication.textualContents);
|
|
19
|
+
|
|
20
|
+
// Find a previous identical publication
|
|
21
|
+
const identicalPublication = mergedPublications.find((p) => {
|
|
22
|
+
return (
|
|
23
|
+
p.severity === publication.severity &&
|
|
24
|
+
p.serviceCondition === publication.serviceCondition &&
|
|
25
|
+
JSON.stringify(p.publicationWindows) === publicationsWindowsStr &&
|
|
26
|
+
JSON.stringify(p.textualContents) === textualContentStr
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (identicalPublication) {
|
|
31
|
+
// We move the lines and stops on the same publication to display them together and avoid duplicates
|
|
32
|
+
identicalPublication.publicationLines = [
|
|
33
|
+
...(identicalPublication.publicationLines || []),
|
|
34
|
+
...(publication.publicationLines || []),
|
|
35
|
+
];
|
|
36
|
+
identicalPublication.publicationStops = [
|
|
37
|
+
...(identicalPublication.publicationStops || []),
|
|
38
|
+
...(publication.publicationStops || []),
|
|
39
|
+
];
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
mergedPublications.push(publication);
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
// JSON parsing could failed, we log the error and return the non merged
|
|
46
|
+
// publications to avoid breaking the display
|
|
47
|
+
console.error("Error merging publications", error);
|
|
48
|
+
return publications;
|
|
49
|
+
}
|
|
50
|
+
return mergedPublications;
|
|
51
|
+
};
|
|
52
|
+
export default mergePublications;
|