@ticketboothapp/booking 0.1.11 → 0.1.12

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 (252) hide show
  1. package/package.json +1 -1
  2. package/src/colours.css +23 -0
  3. package/src/components/BookingDetails.module.css +1591 -0
  4. package/src/components/BookingDetails.tsx +2072 -354
  5. package/src/components/BookingWidget.tsx +28 -248
  6. package/src/components/JobApplicationDialog.module.css +440 -0
  7. package/src/components/JobApplicationDialog.tsx +620 -0
  8. package/src/components/ManageBookingView.tsx +28 -36
  9. package/src/components/PhoneInputWithCountry.module.css +131 -0
  10. package/src/components/PhoneInputWithCountry.tsx +44 -0
  11. package/src/components/PickupLocationDialog.module.css +360 -0
  12. package/src/components/PickupLocationDialog.tsx +357 -0
  13. package/src/components/PickupLocationMap.tsx +110 -0
  14. package/src/components/PostBookingDependentAddOnUpsell.module.css +174 -0
  15. package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -0
  16. package/src/components/accordion.css +27 -0
  17. package/src/components/accordion.tsx +29 -0
  18. package/src/components/analytics/AnalyticsConsentRestore.tsx +19 -0
  19. package/src/components/analytics/AnalyticsScripts.tsx +106 -0
  20. package/src/components/analytics/CookieConsentBanner.css +86 -0
  21. package/src/components/analytics/CookieConsentBanner.tsx +102 -0
  22. package/src/components/booking/AddOnsSection.module.css +10 -0
  23. package/src/components/booking/AddOnsSection.tsx +184 -0
  24. package/src/components/booking/AdminPaymentChoiceModal.tsx +98 -0
  25. package/src/components/booking/BookingDialog.module.css +643 -0
  26. package/src/components/booking/BookingDialog.tsx +356 -0
  27. package/src/components/booking/BookingFlow.tsx +4385 -0
  28. package/src/components/booking/BookingFlowCollage.module.css +148 -0
  29. package/src/components/booking/BookingFlowCollage.tsx +184 -0
  30. package/src/components/booking/BookingFlowPlaceholder.module.css +27 -0
  31. package/src/components/booking/BookingFlowPlaceholder.tsx +25 -0
  32. package/src/components/booking/BookingFlowPreview.tsx +51 -0
  33. package/src/components/booking/BookingProductGrid.module.css +359 -0
  34. package/src/components/booking/BookingProductGrid.tsx +497 -0
  35. package/src/components/booking/Calendar.module.css +616 -0
  36. package/src/components/{Calendar.tsx → booking/Calendar.tsx} +464 -247
  37. package/src/components/booking/CancellationPolicySelector.module.css +124 -0
  38. package/src/components/booking/CancellationPolicySelector.tsx +142 -0
  39. package/src/components/booking/ChangeBookingDialog.tsx +562 -0
  40. package/src/components/booking/CheckoutForm.module.css +244 -0
  41. package/src/components/booking/CheckoutForm.tsx +364 -0
  42. package/src/components/{CheckoutModal.tsx → booking/CheckoutModal.tsx} +176 -19
  43. package/src/components/booking/DapFlowCollage.tsx +88 -0
  44. package/src/components/booking/DapTourDescription.tsx +35 -0
  45. package/src/components/booking/DependentAddOnBookingDialog.tsx +1350 -0
  46. package/src/components/booking/DependentAddOnPaymentForm.tsx +124 -0
  47. package/src/components/booking/InfoTooltip.tsx +108 -0
  48. package/src/components/booking/ItineraryBox.module.css +258 -0
  49. package/src/components/booking/ItineraryBox.tsx +550 -0
  50. package/src/components/{ItineraryBuilder.tsx → booking/ItineraryBuilder.tsx} +1 -2
  51. package/src/components/booking/ItineraryPlaceholder.module.css +45 -0
  52. package/src/components/booking/ItineraryPlaceholder.tsx +26 -0
  53. package/src/components/{MealDrinkAddOnSelector.tsx → booking/MealDrinkAddOnSelector.tsx} +21 -13
  54. package/src/components/booking/PickupLocationSelector.module.css +124 -0
  55. package/src/components/{PickupLocationSelector.tsx → booking/PickupLocationSelector.tsx} +315 -290
  56. package/src/components/booking/PickupTimeSelector.module.css +134 -0
  57. package/src/components/booking/PickupTimeSelector.tsx +112 -0
  58. package/src/components/{PriceBreakdown.tsx → booking/PriceBreakdown.tsx} +3 -3
  59. package/src/components/{PriceSummary.tsx → booking/PriceSummary.tsx} +51 -28
  60. package/src/components/booking/PrivateShuttleBookingFlow.module.css +357 -0
  61. package/src/components/booking/PrivateShuttleBookingFlow.tsx +2662 -0
  62. package/src/components/booking/PromoCodeInput.module.css +166 -0
  63. package/src/components/booking/PromoCodeInput.tsx +99 -0
  64. package/src/components/booking/ReturnTimeSelector.module.css +173 -0
  65. package/src/components/booking/ReturnTimeSelector.tsx +145 -0
  66. package/src/components/{TermsAcceptance.tsx → booking/TermsAcceptance.tsx} +9 -8
  67. package/src/components/booking/TicketSelector.module.css +164 -0
  68. package/src/components/booking/TicketSelector.tsx +199 -0
  69. package/src/components/booking/TourDescription.module.css +304 -0
  70. package/src/components/booking/TourDescription.tsx +273 -0
  71. package/src/components/booking/booking-flow-ui.ts +15 -1
  72. package/src/components/booking/booking-flow.css +944 -0
  73. package/src/components/bottom-sheet.module.css +78 -0
  74. package/src/components/bottom-sheet.tsx +60 -0
  75. package/src/components/breadcrumb.module.css +40 -0
  76. package/src/components/breadcrumb.tsx +36 -0
  77. package/src/components/button.css +245 -0
  78. package/src/components/button.tsx +152 -0
  79. package/src/components/client-bottom-sheet.tsx +14 -0
  80. package/src/components/colorable-svg.tsx +29 -0
  81. package/src/components/conditional-footer.tsx +27 -0
  82. package/src/components/contact-us.module.css +147 -0
  83. package/src/components/contact-us.tsx +49 -0
  84. package/src/components/email-signup.css +151 -0
  85. package/src/components/email-signup.tsx +63 -0
  86. package/src/components/faq-wrapper.module.css +47 -0
  87. package/src/components/faq-wrapper.tsx +15 -0
  88. package/src/components/footer.css +187 -0
  89. package/src/components/footer.tsx +143 -0
  90. package/src/components/global-simple-modal.tsx +33 -0
  91. package/src/components/google-review-summary.module.css +77 -0
  92. package/src/components/google-review-summary.tsx +50 -0
  93. package/src/components/hero-image.css +13 -0
  94. package/src/components/hero-image.tsx +44 -0
  95. package/src/components/image.css +29 -0
  96. package/src/components/image.tsx +113 -0
  97. package/src/components/language-aware-link.tsx +72 -0
  98. package/src/components/language-switcher.module.css +124 -0
  99. package/src/components/language-switcher.tsx +75 -0
  100. package/src/components/map-section.css +59 -0
  101. package/src/components/map-section.tsx +63 -0
  102. package/src/components/navbar.module.css +152 -0
  103. package/src/components/navbar.tsx +125 -0
  104. package/src/components/parallax-provider.tsx +11 -0
  105. package/src/components/partner/PartnerBookingPage.module.css +130 -0
  106. package/src/components/partner/PartnerBookingPage.tsx +390 -0
  107. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +19 -35
  108. package/src/components/product-tag.module.css +30 -0
  109. package/src/components/product-tag.tsx +34 -0
  110. package/src/components/product-theme-pages/best-option.module.css +70 -0
  111. package/src/components/product-theme-pages/best-option.tsx +35 -0
  112. package/src/components/product-theme-pages/extended-tour-options.module.css +22 -0
  113. package/src/components/product-theme-pages/extended-tour-options.tsx +11 -0
  114. package/src/components/product-theme-pages/image-modal.tsx +248 -0
  115. package/src/components/product-theme-pages/photo-gallery.module.css +200 -0
  116. package/src/components/product-theme-pages/photo-gallery.tsx +90 -0
  117. package/src/components/product-theme-pages/product-theme-page-layout.module.css +13 -0
  118. package/src/components/product-theme-pages/product-theme-page-layout.tsx +67 -0
  119. package/src/components/product-theme-pages/top-of-fold.module.css +179 -0
  120. package/src/components/product-theme-pages/top-of-fold.tsx +80 -0
  121. package/src/components/product-tile/image-only-product-tile-desktop.module.css +106 -0
  122. package/src/components/product-tile/image-only-product-tile-desktop.tsx +56 -0
  123. package/src/components/product-tile/image-only-product-tile-mobile.module.css +122 -0
  124. package/src/components/product-tile/image-only-product-tile-mobile.tsx +89 -0
  125. package/src/components/product-tile/image-only-product-tile.tsx +44 -0
  126. package/src/components/product-tile/product-tile-card.module.css +84 -0
  127. package/src/components/product-tile/product-tile-card.tsx +61 -0
  128. package/src/components/review-highlights-section.css +85 -0
  129. package/src/components/review-highlights-section.tsx +127 -0
  130. package/src/components/season-closure-overlay.module.css +99 -0
  131. package/src/components/season-closure-overlay.tsx +98 -0
  132. package/src/components/simple-modal.tsx +69 -0
  133. package/src/components/simple-top-of-fold.module.css +76 -0
  134. package/src/components/simple-top-of-fold.tsx +34 -0
  135. package/src/components/spacer.css +41 -0
  136. package/src/components/spacer.tsx +23 -0
  137. package/src/components/star-rating.module.css +74 -0
  138. package/src/components/star-rating.tsx +48 -0
  139. package/src/components/terms/TermsContent.tsx +178 -0
  140. package/src/components/title-subtitle.module.css +10 -0
  141. package/src/components/title-subtitle.tsx +30 -0
  142. package/src/components/translatable-reviews.tsx +75 -0
  143. package/src/components/value-pill.module.css +59 -0
  144. package/src/components/value-pill.tsx +46 -0
  145. package/src/components/value-props.css +185 -0
  146. package/src/components/value-props.tsx +88 -0
  147. package/src/constants/booking-guide-quiz.ts +64 -0
  148. package/src/constants/contact-info.ts +2 -0
  149. package/src/constants/faq.ts +44 -0
  150. package/src/constants/images.ts +556 -0
  151. package/src/constants/json-ld/faq-json-ld.tsx +170 -0
  152. package/src/constants/json-ld/homepage-json-ld.tsx +138 -0
  153. package/src/constants/json-ld/job-posting-json-ld.tsx +92 -0
  154. package/src/constants/json-ld/organization-json-ld.tsx +62 -0
  155. package/src/constants/json-ld/page-json-ld.tsx +6 -0
  156. package/src/constants/json-ld/product-json-ld.tsx +154 -0
  157. package/src/constants/json-ld/review-json-ld.tsx +377 -0
  158. package/src/constants/navigation-links/footer-links.ts +48 -0
  159. package/src/constants/navigation-links/nav-bar-links.ts +41 -0
  160. package/src/constants/navigation-links/navigation-link.ts +6 -0
  161. package/src/constants/pill-values.ts +210 -0
  162. package/src/constants/products.ts +155 -0
  163. package/src/constants/quiz-recommendations.ts +506 -0
  164. package/src/constants/reviews.ts +75 -0
  165. package/src/constants/staff.ts +197 -0
  166. package/src/constants/value-props.ts +58 -0
  167. package/src/data/dap-descriptions/session-couples-families-friends.en.json +61 -0
  168. package/src/data/dap-descriptions/session-elopements.en.json +60 -0
  169. package/src/data/dap-descriptions/session-proposals.en.json +60 -0
  170. package/src/data/product-descriptions/afternoon-delight.en.json +35 -0
  171. package/src/data/product-descriptions/emerald-lake-escape.en.json +68 -0
  172. package/src/data/product-descriptions/lake-louise-adventure.en.json +74 -0
  173. package/src/data/product-descriptions/moraine-lake-adventure.en.json +78 -0
  174. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +65 -0
  175. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +64 -0
  176. package/src/data/product-descriptions/private-tour.en.json +80 -0
  177. package/src/data/product-descriptions/two-lakes-combo.en.json +65 -0
  178. package/src/data/products-config.json +101 -0
  179. package/src/hooks/use-bottom-sheet.tsx +15 -0
  180. package/src/hooks/use-simple-modal.tsx +27 -0
  181. package/src/hooks/useBookingSourceMetadataFromLocation.ts +21 -0
  182. package/src/hooks/useEmailSubscription.tsx +103 -0
  183. package/src/hooks/useEmbeddedInIframe.ts +16 -0
  184. package/src/hooks/useIsBookingLaunchLive.ts +49 -0
  185. package/src/hooks/useQuiz.tsx +210 -0
  186. package/src/index.ts +27 -2
  187. package/src/lib/analytics.ts +197 -0
  188. package/src/lib/booking/booking-source.ts +20 -2
  189. package/src/lib/{checkout-breakdown.ts → booking/checkout-breakdown.ts} +1 -1
  190. package/src/lib/booking/correlation-id.ts +46 -0
  191. package/src/lib/{i18n → booking/i18n}/messages/en.json +48 -4
  192. package/src/lib/{i18n → booking/i18n}/messages/fr.json +48 -4
  193. package/src/lib/booking/itinerary-display.ts +36 -0
  194. package/src/lib/{itinerary-labels.ts → booking/itinerary-labels.ts} +1 -1
  195. package/src/lib/{location-calculations.ts → booking/location-calculations.ts} +4 -4
  196. package/src/lib/{location-utils.ts → booking/location-utils.ts} +26 -0
  197. package/src/lib/{map-utils.ts → booking/map-utils.ts} +3 -3
  198. package/src/lib/booking/normalize-booking-product-id.ts +7 -0
  199. package/src/lib/{pickup-location-types.ts → booking/pickup-location-types.ts} +2 -2
  200. package/src/lib/{pricing.ts → booking/pricing.ts} +2 -2
  201. package/src/lib/booking/product-option-id.ts +35 -0
  202. package/src/lib/booking/source-metadata.ts +72 -7
  203. package/src/lib/booking/sunday-week.ts +14 -0
  204. package/src/lib/booking/trace-context.ts +62 -0
  205. package/src/lib/booking-api.ts +1793 -0
  206. package/src/lib/{constants.ts → booking-constants.ts} +11 -5
  207. package/src/lib/booking-types.ts +36 -0
  208. package/src/lib/currency.ts +38 -45
  209. package/src/lib/dap-descriptions.ts +50 -0
  210. package/src/lib/dap-itinerary-preview.ts +315 -0
  211. package/src/lib/dependent-add-on-api.ts +434 -0
  212. package/src/lib/env.ts +89 -5
  213. package/src/lib/firebase.ts +20 -0
  214. package/src/lib/job-application-api.ts +83 -0
  215. package/src/lib/manage-booking-embed-print.ts +16 -0
  216. package/src/lib/manage-booking-post-checkout.ts +68 -0
  217. package/src/lib/photo-dap-config.ts +228 -0
  218. package/src/lib/pickup/map-utils.ts +56 -0
  219. package/src/lib/pickup/marker-icons.ts +19 -0
  220. package/src/lib/product-descriptions.ts +66 -0
  221. package/src/lib/products-config.ts +73 -0
  222. package/src/providers/booking-dialog-provider.tsx +107 -38
  223. package/src/providers/bottom-sheet-provider.tsx +40 -0
  224. package/src/providers/dependent-add-on-dialog-provider.tsx +105 -0
  225. package/src/radius.css +5 -0
  226. package/src/spacing.css +7 -0
  227. package/src/strings/en.json +1774 -0
  228. package/src/strings/es.json +1573 -0
  229. package/src/strings/fr.json +1573 -0
  230. package/src/strings/index.js +23 -0
  231. package/src/text-style.css +97 -0
  232. package/src/types/fareharbor.d.ts +12 -0
  233. package/src/types/quiz.ts +59 -0
  234. package/src/utils/currency-converter.ts +101 -0
  235. package/src/components/BookingFlow.tsx +0 -2952
  236. package/src/components/LanguageSwitcher.tsx +0 -30
  237. package/src/components/PrivateShuttleBookingFlow.tsx +0 -2290
  238. package/src/components/ProductList.tsx +0 -78
  239. package/src/components/WhatsAppPhoneInput.tsx +0 -224
  240. package/src/components/index.ts +0 -31
  241. package/src/lib/api.ts +0 -801
  242. package/src/lib/booking-api-auth.ts +0 -9
  243. package/src/lib/checkout-breakdown.test.ts +0 -70
  244. package/src/types/google-maps.d.ts +0 -2
  245. /package/src/components/{CurrencySwitcher.tsx → booking/CurrencySwitcher.tsx} +0 -0
  246. /package/src/components/{ErrorBoundary.tsx → booking/ErrorBoundary.tsx} +0 -0
  247. /package/src/lib/{i18n → booking/i18n}/config.ts +0 -0
  248. /package/src/lib/{i18n → booking/i18n}/index.tsx +0 -0
  249. /package/src/lib/{marker-icons.ts → booking/marker-icons.ts} +0 -0
  250. /package/src/lib/{places-api.ts → booking/places-api.ts} +0 -0
  251. /package/src/lib/{theme.ts → booking/theme.ts} +0 -0
  252. /package/src/lib/{utils.ts → booking/utils.ts} +0 -0
