1/** 2 * NAA Web - About Page Module 3 * Handles team display, profile modals, and fullscreen functionality 4 * 5 * Modernized from legacy about.aspx inline scripts 6 */ 7 8import { getTeams } from '../core/api.js'; 9import { init as initTerritoryMap } from '../components/territory-map.js'; 10 11let teamData = []; 12let currentProfile = null; 13 14/** Rewrite pdfs.newagealpha.com image URLs to proxy through /api/img/ */ 15function proxyImageUrl(url) { 16 if (!url) return url; 17 const prefix = 'https://pdfs.newagealpha.com/'; 18 if (url.startsWith(prefix)) { 19 return '/api/img/' + url.substring(prefix.length); 20 } 21 return url; 22} 23 24/** 25 * Initialize the about page 26 */ 27export async function init() { 28 try { 29 30 await loadTeamData(); 31 32 setupFullscreen(); 33 34 setupScrollBehavior(); 35 36 handleHashNavigation(); 37 38 } catch (error) { 39 console.error('Error initializing about page:', error); 40 } finally { 41 // Isolated: the territory map enhances server-rendered markup, so a 42 // failure there must not take the rest of the page with it â and the 43 // team grid failing must not stop the map from drawing. 44 try { 45 initTerritoryMap(); 46 } catch (error) { 47 console.error('Error initializing territory map:', error); 48 } 49 50 if (window.NAALoader) NAALoader.release(); 51 } 52} 53 54/** 55 * Load team data from the API and render the team grid 56 */ 57async function loadTeamData() { 58 try { 59 teamData = await getTeams(); 60 61 if (teamData && teamData.length > 0) { 62 renderTeamGrid(teamData); 63 setupTeamInteractions(); 64 } 65 } catch (error) { 66 console.error('Error loading team data:', error); 67 68 } 69} 70 71/** 72 * Render the team members grid 73 * @param {Array} members - Array of team member objects 74 */ 75function renderTeamGrid(members) { 76 const container = document.getElementById('teamContent'); 77 if (!container) return; 78 79 container.innerHTML = ''; 80 81 members.forEach(function(member) { 82 const memberEl = createTeamMemberElement(member); 83 container.appendChild(memberEl); 84 }); 85} 86 87/** 88 * Create a team member element 89 * @param {Object} member - Team member data 90 * @returns {HTMLElement} Team member DOM element 91 */ 92function createTeamMemberElement(member) { 93 const wrapper = document.createElement('div'); 94 wrapper.className = 'dgrid'; 95 96 const hasDescription = member.description && member.description.trim().length > 0; 97 const hasLinkedIn = member.linkedin && member.linkedin.trim().length > 0; 98 const hasTwitter = member.twitter && member.twitter.trim().length > 0; 99 100 const profileImage = proxyImageUrl(member.profile_Image || member.profile_image || member.profileImage) || '/assets/images/placeholder.png'; 101 102 wrapper.innerHTML = ` 103 <div class="team-members teamMem ${escapeHtml(member.name)}" id="${escapeHtml(member.code)}" style="cursor: ${hasDescription ? 'pointer' : 'default'};"> 104 <div class="social_media"> 105 ${hasLinkedIn ? ` 106 <a class="linkedin" data-social-url="${escapeAttr(member.linkedin)}"> 107 <svg viewBox="0 0 24 24" style="width: 20px; height: 20px;"> 108 <path fill="#ffffff" d="M4.98 3.5c0 1.381-1.11 2.5-2.48 2.5s-2.48-1.119-2.48-2.5c0-1.38 1.11-2.5 2.48-2.5s2.48 1.12 2.48 2.5zm.02 4.5h-5v16h5v-16zm7.982 0h-4.968v16h4.969v-8.399c0-4.67 6.029-5.052 6.029 0v8.399h4.988v-10.131c0-7.88-8.922-7.593-11.018-3.714v-2.155z"></path> 109 </svg> 110 </a> 111 ` : ''} 112 ${hasTwitter ? ` 113 <a class="twitter" data-social-url="${escapeAttr(member.twitter)}"> 114 <svg xmlns="http://www.w3.org/2000/svg" style="width: 20px; height: 20px;" viewBox="0 0 512 512"> 115 <path fill="#fff" d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"></path> 116 </svg> 117 </a> 118 ` : ''} 119 </div> 120 <div class="team-profile-pic" 121 id="${escapeHtml(member.code)}Image" 122 data-member-code="${escapeAttr(member.code)}" 123 ${hasDescription ? 'data-bs-toggle="modal" data-bs-target="#myMo
123dal"' : ''}> 124 <img src="${escapeAttr(profileImage)}" 125 alt="${escapeAttr(member.name)}" 126 onerror="this.src='/assets/images/placeholder.png'" /> 127 </div> 128 <div class="teamsWrapper ${escapeHtml(member.code)}Overview prof-dec" 129 id="${escapeHtml(member.code)}Wrapper" 130 data-member-code="${escapeAttr(member.code)}" 131 ${hasDescription ? 'data-bs-toggle="modal" data-bs-target="#myModal"' : ''}> 132 <h5 class="teamMemberTitle">${escapeHtml(member.name)}</h5> 133 <p class="role">${escapeHtml(member.title || member.role || '')}</p> 134 </div> 135 <p style="display: none;" class="des" id="${escapeHtml(member.code)}Des">${member.description || ''}</p> 136 </div> 137 `; 138 139 return wrapper; 140} 141 142/** 143 * Set up team member click interactions 144 */ 145function setupTeamInteractions() { 146 const container = document.getElementById('teamContent'); 147 if (!container) return; 148 149 container.addEventListener('click', function(e) { 150 const profilePic = e.target.closest('.team-profile-pic[data-member-code]'); 151 const wrapper = e.target.closest('.teamsWrapper[data-member-code]'); 152 const socialLink = e.target.closest('.social_media a'); 153 154 if (socialLink) { 155 e.preventDefault(); 156 e.stopPropagation(); 157 const url = socialLink.dataset.socialUrl; 158 if (url) { 159 openExternalLink(url); 160 } 161 return; 162 } 163 164 const memberCode = profilePic?.dataset.memberCode || wrapper?.dataset.memberCode; 165 if (memberCode) { 166 const member = teamData.find(m => m.code === memberCode); 167 if (member && member.description && member.description.trim().length > 0) { 168 showProfileModal(member); 169 } 170 } 171 }); 172} 173 174/** 175 * Show the profile modal with member details 176 * @param {Object} member - Team member data 177 */ 178function showProfileModal(member) { 179 currentProfile = member; 180 181 const profileImage = proxyImageUrl(member.profile_Image || member.profile_image || member.profileImage) || '/assets/images/placeholder.png'; 182 183 const imageContainer = document.getElementById('Image'); 184 if (imageContainer) { 185 imageContainer.innerHTML = `<img src="${escapeAttr(profileImage)}" alt="${escapeAttr(member.name)}" onerror="this.src='/assets/images/placeholder.png'" />`; 186 } 187 188 const modalName = document.getElementById('modalName'); 189 if (modalName) { 190 modalName.innerHTML = ` 191 <h5 class="teamMemberTitle">${escapeHtml(member.name)}</h5> 192 <p class="role">${escapeHtml(member.title || member.role || '')}</p> 193 <p class="description">${member.description || ''}</p> 194 `; 195 } 196 197 const linkedinLink = document.querySelector('#myModal #linkedin'); 198 const twitterLink = document.querySelector('#myModal #twitter'); 199 200 if (linkedinLink) { 201 if (member.linkedin && member.linkedin.trim().length > 0) { 202 linkedinLink.style.display = 'inline-block'; 203 linkedinLink.onclick = function(e) { 204 e.preventDefault(); 205 openExternalLink(member.linkedin); 206 }; 207 } else { 208 linkedinLink.style.display = 'none'; 209 } 210 } 211 212 if (twitterLink) { 213 if (member.twitter && member.twitter.trim().length > 0) { 214 twitterLink.style.display = 'inline-block'; 215 twitterLink.onclick = function(e) { 216 e.preventDefault(); 217 openExternalLink(member.twitter); 218 }; 219 } else { 220 twitterLink.style.display = 'none'; 221 } 222 } 223} 224 225/** 226 * Open external link with speed bump modal 227 * @param {string} url - External URL to open 228 */ 229function openExternalLink(url) { 230 const speedBumpModal = document.getElementById('TeamredirectPopupIn'); 231 const acceptBtn = document.getElementById('redirect_accept_teams'); 232 233 if (speedBumpModal && acceptBtn) { 234 acceptBtn.href = url; 235 236 const modal = bootstrap.Modal.getOrCreateInstance(speedBumpModal); 237 modal.show(); 238 } else { 239 240 window.open(url, '_blank'); 241 } 242} 243 244/** 245 * Set up fullscreen functionality for the history timeline 246 */ 247function setupFullscreen() { 248 const fullscreenToggle = document.getElementById('fullscreenToggle'); 249 const historyElement = document.getElementById('myHistory'); 250 251 if (fullscreenToggle && historyElement) { 252 fullscreenToggle.addEventListener('click', function(e) { 253 e.preventDefault(); 254 openFullscreen(historyElement); 255 }); 256 } 257 258 document.addEventListener('fullscreenchange', onFullscreenChange); 259 document.addEventListener('webkitfullscreenchange', onFullscreenChange); 260 document.addEventListener('msfullscreenchange', onFullscreenChange); 261} 262 263/** 264 * Open element in fullscreen mode 265 * @param {HTMLElement} elem - Element to display fullscreen 266 */ 267function openFullscreen(elem) { 268 if (elem.requestFullscreen) { 269 elem.requestFullscreen(); 270 } else if (elem.webkitRequestFullscreen) { 271 elem.webkitRequestFullscreen(); 272 } else if (elem.msRequestFullscreen) { 273 elem.msRequestFullscreen(); 274 } 275 276 const toggleImg = document.querySelector('.fullscreen_toggle img'); 277 if (toggleImg) { 278 toggleImg.style.display = 'none'; 279 } 280} 281 282/** 283 * Handle fullscreen change events 284 */ 285function onFullscreenChange() { 286 if (!document.fullscreenElement && 287 !document.webkitFullscreenElement && 288 !document.msFullscreenElement) { 289 290 const toggleImg = document.querySelector('.fullscreen_toggle img'); 291 if (toggleImg) { 292 toggleImg.style.display = ''; 293 } 294 } 295} 296 297/** 298 * Set up scroll behavior for section navigation 299 */ 300function setupScrollBehavior() { 301 302 window.addEventListener('scroll', function() { 303 const footer = document.getElementById('footer'); 304 const navigation = document.querySelector('.ourTeamnavigation'); 305 306 if (navigation && footer) { 307 const scrollPosition = window.scrollY + window.innerHeight; 308 const footerTop = document.body.scrollHeight - footer.offsetHeight; 309 310 if (scrollPosition > footerTop) { 311 navigation.classList.add('hideFooter'); 312 } else { 313 navigation.classList.remove('hideFooter'); 314 } 315 } 316 }); 317} 318 319/** 320 * Handle hash navigation (e.g., #team) 321 */ 322function handleHashNavigation() { 323 const hash = window.location.hash.substring(1); 324 if (hash) { 325 scrollToSection(hash); 326 } 327} 328 329/** 330 * Scroll to a section by ID 331 * @param {string} sectionId - Section element ID 332 */ 333function scrollToSection(sectionId) { 334 const target = document.getElementById(sectionId); 335 if (target) { 336 setTimeout(function() { 337 const offset = target.getBoundingClientRect().top + window.pageYOffset - 32; 338 window.scrollTo({ 339 top: offset, 340 behavior: 'smooth' 341 }); 342 }, 100); 343 } 344} 345 346/** 347 * Escape HTML to prevent XSS 348 * @param {string} text - Text to escape 349 * @returns {string} Escaped text 350 */ 351function escapeHtml(text) { 352 if (!text) return ''; 353 const div = document.createElement('div'); 354 div.textContent = text; 355 return div.innerHTML; 356} 357 358/** 359 * Escape attribute values 360 * @param {string} text - Text to escape for attribute 361 * @returns {string} Escaped text 362 */ 363function escapeAttr(text) { 364 if (!text) return ''; 365 return text.replace(/"/g, '"').replace(/'/g, '''); 366} 367 368// Init is called externally via dynamic import() from About.razor
369// This ensures re-execution on Blazor enhanced navigation.
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.