1/** 2 * Favorites â cookie-backed favorites for SSR pages. 3 * 4 * Mirrors the legacy anonymous-favorites behavior (the old site tracked favorites 5 * in the ASP.NET Session regardless of login). Here we use a first-party cookie so 6 * the /favorites page can render the list server-side under static SSR, for both 7 * anonymous and logged-in users. No framework dependencies; vanilla JS only. 8 * 9 * Cookie: hpm_favorites = URL-encoded JSON array of 10 * { planNumber, planType, planName, imageUrl }. 11 */ 12(function () { 13 'use strict'; 14 15 var COOKIE_NAME = 'hpm_favorites'; 16 var MAX_FAVORITES = 100; 17 var MAX_AGE_DAYS = 365; 18 19 /** Read the raw cookie value by name, or '' if absent. */ 20 function readCookie(name) { 21 var match = document.cookie.match('(?:^|; )' + name.replace(/([.$?*|{}()\[\]\\\/+^])/g, '\\$1') + '=([^;]*)'); 22 return match ? decodeURIComponent(match[1]) : ''; 23 } 24 25 /** Write a cookie (path=/, SameSite=Lax so it rides along with navigations). */ 26 function writeCookie(name, value) { 27 var expires = new Date(Date.now() + MAX_AGE_DAYS * 864e5).toUTCString(); 28 document.cookie = name + '=' + encodeURIComponent(value) + 29 '; path=/; expires=' + expires + '; SameSite=Lax'; 30 } 31 32 /** Current favorites list. Returns [] on error. */ 33 function getFavorites() { 34 try { 35 var raw = readCookie(COOKIE_NAME); 36 return raw ? JSON.parse(raw) : []; 37 } catch (_) { return []; } 38 } 39 40 /** Persist the favorites list to the cookie. */ 41 function saveFavorites(list) { 42 writeCookie(COOKIE_NAME, JSON.stringify(list)); 43 } 44 45 /** True when the plan number is currently a favorite. */ 46 function isFavorite(planNumber) { 47 return getFavorites().some(function (p) { return p.planNumber === planNumber; }); 48 } 49 50 /** Add a favorite. Returns false if already present or at the limit. */ 51 function addFavorite(planNumber, planName, imageUrl, planType) { 52 var list = getFavorites();
53 if (list.some(function (p) { return p.planNumber === planNumber; })) return false; 54 if (list.length >= MAX_FAVORITES) { 55 alert('You can save up to ' + MAX_FAVORITES + ' favorites. Remove one first.'); 56 return false; 57 } 58 list.push({ 59 planNumber: planNumber, 60 planName: planName || '', 61 imageUrl: imageUrl || '', 62 planType: planType || 'House' 63 }); 64 saveFavorites(list); 65 return true; 66 } 67 68 /** Remove a favorite by plan number. */ 69 function removeFavorite(planNumber) { 70 saveFavorites(getFavorites().filter(function (p) { return p.planNumber !== planNumber; })); 71 } 72 73 /** Reflect favorite state on a single toggle button. */ 74 function paintButton(btn, fav) { 75 var icon = btn.querySelector('i'); 76 if (fav) { 77 btn.classList.add('active'); 78 btn.setAttribute('aria-pressed', 'true'); 79 btn.setAttribute('title', 'Remove from favorites'); 80 if (icon) { icon.classList.remove('fa-regular'); icon.classList.add('fa-solid'); } 81 } else { 82 btn.classList.remove('active'); 83 btn.setAttribute('aria-pressed', 'false'); 84 btn.setAttribute('title', 'Save to favorites'); 85 if (icon) { icon.classList.remove('fa-solid'); icon.classList.add('fa-regular'); } 86 } 87 } 88 89 /** Paint every favorite toggle button on the page from the current cookie. */ 90 function updateButtons() { 91 var favs = {};
92 getFavorites().forEach(function (p) { favs[p.planNumber] = true; }); 93 var buttons = document.querySelectorAll('[data-fav-plan]'); 94 for (var i = 0; i < buttons.length; i++) { 95 paintButton(buttons[i], !!favs[buttons[i].getAttribute('data-fav-plan')]); 96 } 97 } 98 99 /** Toggle a plan's favorite state from its button and repaint. */ 100 function onToggleClick(btn) { 101 var planNumber = btn.getAttribute('data-fav-plan'); 102 if (!planNumber) return; 103 if (isFavorite(planNumber)) { 104 removeFavorite(planNumber); 105 } else { 106 addFavorite( 107 planNumber, 108 btn.getAttribute('data-fav-name') || '', 109 btn.getAttribute('data-fav-img') || '', 110 btn.getAttribute('data-fav-type') || 'House' 111 ); 112 } 113 updateButtons(); 114 } 115 116 /** Remove a favorite from the /favorites page, then reload to re-render server-side. */ 117 function removeFromFavoritesPage(planNumber) { 118 removeFavorite(planNumber); 119 window.location.reload(); 120 } 121 122 function init() { 123 updateButtons(); 124 125 document.addEventListener('click', function (e) { 126 var btn = e.target.closest('[data-fav-plan]'); 127 if (btn) { 128 e.preventDefault(); 129 onToggleClick(btn); 130 } 131 }); 132 } 133 134 if (document.readyState === 'loading') { 135 document.addEventListener('DOMContentLoaded', init); 136 } else { 137 init(); 138 } 139 140 // Repaint after bfcache restore and Blazor enhanced navigation (DOM replaced 141 // without firing DOMContentLoaded). 142 window.addEventListener('pageshow', function (e) { if (e.persisted) updateButtons(); }); 143 if (typeof Blazor !== 'undefined' && Blazor.addEventListener) { 144 Blazor.addEventListener('enhancedload', updateButtons); 145 } 146 147 window.Favorites = { 148 add: addFavorite, 149 remove: removeFavorite, 150 toggle: onToggleClick, 151 removeFromPage: removeFromFavoritesPage, 152 getFavorites: getFavorites, 153 isFavorite: isFavorite 154 }; 155})();
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.