@@ -0,0 +1,620 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import {
5
+ getPresignedUploadUrl,
6
+ uploadFileToPresignedUrl,
7
+ submitJobApplication,
8
+ type SubmitApplicationPayload,
9
+ } from '@/lib/job-application-api';
10
+ import strings from '@/strings';
11
+ import PhoneInputWithCountry from '@/components/PhoneInputWithCountry';
12
+ import styles from './JobApplicationDialog.module.css';
13
+ import Button, { ButtonHoverColor } from './button';
14
+
15
+ const JOB_ID_GUEST_EXPERIENCE = 'guest-experience-coordinator';
16
+
17
+ function YesNoField({
18
+ id,
19
+ label,
20
+ value,
21
+ onChange,
22
+ t,
23
+ submitting,
24
+ required,
25
+ }: {
26
+ id: string;
27
+ label: string;
28
+ value: string;
29
+ onChange: (v: string) => void;
30
+ t: (key: string) => string;
31
+ submitting: boolean;
32
+ required?: boolean;
33
+ }) {
34
+ return (
35
+ <div className={styles.formGroup}>
36
+ <label className={styles.label}>
37
+ {label} {required && '*'}
38
+ </label>
39
+ <div className={styles.radioGroup}>
40
+ <label className={styles.radioLabel}>
41
+ <input
42
+ type="radio"
43
+ name={id}
44
+ value="YES"
45
+ checked={value === 'YES'}
46
+ onChange={(e) => onChange(e.target.value)}
47
+ disabled={submitting}
48
+ required={required}
49
+ />
50
+ <span>{t('yes')}</span>
51
+ </label>
52
+ <label className={styles.radioLabel}>
53
+ <input
54
+ type="radio"
55
+ name={id}
56
+ value="NO"
57
+ checked={value === 'NO'}
58
+ onChange={(e) => onChange(e.target.value)}
59
+ disabled={submitting}
60
+ />
61
+ <span>{t('no')}</span>
62
+ </label>
63
+ </div>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ interface JobApplicationDialogProps {
69
+ isOpen: boolean;
70
+ onClose: () => void;
71
+ jobId: string;
72
+ jobTitle: string;
73
+ jobDescription: string;
74
+ jobDescriptionExpanded?: string;
75
+ }
76
+
77
+ export default function JobApplicationDialog({
78
+ isOpen,
79
+ onClose,
80
+ jobId,
81
+ jobTitle,
82
+ jobDescription,
83
+ jobDescriptionExpanded,
84
+ }: JobApplicationDialogProps) {
85
+ const jobAppStrings = (strings as { careers?: { jobApplication?: Record<string, string> } }).careers?.jobApplication;
86
+ const t = (key: string) => jobAppStrings?.[key] ?? key;
87
+
88
+ const [name, setName] = useState('');
89
+ const [email, setEmail] = useState('');
90
+ const [phone, setPhone] = useState('');
91
+ const [eligibleToWorkInCanada, setEligibleToWorkInCanada] = useState<string>('');
92
+ const [accommodationInBowValley, setAccommodationInBowValley] = useState<string>('');
93
+ const [whyJoinViaVia, setWhyJoinViaVia] = useState('');
94
+ const [comfortableEarlyMornings, setComfortableEarlyMornings] = useState<string>('');
95
+ const [languagesSpoken, setLanguagesSpoken] = useState('');
96
+ const [fastPacedExperience, setFastPacedExperience] = useState<string>('');
97
+ const [whatMakesYouReliable, setWhatMakesYouReliable] = useState('');
98
+ const [eligibleToWorkInCanadaExplain, setEligibleToWorkInCanadaExplain] = useState('');
99
+ const [accommodationInBowValleyExplain, setAccommodationInBowValleyExplain] = useState('');
100
+ const [fastPacedExperienceExplain, setFastPacedExperienceExplain] = useState('');
101
+ const [resumeFile, setResumeFile] = useState<File | null>(null);
102
+ const [coverLetterFile, setCoverLetterFile] = useState<File | null>(null);
103
+ const [descriptionExpanded, setDescriptionExpanded] = useState(false);
104
+ const [error, setError] = useState<string | null>(null);
105
+
106
+ useEffect(() => {
107
+ if (isOpen) setDescriptionExpanded(false);
108
+ }, [isOpen]);
109
+ const [success, setSuccess] = useState(false);
110
+ const [submitting, setSubmitting] = useState(false);
111
+ const [showCloseConfirm, setShowCloseConfirm] = useState(false);
112
+
113
+ const hasMeaningfulPhone = phone.replace(/\D/g, '').length > 1;
114
+ const isFormDirty = Boolean(
115
+ name.trim() ||
116
+ email.trim() ||
117
+ hasMeaningfulPhone ||
118
+ eligibleToWorkInCanada ||
119
+ accommodationInBowValley ||
120
+ whyJoinViaVia.trim() ||
121
+ comfortableEarlyMornings ||
122
+ languagesSpoken.trim() ||
123
+ fastPacedExperience ||
124
+ whatMakesYouReliable.trim() ||
125
+ eligibleToWorkInCanadaExplain.trim() ||
126
+ accommodationInBowValleyExplain.trim() ||
127
+ fastPacedExperienceExplain.trim() ||
128
+ resumeFile ||
129
+ coverLetterFile
130
+ );
131
+
132
+ const resetForm = () => {
133
+ setName('');
134
+ setEmail('');
135
+ setPhone('');
136
+ setEligibleToWorkInCanada('');
137
+ setAccommodationInBowValley('');
138
+ setWhyJoinViaVia('');
139
+ setComfortableEarlyMornings('');
140
+ setLanguagesSpoken('');
141
+ setFastPacedExperience('');
142
+ setWhatMakesYouReliable('');
143
+ setEligibleToWorkInCanadaExplain('');
144
+ setAccommodationInBowValleyExplain('');
145
+ setFastPacedExperienceExplain('');
146
+ setResumeFile(null);
147
+ setCoverLetterFile(null);
148
+ setError(null);
149
+ setSuccess(false);
150
+ setShowCloseConfirm(false);
151
+ };
152
+
153
+ const handleClose = () => {
154
+ setShowCloseConfirm(false);
155
+ resetForm();
156
+ onClose();
157
+ };
158
+
159
+ const requestClose = () => {
160
+ if (!success && isFormDirty) {
161
+ setShowCloseConfirm(true);
162
+ } else {
163
+ handleClose();
164
+ }
165
+ };
166
+
167
+ const handleSubmit = async (e: React.FormEvent) => {
168
+ e.preventDefault();
169
+ setError(null);
170
+
171
+ if (!name.trim()) {
172
+ setError(t('name') + ' is required');
173
+ return;
174
+ }
175
+ if (!email.trim()) {
176
+ setError(t('email') + ' is required');
177
+ return;
178
+ }
179
+ if (!email.includes('@')) {
180
+ setError('Please enter a valid email address');
181
+ return;
182
+ }
183
+ if (!eligibleToWorkInCanada) {
184
+ setError(t('eligibleToWorkInCanada') + ' is required');
185
+ return;
186
+ }
187
+ if (eligibleToWorkInCanada === 'NO' && !eligibleToWorkInCanadaExplain.trim()) {
188
+ setError(t('followUpRequired'));
189
+ return;
190
+ }
191
+ if (!accommodationInBowValley) {
192
+ setError(t('accommodationInBowValley') + ' is required');
193
+ return;
194
+ }
195
+ if (accommodationInBowValley === 'NO' && !accommodationInBowValleyExplain.trim()) {
196
+ setError(t('followUpRequired'));
197
+ return;
198
+ }
199
+ if (!whyJoinViaVia.trim()) {
200
+ setError(t('whyJoinViaVia') + ' is required');
201
+ return;
202
+ }
203
+ if (!comfortableEarlyMornings) {
204
+ setError(t('comfortableEarlyMornings') + ' is required');
205
+ return;
206
+ }
207
+ if (!languagesSpoken.trim()) {
208
+ setError(t('languagesSpoken') + ' is required');
209
+ return;
210
+ }
211
+ if (!fastPacedExperience) {
212
+ setError(t('fastPacedExperience') + ' is required');
213
+ return;
214
+ }
215
+ if (!whatMakesYouReliable.trim()) {
216
+ setError(t('whatMakesYouReliable') + ' is required');
217
+ return;
218
+ }
219
+ if (!resumeFile) {
220
+ setError(t('resume') + ' is required');
221
+ return;
222
+ }
223
+
224
+ const allowedResumeTypes = ['.pdf', '.doc', '.docx'];
225
+ const resumeExt = resumeFile.name.substring(resumeFile.name.lastIndexOf('.')).toLowerCase();
226
+ if (!allowedResumeTypes.includes(resumeExt)) {
227
+ setError('Resume must be PDF, DOC, or DOCX');
228
+ return;
229
+ }
230
+
231
+ setSubmitting(true);
232
+
233
+ try {
234
+ // Upload resume
235
+ const resumePresigned = await getPresignedUploadUrl(jobId, 'resume', resumeFile.name);
236
+ await uploadFileToPresignedUrl(
237
+ resumePresigned.uploadUrl,
238
+ resumeFile,
239
+ resumePresigned.contentType
240
+ );
241
+ const resumeS3Key = resumePresigned.s3Key;
242
+
243
+ // Upload cover letter if provided
244
+ let coverLetterS3Key: string | undefined;
245
+ if (coverLetterFile) {
246
+ const clPresigned = await getPresignedUploadUrl(jobId, 'coverLetter', coverLetterFile.name);
247
+ await uploadFileToPresignedUrl(
248
+ clPresigned.uploadUrl,
249
+ coverLetterFile,
250
+ clPresigned.contentType
251
+ );
252
+ coverLetterS3Key = clPresigned.s3Key;
253
+ }
254
+
255
+ const payload: SubmitApplicationPayload = {
256
+ jobId,
257
+ fullName: name.trim(),
258
+ email: email.trim(),
259
+ phone: hasMeaningfulPhone ? phone.trim() : undefined,
260
+ answers: {
261
+ eligibleToWorkInCanada: eligibleToWorkInCanada.trim(),
262
+ eligibleToWorkInCanadaExplain: eligibleToWorkInCanadaExplain.trim(),
263
+ accommodationInBowValley: accommodationInBowValley.trim(),
264
+ accommodationInBowValleyExplain: accommodationInBowValleyExplain.trim(),
265
+ whyJoinViaVia: whyJoinViaVia.trim(),
266
+ comfortableEarlyMornings: comfortableEarlyMornings.trim(),
267
+ languagesSpoken: languagesSpoken.trim(),
268
+ fastPacedExperience: fastPacedExperience.trim(),
269
+ fastPacedExperienceExplain: fastPacedExperienceExplain.trim(),
270
+ whatMakesYouReliable: whatMakesYouReliable.trim(),
271
+ },
272
+ resumeS3Key,
273
+ coverLetterS3Key,
274
+ };
275
+
276
+ await submitJobApplication(payload);
277
+ setSuccess(true);
278
+ } catch (err) {
279
+ setError(err instanceof Error ? err.message : t('error'));
280
+ } finally {
281
+ setSubmitting(false);
282
+ }
283
+ };
284
+
285
+ if (!isOpen) return null;
286
+
287
+ const handleOverlayClick = () => {
288
+ if (showCloseConfirm) {
289
+ setShowCloseConfirm(false);
290
+ } else {
291
+ requestClose();
292
+ }
293
+ };
294
+
295
+ return (
296
+ <div
297
+ className={styles.overlay}
298
+ onClick={handleOverlayClick}
299
+ role="dialog"
300
+ aria-modal="true"
301
+ aria-labelledby="job-application-dialog-title"
302
+ >
303
+ <div className={styles.modal} onClick={(e) => e.stopPropagation()}>
304
+ <div className={`${styles.header} ${descriptionExpanded ? styles.headerExpanded : ''}`}>
305
+ <div className={styles.headerContent}>
306
+ <h2 id="job-application-dialog-title" className={styles.title}>
307
+ {jobTitle}
308
+ </h2>
309
+ <div
310
+ className={`${styles.descriptionSection} ${descriptionExpanded ? styles.descriptionExpanded : ''}`}
311
+ >
312
+ {descriptionExpanded && jobDescriptionExpanded ? (
313
+ <div
314
+ className={styles.descriptionExpandedContent}
315
+ dangerouslySetInnerHTML={{ __html: jobDescriptionExpanded }}
316
+ />
317
+ ) : (
318
+ <p className={styles.description}>{jobDescription}</p>
319
+ )}
320
+ <button
321
+ type="button"
322
+ onClick={() => setDescriptionExpanded((v) => !v)}
323
+ className={styles.descriptionToggle}
324
+ aria-label={descriptionExpanded ? t('collapseDescription') : t('expandDescription')}
325
+ aria-expanded={descriptionExpanded}
326
+ >
327
+ <svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
328
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
329
+ </svg>
330
+ </button>
331
+ </div>
332
+ </div>
333
+ <button
334
+ type="button"
335
+ onClick={requestClose}
336
+ className={styles.closeBtn}
337
+ aria-label={t('close')}
338
+ >
339
+ <svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
340
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
341
+ </svg>
342
+ </button>
343
+ </div>
344
+
345
+ <div className={styles.formRow}>
346
+ <div className={styles.bodyScroll}>
347
+ <div className={styles.content}>
348
+ {success ? (
349
+ <>
350
+ <div className={styles.success}>{t('success')}</div>
351
+ <div className={styles.footer}>
352
+ <button type="button" onClick={handleClose} className={styles.submitBtn}>
353
+ {t('close')}
354
+ </button>
355
+ </div>
356
+ </>
357
+ ) : showCloseConfirm ? (
358
+ <div className={styles.closeConfirm}>
359
+ <p className={styles.closeConfirmTitle}>{t('closeConfirmTitle')}</p>
360
+ <p className={styles.closeConfirmMessage}>{t('closeConfirmMessage')}</p>
361
+ <div className={styles.closeConfirmActions}>
362
+ <button
363
+ type="button"
364
+ onClick={() => setShowCloseConfirm(false)}
365
+ className={styles.cancelBtn}
366
+ >
367
+ {t('keepEditing')}
368
+ </button>
369
+ <button
370
+ type="button"
371
+ onClick={handleClose}
372
+ className={styles.submitBtn}
373
+ >
374
+ {t('discard')}
375
+ </button>
376
+ </div>
377
+ </div>
378
+ ) : (
379
+ <form onSubmit={handleSubmit} className={styles.form}>
380
+ <div className={styles.formGroup}>
381
+ <label htmlFor="name" className={styles.label}>
382
+ {t('name')} *
383
+ </label>
384
+ <input
385
+ id="name"
386
+ type="text"
387
+ value={name}
388
+ onChange={(e) => setName(e.target.value)}
389
+ className={styles.input}
390
+ required
391
+ disabled={submitting}
392
+ />
393
+ </div>
394
+
395
+ <div className={styles.formGroup}>
396
+ <label htmlFor="email" className={styles.label}>
397
+ {t('email')} *
398
+ </label>
399
+ <input
400
+ id="email"
401
+ type="email"
402
+ value={email}
403
+ onChange={(e) => setEmail(e.target.value)}
404
+ className={styles.input}
405
+ required
406
+ disabled={submitting}
407
+ />
408
+ </div>
409
+
410
+ <div className={styles.formGroup}>
411
+ <label htmlFor="phone" className={styles.label}>
412
+ {t('phoneNumber')}
413
+ </label>
414
+ <PhoneInputWithCountry
415
+ id="phone"
416
+ value={phone}
417
+ onChange={(value) => setPhone(value ?? '')}
418
+ placeholder="e.g. +1 403 555 0123"
419
+ disabled={submitting}
420
+ className={styles.phoneInputMatch}
421
+ />
422
+ </div>
423
+
424
+ <YesNoField
425
+ id="eligibleToWorkInCanada"
426
+ label={t('eligibleToWorkInCanada')}
427
+ value={eligibleToWorkInCanada}
428
+ onChange={setEligibleToWorkInCanada}
429
+ t={t}
430
+ submitting={submitting}
431
+ required
432
+ />
433
+ {eligibleToWorkInCanada === 'NO' && (
434
+ <div className={styles.formGroup}>
435
+ <label htmlFor="eligibleToWorkInCanadaExplain" className={styles.label}>
436
+ {t('eligibleToWorkInCanadaFollowUp')} *
437
+ </label>
438
+ <input
439
+ id="eligibleToWorkInCanadaExplain"
440
+ type="text"
441
+ value={eligibleToWorkInCanadaExplain}
442
+ onChange={(e) => setEligibleToWorkInCanadaExplain(e.target.value)}
443
+ className={styles.input}
444
+ required={eligibleToWorkInCanada === 'NO'}
445
+ disabled={submitting}
446
+ />
447
+ </div>
448
+ )}
449
+
450
+ <YesNoField
451
+ id="accommodationInBowValley"
452
+ label={t('accommodationInBowValley')}
453
+ value={accommodationInBowValley}
454
+ onChange={setAccommodationInBowValley}
455
+ t={t}
456
+ submitting={submitting}
457
+ required
458
+ />
459
+ {accommodationInBowValley === 'NO' && (
460
+ <div className={styles.formGroup}>
461
+ <label htmlFor="accommodationInBowValleyExplain" className={styles.label}>
462
+ {t('accommodationInBowValleyFollowUp')} *
463
+ </label>
464
+ <input
465
+ id="accommodationInBowValleyExplain"
466
+ type="text"
467
+ value={accommodationInBowValleyExplain}
468
+ onChange={(e) => setAccommodationInBowValleyExplain(e.target.value)}
469
+ className={styles.input}
470
+ required={accommodationInBowValley === 'NO'}
471
+ disabled={submitting}
472
+ />
473
+ </div>
474
+ )}
475
+
476
+ <div className={styles.formGroup}>
477
+ <label htmlFor="whyJoinViaVia" className={styles.label}>
478
+ {t('whyJoinViaVia')} *
479
+ </label>
480
+ <textarea
481
+ id="whyJoinViaVia"
482
+ value={whyJoinViaVia}
483
+ onChange={(e) => setWhyJoinViaVia(e.target.value)}
484
+ className={`${styles.input} ${styles.textarea}`}
485
+ disabled={submitting}
486
+ required
487
+ />
488
+ </div>
489
+
490
+ <YesNoField
491
+ id="comfortableEarlyMornings"
492
+ label={t('comfortableEarlyMornings')}
493
+ value={comfortableEarlyMornings}
494
+ onChange={setComfortableEarlyMornings}
495
+ t={t}
496
+ submitting={submitting}
497
+ required
498
+ />
499
+
500
+ <div className={styles.formGroup}>
501
+ <label htmlFor="languagesSpoken" className={styles.label}>
502
+ {t('languagesSpoken')} *
503
+ </label>
504
+ <input
505
+ id="languagesSpoken"
506
+ type="text"
507
+ value={languagesSpoken}
508
+ onChange={(e) => setLanguagesSpoken(e.target.value)}
509
+ className={styles.input}
510
+ required
511
+ disabled={submitting}
512
+ />
513
+ </div>
514
+
515
+ <YesNoField
516
+ id="fastPacedExperience"
517
+ label={t('fastPacedExperience')}
518
+ value={fastPacedExperience}
519
+ onChange={setFastPacedExperience}
520
+ t={t}
521
+ submitting={submitting}
522
+ required
523
+ />
524
+ {(fastPacedExperience === 'YES' || fastPacedExperience === 'NO') && (
525
+ <div className={styles.formGroup}>
526
+ <label htmlFor="fastPacedExperienceExplain" className={styles.label}>
527
+ {t('explainOptional')}
528
+ </label>
529
+ <input
530
+ id="fastPacedExperienceExplain"
531
+ type="text"
532
+ value={fastPacedExperienceExplain}
533
+ onChange={(e) => setFastPacedExperienceExplain(e.target.value)}
534
+ className={styles.input}
535
+ disabled={submitting}
536
+ />
537
+ </div>
538
+ )}
539
+
540
+ <div className={styles.formGroup}>
541
+ <label htmlFor="whatMakesYouReliable" className={styles.label}>
542
+ {t('whatMakesYouReliable')} *
543
+ </label>
544
+ <textarea
545
+ id="whatMakesYouReliable"
546
+ value={whatMakesYouReliable}
547
+ onChange={(e) => setWhatMakesYouReliable(e.target.value)}
548
+ className={`${styles.input} ${styles.textarea}`}
549
+ disabled={submitting}
550
+ required
551
+ />
552
+ </div>
553
+
554
+ <div className={styles.formGroup}>
555
+ <label className={styles.label}>
556
+ {t('resume')} *
557
+ </label>
558
+ <div className={`${styles.fileInput} ${submitting ? styles.fileInputDisabled : ''}`}>
559
+ <input
560
+ id="resume-file"
561
+ type="file"
562
+ accept=".pdf,.doc,.docx"
563
+ onChange={(e) => setResumeFile(e.target.files?.[0] ?? null)}
564
+ disabled={submitting}
565
+ className={styles.fileInputHidden}
566
+ />
567
+ <label htmlFor="resume-file" className={styles.fileInputButton}>
568
+ {t('chooseFile')}
569
+ </label>
570
+ {resumeFile && <div className={styles.fileName}>{resumeFile.name}</div>}
571
+ </div>
572
+ </div>
573
+
574
+ <div className={styles.formGroup}>
575
+ <label className={styles.label}>
576
+ {t('coverLetter')}
577
+ </label>
578
+ <div className={`${styles.fileInput} ${submitting ? styles.fileInputDisabled : ''}`}>
579
+ <input
580
+ id="cover-letter-file"
581
+ type="file"
582
+ accept=".pdf,.doc,.docx"
583
+ onChange={(e) => setCoverLetterFile(e.target.files?.[0] ?? null)}
584
+ disabled={submitting}
585
+ className={styles.fileInputHidden}
586
+ />
587
+ <label htmlFor="cover-letter-file" className={styles.fileInputButton}>
588
+ {t('chooseFile')}
589
+ </label>
590
+ {coverLetterFile && <div className={styles.fileName}>{coverLetterFile.name}</div>}
591
+ </div>
592
+ </div>
593
+
594
+ {error && <div className={styles.error}>{error}</div>}
595
+
596
+ <div className={styles.footer}>
597
+ <Button
598
+ variant={submitting ? 'disabled' : 'primary'}
599
+ hoverColor={ButtonHoverColor.Turquoise}
600
+ onClick={handleSubmit}
601
+ >
602
+ {t('submit')}
603
+ </Button>
604
+ </div>
605
+ </form>
606
+ )}
607
+ </div>
608
+ </div>
609
+ <div className={styles.rightColumn}>
610
+ <p className={styles.rightColumnText}>Good people.</p>
611
+ <p className={styles.rightColumnText}>Epic journeys.</p>
612
+ <p className={styles.rightColumnText}>Real impact.</p>
613
+ </div>
614
+ </div>
615
+ </div>
616
+ </div>
617
+ );
618
+ }
619
+
620
+ export { JOB_ID_GUEST_EXPERIENCE };