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.