@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.
Files changed (106) hide show
  1. package/package.json +2 -1
  2. package/src/components/BookingWidget.tsx +282 -26
  3. package/src/components/ManageBookingView.tsx +75 -23
  4. package/src/components/PostBookingDependentAddOnUpsell.tsx +1 -1
  5. package/src/components/booking/BookingProductGrid.tsx +1 -1
  6. package/src/components/booking/Calendar.module.css +3 -3
  7. package/src/components/booking/CheckoutForm.tsx +1 -1
  8. package/src/components/booking/InfoTooltip.tsx +2 -13
  9. package/src/components/booking/PickupLocationSelector.tsx +2 -2
  10. package/src/components/booking/PriceBreakdown.tsx +11 -34
  11. package/src/index.ts +3 -1
  12. package/tsconfig.json +1 -1
  13. package/src/components/JobApplicationDialog.module.css +0 -440
  14. package/src/components/JobApplicationDialog.tsx +0 -620
  15. package/src/components/PickupLocationMap.tsx +0 -110
  16. package/src/components/accordion.css +0 -27
  17. package/src/components/accordion.tsx +0 -29
  18. package/src/components/analytics/AnalyticsConsentRestore.tsx +0 -19
  19. package/src/components/analytics/AnalyticsScripts.tsx +0 -106
  20. package/src/components/analytics/CookieConsentBanner.css +0 -86
  21. package/src/components/analytics/CookieConsentBanner.tsx +0 -102
  22. package/src/components/bottom-sheet.module.css +0 -78
  23. package/src/components/bottom-sheet.tsx +0 -60
  24. package/src/components/breadcrumb.module.css +0 -40
  25. package/src/components/breadcrumb.tsx +0 -36
  26. package/src/components/client-bottom-sheet.tsx +0 -14
  27. package/src/components/conditional-footer.tsx +0 -27
  28. package/src/components/contact-us.module.css +0 -147
  29. package/src/components/contact-us.tsx +0 -49
  30. package/src/components/email-signup.css +0 -151
  31. package/src/components/email-signup.tsx +0 -63
  32. package/src/components/faq-wrapper.module.css +0 -47
  33. package/src/components/faq-wrapper.tsx +0 -15
  34. package/src/components/footer.css +0 -187
  35. package/src/components/footer.tsx +0 -143
  36. package/src/components/global-simple-modal.tsx +0 -33
  37. package/src/components/google-review-summary.module.css +0 -77
  38. package/src/components/google-review-summary.tsx +0 -50
  39. package/src/components/hero-image.css +0 -13
  40. package/src/components/hero-image.tsx +0 -44
  41. package/src/components/language-aware-link.tsx +0 -72
  42. package/src/components/language-switcher.module.css +0 -124
  43. package/src/components/language-switcher.tsx +0 -75
  44. package/src/components/map-section.css +0 -59
  45. package/src/components/map-section.tsx +0 -63
  46. package/src/components/navbar.module.css +0 -152
  47. package/src/components/navbar.tsx +0 -125
  48. package/src/components/parallax-provider.tsx +0 -11
  49. package/src/components/product-theme-pages/best-option.module.css +0 -70
  50. package/src/components/product-theme-pages/best-option.tsx +0 -35
  51. package/src/components/product-theme-pages/extended-tour-options.module.css +0 -22
  52. package/src/components/product-theme-pages/extended-tour-options.tsx +0 -11
  53. package/src/components/product-theme-pages/photo-gallery.tsx +0 -90
  54. package/src/components/product-theme-pages/product-theme-page-layout.module.css +0 -13
  55. package/src/components/product-theme-pages/product-theme-page-layout.tsx +0 -67
  56. package/src/components/product-theme-pages/top-of-fold.module.css +0 -179
  57. package/src/components/product-theme-pages/top-of-fold.tsx +0 -80
  58. package/src/components/product-tile/image-only-product-tile-desktop.module.css +0 -106
  59. package/src/components/product-tile/image-only-product-tile-desktop.tsx +0 -56
  60. package/src/components/product-tile/image-only-product-tile-mobile.module.css +0 -122
  61. package/src/components/product-tile/image-only-product-tile-mobile.tsx +0 -89
  62. package/src/components/product-tile/image-only-product-tile.tsx +0 -44
  63. package/src/components/product-tile/product-tile-card.module.css +0 -84
  64. package/src/components/product-tile/product-tile-card.tsx +0 -61
  65. package/src/components/review-highlights-section.css +0 -85
  66. package/src/components/review-highlights-section.tsx +0 -127
  67. package/src/components/season-closure-overlay.module.css +0 -99
  68. package/src/components/season-closure-overlay.tsx +0 -98
  69. package/src/components/simple-modal.tsx +0 -69
  70. package/src/components/simple-top-of-fold.module.css +0 -76
  71. package/src/components/simple-top-of-fold.tsx +0 -34
  72. package/src/components/spacer.css +0 -41
  73. package/src/components/spacer.tsx +0 -23
  74. package/src/components/star-rating.module.css +0 -74
  75. package/src/components/star-rating.tsx +0 -48
  76. package/src/components/title-subtitle.module.css +0 -10
  77. package/src/components/title-subtitle.tsx +0 -30
  78. package/src/components/translatable-reviews.tsx +0 -75
  79. package/src/components/value-props.css +0 -185
  80. package/src/components/value-props.tsx +0 -88
  81. package/src/constants/booking-guide-quiz.ts +0 -64
  82. package/src/constants/contact-info.ts +0 -2
  83. package/src/constants/faq.ts +0 -44
  84. package/src/constants/json-ld/faq-json-ld.tsx +0 -170
  85. package/src/constants/json-ld/homepage-json-ld.tsx +0 -138
  86. package/src/constants/json-ld/job-posting-json-ld.tsx +0 -92
  87. package/src/constants/json-ld/organization-json-ld.tsx +0 -62
  88. package/src/constants/json-ld/page-json-ld.tsx +0 -6
  89. package/src/constants/json-ld/product-json-ld.tsx +0 -154
  90. package/src/constants/json-ld/review-json-ld.tsx +0 -377
  91. package/src/constants/navigation-links/footer-links.ts +0 -48
  92. package/src/constants/navigation-links/nav-bar-links.ts +0 -41
  93. package/src/constants/navigation-links/navigation-link.ts +0 -6
  94. package/src/constants/quiz-recommendations.ts +0 -506
  95. package/src/constants/reviews.ts +0 -75
  96. package/src/constants/staff.ts +0 -197
  97. package/src/constants/value-props.ts +0 -58
  98. package/src/hooks/use-bottom-sheet.tsx +0 -15
  99. package/src/hooks/use-simple-modal.tsx +0 -27
  100. package/src/hooks/useEmailSubscription.tsx +0 -103
  101. package/src/hooks/useEmbeddedInIframe.ts +0 -16
  102. package/src/hooks/useQuiz.tsx +0 -210
  103. package/src/providers/bottom-sheet-provider.tsx +0 -40
  104. package/src/types/fareharbor.d.ts +0 -12
  105. package/src/types/quiz.ts +0 -59
  106. /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
- }