@ticketboothapp/booking 0.1.19 → 0.1.22
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/package.json +2 -1
- package/src/components/BookingWidget.tsx +282 -26
- package/src/components/ManageBookingView.tsx +75 -23
- package/src/components/PostBookingDependentAddOnUpsell.tsx +1 -1
- package/src/components/booking/BookingProductGrid.tsx +1 -1
- package/src/components/booking/Calendar.module.css +3 -3
- package/src/components/booking/CheckoutForm.tsx +1 -1
- package/src/components/booking/InfoTooltip.tsx +2 -13
- package/src/components/booking/PickupLocationSelector.tsx +2 -2
- package/src/components/booking/PriceBreakdown.tsx +11 -34
- package/src/index.ts +3 -1
- package/tsconfig.json +1 -1
- package/src/components/JobApplicationDialog.module.css +0 -440
- package/src/components/JobApplicationDialog.tsx +0 -620
- package/src/components/PickupLocationMap.tsx +0 -110
- package/src/components/accordion.css +0 -27
- package/src/components/accordion.tsx +0 -29
- package/src/components/analytics/AnalyticsConsentRestore.tsx +0 -19
- package/src/components/analytics/AnalyticsScripts.tsx +0 -106
- package/src/components/analytics/CookieConsentBanner.css +0 -86
- package/src/components/analytics/CookieConsentBanner.tsx +0 -102
- package/src/components/bottom-sheet.module.css +0 -78
- package/src/components/bottom-sheet.tsx +0 -60
- package/src/components/breadcrumb.module.css +0 -40
- package/src/components/breadcrumb.tsx +0 -36
- package/src/components/client-bottom-sheet.tsx +0 -14
- package/src/components/conditional-footer.tsx +0 -27
- package/src/components/contact-us.module.css +0 -147
- package/src/components/contact-us.tsx +0 -49
- package/src/components/email-signup.css +0 -151
- package/src/components/email-signup.tsx +0 -63
- package/src/components/faq-wrapper.module.css +0 -47
- package/src/components/faq-wrapper.tsx +0 -15
- package/src/components/footer.css +0 -187
- package/src/components/footer.tsx +0 -143
- package/src/components/global-simple-modal.tsx +0 -33
- package/src/components/google-review-summary.module.css +0 -77
- package/src/components/google-review-summary.tsx +0 -50
- package/src/components/hero-image.css +0 -13
- package/src/components/hero-image.tsx +0 -44
- package/src/components/language-aware-link.tsx +0 -72
- package/src/components/language-switcher.module.css +0 -124
- package/src/components/language-switcher.tsx +0 -75
- package/src/components/map-section.css +0 -59
- package/src/components/map-section.tsx +0 -63
- package/src/components/navbar.module.css +0 -152
- package/src/components/navbar.tsx +0 -125
- package/src/components/parallax-provider.tsx +0 -11
- package/src/components/product-theme-pages/best-option.module.css +0 -70
- package/src/components/product-theme-pages/best-option.tsx +0 -35
- package/src/components/product-theme-pages/extended-tour-options.module.css +0 -22
- package/src/components/product-theme-pages/extended-tour-options.tsx +0 -11
- package/src/components/product-theme-pages/photo-gallery.tsx +0 -90
- package/src/components/product-theme-pages/product-theme-page-layout.module.css +0 -13
- package/src/components/product-theme-pages/product-theme-page-layout.tsx +0 -67
- package/src/components/product-theme-pages/top-of-fold.module.css +0 -179
- package/src/components/product-theme-pages/top-of-fold.tsx +0 -80
- package/src/components/product-tile/image-only-product-tile-desktop.module.css +0 -106
- package/src/components/product-tile/image-only-product-tile-desktop.tsx +0 -56
- package/src/components/product-tile/image-only-product-tile-mobile.module.css +0 -122
- package/src/components/product-tile/image-only-product-tile-mobile.tsx +0 -89
- package/src/components/product-tile/image-only-product-tile.tsx +0 -44
- package/src/components/product-tile/product-tile-card.module.css +0 -84
- package/src/components/product-tile/product-tile-card.tsx +0 -61
- package/src/components/review-highlights-section.css +0 -85
- package/src/components/review-highlights-section.tsx +0 -127
- package/src/components/season-closure-overlay.module.css +0 -99
- package/src/components/season-closure-overlay.tsx +0 -98
- package/src/components/simple-modal.tsx +0 -69
- package/src/components/simple-top-of-fold.module.css +0 -76
- package/src/components/simple-top-of-fold.tsx +0 -34
- package/src/components/spacer.css +0 -41
- package/src/components/spacer.tsx +0 -23
- package/src/components/star-rating.module.css +0 -74
- package/src/components/star-rating.tsx +0 -48
- package/src/components/title-subtitle.module.css +0 -10
- package/src/components/title-subtitle.tsx +0 -30
- package/src/components/translatable-reviews.tsx +0 -75
- package/src/components/value-props.css +0 -185
- package/src/components/value-props.tsx +0 -88
- package/src/constants/booking-guide-quiz.ts +0 -64
- package/src/constants/contact-info.ts +0 -2
- package/src/constants/faq.ts +0 -44
- package/src/constants/json-ld/faq-json-ld.tsx +0 -170
- package/src/constants/json-ld/homepage-json-ld.tsx +0 -138
- package/src/constants/json-ld/job-posting-json-ld.tsx +0 -92
- package/src/constants/json-ld/organization-json-ld.tsx +0 -62
- package/src/constants/json-ld/page-json-ld.tsx +0 -6
- package/src/constants/json-ld/product-json-ld.tsx +0 -154
- package/src/constants/json-ld/review-json-ld.tsx +0 -377
- package/src/constants/navigation-links/footer-links.ts +0 -48
- package/src/constants/navigation-links/nav-bar-links.ts +0 -41
- package/src/constants/navigation-links/navigation-link.ts +0 -6
- package/src/constants/quiz-recommendations.ts +0 -506
- package/src/constants/reviews.ts +0 -75
- package/src/constants/staff.ts +0 -197
- package/src/constants/value-props.ts +0 -58
- package/src/hooks/use-bottom-sheet.tsx +0 -15
- package/src/hooks/use-simple-modal.tsx +0 -27
- package/src/hooks/useEmailSubscription.tsx +0 -103
- package/src/hooks/useEmbeddedInIframe.ts +0 -16
- package/src/hooks/useQuiz.tsx +0 -210
- package/src/providers/bottom-sheet-provider.tsx +0 -40
- package/src/types/fareharbor.d.ts +0 -12
- package/src/types/quiz.ts +0 -59
- /package/src/{app/photo-sessions → lib}/photo-packages.ts +0 -0
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { useCallback } from 'react';
|
|
4
|
-
import { useJsApiLoader, GoogleMap, Marker, InfoWindow } from '@react-google-maps/api';
|
|
5
|
-
import { calculateMapCenter, getMapOptions, calculateMapBounds } from '@/lib/pickup/map-utils';
|
|
6
|
-
import { createPinMarkerIcon } from '@/lib/pickup/marker-icons';
|
|
7
|
-
import { ENV } from '@/lib/env';
|
|
8
|
-
import type { PickupLocation } from '@/lib/booking-api';
|
|
9
|
-
import styles from './PickupLocationDialog.module.css';
|
|
10
|
-
|
|
11
|
-
const libraries: ('places')[] = ['places'];
|
|
12
|
-
const MARKER_COLOR = '#dc2626';
|
|
13
|
-
const MARKER_HOVER = '#1e3a8a';
|
|
14
|
-
|
|
15
|
-
interface PickupLocationMapProps {
|
|
16
|
-
pickupLocations: PickupLocation[];
|
|
17
|
-
selectedLocationId: string | null;
|
|
18
|
-
onSelectLocation: (id: string) => void;
|
|
19
|
-
onMarkerClick?: (id: string) => void;
|
|
20
|
-
saving: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export default function PickupLocationMap({
|
|
24
|
-
pickupLocations,
|
|
25
|
-
selectedLocationId,
|
|
26
|
-
onSelectLocation,
|
|
27
|
-
onMarkerClick,
|
|
28
|
-
saving,
|
|
29
|
-
}: PickupLocationMapProps) {
|
|
30
|
-
const [hoveredMarker, setHoveredMarker] = React.useState<string | null>(null);
|
|
31
|
-
const [selectedMarker, setSelectedMarker] = React.useState<string | null>(null);
|
|
32
|
-
|
|
33
|
-
const { isLoaded, loadError } = useJsApiLoader({
|
|
34
|
-
id: 'google-map-pickup',
|
|
35
|
-
googleMapsApiKey: ENV.GOOGLE_MAPS_API_KEY,
|
|
36
|
-
libraries,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const locationsWithCoords = pickupLocations.filter((l) => l.coordinates);
|
|
40
|
-
const mapCenter = calculateMapCenter(locationsWithCoords);
|
|
41
|
-
const mapOptions = getMapOptions();
|
|
42
|
-
const mapBounds = calculateMapBounds(locationsWithCoords);
|
|
43
|
-
|
|
44
|
-
const onMapLoad = useCallback(
|
|
45
|
-
(map: google.maps.Map) => {
|
|
46
|
-
if (mapBounds) {
|
|
47
|
-
map.fitBounds(mapBounds, { top: 50, right: 50, bottom: 50, left: 50 });
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
[mapBounds]
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
if (!isLoaded || loadError || locationsWithCoords.length === 0) {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<div className={styles.mapContainer}>
|
|
59
|
-
<GoogleMap
|
|
60
|
-
mapContainerClassName={styles.map}
|
|
61
|
-
center={mapCenter}
|
|
62
|
-
zoom={10}
|
|
63
|
-
options={mapOptions}
|
|
64
|
-
onLoad={onMapLoad}
|
|
65
|
-
>
|
|
66
|
-
{locationsWithCoords.map((loc) => {
|
|
67
|
-
if (!loc.coordinates) return null;
|
|
68
|
-
const isHovered = hoveredMarker === loc.id;
|
|
69
|
-
const isSelected = selectedLocationId === loc.id;
|
|
70
|
-
return (
|
|
71
|
-
<Marker
|
|
72
|
-
key={loc.id}
|
|
73
|
-
position={loc.coordinates}
|
|
74
|
-
title={loc.name}
|
|
75
|
-
zIndex={isHovered || isSelected ? 200 : 100}
|
|
76
|
-
icon={{
|
|
77
|
-
url: createPinMarkerIcon(isHovered || isSelected ? MARKER_HOVER : MARKER_COLOR),
|
|
78
|
-
scaledSize: new google.maps.Size(32, 40),
|
|
79
|
-
anchor: new google.maps.Point(16, 40),
|
|
80
|
-
}}
|
|
81
|
-
onMouseOver={() => setHoveredMarker(loc.id)}
|
|
82
|
-
onMouseOut={() => setHoveredMarker(null)}
|
|
83
|
-
onClick={() => {
|
|
84
|
-
setSelectedMarker(loc.id);
|
|
85
|
-
onMarkerClick?.(loc.id);
|
|
86
|
-
}}
|
|
87
|
-
>
|
|
88
|
-
{selectedMarker === loc.id && (
|
|
89
|
-
<InfoWindow onCloseClick={() => setSelectedMarker(null)}>
|
|
90
|
-
<div className={styles.infoWindow}>
|
|
91
|
-
<h3 className={styles.infoTitle}>{loc.name}</h3>
|
|
92
|
-
<p className={styles.infoAddress}>{loc.address}</p>
|
|
93
|
-
<button
|
|
94
|
-
type="button"
|
|
95
|
-
onClick={() => onSelectLocation(loc.id)}
|
|
96
|
-
className={styles.selectBtn}
|
|
97
|
-
disabled={saving}
|
|
98
|
-
>
|
|
99
|
-
{saving ? 'Saving…' : 'Select this location'}
|
|
100
|
-
</button>
|
|
101
|
-
</div>
|
|
102
|
-
</InfoWindow>
|
|
103
|
-
)}
|
|
104
|
-
</Marker>
|
|
105
|
-
);
|
|
106
|
-
})}
|
|
107
|
-
</GoogleMap>
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
.via-via-accordion-base {
|
|
2
|
-
background-color: var(--light-orange-background-dark);
|
|
3
|
-
box-shadow: none;
|
|
4
|
-
width: 100%;
|
|
5
|
-
box-sizing: border-box;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
.via-via-accordion-base button[data-slot="trigger"] {
|
|
9
|
-
cursor: pointer;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
.via-via-accordion-content {
|
|
13
|
-
width: 100%;
|
|
14
|
-
box-sizing: border-box;
|
|
15
|
-
cursor: pointer;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.via-via-accordion-title {
|
|
19
|
-
color: var(--primary-text);
|
|
20
|
-
font-family: 'Figtree', sans-serif;
|
|
21
|
-
font-weight: 400;
|
|
22
|
-
font-size: 1.25rem;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.accordion-icon {
|
|
26
|
-
color: var(--accent-orange);
|
|
27
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {Accordion, AccordionItem} from "@heroui/accordion";
|
|
4
|
-
import "./accordion.css";
|
|
5
|
-
import PlusIcon from "@/assets/icons/plus.svg";
|
|
6
|
-
import MinusIcon from "@/assets/icons/minus.svg";
|
|
7
|
-
import { faqItem } from "@/constants/faq";
|
|
8
|
-
|
|
9
|
-
export default function AccordionComponent({items, className, selectionMode}: {items: faqItem[], className: string, selectionMode: "single" | "multiple"}) {
|
|
10
|
-
return (
|
|
11
|
-
<Accordion
|
|
12
|
-
selectionMode={selectionMode}
|
|
13
|
-
variant="splitted"
|
|
14
|
-
className={className}
|
|
15
|
-
itemClasses={{
|
|
16
|
-
base: "via-via-accordion-base",
|
|
17
|
-
title: "via-via-accordion-title",
|
|
18
|
-
trigger: "bg-transparent hover:bg-transparent border-none outline-none",
|
|
19
|
-
content: "via-via-accordion-content",
|
|
20
|
-
}}
|
|
21
|
-
>
|
|
22
|
-
{items.map((item) => (
|
|
23
|
-
<AccordionItem key={item.question} aria-label={item.question} title={item.question} indicator={({isOpen}) => (isOpen ? <MinusIcon className="accordion-icon" /> : <PlusIcon className="accordion-icon" />)}>
|
|
24
|
-
<div dangerouslySetInnerHTML={{__html: item.answer}} />
|
|
25
|
-
</AccordionItem>
|
|
26
|
-
))}
|
|
27
|
-
</Accordion>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect } from 'react';
|
|
4
|
-
import { updateAnalyticsConsent } from './AnalyticsScripts';
|
|
5
|
-
import { hasAnalyticsConsent } from '@/lib/analytics';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Restores analytics consent on load if user previously accepted.
|
|
9
|
-
* Must run after AnalyticsScripts have loaded gtag/fbq.
|
|
10
|
-
*/
|
|
11
|
-
export function AnalyticsConsentRestore() {
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
if (hasAnalyticsConsent()) {
|
|
14
|
-
updateAnalyticsConsent(true);
|
|
15
|
-
}
|
|
16
|
-
}, []);
|
|
17
|
-
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
4
|
-
import Script from 'next/script';
|
|
5
|
-
import { ENV, isLocalhost, isProduction } from '@/lib/env';
|
|
6
|
-
import { setAnalyticsConsentGranted } from '@/lib/analytics';
|
|
7
|
-
|
|
8
|
-
const gaId = ENV.GA4_MEASUREMENT_ID;
|
|
9
|
-
const pixelId = ENV.META_PIXEL_ID;
|
|
10
|
-
|
|
11
|
-
/** Update GA4 and Meta consent. Call when user accepts cookies or on load if previously granted. */
|
|
12
|
-
export function updateAnalyticsConsent(granted: boolean): void {
|
|
13
|
-
if (granted) {
|
|
14
|
-
setAnalyticsConsentGranted();
|
|
15
|
-
if (typeof window !== 'undefined') {
|
|
16
|
-
if (window.gtag) {
|
|
17
|
-
window.gtag('consent', 'update', {
|
|
18
|
-
ad_user_data: 'granted',
|
|
19
|
-
ad_personalization: 'granted',
|
|
20
|
-
ad_storage: 'granted',
|
|
21
|
-
analytics_storage: 'granted',
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
if (window.fbq) {
|
|
25
|
-
window.fbq('consent', 'grant');
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function AnalyticsScripts() {
|
|
32
|
-
// Don't load on localhost (prod build testing) or when not production env
|
|
33
|
-
const [shouldLoad, setShouldLoad] = useState(false);
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
setShouldLoad(!isLocalhost() && isProduction() && (!!gaId || !!pixelId));
|
|
36
|
-
}, []);
|
|
37
|
-
|
|
38
|
-
if (!shouldLoad) return null;
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<>
|
|
42
|
-
{/* GA4 with Consent Mode v2 (default denied) */}
|
|
43
|
-
{gaId && (
|
|
44
|
-
<>
|
|
45
|
-
<Script
|
|
46
|
-
id="gtag-consent"
|
|
47
|
-
strategy="beforeInteractive"
|
|
48
|
-
dangerouslySetInnerHTML={{
|
|
49
|
-
__html: `
|
|
50
|
-
window.dataLayer = window.dataLayer || [];
|
|
51
|
-
function gtag(){dataLayer.push(arguments);}
|
|
52
|
-
gtag('js', new Date());
|
|
53
|
-
gtag('consent', 'default', {
|
|
54
|
-
ad_user_data: 'denied',
|
|
55
|
-
ad_personalization: 'denied',
|
|
56
|
-
ad_storage: 'denied',
|
|
57
|
-
analytics_storage: 'denied',
|
|
58
|
-
wait_for_update: 500
|
|
59
|
-
});
|
|
60
|
-
`,
|
|
61
|
-
}}
|
|
62
|
-
/>
|
|
63
|
-
<Script
|
|
64
|
-
src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
|
|
65
|
-
strategy="afterInteractive"
|
|
66
|
-
/>
|
|
67
|
-
<Script
|
|
68
|
-
id="gtag-config"
|
|
69
|
-
strategy="afterInteractive"
|
|
70
|
-
dangerouslySetInnerHTML={{
|
|
71
|
-
__html: `
|
|
72
|
-
window.dataLayer = window.dataLayer || [];
|
|
73
|
-
function gtag(){dataLayer.push(arguments);}
|
|
74
|
-
gtag('config', '${gaId}', { anonymize_ip: true });
|
|
75
|
-
`,
|
|
76
|
-
}}
|
|
77
|
-
/>
|
|
78
|
-
</>
|
|
79
|
-
)}
|
|
80
|
-
|
|
81
|
-
{/* Meta Pixel with consent revoked by default */}
|
|
82
|
-
{pixelId && (
|
|
83
|
-
<Script
|
|
84
|
-
id="meta-pixel"
|
|
85
|
-
strategy="afterInteractive"
|
|
86
|
-
dangerouslySetInnerHTML={{
|
|
87
|
-
__html: `
|
|
88
|
-
!function(f,b,e,v,n,t,s)
|
|
89
|
-
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
|
90
|
-
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
|
91
|
-
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
|
92
|
-
n.queue=[];t=b.createElement(e);t.async=!0;
|
|
93
|
-
t.src=v;s=b.getElementsByTagName(e)[0];
|
|
94
|
-
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
|
95
|
-
'https://connect.facebook.net/en_US/fbevents.js');
|
|
96
|
-
fbq('consent', 'revoke');
|
|
97
|
-
fbq('init', '${pixelId}');
|
|
98
|
-
fbq('track', 'PageView');
|
|
99
|
-
`,
|
|
100
|
-
}}
|
|
101
|
-
/>
|
|
102
|
-
)}
|
|
103
|
-
|
|
104
|
-
</>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/* Cookie consent banner – customize appearance here */
|
|
2
|
-
|
|
3
|
-
.cookieConsentBanner {
|
|
4
|
-
position: fixed;
|
|
5
|
-
bottom: 0;
|
|
6
|
-
left: 0;
|
|
7
|
-
right: 0;
|
|
8
|
-
z-index: 50;
|
|
9
|
-
background-color: rgba(28, 25, 23, 0.9);
|
|
10
|
-
color: #ffffff;
|
|
11
|
-
padding: 1rem;
|
|
12
|
-
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.cookieConsentBannerInner {
|
|
16
|
-
max-width: 850px;
|
|
17
|
-
margin-left: auto;
|
|
18
|
-
margin-right: auto;
|
|
19
|
-
display: flex;
|
|
20
|
-
flex-direction: column;
|
|
21
|
-
gap: var(--spacing-small);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
@media (min-width: 1023px) {
|
|
25
|
-
.cookieConsentBannerInner {
|
|
26
|
-
flex-direction: row;
|
|
27
|
-
align-items: center;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.cookieConsentBannerText {
|
|
32
|
-
flex: 1 1 0%;
|
|
33
|
-
font-size: 0.875rem;
|
|
34
|
-
line-height: 1.25rem;
|
|
35
|
-
color: #ffffff;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.cookieConsentBannerLink {
|
|
39
|
-
text-decoration: underline;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.cookieConsentBannerLink:hover {
|
|
43
|
-
color: #34d399;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.cookieConsentBannerButtons {
|
|
47
|
-
display: flex;
|
|
48
|
-
gap: 0.75rem;
|
|
49
|
-
width: 100%;
|
|
50
|
-
justify-content: center;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
@media (min-width: 640px) {
|
|
54
|
-
.cookieConsentBannerButtons {
|
|
55
|
-
width: auto;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.cookieConsentBannerDecline {
|
|
60
|
-
padding: 0.5rem 1rem;
|
|
61
|
-
font-size: 0.875rem;
|
|
62
|
-
border: 1px solid #57534e;
|
|
63
|
-
border-radius: 0.5rem;
|
|
64
|
-
background: transparent;
|
|
65
|
-
color: inherit;
|
|
66
|
-
cursor: pointer;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.cookieConsentBannerDecline:hover {
|
|
70
|
-
background-color: #292524;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.cookieConsentBannerAccept {
|
|
74
|
-
padding: 0.5rem 1rem;
|
|
75
|
-
font-size: 0.875rem;
|
|
76
|
-
font-weight: 500;
|
|
77
|
-
background-color: #059669;
|
|
78
|
-
color: white;
|
|
79
|
-
border: none;
|
|
80
|
-
border-radius: 0.5rem;
|
|
81
|
-
cursor: pointer;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.cookieConsentBannerAccept:hover {
|
|
85
|
-
background-color: #047857;
|
|
86
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
4
|
-
import { usePathname } from 'next/navigation';
|
|
5
|
-
import { useEmbeddedInIframe } from '@/hooks/useEmbeddedInIframe';
|
|
6
|
-
import { updateAnalyticsConsent } from './AnalyticsScripts';
|
|
7
|
-
import { ENV, isLocalhost, isProduction, isStaging } from '@/lib/env';
|
|
8
|
-
import Link from 'next/link';
|
|
9
|
-
import './CookieConsentBanner.css';
|
|
10
|
-
|
|
11
|
-
const CONSENT_KEY = 'cookie-consent';
|
|
12
|
-
|
|
13
|
-
/** Show banner when we load GA4/Meta (production), or on localhost/staging for testing. */
|
|
14
|
-
function shouldShowBanner(): boolean {
|
|
15
|
-
const hasAnalyticsIds = !!ENV.GA4_MEASUREMENT_ID || !!ENV.META_PIXEL_ID;
|
|
16
|
-
if (isProduction() && !isLocalhost() && hasAnalyticsIds) return true;
|
|
17
|
-
if (isLocalhost() || isStaging()) return true; // show for testing
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function CookieConsentBanner() {
|
|
22
|
-
const pathname = usePathname();
|
|
23
|
-
const embeddedInIframe = useEmbeddedInIframe();
|
|
24
|
-
const [mounted, setMounted] = useState(false);
|
|
25
|
-
const [showBanner, setShowBanner] = useState(false);
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
setMounted(true);
|
|
29
|
-
}, []);
|
|
30
|
-
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
if (!mounted || typeof window === 'undefined') return;
|
|
33
|
-
if (!shouldShowBanner()) return;
|
|
34
|
-
try {
|
|
35
|
-
// On localhost/staging: always show for testing. On production: only when user hasn't accepted.
|
|
36
|
-
const alwaysShowForTesting = isLocalhost() || isStaging();
|
|
37
|
-
const stored = localStorage.getItem(CONSENT_KEY);
|
|
38
|
-
setShowBanner(alwaysShowForTesting || stored !== 'granted');
|
|
39
|
-
} catch {
|
|
40
|
-
setShowBanner(true);
|
|
41
|
-
}
|
|
42
|
-
}, [mounted]);
|
|
43
|
-
|
|
44
|
-
// Signal to other UI (e.g. floating book button) that banner is visible, so they can position above it
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
if (typeof document === 'undefined') return;
|
|
47
|
-
if (showBanner && !embeddedInIframe && pathname !== '/live-pickups') {
|
|
48
|
-
document.body.dataset.cookieBannerVisible = 'true';
|
|
49
|
-
} else {
|
|
50
|
-
delete document.body.dataset.cookieBannerVisible;
|
|
51
|
-
}
|
|
52
|
-
return () => {
|
|
53
|
-
delete document.body.dataset.cookieBannerVisible;
|
|
54
|
-
};
|
|
55
|
-
}, [showBanner, embeddedInIframe, pathname]);
|
|
56
|
-
|
|
57
|
-
const handleAccept = () => {
|
|
58
|
-
updateAnalyticsConsent(true);
|
|
59
|
-
setShowBanner(false);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const handleDecline = () => {
|
|
63
|
-
try {
|
|
64
|
-
localStorage.setItem(CONSENT_KEY, 'denied');
|
|
65
|
-
} catch {
|
|
66
|
-
/* ignore */
|
|
67
|
-
}
|
|
68
|
-
setShowBanner(false);
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
if (embeddedInIframe || pathname === '/live-pickups' || !showBanner || !mounted) return null;
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<div
|
|
75
|
-
className="cookieConsentBanner"
|
|
76
|
-
role="dialog"
|
|
77
|
-
aria-label="Cookie consent"
|
|
78
|
-
>
|
|
79
|
-
<div className="cookieConsentBannerInner">
|
|
80
|
-
<p className="cookieConsentBannerText">
|
|
81
|
-
Accept to approve the use of cookies for analytics and advertising. See our <Link href="/privacy-policy" className="cookieConsentBannerLink">privacy policy</Link> for details.
|
|
82
|
-
</p>
|
|
83
|
-
<div className="cookieConsentBannerButtons">
|
|
84
|
-
<button
|
|
85
|
-
type="button"
|
|
86
|
-
onClick={handleDecline}
|
|
87
|
-
className="cookieConsentBannerDecline"
|
|
88
|
-
>
|
|
89
|
-
Decline
|
|
90
|
-
</button>
|
|
91
|
-
<button
|
|
92
|
-
type="button"
|
|
93
|
-
onClick={handleAccept}
|
|
94
|
-
className="cookieConsentBannerAccept"
|
|
95
|
-
>
|
|
96
|
-
Accept
|
|
97
|
-
</button>
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
.container {
|
|
2
|
-
position: fixed;
|
|
3
|
-
top: 0;
|
|
4
|
-
left: 0;
|
|
5
|
-
right: 0;
|
|
6
|
-
bottom: 0;
|
|
7
|
-
z-index: 9999;
|
|
8
|
-
display: flex;
|
|
9
|
-
flex-direction: column;
|
|
10
|
-
pointer-events: none;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
.overlay {
|
|
14
|
-
position: absolute;
|
|
15
|
-
top: 0;
|
|
16
|
-
left: 0;
|
|
17
|
-
right: 0;
|
|
18
|
-
bottom: 0;
|
|
19
|
-
background: var(--accent-orange-70);
|
|
20
|
-
pointer-events: auto;
|
|
21
|
-
backdrop-filter: blur(4px);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.overlayVisible {
|
|
25
|
-
opacity: 1;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.sheet {
|
|
29
|
-
position: absolute;
|
|
30
|
-
left: 0;
|
|
31
|
-
right: 0;
|
|
32
|
-
bottom: 0;
|
|
33
|
-
border-radius: 24px 24px 0 0;
|
|
34
|
-
pointer-events: auto;
|
|
35
|
-
height: 100vh;
|
|
36
|
-
display: flex;
|
|
37
|
-
flex-direction: column;
|
|
38
|
-
overflow: hidden;
|
|
39
|
-
margin: 0;
|
|
40
|
-
padding: 0;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.sheetOpen {
|
|
44
|
-
transform: translateY(0);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.header {
|
|
48
|
-
position: sticky;
|
|
49
|
-
top: 0;
|
|
50
|
-
background: transparent;
|
|
51
|
-
backdrop-filter: blur(8px);
|
|
52
|
-
display: flex;
|
|
53
|
-
justify-content: center;
|
|
54
|
-
z-index: 1;
|
|
55
|
-
padding: 1rem 0;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.closeButton {
|
|
59
|
-
width: 40px;
|
|
60
|
-
height: 40px;
|
|
61
|
-
border-radius: 50%;
|
|
62
|
-
border: none;
|
|
63
|
-
background: var(--accent-orange);
|
|
64
|
-
color: white;
|
|
65
|
-
font-size: 24px;
|
|
66
|
-
cursor: pointer;
|
|
67
|
-
display: flex;
|
|
68
|
-
align-items: center;
|
|
69
|
-
justify-content: center;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.content {
|
|
73
|
-
flex: 1;
|
|
74
|
-
padding: 1rem 1rem 0 1rem;
|
|
75
|
-
background: var(--light-orange-background);
|
|
76
|
-
border-radius: 24px 24px 0 0;
|
|
77
|
-
overflow-y: auto;
|
|
78
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import styles from './bottom-sheet.module.css';
|
|
5
|
-
import { motion, animate } from "motion/react";
|
|
6
|
-
|
|
7
|
-
interface BottomSheetProps {
|
|
8
|
-
isOpen: boolean;
|
|
9
|
-
onClose: () => void;
|
|
10
|
-
children: React.ReactNode;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export default function BottomSheet({ isOpen, onClose, children }: BottomSheetProps) {
|
|
14
|
-
const [isAnimating, setIsAnimating] = useState(isOpen);
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
if (!isOpen) return;
|
|
18
|
-
|
|
19
|
-
setIsAnimating(true);
|
|
20
|
-
document.body.style.overflow = 'hidden';
|
|
21
|
-
animate(".bottom-sheet", { y: ["100%", "0%"] }, { duration: 0.5 });
|
|
22
|
-
animate(".overlay", { opacity: [0, 1] }, { duration: 0.3 });
|
|
23
|
-
|
|
24
|
-
return () => {
|
|
25
|
-
document.body.style.overflow = 'unset';
|
|
26
|
-
};
|
|
27
|
-
}, [isOpen]);
|
|
28
|
-
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
if (!isOpen && isAnimating) {
|
|
31
|
-
animate(".bottom-sheet", { y: ["0%", "100%"] }, { duration: 0.5 });
|
|
32
|
-
animate(".overlay", { opacity: [1, 0] }, { duration: 0.3 });
|
|
33
|
-
const timer = setTimeout(() => setIsAnimating(false), 500);
|
|
34
|
-
return () => clearTimeout(timer);
|
|
35
|
-
}
|
|
36
|
-
}, [isOpen, isAnimating]);
|
|
37
|
-
|
|
38
|
-
if (!isOpen && !isAnimating) return null;
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<div className={styles.container}>
|
|
42
|
-
<motion.div
|
|
43
|
-
className={`${styles.overlay} overlay`}
|
|
44
|
-
onClick={onClose}
|
|
45
|
-
/>
|
|
46
|
-
<motion.div
|
|
47
|
-
className={`${styles.sheet} bottom-sheet`}
|
|
48
|
-
>
|
|
49
|
-
<div className={styles.header}>
|
|
50
|
-
<button className={styles.closeButton} onClick={onClose}>
|
|
51
|
-
<span>×</span>
|
|
52
|
-
</button>
|
|
53
|
-
</div>
|
|
54
|
-
<div className={styles.content}>
|
|
55
|
-
{children}
|
|
56
|
-
</div>
|
|
57
|
-
</motion.div>
|
|
58
|
-
</div>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
.breadcrumb {
|
|
2
|
-
padding: 1rem 0;
|
|
3
|
-
margin: 0 auto;
|
|
4
|
-
max-width: var(--max-content-width);
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
.breadcrumb ol {
|
|
8
|
-
list-style: none;
|
|
9
|
-
margin: 0;
|
|
10
|
-
padding: 0;
|
|
11
|
-
display: flex;
|
|
12
|
-
flex-wrap: wrap;
|
|
13
|
-
gap: 0.25rem;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.breadcrumb li {
|
|
17
|
-
display: flex;
|
|
18
|
-
align-items: center;
|
|
19
|
-
color: var(--primary-text);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.breadcrumb li:not(:last-child)::after {
|
|
23
|
-
content: '/';
|
|
24
|
-
margin: 0 0.25rem;
|
|
25
|
-
color: var(--secondary-text);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.breadcrumb a {
|
|
29
|
-
color: var(--primary-text);
|
|
30
|
-
text-decoration: none;
|
|
31
|
-
transition: color 0.2s ease;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.breadcrumb a:hover {
|
|
35
|
-
color: var(--accent-turquoise);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.breadcrumb span {
|
|
39
|
-
color: var(--grey-text);
|
|
40
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import Link from 'next/link';
|
|
4
|
-
import { usePathname } from 'next/navigation';
|
|
5
|
-
import styles from './breadcrumb.module.css';
|
|
6
|
-
|
|
7
|
-
export default function Breadcrumb() {
|
|
8
|
-
const pathname = usePathname();
|
|
9
|
-
const paths = pathname.split('/').filter(Boolean);
|
|
10
|
-
|
|
11
|
-
return (
|
|
12
|
-
<nav className={styles.breadcrumb} aria-label="Breadcrumb">
|
|
13
|
-
<ol>
|
|
14
|
-
<li>
|
|
15
|
-
<Link href="/">Home</Link>
|
|
16
|
-
</li>
|
|
17
|
-
{paths.map((path, index) => {
|
|
18
|
-
const href = `/${paths.slice(0, index + 1).join('/')}`;
|
|
19
|
-
const isLast = index === paths.length - 1;
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<li key={href}>
|
|
23
|
-
{isLast ? (
|
|
24
|
-
<span>{path.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}</span>
|
|
25
|
-
) : (
|
|
26
|
-
<Link href={href}>
|
|
27
|
-
{path.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
|
28
|
-
</Link>
|
|
29
|
-
)}
|
|
30
|
-
</li>
|
|
31
|
-
);
|
|
32
|
-
})}
|
|
33
|
-
</ol>
|
|
34
|
-
</nav>
|
|
35
|
-
);
|
|
36
|
-
}
|