PageSourceSearch

https://saddleback.com/scripts/LocationsPage/LocationsPage.js?v=517cdeafa93af7720f65d0a6f449ca34186eeedc

js saddleback.com collected 2026-09-24 17:17:07 UTC 68,624 bytes, 1,500 lines download raw bytes

1import LocationsSearch from '/scripts/LocationsPage/Components/LocationsSearch.js';
2import LocationsMap from '/scripts/LocationsPage/Components/LocationsMap.js';
3import LocationsList from '/scripts/LocationsPage/Components/LocationsList.js';
4import Toggle from '/scripts/LocationsPage/Components/Toggle.js';
5import ExtensionDetailsPopup from '/scripts/LocationsPage/Components/Modals/ExtensionDetailsPopup.js';
6import LanguageServiceTimesModal from '/scripts/LocationsPage/Components/Modals/LanguageServiceTimesModal.js';
7import UserDistanceModal from '/scripts/LocationsPage/Components/Modals/UserDistanceModal.js';
8import convertRegionByCountry, { countryAbbreviations } from '/scripts/utils/StateConverter.js';
9import LoadingSpinner from '/scripts/VueComponents/LoadingSpinner.js';
10
11export default {
12    name: 'LocationsPage',
13
14    components: {
15        LocationsSearch,
16        LocationsMap,
17        LocationsList,
18        Toggle,
19        ExtensionDetailsPopup,
20        LanguageServiceTimesModal,
21        UserDistanceModal,
22        LoadingSpinner,
23    },
24
25    props: {
26        sbTechApi: { type: String, default: '' },
27
28        // Rock API data
29        campuses: { type: Array, default: () => [] },
30
31        enableExtensions: {type: Boolean, default: false },
32
33        // Marker Icons
34        defaultMarkerIconCampus: { type: String, default: '' },
35        activeMarkerIconCampus: { type: String, default: '' },
36        defaultMarkerIconExtension: { type: String, default: '' },
37        activeMarkerIconExtension: { type: String, default: '' },
38        userLocationIcon: { type: String, default: '' },
39
40        // Dictionary Text props
41        enterYourLocationText: { type: String, default: 'Enter your location' },
42        noRouteAvailableText: { type: String, default: 'No route available' },
43        directionsText: { type: String, default: 'Directions' },
44        viewCampusText: { type: String, default: 'View Campus' },
45        viewDetailsText: { type: String, default: 'View Details' },
46        extensionText: { type: String, default: 'Extension' },
47        extensionDetailsText: { type: String, default: 'Extension Details' },
48        listText: { type: String, default: 'List' },
49        mapText: { type: String, default: 'Map' },
50        showText: { type: String, default: 'Show' },
51        hideText: { type: String, default: 'Hide' },
52        amText: { type: String, default: 'AM' },
53        pmText: { type: String, default: 'PM' },
54        kmText: { type: String, default: 'km' },
55        miText: { type: String, default: 'mi' },
56        monthsText: { type: String, default: 'Months' },
57        englishText: { type: String, default: 'English' },
58        germanText: { type: String, default: 'German' },
59        spanishText: { type: String, default: 'Spanish' },
60        serviceTypeText: { type: String, default: 'Service Type' },
61        dailyText: { type: String, default: 'Daily' },
62        weeklyText: { type: String, default: 'Weekly' },
63        biWeeklyText: { type: String, default: 'Bi-Weekly' },
64        monthlyText: { type: String, default: 'Monthly' },
65        firstText: { type: String, default: '1st' },
66        secondText: { type: String, default: '2nd' },
67        thirdText: { type: String, default: '3rd' },
68        fourthText: { type: String, default: '4th' },
69        lastText: { type: String, default: 'Last' },
70        connectWithLeaderText: { type: String, default: 'Connect with Extension Leader' },
71        contactLeaderForInfoText: { type: String, default: 'Contact leader for more info' },
72        watchOnlineText: { type: String, default: 'Watch Online' },
73        learnMoreText: { type: String, default: 'Learn More' },
74        viewExtensionsText: { type: String, default: 'View Extensions' },
75        locationsPageHeader: { type: String, default: 'Locations' },
76        contactExtensionLeaderText: { type: String, default: 'Contact Extension Leader' },
77        noLocationsHeaderText: { type: String, default: 'No Saddleback locations near you' },
78        noCampusHeaderText: { type: String, default: 'No Saddleback campus near you' },
79        noLocationsBodyText: { type: String, default: "Looks like you're pretty far from any of our locations. Consider starting an Extension or watch online." },
80        noCampusBodyText: { type: String, default: "Looks like you're pretty far from any of our campuses. Check out more ways to worship with us." },
81
82        // Language Service Times Modal props - all content driven, per culture
83        languageServiceTimes: { type: Array, default: () => [] },
84        languagePopupIntro: { type: String, default: '' },
85        languagePopupIntroEnglish: { type: String, default: '' },
86        dayNamesText: { type: Object, default: () => ({}) },
87        englishDayNamesText: { type: Object, default: () => ({}) },
88        switchToEnglishText: { type: String, default: 'Switch to English' },
89        languageWordText: { type: String, default: 'Language' },
90        switchToNativeText: { type: String, default: '' },
91        languageWordNativeText: { type: String, default: '' },
92
93        // Bottom buttons props
94        onlineCommunityLink: { type: String, default: '' },
95        onlineCommunityIcon: { type: String, default: '' },
96        onlineCommunityHeader: { type: String, default: '' },
97        onlineCommunityText: { type: String, default: '' },
98        extensionsLink: { type: String, default: '' },
99        extensionsIcon: { type: String, default: '' },
100        extensionsHeader: { type: String, default: '' },
101        extensionsText: { type: String, default: '' },
102        planAVisitLink: { type: String, default: '' },
103        planAVisitIcon: { type: String, default: '' },
104        planAVisitHeader: { type: String, default: '' },
105        planAVisitText: { type: String, default: '' }
106    },
107
108    data() {
109        return {
110            // Responsive state
111            windowWidth: window.innerWidth,
112            showingList: true,
113
114            isLoadingLocation: true,
115
116            // Location data
117            extensions: [],
118            processedCampuses: [],
119
120            // Search and location state
121            userLocation: null,
122            originAddress: null,
123            searchInput: '',
124
125            // Selected location and UI state
126            selectedLocation: null,
127            selectedExtension: null,
128            showInfoWindow: false,
129
130            // Services
131            locationGeoCoder: null,
132            campusDistanceService: null,
133
134            // Distance tracking
135            sortedDistances: [],
136            extensionDistancesCalculated: false,
137            originCountry: null,
138
139            // Constants
140            BREAKPOINTS: {
141                MOBILE: 744,
142                DESKTOP: 1024
143            },
144
145            // Toggle switch
146            showExtensions: false,
147            pendingShowExtensions: false,
148
149            isExtensionDetailsPopupVisible: false,
150
151            // Language Service Time Modal vars
152            isLanguageServiceTimesModalVisible: false,
153            currentLanguage: null,
154
155            // Distance Modal vars
156            isUserDistanceModalVisible: false,
157        };
158    },
159
160    computed: {
161        isMobile() { return this.windowWidth < this.BREAKPOINTS.MOBILE; },
162        isTablet() { return this.windowWidth >= this.BREAKPOINTS.MOBILE && this.windowWidth < this.BREAKPOINTS.DESKTOP; },
163        isDesktop() { return this.windowWidth >= this.BREAKPOINTS.DESKTOP; },
164        
165        normalizedCampuses() {
166            return this.normalizeRockCampuses(this.campuses);
167        },
168
169        // Glyph inside the language popup's speech-bubble icon. Chinese gets a
170        // character, everything else the culture code, so new languages need no change.
171        languagePopupIconText() {
172            const language = this.currentLanguage || this.detectCurrentLanguage();
173
174            if (language === 'zh-hans') return '中';
175            if (language === 'zh-hant') return '中';
176
177            return language.split('-')[0].toUpperCase();
178        },
179
180        filteredCampuses() {
181            return this.normalizedCampuses.filter(campus =>
182                campus.name !== "Online Community" &&
183                campus.churchEntityTypeId &&
184                typeof campus.address1 !== 'undefined' &&
185                campus.id !== 20 && // Vancouver Campus - changed to a "Community"
186                campus.id !== 23 && // Whittier Espanol
187                campus.id !== 24 && // San Juan Capistrano Espanol
188                campus.id !== 27 && // Extension Campus
189                campus.id !== 29 && // Mandarin Community
190                typeof campus.latitude !== 'undefined'
191            );
192        },
193
194        sortedCampusesForDisplay() {
195            if (this.sortedDistances.length === 0) {
196                return this.processedCampuses;
197            }
198
199            const sorted = [];
200            this.sortedDistances.forEach(distanceInfo => {
201                const campus = this.processedCampuses.find(c => c.id === distanceInfo.campusId);
202                if (campus) {
203                    sorted.push(campus);
204                }
205            });
206
207            this.processedCampuses.forEach(campus => {
208                if (!sorted.find(c => c.id === campus.id)) {
209                    sorted.push(campus);
210                }
211            });
212
213            return sorted;
214        },
215
216        allLocations() {
217            return [
218                ...this.sortedCampusesForDisplay.map(campus => {
219                    campus.type = 'campus';
220                    return campus;
221                }),
222                ...this.extensions.map(extension => {
223                    extension.type = 'extension';
224                    return extension;
225                })
226            ];
227        },
228
229        sortedExtensionsForDisplay() {
230            if (!this.extensions || this.extensions.length === 0) {
231                return [];
232            }
233
234            if (this.originAddress && this.sortedDistances.length > 0) {
235                // Sort by distance if user location is known
236                return [...this.extensions].sort((a, b) => {
237                    const aDistance = a.distanceValue || 999999;
238                    const bDistance = b.distanceValue || 999999;
239                    return aDistance - bDistance;
240                });
241            } else {
242                // Sort alphabetically if no user location
243                return [...this.extensions].sort((a, b) => 
244                    a.name.localeCompare(b.name)
245                );
246            }
247        },
248
249        allSortedLocationsForDisplay() {
250            if (!this.showExtensions) {
251                return this.sortedCampusesForDisplay.map(campus => ({ ...campus, type: 'campus' }));
252            }
253
254            const campuses = this.processedCampuses.map(campus => ({ ...campus, type: 'campus' }));
255            // Extensions WITH location data
256            const extensionsWithLocation = this.extensions.filter(ext => ext.latitude && ext.longitude);
257            // Extensions WITHOUT location data (includes both virtual and in-person without coordinates)
258            const extensionsWithoutLocation = this.extensions.filter(ext => !ext.latitude || !ext.longitude);
259
260            if (this.originAddress) {
261                // Sort physical locations by distance
262                const allPhysicalLocations = [...campuses, ...extensionsWithLocation];
263                
264                const sorted = allPhysicalLocations.sort((a, b) => {
265                    // Handle "no route available" items
266                    const aIsNoRoute = a.distanceText === this.noRouteAvailableText;
267                    const bIsNoRoute = b.distanceText === this.noRouteAvailableText;
268                    
269                    if (aIsNoRoute && bIsNoRoute) {
270                        return a.name.localeCompare(b.name);
271                    }
272                    if (aIsNoRoute) return 1;  // a goes to bottom
273                    if (bIsNoRoute) return -1; // b goes to bottom
274
275                    // Sort all other locations by actual distance
276                    const aDistance = a.distanceValue !== undefined ? a.distanceValue : 999999;
277                    const bDistance = b.distanceValue !== undefined ? b.distanceValue : 999999;
278                    
279                    return aDistance - bDistance;
280                });
281                
282                // Add extensions without location at the very end, sorted alphabetically
283                const sortedNoLocation = extensionsWithoutLocation.sort((a, b) => a.name.localeCompare(b.name));
284                return [...sorted, ...sortedNoLocation];
285            } else {
286                // When no user location, alphabetize EVERYTHING together
287                const allLocations = [...campuses, ...extensionsWithLocation, ...extensionsWithoutLocation];
288                return allLocations.sort((a, b) => a.name.localeCompare(b.name));
289            }
290        },
291
292        userDistanceScenario() {
293            // Feature not enabled - don't show distance modal
294            if (!this.enableExtensions) {
295                return null;
296            }
297
298            // Don't calculate if no location data
299            if (!this.originAddress || this.sortedDistances.length === 0) {
300                return null;
301            }
302            
303            const TWENTY_MILES_IN_METERS = 32186.88;
304            
305            // Check campus distance
306            const closestCampusDistance = this.sortedDistances[0];
307            const farFromCampus = closestCampusDistance.distance.value > TWENTY_MILES_IN_METERS;
308            
309            if (!farFromCampus) {
310                return null; // User is close to a campus, no modal needed
311            }
312            
313            // User is far from campus, check extensions
314            const extensionsWithDistance = this.extensions.filter(ext => 
315                ext.distanceValue !== undefined && ext.distanceValue !== null
316            );
317            
318            if (extensionsWithDistance.length === 0) {
319                return 'far-from-both'; // No extensions or no distance data
320            }
321            
322            const closestExtension = extensionsWithDistance.reduce((closest, ext) => 
323                ext.distanceValue < closest.distanceValue ? ext : closest
324            );
325            
326            const farFromExtensions = closestExtension.distanceValue > TWENTY_MILES_IN_METERS;
327            
328            return farFromExtensions ? 'far-from-both' : 'far-from-campus';
329        },
330        
331        shouldShowDistanceModal() {
332            // Distance modal takes priority
333            return this.userDistanceScenario !== null;
334        },
335    },
336
337    watch: {
338        sortedDistances: {
339            handler(newDistances) {
340                if (newDistances.length > 0 && this.sortedCampusesForDisplay.length > 0) {
341                    this.processedCampuses.forEach(campus => {
342                        campus.highlighted = false;
343                    });
344
345                    const closestCampus = this.sortedCampusesForDisplay[0];
346                    
347                    // Only auto-select if NOT on mobile AND extensions are not being shown
348                    if (!this.isMobile && !this.pendingShowExtensions) {
349                        this.selectLocation(closestCampus);
350                    }
351
352                    // Check if user should see distance modal
353                    this.openUserDistanceModal();
354                    
355                    // NOW toggle extensions if it was requested
356                    if (this.pendingShowExtensions) {
357                        this.$nextTick(() => {
358                            this.showExtensions = true;
359                            this.pendingShowExtensions = false; // Clear the flag
360                        });
361                    }
362                }
363            },
364            deep: true
365        },
366
367        showExtensions(newValue) {
368            if (newValue && !this.extensionDistancesCalculated) {
369                this.calculateExtensionDistances();
370            }
371        },
372
373        shouldShowDistanceModal: {
374            handler(shouldShow) {
375                if (shouldShow && !this.isUserDistanceModalVisible) {
376                    // Distance modal takes priority - close language modal if open
377                    if (this.isLanguageServiceTimesModalVisible) {
378                        this.isLanguageServiceTimesModalVisible = false;
379                    }
380
381                    // Distance modal is NOT needed, check if we should show language modal
382                    this.openLanguageServiceTimesModal();
383                }
384            },
385            immediate: false
386        },
387    },
388
389    mounted() {
390        this.initializeComponent();
391
392        // Testing: open the language popup straight away rather than waiting on
393        // geolocation, which may never resolve if the browser blocks the prompt.
394        if (this.isLanguageModalTestMode()) {
395            this.openLanguageServiceTimesModal();
396        }
397
398        // Only allow extensions URL param if feature is enabled
399        if (this.enableExtensions) {
400            const urlParams = new URLSearchParams(window.location.search);
401            let showExtensionsParam = urlParams.get('showextensions') || urlParams.get('showExtensions');
402            
403            if (showExtensionsParam?.toLowerCase() === 'true') {
404                this.pendingShowExtensions = true;
405            }
406        }
407
408        setTimeout(() => {
409            if (this.isLoadingLocation) {
410                console.log('Location timeout - showing unsorted list');
411                this.isLoadingLocation = false;
412            }
413        }, 5000);
414    },
415
416    beforeUnmount() {
417        window.removeEventListener('resize', this.handleResize);
418    },
419
420    methods: {
421        initializeComponent() {
422            // Initialize Google Maps services
423            this.locationGeoCoder = new google.maps.Geocoder();
424            this.campusDistanceService = new google.maps.DistanceMatrixService();
425
426            // Process campuses for Vue
427            this.processCampusesForVue();
428
429            // Get extensions ONLY if feature is enabled
430            if (this.enableExtensions) {
431                this.getExtensionsList();
432            }
433
434            // Set up event listeners
435            this.setupEventListeners();
436
437            // Get user location after a delay
438            setTimeout(() => {
439                this.getUserLocation();
440            }, 1000);
441        },
442
443        setupEventListeners() {
444            window.addEventListener('resize', this.handleResize);
445        },
446
447        // Search handlers
448        handleSearch(searchTerm) {
449            this.performManualSearch(searchTerm);
450        },
451
452        handleGetCurrentLocation() {
453            this.getUserLocation();
454        },
455
456        handleToggleView(view) {
457            this.showingList = view === 'list';
458            
459            // When switching to map view on mobile
460            if (view === 'map' && this.isMobile) {
461                this.$nextTick(() => {
462                    // If user location is known, center on it
463                    if (this.originAddress && this.$refs.locationsMap && this.$refs.locationsMap._map) {
464                        this.$refs.locationsMap._map.setCenter(this.originAddress);
465                        // Removed auto-selection of closest campus
466                    }
467                });
468            }
469        },
470
471        // Location selection handlers
472        handleLocationSelected(location) {
473            this.selectLocation(location);
474        },
475
476        handleCloseInfoWindow() {
477            this.showInfoWindow = false;
478            this.selectedLocation = null;
479
480            // Reset highlighting
481            this.processedCampuses.forEach(campus => {
482                campus.highlighted = false;
483            });
484        },
485
486        selectLocation(location) {            
487            // Reset all highlighting
488            this.processedCampuses.forEach(campus => {
489                campus.highlighted = false;
490            });
491            this.extensions.forEach(extension => {
492                extension.highlighted = false;
493            });
494
495            // Set selected location
496            this.selectedLocation = location;
497            this.showInfoWindow = true;
498
499            // Only highlight selected location on tablet and desktop (NOT mobile)
500            if (!this.isMobile) {
501                if (location.type === 'extension') {
502                    const extension = this.extensions.find(e => e.id === location.id);
503                    if (extension) {
504                        extension.highlighted = true;
505                    }
506                } else {
507                    const campus = this.processedCampuses.find(c => c.id === location.id);
508                    if (campus) {
509                        campus.highlighted = true;
510                    }
511                }
512
513                // Scroll to card on tablet and desktop
514                this.$nextTick(() => {
515                    let cardSelector;
516                    if (location.type === 'extension') {
517                        cardSelector = `#extension_${location.id}`;
518                    } else {
519                        cardSelector = `#campus_${location.url}`;
520                    }
521
522                    const targetCard = document.querySelector(cardSelector);
523                    if (targetCard) {
524                        targetCard.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
525                    }
526                });
527            }
528        },
529
530        // Data processing methods
531        processCampusesForVue() {
532            this.processedCampuses = this.filteredCampuses.map(campus => {
533                const hasUrl = campus.url && campus.url.trim() !== '';
534                return {
535                    ...campus,
536                    extendedData: { address: this.buildCampusAddress(campus) },
537                    countryDisplay: this.buildCountryDisplay(campus),
538                    directionsLink: this.buildDirectionsLink(campus),
539                    campusLink: hasUrl ? this.buildCampusLink(campus.url) : '',
540                    hasCampusLink: hasUrl,
541                    distanceText: '',
542                    distanceValue: 0,
543                    highlighted: false
544                };
545            });
546        },
547
548        normalizeRockCampuses(rockCampuses) {
549            return (rockCampuses || []).map(rc => ({
550                id: rc.id,
551                name: rc.name,
552                address1: rc.address && rc.address.address1 ? rc.address.address1 : '',
553                address2: rc.address && rc.address.address2 ? rc.address.address2 : '',
554                city: rc.address && rc.address.city ? rc.address.city : '',
555                region: rc.address && rc.address.region ? rc.address.region : '',
556                postalCode: rc.address && rc.address.postalCode ? rc.address.postalCode : '',
557                country: rc.address && rc.address.country ? rc.address.country : '',
558                latitude: rc.latitude,
559                longitude: rc.longitude,
560                url: rc.url || '',
561                isActive: true,
562                churchEntityTypeId: 1,
563                languages: rc.languages || [],
564            }));
565        },
566
567        // Helper methods
568        buildCampusAddress(campus) {
569            let address = campus.address1;
570            if (campus.address2) {
571                address += `, ${campus.address2}`;
572            }
573            return `${address}, ${campus.city}, ${campus.region} ${campus.postalCode}, ${campus.country}`;
574        },
575
576        buildCountryDisplay(campus) {
577            const regionTrimmed = campus.region ? campus.region.trim() : '';
578            const fullRegion = convertRegionByCountry(regionTrimmed, campus.country) || regionTrimmed;
579            const countryDisplay = countryAbbreviations[campus.country] || c
579ampus.country;
580            
581            // If there's a region, show "Region, Country"
582            if (regionTrimmed) {
583            return `${fullRegion}, ${countryDisplay}`;
584            }
585
586            // If no region, just show country
587            return countryDisplay;
588        },
589
590        buildDirectionsLink(campus) {
591            const address = this.buildCampusAddress(campus);
592            return `https://maps.google.com/maps?f=d&hl=en&daddr=${encodeURIComponent(address)}&ie=UTF8&z=7`;
593        },
594
595        buildCampusLink(campusUrl) {
596            const baseDomain = window.location.origin;
597            const languageCode = this.getLanguageCode();
598
599            if (!languageCode) {
600                return `${baseDomain}/locations/campus-page/${campusUrl}-campus/`;
601            } else {
602                return `${baseDomain}/${languageCode}/locations/campus-page/${campusUrl}-campus/`;
603            }
604        },
605
606        getLanguageCode() {
607            const urlParts = window.location.href.split('/');
608            const languageCode = urlParts.length > 3 ? urlParts[3] : null;
609            
610            // Strip query parameters and hash
611            const cleanLanguageCode = languageCode ? languageCode.split('?')[0].split('#')[0] : null;
612            
613            return cleanLanguageCode === 'locations' ? null : cleanLanguageCode;
614        },
615
616        performManualSearch(searchTerm) {
617            const destinationCampuses = this.createDestinationsArray(false); // Don't include extensions
618            const originAddress = searchTerm === "90210" ? "90210, CA" : searchTerm;
619
620            if (originAddress && originAddress !== "") {
621                this.locationGeoCoder.geocode({ address: originAddress }, (results) => {
622                    this.searchInput = results[0].formatted_address;
623                    this.originAddress = results[0].formatted_address;
624                    this.originCountry = this.getCountryName(results[0]); // Manual searches aren't cached
625                    
626                    if (this.$refs.locationsMap && this.$refs.locationsMap._map) {
627                        this.$refs.locationsMap._map.setCenter(results[0].geometry.location);
628                    }
629
630                    this.getDistancesArray(this.originAddress, this.originCountry, destinationCampuses, false);
631                });
632            }
633        },
634
635        getUserLocation() {
636            if (navigator.geolocation) {
637                navigator.geolocation.getCurrentPosition(
638                    (position) => {
639                        const latitude = position.coords.latitude;
640                        const longitude = position.coords.longitude;
641                        
642                        this.userLocation = {
643                            latitude: latitude,
644                            longitude: longitude
645                        };
646                        
647                        this.originAddress = new google.maps.LatLng(latitude, longitude);
648
649                        if (this.$refs.locationsMap && this.$refs.locationsMap._map) {
650                            this.$refs.locationsMap._map.setCenter(this.originAddress);
651                        }
652
653                        // Reuse this session's cached results for this spot — skips the billed geocode + distance matrix calls
654                        const cached = this.readDistanceCache(latitude, longitude);
655                        if (cached) {
656                            this.searchInput = cached.formattedAddress;
657                            this.originCountry = cached.country;
658                            this.updateLocationDistances(cached.distances, cached.country);
659                            return;
660                        }
661
662                        this.locationGeoCoder.geocode({ location: this.originAddress }, (results, geoStatus) => {
663                            if (!results || !results[0]) {
664                                console.error('Reverse geocode failed:', geoStatus);
665                                this.isLoadingLocation = false;
666                                return;
667                            }
668                            this.searchInput = results[0].formatted_address;
669                            this.originCountry = this.getCountryName(results[0]);
670                            // Cache under this spot once the distance matrix results come back
671                            const cacheMeta = {
672                                key: this.distanceCacheKey(latitude, longitude),
673                                formattedAddress: results[0].formatted_address,
674                            };
675                            const destinationCampuses = this.createDestinationsArray(false); // Don't include extensions yet
676                            this.getDistancesArray(this.originAddress, this.originCountry, destinationCampuses, false, cacheMeta);
677                        });
678                    },
679                    (error) => {
680                        console.error('Geolocation error:', error);
681                        this.isLoadingLocation = false;
682                        this.openLanguageServiceTimesModal();
683                    }
684                );
685            } else {
686                alert("Your browser does not support using GeoLocation services.");
687                this.isLoadingLocation = false;
688                this.openLanguageServiceTimesModal();
689            }
690        },
691
692        calculateExtensionDistances() {
693            if (!this.originAddress || this.extensionDistancesCalculated) {
694                return; // No origin or already calculated
695            }
696
697            // Get only extensions for distance calculation
698            const extensionDestinations = [];
699            const extensionMapping = [];
700            
701            this.extensions.forEach(extension => {
702                if (!extension.isVirtual && extension.latitude && extension.longitude) {
703                    extensionDestinations.push(new google.maps.LatLng(extension.latitude, extension.longitude));
704                    extensionMapping.push(extension);
705                }
706            });
707
708            if (extensionDestinations.length === 0) {
709                // Only mark done when the loaded list truly has nothing to measure — the fetch may still be in flight
710                this.extensionDistancesCalculated = this.extensions.length > 0;
711                return;
712            }
713
714            const runBatches = (originCountry) => {
715                
716                // Split into batches of 20
717                const BATCH_SIZE = 20;
718                const batches = [];
719                
720                for (let i = 0; i < extensionDestinations.length; i += BATCH_SIZE) {
721                    batches.push(extensionDestinations.slice(i, i + BATCH_SIZE));
722                }
723                
724                let completedBatches = 0;
725                
726                batches.forEach((batch, batchIndex) => {
727                    this.campusDistanceService.getDistanceMatrix(
728                        {
729                            origins: [this.originAddress],
730                            destinations: batch,
731                            travelMode: google.maps.TravelMode.DRIVING,
732                            avoidHighways: false,
733                            avoidTolls: false,
734                            unitSystem: originCountry === "United States" ? google.maps.UnitSystem.IMPERIAL : google.maps.UnitSystem.METRIC
735                        },
736                        (response, status) => {
737                            if (response && response.rows && response.rows.length > 0) {
738                                const batchStartIndex = batchIndex * BATCH_SIZE;
739                                
740                                response.rows[0].elements.forEach((distanceData, elementIndex) => {
741                                    const globalIndex = batchStartIndex + elementIndex;
742                                    
743                                    if (globalIndex < extensionMapping.length) {
744                                        const extension = extensionMapping[globalIndex];
745                                        
746                                        if (typeof distanceData.distance === 'undefined') {
747                                            extension.distanceText = this.noRouteAvailableText;
748                                            extension.distanceValue = 100000;
749                                        } else {
750                                            extension.distanceText = distanceData.distance.text;
751                                            extension.distanceValue = distanceData.distance.value;
752                                        }
753                                        
754                                        delete extension.distance;
755                                    }
756                                });
757                            } else {
758                                console.error('Distance Matrix request failed (extensions):', status);
759                            }
760                            
761                            completedBatches++;
762                            
763                            if (completedBatches === batches.length) {
764                                this.extensionDistancesCalculated = true;
765                            }
766                        }
767                    );
768                });
769            };
770
771            // Reuse the country from the initial geocode when available — re-geocoding here just re-bills the API
772            if (this.originCountry) {
773                runBatches(this.originCountry);
774            } else {
775                this.locationGeoCoder.geocode({ address: this.searchInput }, (results) => runBatches(this.getCountryName(results[0])));
776            }
777        },
778
779        getDistancesArray(origin, originCountry, destinations, includeExtensions = true, cacheMeta = null) {            
780            // Create a mapping of destination index to location data
781            const locationMapping = [];
782            
783            // Build mapping in the same order as createDestinationsArray
784            this.processedCampuses.forEach(campusData => {
785                if (campusData.name !== "Online Community") {
786                    if (typeof campusData.address1 !== "undefined" && campusData.id !== 57) {
787                        locationMapping.push(campusData);
788                    } else if (typeof campusData.address1 !== "undefined" && campusData.id === 57) {
789                        locationMapping.push(campusData);
790                    } else {
791                        if (typeof campusData.latitude !== "undefined") {
792                            locationMapping.push(campusData);
793                        }
794                    }
795                } else {
796                    locationMapping.push(campusData);
797                }
798            });
799            
800            // Only add extensions if requested
801            if (includeExtensions) {
802                this.extensions.forEach(extension => {
803                    if (!extension.isVirtual && extension.latitude && extension.longitude) {
804                        locationMapping.push(extension);
805                    }
806                });
807            }
808            
809            // Split destinations into batches of 20
810            const BATCH_SIZE = 20;
811            const batches = [];
812            
813            for (let i = 0; i < destinations.length; i += BATCH_SIZE) {
814                batches.push(destinations.slice(i, i + BATCH_SIZE));
815            }
816            
817            let allDistances = [];
818            let completedBatches = 0;
819            
820            batches.forEach((batch, batchIndex) => {                
821                this.campusDistanceService.getDistanceMatrix(
822                    {
823                        origins: [origin],
824                        destinations: batch,
825                        travelMode: google.maps.TravelMode.DRIVING,
826                        avoidHighways: false,
827                        avoidTolls: false,
828                        unitSystem: originCountry === "United States" ? google.maps.UnitSystem.IMPERIAL : google.maps.UnitSystem.METRIC
829                    },
830                    (response, status) => {                        
831                        if (response && response.rows && response.rows.length > 0) {
832                            const batchStartIndex = batchIndex * BATCH_SIZE;
833                            
834                            response.rows[0].elements.forEach((distanceData, elementIndex) => {
835                                const globalIndex = batchStartIndex + elementIndex;
836                                
837                                if (globalIndex < locationMapping.length) {
838                                    const locationData = locationMapping[globalIndex];
839                                    
840                                    const distanceInfo = {
841                                        locationId: locationData.id,
842                                        locationName: locationData.name,
843                                        locationType: locationData.type || 'campus',
844                                        latitude: locationData.latitude,
845                                        longitude: locationData.longitude
846                                    };
847                                    
848                                    if (typeof distanceData.distance === 'undefined') {
849                                        distanceInfo.distance = {
850                                            text: (originCountry === 'United States' ? '100000 mi' : '100000 km'),
851                                            value: 100000
852                                        };
853                                    } else {
854                                        distanceInfo.distance = distanceData.distance;
855                                    }
856                                    
857                                    allDistances.push(distanceInfo);
858                                }
859                            });
860                        } else {
861                            console.error('Distance Matrix request failed:', status);
862                        }
863                        
864                        completedBatches++;
865                        
866                        // When all batches are complete, process the results
867                        if (completedBatches === batches.length) {
868                            // Sort distances array
869                            allDistances.sort((a, b) => {
870                                const aValue = parseFloat(a.distance.text.replace(/\s(mi|ft|km|m)/g, "").replace(",", ""));
871                                const bValue = parseFloat(b.distance.text.replace(/\s(mi|ft|km|m)/g, "").replace(",", ""));
872                                return aValue < bValue ? -1 : 1;
873                            });
874                            
875                            // Update both campuses and extensions with distance info
876                            this.updateLocationDistances(allDistances, originCountry, cacheMeta);
877                        }
878                    }
879                );
880            });
881        },
882
883        updateLocationDistances(distances, originCountry, cacheMeta = null) {            
884            distances.forEach(distanceInfo => {                
885                if (distanceInfo.locationType === 'campus') {
886                    const campus = this.processedCampuses.find(c => c.id === distanceInfo.locationId);
887                    
888                    if (campus) {
889                        campus.distanceText = distanceInfo.distance.text === '100000 mi' || distanceInfo.distance.text === '100000 km' ?
890                            this.noRouteAvailableText : distanceInfo.distance.text;
891                        campus.distanceValue = distanceInfo.distance.value;
892                        
893                        // Remove any stray 'distance' property to avoid confusion
894                        delete campus.distance;
895                        
896                        // Update directions link with origin
897                        if (campus.id === 25) { // Santa Rosa Campus
898                            campus.directionsLink = `https://maps.google.com/maps?f=d&hl=en&saddr=${this.originAddress}&daddr=${campus.latitude},${campus.longitude}&ie=UTF8&z=7`;
899                        } else {
900                            const cleanAddress = campus.extendedData.address.replace(/null/g, '');
901                            campus.directionsLink = `https://maps.google.com/maps?f=d&hl=en&saddr=${this.originAddress}&daddr=${encodeURIComponent(cleanAddress)}&ie=UTF8&z=7`;
902                        }
903                    }
904                } else if (distanceInfo.locationType === 'extension') {
905                    const extension = this.extensions.find(e => e.id === distanceInfo.locationId);
906                    
907                    if (extension) {
908                        extension.distanceText = distanceInfo.distance.text === '100000 mi' || distanceInfo.distance.text === '100000 km' ?
909                            this.noRouteAvailableText : distanceInfo.distance.text;
910                        extension.distanceValue = distanceInfo.distance.value;
911                        
912                        // Remove any stray 'distance' property to avoid confusion
913                        delete extension.distance;
914                    } else {
915                        console.error('Extension not found!');
916                    }
917                }
918            });
919
920            // Update sortedDistances for campus sorting (only for campuses)
921            const campusDistances = distances.filter(d => d.locationType === 'campus').map(d => ({
922                campusId: d.locationId,
923                campusName: d.locationName,
924                titleUrl: '', // You might need to get this from the campus data
925                distance: d.distance,
926                latitude: d.latitude,
927                longitude: d.longitude
928            }));
929            
930            this.sortedDistances = campusDistances;
931
932            // Cache geolocation results for this session so repeat page views don't re-bill the Maps APIs
933            if (cacheMeta) {
934                this.writeDistanceCache(cacheMeta, distances, originCountry);
935            }
936            
937            this.$nextTick(() => {
938                if (!this.shouldShowDistanceModal) {
939                    this.openLanguageServiceTimesModal();
940                }
941
942                this.isLoadingLocation = false;
943            });
944        },
945
946        getCountryName(geoAddress) {
947            let countryName;
948            geoAddress.address_components.forEach(component => {
949                if (component.types[0] === "country") {
950                    countryName = component.long_name;
951                }
952            });
953            return countryName;
954        },
955
956        distanceCacheKey(latitude, longitude) {
957            // ~1km grid so normal geolocation jitter between page views still hits the cache
958            return `sbCampusDistances:v1:${latitude.toFixed(2)},${longitude.toFixed(2)}`;
959        },
960
961        readDistanceCache(latitude, longitude) {
962            try {
963                const cached = JSON.parse(sessionStorage.getItem(this.distanceCacheKey(latitude, longitude)));
964                // Ignore the entry if it's empty or the published campus list changed since it was written
965                const valid = cached && cached.distances && cached.distances.length > 0 &&
966                    cached.campusIds === this.processedCampuses.map(c => c.id).join(',');
967                return valid ? cached : null;
968            } catch {
969                return null;
970            }
971        },
972
973        writeDistanceCache(cacheMeta, distances, country) {
974            if (!distances || distances.length === 0) {
975                return; // Never cache a failed/empty result
976            }
977            try {
978                sessionStorage.setItem(cacheMeta.key, JSON.stringify({
979                    formattedAddress: cacheMeta.formattedAddress,
980                    country: country,
981                    campusIds: this.processedCampuses.map(c => c.id).join(','),
982                    distances: distances
983                }));
984            } catch {
985                // Storage blocked or full — skip caching, the live flow still works
986            }
987        },
988
989        createDestinationsArray(includeExtensions = true) {
990            const destinations = [];
991
992            // Add campuses
993            this.processedCampuses.forEach(campusData => {
994                if (campusData.name !== "Online Community") {
995                    if (typeof campusData.address1 !== "undefined" && campusData.id !== 57) {
996                        destinations.push(`${campusData.address1}, ${campusData.address2}, ${campusData.city}, ${campusData.region} ${campusData.postalCode}`);
997                    } else if (typeof campusData.address1 !== "undefined" && campusData.id === 57) {
998                        destinations.push(new google.maps.LatLng(campusData.latitude, campusData.longitude));
999                    } else {
1000                        if (typeof campusData.latitude !== "undefined") {
1001                            destinations.push(new google.maps.LatLng(campusData.latitude, campusData.longitude));
1002                        }
1003                    }
1004                } else {
1005                    destinations.push('1 Saddleback Pkwy');
1006                }
1007            });
1008
1009            // Only add extensions if requested
1010            if (includeExtensions) {
1011                this.extensions.forEach(extension => {
1012                    if (!extension.isVirtual && extension.latitude && extension.longitude) {
1013                        destinations.push(new google.maps.LatLng(extension.latitude, extension.longitude));
1014                    }
1015                });
1016            }
1017
1018            return destinations;
1019        },
1020
1021        getExtensionsList() {
1022            var pageSize = 500;
1023            var url = `${this.sbTechApi}/api/v4/extensions?pagesize=${pageSize}`;
1024
1025            fetch(url)
1026                .then(response => response.json())
1027                .then(data => {
1028                    const rawExtensions = data.results.map(extension => {
1029                        const location = extension.locations && extension.locations.length > 0 
1030                            ? extension.locations[0] 
1031                            : null;
1032
1033                        const languageText = Array.isArray(extension.languages)
1034                            ? extension.languages.map(l => l.text).filter(Boolean).join(', ')
1035                            : '';
1036
1037                        return {
1038                            id: extension.id,
1039                            name: extension.name || '',
1040                            customHTML: extension.customHTML || '',
1041                            leaderName: extension.contactName || '',
1042                            leaderEmail: extension.leaderEmail || '',
1043                            latitude: location ? parseFloat(location.latitude) || null : null,
1044                            longitude: location ? parseFloat(location.longitude) || null : null,
1045                            city: extension.city || '',
1046                            region: extension.region || '',
1047                            country: extension.country || '',
1048                            language: languageText || null,
1049                            schedule: extension.schedule || null,
1050                            timeZone: extension.timeZone,
1051                            isActive: extension.isActive,
1052                            type: 'extension',
1053                            distanceText: '',
1054                            distanceValue: 0,
1055                            highlighted: false,
1056                            serviceType: extension.serviceType,
1057                            serviceTypeId: extension.serviceTypeId,
1058                            mapZoomLevel: extension.mapZoomLevel || null,
1059                            isVirtual: extension.serviceType === 'Virtual',
1060                        };
1061                    }).filter(ext => ext.isActive);
1062
1063                    this.extensions = rawExtensions;
1064
1065                    // A cache hit can resolve distances before this fetch returns — catch up here
1066                    if (this.showExtensions && !this.extensionDistancesCalculated) {
1067                        this.calculateExtensionDistances();
1068                    }
1069                })
1070                .catch(error => {
1071                    console.error('Error fetching extensions list:', error);
1072                });
1073        },
1074
1075        handleResize() {
1076            const oldWidth = this.windowWidth;
1077            this.windowWidth = window.innerWidth;
1078
1079            // Only reset when crossing mobile/tablet→desktop breakpoint
1080            const wasDesktop = oldWidth >= this.BREAKPOINTS.DESKTOP;
1081            const isNowDesktop = this.windowWidth >= this.BREAKPOINTS.DESKTOP;
1082
1083            if (!wasDesktop && isNowDesktop) {
1084                // Just switched to desktop - reset state
1085                this.showingList = true;
1086                this.resetForDesktop();
1087            }
1088        },
1089
1090        resetForDesktop() {
1091            // Auto-select first campus if location is known
1092            if (this.originAddress && this.sortedCampusesForDisplay.length > 0) {
1093                this.$nextTick(() => {
1094                    this.selectLocation(this.sortedCampusesForDisplay[0]);
1095                });
1096            }
1097        },
1098
1099        showExtensionDetailsPopup(show) {
1100            this.isExtensionDetailsPopupVisible = show;
1101        },
1102
1103        // Handle "View Extensions" button click from distance modal
1104        // Clears campus highlights and toggles to show extensions view
1105        handleViewExtensions() {
1106            // Set the pending flag - the sortedDistances watcher will handle the rest
1107            this.pendingShowExtensions = true;
1108            
1109            // Close the modal
1110            this.closeUserDistanceModal();
1111            
1112            // If distances are already calculated, trigger the toggle immediately
1113            if (this.sortedDistances.length > 0) {
1114                // Clear all highlights
1115                this.processedCampuses.forEach(campus => {
1116                    campus.highlighted = false;
1117                });
1118                this.extensions.forEach(extension => {
1119                    extension.highlighted = false;
1120                });
1121                
1122                // Close info window
1123                this.showInfoWindow = false;
1124                this.selectedLocation = null;
1125                
1126                // Toggle extensions
1127                this.$nextTick(() => {
1128                    this.showExtensions = true;
1129                    this.pendingShowExtensions = false;
1130                });
1131            }
1132        },
1133
1134        handleExtensionDetails(extension) {
1135            // Store the selected extension data for the popup
1136            this.selectedExtension = extension;
1137            
1138            // Show the popup
1139            this.showExtensionDetailsPopup(true);
1140        },
1141
1142        detectCurrentLanguage() {
1143            // The layout stamps the active culture on <html lang>, so a new language
1144            // needs no change here - it just works from the content it is given.
1145            const htmlLang = document.documentElement.lang;
1146            if (htmlLang && htmlLang.trim() !== '') {
1147                return htmlLang.trim().toLowerCase();
1148            }
1149
1150            // Fallback for any layout that does not set lang
1151            const pathParts = window.location.href.toLowerCase().split('/');
1152            if (pathParts.includes('es')) return 'es';
1153            if (pathParts.includes('zh-hans')) return 'zh-hans';
1154            if (pathParts.includes('zh-hant')) return 'zh-hant';
1155
1156            return 'en';
1157        },
1158
1159        showLanguageServiceTimesModal(show) {
1160            this.isLanguageServiceTimesModalVisible = show;
1161        },
1162
1163        // Testing helper: ?showlanguagemodal=true re-opens the popup on every load,
1164        // ignoring (and not setting) the languageModalSeen cookie.
1165        isLanguageModalTestMode() {
1166            const urlParams = new URLSearchParams(window.location.search);
1167            const param = urlParams.get('showlanguagemodal') || urlParams.get('showLanguageModal');
1168
1169            return param?.toLowerCase() === 'true';
1170        },
1171
1172        openLanguageServiceTimesModal() {
1173            const testMode = this.isLanguageModalTestMode();
1174
1175            // Check if user has already seen the modal
1176            if (!testMode && this.getCookie('languageModalSeen')) {
1177                return; // Don't show if cookie exists
1178            }
1179
1180            this.currentLanguage = this.detectCurrentLanguage();
1181
1182            // Never show the modal when the distance modal has priority
1183            if (this.userDistanceScenario) {
1184                return;
1185            }
1186
1187            // Purely content driven: the block list varies by culture, so an entry
1188            // existing here means an editor set one up for the language being viewed
1189            if (this.languageServiceTimes.length === 0) {
1190                return;
1191            }
1192
1193            this.showLanguageServiceTimesModal(true);
1194
1195            // Set cookie when modal is shown (skipped while testing)
1196            if (!testMode) {
1197                this.setCookie('languageModalSeen', 'true', 90);
1198            }
1199        },
1200
1201        closeLanguageServiceTimesModal() {
1202            this.isLanguageServiceTimesModalVisible = false;
1203        },
1204
1205        showUserDistanceModal(show) {
1206            this.isUserDistanceModalVisible = show;
1207        },
1208
1209        openUserDistanceModal() {
1210            // Check if user has already seen the modal
1211            if (this.getCookie('distanceModalSeen')) {
1212                return; // Don't show if cookie exists
1213            }
1214
1215            if (this.userDistanceScenario) {
1216                this.isUserDistanceModalVisible = true;
1217                // Set cookie when modal is shown
1218                this.setCookie('distanceModalSeen', 'true', 90);
1219            }
1220        },
1221        
1222        closeUserDistanceModal() {
1223            this.isUserDistanceModalVisible = false;
1224        },
1225
1226        setCookie(name, value, days) {
1227            const expires = new Date();
1228            expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
1229            document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax;Secure`;
1230        },
1231
1232        getCookie(name) {
1233            const nameEQ = name + "=";
1234            const ca = document.cookie.split(';');
1235            for (let i = 0; i < ca.length; i++) {
1236                let c = ca[i];
1237                while (c.charAt(0) === ' ') c = c.substring(1, c.length);
1238                if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
1239            }
1240            return null;
1241        },
1242    },
1243
1244    template: `
1245        <div class="locations-page-inner">
1246            <h1 class="header-two">{{ locationsPageHeader }}</h1>
1247
1248            <!-- Search container in original position on mobile/tablet -->
1249            <div id="locationsPageSearchContainer" v-if="!isDesktop">
1250                <LocationsSearch
1251                    v-model="searchInput"
1252                    :placeholder="enterYourLocationText"
1253                    :is-mobile="isMobile"
1254                    :is-desktop="isDesktop"
1255                    :showing-list="showingList"
1256                    :map-text="mapText"
1257                    :list-text="listText"
1258                    @search="handleSearch"
1259                    @get-current-location="handleGetCurrentLocation"
1260                    @toggle-view="handleToggleView"
1261                    ref="mobileSearch"
1262                />
1263
1264                <Toggle
1265                    v-show="isMobile && enableExtensions"
1266                    :checked="showExtensions"
1267                    :show-text="showText"
1268                    :hide-text="hideText"
1269                    @input="showExtensions = $event"
1270                />
1271            </div>
1272
1273            <div class="desktop-flex">
1274                <!-- Map Component -->
1275                <LocationsMap
1276                    ref="locationsMap"
1277                    :locations="allLocations"
1278                    :show-extensions="showExtensions"
1279                    :enable-extensions="enableExtensions"
1280                    @toggle-extensions="showExtensions = $event"
1281                    :selected-location="selectedLocation"
1282                    :is-mobile="isMobile"
1283                    :is-tablet="isTablet"
1284                    :is-desktop="isDesktop"
1285                    :showing-list="showingList"
1286                    :show-info-window="showInfoWindow"
1287                    :directions-text="directionsText"
1288                    :view-campus-text="viewCampusText"
1289                    :extension-text="extensionText"
1290                    :view-details-text="viewDetailsText"
1291                    :no-route-available-text="noRouteAvailableText"
1292                    :default-marker-icon-campus="defaultMarkerIconCampus"
1293                    :active-marker-icon-campus="activeMarkerIconCampus"
1294                    :default-marker-icon-extension="defaultMarkerIconExtension"
1295                    :active-marker-icon-extension="activeMarkerIconExtension"
1296                    :user-location-icon="userLocationIcon"
1297                    @location-selected="handleLocationSelected"
1298                    @close-info-window="handleCloseInfoWindow"
1299                    @extension-details="handleExtensionDetails"
1300                />
1301
1302                <div class="locations-page-list-container">
1303                    <!-- Desktop search input for locations page -->
1304                    <LocationsSearch
1305                        v-if="isDesktop"
1306                        v-model="searchInput"
1307                        :placeholder="enterYourLocationText"
1308                        :is-mobile="isMobile"
1309                        :is-desktop="isDesktop"
1310                        :showing-list="showingList"
1311                        @search="handleSearch"
1312                        @get-current-location="handleGetCurrentLocation"
1313                        @toggle-view="handleToggleView"
1314                        ref="desktopSearch"
1315                    />
1316                    <!-- End of desktop search input -->
1317
1318                    <!-- Loading State: region loads show the dots+logo widget -->
1319                    <div v-if="isLoadingLocation" class="loading-results">
1320                        <loading-spinner></loading-spinner>
1321                    </div>
1322
1323                    <!-- Locations List -->
1324                    <LocationsList
1325                        v-if="!isLoadingLocation"
1326                        :locations="allSortedLocationsForDisplay"
1327                        :show-extensions="showExtensions"
1328                        :is-mobile="isMobile"
1329                        :is-tablet="isTablet"
1330                        :is-desktop="isDesktop"
1331                        :showing-list="showingList"
1332                        :directions-text="directionsText"
1333                        :view-campus-text="viewCampusText"
1334                        :extension-text="extensionText"
1335                        :view-details-text="viewDetailsText"
1336                        :no-route-available-text="noRouteAvailableText"
1337                        @location-selected="handleLocationSelected"
1338                        @extension-details="handleExtensionDetails"
1339                        ref="locationsList"
1340                    />
1341                </div>
1342
1343                <div class="bottom-border"></div>
1344            </div>
1345
1346            <!-- Bottom buttons section -->
1347            <div class="locations-page-buttons">
1348                <div class="button-container mobile">
1349                    <a :href="onlineCommunityLink" class="locations-btn">
1350                        <img :src="onlineCommunityIcon" alt="Online Community Icon" />
1351                        <div>
1352                            <span class="top-subtext">{{ onlineCommunityHeader }}</span>
1353                            <span class="btn-subtext">{{ onlineCommunityText }}</span>
1354                        </div>
1355                    </a>
1356
1357                    <a :href="extensionsLink" class="locations-btn" v-show="!enableExtensions">
1358                        <img :src="extensionsIcon" alt="Extensions Icon" />
1359                        <div>
1360                            <span class="top-subtext">{{ extensionsHeader }}</span>
1361                            <span class="btn-subtext">{{ extensionsText }}</span>
1362                        </div>
1363                    </a>
1364
1365                    <a :href="planAVisitLink" class="locations-btn">
1366                        <img :src="planAVisitIcon" alt="Plan a Visit Icon" />
1367                        <div>
1368                            <span class="top-subtext">{{ planAVisitHeader }}</span>
1369                            <span class="btn-subtext">{{ planAVisitText }}</span>
1370                        </div>
1371                    </a>
1372                </div>
1373
1374                <div class="button-container tablet" v-show="!enableExtensions">
1375                    <div class="locations-btn">
1376                        <a :href="onlineCommunityLink">
1377                            <img :src="onlineCommunityIcon" alt="Online Community Icon" />
1378                            <span class="top-subtext">{{ onlineCommunityHeader }}</span>
1379                        </a>
1380                        <span class="btn-subtext">{{ onlineCommunityText }}</span>
1381                    </div>
1382
1383                    <div class="locations-btn">
1384                        <a :href="extensionsLink">
1385                            <img :src="extensionsIcon" alt="Extensions Icon" />
1386                            <span class="top-subtext">{{ extensionsHeader }}</span>
1387                        </a>
1388                        <span class="btn-subtext">{{ extensionsText }}</span>
1389                    </div>
1390
1391                    <div class="locations-btn">
1392                        <a :href="planAVisitLink">
1393                            <img :src="planAVisitIcon" alt="Plan a Visit Icon" />
1394                            <span class="top-subtext">{{ planAVisitHeader }}</span>
1395                        </a>
1396                        <span class="btn-subtext">{{ planAVisitText }}</span>
1397                    </div>
1398                </div>
1399
1400                <div class="button-container desktop" v-show="!enableExtensions">
1401                    <a :href="onlineCommunityLink" class="locations-btn">
1402                        <img :src="onlineCommunityIcon" alt="Online Community Icon" />
1403                        <div>
1404                            <span class="top-subtext">{{ onlineCommunityHeader }}</span>
1405                            <span class="btn-subtext">{{ onlineCommunityText }}</span>
1406                        </div>
1407                    </a>
1408
1409                    <a :href="extensionsLink" class="locations-btn">
1410                        <img :src="extensionsIcon" alt="Extensions Icon" />
1411                        <div>
1412                            <span class="top-subtext">{{ extensionsHeader }}</span>
1413                            <span class="btn-subtext">{{ extensionsText }}</span>
1414                        </div>
1415                    </a>
1416
1417                    <a :href="planAVisitLink" class="locations-btn">
1418                        <img :src="planAVisitIcon" alt="Plan a Visit Icon" />
1419                        <div>
1420                            <span class="top-subtext">{{ planAVisitHeader }}</span>
1421                            <span class="btn-subtext">{{ planAVisitText }}</span>
1422                        </div>
1423                    </a>
1424                </div>
1425
1426                <!-- Extensions Enabled Buttons for tablet and desktop -->
1427                <div class="button-container-enable-extensions" v-show="enableExtensions">
1428                    <a :href="onlineCommunityLink" class="locations-btn">
1429                        <img :src="onlineCommunityIcon" alt="Online Community Icon" />
1430                        <div class="btn-text">
1431                            <span class="top-subtext">{{ onlineCommunityHeader }}</span>
1432                            <span class="btn-subtext">{{ onlineCommunityText }}</span>
1433                        </div>
1434                    </a>
1435
1436                    <a :href="planAVisitLink" class="locations-btn">
1437                        <img :src="planAVisitIcon" alt="Plan a Visit Icon" />
1438                        <div class="btn-text">
1439                            <span class="top-subtext">{{ planAVisitHeader }}</span>
1440                            <span class="btn-subtext">{{ planAVisitText }}</span>
1441                        </div>
1442                    </a>
1443                </div>
1444            </div>
1445            
1446            <ExtensionDetailsPopup
1447                :visible="isExtensionDetailsPopupVisible"
1448                :extension="selectedExtension"
1449                :extension-details-text="extensionDetailsText"
1450                :service-type-text="serviceTypeText"
1451                :connect-with-leader-text="connectWithLeaderText"
1452                :contact-leader-for-info-text="contactLeaderForInfoText"
1453                :contact-extension-leader-text="contactExtensionLeaderText"
1454                :daily-text="dailyText"
1455                :weekly-text="weeklyText"
1456                :bi-weekly-text="biWeeklyText"
1457                :monthly-text="monthlyText"
1458                :first-text="firstText"
1459                :second-text="secondText"
1460                :third-text="thirdText"
1461                :fourth-text="fourthText"
1462                :last-text="lastText"
1463                v-on:close="showExtensionDetailsPopup(false)"
1464            />
1465
1466            <LanguageServiceTimesModal
1467                :visible="isLanguageServiceTimesModalVisible"
1468                :is-desktop="isDesktop"
1469                :user-location="userLocation"
1470                :service-times-blocks="languageServiceTimes"
1471                :intro-text="languagePopupIntro"
1472                :intro-text-english="languagePopupIntroEnglish"
1473                :day-names="dayNamesText"
1474                :english-day-names="englishDayNamesText"
1475                :switch-to-english-text="switchToEnglishText"
1476                :language-word-text="languageWordText"
1477                :switch-to-native-text="switchToNativeText"
1478                :language-word-native-text="languageWordNativeText"
1479                :icon-text="languagePopupIconText"
1480                :am-text="amText"
1481                :pm-text="pmText"
1482                v-on:close="closeLanguageServiceTimesModal"
1483            />
1484
1485            <UserDistanceModal
1486                :visible="isUserDistanceModalVisible"
1487                :scenario="userDistanceScenario"
1488                :watch-online-text="watchOnlineText"
1489                :learn-more-text="learnMoreText"
1490                :view-extensions-text="viewExtensionsText"
1491                :no-locations-header-text="noLocationsHeaderText"
1492                :no-campus-header-text="noCampusHeaderText"
1493                :no-locations-body-text="noLocationsBodyText"
1494                :no-campus-body-text="noCampusBodyText"
1495                v-on:close="closeUserDistanceModal"
1496                @view-extensions="handleViewExtensions"
1497            />
1498        </div>
1499    `
1500};

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.