1/** 2 * UI Management for Phase-Aware Components 3 * Handles rendering of navigation, CTAs, and status messages based on current phase 4 */ 5 6class UIManager { 7 constructor(phaseManager) { 8 this.phaseManager = phaseManager; 9 this.config = null; 10 this.basePath = this.calculateBasePath(); 11 } 12 13 /** 14 * Calculate the base path based on current page depth in directory structure 15 * Returns appropriate relative path prefix (e.g., './', '../', '../../') 16 */ 17 calculateBasePath() { 18 const fullPath = window.location.pathname; 19 let relativePath; 20 21 if (window.location.protocol === 'file:') { 22 // For file://, find 'docs/' and get everything after it 23 const parts = fullPath.split('/docs/'); 24 relativePath = parts.length > 1 ? parts[1] : fullPath; 25 } else { 26 // For http(s)://, remove leading / 27 relativePath = fullPath.slice(1); 28 } 29 30 // Calculate depth from relative path 31 const depth = relativePath.split('/').filter(part => part && !part.endsWith('.html')).length; 32 return depth > 0 ? '../'.repeat(depth) : './'; 33 } 34 35 init() { 36 // Wait for DOM to be ready 37 if (document.readyState === 'loading') { 38 document.addEventListener('DOMContentLoaded', () => this.render()); 39 } else { 40 this.render(); 41 } 42 } 43 44 render() { 45 this.config = this.phaseManager.getPhaseConfig(); 46 47 this.renderStatusBanner(); 48 this.renderNavigation(); 49 this.updatePrimaryCTA(); 50 this.renderSecondaryActions(); 51 } 52 53 renderStatusBanner() { 54 const statusMessage = this.phaseManager.getStatusMessage(); 55 if (!statusMessage) return; 56 57 // Check if banner was previously dismissed 58 const dismissedKey = `status-banner-dismissed-${this.config.phase}-${this.config.awardsYear}`; 59 if (statusMessage.dismissible && localStorage.getItem(dismissedKey)) { 60 return; 61 } 62 63 const existingBanner = document.querySelector('.status-banner'); 64 if (existingBanner) { 65 existingBanner.remove(); 66 } 67 68 const banner = document.createElement('div'); 69 banner.className = `status-banner ${statusMessage.type}`; 70 banner.innerHTML = ` 71 <div class="status-banner-content"> 72 <span>${statusMessage.icon}</span> 73 <span>${statusMessage.message}</span> 74 </div> 75 ${statusMessage.dismissible ? '<button class="status-banner-dismiss" aria-label="Dismiss banner">×</button>' : ''} 76 `; 77 78 // Insert at the top of the page 79 document.body.insertBefore(banner, document.body.firstChild); 80 81 // Handle dismiss 82 if (statusMessage.dismissible) { 83 const dismissBtn = banner.querySelector('.status-banner-dismiss'); 84 dismissBtn.addEventListener('click', () => { 85 banner.remove(); 86 localStorage.setItem(dismissedKey, 'true'); 87 }); 88 } 89 } 90 91 renderNavigation() { 92 if (!this.config.showNavigation) return; 93 94 const existingNav = document.querySelector('.phase-nav'); 95 if (existingNav) { 96 existingNav.remove(); 97 } 98 99 const nav = document.createElement('nav'); 100 nav.className = 'phase-nav'; 101 nav.setAttribute('aria-label', 'Phase navigation'); 102 103 const breadcrumbList = document.createElement('ul'); 104 breadcrumbList.className = 'nav-breadcrumb'; 105 106 // Add home link 107 const homeItem = document.createElement('li'); 108 if (this.isCurrentPage('index.html')) { 109 homeItem.className = 'current'; 110 homeItem.textContent = 'Home'; 111 homeItem.setAttribute('aria-current', 'page'); 112 } else { 113 const homeLink = document.createElement('a'); 114 homeLink.href = this.basePath + 'index.html'; 115 homeLink.textContent = 'Home'; 116 homeItem.appendChild(homeLink); 117 } 118 breadcrumbList.appendChild(homeItem); 119 120 // Add phase-specific navigation items 121 this.config.navigation.forEach(item => { 122 const listItem = document.createElement('li'); 123 124 if (this.isCurrentPage(item.href)) { 125 listItem.className = 'current'; 126 listItem.textContent = item.text; 127 listItem.setAttribute('aria-current', 'page'); 128 } else { 129 const link = document.createElement('a'); 130 link.href = this.basePath + item.href; 131 link.textContent = item.text; 132 133 if (item.highlight) { 134 link.classList.add('highlight'); 135 } 136 137 listItem.appendChild(link); 138 } 139 140 breadcrumbList.appendChild(listItem); 141 }); 142 143 nav.appendChild(breadcrumbList); 144 145 // Insert after header 146 const header = document.querySelector('header'); 147 if (header) { 148 header.insertAdjacentElement('afterend', nav); 149 } else { 150 document.body.insertBefore(nav, document.body.firstChild); 151 } 152 } 153 154 updatePrimaryCTA() { 155 const button = document.getElementById('nominees-button'); 156 if (!button) return; 157 158 const cta = this.config.primaryCTA; 159 const buttonText = button.querySelector('.button-text'); 160 const buttonSubtext = button.querySelector('.button-subtext'); 161 162 if (buttonText) buttonText.textContent = cta.text; 163 if (buttonSubtext) buttonSubtext.textContent = cta.subtext; 164 165 // Update click handler with base path 166 button.onclick = () => window.location.href = this.basePath + cta.href; 167 } 168 169 renderSecondaryActions() { 170 const existingActions = document.querySelector('.secondary-actions'); 171 if (existingActions) { 172 existingActions.remove(); 173 } 174 175 if (this.config.secondaryActions.length === 0) return; 176 177 const actionsContainer = document.createElement('div'); 178 actionsContainer.className = 'secondary-actions'; 179 180 this.config.secondaryActions.forEach(action => { 181 const link = document.createElement('a'); 182 link.href = this.basePath + action.href; 183 link.className = 'secondary-cta'; 184 link.textContent = action.text; 185 actionsContainer.appendChild(link); 186 }); 187 188 // Insert after primary CTA (home page only) 189 const primaryCTA = document.getElementById('nominees-cta-container'); 190 if (primaryCTA) { 191 primaryCTA.insertAdjacentElement('afterend', actionsContainer); 192 } 193 } 194 195 isCurrentPage(href) { 196 const currentPath = window.location.pathname; 197 const currentFile = currentPath.split('/').pop() || 'index.html'; 198 const targetFile = href.split('/').pop(); 199 200 return currentFile === targetFile || 201 (currentFile === '' && targetFile === 'index.html') || 202 (currentFile === 'index.html' && targetFile === ''); 203 } 204} 205 206// Auto-initialize when phase management is available 207document.addEventListener('DOMContentLoaded', function() { 208 if (typeof phaseManager !== 'undefined') { 209 const uiManager = new UIManager(phaseManager); 210 uiManager.init(); 211 } 212});
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.