PageSourceSearch

https://englishpartner.ai/script.js

js englishpartner.ai collected 2026-10-05 12:13:23 UTC 23,764 bytes, 568 lines download raw bytes

1/* =================================================================
2   FORGMIND.AI — Interactive Behaviors
3================================================================= */
4
5document.addEventListener('DOMContentLoaded', () => {
6
7    /* ---------- Cursor Glow ---------- */
8    const cursorGlow = document.getElementById('cursorGlow');
9    if (cursorGlow && window.matchMedia('(pointer: fine)').matches) {
10        let mouseX = 0, mouseY = 0, glowX = 0, glowY = 0;
11
12        document.addEventListener('mousemove', (e) => {
13            mouseX = e.clientX;
14            mouseY = e.clientY;
15        });
16
17        const animateGlow = () => {
18            glowX += (mouseX - glowX) * 0.1;
19            glowY += (mouseY - glowY) * 0.1;
20            cursorGlow.style.transform = `translate(${glowX}px, ${glowY}px) translate(-50%, -50%)`;
21            requestAnimationFrame(animateGlow);
22        };
23        animateGlow();
24    } else if (cursorGlow) {
25        cursorGlow.style.display = 'none';
26    }
27
28    /* ---------- Navbar scroll state + WhatsApp bubble reveal ---------- */
29    const navbar = document.getElementById('navbar');
30    const heroSection = document.querySelector('.hero');
31    const onScroll = () => {
32        if (window.scrollY > 30) navbar.classList.add('scrolled');
33        else navbar.classList.remove('scrolled');
34
35        const heroHeight = heroSection ? heroSection.offsetHeight : 600;
36        if (window.scrollY > heroHeight - 120) document.body.classList.add('scrolled-past-hero');
37        else document.body.classList.remove('scrolled-past-hero');
38    };
39    window.addEventListener('scroll', onScroll, { passive: true });
40    onScroll();
41
42    /* ---------- Mobile menu toggle ---------- */
43    const mobileToggle = document.getElementById('mobileToggle');
44    const navLinks = document.querySelector('.nav-links');
45
46    if (mobileToggle && navLinks) {
47        mobileToggle.addEventListener('click', () => {
48            mobileToggle.classList.toggle('open');
49            navLinks.classList.toggle('mobile-open');
50        });
51
52        navLinks.querySelectorAll('a').forEach(link => {
53            link.addEventListener('click', () => {
54                mobileToggle.classList.remove('open');
55                navLinks.classList.remove('mobile-open');
56            });
57        });
58    }
59
60    /* ---------- ForgeMind logo: mouse-tracking eyes ---------- */
61    const fmSvg = document.getElementById('fmLogoNav');
62    const fmEyeL = document.getElementById('fmEyeLeft');
63    const fmEyeR = document.getElementById('fmEyeRight');
64
65    if (fmSvg && fmEyeL && fmEyeR && window.matchMedia('(pointer: fine)').matches && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
66        const LC = { x: 112, y: 143.9 };
67        const RC = { x: 159.75, y: 143.9 };
68        const EYE_MAX = 5;      // max pixel offset in SVG viewBox units
69        const LERP = 0.18;
70
71        let mx = window.innerWidth / 2;
72        let my = window.innerHeight / 2;
73        let svgRect = fmSvg.getBoundingClientRect();
74        let lx = 0, ly = 0, rx = 0, ry = 0;
75
76        const updateRect = () => { svgRect = fmSvg.getBoundingClientRect(); };
77        window.addEventListener('resize', updateRect, { passive: true });
78        window.addEventListener('scroll', updateRect, { passive: true });
79
80        document.addEventListener('mousemove', (e) => {
81            mx = e.clientX;
82            my = e.clientY;
83        }, { passive: true });
84
85        const targetFor = (center) => {
86            if (svgRect.width < 1) return { x: 0, y: 0 };
87            const sx = 270 / svgRect.width;
88            const sy = 270 / svgRect.height;
89            const dx = (mx - svgRect.left) * sx - center.x;
90            const dy = (my - svgRect.top) * sy - center.y;
91            const d = Math.sqrt(dx * dx + dy * dy);
92            if (d < 0.01) return { x: 0, y: 0 };
93            const m = Math.min(d * 0.07, EYE_MAX);
94            return { x: (dx / d) * m, y: (dy / d) * m };
95        };
96
97        const tick = () => {
98            const tL = targetFor(LC);
99            const tR = targetFor(RC);
100            lx += (tL.x - lx) * LERP;
101            ly += (tL.y - ly) * LERP;
102            rx += (tR.x - rx) * LERP;
103            ry += (tR.y - ry) * LERP;
104            fmEyeL.setAttribute('transform', `translate(${lx.toFixed(2)} ${ly.toFixed(2)})`);
105            fmEyeR.setAttribute('transform', `translate(${rx.toFixed(2)} ${ry.toFixed(2)})`);
106            requestAnimationFrame(tick);
107        };
108        requestAnimationFrame(tick);
109    }
110
111    /* ---------- ForgeMind hero logo: mouse-tracking eyes ---------- */
112    const fmHeroSvg = document.getElementById('fmLogoHero');
113    const fmHeroEyeL = document.getElementById('fmHeroEyeLeft');
114    const fmHeroEyeR = document.getElementById('fmHeroEyeRight');
115
116    if (fmHeroSvg && fmHeroEyeL && fmHeroEyeR && window.matchMedia('(pointer: fine)').matches && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
117        const HLC = { x: 112, y: 143.9 };
118        const HRC = { x: 159.75, y: 143.9 };
119        const H_EYE_MAX = 7;
120        const H_LERP = 0.18;
121
122        let hmx = window.innerWidth / 2;
123        let hmy = window.innerHeight / 2;
124        let hRect = fmHeroSvg.getBoundingClientRect();
125        let hlx = 0, hly = 0, hrx = 0, hry = 0;
126
127        const hUpdateRect = () => { hRect = fmHeroSvg.getBoundingClientRect(); };
128        window.addEventListener('resize', hUpdateRect, { passive: true });
129        window.addEventListener('scroll', hUpdateRect, { passive: true });
130
131        document.addEventListener('mousemove', (e) => {
132            hmx = e.clientX;
133            hmy = e.clientY;
134        }, { passive: true });
135
136        const hTargetFor = (center) => {
137            if (hRect.width < 1) return { x: 0, y: 0 };
138            const sx = 270 / hRect.width;
139            const sy = 270 / hRect.height;
140            const dx = (hmx - hRect.left) * sx - center.x;
141            const dy = (hmy - hRect.top) * sy - center.y;
142            const d = Math.sqrt(dx * dx + dy * dy);
143            if (d < 0.01) return { x: 0, y: 0 };
144            const m = Math.min(d * 0.07, H_EYE_MAX);
145            return { x: (dx / d) * m, y: (dy / d) * m };
146        };
147
148        const hTick = () => {
149            const tL = hTargetFor(HLC);
150            const tR = hTargetFor(HRC);
151            hlx += (tL.x - hlx) * H_LERP;
152            hly += (tL.y - hly) * H_LERP;
153            hrx += (tR.x - hrx) * H_LERP;
154            hry += (tR.y - hry) * H_LERP;
155            fmHeroEyeL.setAttribute('transform', `translate(${hlx.toFixed(2)} ${hly.toFixed(2)})`);
156            fmHeroEyeR.setAttribute('transform', `translate(${hrx.toFixed(2)} ${hry.toFixed(2)})`);
157            requestAnimationFrame(hTick);
158        };
159        requestAnimationFrame(hTick);
160    }
161
162    /* ---------- Smooth scroll for anchor links ---------- */
163    document.querySelectorAll('a[href^="#"]').forEach(anchor => {
164        anchor.addEventListener('click', (e) => {
165            const href = anchor.getAttribute('href');
166            if (href === '#' || href.length < 2) return;
167            const target = document.querySelector(href);
168            if (target) {
169                e.preventDefault();
170                const offset = 80;
171                const top = target.getBoundingClientRect().top + window.pageYOffset - offset;
172                window.scrollTo({ top, behavior: 'smooth' });
173            }
174        });
175    });
176
177    /* ---------- Curriculum unit tabs ---------- */
178    const unitTabs = document.querySelectorAll('.unit-tab');
179    const unitContents = document.querySelectorAll('.unit-content');
180
181    unitTabs.forEach(tab => {
182        tab.addEventListener('click', () => {
183            const target = tab.getAttribute('data-tab');
184
185            unitTabs.forEach(t => {
186                t.classList.remove('active');
187                t.setAttribute('aria-selected', 'false');
188            });
189            unitContents.forEach(c => c.classList.remove('active'));
190
191            tab.classList.add('active');
192            tab.setAttribute('aria-selected', 'true');
193            const targetEl = document.getElementById(target);
194            if (targetEl) targetEl.classList.add('active');
195        });
196    });
197
198    /* ---------- Tools rail: prev/next scroll ---------- */
199    const toolsRail = document.getElementById('toolsRail');
200    const railPrev = document.querySelector('.tools-rail-prev');
201    const railNext = document.querySelector('.tools-rail-next');
202
203    if (toolsRail && railPrev && railNext) {
204        const scrollByCard = (dir) => {
205            const card = toolsRail.querySelector('.tool-card');
206            const step = card ? (card.getBoundingClientRect().width + 14) * 2 : 300;
207            toolsRail.scrollBy({ left: dir * step, behavior: 'smooth' });
208        };
209
210        railPrev.addEventListener('click', () => scrollByCard(-1));
211        railNext.addEventListener('click', () => scrollByCard(1));
212
213        const updateRailBtns = () => {
214            const max = toolsRail.scrollWidth - toolsRail.clientWidth - 2;
215            railPrev.disabled = toolsRail.scrollLeft <= 2;
216            railNext.disabled = toolsRail.scrollLeft >= max;
217        };
218
219        toolsRail.addEventListener('scroll', updateRailBtns, { passive: true });
220        window.addEventListener('resize', updateRailBtns);
221        updateRailBtns();
222    }
223
224    /* ---------- English carousel: arrows + dots ---------- */
225    const englishTrack = document.querySelector('.english-track');
226    const englishDots = document.querySelectorAll('.english-dot');
227    const englishPrev = document.querySelector('.english-arrow-prev');
228    const englishNext = document.querySelector('.english-arrow-next');
229
230    if (englishTrack && englishDots.length && englishPrev && englishNext) {
231        const cards = [...englishTrack.querySelectorAll('.english-card')];
232        const total = cards.length;
233        let current = 0;
234
235        const goTo = (idx) => {
236            current = ((idx % total) + total) % total;
237            englishTrack.style.transform = `translateX(-${current * 100}%)`;
238            englishDots.forEach((d, i) => d.classList.toggle('active', i === current));
239        };
240
241        englishPrev.addEventListener('click', () => goTo(current - 1));
242        englishNext.addEventListener('click', () => goTo(current + 1));
243
244        englishDots.forEach((dot) => {
245            dot.addEventListener('click', () => {
246                goTo(parseInt(dot.getAttribute('data-idx'), 10) || 0);
247            });
248        });
249    }
250
251    /* ---------- 14-Day Journey phase accordion ---------- */
252    document.querySelectorAll('.phase-toggle').forEach(btn => {
253        const label = btn.querySelector('.phase-toggle-label');
254        btn.addEventListener('click', () => {
255            const card = btn.closest('.phase-card');
256            if (!card) return;
257            const open = card.classList.toggle('is-open');
258            btn.setAttribute('aria-expanded', open ? 'true' : 'false');
259            if (label) label.textContent = open ? 'Hide details' : 'View details';
260        });
261    });
262
263    /* ---------- Curriculum mobile accordion (≤768px) ---------- */
264    const mobileMQ = window.matchMedia('(max-width: 768px)');
265    document.querySelectorAll('.curriculum-section .unit-content').forEach((uc, idx) => {
266        const card = uc.querySelector('.unit-card');
267        if (!card) return;
268        if (idx === 0) uc.classList.add('mobile-open');
269        card.addEventListener('click', () => {
270            if (!mobileMQ.matches) return;
271            uc.classList.toggle('mobile-open');
272        });
273    });
274
275    /* ---------- Syllabus PDF modal ---------- */
276    const pdfModal = document.getElementById('syllabusModal');
277    if (pdfModal) {
278        const frame = pdfModal.querySelector('.pdf-modal-frame');
279        const pdfSrc = frame?.getAttribute('data-pdf-src');
280
281        const openModal = () => {
282            if (frame && pdfSrc && !frame.getAttribute('src')) {
283                frame.setAttribute('src', pdfSrc);
284            }
285            pdfModal.classList.add('is-open');
286            pdfModal.setAttribute('aria-hidden', 'false');
287            document.body.classList.add('pdf-modal-open');
288        };
289        const closeModal = () => {
290            pdfModal.classList.remove('is-open');
291            pdfModal.setAttribute('aria-hidden', 'true');
292            document.body.classList.remove('pdf-modal-open');
293        };
294
295        document.querySelectorAll('[data-syllabus-open]').forEach(btn => {
296            btn.addEventListener('click', openModal);
297        });
298        pdfModal.querySelectorAll('[data-syllabus-close]').forEach(el => {
299            el.addEventListener('click', closeModal);
300        });
301        document.addEventListener('keydown', (e) => {
302            if (e.key === 'Escape' && pdfModal.classList.contains('is-open')) closeModal();
303        });
304    }
305
306    /* ---------- FAQ accordion ---------- */
307    document.querySelectorAll('.faq-item').forEach(item => {
308        const q = item.querySelector('.faq-q');
309        q.addEventListener('click', () => {
310            const isActive = item.classList.contains('active');
311            document.querySelectorAll('.faq-item.active').forEach(el => el.classList.remove('active'));
312            if (!isActive) item.classList.add('active');
313        });
314    });
315
316    /* ---------- Reveal-on-scroll ---------- */
317    const revealTargets = document.querySelectorAll(
318        '.objective-card, .phase-card, .audience-card, .method-list li, .assess-card, .outcome-card, .faq-item, .day-dot, .message-card, .unit-card, .day-strip, .spotlight-content, .spotlight-card, .for-card'
319    );
320
321    revealTargets.forEach(el => el.classList.add('reveal'));
322
323    const revealObserver = new IntersectionObserver((entries) => {
324        entries.forEach(entry => {
325            if (entry.isIntersecting) {
326                const i = Array.from(entry.target.parentElement?.children || []).indexOf(entry.target);
327                entry.target.style.transitionDelay = `${Math.max(0, Math.min(i, 6)) * 60}ms`;
328                entry.target.classList.add('visible');
329                revealObserver.unobserve(entry.target);
330            }
331        });
332    }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
333
334    revealTargets.forEach(el => revealObserver.observe(el));
335
336    /* ---------- Day dot progressive reveal ---------- */
337    const dayDots = document.querySelectorAll('.day-dot');
338    const dayObserver = new IntersectionObserver((entries) => {
339        entries.forEach((entry, idx) => {
340            if (entry.isIntersecting) {
341                dayDots.forEach((dot, i) => {
342                    setTimeout(() => dot.classList.add('visible'), i * 70);
343                });
344                dayObserver.disconnect();
345            }
346        });
347    }, { threshold: 0.3 });
348
349    if (dayDots.length) dayObserver.observe(dayDots[0]);
350
351    /* ---------- Active nav link based on scroll ---------- */
352    const navAnchors = document.querySelectorAll('.nav-links a[href^="#"]');
353    const sections = Array.from(navAnchors)
354        .map(a => document.querySelector(a.getAttribute('href')))
355        .filter(Boolean);
356
357    const setActiveNav = () => {
358        const scrollPos = window.scrollY + 120;
359        let current = sections[0];
360        sections.forEach(sec => {
361            if (sec.offsetTop <= scrollPos) current = sec;
362        });
363        navAnchors.forEach(a => {
364            const id = a.getAttribute('href').slice(1);
365            if (current && current.id === id) a.style.color = 'var(--text-0)';
366            else a.style.color = '';
367        });
368    };
369    window.addEventListener('scroll', setActiveNav, { passive: true });
370
371    /* ---------- Subtle parallax on hero orbs ---------- */
372    const orbs = document.querySelectorAll('.orb');
373    if (orbs.length && window.matchMedia('(pointer: fine)').matches) {
374        document.addEventListener('mousemove', (e) => {
375            const cx = window.innerWidth / 2;
376            const cy = window.innerHeight / 2;
377            const dx = (e.clientX - cx) / cx;
378            const dy = (e.clientY - cy) / cy;
379            orbs.forEach((orb, i) => {
380                const factor = (i + 1) * 12;
381                orb.style.transform = `translate(${dx * factor}px, ${dy * factor}px)`;
382            });
383        });
384    }
385
386    /* ---------- CTA click ripple feedback ---------- */
387    document.querySelectorAll('.btn').forEach(btn => {
388        btn.addEventListener('click', function (e) {
389            const rect = this.getBoundingClientRect();
390            const ripple = document.createElement('span');
391            ripple.style.cssText = `
392                position: absolute;
393                width: 10px;
394                height: 10px;
395                border-radius: 50%;
396                background: rgba(255,255,255,0.5);
397                left: ${e.clientX - rect.left}px;
398                top: ${e.clientY - rect.top}px;
399                transform: translate(-50%, -50%) scale(0);
400                pointer-events: none;
401                animation: rippleExpand 0.6s ease-out forwards;
402            `;
403            this.appendChild(ripple);
404            setTimeout(() => ripple.remove(), 700);
405        });
406    });
407
408    // Inject ripple keyframes once
409    const rippleStyle = document.createElement('style');
410    rippleStyle.textContent = `
411        @keyframes rippleExpand {
412            to {
413                transform: translate(-50%, -50%) scale(40);
414                opacity: 0;
415            }
416        }
417    `;
418    document.head.appendChild(rippleStyle);
419
420    /* ---------- Student photo rotator (mobile only) ---------- */
421    const sphGrid = document.getElementById('studentPhotosGrid');
422    if (sphGrid) {
423        const sphCards = Array.from(sphGrid.querySelectorAll('.sph-card'));
424        const sphStates = [
425            ['is-main',        'is-side-top',    'is-side-bottom'],
426            ['is-side-bottom', 'is-main',        'is-side-top'   ],
427            ['is-side-top',    'is-side-bottom', 'is-main'       ],
428        ];
429        let sphStep = 0;
430        setInterval(() => {
431            sphStep = (sphStep + 1) % 3;
432            sphCards.forEach((card, i) => {
433                card.className = 'sph-card ' + sphStates[sphStep][i];
434            });
435        }, 3000);
436    }
437
438    /* ---------- Trainer placeholder photo fade-in ---------- */
439    document.querySelectorAll('.trainer-profile-photo--placeholder img').forEach(img => {
440        if (img.complete && img.naturalWidth > 0) {
441            img.classList.add('loaded');
442        } else {
443            img.addEventListener('load', () => img.classList.add('loaded'));
444        }
445    });
446
447    /* ---------- Tamil / EN language toggle ---------- */
448    const translateBtn = document.getElementById('translateBtn');
449
450    const getLangCookie = () => {
451        const match = document.cookie.match(/googtrans=([^;]+)/);
452        return match ? decodeURIComponent(match[1]) : '';
453    };
454
455    const setActiveTamilState = (active) => {
456        if (!translateBtn) return;
457        const label = translateBtn.querySelector('.translate-label');
458        if (active) {
459            translateBtn.classList.add('is-tamil');
460            if (label) label.textContent = 'English';
461            translateBtn.setAttribute('aria-label', 'Switch back to English');
462        } else {
463            translateBtn.classList.remove('is-tamil');
464            if (label) label.textContent = 'தமிழ்';
465            translateBtn.setAttribute('aria-label', 'Switch to Tamil');
466        }
467    };
468
469    const triggerTamil = () => {
470        const select = document.querySelector('.goog-te-combo');
471        if (select) {
472            select.value = 'ta';
473            select.dispatchEvent(new Event('change'));
474            setActiveTamilState(true);
475        }
476    };
477
478    const triggerEnglish = () => {
479        // Clear the googtrans cookie and reload to restore original text
480        const host = window.location.hostname;
481        document.cookie = 'googtrans=/en/en; path=/';
482        document.cookie = `googtrans=/en/en; domain=${host}; path=/`;
483        window.location.reload();
484    };
485
486    if (translateBtn) {
487        // On page load: if already in Tamil (cookie set), update button state
488        const cookieVal = getLangCookie();
489        if (cookieVal.includes('/ta')) setActiveTamilState(true);
490
491        // Wait for Google Translate widget to initialise, then wire up the button
492        const waitForSelect = setInterval(() => {
493            const select = document.querySelector('.goog-te-combo');
494            if (select) {
495                clearInterval(waitForSelect);
496                // If cookie says Tamil but widget hasn't translated yet, fire it
497                if (getLangCookie().includes('/ta')) triggerTamil();
498            }
499        }, 200);
500
501        translateBtn.addEventListener('click', () => {
502            if (translateBtn.classList.contains('is-tamil')) {
503                triggerEnglish();
504            } else {
505                triggerTamil();
506            }
507        });
508    }
509
510    /* ---------- Trainer image lightbox ---------- */
511    const trainerLightbox = document.getElementById('trainerLightbox');
512    if (trainerLightbox) {
513        const lbImg    = document.getElementById('trainerLightboxImg');
514        const lbEye    = document.getElementById('trainerLightboxEyebrow');
515        const lbName   = document.getElementById('trainerLightboxName');
516        const lbBadge  = document.getElementById('trainerLightboxBadge');
517        const lbTags   = document.getElementById('trainerLightboxTags');
518
519        const openLightbox = (card) => {
520            const img    = card.querySelector('.trainer-profile-photo img');
521            const eyebrow = card.querySelector('.trainer-profile-eyebrow');
522            const name   = card.querySelector('.trainer-profile-name');
523            const badge  = card.querySelector('.trainer-profile-badge');
524            const tags   = card.querySelectorAll('.trainer-profile-tag');
525            const isAI   = card.classList.contains('trainer-profile-card--ai');
526
527            lbImg.src = img.src;
528            lbImg.alt = img.alt;
529            lbImg.style.objectPosition = img.classList.contains('face-focus') ? '50% 12%' : '50% 15%';
530            lbEye.textContent = eyebrow ? eyebrow.textContent : '';
531            lbEye.className = 'trainer-lightbox-eyebrow' + (isAI ? '' : ' is-ep');
532            lbName.textContent = name ? name.textContent : '';
533            lbBadge.innerHTML = badge
534                ? `<span class="trainer-lightbox-badge${isAI ? ' is-ai' : ''}">${badge.textContent}</span>`
535                : '';
536            lbTags.innerHTML = [...tags].map(t =>
537                `<span class="trainer-lightbox-tag">${t.textContent}</span>`
538            ).join('');
539
540            trainerLightbox.classList.add('is-open');
541            trainerLightbox.setAttribute('aria-hidden', 'false');
542            document.body.style.overflow = 'hidden';
543        };
544
545        const closeLightbox = () => {
546            trainerLightbox.classList.remove('is-open');
547            trainerLightbox.setAttribute('aria-hidden', 'true');
548            document.body.style.overflow = '';
549        };
550
551        document.querySelectorAll('.trainer-profile-photo img').forEach(img => {
552            img.addEventListener('click', () => {
553                const card = img.closest('.trainer-profile-card');
554                if (card) openLightbox(card);
555            });
556        });
557
558        trainerLightbox.querySelectorAll('[data-lightbox-close]').forEach(el => {
559            el.addEventListener('click', closeLightbox);
560        });
561
562        document.addEventListener('keydown', (e) => {
563            if (e.key === 'Escape' && trainerLightbox.classList.contains('is-open')) closeLightbox();
564        });
565    }
566
567    console.log('%c ForgeMind AI ', 'background: #E50914; color: white; padding: 6px 12px; border-radius: 4px; font-weight: bold;', 'AI Basics Course landing loaded.');
568});

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.