PageSourceSearch

https://thinkreview.dev/scripts/header.js

js thinkreview.dev collected 2026-10-01 14:02:32 UTC 17,840 bytes, 382 lines download raw bytes

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.