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

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,35 +568,41 @@ 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"} />
595
+ <div
596
+ className={twMerge(
597
+ "border-grey relative flex gap-[1px] overflow-hidden rounded-2xl border",
598
+ "*:size-[38px] *:rounded-none *:border-none *:@sm/main:size-[42px] *:@md/main:!size-[46px]",
599
+ "*:first:!rounded-l-2xl",
600
+ "*:last:!rounded-r-2xl",
601
+ isSearchOpen
602
+ ? "@sm:rounded-r-none @sm:border-r-0 @sm:*:last:!rounded-r-none @sm:*:last:border-r-0"
603
+ : "",
581
604
  )}
605
+ >
582
606
  {!hasToolbar && hasPrint && (
583
607
  <RvfExportMenuButton title={"Drucken"} />
584
608
  )}
@@ -596,8 +620,8 @@ function RvfMobilityMap(props: RvfMobilityMapProps) {
596
620
 
597
621
  <div
598
622
  className={twMerge(
599
- "flex w-0 flex-1 resize flex-col overflow-hidden rounded-2xl",
600
- isOverlayDisplayed ? "@lg:w-60" : "p-0",
623
+ "flex w-0 flex-1 flex-col overflow-hidden rounded-2xl",
624
+ isOverlayDisplayed ? "@lg:min-w-60" : "p-0",
601
625
  )}
602
626
  style={{ containerType: "normal" }}
603
627
  >
@@ -617,25 +641,13 @@ function RvfMobilityMap(props: RvfMobilityMapProps) {
617
641
  />
618
642
  </Overlay>
619
643
  </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
644
  </div>
633
645
 
634
646
  {/* Mobile */}
635
647
  <Overlay
636
648
  className={
637
649
  isOverlayDisplayed
638
- ? "absolute bottom-0 z-20 max-h-[70%] min-h-[75px] w-full border-t bg-white @lg:hidden"
650
+ ? "absolute bottom-0 z-20 flex max-h-[70%] min-h-[75px] w-full flex-col border-t bg-white @lg:hidden"
639
651
  : "@lg:hidden"
640
652
  }
641
653
  ScrollableHandlerProps={scrollableHandlerProps}
@@ -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 && (