@ticketboothapp/booking 0.1.18 → 0.1.20
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 +1 -1
- package/src/components/BookingWidget.tsx +282 -26
- package/src/components/ManageBookingView.tsx +75 -23
- 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/PickupLocationSelector.tsx +1 -1
- package/src/index.ts +3 -1
- package/src/app/photo-sessions/photo-packages.ts +0 -75
- package/src/assets/icons/minus.svg +0 -7
- package/src/assets/icons/partner-logos/getyourguide.svg +0 -8
- package/src/assets/icons/plus.svg +0 -3
- package/src/colours.css +0 -23
- package/src/components/BookingDetails.module.css +0 -1591
- package/src/components/BookingDetails.tsx +0 -2264
- package/src/components/JobApplicationDialog.module.css +0 -440
- package/src/components/JobApplicationDialog.tsx +0 -620
- package/src/components/PhoneInputWithCountry.module.css +0 -131
- package/src/components/PhoneInputWithCountry.tsx +0 -44
- package/src/components/PickupLocationDialog.module.css +0 -360
- package/src/components/PickupLocationDialog.tsx +0 -357
- package/src/components/PickupLocationMap.tsx +0 -110
- package/src/components/PostBookingDependentAddOnUpsell.module.css +0 -174
- package/src/components/PostBookingDependentAddOnUpsell.tsx +0 -407
- 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/button.css +0 -245
- package/src/components/button.tsx +0 -152
- package/src/components/client-bottom-sheet.tsx +0 -14
- package/src/components/colorable-svg.tsx +0 -29
- 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/image.css +0 -29
- package/src/components/image.tsx +0 -113
- 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-tag.module.css +0 -30
- package/src/components/product-tag.tsx +0 -34
- 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/image-modal.tsx +0 -248
- package/src/components/product-theme-pages/photo-gallery.module.css +0 -200
- 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/terms/TermsContent.tsx +0 -178
- 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-pill.module.css +0 -59
- package/src/components/value-pill.tsx +0 -46
- 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/images.ts +0 -556
- 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/pill-values.ts +0 -210
- package/src/constants/products.ts +0 -155
- 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/data/dap-descriptions/session-couples-families-friends.en.json +0 -61
- package/src/data/dap-descriptions/session-elopements.en.json +0 -60
- package/src/data/dap-descriptions/session-proposals.en.json +0 -60
- package/src/data/product-descriptions/afternoon-delight.en.json +0 -35
- package/src/data/product-descriptions/emerald-lake-escape.en.json +0 -68
- package/src/data/product-descriptions/lake-louise-adventure.en.json +0 -74
- package/src/data/product-descriptions/moraine-lake-adventure.en.json +0 -78
- package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +0 -65
- package/src/data/product-descriptions/moraine-lake-sunrise.en.json +0 -64
- package/src/data/product-descriptions/private-tour.en.json +0 -80
- package/src/data/product-descriptions/two-lakes-combo.en.json +0 -65
- package/src/data/products-config.json +0 -101
- package/src/hooks/use-bottom-sheet.tsx +0 -15
- package/src/hooks/use-simple-modal.tsx +0 -27
- package/src/hooks/useBookingSourceMetadataFromLocation.ts +0 -21
- package/src/hooks/useEmailSubscription.tsx +0 -103
- package/src/hooks/useEmbeddedInIframe.ts +0 -16
- package/src/hooks/useIsBookingLaunchLive.ts +0 -49
- package/src/hooks/useQuiz.tsx +0 -210
- package/src/providers/bottom-sheet-provider.tsx +0 -40
- package/src/providers/dependent-add-on-dialog-provider.tsx +0 -105
- package/src/radius.css +0 -5
- package/src/spacing.css +0 -7
- package/src/strings/en.json +0 -1774
- package/src/strings/es.json +0 -1573
- package/src/strings/fr.json +0 -1573
- package/src/strings/index.js +0 -23
- package/src/text-style.css +0 -97
- package/src/types/fareharbor.d.ts +0 -12
- package/src/types/quiz.ts +0 -59
- package/src/utils/currency-converter.ts +0 -101
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
-
import { db } from '@/lib/firebase';
|
|
5
|
-
import { collection, addDoc, query, where, getDocs } from 'firebase/firestore';
|
|
6
|
-
|
|
7
|
-
interface UseEmailSubscriptionProps {
|
|
8
|
-
initialEmail?: string;
|
|
9
|
-
autoSubmit?: boolean;
|
|
10
|
-
source?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function useEmailSubscription({ initialEmail = '', autoSubmit = false, source }: UseEmailSubscriptionProps) {
|
|
14
|
-
const [email, setEmail] = useState(initialEmail);
|
|
15
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
16
|
-
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error' | 'duplicate'>('idle');
|
|
17
|
-
|
|
18
|
-
const subscribeEmail = useCallback(async (emailToSubscribe: string) => {
|
|
19
|
-
if (!emailToSubscribe || isSubmitting) return false;
|
|
20
|
-
|
|
21
|
-
setIsSubmitting(true);
|
|
22
|
-
setSubmitStatus('idle');
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
// Check if email already exists
|
|
26
|
-
const emailQuery = query(
|
|
27
|
-
collection(db, 'mail'),
|
|
28
|
-
where('to', '==', emailToSubscribe)
|
|
29
|
-
);
|
|
30
|
-
const existingEmails = await getDocs(emailQuery);
|
|
31
|
-
|
|
32
|
-
if (!existingEmails.empty) {
|
|
33
|
-
setSubmitStatus('duplicate');
|
|
34
|
-
setIsSubmitting(false);
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Add email to Firestore (this will trigger your email function)
|
|
39
|
-
await addDoc(collection(db, 'mail'), {
|
|
40
|
-
to: emailToSubscribe,
|
|
41
|
-
message: {
|
|
42
|
-
subject: 'Via Via Moraine Lake Shuttle - 2026 Season Updates',
|
|
43
|
-
text: `Hi there! Thank you for subscribing to our updates! We're excited to keep you informed about the 2026 season at Moraine Lake, Lake Louise, and Emerald Lake. Moraine Lake Road is currently closed as Parks Canada closes the access road every year for the winter season. We'll get back to you with updates about 2026 ticket release dates and special offers as soon as we have more information. Best regards, The Via Via Team`,
|
|
44
|
-
html: `
|
|
45
|
-
<div style="font-family: Figtree, sans-serif; max-width: 700px; margin: 0 auto; padding: 20px;">
|
|
46
|
-
<h2 style="color: #ff4d00; text-align: center; margin-bottom: 20px;">Thanks for signing up!</h2>
|
|
47
|
-
|
|
48
|
-
<p style="color: inherit;">Hi there,</p>
|
|
49
|
-
<p style="color: inherit;">Thank you for subscribing to our updates! We're excited to keep you informed about the 2026 season at Moraine Lake, Lake Louise, and Emerald Lake.</p>
|
|
50
|
-
|
|
51
|
-
<p style="color: inherit;">Moraine Lake Road is currently closed as Parks Canada closes the access road every year for the winter season.</p>
|
|
52
|
-
|
|
53
|
-
<p style="color: inherit;">We'll get back to you with updates about 2026 ticket release dates and special offers as soon as we have more information.</p>
|
|
54
|
-
|
|
55
|
-
<p style="color: inherit;">In the meantime, feel free to follow us on <a href="https://www.instagram.com/viaviamorainelake/" style="color: #ff4d00; -webkit-text-size-adjust: 100%;">Instagram</a> for the latest news and beautiful photos from Banff National Park.</p>
|
|
56
|
-
|
|
57
|
-
<p style="color: inherit;">Best regards,<br>
|
|
58
|
-
The Via Via Team 🌄🚐</p>
|
|
59
|
-
|
|
60
|
-
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
|
61
|
-
|
|
62
|
-
<div style="text-align: center; margin-bottom: 30px;">
|
|
63
|
-
<img src="https://viaviamorainelake.b-cdn.net/logo192.png?format=png&width=200" alt="Via Via Moraine Lake Shuttle" style="max-width: 200px; height: auto;">
|
|
64
|
-
</div>
|
|
65
|
-
<p style="font-size: 12px; color: #666; text-align: center;">
|
|
66
|
-
Via Via Moraine Lake Shuttle • Canmore, Alberta<br>
|
|
67
|
-
<a href="https://viaviamorainelake.com/?utm_source=email&utm_medium=sign-up-email&utm_campaign=2026-updates-signup-confirmation" style="color: #ff4d00;">viaviamorainelake.com</a><br>
|
|
68
|
-
<a href="https://viaviamorainelake.com/unsubscribe?email=${emailToSubscribe}" style="color: #ff4d00; font-size: 11px;">Unsubscribe</a>
|
|
69
|
-
</p>
|
|
70
|
-
</div>
|
|
71
|
-
`
|
|
72
|
-
},
|
|
73
|
-
createdAt: new Date(),
|
|
74
|
-
source: source || 'unknown'
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
setSubmitStatus('success');
|
|
78
|
-
setEmail('');
|
|
79
|
-
return true;
|
|
80
|
-
} catch (error) {
|
|
81
|
-
console.error('Email signup error:', error);
|
|
82
|
-
setSubmitStatus('error');
|
|
83
|
-
return false;
|
|
84
|
-
} finally {
|
|
85
|
-
setIsSubmitting(false);
|
|
86
|
-
}
|
|
87
|
-
}, [isSubmitting, source]);
|
|
88
|
-
|
|
89
|
-
// Auto-submit if requested and email is provided
|
|
90
|
-
useEffect(() => {
|
|
91
|
-
if (autoSubmit && initialEmail && submitStatus === 'idle') {
|
|
92
|
-
subscribeEmail(initialEmail);
|
|
93
|
-
}
|
|
94
|
-
}, [autoSubmit, initialEmail, submitStatus, subscribeEmail]);
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
email,
|
|
98
|
-
setEmail,
|
|
99
|
-
isSubmitting,
|
|
100
|
-
submitStatus,
|
|
101
|
-
subscribeEmail
|
|
102
|
-
};
|
|
103
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useLayoutEffect, useState } from 'react';
|
|
4
|
-
|
|
5
|
-
/** True when this document is loaded inside an iframe (e.g. partner portal manage-booking embed). */
|
|
6
|
-
export function useEmbeddedInIframe(): boolean {
|
|
7
|
-
const [embedded, setEmbedded] = useState(() => {
|
|
8
|
-
if (typeof window === 'undefined') return false;
|
|
9
|
-
return window.parent !== window.self;
|
|
10
|
-
});
|
|
11
|
-
useLayoutEffect(() => {
|
|
12
|
-
if (typeof window === 'undefined') return;
|
|
13
|
-
setEmbedded(window.parent !== window.self);
|
|
14
|
-
}, []);
|
|
15
|
-
return embedded;
|
|
16
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
4
|
-
import { BOOKING_LAUNCH_AT } from '@/lib/booking-constants';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Returns whether the full booking flow is live (past launch time).
|
|
8
|
-
* Before launch: shows partial flow (collage + tour description).
|
|
9
|
-
* After launch: shows full flow (calendar, checkout, etc.).
|
|
10
|
-
*
|
|
11
|
-
* Schedules a state update at launch time so users on the site at 8am
|
|
12
|
-
* see the full flow without refreshing.
|
|
13
|
-
*
|
|
14
|
-
* Dev override: ?booking_preview=full or ?booking_preview=partial in the URL
|
|
15
|
-
* forces the flow for local testing (e.g. localhost:3000/?booking_preview=partial).
|
|
16
|
-
*/
|
|
17
|
-
export function useIsBookingLaunchLive(): boolean {
|
|
18
|
-
const [isLive, setIsLive] = useState(false);
|
|
19
|
-
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
// Dev override for local testing
|
|
22
|
-
if (typeof window !== 'undefined') {
|
|
23
|
-
const params = new URLSearchParams(window.location.search);
|
|
24
|
-
const preview = params.get('booking_preview');
|
|
25
|
-
if (preview === 'full') {
|
|
26
|
-
setIsLive(true);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
if (preview === 'partial') {
|
|
30
|
-
setIsLive(false);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const launchMs = BOOKING_LAUNCH_AT.getTime();
|
|
36
|
-
const now = Date.now();
|
|
37
|
-
|
|
38
|
-
if (now >= launchMs) {
|
|
39
|
-
setIsLive(true);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const msUntil = launchMs - now;
|
|
44
|
-
const id = setTimeout(() => setIsLive(true), msUntil);
|
|
45
|
-
return () => clearTimeout(id);
|
|
46
|
-
}, []);
|
|
47
|
-
|
|
48
|
-
return isLive;
|
|
49
|
-
}
|
package/src/hooks/useQuiz.tsx
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { createContext, useContext, useCallback, useState, ReactNode, Dispatch, SetStateAction } from 'react';
|
|
4
|
-
import { QuizState, UserAnswers } from '@/types/quiz';
|
|
5
|
-
import { QUIZ_QUESTIONS } from '@/constants/booking-guide-quiz';
|
|
6
|
-
import { getQuizRecommendations } from '@/constants/quiz-recommendations';
|
|
7
|
-
|
|
8
|
-
interface QuizContextType {
|
|
9
|
-
state: QuizState;
|
|
10
|
-
setState: Dispatch<SetStateAction<QuizState>>;
|
|
11
|
-
submitAnswer: (questionId: string, answerId: string) => boolean;
|
|
12
|
-
toggleAnswer: (questionId: string, answerId: string) => void;
|
|
13
|
-
getSelectedAnswers: (questionId: string) => string[];
|
|
14
|
-
goToNextQuestion: () => void;
|
|
15
|
-
goToPreviousQuestion: () => void;
|
|
16
|
-
restartQuiz: () => void;
|
|
17
|
-
getRecommendedProducts: () => ReturnType<typeof getQuizRecommendations>;
|
|
18
|
-
getCurrentQuestion: () => typeof QUIZ_QUESTIONS[0] | undefined;
|
|
19
|
-
deselectAnswer: (questionId: string) => boolean;
|
|
20
|
-
getProgress: () => number;
|
|
21
|
-
getVisibleQuestions: () => typeof QUIZ_QUESTIONS[0][];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const QuizContext = createContext<QuizContextType | undefined>(undefined);
|
|
25
|
-
|
|
26
|
-
const initialState: QuizState = {
|
|
27
|
-
currentQuestionIndex: 0,
|
|
28
|
-
answers: {},
|
|
29
|
-
isComplete: false,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export function QuizProvider({ children }: { children: ReactNode }) {
|
|
33
|
-
const [state, setState] = useState<QuizState>(initialState);
|
|
34
|
-
|
|
35
|
-
const getVisibleQuestions = useCallback((answers = state.answers) => {
|
|
36
|
-
return QUIZ_QUESTIONS.filter(question => {
|
|
37
|
-
if (!question.showIf) return true;
|
|
38
|
-
return question.showIf(answers);
|
|
39
|
-
});
|
|
40
|
-
}, [state.answers]);
|
|
41
|
-
|
|
42
|
-
const getCurrentQuestion = useCallback(() => {
|
|
43
|
-
const visibleQuestions = getVisibleQuestions();
|
|
44
|
-
return visibleQuestions[state.currentQuestionIndex];
|
|
45
|
-
}, [state.currentQuestionIndex, getVisibleQuestions]);
|
|
46
|
-
|
|
47
|
-
const getSelectedAnswers = useCallback((questionId: string): string[] => {
|
|
48
|
-
const answer = state.answers[questionId];
|
|
49
|
-
if (!answer) return [];
|
|
50
|
-
|
|
51
|
-
const question = QUIZ_QUESTIONS.find(q => q.id === questionId);
|
|
52
|
-
if (question?.isMultiSelect) {
|
|
53
|
-
return Array.isArray(answer) ? answer : [answer];
|
|
54
|
-
}
|
|
55
|
-
return [answer as string];
|
|
56
|
-
}, [state.answers]);
|
|
57
|
-
|
|
58
|
-
const submitAnswer = useCallback((questionId: string, answerId: string) => {
|
|
59
|
-
const question = QUIZ_QUESTIONS.find(q => q.id === questionId);
|
|
60
|
-
|
|
61
|
-
setState(prev => {
|
|
62
|
-
if (question?.isMultiSelect) {
|
|
63
|
-
const currentAnswers = Array.isArray(prev.answers[questionId])
|
|
64
|
-
? prev.answers[questionId] as string[]
|
|
65
|
-
: [];
|
|
66
|
-
|
|
67
|
-
const updatedAnswers = currentAnswers.includes(answerId)
|
|
68
|
-
? currentAnswers.filter(id => id !== answerId)
|
|
69
|
-
: [...currentAnswers, answerId];
|
|
70
|
-
|
|
71
|
-
// If this is the activities question, handle hiking check
|
|
72
|
-
if (questionId === 'what_activities') {
|
|
73
|
-
const shouldContinue = updatedAnswers.includes('hiking');
|
|
74
|
-
return {
|
|
75
|
-
...prev,
|
|
76
|
-
answers: {
|
|
77
|
-
...prev.answers,
|
|
78
|
-
[questionId]: updatedAnswers
|
|
79
|
-
},
|
|
80
|
-
isComplete: !shouldContinue
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
...prev,
|
|
86
|
-
answers: {
|
|
87
|
-
...prev.answers,
|
|
88
|
-
[questionId]: updatedAnswers
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// For single select, just store the value directly
|
|
94
|
-
return {
|
|
95
|
-
...prev,
|
|
96
|
-
answers: {
|
|
97
|
-
...prev.answers,
|
|
98
|
-
[questionId]: answerId
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// Return true unless it's activities without hiking
|
|
104
|
-
if (questionId === 'what_activities') {
|
|
105
|
-
const answers = getSelectedAnswers(questionId);
|
|
106
|
-
return answers.includes('hiking');
|
|
107
|
-
}
|
|
108
|
-
return true;
|
|
109
|
-
}, [getSelectedAnswers]);
|
|
110
|
-
|
|
111
|
-
const toggleAnswer = useCallback((questionId: string, answerId: string) => {
|
|
112
|
-
setState(prev => {
|
|
113
|
-
const currentAnswers = Array.isArray(prev.answers[questionId])
|
|
114
|
-
? prev.answers[questionId] as string[]
|
|
115
|
-
: [];
|
|
116
|
-
|
|
117
|
-
const updatedAnswers = currentAnswers.includes(answerId)
|
|
118
|
-
? currentAnswers.filter(id => id !== answerId)
|
|
119
|
-
: [...currentAnswers, answerId];
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
...prev,
|
|
123
|
-
answers: {
|
|
124
|
-
...prev.answers,
|
|
125
|
-
[questionId]: updatedAnswers
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
});
|
|
129
|
-
}, []);
|
|
130
|
-
|
|
131
|
-
const goToNextQuestion = useCallback(() => {
|
|
132
|
-
setState(prev => ({
|
|
133
|
-
...prev,
|
|
134
|
-
currentQuestionIndex: prev.currentQuestionIndex + 1
|
|
135
|
-
}));
|
|
136
|
-
}, []);
|
|
137
|
-
|
|
138
|
-
const goToPreviousQuestion = useCallback(() => {
|
|
139
|
-
setState(prev => ({
|
|
140
|
-
...prev,
|
|
141
|
-
currentQuestionIndex: Math.max(0, prev.currentQuestionIndex - 1),
|
|
142
|
-
isComplete: false
|
|
143
|
-
}));
|
|
144
|
-
}, []);
|
|
145
|
-
|
|
146
|
-
const restartQuiz = useCallback(() => {
|
|
147
|
-
setState(initialState);
|
|
148
|
-
}, []);
|
|
149
|
-
|
|
150
|
-
const getRecommendedProducts = useCallback(() => {
|
|
151
|
-
return getQuizRecommendations(state.answers);
|
|
152
|
-
}, [state.answers]);
|
|
153
|
-
|
|
154
|
-
const deselectAnswer = useCallback((questionId: string) => {
|
|
155
|
-
const newAnswers = { ...state.answers };
|
|
156
|
-
delete newAnswers[questionId];
|
|
157
|
-
|
|
158
|
-
// Check if removing this answer makes any questions disappear
|
|
159
|
-
const nextVisible = QUIZ_QUESTIONS.filter(question => {
|
|
160
|
-
if (!question.showIf) return true;
|
|
161
|
-
return question.showIf(newAnswers);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// If we're currently on a question that will disappear, mark as complete
|
|
165
|
-
const willCurrentQuestionDisappear = state.currentQuestionIndex >= nextVisible.length;
|
|
166
|
-
|
|
167
|
-
setState(prev => ({
|
|
168
|
-
...prev,
|
|
169
|
-
answers: newAnswers,
|
|
170
|
-
isComplete: willCurrentQuestionDisappear
|
|
171
|
-
}));
|
|
172
|
-
|
|
173
|
-
return !willCurrentQuestionDisappear;
|
|
174
|
-
}, [state.currentQuestionIndex, state.answers]);
|
|
175
|
-
|
|
176
|
-
const getProgress = useCallback(() => {
|
|
177
|
-
const visibleQuestions = getVisibleQuestions();
|
|
178
|
-
return ((state.currentQuestionIndex + 1) / visibleQuestions.length) * 100;
|
|
179
|
-
}, [state.currentQuestionIndex, getVisibleQuestions]);
|
|
180
|
-
|
|
181
|
-
const value = {
|
|
182
|
-
state,
|
|
183
|
-
setState,
|
|
184
|
-
submitAnswer,
|
|
185
|
-
toggleAnswer,
|
|
186
|
-
getSelectedAnswers,
|
|
187
|
-
goToNextQuestion,
|
|
188
|
-
goToPreviousQuestion,
|
|
189
|
-
restartQuiz,
|
|
190
|
-
getRecommendedProducts,
|
|
191
|
-
getCurrentQuestion,
|
|
192
|
-
deselectAnswer,
|
|
193
|
-
getProgress,
|
|
194
|
-
getVisibleQuestions
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
return (
|
|
198
|
-
<QuizContext.Provider value={value}>
|
|
199
|
-
{children}
|
|
200
|
-
</QuizContext.Provider>
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function useQuiz() {
|
|
205
|
-
const context = useContext(QuizContext);
|
|
206
|
-
if (context === undefined) {
|
|
207
|
-
throw new Error('useQuiz must be used within a QuizProvider');
|
|
208
|
-
}
|
|
209
|
-
return context;
|
|
210
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { createContext, useContext, useState } from 'react';
|
|
4
|
-
import BottomSheet from '@/components/bottom-sheet';
|
|
5
|
-
|
|
6
|
-
type BottomSheetContextType = {
|
|
7
|
-
openSheet: (content: React.ReactNode) => void;
|
|
8
|
-
closeSheet: () => void;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const BottomSheetContext = createContext<BottomSheetContextType | null>(null);
|
|
12
|
-
|
|
13
|
-
export function BottomSheetProvider({ children }: { children: React.ReactNode }) {
|
|
14
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
15
|
-
const [content, setContent] = useState<React.ReactNode>(null);
|
|
16
|
-
|
|
17
|
-
const openSheet = (content: React.ReactNode) => {
|
|
18
|
-
setContent(content);
|
|
19
|
-
setIsOpen(true);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const closeSheet = () => {
|
|
23
|
-
setIsOpen(false);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<BottomSheetContext.Provider value={{ openSheet, closeSheet }}>
|
|
28
|
-
{children}
|
|
29
|
-
<BottomSheet isOpen={isOpen} onClose={closeSheet}>
|
|
30
|
-
{content}
|
|
31
|
-
</BottomSheet>
|
|
32
|
-
</BottomSheetContext.Provider>
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function useBottomSheet() {
|
|
37
|
-
const context = useContext(BottomSheetContext);
|
|
38
|
-
if (!context) throw new Error('useBottomSheet must be used within BottomSheetProvider');
|
|
39
|
-
return context;
|
|
40
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
createContext,
|
|
5
|
-
useCallback,
|
|
6
|
-
useContext,
|
|
7
|
-
useMemo,
|
|
8
|
-
useRef,
|
|
9
|
-
useState,
|
|
10
|
-
type ReactNode,
|
|
11
|
-
} from 'react';
|
|
12
|
-
import type { PhotoDapSlug } from '@/lib/photo-dap-config';
|
|
13
|
-
|
|
14
|
-
export type DependentAddOnProductOptionChoice = {
|
|
15
|
-
dependentAddOnProductOptionId: string;
|
|
16
|
-
label: string;
|
|
17
|
-
photosLabel?: string;
|
|
18
|
-
startingAtLabel?: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type DependentAddOnDialogOpenPayload = {
|
|
22
|
-
/** Card title shown in dialog header */
|
|
23
|
-
productDisplayTitle: string;
|
|
24
|
-
dependentAddOnProductId: string;
|
|
25
|
-
/** Fixed catalog option (no picker) */
|
|
26
|
-
dependentAddOnProductOptionId?: string;
|
|
27
|
-
/** When provided without a fixed option id, user picks one (e.g. 30 / 60 / 90 min) */
|
|
28
|
-
productOptions?: DependentAddOnProductOptionChoice[];
|
|
29
|
-
/**
|
|
30
|
-
* Default session-length id when multiple `productOptions` exist (e.g. manage-booking upsell probed 30 min).
|
|
31
|
-
* Unlike `dependentAddOnProductOptionId`, this does not hide the picker — the user can switch length.
|
|
32
|
-
*/
|
|
33
|
-
initialSelectedProductOptionId?: string;
|
|
34
|
-
/** Hero + grid images (DapFlowCollage); Bunny CDN IDs */
|
|
35
|
-
collageImageIds?: string[];
|
|
36
|
-
/** Loads expandable copy from dap-descriptions */
|
|
37
|
-
dapDescriptionSlug?: PhotoDapSlug;
|
|
38
|
-
/**
|
|
39
|
-
* From DAP catalog / TicketBooth product — days before the photo session for full-refund cancellation.
|
|
40
|
-
* Availability API may override when it returns the same field.
|
|
41
|
-
*/
|
|
42
|
-
cancellationDaysBeforeSession: number;
|
|
43
|
-
/**
|
|
44
|
-
* Pre-fill primary booking reference (e.g. manage-booking upsell after shuttle checkout).
|
|
45
|
-
* Accepts short or bookRef_ form; dialog normalizes for display.
|
|
46
|
-
*/
|
|
47
|
-
initialPrimaryBookingReference?: string;
|
|
48
|
-
/** Optional pre-fill for booking-owner verification on DAP availability checks. */
|
|
49
|
-
initialPrimaryBookingLastName?: string;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
interface DependentAddOnDialogContextValue {
|
|
53
|
-
isOpen: boolean;
|
|
54
|
-
payload: DependentAddOnDialogOpenPayload | null;
|
|
55
|
-
open: (p: DependentAddOnDialogOpenPayload) => void;
|
|
56
|
-
close: () => void;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const DependentAddOnDialogContext =
|
|
60
|
-
createContext<DependentAddOnDialogContextValue | null>(null);
|
|
61
|
-
|
|
62
|
-
export function useDependentAddOnDialog() {
|
|
63
|
-
const ctx = useContext(DependentAddOnDialogContext);
|
|
64
|
-
if (!ctx) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
'useDependentAddOnDialog must be used within DependentAddOnDialogProvider'
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
return ctx;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function DependentAddOnDialogProvider({ children }: { children: ReactNode }) {
|
|
73
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
74
|
-
const [payload, setPayload] = useState<DependentAddOnDialogOpenPayload | null>(null);
|
|
75
|
-
const previouslyFocusedRef = useRef<HTMLElement | null>(null);
|
|
76
|
-
|
|
77
|
-
const open = useCallback((p: DependentAddOnDialogOpenPayload) => {
|
|
78
|
-
previouslyFocusedRef.current =
|
|
79
|
-
document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
80
|
-
setPayload(p);
|
|
81
|
-
setIsOpen(true);
|
|
82
|
-
}, []);
|
|
83
|
-
|
|
84
|
-
const close = useCallback(() => {
|
|
85
|
-
setIsOpen(false);
|
|
86
|
-
setPayload(null);
|
|
87
|
-
const prev = previouslyFocusedRef.current;
|
|
88
|
-
requestAnimationFrame(() => {
|
|
89
|
-
if (prev && typeof prev.focus === 'function') {
|
|
90
|
-
prev.focus();
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}, []);
|
|
94
|
-
|
|
95
|
-
const value = useMemo(
|
|
96
|
-
() => ({ isOpen, payload, open, close }),
|
|
97
|
-
[isOpen, payload, open, close]
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<DependentAddOnDialogContext.Provider value={value}>
|
|
102
|
-
{children}
|
|
103
|
-
</DependentAddOnDialogContext.Provider>
|
|
104
|
-
);
|
|
105
|
-
}
|
package/src/radius.css
DELETED