PageSourceSearch

https://saddleback.com/scripts/ServingOpps/ServingOpps.js?v=b2d0741167059c5ea156aba9420b4bee89d6778d

js saddleback.com collected 2026-10-02 02:47:49 UTC 71,510 bytes, 1,792 lines download raw bytes

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.