@geops/rvf-mobility-web-component 0.1.47-beta.0 → 0.1.48

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.
@@ -1,14 +1,16 @@
1
- // import RvfLink from "../../RvfLink";
2
- // import { icons } from "../../utils/addSourceAndLayers";
3
- import Button from "../../ui/Button";
4
- import { LAYER_NAME_NOTIFICATION } from "../../utils/constants";
1
+ import Warning from "../../icons/Warning";
5
2
  import getBgColor from "../../utils/getBgColor";
6
3
  import useI18n from "../../utils/hooks/useI18n";
7
- import useLayerConfig from "../../utils/hooks/useLayerConfig";
8
4
 
5
+ import type {
6
+ AffectedTimeIntervalType,
7
+ PublicationType,
8
+ SituationType,
9
+ TextualContentType,
10
+ } from "mobility-toolbox-js/types";
9
11
  import type { Feature } from "ol";
10
12
 
11
- const toShortDate = (date, showTime) => {
13
+ const toShortDate = (date: Date, showTime, showYear?: boolean) => {
12
14
  const time = date.toLocaleTimeString(["de"], {
13
15
  hour: "2-digit",
14
16
  minute: "2-digit",
@@ -17,6 +19,7 @@ const toShortDate = (date, showTime) => {
17
19
  day: "2-digit",
18
20
  month: "short",
19
21
  weekday: "short",
22
+ year: showYear ? "numeric" : undefined,
20
23
  });
21
24
 
22
25
  return `${dateString}${showTime && showTime !== time ? ` ${time}` : ""}`
@@ -53,8 +56,6 @@ const getLine = (name) => {
53
56
  // html = converter.makeHtml(text);
54
57
  function RvfNotificationDetails({ feature }: { feature: Feature }) {
55
58
  const { t } = useI18n();
56
- const layerConfig = useLayerConfig(LAYER_NAME_NOTIFICATION);
57
-
58
59
  const {
59
60
  affected_products: affectedProducts,
60
61
  affected_time_intervals: timeIntervals,
@@ -62,13 +63,13 @@ function RvfNotificationDetails({ feature }: { feature: Feature }) {
62
63
  description_de: descriptionDe,
63
64
  // title,
64
65
  disruption_type: disruptionType,
65
- publications,
66
66
  // disruption_type: disruptionType,
67
67
  // duration_text_de: durationText,
68
68
  // links,
69
69
  // long_description: description,
70
70
  reason_de: reason,
71
71
  recommendation_de: recommendation,
72
+ situation,
72
73
  summary_de: summary,
73
74
  } = feature.getProperties();
74
75
 
@@ -224,21 +225,21 @@ function RvfNotificationDetails({ feature }: { feature: Feature }) {
224
225
  // })}
225
226
  // {!!products?.length && (
226
227
  // <>
227
- // <div className={"font-bold"}>Betroffene Lines:</div>
228
- // <div className={"flex flex-wrap gap-1"}>
229
- // {products?.map(({ name }) => {
230
- // return (
231
- // <div
232
- // className={
233
- // "rounded-md bg-red px-[12px] py-[9px] font-bold leading-none text-white"
234
- // }
235
- // key={name}
236
- // >
237
- // {name}
238
- // </div>
239
- // );
240
- // })}
241
- // </div>
228
+ // <div className={"font-bold"}>Betroffene Lines:</div>
229
+ // <div className={"flex flex-wrap gap-1"}>
230
+ // {products?.map(({ name }) => {
231
+ // return (
232
+ // <div
233
+ // className={
234
+ // "rounded-md bg-red px-[12px] py-[9px] font-bold leading-none text-white"
235
+ // }
236
+ // key={name}
237
+ // >
238
+ // {name}
239
+ // </div>
240
+ // );
241
+ // })}
242
+ // </div>
242
243
  // </>
243
244
  // )}
244
245
  // </div>
@@ -246,29 +247,184 @@ function RvfNotificationDetails({ feature }: { feature: Feature }) {
246
247
  }
247
248
 
248
249
  // moco export v2
249
- // const textuatlContent = {};
250
+ let textualContent: Partial<TextualContentType> = {};
251
+ let timeIntervalsToDisplay = [];
252
+ let publicationsToDisplay: PublicationType[] = [];
253
+ let reasonsToDisplay: string[] = [];
254
+
250
255
  try {
251
- const publicationsObj = JSON.parse(publications);
252
- console.log(publicationsObj);
256
+ const situationParsed: SituationType = JSON.parse(situation) || {};
257
+ const publicationsArr: PublicationType[] =
258
+ situationParsed?.publications || [];
259
+
260
+ // Find the current publication(s) at the current date
261
+ publicationsToDisplay =
262
+ publicationsArr?.filter(({ publicationWindows }) => {
263
+ return publicationWindows.find(({ endTime, startTime }) => {
264
+ const now = new Date();
265
+ const startT = new Date(startTime);
266
+ const endT = new Date(endTime);
267
+ return startT <= now && now <= endT;
268
+ });
269
+ }) || [];
270
+
271
+ // Display the current and next affected time intervals not the one in the past
272
+ timeIntervalsToDisplay =
273
+ (situationParsed?.affectedTimeIntervals || []).filter(
274
+ ({ endTime, startTime }) => {
275
+ const now = new Date();
276
+ const startT = new Date(startTime);
277
+ const endT = new Date(endTime);
278
+ return (startT <= now && now <= endT) || now < startT;
279
+ },
280
+ ) || [];
281
+
282
+ // Display the reasons
283
+ reasonsToDisplay = (situationParsed?.reasons || []).map(({ name }) => {
284
+ return name;
285
+ });
253
286
  } catch (e) {
254
287
  // eslint-disable-next-line no-console
255
288
  console.error("Failed to parse publications", e);
256
289
  }
290
+
257
291
  return (
258
- <div className="text-base">
259
- <p> Keine Details verfügbar</p>
292
+ <div className="space-y-6 text-base">
293
+ {publicationsToDisplay?.map(
294
+ ({
295
+ id,
296
+ publicationLines,
297
+ publicationStops,
298
+ textualContentLarge,
299
+ textualContentMedium,
300
+ textualContentSmall,
301
+ }) => {
302
+ // Get the textual content in German
303
+ textualContent = (
304
+ textualContentLarge ||
305
+ textualContentMedium ||
306
+ textualContentSmall
307
+ )?.de;
260
308
 
261
- {layerConfig.link && (
262
- <div className={"flex"}>
263
- <Button
264
- className={"text-base"}
265
- href={layerConfig.link.href.replace("{{id}}", feature.get("id"))}
266
- target="_blank"
267
- theme="primary"
268
- >
269
- {layerConfig.link.text || "Mehr erfahren"}
270
- </Button>
271
- </div>
309
+ const lines =
310
+ publicationLines?.flatMap((publication) => {
311
+ return (
312
+ publication.lines?.map(({ name }) => {
313
+ return name;
314
+ }) || []
315
+ );
316
+ }) || [];
317
+
318
+ const stations = publicationStops.map((publication) => {
319
+ return publication?.name || "";
320
+ });
321
+
322
+ return (
323
+ <div className={"text-base"} key={id}>
324
+ <div className="text-xs uppercase">{reasonsToDisplay}</div>
325
+ <h3 className="space-x-2 text-lg font-bold text-balance">
326
+ <span className={"line-height-[1.3] inline-block align-middle"}>
327
+ <Warning />
328
+ </span>
329
+ <span
330
+ dangerouslySetInnerHTML={{
331
+ __html: textualContent?.summary,
332
+ }}
333
+ ></span>
334
+ </h3>
335
+ <hr className="my-1" />
336
+
337
+ {timeIntervalsToDisplay?.map(
338
+ ({
339
+ dailyEndTime = "22:00",
340
+ dailyStartTime = "08:00",
341
+ endTime,
342
+ startTime,
343
+ }: AffectedTimeIntervalType) => {
344
+ const hasDailyTime = dailyEndTime && dailyStartTime;
345
+ const isStartCurrentYear =
346
+ new Date().getFullYear() ===
347
+ new Date(startTime).getFullYear();
348
+ const isEndCurrentYear =
349
+ new Date().getFullYear() ===
350
+ new Date(endTime).getFullYear();
351
+ const isEndInfinite = endTime.includes("2500");
352
+
353
+ return (
354
+ <div
355
+ className="text-sm font-bold text-balance"
356
+ key={startTime}
357
+ >
358
+ <span>
359
+ {`von ${toShortDate(new Date(startTime), !hasDailyTime, !isStartCurrentYear)}`}
360
+ {!isEndInfinite &&
361
+ ` bis ${toShortDate(new Date(endTime), !hasDailyTime, !isEndCurrentYear)}`}
362
+ </span>
363
+ {hasDailyTime && (
364
+ <span>{` (täglich von ${dailyStartTime} bis ${dailyEndTime})`}</span>
365
+ )}
366
+ </div>
367
+ );
368
+ },
369
+ )}
370
+ <div
371
+ className="mt-4"
372
+ dangerouslySetInnerHTML={{
373
+ __html:
374
+ textualContent?.description || "Keine Details verfügbar",
375
+ }}
376
+ />
377
+ {!!lines?.length && (
378
+ <div>
379
+ <br />
380
+ <div className={"font-bold"}>Betroffene Lines:</div>
381
+ <div className={"flex flex-wrap gap-1 text-sm"}>
382
+ {lines?.map((name) => {
383
+ return (
384
+ <div
385
+ className={
386
+ "bg-red rounded-md px-2 py-1 font-bold text-white"
387
+ }
388
+ key={name}
389
+ >
390
+ {name}
391
+ </div>
392
+ );
393
+ })}
394
+ </div>
395
+ </div>
396
+ )}
397
+ <div>
398
+ <br />
399
+ <div className={"font-bold"}>Betroffene Haltestellen:</div>
400
+ <div className={"flex flex-wrap gap-1 text-sm"}>
401
+ {stations?.length ? (
402
+ stations.map((name) => {
403
+ return (
404
+ <div
405
+ className={
406
+ "bg-red rounded-md px-2 py-1 font-bold text-white"
407
+ }
408
+ key={name}
409
+ >
410
+ {name}
411
+ </div>
412
+ );
413
+ })
414
+ ) : (
415
+ <div
416
+ className={
417
+ "bg-red rounded-md px-2 py-1 font-bold text-white"
418
+ }
419
+ >
420
+ Alle Bahnhöfe auf dieser Strecke
421
+ </div>
422
+ )}
423
+ </div>
424
+ </div>
425
+ </div>
426
+ );
427
+ },
272
428
  )}
273
429
  </div>
274
430
  );
@@ -0,0 +1,43 @@
1
+ import { memo } from "preact/compat";
2
+
3
+ import ArrowRight from "../icons/ArrowRight";
4
+ import Button from "../ui/Button";
5
+ import useLayerConfig from "../utils/hooks/useLayerConfig";
6
+ import useRvfContext from "../utils/hooks/useRvfContext";
7
+
8
+ import type { HTMLAttributes, PreactDOMAttributes } from "preact";
9
+
10
+ export type RvfFeatureDetailsFooterProps = {
11
+ onClose?: () => void;
12
+ title?: React.ReactNode;
13
+ } & HTMLAttributes<HTMLDivElement> &
14
+ PreactDOMAttributes;
15
+
16
+ function RvfFeatureDetailsFooter({ ...props }: RvfFeatureDetailsFooterProps) {
17
+ const { selectedFeature } = useRvfContext();
18
+ const layerConfig = useLayerConfig(selectedFeature?.get("layerName"));
19
+ if (!layerConfig?.link || layerConfig?.link?.show === false) {
20
+ return null;
21
+ }
22
+ let id = selectedFeature?.get("id");
23
+ const situation = selectedFeature?.get("situation");
24
+ if (situation) {
25
+ const situationParsed = JSON.parse(situation);
26
+ id = situationParsed?.id || id;
27
+ }
28
+ return (
29
+ <div {...props}>
30
+ <Button
31
+ className={"text-base"}
32
+ href={layerConfig.link.href.replace("{{id}}", id)}
33
+ target="_blank"
34
+ theme="primary"
35
+ >
36
+ <span>{layerConfig.link.text || "Mehr erfahren"}</span>
37
+ <ArrowRight />
38
+ </Button>
39
+ </div>
40
+ );
41
+ }
42
+
43
+ export default memo(RvfFeatureDetailsFooter);
@@ -0,0 +1 @@
1
+ export { default } from "./RvfFeatureDetailsFooter";
@@ -181,9 +181,9 @@ function RvfMobilityMap(props: RvfMobilityMapProps) {
181
181
  embed,
182
182
  geolocation,
183
183
  layers,
184
- layersconfig,
185
184
  layertree,
186
185
  mainlink,
186
+ mainlinktitle,
187
187
  notification,
188
188
  permalink,
189
189
  print,
@@ -193,7 +193,6 @@ function RvfMobilityMap(props: RvfMobilityMapProps) {
193
193
  tenant,
194
194
  toolbar,
195
195
  } = props;
196
- console.log("RvfMobilityMap props:", mainlink, layersconfig);
197
196
 
198
197
  // Convert string boolean to boolean
199
198
  const hasToolbar = useMemo(() => {
@@ -527,6 +526,19 @@ function RvfMobilityMap(props: RvfMobilityMapProps) {
527
526
  title={RVF_LAYERS_TITLES.sharedMobility}
528
527
  />
529
528
 
529
+ {mainlink && (
530
+ <IconButton
531
+ className={
532
+ "absolute inset-x-2 bottom-8 z-10 rounded-xl border-3 border-white"
533
+ }
534
+ href={mainlink}
535
+ target="_blank"
536
+ theme="primary"
537
+ title={mainlinktitle}
538
+ >
539
+ <LiaMapSolid />
540
+ </IconButton>
541
+ )}
530
542
  <div className="pointer-events-none absolute inset-x-2 bottom-2 z-10 flex items-end justify-between gap-2 text-[10px]">
531
543
  <ScaleLine className="bg-slate-50/70" />
532
544
  <Copyright
@@ -535,6 +547,12 @@ function RvfMobilityMap(props: RvfMobilityMapProps) {
535
547
  />
536
548
  </div>
537
549
 
550
+ <div className="absolute top-2 right-2 z-10 flex">
551
+ {hasGeolocation && (
552
+ <GeolocationButton title={"Geolokalisierung"} />
553
+ )}
554
+ </div>
555
+
538
556
  <div className="absolute right-2 bottom-10 z-10 flex flex-col justify-between gap-2">
539
557
  <RvfZoomButtons />
540
558
 
@@ -550,54 +568,54 @@ function RvfMobilityMap(props: RvfMobilityMapProps) {
550
568
  </div>
551
569
  </Map>
552
570
 
553
- <div className="pointer-events-none absolute top-2 bottom-8 left-2 z-10 flex flex-col gap-2 *:pointer-events-auto">
554
- <div className={"relative z-0"}>
571
+ <div className="pointer-events-none absolute top-2 bottom-2 left-2 z-10 flex flex-col gap-2 *:pointer-events-auto">
572
+ <div
573
+ className={
574
+ "relative z-10 w-fit rounded-2xl bg-black/10 p-0 backdrop-blur-sm"
575
+ }
576
+ // className="w-fit rounded-2xl bg-black/10 p-1 backdrop-blur-sm">
577
+ >
555
578
  <div
556
579
  className={twMerge(
557
- "absolute top-[54px] left-0 h-[40px] w-0 p-0 opacity-0 transition-all @sm/main:top-2 @sm/main:left-[calc(100%-43px)] @md/main:left-[calc(100%-47px)]",
580
+ "absolute top-12 left-0 h-[40px] w-0 p-0 opacity-0 transition-all @sm:top-0 @sm:left-[calc(100%-43px)] @md:left-[calc(100%-47px)]",
558
581
  isSearchOpen ? "w-64 opacity-100" : "",
559
582
  )}
560
583
  >
561
584
  <Search
562
585
  className={
563
- "border-grey @container m-0 h-[40px] rounded-2xl border p-2 px-4 text-base @sm/main:h-[44px] @md/main:h-[48px]"
586
+ "border-grey @container m-0 h-[40px] rounded-2xl border p-2 px-4 text-base @sm/main:h-[44px] @sm/main:rounded-l-none @sm/main:rounded-r-2xl @md/main:h-[48px]"
564
587
  }
565
588
  inputClassName="h-6 text-base"
589
+ inputContainerClassName="border-none"
566
590
  resultClassName="text-base **:hover:cursor-pointer hover:text-red-500 p-2"
567
591
  resultsContainerClassName="@container rounded-b-2xl max-h-[200px] overflow-y-auto border border-grey border-t-0 "
568
- withResultsClassName="text-base rounded-b-none border-b-lightgrey"
592
+ withResultsClassName="text-base !rounded-b-none"
569
593
  />
570
594
  </div>
571
- </div>
572
- <div
573
- className={
574
- "relative z-10 w-fit rounded-2xl bg-black/10 p-0 backdrop-blur-sm"
575
- }
576
- // className="w-fit rounded-2xl bg-black/10 p-1 backdrop-blur-sm">
577
- >
578
- <div className="border-grey relative flex gap-[1px] overflow-hidden rounded-2xl border *:size-[38px] *:rounded-none *:border-white first:!rounded-l-2xl last:!rounded-r-2xl *:@sm/main:size-[42px] *:@md/main:!size-[46px]">
579
- {hasGeolocation && (
580
- <GeolocationButton title={"Geolokalisierung"} />
581
- )}
582
- {!hasToolbar && hasPrint && (
583
- <RvfExportMenuButton title={"Drucken"} />
584
- )}
585
- {!hasToolbar && hasShare && (
586
- <RvfShareMenuButton title={"Share"} />
587
- )}
588
- {!hasToolbar && hasLayerTree && (
589
- <RvfLayerTreeButton title={"Layers"} />
590
- )}
591
- {!hasToolbar && hasSearch && (
592
- <RvfSearchButton title={"Suche"} />
593
- )}
594
- </div>
595
+ {hasToolbar && (
596
+ <div
597
+ className={twMerge(
598
+ "border-grey relative flex gap-[1px] overflow-hidden rounded-2xl border",
599
+ "*:size-[38px] *:rounded-none *:border-none *:@sm/main:size-[42px] *:@md/main:!size-[46px]",
600
+ "*:first:!rounded-l-2xl",
601
+ "*:last:!rounded-r-2xl",
602
+ isSearchOpen
603
+ ? "@sm:rounded-r-none @sm:border-r-0 @sm:*:last:!rounded-r-none @sm:*:last:border-r-0"
604
+ : "",
605
+ )}
606
+ >
607
+ {hasPrint && <RvfExportMenuButton title={"Drucken"} />}
608
+ {hasShare && <RvfShareMenuButton title={"Share"} />}
609
+ {hasLayerTree && <RvfLayerTreeButton title={"Layers"} />}
610
+ {hasSearch && <RvfSearchButton title={"Suche"} />}
611
+ </div>
612
+ )}
595
613
  </div>
596
614
 
597
615
  <div
598
616
  className={twMerge(
599
- "flex w-0 flex-1 resize flex-col overflow-hidden rounded-2xl",
600
- isOverlayDisplayed ? "@lg:w-60" : "p-0",
617
+ "flex w-0 flex-1 flex-col overflow-hidden rounded-2xl",
618
+ isOverlayDisplayed ? "@lg:min-w-60" : "p-0",
601
619
  )}
602
620
  style={{ containerType: "normal" }}
603
621
  >
@@ -617,25 +635,13 @@ function RvfMobilityMap(props: RvfMobilityMapProps) {
617
635
  />
618
636
  </Overlay>
619
637
  </div>
620
-
621
- {mainlink && (
622
- <IconButton
623
- className={"pointer-events-auto"}
624
- href={mainlink}
625
- target="_blank"
626
- theme="primary"
627
- title="Hauptseite"
628
- >
629
- <LiaMapSolid />
630
- </IconButton>
631
- )}
632
638
  </div>
633
639
 
634
640
  {/* Mobile */}
635
641
  <Overlay
636
642
  className={
637
643
  isOverlayDisplayed
638
- ? "absolute bottom-0 z-20 max-h-[70%] min-h-[75px] w-full border-t bg-white @lg:hidden"
644
+ ? "absolute bottom-0 z-20 flex max-h-[70%] min-h-[75px] w-full flex-col border-t bg-white @lg:hidden"
639
645
  : "@lg:hidden"
640
646
  }
641
647
  ScrollableHandlerProps={scrollableHandlerProps}
@@ -650,7 +656,7 @@ function RvfMobilityMap(props: RvfMobilityMapProps) {
650
656
  />
651
657
  </Overlay>
652
658
 
653
- {hasToolbar && (
659
+ {/* {hasToolbar && (
654
660
  <div
655
661
  className={
656
662
  "z-[100] flex justify-around overflow-x-hidden border-t bg-white p-1 *:border-none @lg/main:block @lg/main:border-t-0 @lg/main:border-r @lg/main:p-0"
@@ -677,7 +683,7 @@ function RvfMobilityMap(props: RvfMobilityMapProps) {
677
683
  />
678
684
  )}
679
685
  </div>
680
- )}
686
+ )} */}
681
687
 
682
688
  {/* Modal display */}
683
689
  {/* {!hasToolbar && hasPrint && isExportMenuOpen && (
@@ -3,6 +3,7 @@ import { twMerge } from "tailwind-merge";
3
3
  import RouteSchedule from "../RouteSchedule";
4
4
  import RvfExportMenu from "../RvfExportMenu";
5
5
  import RvfFeatureDetails from "../RvfFeatureDetails";
6
+ import RvfFeatureDetailsFooter from "../RvfFeatureDetailsFooter";
6
7
  import RvfFeatureDetailsTitle from "../RvfFeatureDetailsTitle/RvfFeatureDetailsTitle";
7
8
  import RvfOverlayHeader from "../RvfOverlayHeader";
8
9
  import RvfShare from "../RvfShare";
@@ -76,8 +77,9 @@ function RvfOverlayContent({
76
77
  title={<RvfFeatureDetailsTitle feature={selectedFeature} />}
77
78
  ></RvfOverlayHeader>
78
79
  <RvfFeatureDetails
79
- className={twMerge(contentClassName, "flex flex-col gap-4 p-4")}
80
+ className={twMerge(contentClassName, "relative")}
80
81
  />
82
+ <RvfFeatureDetailsFooter className={"flex flex-row p-4 pt-2"} />
81
83
  </>
82
84
  )}
83
85
  {hasPrint && isExportMenuOpen && (