PageSourceSearch

https://www.ranocoder.com/js/home.js

js ranocoder.com collected 2026-10-05 01:22:38 UTC 10,437 bytes, 298 lines download raw bytes

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: "&nbsp;" },
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: "&nbsp;&nbsp;<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: "&nbsp;&nbsp;<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: "&nbsp;" },
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.