1/** 2 * Home Page JavaScript 3 * Typewriter effect and other home-specific functionality 4 */ 5 6/** 7 * Load the shared testimonials component so Trustpilot badge + cards stay reusable. 8 */ 9async function loadTestimonialsSection() { 10 const placeholder = document.getElementById('testimonials-placeholder'); 11 if (!placeholder || !window.ComponentLoader) return; 12 13 const base = window.App && typeof window.App.getBasePath === 'function' ? window.App.getBasePath() : ''; 14 await ComponentLoader.load('testimonials-placeholder', base + 'components/testimonials.html'); 15} 16 17/** 18 * Load client logos from data/clients.json and render the marquee. 19 * - Renders active logos only 20 * - Duplicates the set for a seamless infinite-scroll effect 21 * - Supports optional dark-mode variant via logoDark field 22 * - Hides the entire section if there are no active logos 23 */ 24async function loadClientLogos() { 25 const track = document.getElementById('client-logos-track'); 26 if (!track) return; 27 28 const base = window.App && typeof window.App.getBasePath === 'function' ? window.App.getBasePath() : ''; 29 30 let clients; 31 try { 32 const res = await fetch(base + 'data/clients.json'); 33 if (!res.ok) throw new Error('Failed to fetch clients.json'); 34 const json = await res.json(); 35 clients = (json.clients || []).filter(c => c.active === true); 36 } catch (e) { 37 // On error, hide the section gracefully 38 const section = document.getElementById('client-logos-section'); 39 if (section) section.style.display = 'none'; 40 return; 41 } 42 43 // No active logos yet â keep the skeleton placeholders visible so the 44 // section still shows on the homepage until real logos are added. 45 if (clients.length === 0) { 46 return; 47 } 48 49 /** 50 * Build a single logo element from a client object. 51 * @param {Object} client 52 * @returns {HTMLElement} 53 */ 54 function buildLogoItem(client) { 55 const wrapper = document.createElement('div'); 56 wrapper.className = 'client-logo-item'; 57 58 // Detect square logos by checking filename hints or logoDark absence 59 if (client.logo && /(-icon|-square|icon\.|logo-icon)/.test(client.logo)) { 60 wrapper.classList.add('logo-square'); 61 } 62 63 // Build the inner element: <a> if website provided, else <span> 64 const inner = client.website 65 ? Object.assign(document.createElement('a'), { 66 href: client.website, 67 target: '_blank', 68 rel: 'noopener noreferrer', 69 title: client.name, 70 }) 71 : document.createElement('span'); 72 73 // Light-mode image 74 const imgLight = document.createElement('img'); 75 imgLight.src = base + client.logo; 76 imgLight.alt = client.name + ' logo'; 77 imgLight.loading = 'lazy'; 78 imgLight.decoding = 'async'; 79 imgLight.width = 240; 80 imgLight.height = 80; 81 if (client.logoDark) { 82 imgLight.className = 'logo-light-variant'; 83 } 84 85 inner.appendChild(imgLight); 86 87 // Dark-mode variant (optional) 88 if (client.logoDark) { 89 const imgDark = document.createElement('img'); 90 imgDark.src = base + client.logoDark; 91 imgDark.alt = client.name + ' logo'; 92 imgDark.loading = 'lazy'; 93 imgDark.decoding = 'async'; 94 imgDark.width = 240; 95 imgDark.height = 80; 96 imgDark.className = 'logo-dark-variant'; 97 inner.appendChild(imgDark); 98 } 99 100 wrapper.appendChild(inner); 101 return wrapper; 102 } 103 104 // Render first set + duplicate for seamless loop 105 // Need enough items to fill the viewport â duplicate until we have at least 10 slots 106 const minItems = Math.max(clients.length * 2, 10); 107 const sets = Math.ceil(minItems / clients.length); 108 109 const fragment = document.createDocumentFragment(); 110 for (let s = 0; s < sets; s++) {
111 clients.forEach(client => fragment.appendChild(buildLogoItem(client))); 112 } 113 114 // Replace skeleton placeholders with real logos 115 track.innerHTML = ''; 116 track.appendChild(fragment); 117 118 // Adjust animation speed based on item count (more logos = slightly faster) 119 const speed = Math.max(20, 10 + clients.length * 4); 120 track.style.animationDuration = speed + 's'; 121} 122 123/** 124 * Initialize typewriter effect for hero code card 125 */ 126function initTypewriter() { 127 const container = document.getElementById('typing-code-container'); 128 if (!container) return; 129 130 const codeLines = [ 131 { 132 text: "import { Success } from '@ranocoder/core';", 133 html: "<span class='code-keyword'>import</span> { <span class='code-variable'>Success</span> } <span class='code-keyword'>from</span> <span class='code-string'>'@ranocoder/core'</span>;" 134 }, 135 { text: " ", html: " " }, 136 { 137 text: "// Nothing is iMpossible!", 138 html: "<span class='code-comment'>// Nothing is iMpossible!</span>" 139 }, 140 { 141 text: "const buildDigitalSolution = async () => {", 142 html: "<span class='code-keyword'>const</span> <span class='code-function'>buildDigitalSolution</span> = <span class='code-keyword'>async</span> () => {" 143 }, 144 { 145 text: " const stack = ['React', 'Node', 'Python'];", 146 html: " <span class='code-keyword'>const</span> <span class='code-variable'>stack</span> = [<span class='code-string'>'React'</span>, <span class='code-string'>'Node'</span>, <span class='code-string'>'Python'</span>];" 147 }, 148 { 149 text: " return launch(stack);", 150 html: " <span class='code-keyword'>return</span> <span class='code-function'>launch</span>(<span class='code-variable'>stack</span>);" 151 }, 152 { text: "};", html: "};" }, 153 { text: " ", html: " " }, 154 { 155 text: "buildDigitalSolution();", 156 html: "<span class='code-function'>buildDigitalSolution</span>();<span class='code-cursor'></span>" 157 } 158 ]; 159 160 let lineIndex = 0; 161 let charIndex = 0; 162 let currentElement = null; 163 164 function type() { 165 if (lineIndex >= codeLines.length) { 166 setTimeout(() => { 167 container.innerHTML = ''; 168 lineIndex = 0; 169 type(); 170 }, 5000); 171 return; 172 } 173 174 if (charIndex === 0) { 175 currentElement = document.createElement('p'); 176 currentElement.className = 'whitespace-pre font-mono text-gray-300 min-h-[1.5em]'; 177 container.appendChild(currentElement); 178 } 179 180 const lineData = codeLines[lineIndex]; 181 182 if (charIndex < lineData.text.length) { 183 currentElement.textContent += lineData.text.charAt(charIndex); 184 charIndex++; 185 setTimeout(type, Math.random() * 30 + 30); 186 } else { 187 // Line finished: Swap with highlighted HTML 188 currentElement.innerHTML = lineData.html; 189 lineIndex++; 190 charIndex = 0; 191 setTimeout(type, 100); 192 } 193 } 194 195 type(); 196} 197 198/** 199 * Initialize FAQ Accordion 200 */ 201function initFaqAccordion() { 202 const faqItems = document.querySelectorAll('.faq-item'); 203 204 faqItems.forEach(item => { 205 const trigger = item.querySelector('.faq-trigger'); 206 const content = item.querySelector('.faq-content'); 207 208 if (!trigger || !content) return; 209 210 trigger.addEventListener('click', () => { 211 const isActive = item.classList.contains('active'); 212 213 // Close all other items 214 faqItems.forEach(otherItem => { 215 if (otherItem !== item) { 216 otherItem.classList.remove('active'); 217 const otherContent = otherItem.querySelector('.faq-content'); 218 if (otherContent) { 219 otherContent.classList.add('hidden'); 220 } 221 } 222 }); 223 224 // Toggle current item 225 item.classList.toggle('active'); 226 content.classList.toggle('hidden'); 227 }); 228 });
229} 230 231/** 232 * Initialize Stats Counter Animation 233 */ 234function initStatsCounter() { 235 const statsItems = document.querySelectorAll('.stats-item [data-count]'); 236 237 if (statsItems.length === 0) return; 238 239 const animateCounter = (element) => { 240 const target = parseInt(element.dataset.count); 241 const suffix = element.textContent.includes('%') ? '%' : '+'; 242 const duration = 2000; 243 const step = target / (duration / 16); 244 let current = 0; 245 246 const counter = setInterval(() => { 247 current += step; 248 if (current >= target) { 249 element.textContent = target + suffix; 250 clearInterval(counter); 251 } else { 252 element.textContent = Math.floor(current) + suffix; 253 } 254 }, 16); 255 }; 256 257 // Use Intersection Observer for scroll trigger 258 const observer = new IntersectionObserver((entries) => { 259 entries.forEach(entry => { 260 if (entry.isIntersecting) { 261 animateCounter(entry.target); 262 observer.unobserve(entry.target); 263 } 264 }); 265 }, { threshold: 0.5 }); 266 267 statsItems.forEach(item => observer.observe(item)); 268} 269 270/** 271 * Initialize home page specific functionality 272 */ 273async function initHomePage() { 274 // Load testimonials component (Trustpilot badge + curated grid) 275 await loadTestimonialsSection(); 276 277 // Load client logos from data/clients.json 278 loadClientLogos(); 279 280 // Initialize typewriter effect 281 initTypewriter(); 282 283 // Initialize FAQ accordion 284 initFaqAccordion(); 285 286 // Initialize stats counter 287 initStatsCounter(); 288} 289 290// Run when page loads 291if (document.readyState === 'loading') { 292 document.addEventListener('DOMContentLoaded', function () { 293 // Wait a bit for app.js to finish loading components 294 setTimeout(initHomePage, 100); 295 }); 296} else { 297 setTimeout(initHomePage, 100); 298}
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.