commit 20260703

This commit is contained in:
2026-07-03 20:34:27 +08:00
parent 296db4a164
commit c1630b8c10
63 changed files with 3497 additions and 1110 deletions
+96
View File
@@ -0,0 +1,96 @@
/**
* About Us 2 — achievements stats count-up animation.
*/
(function () {
var SECTION_SELECTOR = "#about-us-2 .au2-stats";
var VALUE_SELECTOR = ".au2-stats__value[data-au2-count]";
var DURATION_MS = 1800;
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
function formatValue(value, useGrouping) {
if (!useGrouping) return String(value);
return value.toLocaleString("en-US");
}
function parseCountOptions(el) {
var raw = el.getAttribute("data-au2-count");
var to = raw ? Number(raw) : NaN;
if (!Number.isFinite(to)) return null;
return {
to: to,
prefix: el.getAttribute("data-au2-prefix") || "",
suffix: el.getAttribute("data-au2-suffix") || "",
grouping: el.getAttribute("data-au2-grouping") === "true",
};
}
function renderValue(el, value, options) {
el.textContent =
options.prefix + formatValue(value, options.grouping) + options.suffix;
}
function animateValue(el, options) {
if (el.dataset.au2Counted === "true") return;
el.dataset.au2Counted = "true";
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
renderValue(el, options.to, options);
return;
}
var start = performance.now();
function tick(now) {
var progress = Math.min((now - start) / DURATION_MS, 1);
var current = Math.round(easeOutCubic(progress) * options.to);
renderValue(el, current, options);
if (progress < 1) {
requestAnimationFrame(tick);
} else {
renderValue(el, options.to, options);
}
}
renderValue(el, 0, options);
requestAnimationFrame(tick);
}
function initSection(section) {
var values = section.querySelectorAll(VALUE_SELECTOR);
if (!values.length) return;
var observer = new IntersectionObserver(
function (entries, obs) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
values.forEach(function (el) {
var options = parseCountOptions(el);
if (options) animateValue(el, options);
});
obs.unobserve(entry.target);
});
},
{ threshold: 0.35, rootMargin: "0px 0px -40px 0px" }
);
observer.observe(section);
}
function init() {
var section = document.querySelector(SECTION_SELECTOR);
if (section) initSection(section);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
+80
View File
@@ -0,0 +1,80 @@
/**
* Testimonials marquee — same logic as myfundedfutures.com Reviews section:
* - FisherYates shuffle, 3 cards per row
* - 3 rows × 3 duplicate tracks
* - Row speeds: 125s / 75s reverse / 275s (Framer Motion durations → CSS linear)
*/
(function () {
var ROW_CONFIG = [
{ duration: 125, reverse: false },
{ duration: 75, reverse: true },
{ duration: 275, reverse: false },
];
var TRACK_COUNT = 3;
var CARDS_PER_ROW = 3;
function shuffle(list) {
var arr = list.slice();
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
return arr;
}
function pickCards(cards, count) {
return shuffle(cards).slice(0, Math.min(count, cards.length));
}
function buildTrack(cards, duration, reverse, trackIndex) {
var track = document.createElement('div');
track.className =
'au2-testimonials__track' + (reverse ? ' au2-testimonials__track--reverse' : '');
track.style.setProperty('--duration', duration + 's');
if (trackIndex > 0) {
track.setAttribute('aria-hidden', 'true');
}
cards.forEach(function (card) {
track.appendChild(card.cloneNode(true));
});
return track;
}
function buildRow(cards, config) {
var row = document.createElement('div');
row.className = 'au2-testimonials__row';
var picked = pickCards(cards, CARDS_PER_ROW);
for (var i = 0; i < TRACK_COUNT; i++) {
row.appendChild(buildTrack(picked, config.duration, config.reverse, i));
}
return row;
}
function initAu2TestimonialsMarquee() {
var root = document.querySelector('[data-au2-testimonials-marquee]');
if (!root) return;
var source = root.querySelector('.au2-testimonials__source');
var rowsEl = root.querySelector('.au2-testimonials__rows');
if (!source || !rowsEl) return;
var cards = Array.prototype.slice.call(source.querySelectorAll('.au2-testimonial'));
if (!cards.length) return;
ROW_CONFIG.forEach(function (config) {
rowsEl.appendChild(buildRow(cards, config));
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAu2TestimonialsMarquee);
} else {
initAu2TestimonialsMarquee();
}
})();
+183 -1
View File
@@ -1,10 +1,183 @@
/**
* Home hero carousel — autoplay + dot navigation + mobile swipe
* Home hero carousel — autoplay + dot navigation + mobile swipe + layout sync
*
* Layout contract (extensible per banner):
* data-hero-layout-reference — slide that defines carousel height
* data-hero-layout-anchor — element whose bottom anchors dot position
* --hero-coach-dots-gap — gap between anchor bottom and dots top (default 35px)
* Mobile: .is-banner-active fixes dots to viewport bottom while banner is visible
*/
(function () {
var AUTOPLAY_MS = 5000;
var SWIPE_THRESHOLD = 48;
var MOBILE_MQ = '(max-width: 991.98px)';
var DESKTOP_MQ = '(min-width: 992px)';
function readPx(styles, name, fallback) {
var value = parseFloat(styles.getPropertyValue(name));
return Number.isFinite(value) ? value : fallback;
}
function getLayoutScope(root) {
return root.closest('#homepage') || root;
}
function clearMeasuredLayout(scope, root) {
[
'--hero-reference-content-bottom',
'--hero-reference-viewport-height',
'--hero-reference-dots-top',
'--hero-mobile-dots-reserve',
'--hero-dots-y',
].forEach(function (name) {
scope.style.removeProperty(name);
});
if (root) {
var dots = root.querySelector('.home-hero-carousel__dots');
if (dots) {
dots.removeAttribute('data-hero-dots-positioned');
}
}
}
function syncHeroLayout(root) {
var inner = root.querySelector('.home-hero-carousel__inner');
var viewport = root.querySelector('.home-hero-carousel__viewport');
if (!inner || !viewport) return;
var referenceSlide = root.querySelector('[data-hero-layout-reference]')
|| root.querySelector('[data-home-hero-slide]');
if (!referenceSlide) return;
var anchor = referenceSlide.querySelector('[data-hero-layout-anchor]')
|| referenceSlide.querySelector('.home-hero-carousel__ai-coach')
|| referenceSlide.querySelector('.home-hero-carousel__actions :last-child');
if (!anchor) return;
var scope = getLayoutScope(root);
var styles = getComputedStyle(scope);
var coachDotsGap = readPx(styles, '--hero-coach-dots-gap', 35);
var dotsHeight = readPx(styles, '--hero-dots-height', 10);
var innerPaddingBottom = readPx(styles, '--hero-inner-padding-bottom', 40);
var isDesktop = window.matchMedia(DESKTOP_MQ).matches;
if (isDesktop) {
var innerRect = inner.getBoundingClientRect();
var viewportRect = viewport.getBoundingClientRect();
var anchorRect = anchor.getBoundingClientRect();
var contentBottom = Math.ceil(anchorRect.bottom - viewportRect.top);
var viewportHeight = contentBottom + coachDotsGap;
var dotsTop = Math.ceil(anchorRect.bottom - innerRect.top + coachDotsGap);
var dotsBottomY = dotsTop + dotsHeight + innerPaddingBottom;
scope.style.setProperty('--hero-reference-content-bottom', contentBottom + 'px');
scope.style.setProperty('--hero-reference-viewport-height', viewportHeight + 'px');
scope.style.setProperty('--hero-reference-dots-top', dotsTop + 'px');
scope.style.setProperty('--hero-dots-y', dotsBottomY + 'px');
root.setAttribute('data-hero-layout-synced', 'desktop');
var dots = root.querySelector('.home-hero-carousel__dots');
if (dots) {
dots.setAttribute('data-hero-dots-positioned', 'true');
}
root.classList.remove('is-banner-active');
return;
}
clearMeasuredLayout(scope, root);
var viewportRect = viewport.getBoundingClientRect();
var slideGrid = referenceSlide.querySelector('.home-hero-carousel__slide-grid');
var visual = referenceSlide.querySelector('.home-hero-carousel__visual');
var mobileFixedDotsBottom = readPx(styles, '--hero-mobile-fixed-dots-bottom', 16);
var imageScale = readPx(styles, '--hero-visual-image-scale', 1.06);
if (!slideGrid) return;
var visualBottom = visual
? visual.getBoundingClientRect().bottom - viewportRect.top
: slideGrid.getBoundingClientRect().bottom - viewportRect.top;
if (visual) {
var img = visual.querySelector('img');
if (img && imageScale > 1) {
var imgHeight = img.getBoundingClientRect().height;
visualBottom += Math.ceil((imgHeight * imageScale - imgHeight) / 2);
}
}
var viewportHeight = Math.ceil(visualBottom);
var dotsReserve = dotsHeight + mobileFixedDotsBottom;
var homepageRect = scope.getBoundingClientRect();
var dotsBottomY = Math.ceil(
viewportRect.top - homepageRect.top + viewportHeight + dotsReserve + innerPaddingBottom
);
scope.style.setProperty('--hero-reference-viewport-height', viewportHeight + 'px');
scope.style.setProperty('--hero-mobile-dots-reserve', dotsReserve + 'px');
scope.style.setProperty('--hero-dots-y', dotsBottomY + 'px');
root.setAttribute('data-hero-layout-synced', 'mobile');
}
function bindMobileFixedDots(root) {
if (typeof IntersectionObserver === 'undefined') return;
var mobileMq = window.matchMedia(MOBILE_MQ);
var observer = new IntersectionObserver(
function (entries) {
if (!mobileMq.matches) {
root.classList.remove('is-banner-active');
return;
}
root.classList.toggle('is-banner-active', entries[0].isIntersecting);
},
{
threshold: 0,
rootMargin: '0px 0px -8% 0px',
}
);
observer.observe(root);
mobileMq.addEventListener('change', function () {
if (!mobileMq.matches) {
root.classList.remove('is-banner-active');
}
});
}
function bindHeroLayoutSync(root) {
var sync = function () {
syncHeroLayout(root);
};
sync();
if (typeof ResizeObserver !== 'undefined') {
var observer = new ResizeObserver(sync);
observer.observe(root);
var inner = root.querySelector('.home-hero-carousel__inner');
var referenceSlide = root.querySelector('[data-hero-layout-reference]')
|| root.querySelector('[data-home-hero-slide]');
if (inner) observer.observe(inner);
if (referenceSlide) observer.observe(referenceSlide);
var visual = referenceSlide.querySelector('.home-hero-carousel__visual');
if (visual) observer.observe(visual);
}
var resizeTimer;
window.addEventListener('resize', function () {
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(sync, 100);
});
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(sync);
}
}
function initHomeHeroCarousel(root) {
if (root.getAttribute('data-carousel-ready') === 'true') return;
@@ -133,6 +306,8 @@
});
bindTouchSwipe();
bindHeroLayoutSync(root);
bindMobileFixedDots(root);
setSlide(0);
startAutoplay();
}
@@ -142,6 +317,13 @@
}
window.AifoHomeHeroCarouselInit = initHomeHeroCarousel;
window.AifoHomeHeroCarouselSyncLayout = function (root) {
if (root) {
syncHeroLayout(root);
return;
}
document.querySelectorAll('[data-home-hero-carousel]').forEach(syncHeroLayout);
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
+51
View File
@@ -6,6 +6,12 @@
var NAV_URL = "/components/nav.html";
var FOOTER_URL = "/components/footer.html";
var FOOT_CSS_ID = "aifo-foot-styles";
var NAV_CSS_ID = "aifo-nav-styles";
var NAV_CSS_URL = "/assets/css/nav.css";
var AIFO_BTN_CSS_ID = "aifo-btn-styles";
var AIFO_BTN_CSS_URL = "/assets/css/aifo-btn.css";
var ICONFONT_CSS_ID = "aifo-iconfont-styles";
var ICONFONT_CSS_URL = "/assets/vendor/icon/iconfont.css";
var FLAG_ICONS_CSS_ID = "aifo-flag-icons-styles";
var FLAG_ICONS_CSS_URL = "/assets/flagicon/css/flag-icons.min.css";
var FOOT_JS_URL = "/js/footer.js";
@@ -114,6 +120,30 @@
document.head.appendChild(link);
}
function ensureNavStyles() {
if (!document.getElementById(ICONFONT_CSS_ID)) {
var iconLink = document.createElement("link");
iconLink.id = ICONFONT_CSS_ID;
iconLink.rel = "stylesheet";
iconLink.href = ICONFONT_CSS_URL;
document.head.appendChild(iconLink);
}
if (!document.getElementById(NAV_CSS_ID)) {
var navLink = document.createElement("link");
navLink.id = NAV_CSS_ID;
navLink.rel = "stylesheet";
navLink.href = NAV_CSS_URL;
document.head.appendChild(navLink);
}
if (!document.getElementById(AIFO_BTN_CSS_ID)) {
var btnLink = document.createElement("link");
btnLink.id = AIFO_BTN_CSS_ID;
btnLink.rel = "stylesheet";
btnLink.href = AIFO_BTN_CSS_URL;
document.head.appendChild(btnLink);
}
}
function loadFooterScript(callback) {
if (footJsLoaded) {
callback();
@@ -139,6 +169,25 @@
document.body.appendChild(script);
}
var navIconsJsLoading = false;
var navIconsJsLoaded = false;
function loadNavIconsScript() {
if (navIconsJsLoaded || navIconsJsLoading) return;
navIconsJsLoading = true;
var script = document.createElement("script");
script.src = "/js/nav-icons.js";
script.onload = function () {
navIconsJsLoaded = true;
navIconsJsLoading = false;
};
script.onerror = function () {
navIconsJsLoading = false;
console.error("[layout.js] Failed to load /js/nav-icons.js");
};
document.body.appendChild(script);
}
function initFooterModule(root) {
ensureFooterStyles();
updateCopyrightYear(root);
@@ -165,9 +214,11 @@
var navHtml = parts[0];
var footerHtml = parts[1];
if (navHtml) {
ensureNavStyles();
ensureFlagIconsStyles();
var navRoot = injectHtml("nav-container", navHtml);
if (navRoot) highlightActiveNav(navRoot);
loadNavIconsScript();
}
if (footerHtml) {
var footerRoot = injectHtml("footer-container", footerHtml);
+36
View File
@@ -0,0 +1,36 @@
/**
* Nav submenu iconfont helpers.
* Registry: data/nav-icon-registry.json
*
* After uploading new glyphs to iconfont.cn and replacing iconfont.css:
* window.aifoApplyNavIconTargets()
* This swaps data-nav-icon-target classes onto each submenu icon.
*/
(function () {
function applyNavIconTargets(root) {
if (!root) return 0;
var nodes = root.querySelectorAll("[data-nav-icon-target]");
var count = 0;
nodes.forEach(function (el) {
var target = el.getAttribute("data-nav-icon-target");
if (!target) return;
el.className = "icon iconfont " + target;
count += 1;
});
return count;
}
window.aifoApplyNavIconTargets = function () {
var nav = document.getElementById("nav");
var container = document.getElementById("nav-container");
var count = applyNavIconTargets(nav) + applyNavIconTargets(container);
console.info("[nav-icons] Applied " + count + " target icon class(es).");
return count;
};
document.addEventListener("aifo:layout-ready", function () {
if (window.AIFO_NAV_USE_TARGET_ICONS === true) {
window.aifoApplyNavIconTargets();
}
});
})();
+8 -8
View File
@@ -1,22 +1,22 @@
/**
* Partnership page — FAQ accordion.
* Partnership page — FAQ accordion (Figma export style).
*/
(function () {
function initFaq(root) {
if (!root) return;
root.querySelectorAll(".faq-header").forEach(function (header) {
root.querySelectorAll(".partnership-faq__header").forEach(function (header) {
header.addEventListener("click", function () {
var item = header.closest(".faq-item");
var item = header.closest(".partnership-faq__item");
if (!item) return;
var isActive = item.classList.contains("active");
root.querySelectorAll(".faq-item").forEach(function (el) {
el.classList.remove("active");
var isOpen = item.classList.contains("is-open");
root.querySelectorAll(".partnership-faq__item").forEach(function (el) {
el.classList.remove("is-open");
});
if (!isActive) {
item.classList.add("active");
if (!isOpen) {
item.classList.add("is-open");
}
});
});
+30
View File
@@ -0,0 +1,30 @@
/**
* Loads support-cta.section.html into #support-cta-module
*/
(function () {
var MODULE_URL = '/pages/modules/support-cta.section.html';
function loadSupportCtaModule() {
var container = document.getElementById('support-cta-module');
if (!container) return;
fetch(MODULE_URL, { credentials: 'same-origin' })
.then(function (res) {
if (!res.ok) throw new Error('Failed to load ' + MODULE_URL);
return res.text();
})
.then(function (html) {
container.innerHTML = html;
document.dispatchEvent(new CustomEvent('aifo:support-cta-ready'));
})
.catch(function (err) {
console.error('[support-cta-module.js]', err);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadSupportCtaModule);
} else {
loadSupportCtaModule();
}
})();