@geops/rvf-mobility-web-component 0.1.29 → 0.1.31
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 +26 -0
- package/docutils.js +10 -1
- package/index.html +23 -10
- package/index.js +96446 -1064
- package/package.json +2 -1
- package/src/NotificationLayer/NotificationLayer.tsx +32 -2
- package/src/NotificationLayer/notificationUtils.ts +4 -0
- package/src/RvfFeatureDetails/RvfFeatureDetails.tsx +19 -4
- package/src/RvfFeatureDetails/RvfLineNetworkDetails/RvfLineNetworkDetails.tsx +98 -0
- package/src/RvfFeatureDetails/RvfLineNetworkDetails/index.js +1 -0
- package/src/RvfFeatureDetails/RvfNotificationDetails/RvfNotificationDetails.tsx +77 -0
- package/src/RvfFeatureDetails/RvfNotificationDetails/index.ts +1 -0
- package/src/RvfFeatureDetails/{RVFSellingPointDetails/RVFSellingPointDetails.tsx → RvfSellingPointDetails/RvfSellingPointDetails.tsx} +2 -2
- package/src/RvfFeatureDetails/RvfSellingPointDetails/index.js +1 -0
- package/src/RvfLineNetworkPlanLayer/RvfLineNetworkPlanLayer.tsx +3 -2
- package/src/RvfMobilityMap/RvfMobilityMap.tsx +5 -7
- package/src/RvfSelectedFeatureHighlightLayer/RvfSelectedFeatureHighlightLayer.tsx +9 -1
- package/src/utils/getFeatureInformationTitle.ts +4 -0
- package/tailwind.config.mjs +1 -1
- package/src/RvfFeatureDetails/RVFSellingPointDetails/index.js +0 -1
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.31",
|
|
6
6
|
"homepage": "https://rvf-mobility-web-component-geops.vercel.app/",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "index.js",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"react-icons": "^5.4.0",
|
|
22
22
|
"react-spatial": "^1.12.2",
|
|
23
23
|
"rosetta": "^1.1.0",
|
|
24
|
+
"showdown": "^2.1.0",
|
|
24
25
|
"tailwind-merge": "^2.6.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { MaplibreStyleLayer } from "mobility-toolbox-js/ol";
|
|
2
|
+
import { MaplibreStyleLayerOptions } from "mobility-toolbox-js/ol/layers/MaplibreStyleLayer";
|
|
1
3
|
import { useEffect, useMemo, useState } from "preact/hooks";
|
|
2
4
|
|
|
3
5
|
import useMapContext from "../utils/hooks/useMapContext";
|
|
@@ -86,7 +88,8 @@ const useNotifications = () => {
|
|
|
86
88
|
abortCtrl = new AbortController();
|
|
87
89
|
const response = await fetch(url, { signal: abortCtrl.signal });
|
|
88
90
|
const data = await response.json();
|
|
89
|
-
|
|
91
|
+
const notifications = getNotificationsWithStatus(data, now);
|
|
92
|
+
setNotifications(notifications);
|
|
90
93
|
setShouldAddPreviewNotifications(true);
|
|
91
94
|
};
|
|
92
95
|
|
|
@@ -150,7 +153,34 @@ const useNotifications = () => {
|
|
|
150
153
|
return notifications;
|
|
151
154
|
};
|
|
152
155
|
|
|
153
|
-
export default function NotificationLayer() {
|
|
156
|
+
export default function NotificationLayer(props: MaplibreStyleLayerOptions) {
|
|
154
157
|
useNotifications();
|
|
158
|
+
const { baseLayer, map } = useMapContext();
|
|
159
|
+
|
|
160
|
+
const layer = useMemo(() => {
|
|
161
|
+
if (!baseLayer) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return new MaplibreStyleLayer({
|
|
165
|
+
isQueryable: true,
|
|
166
|
+
layersFilter: ({ metadata }) => {
|
|
167
|
+
return metadata?.["general.filter"] === "notifications";
|
|
168
|
+
},
|
|
169
|
+
maplibreLayer: baseLayer,
|
|
170
|
+
...(props || {}),
|
|
171
|
+
});
|
|
172
|
+
}, [baseLayer, props]);
|
|
173
|
+
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (!map || !layer) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
map.addLayer(layer);
|
|
179
|
+
|
|
180
|
+
return () => {
|
|
181
|
+
map.removeLayer(layer);
|
|
182
|
+
};
|
|
183
|
+
}, [map, layer]);
|
|
184
|
+
|
|
155
185
|
return null;
|
|
156
186
|
}
|
|
@@ -142,6 +142,7 @@ const addNotificationsLayers = (
|
|
|
142
142
|
features,
|
|
143
143
|
type: "FeatureCollection",
|
|
144
144
|
},
|
|
145
|
+
generateId: true,
|
|
145
146
|
type: "geojson",
|
|
146
147
|
},
|
|
147
148
|
[
|
|
@@ -154,6 +155,9 @@ const addNotificationsLayers = (
|
|
|
154
155
|
],
|
|
155
156
|
id: "notificationsActive",
|
|
156
157
|
layout: { visibility: "visible" },
|
|
158
|
+
metadata: {
|
|
159
|
+
"general.filter": "notifications",
|
|
160
|
+
},
|
|
157
161
|
paint: {
|
|
158
162
|
"line-color": "rgba(255,0,0,1)",
|
|
159
163
|
"line-dasharray": [2, 2],
|
|
@@ -3,7 +3,9 @@ import type { JSX, PreactDOMAttributes } from "preact";
|
|
|
3
3
|
import { memo } from "preact/compat";
|
|
4
4
|
|
|
5
5
|
import useRvfContext from "../utils/hooks/useRvfContext";
|
|
6
|
-
import
|
|
6
|
+
import RvfLineNetworkDetails from "./RvfLineNetworkDetails/RvfLineNetworkDetails";
|
|
7
|
+
import RvfNotificationDetails from "./RvfNotificationDetails";
|
|
8
|
+
import RvfSellingPointDetails from "./RvfSellingPointDetails";
|
|
7
9
|
import RvfSharedMobilityDetails from "./RvfSharedMobilityDetail";
|
|
8
10
|
|
|
9
11
|
export type RvfFeatureDetailsProps = JSX.HTMLAttributes<HTMLDivElement> &
|
|
@@ -20,9 +22,11 @@ const getIsSharedMobility = (selectedFeature): boolean => {
|
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
function RvfFeatureDetails(props: RvfFeatureDetailsProps) {
|
|
23
|
-
const { selectedFeature } = useRvfContext();
|
|
25
|
+
const { selectedFeature, selectedFeatures } = useRvfContext();
|
|
24
26
|
const isSharedMobility = getIsSharedMobility(selectedFeature);
|
|
25
27
|
const isSellingPoint = !!selectedFeature.get("tickets");
|
|
28
|
+
const isNotification = !!selectedFeature.get("disruption_type");
|
|
29
|
+
const isLineNetwork = !!selectedFeature.get("original_line_id");
|
|
26
30
|
|
|
27
31
|
const showDefaultData = () => {
|
|
28
32
|
return Object.entries(selectedFeature.getProperties()).map(
|
|
@@ -41,11 +45,22 @@ function RvfFeatureDetails(props: RvfFeatureDetailsProps) {
|
|
|
41
45
|
|
|
42
46
|
return (
|
|
43
47
|
<div {...props}>
|
|
44
|
-
{isSellingPoint && <
|
|
48
|
+
{isSellingPoint && <RvfSellingPointDetails feature={selectedFeature} />}
|
|
45
49
|
{isSharedMobility && (
|
|
46
50
|
<RvfSharedMobilityDetails selectedFeature={selectedFeature} />
|
|
47
51
|
)}
|
|
48
|
-
{
|
|
52
|
+
{isNotification && <RvfNotificationDetails feature={selectedFeature} />}
|
|
53
|
+
{isLineNetwork && (
|
|
54
|
+
<RvfLineNetworkDetails
|
|
55
|
+
feature={selectedFeature}
|
|
56
|
+
features={selectedFeatures}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
{!isLineNetwork &&
|
|
60
|
+
!isNotification &&
|
|
61
|
+
!isSharedMobility &&
|
|
62
|
+
!isSellingPoint &&
|
|
63
|
+
showDefaultData()}
|
|
49
64
|
</div>
|
|
50
65
|
);
|
|
51
66
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Feature } from "ol";
|
|
2
|
+
import { useEffect, useMemo, useState } from "preact/hooks";
|
|
3
|
+
|
|
4
|
+
import useMapContext from "../../utils/hooks/useMapContext";
|
|
5
|
+
|
|
6
|
+
let cacheLineInfosById = null;
|
|
7
|
+
|
|
8
|
+
function RvfLineNetworkDetails({
|
|
9
|
+
feature,
|
|
10
|
+
features,
|
|
11
|
+
}: {
|
|
12
|
+
feature: Feature;
|
|
13
|
+
features: Feature[];
|
|
14
|
+
}) {
|
|
15
|
+
const { apikey, mapsurl } = useMapContext();
|
|
16
|
+
const [lineInfos, setLineInfos] = useState(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const fetchInfos = async () => {
|
|
19
|
+
if (!cacheLineInfosById) {
|
|
20
|
+
const response = await fetch(
|
|
21
|
+
`${mapsurl}/data/network_plans_rvf_prototype.json?key=${apikey}`,
|
|
22
|
+
);
|
|
23
|
+
const data = await response.json();
|
|
24
|
+
cacheLineInfosById = data["geops.lnp.lines"];
|
|
25
|
+
}
|
|
26
|
+
setLineInfos(cacheLineInfosById);
|
|
27
|
+
};
|
|
28
|
+
fetchInfos();
|
|
29
|
+
}, [apikey, mapsurl]);
|
|
30
|
+
|
|
31
|
+
const lineNetworkIdsByOperator = useMemo(() => {
|
|
32
|
+
const byOperators = {};
|
|
33
|
+
|
|
34
|
+
[
|
|
35
|
+
...new Set(
|
|
36
|
+
features.map((f) => {
|
|
37
|
+
return f.get("original_line_id");
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
]
|
|
41
|
+
.filter((id) => {
|
|
42
|
+
return !!id && !!lineInfos?.[id];
|
|
43
|
+
})
|
|
44
|
+
.map((id) => {
|
|
45
|
+
const { operator_name: operatorName } = lineInfos[id];
|
|
46
|
+
if (!byOperators[operatorName]) {
|
|
47
|
+
byOperators[operatorName] = [];
|
|
48
|
+
}
|
|
49
|
+
byOperators[operatorName].push(lineInfos[id]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return byOperators;
|
|
53
|
+
}, [features, lineInfos]);
|
|
54
|
+
|
|
55
|
+
if (!feature || !lineInfos) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="flex flex-col gap-4">
|
|
61
|
+
{Object.entries(lineNetworkIdsByOperator).map(
|
|
62
|
+
([operatorName, linesInfos]) => {
|
|
63
|
+
return (
|
|
64
|
+
<div className={"flex flex-col gap-2"} key={operatorName}>
|
|
65
|
+
<div>{operatorName}</div>
|
|
66
|
+
{linesInfos.map((lineInfo) => {
|
|
67
|
+
const {
|
|
68
|
+
// color,
|
|
69
|
+
// external_id,
|
|
70
|
+
long_name: longName,
|
|
71
|
+
short_name: shortName,
|
|
72
|
+
// text_color,
|
|
73
|
+
} = lineInfo;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className={"flex items-center gap-2"} key={shortName}>
|
|
77
|
+
<div>
|
|
78
|
+
<div
|
|
79
|
+
className={
|
|
80
|
+
"rounded-md bg-red px-[12px] py-[9px] font-bold leading-none text-white"
|
|
81
|
+
}
|
|
82
|
+
>
|
|
83
|
+
{shortName}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div>{longName}</div>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default RvfLineNetworkDetails;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./RvfLineNetworkDetails";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Feature } from "ol";
|
|
2
|
+
import showdown from "showdown";
|
|
3
|
+
|
|
4
|
+
import RvfLink from "../../RvfLink";
|
|
5
|
+
const converter = new showdown.Converter();
|
|
6
|
+
// text = "# hello, markdown!",
|
|
7
|
+
// html = converter.makeHtml(text);
|
|
8
|
+
function RvfNotificationDetails({ feature }: { feature: Feature }) {
|
|
9
|
+
const {
|
|
10
|
+
affected_products: affectedProducts,
|
|
11
|
+
affected_time_intervals: timeIntervals,
|
|
12
|
+
links,
|
|
13
|
+
long_description: description,
|
|
14
|
+
title,
|
|
15
|
+
} = feature.getProperties();
|
|
16
|
+
|
|
17
|
+
let end = "",
|
|
18
|
+
start = "";
|
|
19
|
+
let products = [];
|
|
20
|
+
let externalLinks = [];
|
|
21
|
+
try {
|
|
22
|
+
const timeInterval = JSON.parse(timeIntervals)?.[0] || {};
|
|
23
|
+
const dateStart = new Date(timeInterval.start);
|
|
24
|
+
start =
|
|
25
|
+
dateStart.toLocaleDateString() + " " + dateStart.toLocaleTimeString();
|
|
26
|
+
const dateEnd = new Date(timeInterval.end);
|
|
27
|
+
end = dateEnd.toLocaleDateString() + " " + dateEnd.toLocaleTimeString();
|
|
28
|
+
|
|
29
|
+
products = JSON.parse(affectedProducts);
|
|
30
|
+
products.sort((a, b) => {
|
|
31
|
+
return a.name.localeCompare(b.name);
|
|
32
|
+
});
|
|
33
|
+
externalLinks = JSON.parse(links);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.error(e);
|
|
36
|
+
}
|
|
37
|
+
return (
|
|
38
|
+
<div className={"flex flex-col gap-4 text-sm"}>
|
|
39
|
+
<div>
|
|
40
|
+
<div className="text-base font-bold">{title}</div>
|
|
41
|
+
<div className="text-xs">
|
|
42
|
+
{start} - {end}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div
|
|
46
|
+
className={"flex flex-col gap-2"}
|
|
47
|
+
dangerouslySetInnerHTML={{
|
|
48
|
+
__html: converter.makeHtml(description).replace("<hr />", "<br />"),
|
|
49
|
+
}}
|
|
50
|
+
></div>
|
|
51
|
+
{externalLinks?.map(({ label, uri }) => {
|
|
52
|
+
return (
|
|
53
|
+
<RvfLink href={uri} key={uri}>
|
|
54
|
+
{label}
|
|
55
|
+
</RvfLink>
|
|
56
|
+
);
|
|
57
|
+
})}
|
|
58
|
+
<div className={"font-bold"}>Betroffened Lines:</div>
|
|
59
|
+
<div className={"flex flex-wrap gap-1"}>
|
|
60
|
+
{products?.map(({ name }) => {
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
className={
|
|
64
|
+
"rounded-md bg-red px-[12px] py-[9px] font-bold leading-none text-white"
|
|
65
|
+
}
|
|
66
|
+
key={name}
|
|
67
|
+
>
|
|
68
|
+
{name}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default RvfNotificationDetails;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./RvfNotificationDetails";
|
|
@@ -10,7 +10,7 @@ const ICON_BY_OPERATED_BY = {
|
|
|
10
10
|
Videozentrum: <Video />,
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
function
|
|
13
|
+
function RvfSellingPointDetails({ feature }: { feature: Feature }) {
|
|
14
14
|
if (!feature) {
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
@@ -44,4 +44,4 @@ function RVFSellingPointDetails({ feature }: { feature: Feature }) {
|
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export default
|
|
47
|
+
export default RvfSellingPointDetails;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./RvfSellingPointDetails";
|
|
@@ -16,7 +16,6 @@ function RvfLineNetworkPlanLayer(props: MaplibreStyleLayerOptions) {
|
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
18
|
return new MaplibreStyleLayer({
|
|
19
|
-
isQueryable: true,
|
|
20
19
|
layersFilter: ({ metadata }) => {
|
|
21
20
|
return (
|
|
22
21
|
metadata?.["rvf.filter"] === "netzplan_lines" ||
|
|
@@ -24,8 +23,10 @@ function RvfLineNetworkPlanLayer(props: MaplibreStyleLayerOptions) {
|
|
|
24
23
|
);
|
|
25
24
|
},
|
|
26
25
|
maplibreLayer: baseLayer,
|
|
27
|
-
minZoom: 10,
|
|
28
26
|
name: RVF_LAYERS_NAMES.liniennetz,
|
|
27
|
+
queryRenderedLayersFilter: ({ metadata }) => {
|
|
28
|
+
return metadata?.["rvf.filter"] === "netzplan_trips_info";
|
|
29
|
+
},
|
|
29
30
|
...(props || {}),
|
|
30
31
|
});
|
|
31
32
|
}, [baseLayer, props]);
|
|
@@ -117,9 +117,7 @@ const realtimeLayerProps = {
|
|
|
117
117
|
{ properties: { delay: delayA, type: typeA } },
|
|
118
118
|
{ properties: { delay: delayB, type: typeB } },
|
|
119
119
|
) => {
|
|
120
|
-
// console.log(trajA, trajB);
|
|
121
120
|
if (typeA !== typeB) {
|
|
122
|
-
// console.log(trajA.properties.type, trajB.properties.type);
|
|
123
121
|
if (PRIORITY_FROM_TYPE[typeA] < PRIORITY_FROM_TYPE[typeB]) {
|
|
124
122
|
return -1;
|
|
125
123
|
}
|
|
@@ -160,8 +158,8 @@ function RvfMobilityMap({
|
|
|
160
158
|
layers = null,
|
|
161
159
|
layertree = "true",
|
|
162
160
|
// mapsurl = "https://style-review.geops.io",
|
|
163
|
-
mapsurl = "https://maps.style-dev.geops.io",
|
|
164
|
-
|
|
161
|
+
// mapsurl = "https://maps.style-dev.geops.io",
|
|
162
|
+
mapsurl = "https://maps.geops.io",
|
|
165
163
|
maxextent = bbox,
|
|
166
164
|
maxzoom = "20",
|
|
167
165
|
minzoom = null,
|
|
@@ -520,7 +518,7 @@ function RvfMobilityMap({
|
|
|
520
518
|
<MapContext.Provider value={mapContextValue}>
|
|
521
519
|
<RvfContext.Provider value={rvfContextValue}>
|
|
522
520
|
<div
|
|
523
|
-
className="relative size-full border font-sans
|
|
521
|
+
className="relative size-full overflow-hidden rounded-[16px] border font-sans @container/main"
|
|
524
522
|
ref={eventNodeRef}
|
|
525
523
|
style={styleProps}
|
|
526
524
|
>
|
|
@@ -549,7 +547,7 @@ function RvfMobilityMap({
|
|
|
549
547
|
title={RVF_LAYERS_TITLES.verkaufsstellen}
|
|
550
548
|
/>
|
|
551
549
|
<RvfLineNetworkPlanLayer
|
|
552
|
-
isQueryable={
|
|
550
|
+
isQueryable={true}
|
|
553
551
|
minZoom={10}
|
|
554
552
|
title={RVF_LAYERS_TITLES.liniennetz}
|
|
555
553
|
/>
|
|
@@ -666,7 +664,7 @@ function RvfMobilityMap({
|
|
|
666
664
|
{hasToolbar && (
|
|
667
665
|
<div
|
|
668
666
|
className={
|
|
669
|
-
"z-[100] flex justify-around overflow-x-hidden border-t bg-white p-1 @lg/main:block @lg/main:border-r @lg/main:p-0 "
|
|
667
|
+
"z-[100] flex justify-around overflow-x-hidden border-t bg-white p-1 @lg/main:block @lg/main:border-r @lg/main:border-t-0 @lg/main:p-0 "
|
|
670
668
|
}
|
|
671
669
|
>
|
|
672
670
|
{hasLayerTree && (
|
|
@@ -61,7 +61,15 @@ function SingleClickListener() {
|
|
|
61
61
|
feature.set("num_vehicles_available", feature.get("point_count"));
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
if (!feature.get("disruption_type")) {
|
|
65
|
+
try {
|
|
66
|
+
const wrote = geojson.writeFeatureObject(feature);
|
|
67
|
+
source?.setData(wrote);
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// console.error(e);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
|
|
@@ -13,11 +13,15 @@ const getFeatureInformationTitle = (feature: Feature) => {
|
|
|
13
13
|
const selectedFeature = features?.[0] || feature;
|
|
14
14
|
const {
|
|
15
15
|
category,
|
|
16
|
+
disruption_type: disruptionType,
|
|
16
17
|
feed_id: feedId,
|
|
17
18
|
long_name: longName,
|
|
18
19
|
tickets,
|
|
19
20
|
} = selectedFeature.getProperties();
|
|
20
21
|
|
|
22
|
+
if (disruptionType) {
|
|
23
|
+
return "MOCO Meldung";
|
|
24
|
+
}
|
|
21
25
|
if (category) {
|
|
22
26
|
return TITLE_BY_CATEGORY[category] || defaultTitle;
|
|
23
27
|
}
|
package/tailwind.config.mjs
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default } from "./RVFSellingPointDetails";
|