1/** 2 * Header/Navigation Utility 3 * Generates and injects the navigation header into pages 4 * Also handles hamburger menu functionality and navbar scroll behavior 5 */ 6 7(function() { 8 'use strict'; 9 10 const INSTALL_STORES = { 11 chrome: { 12 id: 'chrome', 13 href: 'https://chromewebstore.google.com/detail/thinkreview-ai-copilot-fo/bpgkhgbchmlmpjjpmlaiejhnnbkdjdjn', 14 label: 'Add to Chrome', 15 iconClass: 'fab fa-chrome' 16 }, 17 firefox: { 18 id: 'firefox', 19 href: 'https://addons.mozilla.org/en-US/firefox/addon/thinkreview-code-review/?utm_source=addons.mozilla.org&utm_medium=referral&utm_content=search', 20 label: 'Add to Firefox', 21 iconClass: 'fab fa-firefox' 22 }, 23 edge: { 24 id: 'edge', 25 href: 'https://microsoftedge.microsoft.com/addons/detail/thinkreview-ai-code-revi/hbcanlmjippkplohigflbknfcajdkglk', 26 label: 'Add to Edge', 27 iconClass: 'fab fa-edge' 28 } 29 }; 30 31 /** 32 * Pick the store listing for the current browser. 33 * Edge and Firefox must be checked before Chrome (their UAs also contain "Chrome"). 34 * Other Chromium browsers (Brave, Arc, Opera) use the Chrome Web Store. 35 */ 36 function detectInstallStore() { 37 const ua = navigator.userAgent || ''; 38 if (/firefox/i.test(ua) && !/seamonkey/i.test(ua)) { 39 return INSTALL_STORES.firefox; 40 } 41 if (/edg/i.test(ua)) { 42 return INSTALL_STORES.edge; 43 } 44 const store = Object.assign({}, INSTALL_STORES.chrome); 45 const isChrome = /chrome|crios/i.test(ua) && /google inc/i.test(navigator.vendor || ''); 46 const isBrave = typeof navigator.brave === 'object'; 47 const isOpera = /opr\//i.test(ua); 48 if (!isChrome || isBrave || isOpera) { 49 store.label = 'Add to your browser'; 50 } 51 return store; 52 } 53 54 function applyBrowserInstallCtas() { 55 const store = detectInstallStore(); 56 57 document.querySelectorAll('a.cta-button, a[data-install-cta]').forEach((el) => { 58 el.href = store.href; 59 const labelEl = el.querySelector('[data-install-label]'); 60 const iconEl = el.querySelector('[data-install-icon]'); 61 if (labelEl) { 62 labelEl.textContent = store.label; 63 } else { 64 el.textContent = store.label; 65 } 66 if (iconEl) { 67 iconEl.className = store.iconClass; 68 iconEl.setAttribute('aria-hidden', 'true'); 69 } 70 }); 71 72 document.querySelectorAll('[data-install-group]').forEach((group) => { 73 const buttons = Array.from(group.querySelectorAll('[data-install]')); 74 const match = buttons.find((btn) => btn.getAttribute('data-install') === store.id) || buttons[0]; 75 if (!match) return; 76 77 buttons.forEach((btn) => { 78 const isPrimary = btn === match; 79 btn.classList.toggle('btn-primary-v2', isPrimary); 80 btn.classList.toggle('btn-secondary-v2', !isPrimary); 81 }); 82 83 group.insertBefore(match, group.firstChild); 84 const tertiary = group.querySelector('.btn-tertiary-v2'); 85 if (tertiary) { 86 group.appendChild(tertiary); 87 } 88 }); 89 } 90 91 /** 92 * Initialize the header/navigation 93 * @param {Object} options - Configuration options 94 * @param {Object} options.legalHeader - Optional legal header section 95 * @param {string} options.legalHeader.title - Legal header title 96 * @param {string} options.legalHeader.icon - FontAwesome icon class (e.g., 'fa-shield-alt') 97 * @param {string} options.legalHeader.subtitle - Legal header subtitle 98 */ 99 function initHeader(options = {}) { 100 // Check if header already exists 101 if (document.querySelector('.navbar')) { 102 console.warn('Header already exists, skipping initialization'); 103 applyBrowserInstallCtas(); 104 return; 105 } 106 107 // Determine if we're on the homepage 108 const isHomepage = window.location.pathname === '/' || window.location.pathname === '/index.html'; 109 110 // Platform SVGs â match Thinkreview-webapp src/assets/platformLogos.js (Azure DevOps, Bitbucket) 111 // Agents icon â Material Hub (same as portal AgentsSettings / Sidebar) 112 const logoAzureSvg = '<svg class="integration-logo-svg integration-logo-svg--azure" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M0 8.877L2.247 5.91l8.405-3.416V.022l7.37 5.393L2.966 8.338v8.225L0 15.707zm24-4.45v14.651l-5.753 4.9-9.303-3.057v3.056l-5.978-7.416 15.057 1.798V5.415z"/></svg>'; 113 const logoBitbucketSvg = '<svg class="integration-logo-svg integration-logo-svg--bitbucket" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891L.778 1.213zM14.52 15.53H9.522L8.17 8.466h7.561l-1.211 7.064z"/></svg>'; 114 const logoAgentsHubSvg = '<svg class="integration-logo-svg integration-logo-svg--hub" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8.4 18.2c.38.5.6 1.12.6 1.8 0 1.66-1.34 3-3 3s-3-1.34-3-3 1.34-3 3-3c.44 0 .85.09 1.23.26l1.41-1.77c-.92-1.03-1.29-2.39-1.09-3.69l-2.03-.68c-.54.83-1.46 1.38-2.52 1.38-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3c0 .07 0 .14-.01.21l2.03.68c.64-1.21 1.82-2.09 3.22-2.32V5.91C9.96 5.57 9 4.4 9 3c0-1.
11466 1.34-3 3-3s3 1.34 3 3c0 1.4-.96 2.57-2.25 2.91v2.16c1.4.23 2.58 1.11 3.22 2.32L18 9.71V9.5c0-1.66 1.34-3 3-3s3 1.34 3 3-1.34 3-3 3c-1.06 0-1.98-.55-2.52-1.37l-2.03.68c.2 1.29-.16 2.65-1.09 3.69l1.41 1.77Q17.34 17 18 17c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3c0-.68.22-1.3.6-1.8l-1.41-1.77c-1.35.75-3.01.76-4.37 0z"/></svg>'; 115 116 // Define integrations dropdown items (icon = Font Awesome fab; iconHtml = platform SVG) 117 const integrations = [ 118 { href: '/integrations/gitlab', text: 'GitLab', icon: 'fa-gitlab' }, 119 { href: '/integrations/gitlab-community-edition', text: 'GitLab Community Edition', icon: 'fa-gitlab' }, 120 { href: '/integrations/gitlab-enterprise-edition', text: 'GitLab Enterprise Edition', icon: 'fa-gitlab' }, 121 { href: '/integrations/github', text: 'GitHub', icon: 'fa-github' }, 122 { href: '/integrations/github-enterprise-server', text: 'GitHub Enterprise Server', icon: 'fa-github' }, 123 { href: '/integrations/azure-devops', text: 'Azure DevOps Cloud', iconHtml: logoAzureSvg }, 124 { href: '/integrations/azure-devops-server', text: 'Azure DevOps Server', iconHtml: logoAzureSvg }, 125 { href: '/integrations/tfs-2017', text: 'TFS 2017', iconHtml: logoAzureSvg }, 126 { href: '/integrations/bitbucket', text: 'Bitbucket Cloud', iconHtml: logoBitbucketSvg }, 127 { href: '/integrations/bitbucket-data-center', text: 'Bitbucket Data Center', iconHtml: logoBitbucketSvg } 128 ]; 129 130 const productLinks = [ 131 { href: '/features/custom-rules', text: 'Custom Rules', icon: 'fa-sliders-h', iconStyle: 'fas' }, 132 { href: '/features/review-agents', text: 'Review Agents', iconHtml: logoAgentsHubSvg }, 133 { href: '/features/mcp', text: 'MCP', icon: 'fa-terminal', iconStyle: 'fas' }, 134 { href: '/features/enterprise-gateway', text: 'Enterprise Gateway', icon: 'fa-network-wired', iconStyle: 'fas' }, 135 { href: '/features/ollama', text: 'Local Ollama', icon: 'fa-lock', iconStyle: 'fas' }, 136 { href: '/availablemodels.html', text: 'AI Models', icon: 'fa-microchip', iconStyle: 'fas' } 137 ]; 138 139 // Build navigation links based on page context 140 const navLinks = isHomepage 141 ? [ 142 { href: '#blog', text: 'Blog' }, 143 { href: '#pricing', text: 'Pricing' } 144 ] 145 : [ 146 { href: '/blog', text: 'Blog' }, 147 { href: '/pricing.html', text: 'Pricing' } 148 ]; 149 150 // Determine active link based on current page 151 const currentPath = window.location.pathname; 152 let activeLink = null; 153 if (currentPath.includes('/blog')) { 154 activeLink = '/blog'; 155 } else if (currentPath === '/pricing.html' || currentPath === '/pricing' || currentPath.endsWith('/pricing.html')) { 156 activeLink = '/pricing.html'; 157 } 158 159 const renderDropdownItems = (items) => items.map(item => { 160 const mark = item.iconHtml 161 ? item.iconHtml 162 : `<i class="${item.iconStyle || 'fab'} ${item.icon}"></i>`; 163 return `<li><a href="${item.href}">${mark} ${item.text}</a></li>`; 164 }).join(''); 165 166 // Build navigation HTML 167 const navHTML = ` 168 <nav class="navbar"> 169 <div class="nav-container"> 170 <a href="/" class="nav-logo"> 171 <img src="${isHomepage ? 'icons' : '/icons'}/icon48.png" alt="ThinkReview Logo" class="nav-logo-img"> 172 <span>ThinkReview</span> 173 </a> 174 <div class="nav-menus-wrap"> 175 <ul class="nav-menu nav-menu-main"> 176 ${navLinks.map(link => 177 `<li><a href="${link.href}" ${activeLink === link.href ? 'class="active"' : ''}>${link.text}</a></li>` 178 ).join('')} 179 <li class="nav-dropdown"> 180 <a href="${isHomepage ? '#features' : '/#features'}" class="nav-dropdown-toggle"> 181 Product <i class="fas fa-chevron-down"></i> 182 </a> 183 <ul class="nav-dropdown-menu"> 184 ${renderDropdownItems(productLinks)} 185 </ul> 186 </li> 187 <li><a href="/release-notes.html" class="whats-new-link">What's New<span class="notification-dot" aria-hidden="true"></span></a></li> 188 <li class="nav-dropdown"> 189 <a href="#" class="nav-dropdown-toggle"> 190 Integrations <i class="fas fa-chevron-down"></i> 191 </a> 192 <ul class="nav-dropdown-menu nav-dropdown-menu--wide"> 193 ${renderDropdownItems(integrations)} 194 </ul> 195 </li> 196 </ul> 197 <ul class="nav-menu nav-menu-cta"> 198 <li><a href="https://portal.thinkreview.dev" class="login-button">Login</a></li> 199 <li><a href="https://portal.thinkreview.dev" class="get-started-button">Get started for free</a></li> 200 </ul> 201 </div> 202 <div class="hamburger">
203 <span></span> 204 <span></span> 205 <span></span> 206 </div> 207 </div> 208 </nav> 209 `; 210 211 // Insert navigation at the beginning of body 212 document.body.insertAdjacentHTML('afterbegin', navHTML); 213 214 // Add legal header if provided 215 if (options.legalHeader) { 216 const legalHeaderHTML = ` 217 <section class="legal-header"> 218 <div class="container"> 219 <h1><i class="fas ${options.legalHeader.icon}"></i> ${options.legalHeader.title}</h1> 220 <p>${options.legalHeader.subtitle}</p> 221 </div> 222 </section> 223 `; 224 const navbar = document.querySelector('.navbar'); 225 navbar.insertAdjacentHTML('afterend', legalHeaderHTML); 226 } 227 228 // Initialize hamburger menu functionality 229 initHamburgerMenu(); 230 231 // Initialize navbar scroll behavior 232 initNavbarScroll(); 233 234 // Initialize dropdown functionality 235 initDropdownMenu(); 236 237 applyBrowserInstallCtas(); 238 } 239 240 /** 241 * Initialize hamburger menu toggle functionality 242 */ 243 function initHamburgerMenu() { 244 const hamburger = document.querySelector('.hamburger'); 245 const navMenu = document.querySelector('.nav-menus-wrap'); 246 247 if (hamburger && navMenu) { 248 const hamburgerSpans = hamburger.querySelectorAll('span'); 249 250 function setMobileMenuOpen(isOpen) { 251 hamburger.classList.toggle('active', isOpen); 252 navMenu.classList.toggle('active', isOpen); 253 hamburgerSpans.forEach(span => span.classList.toggle('active', isOpen)); 254 hamburger.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 255 document.body.classList.toggle('nav-open', isOpen); 256 } 257 258 hamburger.setAttribute('role', 'button'); 259 hamburger.setAttribute('tabindex', '0'); 260 hamburger.setAttribute('aria-label', 'Toggle navigation menu'); 261 hamburger.setAttribute('aria-expanded', 'false'); 262 263 hamburger.addEventListener('click', function() { 264 setMobileMenuOpen(!navMenu.classList.contains('active')); 265 }); 266 267 hamburger.addEventListener('keydown', function(e) { 268 if (e.key === 'Enter' || e.key === ' ') { 269 e.preventDefault(); 270 setMobileMenuOpen(!navMenu.classList.contains('active')); 271 } 272 }); 273 274 // Close mobile menu when following a real link, not dropdown toggles 275 document.querySelectorAll('.nav-menu a').forEach(link => { 276 link.addEventListener('click', () => { 277 if (link.classList.contains('nav-dropdown-toggle')) return; 278 setMobileMenuOpen(false); 279 }); 280 }); 281 282 window.__setMobileNavOpen = setMobileMenuOpen; 283 } 284 } 285 286 /** 287 * Initialize navbar scroll behavior (background change on scroll) 288 */ 289 function initNavbarScroll() { 290 window.addEventListener('scroll', function() { 291 const navbar = document.querySelector('.navbar'); 292 if (navbar) { 293 if (window.scrollY > 50) { 294 navbar.style.background = 'rgba(255, 255, 255, 0.98)'; 295 navbar.style.boxShadow = '0 2px 20px rgba(0, 0, 0, 0.1)'; 296 } else { 297 navbar.style.background = 'rgba(255, 255, 255, 0.95)'; 298 navbar.style.boxShadow = 'none'; 299 } 300 } 301 }); 302 } 303 304 /** 305 * Initialize dropdown menu functionality 306 */ 307 function initDropdownMenu() { 308 const dropdowns = document.querySelectorAll('.nav-dropdown'); 309 310 dropdowns.forEach(dropdown => { 311 const toggle = dropdown.querySelector('.nav-dropdown-toggle'); 312 313 // Toggle dropdown on click (for mobile) 314 toggle.addEventListener('click', function(e) { 315 e.preventDefault(); 316 dropdown.classList.toggle('active'); 317 318 // Close other dropdowns 319 dropdowns.forEach(other => { 320 if (other !== dropdown) { 321 other.classList.remove('active'); 322 } 323 }); 324 }); 325 326 // Show dropdown on hover (for desktop) 327 dropdown.addEventListener('mouseenter', function() { 328 if (window.innerWidth > 1100) { 329 dropdown.classList.add('active'); 330 } 331 }); 332 333 dropdown.addEventListener('mouseleave', function() { 334 if (window.innerWidth > 1100) { 335 dropdown.classList.remove('active'); 336 } 337 }); 338 }); 339 340 // Close dropdown when clicking outside 341 document.addEventListener('click', function(e) { 342 if (!e.target.closest('.nav-dropdown')) { 343 dropdowns.forEach(dropdown => { 344 dropdown.classList.remove('active'); 345 }); 346 } 347 }); 348 } 349 350 // Auto-initialize when DOM is ready 351 if (document.readyState === 'loading') { 352 document.addEventListener('DOMContentLoaded', function() { 353 // Check for header config in a script tag with type="application/json" and id="header-config" 354 const configScript = document.getElementById('header-config'); 355 let config = {}; 356 if (configScript) { 357 try { 358 config = JSON.parse(configScript.textContent); 359 } catch (e) { 360 console.warn('Failed to parse header config:', e); 361 } 362 } 363 initHeader(config); 364 }); 365 } else { 366 // DOM already loaded 367 const configScript = document.getElementById('header-config'); 368 let config = {}; 369 if (configScript) { 370 try { 371 config = JSON.parse(configScript.textContent); 372 } catch (e) { 373 console.warn('Failed to parse header config:', e); 374 } 375 } 376 initHeader(config); 377 } 378 379 // Export for manual initialization if needed 380 window.initHeader = initHeader; 381})(); 382
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.