https://saddleback.com/scripts/ServingOpps/ServingOpps.js?v=b2d0741167059c5ea156aba9420b4bee89d6778d
1import FeaturedServingOpps from '/scripts/ServingOpps/FeaturedServingOpps.js'; 2import ServingOppList from '/scripts/ServingOpps/ServingOppList.js'; 3import SearchInput from '/scripts/Ministries/SearchInput.js'; 4import ServingOppFilter from '/scripts/ServingOpps/ServingOppFilter.js'; 5import AppliedFilters from '/scripts/ServingOpps/AppliedFilters.js'; 6import LoadMoreTrigger from '/scripts/Ministries/LoadMoreTrigger.js'; 7import LoadingSpinner from '/scripts/VueComponents/LoadingSpinner.js'; 8import ServePopup from '/scripts/ServingOpps/ServePopup.js'; 9import ShoppingCartModal from '/scripts/ServingOpps/ShoppingCartModal.js'; 10import ServingOppMap from '/scripts/ServingOpps/Components/ServingOppMap.js'; 11import CartStore from '/scripts/ServingOpps/Stores/CartStore.js'; 12import Modal from '/scripts/Modals/BasicModalMixin.js'; 13 14const createApp = ({ 15 accessToken, 16 appId, 17 campusList, 18 churchEntityId, 19 healthyChurchApi, 20 rockDomainUrl, 21 isLoggedIn, 22 logoutEndpoint, 23 ministryCategories, 24 ministryTags, 25 person, 26 rockPerson, 27 servePopupEnabled, 28 servePopupCookieHours, 29 servePopupImageUrl, 30 servePopupButtonText, 31 servePopupFilterTag, 32 servePopupContent, 33}) => { 34 const app = Vue.createApp({ 35 components: { 36 FeaturedServingOpps, 37 ServingOppList, 38 SearchInput, 39 ServingOppFilter, 40 AppliedFilters, 41 LoadMoreTrigger, 42 LoadingSpinner, 43 ServePopup, 44 ShoppingCartModal, 45 ServingOppMap, 46 Modal, 47 }, 48 49 template: ` 50 <div class="serving-opps-title"> 51 <h2>Sign Ups</h2> 52 53 <div class="shopping-cart-container" 54 v-show="isCartReady" 55 :class="{ 'disabled': cart.length === 0 }" 56 v-on:click="cart.length > 0 ? isShoppingCartModalOpen = true : null" 57 > 58 <div class="shopping-cart-icon"> 59 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"> 60 <path d="M6 20C5.45 20 4.97917 19.8042 4.5875 19.4125C4.19583 19.0208 4 18.55 4 18C4 17.45 4.19583 16.9792 4.5875 16.5875C4.97917 16.1958 5.45 16 6 16C6.55 16 7.02083 16.1958 7.4125 16.5875C7.80417 16.9792 8 17.45 8 18C8 18.55 7.80417 19.0208 7.4125 19.4125C7.02083 19.8042 6.55 20 6 20ZM16 20C15.45 20 14.9792 19.8042 14.5875 19.4125C14.1958 19.0208 14 18.55 14 18C14 17.45 14.1958 16.9792 14.5875 16.5875C14.9792 16.1958 15.45 16 16 16C16.55 16 17.0208 16.1958 17.4125 16.5875C17.8042 16.9792 18 17.45 18 18C18 18.55 17.8042 19.0208 17.4125 19.4125C17.0208 19.8042 16.55 20 16 20ZM5.15 4L7.55 9H14.55L17.3 4H5.15ZM4.2 2H18.95C19.3333 2 19.625 2.17083 19.825 2.5125C20.025 2.85417 20.0333 3.2 19.85 3.55L16.3 9.95C16.1167 10.2833 15.8708 10.5417 15.5625 10.725C15.2542 10.9083 14.9167 11 14.55 11H7.1L6 13H18V15H6C5.25 15 4.68333 14.6708 4.3 14.0125C3.91667 13.3542 3.9 12.7 4.25 12.05L5.6 9.6L2 2H0V0H3.25L4.2 2Z" fill="currentColor"/> 61 </svg> 62 <div class="cart-count-container" v-if="cart.length"> 63 <span class="cart-count">{{ cart.length }}</span> 64 </div> 65 </div> 66 <span>My Sign Ups</span> 67 68 <ShoppingCartModal 69 v-if="!isMobileView" 70 :visible="isShoppingCartModalOpen" 71 :cart="cart" 72 :rockDomainUrl="rockDomainUrl" 73 :isMobileView="isMobileView" 74 :isLoggedIn="isLoggedIn" 75 :accessToken="accessToken" 76 v-on:close="isShoppingCartModalOpen = false" 77 v-on:remove-from-cart="onRemoveFromCart" 78 /> 79 </div> 80 </div> 81 82 <ShoppingCartModal 83 v-if="isMobileView" 84 :visible="isShoppingCartModalOpen" 85 :cart="cart" 86 :rockDomainUrl="rockDomainUrl" 87 :isMobileView="isMobileView" 88 :isLoggedIn="isLoggedIn" 89 :accessToken="accessToken"
90 v-on:close="isShoppingCartModalOpen = false" 91 v-on:remove-from-cart="onRemoveFromCart" 92 /> 93 94 <div class="serving-opps-inner"> 95 <div class="serving-opp-content-column"> 96 <ServingOppMap 97 v-if="!isMobileView || mobileView === 'map'" 98 :servingOpps="visibleOpps" 99 :cart="cart" 100 :mySignUpsIds="mySignUpsIds" 101 :selectedKey="selectedOppKey" 102 :hasActiveFilters="mapHasActiveFilters" 103 :isMobile="isMobileView" 104 v-on:select-opp="onMapPinSelected" 105 v-on:clear-selection="onMapClearSelection" 106 v-on:add-to-cart="onAddToCart" 107 v-on:sign-up="onMapSignUp" 108 /> 109 110 <div class="serving-opp-list-column" v-if="showSearchResults && (!isMobileView || mobileView === 'list')"> 111 <FeaturedServingOpps 112 :authHeaders="authHeaders" 113 :churchEntityId="churchEntityId" 114 :healthyChurchApi="healthyChurchApi" 115 :isLoggedIn="isLoggedIn" 116 :rockPerson="rockPerson" 117 :selectedCampusId="appliedCampusId" 118 v-show="onlyCampusFilter" 119 v-on:on-click-featured="onClickFeatured" 120 /> 121 122 <AppliedFilters 123 :searchQuery="appliedSearchQuery" 124 :selectedCategories="categoryNamesArray" 125 :selectedTags="selectedTags" 126 :selectedOccurrenceTypes="appliedOccurrenceTypes" 127 :selectedCampusId="appliedCampusId" 128 :resultCount="searchResultCount" 129 v-if="hasAppliedFilters" 130 v-on:reset-filter="onResetFilter" 131 /> 132 133 <!-- Region load: dots+logo while search results fetch. When hasMore is true 134 the LoadMoreTrigger below already shows the same widget, so gate on it 135 to avoid two at once. --> 136 <div class="loading-results" v-if="isSearchResultLoading && !searchResultHasMore"> 137 <loading-spinner></loading-spinner> 138 </div> 139 140 <ServingOppList 141 :data="searchResults" 142 :showNoEventsMessage="showNoEventsMessage" 143 :mySignUpsIds="mySignUpsIds" 144 :rockDomainUrl="rockDomainUrl" 145 :isLoggedIn="isLoggedIn" 146 :accessToken="accessToken" 147 :cart="cart" 148 :selectedOppKey="listSelectedOppKey" 149 :appliedStartDate="appliedStartDate" 150 :appliedEndDate="appliedEndDate" 151 v-if="!isSearchResultLoading" 152 v-on:category-selected="onCategoryClickedFromListItem" 153 v-on:tag-selected="onTagClickedFromListItem" 154 v-on:add-to-cart="onAddToCart" 155 v-on:sign-up="onListItemSignUp" 156 v-on:select="onListItemSelected" 157 /> 158 159 <LoadMoreTrigger 160 :hasMore="searchResultHasMore" 161 :isLoading="isSearchResultLoading" 162 v-on:load-more="onLoadMoreSearchResult" 163 /> 164 165 <div class="empty-search-results" id="noEvents" v-if="showNoEventsMessage"> 166 <h3>Oops!</h3> 167 <p class="message-text"> 168 There are no sign ups to display at this time. 169 <span style="display: inline-block;">To find a place to volunteer, <a href="/sections/get-involved/adults/serve/">click here to explore our Dream Teams.</a></span> 170 </p> 171 </div> 172 </div> 173 174 <div class="serving-opp-list-column" v-if="!showSearchResults && (!isMobileView || mobileView === 'list')"> 175 <FeaturedServingOpps 176 :authHeaders="authHeaders" 177 :churchEntityId="churchEntityId" 178 :healthyChurchApi="healthyChurchApi" 179 :isLoggedIn="isLoggedIn" 180 :rockPerson="rockPerson" 181 v-on:on-click-featured="onClickFeatured" 182 /> 183 184 <!-- Region load: dots+logo for the initial browse-list fetch (the 185 LoadMoreTrigger covers subsequent pages). --> 186 <div class="loading-results" v-if="isServingOppListLoading && servingOppList.length === 0"> 187 <loading-spinner></loading-spinner> 188 </div> 189 190 <ServingOppList 191 v-if="servingOppList.length > 0" 192 :data="servingOppList"
193 :showNoEventsMessage="showNoEventsMessage" 194 :mySignUpsIds="mySignUpsIds" 195 :cart="cart" 196 :selectedOppKey="listSelectedOppKey" 197 :appliedStartDate="appliedStartDate" 198 :appliedEndDate="appliedEndDate" 199 v-on:category-selected="onCategoryClickedFromListItem" 200 v-on:tag-selected="onTagClickedFromListItem" 201 v-on:add-to-cart="onAddToCart" 202 v-on:sign-up="onListItemSignUp" 203 v-on:select="onListItemSelected" 204 /> 205 206 <LoadMoreTrigger 207 :hasMore="servingOppListHasMore" 208 :isLoading="isServingOppListLoading" 209 v-on:load-more="onLoadMoreServingOppList" 210 /> 211 212 <div class="empty-search-results" id="noEvents" v-if="showNoEventsMessage"> 213 <h3>Oops!</h3> 214 <p class="message-text"> 215 There are no sign ups to display at this time. 216 <span style="display: inline-block;">To find a place to volunteer, <a href="/sections/get-involved/adults/serve/">click here to explore our Dream Teams.</a></span> 217 </p> 218 </div> 219 </div> 220 221 </div> 222 223 <div class="filters-column" :class="{ 'sticky': isSticky }"> 224 <SearchInput 225 :searchQuery="searchQuery" 226 v-on:input="onInputSearchQuery" 227 v-on:see-results="onSearchClicked" 228 v-on:clear="onClearSearch" 229 /> 230 231 <div class="mobile-actions-bar"> 232 <button 233 type="button" 234 class="mobile-action" 235 v-on:click="toggleMobileView" 236 > 237 <img v-if="mobileView === 'list'" src="https://saddleback.com/media/3qbljjwh/map.svg" alt="" /> 238 <img v-else src="https://saddleback.com/media/ndjculk4/list.svg" alt="" /> 239 <span>{{ mobileView === 'list' ? 'Map' : 'List' }}</span> 240 </button> 241 242 <a 243 class="mobile-action" 244 :class="{'is-applied': isFilterApplied}" 245 v-on:click="showServingOppFilter(!isServingOppFilterOpen)" 246 href="javascript:;" 247 > 248 <img src="/icons/filter.svg" v-if="!isFilterApplied"/> 249 <img src="/icons/filter-white.svg" v-if="isFilterApplied"/> 250 <span>Filters</span> 251 </a> 252 </div> 253 254 <ServingOppFilter 255 :campusList="campusList" 256 :categories="ministryCategories" 257 :tags="ministryTags" 258 :selectedCategories="selectedCategories" 259 :categoryNamesArray="categoryNamesArray" 260 :selectedTags="selectedTags" 261 :selectedOccurrenceTypes="selectedOccurrenceTypes" 262 :selectedCampusId="selectedCampusId" 263 :selectedStartDate="selectedStartDate" 264 :selectedEndDate="selectedEndDate" 265 :resultCount="liveSearchResultCount" 266 :isResultLoading="isSearchResultLoading" 267 :isFilterChanged="isFilterChanged" 268 :isMobile="false" 269 270 v-on:category-selected="onCategorySelected" 271 v-on:tag-selected="onTagSelected" 272 v-on:occurrence-selected="onOccurrenceSelected" 273 v-on:campus-selected="onCampusSelected" 274 v-on:start-date-selected="onStartDateSelected" 275 v-on:end-date-selected="onEndDateSelected" 276 v-on:reset-filter="onResetFilter" 277 v-on:see-results="onSeeResultsClicked" 278 /> 279 280 <ServingOppFilter 281 :campusList="campusList" 282 :categories="ministryCategories" 283 :tags="ministryTags" 284 :selectedCategories="selectedCategories" 285 :categoryNamesArray="categoryNamesArray" 286 :selectedTags="selectedTags" 287 :selectedOccurrenceTypes="selectedOccurrenceTypes" 288 :selectedCampusId="selectedCampusId" 289 :selectedStartDate="selectedStartDate" 290 :selectedEndDate="selectedEndDate" 291 :resultCount="liveSearchResultCount" 292 :isResultLoading="isSearchResultLoading" 293 :isFilterChanged="isFilterChanged" 294 :isMobile="true" 295 :class="{ 'open': isServingOppFilterOpen }" 296 297 v-on:category-selected="onCategorySelected" 298 v-on:tag-selected="onTagSelected" 299 v-on:occurrence-selected="onOccurrenceSelected" 300 v-on:campus-selected="onCampusSelected" 301 v-on:start-date-selected="onStartDateSelected" 302 v-on:end-date-selected="onEndDateSelected" 303 v-on:reset-filter="onResetFilter" 304 v-on:see-results="onSeeResultsClicked"
305 v-on:close="showServingOppFilter(false)" 306 /> 307 </div> 308 </div> 309 310 <ServePopup 311 :visible="showServePopup" 312 :imageUrl="servePopupImageUrl" 313 :buttonText="servePopupButtonText" 314 :content="servePopupContent" 315 @close="handleServePopupClose" 316 @explore="handleServePopupExplore" 317 /> 318 319 <Modal v-if="confirmingAction" v-on:close="onCancelRecurring" class="confirm-modal"> 320 <template v-slot:header> 321 <h3>This Sign Up is recurring!</h3> 322 </template> 323 <template v-slot:body> 324 <p class="confirm-modal-message">Are you sure you want to register for all occurrences?</p> 325 </template> 326 <template v-slot:footer> 327 <div class="confirm-modal-actions"> 328 <button class="btn invert" v-on:click="onCancelRecurring">Cancel</button> 329 <button class="btn" v-on:click="onConfirmRecurring">Yes, I'm Sure</button> 330 </div> 331 </template> 332 </Modal> 333 334 <div v-if="showCartTotalHoverDisplay" class="shopping-cart-hover-display" :class="{ 'cart-pill-slide-in': isCartPillAnimating }"> 335 <div class="shopping-cart-container" 336 :class="{ 'disabled': cart.length === 0 }" 337 v-on:click="cart.length > 0 ? isShoppingCartModalOpen = true : null" 338 > 339 <div class="shopping-cart-icon"> 340 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"> 341 <mask id="mask0_815_3522" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"> 342 <rect width="24" height="24" fill="#D9D9D9"/> 343 </mask> 344 <g mask="url(#mask0_815_3522)"> 345 <path d="M7 22C6.45 22 5.97917 21.8042 5.5875 21.4125C5.19583 21.0208 5 20.55 5 20C5 19.45 5.19583 18.9792 5.5875 18.5875C5.97917 18.1958 6.45 18 7 18C7.55 18 8.02083 18.1958 8.4125 18.5875C8.80417 18.9792 9 19.45 9 20C9 20.55 8.80417 21.0208 8.4125 21.4125C8.02083 21.8042 7.55 22 7 22ZM17 22C16.45 22 15.9792 21.8042 15.5875 21.4125C15.1958 21.0208 15 20.55 15 20C15 19.45 15.1958 18.9792 15.5875 18.5875C15.9792 18.1958 16.45 18 17 18C17.55 18 18.0208 18.1958 18.4125 18.5875C18.8042 18.9792 19 19.45 19 20C19 20.55 18.8042 21.0208 18.4125 21.4125C18.0208 21.8042 17.55 22 17 22ZM5.2 4H19.95C20.3333 4 20.625 4.17083 20.825 4.5125C21.025 4.85417 21.0333 5.2 20.85 5.55L17.3 11.95C17.1167 12.2833 16.8708 12.5417 16.5625 12.725C16.2542 12.9083 15.9167 13 15.55 13H8.1L7 15H19V17H7C6.25 17 5.68333 16.6708 5.3 16.0125C4.91667 15.3542 4.9 14.7 5.25 14.05L6.6 11.6L3 4H1V2H4.25L5.2 4Z" fill="white"/> 346 </g> 347 </svg> 348 <div class="cart-count-container" v-if="cart.length"> 349 <span class="cart-count">{{ cart.length }}</span> 350 </div> 351 </div> 352 <span>My Sign Ups</span> 353 354 <ShoppingCartModal 355 v-if="!isMobileView" 356 :visible="isShoppingCartModalOpen" 357 :cart="cart" 358 :rockDomainUrl="rockDomainUrl" 359 :isMobileView="isMobileView" 360 :isLoggedIn="isLoggedIn" 361 :accessToken="accessToken" 362 v-on:close="isShoppingCartModalOpen = false" 363 v-on:remove-from-cart="onRemoveFromCart" 364 /> 365 </div> 366 </div> 367 `, 368 369 data() { 370 return { 371 debug: new URLSearchParams(window.location.search).has('debug'), 372 DEFAULT_END_DATE: `${new Date().getFullYear()}-12-31`, 373 isLoggedIn, 374 churchEntityId, 375 healthyChurchApi, 376 accessToken, 377 rockDomainUrl, 378 authHeaders: { headers: { Authorization: `Bearer ${accessToken}` } }, 379 ministryCategories: [], 380 ministryTags: [], 381 campusList, 382 showServePopup: false, 383 servePopupEnabled, 384 servePopupCookieHours, 385 servePopupImageUrl, 386 servePopupButtonText, 387 servePopupFilterTag, 388 servePopupContent, 389 categoryNamesArray: [], 390 categoryElementsArray: [], 391 tagElementsArray: [], 392 393 servingOppList: [], 394 servingOppListCount: 0, 395 servingOppListPage: 0, 396 servingOppListHasMore: true, 397 isServingOppListLoading: false, 398 399 showSearchResults: false, 400 401 searchQuery: '', 402 ministryId: null, 403 selectedCategories: [], 404 selectedTags: [], 405 // Occurrence-type filter (client-side): 'oneTime' | 'recurring'. Empty = all. 406 selectedOccurrenceTypes: [], 407 selectedCampusId: churchEntityId || 0, 408 selectedStartDate: null, 409 selectedEndDate: null, 410 allMinistryCategories: [], 411 allMinistryTags: [], 412 413 appliedSearchQuery: '', 414 appliedCategories: [], 415 appliedTags: [], 416 appliedOccurrenceTypes: [], 417 appliedCampusId: churchEntityId || 0, 418 appliedStartDate: null, 419 appliedEndDate: null, 420 421 searchResults: [], 422 searchResultCount: 0, 423 liveSearchResultCount: 0, 424 searchResultPage: 0, 425 searchResultHasMore: true, 426 isSearchResultLoading: false, 427 isCountLoading: false, 428 429 isServingOppFilterOpen: false, 430 showNoEventsMessage: false, 431 432 mySignUpsIds: [], 433 434 isMobileView: false,
435 isSticky: false, 436 headerHeight: 174, 437 438 // cart is the shared store's reactive array, so mutations propagate everywhere. 439 isShoppingCartModalOpen: false, 440 cart: CartStore.items, 441 // Hide the cart icon until reconciled so a stale count doesn't flash. 442 isCartReady: !isLoggedIn, 443 444 // Map state 445 selectedOppKey: null, 446 mobileView: 'list', // 'list' | 'map' 447 448 // Recurring-opp confirmation modal, shared by add-to-cart and sign-up flows. 449 confirmingOpp: null, 450 confirmingAction: null, 451 confirmingContext: null, 452 isCartPillAnimating: false, 453 mapInfoCloseTimeoutId: null, 454 cartPillAnimationTimeoutId: null, 455 }; 456 }, 457 458 computed: { 459 isFilterApplied() { 460 return ( 461 this.selectedCampusId || this.selectedCategories.length || this.selectedTags.length || this.selectedOccurrenceTypes.length || this.selectedStartDate || this.selectedEndDate 462 ); 463 }, 464 465 isFilterChanged() { 466 return ( 467 this.selectedCampusId !== this.appliedCampusId || 468 this.selectedStartDate !== this.appliedStartDate || 469 this.selectedEndDate !== this.appliedEndDate || 470 JSON.stringify(this.selectedCategories) !== JSON.stringify(this.appliedCategories) || 471 JSON.stringify(this.selectedTags) !== JSON.stringify(this.appliedTags) || 472 JSON.stringify(this.selectedOccurrenceTypes) !== JSON.stringify(this.appliedOccurrenceTypes) 473 ); 474 }, 475 476 isSelectedDatesValid() { 477 if (this.selectedStartDate && this.selectedEndDate) { 478 return true; 479 } 480 return false; 481 }, 482 483 onlyCampusFilter() { 484 return ( 485 this.appliedCampusId >= 0 && 486 !this.appliedSearchQuery && 487 (!this.appliedCategories || this.appliedCategories.length === 0) && 488 (!this.appliedTags || this.appliedTags.length === 0) && 489 !this.appliedStartDate && 490 !this.appliedEndDate 491 ); 492 }, 493 494 hasAppliedFilters() { 495 return ( 496 (this.appliedCampusId && this.appliedCampusId !== 0) || 497 this.appliedSearchQuery || 498 this.appliedCategories.length > 0 || 499 this.appliedStartDate || 500 this.appliedEndDate || 501 this.appliedTags.length > 0 || 502 this.appliedOccurrenceTypes.length > 0 503 ); 504 }, 505 506 visibleOpps() { 507 return this.showSearchResults ? this.searchResults : this.servingOppList; 508 }, 509 510 // Cards highlight on desktop only; null on mobile (card is behind the map view). 511 listSelectedOppKey() { 512 return this.isMobileView ? null : this.selectedOppKey; 513 }, 514 515 isMobileMapDrawerOpen() { 516 return this.isMobileView && this.mobileView === 'map' && !!this.selectedOppKey; 517 }, 518 519 // Floating "My Sign Ups" pill â mobile only, once the cart is reconciled and has 520 // items. Hide it behind the open mobile map drawer unless we're actively animating 521 // the add-to-cart feedback back into view. 522 showCartTotalHoverDisplay() { 523 return this.isCartReady && this.isMobileView && this.cart.length > 0 && (!this.isMobileMapDrawerOpen || this.isCartPillAnimating); 524 }, 525 526 // Drives map focus: false â SoCal overview, true â fit markers. Campus counts only 527 // when changed from the load default, so the home campus still shows the overview. 528 mapHasActiveFilters() { 529 const defaultCampus = this.churchEntityId || 0; 530 return !!( 531 this.appliedSearchQuery || 532 (this.appliedCategories && this.appliedCategories.length) || 533 (this.appliedTags && this.appliedTags.length) || 534 (this.appliedOccurrenceTypes && this.appliedOccurrenceTypes.length) || 535 this.appliedStartDate || 536 this.appliedEndDate || 537 (this.appliedCampusId && this.appliedCampusId !== defaultCampus) 538 ); 539 }, 540 }, 541 542 watch: { 543 selectedOppKey(newKey) { 544 if (this.isMobileView || !newKey) return; 545 this.$nextTick(() => this.scrollListItemIntoView(newKey)); 546 }, 547 }, 548 549 created() { 550 if (this.debug) { 551 console.log(`Is Logged In: ${this.isLoggedIn} 552 Person ID: ${this.personId}, 553 Ministry Categories: ${JSON.stringify(ministryCategories)}, 554 Campus List: ${JSON.stringify(campusList)}, 555 `); 556 } 557 this.rockPerson = rockPerson; 558 }, 559 560 mounted() { 561 if (this.isLoggedIn) { 562 this.getMySignUps(); 563 } 564 565 this.showServePopupOnLoad(); 566 567 // Init filters from URL before fetching data. 568 this.initializeFromUrlParams(); 569 570 // Master (unfiltered) lists. 571 this.fetchMasterCategoryList(); 572 573 this.fetchMasterTagList(); 574 575 // Use current filter state. 576 this.fetchCategories(true, true, false); 577 578 this.fetchTags(); 579 580 window.addEventListener('scroll', this.handleScroll); 581 window.addEventListener('resize', this.handleResize); 582 583 this.handleResize(); 584 585 // Default list when unfiltered; otherwise run the URL-derived search. 586 if (!this.showSearchResults) {
587 this.getServingOppList(); 588 } else { 589 this.getSearchResult(); 590 } 591 }, 592 593 methods: { 594 setCookie(name, value, hours) { 595 const expires = new Date(Date.now() + hours * 60 * 60 * 1000).toUTCString(); 596 document.cookie = `${name}=${value}; expires=${expires}; path=/; SameSite=Lax; Secure`; 597 }, 598 599 getCookie(name) { 600 const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); 601 return match ? match[2] : null; 602 }, 603 604 showServePopupOnLoad() { 605 if (!this.servePopupEnabled) { 606 return; 607 } 608 if (this.getCookie('servePopupDismissed')) { 609 return; 610 } 611 const urlParams = new URLSearchParams(window.location.search); 612 if (urlParams.has("servePopup")) { 613 this.showServePopup = urlParams.get("servePopup").toLowerCase() === "true"; 614 return; 615 } 616 617 this.showServePopup = true; 618 }, 619 620 handleServePopupClose() { 621 this.showServePopup = false; 622 if (this.servePopupCookieHours > 0) { 623 this.setCookie('servePopupDismissed', '1', this.servePopupCookieHours); 624 } 625 }, 626 627 async handleServePopupExplore() { 628 this.showServePopup = false; 629 if (this.servePopupCookieHours > 0) { 630 this.setCookie('servePopupDismissed', '1', this.servePopupCookieHours); 631 } 632 633 const urlParams = new URLSearchParams(window.location.search); 634 const campusFromUrl = this.parseCampusIdFromUrl(urlParams); 635 636 if (campusFromUrl !== null && campusFromUrl !== this.churchEntityId) { 637 this.selectedCampusId = campusFromUrl; 638 } else if (this.churchEntityId && this.churchEntityId > 0) { 639 this.selectedCampusId = this.churchEntityId; 640 } 641 642 if (this.servePopupFilterTag && !this.selectedTags.includes(this.servePopupFilterTag)) { 643 this.selectedTags.push(this.servePopupFilterTag); 644 } 645 646 await this.fetchTags(); 647 648 // Ensure the filter tag appears in the filter panel even if this campus doesn't have it 649 if (this.servePopupFilterTag && !this.ministryTags.some(t => t.name === this.servePopupFilterTag)) { 650 this.ministryTags.push({ id: [null], name: this.servePopupFilterTag }); 651 652 // Campus has no Easter opportunities â show Oops directly 653 this.showSearchResults = true; 654 this.searchResults = []; 655 this.searchResultCount = 0; 656 this.liveSearchResultCount = 0; 657 this.showNoEventsMessage = true; 658 return; 659 } 660
661 this.getSearchResultCount(); 662 this.onSeeResultsClicked(); 663 }, 664 665 initializeFromUrlParams() { 666 const urlParams = new URLSearchParams(window.location.search); 667 let hasFilters = false; 668 669 // Initialize MinistryIds 670 const ministryNames = this.parseMinistryNamesFromUrl(urlParams); 671 if (ministryNames.length > 0) { 672 this.selectedCategories = ministryNames; 673 this.categoryNamesArray = ministryNames; 674 this.appliedCategories = ministryNames; 675 hasFilters = true; 676 } 677 678 // Initialize TagNames 679 const tagNames = this.parseTagNamesFromUrl(urlParams); 680 681 const tagNamesAndIdArray = tagNames.map(name => ({ tagName: name, tagId: null })); 682 if (tagNames.length > 0) { 683 this.selectedTags = tagNames; 684 this.ministryTags = this.buildTagList(tagNamesAndIdArray); 685 this.appliedTags = tagNames; 686 hasFilters = true; 687 } 688 689 // Initialize OccurrenceType (client-side filter) 690 const occurrenceTypes = this.parseOccurrenceTypesFromUrl(urlParams); 691 if (occurrenceTypes.length > 0) { 692 this.selectedOccurrenceTypes = occurrenceTypes; 693 this.appliedOccurrenceTypes = occurrenceTypes; 694 hasFilters = true; 695 } 696 697 // Initialize CampusIds 698 const campusId = this.parseCampusIdFromUrl(urlParams); 699 if (campusId !== null) { 700 this.selectedCampusId = campusId; 701 this.appliedCampusId = campusId; 702 hasFilters = true; 703 } else if (this.isLoggedIn && this.churchEntityId > 0) { 704 // No CampusIds in the URL. If any OTHER filter is present, the user is returning with 705 // "All Campuses" selected (campus 0 isn't written to the URL) â honor it. Only default 706 // to the home campus on a genuinely fresh load. 707 const hasOtherUrlFilters = 708 urlParams.has('MinistryNames') || 709 urlParams.has('Tags') || 710 urlParams.has('OccurrenceType') || 711 urlParams.has('SearchString') || 712 urlParams.has('StartDate') || 713 urlParams.has('EndDate'); 714 if (hasOtherUrlFilters) { 715 this.selectedCampusId = 0; 716 this.appliedCampusId = 0; 717 } else { 718 this.selectedCampusId = this.churchEntityId; 719 this.appliedCampusId = this.churchEntityId; 720 hasFilters = true; 721 urlParams.set('CampusIds', this.churchEntityId); 722 this.updateUrlParams(urlParams); 723 } 724 } 725 726 // Initialize SearchString 727 const searchString = this.parseSearchStringFromUrl(urlParams); 728 if (searchString) { 729 this.searchQuery = searchString; 730 this.appliedSearchQuery = searchString; 731 hasFilters = true; 732 } 733 734 // Initialize Dates 735 const dates = this.parseDatesFromUrl(urlParams); 736 if (dates.startDate) { 737 this.selectedStartDate = dates.startDate; 738 this.appliedStartDate = dates.startDate; 739 hasFilters = true; 740 } 741 if (dates.endDate) { 742 this.selectedEndDate = dates.endDate; 743 this.appliedEndDate = dates.endDate; 744 hasFilters = true; 745 } 746 747 // getSearchResult() in mounted() also sets the count; no separate fetch here. 748 if (hasFilters) { 749 this.showSearchResults = true; 750 } 751 }, 752 753 parseMinistryIdsFromUrl(urlParams) { 754 const ministryIdsParam = urlParams.get('MinistryNames'); 755 if (!ministryIdsParam) return []; 756 757 return ministryIdsParam 758 .split(',') 759 .map(id => parseInt(id, 10)) 760 .filter(id => !isNaN(id)); 761 }, 762 763 parseTagNamesFromUrl(urlParams) { 764 const tagNamesParam = urlParams.get('Tags'); 765 if (!tagNamesParam) return []; 766 767 return tagNamesParam 768 .split(',') 769 .map(name => name.trim()) 770 .filter(name => name.length > 0); 771 }, 772 773 parseOccurrenceTypesFromUrl(urlParams) {
774 const occurrenceParam = urlParams.get('OccurrenceType'); 775 if (!occurrenceParam) return []; 776 777 return occurrenceParam 778 .split(',') 779 .map(type => type.trim()) 780 .filter(type => type === 'oneTime' || type === 'recurring'); 781 }, 782 783 parseMinistryNamesFromUrl(urlParams) { 784 const ministryNamesParam = urlParams.get('MinistryNames'); 785 if (!ministryNamesParam) return []; 786 787 return ministryNamesParam 788 .split(',') 789 .map(name => name.trim()) 790 .filter(name => name.length > 0); 791 }, 792 793 parseCampusIdFromUrl(urlParams) { 794 const campusIdsParam = urlParams.get('CampusIds'); 795 if (!campusIdsParam) return null; 796 797 const campusId = parseInt(campusIdsParam, 10); 798 return isNaN(campusId) ? null : campusId; 799 }, 800 801 parseSearchStringFromUrl(urlParams) { 802 return urlParams.get('SearchString') || ''; 803 }, 804 805 parseDatesFromUrl(urlParams) { 806 const startDateParam = urlParams.get('StartDate'); 807 const endDateParam = urlParams.get('EndDate'); 808 809 return { 810 startDate: startDateParam ? dayjs(startDateParam).format('YYYY-MM-DD') : null, 811 endDate: endDateParam ? dayjs(endDateParam).format('YYYY-MM-DD') : null 812 }; 813 }, 814 815 buildCategoryList(results) { 816 const ministryMap = {}; 817 results.forEach((ministry) => { 818 const name = ministry.ministryName; 819 const id = ministry.ministryId; 820 if (name) { 821 if (!ministryMap[name]) { 822 ministryMap[name] = { name, ids: [id] }; 823 } else if (!ministryMap[name].ids.includes(id)) { 824 ministryMap[name].ids.push(id); 825 } 826 } 827 }); 828 829 return Object.values(ministryMap) 830 .map((m) => ({ 831 id: m.ids, 832 name: m.name, 833 })) 834 .sort((a, b) => a.name.localeCompare(b.name)); 835 }, 836 837 buildTagList(results) { 838 const tagMap = {}; 839 results.forEach((tag) => { 840 const name = tag.tagName; 841 const id = tag.tagId; 842 if (name) { 843 if (!tagMap[name]) { 844 tagMap[name] = { name, ids: [id] }; 845 } else if (!tagMap[name].ids.includes(id)) { 846 tagMap[name].ids.push(id); 847 } 848 } 849 }); 850 851 return Object.values(tagMap) 852 .map((t) => ({ 853 id: t.ids, 854 name: t.name, 855 })) 856 .sort((a, b) => a.name.localeCompare(b.name)); 857 }, 858 859 async fetchCategories(useCampusFilter = true, useDateFilter = true, updateCategoryNamesArray = true) { 860 try { 861 const urlSearchParams = new URLSearchParams(); 862 863 // Add campus filter if enabled and selected 864 if (useCampusFilter && this.selectedCampusId && this.selectedCampusId !== 0) { 865 urlSearchParams.append('CampusIds', this.selectedCampusId); 866 } 867 868 // Add date filter if enabled and both dates are selected 869 if (useDateFilter && this.selectedStartDate && this.selectedEndDate) { 870 urlSearchParams.append('StartDate', this.selectedStartDate); 871 urlSearchParams.append('EndDate', this.selectedEndDate); 872 } 873 874 if(this.selectedTags && this.selectedTags.length > 0) { 875 urlSearchParams.append('Tags', this.selectedTags.join(',')); 876 } 877 878 const apiUrl = `${this.healthyChurchApi}/api/v4/ministries/with-signups?PageSize=500${urlSearchParams.size > 0 ? '&' + urlSearchParams.toString() : ''}`; 879 const response = await axios.get(apiUrl); 880 881 if (response.data.results && response.data.results.length > 0) { 882 this.ministryCategories = []; 883 this.ministryCategories = this.buildCategoryList(response.data.results); 884 // This repopulates the category names array if there are selected categories 885 886 if(updateCategoryNamesArray && this.categoryElementsArray.length > 0) { 887 this.categoryNamesArray = this.ministryCategories.map(c => c.name); 888 } 889 890 } else { 891 this.ministryCategories = []; 892 } 893 } catch (error) { 894 console.error('Error fetching categories:', error); 895 this.ministryCategories = []; 896 } 897 }, 898 899 async fetchTags(useCampusFilter = true, useDateFilter = true) { 900 try { 901 const urlSearchParams = new URLSearchParams(); 902 903 // Add campus filter if enabled and selected 904 if (useCampusFilter && this.selectedCampusId && this.selectedCampusId !== 0) { 905 urlSearchParams.append('CampusIds', this.selectedCampusId); 906 } 907 908 // Add date filter if enabled and both dates are selected 909 if (useDateFilter && this.selectedStartDate && this.selectedEndDate) { 910 urlSearchParams.append('StartDate', this.selectedStartDate); 911 urlSearchParams.append('EndDate', this.selectedEndDate); 912 } 913 914 if(this.categoryNamesArray && this.categoryNamesArray.length > 0) { 915 urlSearchParams.append('MinistryNames', this.categoryNamesArray.join(',')); 916 } 917 918 const apiUrl = `${this.healthyChurchApi}/api/v4/serving-opportunities/tags?PageSize=500${urlSearchParams.size > 0 ? '&' + urlSearchParams.toString() : ''}`; 919 920 const response = await axios.get(apiUrl); 921 922 if (response.data && response.data.length > 0) { 923 this.ministryTags = []; 924 this.ministryTags = this.buildTagList(response.data); 925 } else { 926 this.ministryTags = []; 927 } 928 } catch (error) { 929 console.error('Error fetching tags:', error); 930 this.ministryTags = []; 931 } 932 }, 933 934 async fetchMasterTagList() { 935 try { 936 const apiUrl = `${this.healthyChurchApi}/api/v4/serving-opportunities/tags${this.selectedCampusId !== 0 ? `?CampusIds=${this.selectedCampusId}` : ''}`; 937 const response = await axios.get(apiUrl); 938 939 if (response.data && response.data.length > 0) { 940 this.allMinistryTags = this.buildTagList(response.data); 941 } 942 } catch (error) { 943 console.error('Error fetching master tag list:', error); 944 this.allMinistryTags = []; 945 } 946 }, 947 948 async fetchMasterCategoryList() { 949 try { 950 const apiUrl = `${this.healthyChurchApi}/api/v4/ministries/with-signups?PageSize=500`; 951 const response = await axios.get(apiUrl); 952 953 if (response.data.results && response.data.results.length > 0) { 954 this.allMinistryCategories = this.buildCategoryList(response.data.results); 955 } 956 } catch (error) { 957 console.error('Error fetching master category list:', error); 958 this.allMinistryCategories = []; 959 } 960 }, 961 962 getMySignUps() { 963 const apiUrl = `${this.healthyChurchApi}/api/v4/serving-opportunities/mine`; 964 965 axios 966 .get(apiUrl, this.authHeaders) 967 .then((response) => { 968 this.mySignUpsIds = response.data.map((signup) => ({ 969 id: signup.id, 970 locationId: signup.location?.id, 971 scheduleId: signup.schedules && signup.schedules.length > 0 ? signup.schedules[0].id : null 972 })); 973 this.reconcileCartWithSignUps(); 974 }) 975 .catch((error) => { 976 console.error('Error fetching my sign ups:', error); 977 }) 978 .finally(() => { 979 this.isCartReady = true; 980 }); 981 }, 982 983 // On return from the Rock form, drop cart items the user is now signed up for. 984 reconcileCartWithSignUps() { 985 if (!this.cart.length) return; 986 const signedUpKeys = new Set( 987 this.mySignUpsIds.map((s) => `${s.id ?? 0}-${s.locationId ?? 0}-${s.scheduleId ?? 0}`) 988 ); 989 // Snapshot since removeItem mutates the cart. 990 [...this.cart].forEach((item) => { 991 if (signedUpKeys.has(CartStore.oppKey(item))) { 992 CartStore.removeItem(item); 993 } 994 }); 995 }, 996 997 handleResize() { 998 this.isMobileView = window.innerWidth < 1024; 999 }, 1000 1001 handleScroll() { 1002 if (!this.isMobileView) { 1003 const currentScrollPosition = window.pageYOffset || document.documentElement.scrollTop; 1004 this.isSticky = currentScrollPosition > this.headerHeight; 1005 } 1006 }, 1007 1008 showServingOppFilter(show) { 1009 this.isServingOppFilterOpen = show; 1010 if (show) { 1011 document.body.classList.add('serving-opp-filter-open'); 1012 } else { 1013 document.body.classList.remove('serving-opp-filter-open'); 1014 } 1015 }, 1016 1017 onLoadMoreServingOppList() {
1018 this.getServingOppList(false); 1019 }, 1020 1021 onLoadMoreSearchResult() { 1022 // Disabled: was firing the API call twice per View Results click. 1023 //this.getSearchResult(false); 1024 }, 1025 1026 // Clearing the field re-runs the query with the search term dropped, so any campus or 1027 // category filter the user still has applied stays in effect. 1028 onClearSearch() { 1029 this.searchQuery = ''; 1030 1031 const hasOtherFilters = 1032 this.selectedCampusId || 1033 this.selectedCategories.length || 1034 this.selectedTags.length || 1035 this.selectedOccurrenceTypes.length || 1036 this.selectedStartDate || 1037 this.selectedEndDate; 1038 1039 if (hasOtherFilters) { 1040 this.onSeeResultsClicked(); 1041 1042 return; 1043 } 1044 1045 // Nothing left narrowing the list, so drop back to the browse view 1046 // (onResetFilter already re-fetches the unfiltered list when needed) 1047 this.onResetFilter(); 1048 }, 1049 1050 onSearchClicked() { 1051 this.onSeeResultsClicked(); 1052 }, 1053 1054 onClickFeatured() { 1055 const urlParams = new URLSearchParams(window.location.search); 1056 1057 // Parse the ministry names from URL 1058 const ministryNames = this.parseMinistryNamesFromUrl(urlParams); 1059 if (ministryNames.length > 0) { 1060 this.selectedCategories = ministryNames; 1061 this.categoryNamesArray = ministryNames; 1062 this.appliedCategories = ministryNames; 1063 } 1064 1065 // Parse the tag names from URL 1066 const tagNames = this.parseTagNamesFromUrl(urlParams); 1067 if (tagNames.length > 0) { 1068 this.selectedTags = tagNames; 1069 this.appliedTags = tagNames; 1070 } 1071 1072 // Parse the campus ID from URL 1073 const campusId = this.parseCampusIdFromUrl(urlParams); 1074 if (campusId !== null) { 1075 this.selectedCampusId = campusId; 1076 this.appliedCampusId = campusId; 1077 } 1078 1079 // Clear search query and dates 1080 this.searchQuery = ''; 1081 this.appliedSearchQuery = ''; 1082 this.selectedStartDate = null; 1083 this.selectedEndDate = null; 1084 this.appliedStartDate = null; 1085 this.appliedEndDate = null; 1086 1087 // Show search results and trigger search 1088 this.showSearchResults = true; 1089 this.searchResultHasMore = true; 1090 this.searchResultPage = 0; 1091 1092 this.fetchCategories(); 1093 this.fetchTags(); 1094
1095 this.getSearchResultCount(); 1096 1097 this.getSearchResult(); 1098 1099 // Scroll to top 1100 window.scrollTo({ 1101 top: 0, 1102 behavior: 'smooth', 1103 }); 1104 }, 1105 1106 onSeeResultsClicked() { 1107 this.searchResultHasMore = true; 1108 1109 this.appliedSearchQuery = this.searchQuery; 1110 this.appliedCampusId = this.selectedCampusId; 1111 this.appliedStartDate = this.selectedStartDate; 1112 this.appliedEndDate = this.selectedEndDate; 1113 this.appliedCategories = [...this.selectedCategories]; 1114 this.appliedTags = [...this.selectedTags]; 1115 this.appliedOccurrenceTypes = [...this.selectedOccurrenceTypes]; 1116 this.showSearchResults = true; 1117 this.showServingOppFilter(false); 1118 1119 this.getSearchResult(); 1120 1121 window.scrollTo({ 1122 top: 0, 1123 behavior: 'smooth', 1124 }); 1125 }, 1126 1127 onInputSearchQuery(e) { 1128 this.searchQuery = e.target.value; 1129 }, 1130 1131 onStartDateSelected(startDate) { 1132 this.selectedStartDate = startDate; 1133 if (this.isSelectedDatesValid) { 1134 this.fetchCategories(); 1135 this.fetchTags();
1136 this.getSearchResultCount(); 1137 } else if (!startDate && !this.selectedEndDate) { 1138 this.fetchCategories(); 1139 this.fetchTags(); 1140 this.getSearchResultCount(); 1141 } 1142 }, 1143 1144 onEndDateSelected(endDate) { 1145 this.selectedEndDate = endDate; 1146 if (this.isSelectedDatesValid) { 1147 this.fetchCategories(); 1148 this.fetchTags(); 1149 this.getSearchResultCount(); 1150 } else if (!this.selectedStartDate && !endDate) { 1151 this.fetchCategories(); 1152 this.fetchTags(); 1153 this.getSearchResultCount(); 1154 } 1155 }, 1156 1157 onCampusSelected(campusId) { 1158 this.selectedCampusId = campusId; 1159 1160 this.fetchCategories(); 1161 this.fetchTags(); 1162 1163 this.getSearchResultCount(); 1164 }, 1165 1166 onCategorySelected(categoryIds, categoryNames) { 1167 // If no categoryIds (View All selected), clear the array 1168 if (!categoryIds) { 1169 this.selectedCategories = []; 1170 this.selectedTags = []; 1171 this.ministryCategories = this.allMinistryCategories; 1172 this.ministryTags = this.allMinistryTags; 1173 this.categoryNamesArray = []; 1174 this.fetchCategories(); 1175 this.fetchTags();
1176 this.getSearchResultCount(); 1177 return; 1178 } 1179 1180 // categoryIds is an array of ministry IDs for the selected category 1181 const alreadySelected = categoryIds.filter(id => 1182 this.selectedCategories.includes(id) 1183 ); 1184 1185 const nameAlreadySelected = this.categoryNamesArray.includes(categoryNames); 1186 1187 if (alreadySelected.length > 0) { 1188 // Remove all IDs for this category 1189 this.selectedCategories = this.selectedCategories.filter(id => 1190 !categoryIds.includes(id) 1191 ); 1192 } else { 1193 // Add all IDs for this category 1194 this.selectedCategories.push(categoryNames); 1195 } 1196 1197 if (nameAlreadySelected) { 1198 // Remove all names for this category 1199 this.categoryNamesArray = this.categoryNamesArray.filter(name => 1200 !categoryNames.includes(name) 1201 ); 1202 } else { 1203 // Add all names for this category 1204 this.categoryNamesArray.push(categoryNames); 1205 } 1206 1207 this.fetchTags(); 1208
1209 this.getSearchResultCount(); 1210 }, 1211 1212 onTagSelected(tagName) { 1213 // If no tagName (View All selected), clear the array 1214 if (!tagName) { 1215 this.selectedTags = []; 1216 this.selectedCategories = []; 1217 this.ministryTags = this.allMinistryTags; 1218 this.ministryCategories = this.allMinistryCategories; 1219 this.categoryNamesArray = []; 1220 this.fetchCategories(); 1221 this.fetchTags(); 1222 this.getSearchResultCount(); 1223 return; 1224 } 1225 1226 // Check if the tag is already selected 1227 const alreadySelected = this.selectedTags.includes(tagName); 1228 1229 if (alreadySelected) { 1230 // Remove the tag from the selectedTags array 1231 this.selectedTags = this.selectedTags.filter(name => name !== tagName); 1232 } else { 1233 // Add the tag to the selectedTags array 1234 this.selectedTags = [...this.selectedTags, tagName]; 1235 } 1236 1237 this.fetchCategories(); 1238
1239 this.getSearchResultCount(); 1240 1241 }, 1242 1243 onOccurrenceSelected(occurrenceType) { 1244 // No value (shouldn't happen from the UI) clears the filter. 1245 if (!occurrenceType) { 1246 this.selectedOccurrenceTypes = []; 1247 } else if (this.selectedOccurrenceTypes.includes(occurrenceType)) { 1248 // Toggle off if already selected. 1249 this.selectedOccurrenceTypes = this.selectedOccurrenceTypes.filter((type) => type !== occurrenceType); 1250 } else { 1251 this.selectedOccurrenceTypes = [...this.selectedOccurrenceTypes, occurrenceType]; 1252 } 1253 1254 this.getSearchResultCount(); 1255 }, 1256 1257 onCategoryClickedFromListItem(ministryName) { 1258 // Clear filters 1259 this.searchQuery = ''; 1260 this.selectedTags = []; 1261 this.selectedCategories = []; 1262 this.selectedOccurrenceTypes = []; 1263 this.appliedOccurrenceTypes = []; 1264 this.selectedCampusId = this.selectedCampusId > 0 ? this.selectedCampusId : 0; 1265 this.selectedStartDate = null; 1266 this.selectedEndDate = null; 1267 this.ministryCategories = this.allMinistryCategories; 1268 this.ministryTags = this.allMinistryTags; 1269 this.categoryNamesArray = []; 1270 1271 // Apply filters 1272 this.appliedSearchQuery = ''; 1273 this.appliedCampusId = 0; 1274 this.appliedStartDate = null; 1275 this.appliedEndDate = null; 1276 1277 this.showSearchResults = true; 1278 this.searchResultHasMore = true; 1279 this.searchResultPage = 0; 1280 1281 this.searchResultHasMore = true; 1282
1283 this.getSearchResult(); 1284 1285 this.selectedCategories = [ministryName]; 1286 1287 const nameAlreadySelected = this.categoryNamesArray.includes(ministryName); 1288 1289 if (nameAlreadySelected) { 1290 // Remove all names for this category 1291 this.categoryNamesArray = this.categoryNamesArray.filter(name => 1292 !ministryName.includes(name) 1293 ); 1294 } else { 1295 // Add all names for this category 1296 this.categoryNamesArray.push(ministryName); 1297 } 1298 1299 // We only need to update the appliedCategories since it's the only thing that changes here. 1300 this.appliedCategories = [...this.selectedCategories]; 1301 1302 // Reload all categories since we're clearing campus/date filters 1303 this.fetchCategories( true, true, false ); 1304 this.fetchTags(); 1305 1306 // Set live count for the filter button 1307 this.getSearchResultCount(); 1308 1309 // Trigger search 1310 this.getSearchResult(); 1311 1312 window.scrollTo({ 1313 top: 0, 1314 behavior: 'smooth', 1315 }); 1316 }, 1317 1318 onTagClickedFromListItem(ministryName) { 1319 // Reset the selected tags so we only include the new one that has been clicked 1320 this.selectedTags = []; 1321 this.selectedOccurrenceTypes = []; 1322 this.appliedOccurrenceTypes = []; 1323 // If no tagName (View All selected), clear the array 1324 if (!ministryName) { 1325 this.selectedTags = []; 1326 this.selectedCategories = []; 1327 this.ministryTags = this.allMinistryTags; 1328 this.ministryCategories = this.allMinistryCategories; 1329 this.categoryNamesArray = [];
1330 this.getSearchResultCount(); 1331 return; 1332 } 1333 1334 this.selectedTags = [ministryName]; 1335 1336 // Check if the tag is already selected 1337 const alreadySelected = this.selectedTags.includes(ministryName); 1338 1339 if (alreadySelected) { 1340 // Remove the tag from the selectedTags array 1341 this.selectedTags = this.selectedTags.filter(name => name !== ministryName); 1342 } else { 1343 // Add the tag to the selectedTags array 1344 this.selectedTags = [...this.selectedTags, ministryName]; 1345 } 1346 1347 this.fetchCategories(); 1348 1349 this.getSearchResultCount(); 1350 1351 this.getSearchResult(); 1352 1353 window.scrollTo({ 1354 top: 0, 1355 behavior: 'smooth', 1356 }); 1357 }, 1358 1359 onResetFilter() { 1360 this.searchQuery = ''; 1361 this.ministryId = null; 1362 this.selectedCategories = []; 1363 this.categoryNamesArray = []; 1364 this.selectedTags = []; 1365 this.selectedOccurrenceTypes = []; 1366 this.selectedCampusId = 0; 1367 this.selectedStartDate = null; 1368 this.selectedEndDate = null; 1369 1370 this.appliedSearchQuery = ''; 1371 this.appliedCategories = []; 1372 this.appliedTags = []; 1373 this.appliedOccurrenceTypes = []; 1374 this.appliedCampusId = 0; 1375 this.appliedStartDate = null; 1376 this.appliedEndDate = null; 1377 1378 this.searchResults = []; 1379 this.searchResultCount = 0; 1380 this.showSearchResults = false; 1381 this.searchResultPage = 0; 1382 this.searchResultHasMore = true; 1383 this.showNoEventsMessage = false; 1384 1385 // Close the map drawer; the selected opp may not be in the unfiltered set. 1386 this.selectedOppKey = null; 1387 1388 // Close the filter drawer so the reset (unfiltered) list is revealed. 1389 this.showServingOppFilter(false); 1390 1391 this.fetchCategories(); 1392 this.fetchTags(); 1393 1394 // Fetch the unfiltered list if it was never loaded (e.g. landed via URL filters). 1395 if (this.servingOppList.length === 0) {
1396 this.getServingOppList(); 1397 } 1398 1399 const url = window.location.origin + window.location.pathname; 1400 window.history.replaceState({}, '', `${url}`); 1401 }, 1402 1403 getServingOppList(reset = true) { 1404 if (this.isServingOppListLoading || !this.servingOppListHasMore) return; 1405 1406 this.isServingOppListLoading = true; 1407 1408 if (reset) { 1409 this.servingOppListPage = 0; 1410 } 1411 1412 $('.search-input').addClass('filter-btn-disabled'); 1413 $('.date-range').addClass('filter-btn-disabled'); 1414 $('.serving-opp-category-filter').addClass('filter-btn-disabled'); 1415 $('.saddleback-dropdown').addClass('filter-btn-disabled'); 1416 1417 const SearchServingOpps = `${healthyChurchApi}/api/v4/serving-opportunities?PageSize=500&EndDate=${this.DEFAULT_END_DATE}`; 1418 1419 axios 1420 .get(SearchServingOpps) 1421 .then((response) => { 1422 this.servingOppList = [...this.servingOppList, ...response.data]; 1423 this.servingOppListCount = response.data.length; 1424 this.servingOppListPage++; 1425 this.servingOppListHasMore = false; 1426 if (this.servingOppListCount <= 0 && this.searchResultCount <= 0) { 1427 this.showNoEventsMessage = true; 1428 } else { 1429 this.showNoEventsMessage = false; 1430 } 1431 }) 1432 .catch(() => { 1433 this.servingOppList = []; 1434 this.servingOppListCount = 0; 1435 this.servingOppListPage = 0; 1436 this.servingOppListHasMore = false; 1437 }) 1438 .finally(() => { 1439 this.isServingOppListLoading = false; 1440 1441 $('.search-input').removeClass('filter-btn-disabled'); 1442 $('.date-range').removeClass('filter-btn-disabled'); 1443 $('.serving-opp-category-filter').removeClass('filter-btn-disabled'); 1444 $('.saddleback-dropdown').removeClass('filter-btn-disabled'); 1445 }); 1446 }, 1447 1448 getSearchResultCount() { 1449 this.isCountLoading = true; 1450 let SearchServingOpps = ''; 1451 1452 $('.search-input').addClass('filter-btn-disabled'); 1453 $('.date-range').addClass('filter-btn-disabled'); 1454 $('.serving-opp-category-filter').addClass('filter-btn-disabled'); 1455 $('.saddleback-dropdown').addClass('filter-btn-disabled'); 1456 1457 const urlSearchParams = new URLSearchParams(); 1458 1459 if (this.searchQuery) urlSearchParams.append('SearchString', this.searchQuery); 1460 if (this.ministryId) urlSearchParams.append('MinistryNames', this.ministryId); 1461 if (this.selectedCampusId) urlSearchParams.append('CampusIds', this.selectedCampusId); 1462 1463 // if (this.selectedCategories.length > 0) { 1464 // urlSearchParams.append('MinistryIds', this.selectedCategories.join(',')); 1465 // } 1466 1467 if(this.categoryNamesArray && this.categoryNamesArray.length > 0) { 1468 urlSearchParams.append('MinistryNames', this.categoryNamesArray.join(',')); 1469 } 1470 1471 // Keep only selectedTags that still exist in ministryTags. 1472 this.selectedTags = this.selectedTags.filter(tag => { 1473 return this.ministryTags.some(ministryTag => ministryTag.name === tag); 1474 }); 1475 1476 if (this.selectedTags.length > 0) { 1477 urlSearchParams.append('Tags', this.selectedTags.join(',')); 1478 } 1479 1480 if (this.selectedStartDate && this.selectedEndDate) { 1481 urlSearchParams.append('StartDate', this.selectedStartDate); 1482 urlSearchParams.append('EndDate', this.selectedEndDate); 1483 } else { 1484 // Default end date for non-date searches. 1485 urlSearchParams.append('EndDate', this.DEFAULT_END_DATE); 1486 } 1487 1488 SearchServingOpps = `${healthyChurchApi}/api/v4/serving-opportunities?PageSize=500${urlSearchParams.size > 0 ? '&' + urlSearchParams.toString() : ''}`; 1489 1490 axios 1491 .get(SearchServingOpps) 1492 .then((response) => { 1493 this.liveSearchResultCount = this.filterByOccurrence(response.data).length; 1494 }) 1495 .catch(() => { 1496 this.searchResultPage = 0; 1497 }) 1498 .finally(() => { 1499 this.isSearchResultLoading = false; 1500 1501 $('.search-input').removeClass('filter-btn-disabled'); 1502 $('.date-range').removeClass('filter-btn-disabled'); 1503 $('.serving-opp-category-filter').removeClass('filter-btn-disabled'); 1504 $('.saddleback-dropdown').removeClass('filter-btn-disabled'); 1505 }); 1506 }, 1507 1508 getSearchResult(reset = true) { 1509 this.isSearchResultLoading = true; 1510 let SearchServingOpps = ''; 1511 const urlSearchParams = new URLSearchParams(); 1512 1513 // Fade the search input + filters while the search runs (same treatment as /events) 1514 $('.search-input').addClass('filter-btn-disabled'); 1515 $('.date-range').addClass('filter-btn-disabled'); 1516 $('.serving-opp-category-filter').addClass('filter-btn-disabled'); 1517 $('.saddleback-dropdown').addClass('filter-btn-disabled'); 1518 1519 if (this.searchQuery) urlSearchParams.append('SearchString', this.searchQuery); 1520 if (this.ministryId) urlSearchParams.append('MinistryNames', this.ministryId); 1521 if (this.selectedCampusId) urlSearchParams.append('CampusIds', this.selectedCampusId); 1522 1523 // if (this.selectedCategories.length > 0) { 1524 // urlSearchParams.append('MinistryIds', this.selectedCategories.join(',')); 1525 // } 1526 1527 if(this.categoryNamesArray && this.categoryNamesArray.length > 0) { 1528 urlSearchParams.append('MinistryNames', this.categoryNamesArray.join(',')); 1529 } 1530 1531 // Keep only selectedTags that still exist in ministryTags. 1532 this.selectedTags = this.selectedTags.filter(tag => { 1533 return this.ministryTags.some(ministryTag => ministryTag.name === tag); 1534 }); 1535 1536 if (this.selectedTags.length > 0) { 1537 urlSearchParams.append('Tags', this.selectedTags.join(',')); 1538 } 1539 1540 // API params kept separate from the URL params. 1541 const apiSearchParams = new URLSearchParams(urlSearchParams); 1542 1543 // Occurrence type is client-side only â put it on the URL (for deep-links / 1544 // back-button retention) but never on the API call. 1545 if (this.selectedOccurrenceTypes.length > 0) { 1546 urlSearchParams.append('OccurrenceType', this.selectedOccurrenceTypes.join(',')); 1547 } 1548 1549 if (this.selectedStartDate && this.selectedEndDate) { 1550 urlSearchParams.append('StartDate', this.selectedStartDate); 1551 apiSearchParams.append('StartDate', this.selectedStartDate); 1552 urlSearchParams.append('EndDate', this.selectedEndDate); 1553 apiSearchParams.append('EndDate', this.selectedEndDate); 1554 } else { 1555 // Default EndDate on the API call only, not the URL. 1556 apiSearchParams.append('EndDate', this.DEFAULT_END_DATE); 1557 } 1558 1559 SearchServingOpps = `${healthyChurchApi}/api/v4/serving-opportunities?PageSize=500${apiSearchParams.size > 0 ? '&' + apiSearchParams.toString() : ''}`; 1560 1561 if (reset) { 1562 this.searchResultPage = 0; 1563 } 1564 1565 axios 1566 .get(SearchServingOpps) 1567 .then((response) => { 1568 // Occurrence type has no API param, so filter the response client-side. 1569 const results = this.filterByOccurrence(response.data); 1570 this.searchResultCount = results.length;
1571 // Sync the filter-panel count from this response (avoids a duplicate fetch). 1572 this.liveSearchResultCount = results.length; 1573 this.searchResults = results; 1574 this.searchResultPage++; 1575 this.searchResultHasMore = false; 1576 // urlSearchParams omits the default EndDate. 1577 this.updateUrlParams(urlSearchParams); 1578 if(this.searchResultCount <= 0) { 1579 this.showNoEventsMessage = true; 1580 } else { 1581 this.showNoEventsMessage = false; 1582 } 1583 }) 1584 .catch((error) => { 1585 console.error('Error fetching search results:', error); 1586 this.searchResultCount = 0; 1587 this.searchResults = []; 1588 this.searchResultPage = 0; 1589 this.searchResultHasMore = true; 1590 }) 1591 .finally(() => { 1592 this.isSearchResultLoading = false; 1593 1594 $('.search-input').removeClass('filter-btn-disabled'); 1595 $('.date-range').removeClass('filter-btn-disabled'); 1596 $('.serving-opp-category-filter').removeClass('filter-btn-disabled'); 1597 $('.saddleback-dropdown').removeClass('filter-btn-disabled'); 1598 }); 1599 }, 1600 1601 updateUrlParams(urlParamsList) { 1602 const url = window.location.origin + window.location.pathname; 1603 const newUrl = `${url}${urlParamsList.size > 0 ? '?' : ''}${urlParamsList.toString()}`; 1604 window.history.replaceState({}, '', newUrl); 1605 }, 1606 1607 // Recurring opps require a confirmation step before any add-to-cart or sign-up action. 1608 isRecurringOpp(opp) { 1609 return opp?.schedules?.[0]?.isRecurring === true; 1610 }, 1611 1612 // Occurrence-type filter is client-side (no API param). Empty or both types 1613 // selected means "no filter" â return everything. 1614 filterByOccurrence(list) { 1615 const types = this.selectedOccurrenceTypes; 1616 // Two types exist (oneTime, recurring); selecting both is the same as no filter. 1617 if (!types.length || types.length === 2) return list; 1618 const wantRecurring = types.includes('recurring'); 1619 return list.filter((opp) => this.isRecurringOpp(opp) === wantRecurring); 1620 }, 1621 1622 onAddToCart(servingOpp, context = {}) { 1623 this.clearPendingMapInfoClose(); 1624 if (this.isRecurringOpp(servingOpp)) { 1625 this.confirmingOpp = servingOpp; 1626 this.confirmingAction = 'add-to-cart'; 1627 this.confirmingContext = context; 1628 return; 1629 } 1630 this._addToCart(servingOpp, context); 1631 }, 1632 1633 _addToCart(servingOpp, context = {}) { 1634 const isFirstAdd = this.cart.length === 0; 1635 CartStore.addItem(servingOpp); 1636 1637 // Desktop: pop the cart modal on the first add. (The mobile floating pill is a 1638 // computed off cart length, so no manual toggle needed.) 1639 this.isShoppingCartModalOpen = isFirstAdd && !this.isMobileView; 1640 1641 const isMobileMapFlow = this.isMobileView && this.mobileView === 'map' && context?.source === 'map'; 1642 if (isMobileMapFlow) { 1643 this.triggerCartPillSlideIn(); 1644 this.mapInfoCloseTimeoutId = window.setTimeout(() => { 1645 this.selectedOppKey = null; 1646 this.mapInfoCloseTimeoutId = null; 1647 }, 540); 1648 } 1649 }, 1650 1651 // Sign-up emitted from a list-item card. 1652 onListItemSignUp(servingOpp) { 1653 if (this.isRecurringOpp(servingOpp)) { 1654 this.confirmingOpp = servingOpp; 1655 this.confirmingAction = 'sign-up'; 1656 return; 1657 } 1658 this._goToSignUp(servingOpp); 1659 }, 1660 1661 _goToSignUp(servingOpp) { 1662 if (!servingOpp?.signUpUrl) return; 1663 const signUpUrl = this.isLoggedIn 1664 ? `${this.rockDomainUrl}/sso.ashx?redirectUrl=${encodeURIComponent(servingOpp.signUpUrl)}&t=${this.accessToken}` 1665 : servingOpp.signUpUrl; 1666 window.open(signUpUrl, '_blank'); 1667 }, 1668 1669 // Run the pending action, then clear state. 1670 onConfirmRecurring() { 1671 const opp = this.confirmingOpp; 1672 const action = this.confirmingAction; 1673 const context = this.confirmingContext; 1674 this.confirmingOpp = null; 1675 this.confirmingAction = null; 1676 this.confirmingContext = null; 1677 if (!opp || !action) return; 1678 if (action === 'add-to-cart') this._addToCart(opp, context); 1679 else if (action === 'sign-up') this._goToSignUp(opp); 1680 }, 1681 1682 onCancelRecurring() { 1683 this.confirmingOpp = null; 1684 this.confirmingAction = null; 1685 this.confirmingContext = null; 1686 }, 1687 1688 onRemoveFromCart(servingOpp) { 1689 // Remove via the store to preserve the array reference (keeps bindings live). 1690 CartStore.removeItem(servingOpp); 1691 1692 if (this.cart.length === 0) { 1693 this.isShoppingCartModalOpen = false; 1694 } 1695 }, 1696 1697 oppKey(opp) { 1698 const id = opp?.id ?? 0; 1699 const locationId = opp?.location?.id ?? 0; 1700 const scheduleId = opp?.schedules?.[0]?.id ?? 0; 1701 return `${id}-${locationId}-${scheduleId}`; 1702 }, 1703 1704 onMapPinSelected(opp) { 1705 this.clearPendingMapInfoClose(); 1706 this.selectedOppKey = this.oppKey(opp); 1707 }, 1708 1709 // Card click â select its map marker (desktop only; no-op on mobile's toggled view). 1710 onListItemSelected(opp) { 1711 if (this.isMobileView) return; 1712 this.selectedOppKey = this.oppKey(opp); 1713 this.anchorToResults(); 1714 }, 1715 1716 // Smooth-scroll .serving-opps-inner (map + list) to just below the header. 1717 anchorToResults() { 1718 const inner = document.querySelector('.serving-opps-inner'); 1719 if (!inner) return; 1720 const offset = 100; 1721 const top = window.scrollY + inner.getBoundingClientRect().top - offset; 1722 window.scrollTo({ top, behavior: 'smooth' });
1723 }, 1724 1725 onMapClearSelection() { 1726 this.clearPendingMapInfoClose(); 1727 this.selectedOppKey = null; 1728 }, 1729 1730 clearPendingMapInfoClose() { 1731 if (this.mapInfoCloseTimeoutId !== null) { 1732 clearTimeout(this.mapInfoCloseTimeoutId); 1733 this.mapInfoCloseTimeoutId = null; 1734 } 1735 }, 1736 1737 triggerCartPillSlideIn() { 1738 if (this.cartPillAnimationTimeoutId !== null) { 1739 clearTimeout(this.cartPillAnimationTimeoutId); 1740 this.cartPillAnimationTimeoutId = null; 1741 } 1742 1743 this.isCartPillAnimating = false; 1744 this.$nextTick(() => { 1745 this.isCartPillAnimating = true; 1746 this.cartPillAnimationTimeoutId = window.setTimeout(() => { 1747 this.isCartPillAnimating = false; 1748 this.cartPillAnimationTimeoutId = null; 1749 }, 500); 1750 }); 1751 }, 1752 1753 onMapSignUp(servingOpp) { 1754 if (this.isRecurringOpp(servingOpp)) { 1755 this.confirmingOpp = servingOpp; 1756 this.confirmingAction = 'sign-up'; 1757 return; 1758 } 1759 this._goToSignUp(servingOpp); 1760 }, 1761 1762 scrollListItemIntoView(oppKey) { 1763 const itemEl = document.querySelector(`[data-opp-key="${oppKey}"]`); 1764 if (!itemEl) return; 1765 const container = itemEl.closest('.serving-opp-list-column'); 1766 if (!container) return; 1767 const containerRect = container.getBoundingClientRect(); 1768 const itemRect = itemEl.getBoundingClientRect(); 1769 const scrollOffset = container.scrollTop + (itemRect.top - containerRect.top) - 20; 1770 container.scrollTo({ top: scrollOffset, behavior: 'smooth' }); 1771 }, 1772 1773 toggleMobileView() { 1774 this.mobileView = this.mobileView === 'list' ? 'map' : 'list'; 1775 }, 1776 }, 1777 1778 beforeUnmount() { 1779 window.removeEventListener('scroll', this.handleScroll); 1780 window.removeEventListener('resize', this.handleResize); 1781 this.clearPendingMapInfoClose(); 1782 if (this.cartPillAnimationTimeoutId !== null) { 1783 clearTimeout(this.cartPillAnimationTimeoutId); 1784 this.cartPillAnimationTimeoutId = null; 1785 } 1786 }, 1787 }); 1788 1789 const vm = app.mount(appId); 1790}; 1791 1792export default createApp;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.