iticket-seatingplan-dev 1.5.2 → 1.5.4

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.
@@ -0,0 +1,756 @@
1
+ import "leaflet-gesture-handling/dist/leaflet-gesture-handling.css";
2
+ import "leaflet-draw/dist/leaflet.draw.css";
3
+ import React, { SetStateAction, useEffect, useRef, useState } from "react";
4
+ import {
5
+ Circle,
6
+ ImageOverlay,
7
+ Popup,
8
+ Tooltip,
9
+ useMap,
10
+ FeatureGroup,
11
+ useMapEvents,
12
+ } from "react-leaflet";
13
+ import * as L from "leaflet";
14
+ import { EditControl } from "react-leaflet-draw";
15
+ import { GestureHandling } from "leaflet-gesture-handling";
16
+ import Control from "react-leaflet-custom-control";
17
+ import {
18
+ expandIcon,
19
+ closeIcon,
20
+ userIcon,
21
+ wheelchairIcon,
22
+ collapseIcon,
23
+ } from "./assets/encodedSvgs";
24
+ import { statuses, getInitialColor, modes } from "../utils";
25
+ import { ChosenSeat, Mode, Price, Seat, SeatData } from "../utils/types";
26
+ import PriceButton from "./PriceButton";
27
+ import Flexi from "./Flexi";
28
+ import CloseIcon from "./icons/CloseIcon";
29
+ import TicketIcon from "./icons/TicketIcon";
30
+ import FlexiIcon from "./icons/FlexiIcon";
31
+
32
+ interface Props {
33
+ seats: SeatData;
34
+ height: number;
35
+ width: number;
36
+ bounds: L.LatLngBounds;
37
+ handleClickSeat: (e: L.LeafletMouseEvent, s: Seat) => void;
38
+ ticketPopupOpen: boolean;
39
+ setTicketPopupOpen: React.Dispatch<SetStateAction<boolean>>;
40
+ chosenSeat?: ChosenSeat;
41
+ bookedSeats: Seat[];
42
+ addTicketToCart: (seat: Seat, price: Price, cs?: ChosenSeat) => void;
43
+ batchRemoveTicketsFromCart: (seats: Seat[]) => void;
44
+ isReloading: boolean;
45
+ initialFetch: (isReload?: boolean) => void;
46
+ isFullScreen: boolean;
47
+ setIsFullScreen: React.Dispatch<SetStateAction<boolean>>;
48
+ priceSectionIds?: number[];
49
+ areaName?: string;
50
+ price?: number;
51
+ canMultiSelect?: boolean;
52
+ mode: Mode;
53
+ setMode: React.Dispatch<SetStateAction<Mode>>;
54
+ pricingPopupOpen: boolean;
55
+ setPricingPopupOpen: React.Dispatch<SetStateAction<boolean>>;
56
+ selectedSeats: Seat[];
57
+ setSelectedSeats: React.Dispatch<SetStateAction<Seat[]>>;
58
+ }
59
+
60
+ export default function Map({
61
+ seats,
62
+ height,
63
+ width,
64
+ bounds,
65
+ handleClickSeat,
66
+ ticketPopupOpen,
67
+ setTicketPopupOpen,
68
+ chosenSeat,
69
+ bookedSeats,
70
+ addTicketToCart,
71
+ batchRemoveTicketsFromCart,
72
+ isReloading,
73
+ initialFetch,
74
+ isFullScreen,
75
+ setIsFullScreen,
76
+ priceSectionIds,
77
+ areaName,
78
+ price,
79
+ canMultiSelect,
80
+ mode,
81
+ setMode,
82
+ pricingPopupOpen,
83
+ setPricingPopupOpen,
84
+ selectedSeats,
85
+ setSelectedSeats,
86
+ }: Props) {
87
+ const [isLegendOpen, setIsLegendOpen] = useState(false);
88
+ const [isDragging, setIsDragging] = useState(false);
89
+ const drawLayersRef = useRef(null);
90
+ const drawRef = useRef(null);
91
+ const popupRef = useRef(null);
92
+
93
+ const map = useMap();
94
+
95
+ L.drawLocal.draw.toolbar.buttons.rectangle = "Box select";
96
+
97
+ useMapEvents({
98
+ mouseover: () => {
99
+ if (mode === modes.DRAG || mode === modes.DRAW || mode === modes.REMOVE) {
100
+ map.dragging.disable();
101
+ }
102
+ },
103
+ mousedown: (e) => {
104
+ if (!canMultiSelect) return;
105
+ const parent = document.querySelector(".leaflet-pane.leaflet-map-pane");
106
+ if (
107
+ e.target !== parent &&
108
+ !parent.contains(e.originalEvent.target as Node)
109
+ ) {
110
+ return;
111
+ }
112
+ if (mode === modes.DRAG || mode === modes.REMOVE) {
113
+ setIsDragging(true);
114
+ }
115
+ },
116
+ mouseup: () => {
117
+ if (!canMultiSelect) return;
118
+ if ((mode === modes.DRAG || mode === modes.REMOVE) && isDragging) {
119
+ setIsDragging(false);
120
+ if (mode === modes.DRAG) {
121
+ if (selectedSeats.length > 0) {
122
+ setPricingPopupOpen(true);
123
+ }
124
+ } else if (mode === modes.REMOVE) {
125
+ const seatsToRemove = [];
126
+ selectedSeats.forEach((seat) => {
127
+ if (
128
+ !seatsToRemove.find((s) => s.ssId === seat.ssId) &&
129
+ seat.s === statuses.USER_PENDING
130
+ ) {
131
+ seatsToRemove.push(seat);
132
+ }
133
+ });
134
+
135
+ if (seatsToRemove.length > 0) {
136
+ batchRemoveTicketsFromCart(seatsToRemove);
137
+ }
138
+ setSelectedSeats([]);
139
+ }
140
+ }
141
+ },
142
+ });
143
+
144
+ useEffect(() => {
145
+ const keyboardHandler = (e: KeyboardEvent) => {
146
+ if (e.key === "Escape") {
147
+ if (ticketPopupOpen) {
148
+ map.closePopup();
149
+ } else if (pricingPopupOpen) {
150
+ setPricingPopupOpen(false);
151
+ setSelectedSeats([]);
152
+ } else if (mode !== modes.SINGLE) {
153
+ if (mode === modes.DRAW) {
154
+ drawRef.current?._toolbars.draw.disable();
155
+ }
156
+ setMode(modes.SINGLE);
157
+ } else if (isFullScreen) {
158
+ setIsFullScreen(false);
159
+ }
160
+ } else if (e.key === "Control" && canMultiSelect) {
161
+ if (mode !== modes.DRAG) {
162
+ if (mode === modes.DRAW) {
163
+ drawRef.current?._toolbars.draw.disable();
164
+ }
165
+ setMode(modes.DRAG);
166
+ } else {
167
+ setMode(modes.SINGLE);
168
+ }
169
+ } else if (e.key === "Alt" && canMultiSelect) {
170
+ if (mode !== modes.DRAW) {
171
+ drawRef.current?._toolbars.draw._modes.rectangle.handler.enable();
172
+ setMode(modes.DRAW);
173
+ } else {
174
+ drawRef.current?._toolbars.draw.disable();
175
+ setMode(modes.SINGLE);
176
+ }
177
+ }
178
+ };
179
+
180
+ if (typeof window !== "undefined") {
181
+ window.addEventListener("keydown", keyboardHandler);
182
+ }
183
+
184
+ return () => {
185
+ if (typeof window !== "undefined") {
186
+ window.removeEventListener("keydown", keyboardHandler);
187
+ }
188
+ };
189
+ }, [isFullScreen, ticketPopupOpen, mode]);
190
+
191
+ const getNorthSeatLat = (seat: Seat) => {
192
+ return height / 33.33 - (height / 33.33) * seat.y * 0.0105 + 1.4;
193
+ };
194
+
195
+ const getSeatCenterLatLng = (seat: Seat, isIcon: boolean) => {
196
+ return {
197
+ lat:
198
+ height * 0.05 -
199
+ seat.y /
200
+ (height > 800 ? (height > 999 ? (isIcon ? 3.5 : 3.45) : 3.3) : 3.35) -
201
+ (height > 800
202
+ ? height > 999
203
+ ? height * 0.0201
204
+ : height * 0.0198
205
+ : height > 587
206
+ ? height * 0.0201
207
+ : height > 399
208
+ ? height * 0.02035
209
+ : height * 0.0207),
210
+ lng: seat.x * 0.3 + 0.15,
211
+ };
212
+ };
213
+
214
+ const handleDrawBox = (v: L.DrawEvents.Created) => {
215
+ if (!canMultiSelect || !(v.layer instanceof L.Rectangle)) return;
216
+ const seatsToBook: Seat[] = [];
217
+ const boxBounds = v.layer.getBounds();
218
+ seats?.seats?.forEach((seat) => {
219
+ const seatCenter = getSeatCenterLatLng(seat, false);
220
+ const latlng: L.LatLngExpression = [
221
+ seat.r.includes("NORTH") ? getNorthSeatLat(seat) : seatCenter.lat,
222
+ seatCenter.lng,
223
+ ];
224
+ if (
225
+ boxBounds.contains(latlng) &&
226
+ !seatsToBook.find((s) => s.ssId === seat.ssId) &&
227
+ seat.s === statuses.UNSOLD
228
+ ) {
229
+ seatsToBook.push(seat);
230
+ }
231
+ });
232
+
233
+ setSelectedSeats(seatsToBook);
234
+ if (seatsToBook.length > 0) {
235
+ setPricingPopupOpen(true);
236
+ }
237
+ drawLayersRef.current?.clearLayers();
238
+ };
239
+
240
+ // gesture-handling plugin disallows scroll zooming and touch panning,
241
+ // to allow users to scroll past the map when it's not full screen.
242
+ useEffect(() => {
243
+ if (!isFullScreen) {
244
+ map.addHandler("gestureHandling", GestureHandling);
245
+ // @ts-ignore
246
+ map.gestureHandling.enable();
247
+ }
248
+ }, [map, isFullScreen]);
249
+
250
+ useEffect(() => {
251
+ if (mode === modes.SINGLE) {
252
+ map.dragging.enable();
253
+ } else {
254
+ map.dragging.disable();
255
+ }
256
+ }, [mode, map]);
257
+
258
+ return (
259
+ <>
260
+ <Control position="bottomright">
261
+ <div className="legendBox">
262
+ <div className="legend-header">
263
+ <button
264
+ className={`legend-button ${isLegendOpen ? "expanded" : ""}`}
265
+ onClick={() => setIsLegendOpen(!isLegendOpen)}
266
+ >
267
+ <p>{isLegendOpen ? "LEGEND:" : "VIEW LEGEND"}</p>
268
+ </button>
269
+ {isLegendOpen && (
270
+ <button
271
+ className="close-button"
272
+ onClick={() => setIsLegendOpen(false)}
273
+ >
274
+ <img src={closeIcon} alt="close" />
275
+ </button>
276
+ )}
277
+ </div>
278
+ <div className={`legend-contents ${isLegendOpen ? "expanded" : ""}`}>
279
+ <div>
280
+ <div className="legend-item">
281
+ <div className="legend-symbol legend-available"></div>
282
+ <p>available</p>
283
+ </div>
284
+ </div>
285
+ <div>
286
+ <div className="legend-item">
287
+ <div className="legend-symbol legend-sold"></div>
288
+ <p>sold</p>
289
+ </div>
290
+ </div>
291
+ <div>
292
+ <div className="legend-item">
293
+ <div className="legend-symbol legend-in-cart"></div>
294
+ <p>in cart</p>
295
+ </div>
296
+ </div>
297
+ <div>
298
+ <div className="legend-item">
299
+ <img src={wheelchairIcon} width={15} height={15} />
300
+ <p>wheelchair</p>
301
+ </div>
302
+ </div>
303
+ {seats.seats.filter((seat) => seat.m).length > 0 ? (
304
+ <div>
305
+ <div className="legend-item">
306
+ <img src={userIcon} width={15} height={15} />
307
+ <p>membership seats</p>
308
+ </div>
309
+ </div>
310
+ ) : (
311
+ <></>
312
+ )}
313
+ </div>
314
+ </div>
315
+ </Control>
316
+
317
+ <Control position="topleft">
318
+ <div className="extra-controls">
319
+ <button
320
+ title="Re-centre Seating Plan"
321
+ onClick={() => map.fitBounds(bounds)}
322
+ className="leaflet-control-zoom-in"
323
+ >
324
+ <img
325
+ src="https://au-iticket-shop-legacy.azurewebsites.net/assets/images/fit-icon.jpg"
326
+ style={{
327
+ height: "17px",
328
+ }}
329
+ ></img>
330
+ </button>
331
+ <button
332
+ title="Reload Seating Plan"
333
+ disabled={isReloading}
334
+ onClick={() => {
335
+ initialFetch(true);
336
+ map.fitBounds(bounds);
337
+ }}
338
+ className="leaflet-control-zoom-in reload-seating-plan"
339
+ style={{
340
+ background: isReloading ? "rgb(0, 230, 64)" : undefined,
341
+ }}
342
+ >
343
+ <img
344
+ className={isReloading ? "rotate" : ""}
345
+ src="https://au-iticket-shop-legacy.azurewebsites.net/assets/images/refresh.png"
346
+ style={{ height: "17px" }}
347
+ ></img>
348
+ </button>
349
+ </div>
350
+ </Control>
351
+ <Control position="topleft">
352
+ <div className="extra-controls full-screen-control">
353
+ <button
354
+ title={
355
+ isFullScreen ? "Collapse Seating Plan" : "Expand Seating Plan"
356
+ }
357
+ onClick={() => {
358
+ setIsFullScreen((prev) => !prev);
359
+ }}
360
+ className="leaflet-control-zoom-in"
361
+ >
362
+ <img
363
+ src={isFullScreen ? collapseIcon : expandIcon}
364
+ style={{
365
+ height: "24px",
366
+ }}
367
+ ></img>
368
+ </button>
369
+ </div>
370
+ </Control>
371
+
372
+ {canMultiSelect && (
373
+ <>
374
+ <Control position="bottomleft">
375
+ <div className="extra-controls multi-select-control">
376
+ <div className="drag-select">
377
+ <button
378
+ title="Drag select"
379
+ onClick={() => {
380
+ drawRef.current?._toolbars.draw.disable();
381
+ setMode((prev) =>
382
+ prev === modes.DRAG ? modes.SINGLE : modes.DRAG
383
+ );
384
+ }}
385
+ className="leaflet-control-zoom-in drag-button"
386
+ >
387
+ <span />
388
+ </button>
389
+ <ul
390
+ className="leaflet-draw-actions leaflet-draw-actions-top leaflet-draw-actions-bottom cancel-button"
391
+ style={{ display: mode === modes.DRAG ? "block" : "none" }}
392
+ >
393
+ <li>
394
+ <a
395
+ href="#"
396
+ onClick={(e) => {
397
+ e.preventDefault();
398
+ setMode(modes.SINGLE);
399
+ }}
400
+ >
401
+ Cancel
402
+ </a>
403
+ </li>
404
+ </ul>
405
+ </div>
406
+ {(bookedSeats.length > 0 || mode === modes.REMOVE) && (
407
+ <div className="remove-seats">
408
+ <button
409
+ title="Remove seats"
410
+ onClick={() => {
411
+ drawRef.current?._toolbars.draw.disable();
412
+ setMode(modes.REMOVE);
413
+ }}
414
+ className="leaflet-control-zoom-in remove-button"
415
+ >
416
+ <span />
417
+ </button>
418
+ <ul
419
+ className="leaflet-draw-actions leaflet-draw-actions-top leaflet-draw-actions-bottom cancel-button"
420
+ style={{
421
+ display: mode === modes.REMOVE ? "block" : "none",
422
+ }}
423
+ >
424
+ <li>
425
+ <a
426
+ href="#"
427
+ onClick={(e) => {
428
+ e.preventDefault();
429
+ setMode(modes.SINGLE);
430
+ }}
431
+ >
432
+ Cancel
433
+ </a>
434
+ </li>
435
+ </ul>
436
+ </div>
437
+ )}
438
+ <button
439
+ title="Single select"
440
+ onClick={() => {
441
+ drawRef.current?._toolbars.draw.disable();
442
+ setMode(modes.SINGLE);
443
+ }}
444
+ className="leaflet-control-zoom-in single-button"
445
+ >
446
+ <span />
447
+ </button>
448
+ </div>
449
+ </Control>
450
+ <FeatureGroup ref={drawLayersRef}>
451
+ <EditControl
452
+ position="bottomleft"
453
+ onCreated={handleDrawBox}
454
+ onDrawStart={() => {
455
+ setMode(modes.DRAW);
456
+ }}
457
+ onDrawStop={() => {
458
+ setMode((prev) => (prev === modes.DRAW ? modes.SINGLE : prev));
459
+ }}
460
+ draw={{
461
+ rectangle: true,
462
+ circle: false,
463
+ polyline: false,
464
+ polygon: false,
465
+ marker: false,
466
+ circlemarker: false,
467
+ }}
468
+ edit={{ edit: false, remove: false }}
469
+ onMounted={(drawInstance) => {
470
+ drawRef.current = drawInstance;
471
+ }}
472
+ />
473
+ </FeatureGroup>
474
+ </>
475
+ )}
476
+
477
+ <Control position="topright">
478
+ <button
479
+ title="Close full screen"
480
+ onClick={() => setIsFullScreen(false)}
481
+ className={`full-screen-close-button ${isFullScreen ? "show" : ""}`}
482
+ >
483
+ <img
484
+ src={closeIcon}
485
+ style={{
486
+ height: "24px",
487
+ }}
488
+ ></img>
489
+ </button>
490
+ </Control>
491
+
492
+ <ImageOverlay
493
+ url={seats.background}
494
+ bounds={[
495
+ [0, 0],
496
+ [height * 0.03, width * 0.03],
497
+ ]}
498
+ zIndex={10}
499
+ />
500
+
501
+ {seats &&
502
+ seats.seats &&
503
+ seats.seats.map((s, i) => {
504
+ const isIcon =
505
+ (!priceSectionIds || priceSectionIds.includes(s.psId)) &&
506
+ (s.s === statuses.WHEELCHAIR_ACCESS ||
507
+ (s.s === statuses.USER_PENDING && s.m));
508
+ const seatCenter = getSeatCenterLatLng(s, isIcon);
509
+ const seatPrices = seats.pricing.filter(
510
+ (price) =>
511
+ (!priceSectionIds || priceSectionIds.includes(price.psId)) &&
512
+ price.psId === s.psId
513
+ );
514
+ const isSingleFlexi =
515
+ seatPrices.length === 1 &&
516
+ !!seatPrices[0].pMax &&
517
+ !!seatPrices[0].pMin &&
518
+ seatPrices[0].pMax > seatPrices[0].pMin;
519
+
520
+ return (
521
+ <div key={i}>
522
+ <ImageOverlay
523
+ url={`https://iticketseatingplan.blob.core.windows.net/embed/static/media/rippleload.08e6a08dd832819226ef.gif`}
524
+ bounds={[
525
+ [
526
+ (s.r.includes("NORTH")
527
+ ? getNorthSeatLat(s)
528
+ : seatCenter.lat) - 0.11,
529
+ seatCenter.lng - 0.11,
530
+ ],
531
+ [
532
+ (s.r.includes("NORTH")
533
+ ? getNorthSeatLat(s)
534
+ : seatCenter.lat) + 0.11,
535
+ seatCenter.lng + 0.11,
536
+ ],
537
+ ]}
538
+ opacity={
539
+ s.loading ||
540
+ (ticketPopupOpen && chosenSeat?.seat?.ssId === s.ssId)
541
+ ? 100
542
+ : 0
543
+ }
544
+ zIndex={10}
545
+ />
546
+ {!priceSectionIds || priceSectionIds.includes(s.psId) ? (
547
+ s.s === statuses.WHEELCHAIR_ACCESS ? (
548
+ <ImageOverlay
549
+ url={wheelchairIcon}
550
+ bounds={[
551
+ [seatCenter.lat - 0.11, seatCenter.lng - 0.11],
552
+ [seatCenter.lat + 0.11, seatCenter.lng + 0.11],
553
+ ]}
554
+ zIndex={10}
555
+ />
556
+ ) : s.s === statuses.USER_PENDING && s.m ? (
557
+ <ImageOverlay
558
+ url={userIcon}
559
+ bounds={[
560
+ [seatCenter.lat - 0.11, seatCenter.lng - 0.15],
561
+ [seatCenter.lat + 0.11, seatCenter.lng + 0.15],
562
+ ]}
563
+ zIndex={10}
564
+ />
565
+ ) : (
566
+ <Circle
567
+ center={[
568
+ s.r.includes("NORTH")
569
+ ? getNorthSeatLat(s)
570
+ : seatCenter.lat,
571
+ seatCenter.lng,
572
+ ]}
573
+ pathOptions={getInitialColor(
574
+ s,
575
+ price
576
+ // TEST selected state for multiselect
577
+ // !!selectedSeats.find(
578
+ // (selectedSeat) => selectedSeat.ssId === s.ssId
579
+ // )
580
+ )}
581
+ radius={12000}
582
+ eventHandlers={{
583
+ click: (e) => {
584
+ if (mode === modes.SINGLE) {
585
+ handleClickSeat(e, s);
586
+ }
587
+ },
588
+ popupopen: (popup) => {
589
+ setTicketPopupOpen(true);
590
+ popupRef.current = popup;
591
+ },
592
+ popupclose: () => {
593
+ setTicketPopupOpen(false);
594
+ popupRef.current = null;
595
+ },
596
+ mousedown: () => {
597
+ if (
598
+ (mode === modes.DRAG || mode === modes.REMOVE) &&
599
+ canMultiSelect
600
+ ) {
601
+ if (
602
+ !selectedSeats.find(
603
+ (seat) => seat.ssId === s.ssId
604
+ ) &&
605
+ s.s ===
606
+ (mode === modes.DRAG
607
+ ? statuses.UNSOLD
608
+ : statuses.USER_PENDING)
609
+ ) {
610
+ setSelectedSeats((prev) => [...prev, s]);
611
+ }
612
+ }
613
+ },
614
+ mouseover: () => {
615
+ if (
616
+ (mode === modes.DRAG || mode === modes.REMOVE) &&
617
+ isDragging &&
618
+ canMultiSelect
619
+ ) {
620
+ if (
621
+ !selectedSeats.find(
622
+ (seat) => seat.ssId === s.ssId
623
+ ) &&
624
+ s.s ===
625
+ (mode === modes.DRAG
626
+ ? statuses.UNSOLD
627
+ : statuses.USER_PENDING)
628
+ ) {
629
+ setSelectedSeats((prev) => [...prev, s]);
630
+ }
631
+ }
632
+ },
633
+ }}
634
+ // value={s}
635
+ >
636
+ <Tooltip
637
+ className="seatname-tooltip"
638
+ direction={"top"}
639
+ offset={[0, height * 0.03 + -30]}
640
+ >
641
+ {s.r + "-" + s.c}
642
+ </Tooltip>
643
+
644
+ {s.s === statuses.UNSOLD && (
645
+ <Popup className={`ticket-popup popup-${s.sId}`}>
646
+ {isSingleFlexi ? (
647
+ <div className="single-flexi">
648
+ <div className="popup-header">
649
+ <div className="flexi-title">
650
+ <FlexiIcon
651
+ className="ticket-icon"
652
+ height="56px"
653
+ width="56px"
654
+ />
655
+ <p className="seatname">{`${
656
+ areaName ? areaName + " | " : ""
657
+ }${s.r}-${s.c}`}</p>
658
+ </div>
659
+ <button
660
+ className="close-button"
661
+ aria-label="close flexi popup"
662
+ onClick={() => map.closePopup()}
663
+ >
664
+ <CloseIcon />
665
+ </button>
666
+ </div>
667
+ <div className="popup-content">
668
+ <p className="heading">FLEXi Price</p>
669
+ <Flexi
670
+ price={seatPrices[0]}
671
+ onClose={() => map.closePopup()}
672
+ onConfirm={(amount) =>
673
+ addTicketToCart(s, {
674
+ ...seatPrices[0],
675
+ p: amount,
676
+ })
677
+ }
678
+ buttonText="Add to cart"
679
+ isOpen
680
+ />
681
+ </div>
682
+ </div>
683
+ ) : (
684
+ <>
685
+ <div className="popup-header">
686
+ <TicketIcon
687
+ className="ticket-icon"
688
+ height="56px"
689
+ width="56px"
690
+ />
691
+ <p className="seatname">{`${
692
+ areaName ? areaName + " | " : ""
693
+ }${s.r}-${s.c}`}</p>
694
+ <button
695
+ className="close-button"
696
+ aria-label="close popup"
697
+ onClick={() => map.closePopup()}
698
+ >
699
+ <CloseIcon />
700
+ </button>
701
+ </div>
702
+ <div className="popup-content">
703
+ <p className="heading">Select option:</p>
704
+ <div className="priceages">
705
+ {seats.pricing
706
+ .filter(
707
+ (price) =>
708
+ (!priceSectionIds ||
709
+ priceSectionIds.includes(price.psId)) &&
710
+ price.psId === s.psId
711
+ )
712
+ .map((price) => (
713
+ <PriceButton
714
+ key={price.paId}
715
+ price={price}
716
+ addToCart={(amount) =>
717
+ addTicketToCart(s, {
718
+ ...price,
719
+ p: amount,
720
+ })
721
+ }
722
+ // When the flexi dropdown opens, pan map to re-fit popup
723
+ panMap={() => {
724
+ const p = document.querySelector(
725
+ `.popup-${s.sId}`
726
+ );
727
+ const h = p?.clientHeight;
728
+ if (h) {
729
+ const offsetTop =
730
+ map.latLngToContainerPoint(
731
+ popupRef.current.popup.getLatLng()
732
+ ).y - h;
733
+ if (offsetTop < 0) {
734
+ map.panBy([0, offsetTop - 20]);
735
+ }
736
+ }
737
+ }}
738
+ />
739
+ ))}
740
+ </div>
741
+ </div>
742
+ </>
743
+ )}
744
+ </Popup>
745
+ )}
746
+ </Circle>
747
+ )
748
+ ) : (
749
+ <></>
750
+ )}
751
+ </div>
752
+ );
753
+ })}
754
+ </>
755
+ );
756
+ }