266 lines
19 KiB
JavaScript
266 lines
19 KiB
JavaScript
const menuButton = document.querySelector('.menu-toggle');
|
||
const nav = document.querySelector('.nav');
|
||
const header = document.querySelector('.site-header');
|
||
const scrollProgress = document.querySelector('.scroll-progress');
|
||
|
||
const EN_TO_ZH = {
|
||
'Opening October 2027': '预计 2027 年 10 月开业',
|
||
'Old Klang Road, Kuala Lumpur': '吉隆坡旧巴生路',
|
||
'Venue': '场馆介绍', 'Courts': '球场', 'Spaces': '空间规划', 'Academy': '羽球学院', 'Community': '社区活动', 'Contact': '联系我们',
|
||
'Register interest': '登记意向', 'Register interest →': '发送邮件咨询 →',
|
||
"Kuala Lumpur's new court-sports destination": '吉隆坡全新综合球类运动目的地',
|
||
'Your court.': '你的球场。', 'Your community.': '你的社区。',
|
||
'A purpose-built home for badminton, pickleball and tennis - created for players, families, juniors and teams.': '为羽毛球、匹克球与网球量身打造,为球员、家庭、青少年和团队提供共同的运动主场。',
|
||
'Explore academy': '了解学院', 'Three sports': '三种运动', 'One home': '一个主场',
|
||
'Grand opening': '预计开业', 'October 2027': '2027 年 10 月',
|
||
'Indoor': '室内', 'badminton courts': '羽毛球场', 'pickleball courts': '匹克球场', 'tennis courts': '网球场',
|
||
'Professional spaces.': '专业运动空间。', 'Designed for everyone.': '为每一个人打造。',
|
||
'The venue': '场馆介绍', 'Built around how': '真正围绕', 'people': '人们的', 'really play.': '运动方式打造。',
|
||
"A modern indoor sports centre planned for one of Kuala Lumpur's most established and connected neighbourhoods.": '一座规划于吉隆坡成熟、交通便利社区中的现代室内体育中心。',
|
||
'From casual evening games to focused academy training, every floor brings courts, support spaces and community amenities together in one accessible destination.': '从轻松的晚间约球到系统的学院训练,每一层都把球场、辅助空间与社区配套集中在同一个便捷目的地。',
|
||
'Entrance perspective': '入口效果图', 'Front elevation': '建筑正立面', 'Street context': '街景效果图',
|
||
'Inside the venue': '走进场馆', 'Step inside the': '走进', 'game.': '运动现场。',
|
||
'Explore the indoor court environments referenced in the proposal - spaces planned for everyday play, coaching and competition.': '查看提案中的场内环境,为日常约球、专业训练和赛事活动规划的运动空间。',
|
||
'Indoor badminton courts': '室内羽毛球场', 'Pickleball courts': '匹克球场', 'Indoor tennis courts': '室内网球场', 'Indoor multi-court space': '室内多用途球场', 'Outdoor tennis courts': '室外网球场',
|
||
'Three ways to move': '三种运动方式', 'Choose your': '选择你的', 'court.': '球场。',
|
||
'Dedicated indoor spaces planned for year-round play, training and competition.': '为全年运动、训练和比赛规划的专业室内空间。',
|
||
'14 courts': '14 个球场', '5 courts': '5 个球场', '4 courts': '4 个球场',
|
||
'01 / Badminton': '01 / 羽毛球', '02 / Pickleball': '02 / 匹克球', '03 / Tennis': '03 / 网球',
|
||
'Fast. Focused.': '快速。专注。', 'Made for Malaysia.': '为马来西亚而生。',
|
||
'Fourteen indoor courts form the heart of the venue, supporting everyday bookings, academy programmes and tournament play.': '14 个室内羽毛球场构成场馆核心,满足日常预约、学院课程及赛事活动。',
|
||
'Register for court updates →': '登记球场资讯 →',
|
||
'Social by nature.': '天生适合社交。', 'Serious when you are.': '也能认真竞技。',
|
||
'Five indoor pickleball courts - convertible for badminton - give new players and regular groups room to play their way.': '5 个室内匹克球场可转换为羽毛球场,让新手与固定球友都能自在开打。',
|
||
'Join the community →': '加入社区 →',
|
||
'More court time.': '更多上场时间。', 'Whatever the weather.': '无惧天气变化。',
|
||
'Four indoor tennis courts provide a dependable setting for lessons, friendly matches and performance sessions.': '4 个室内网球场为课程、友谊赛和进阶训练提供稳定环境。',
|
||
'Ask about tennis →': '咨询网球场 →',
|
||
'More than a court': '不只是球场', 'Everything your game needs,': '运动所需的一切,', 'under one roof.': '都在同一屋檐下。',
|
||
'Reception & waiting area': '前台与等候区', 'Badminton pro shop': '羽毛球用品店', 'Changing rooms': '更衣室', 'Fitness gym': '健身房', 'Meeting room': '会议室', 'Lounge & food bar': '休息室与餐饮吧', 'Court separation nets': '球场隔离网', 'Fire-safety access': '消防通道',
|
||
'Space planning': '空间规划', 'A destination built': '逐层规划的', 'across every level.': '综合运动目的地。',
|
||
'Ground': '底层', 'Arrival & first courts': '接待与首层球场', 'Level 1': '一楼', 'Play, train & recharge': '运动、训练与休息', 'Level 2': '二楼', 'Expanded court capacity': '扩展球场容量', 'Upper level': '上层', 'The full venue programme': '完整场馆布局',
|
||
'Arrival, reception, waiting area, pro shop, changing rooms, badminton courts and tennis courts.': '设置场馆入口、前台、等候区、用品店、更衣室、羽毛球场与网球场。',
|
||
'Badminton and pickleball courts supported by changing rooms, meeting room, fitness gym, lounge and food bar.': '羽毛球与匹克球场配套更衣室、会议室、健身房、休息室与餐饮吧。',
|
||
'Additional badminton, pickleball and tennis capacity with dedicated circulation and court separation.': '增加羽毛球、匹克球与网球场地,并配置独立动线与球场隔离。',
|
||
"The upper-level court plan completes the building's multi-sport programme and maximises playable space.": '上层球场完善整栋建筑的综合运动布局,并最大化可运动空间。',
|
||
'Simple court booking': '轻松预约球场', 'From “let\'s play”': '从“约场球吧”', 'to': '到', 'court confirmed.': '确认球场。',
|
||
'Choose your sport': '选择运动项目', 'Select your area': '选择地区', 'Pick date & time': '选择日期与时间', 'Confirm your court': '确认时间与场地', 'Pay online': '线上付款',
|
||
'The digital booking experience is planned to make court access easy, anytime and anywhere.': '数字预约流程让用户随时随地轻松预订球场。',
|
||
'JUN Badminton Academy': 'JUN 羽毛球学院', 'Skills for the court.': '练就球场技能。', 'Character for life.': '塑造终身品格。',
|
||
'Tailored programmes for ages 4 to 50, from beginners taking their first steps to advanced players preparing for competition.': '为 4 至 50 岁不同水平的学员提供定制课程,从初学入门到进阶备赛。',
|
||
'Kids & adult group classes': '儿童与成人团体课程', 'Private one-to-one training': '私人一对一训练', 'Singles & doubles programmes': '单打与双打课程', 'Corporate & school training': '企业与学校训练',
|
||
'Enquire about training →': '咨询训练课程 →', 'Founder & head coach': '创办人兼主教练', 'Former Kedah state player': '前吉打州代表队球员', 'BAM Level 1-certified coach': '马来西亚羽总一级认证教练',
|
||
'Innovative drills': '创新训练', 'Fitness programmes': '体能课程', 'Mental conditioning': '心理训练', 'Local competitions': '本地赛事',
|
||
'A venue with a pulse': '充满活力的场馆', 'Play here.': '在这里运动。', 'Belong here.': '在这里找到归属。',
|
||
'The proposal is built around more than bookings: it imagines an active, connected local sports community.': '项目不只关注预约,更希望打造一个活跃、紧密连接的本地运动社区。',
|
||
'Community events': '社区活动', 'Regular activities that bring neighbours, families and players together.': '通过定期活动连接邻里、家庭和球友。',
|
||
'Seasonal programmes': '季节性活动', 'Campaigns, clinics and formats that keep the calendar fresh year-round.': '以主题推广、训练营和多种赛制丰富全年活动。',
|
||
'Leagues & competition': '联赛与竞技', 'Pathways to play casually, test your level and compete with confidence.': '从休闲约球、水平测试到正式竞技,提供清晰进阶路径。',
|
||
'Business partnerships': '商业合作', 'Space for corporate play, school programmes and local collaborations.': '承接企业活动、学校课程及本地合作项目。',
|
||
'Old Klang Road / 旧巴生路': '旧巴生路 / Old Klang Road', 'Connected to the city.': '连接城市。', 'Rooted in the neighbourhood.': '扎根社区。',
|
||
"One of Kuala Lumpur's oldest major corridors, Old Klang Road sits between the city and Petaling Jaya, surrounded by mature residential and commercial communities.": '旧巴生路是吉隆坡历史悠久的重要道路之一,连接市区与八打灵再也,周边拥有成熟住宅和商业社区。',
|
||
'Links to Federal Highway, NPE and LDP': '连接联邦大道、NPE 与 LDP', 'A multicultural neighbourhood with a strong local food culture': '多元族群共融,并拥有丰富本地美食文化', 'Accessible to families, students, residents and corporate groups': '方便家庭、学生、居民与企业团队到访',
|
||
'Road to opening': '开业进程', 'Building towards': '稳步迈向', 'Planning permission': '规划许可', 'Earthwork & building plans': '土方与建筑计划', 'Construction': '场馆施工', 'Completion & handover': '完工审批与交付', 'Grand opening': '正式开业',
|
||
'Four-month planning-permission phase within the wider project programme.': '项目规划中的四个月规划许可申请阶段。', 'Approval work for earthworks and the building plan.': '完成土方工程与建筑计划审批。', 'Six months planned for venue construction.': '预计用六个月完成场馆施工。', 'CCC approval, completion and handover.': '完成合规证书审批、完工与交付。', 'The first serves, rallies and community games begin.': '迎来首场发球、对打与社区活动。',
|
||
'Get on court early': '抢先上场', 'Be the first to play.': '成为首批球友。', 'Be part of the community.': '加入我们的社区。', 'Register for opening updates, academy news and early booking information.': '登记获取开业动态、学院资讯与优先预约信息。',
|
||
'Choose the channel that works best for you. Every contact detail can be copied individually.': '选择最方便的联系方式,每条信息都可以单独复制。', 'Training programmes': '训练课程', 'Court enquiries': '球场咨询', 'Corporate & school sessions': '企业与学校课程', 'View contact details →': '查看联系方式 →',
|
||
'Contact JUN Academy': '联系 JUN 学院', 'Choose a channel.': '选择联系方式。', 'Copy and connect.': '复制后自行联系。', 'Copy any contact detail below, then reach out using the app you prefer.': '单独复制下方任意联系信息,再使用你常用的应用与我们联系。',
|
||
'Phone': '电话', 'Email': '邮箱', 'Xiaohongshu': '小红书', 'Address': '地址', 'Copy': '复制',
|
||
'Your name': '姓名', 'Email address': '电子邮箱', "I'm interested in": '感兴趣的内容', 'Name': '姓名',
|
||
'Court bookings': '球场预约', 'JUN Badminton Academy': 'JUN 羽毛球学院', 'Corporate or school programmes': '企业或学校课程', 'Opening updates': '开业动态',
|
||
'A new home for court sports at Old Klang Road, Kuala Lumpur.': '位于吉隆坡旧巴生路的全新球类运动主场。', 'JUN Academy': 'JUN 学院', 'Explore': '网站导航', 'Location': '场馆位置',
|
||
'Ground Floor, Jalan Perdana 1,': '底层,Jalan Perdana 1,', 'Taman Segar Perdana, 43200 Cheras, Selangor': 'Taman Segar Perdana,43200 蕉赖,雪兰莪',
|
||
'Proposed venue information based on the current project proposal. Plans and opening schedule are subject to final approvals.': '场馆资料来自当前项目提案,具体规划与开业时间以最终审批为准。'
|
||
};
|
||
|
||
const ORIGINAL_TEXT = new WeakMap();
|
||
|
||
function translateTextNodes(isChinese) {
|
||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
||
const nodes = [];
|
||
while (walker.nextNode()) nodes.push(walker.currentNode);
|
||
nodes.forEach((node) => {
|
||
if (['SCRIPT', 'STYLE'].includes(node.parentElement?.tagName)) return;
|
||
if (node.parentElement?.closest('.footer-contact') || node.parentElement?.closest('.contact-value')) return;
|
||
if (!isChinese) {
|
||
if (ORIGINAL_TEXT.has(node)) node.nodeValue = ORIGINAL_TEXT.get(node);
|
||
return;
|
||
}
|
||
if (!ORIGINAL_TEXT.has(node)) ORIGINAL_TEXT.set(node, node.nodeValue);
|
||
const original = ORIGINAL_TEXT.get(node);
|
||
const value = original.trim();
|
||
if (value && EN_TO_ZH[value]) node.nodeValue = original.replace(value, EN_TO_ZH[value]);
|
||
});
|
||
}
|
||
|
||
function setLanguage(language) {
|
||
const isChinese = language === 'zh';
|
||
translateTextNodes(isChinese);
|
||
document.documentElement.lang = isChinese ? 'zh-CN' : 'en';
|
||
document.querySelectorAll('.language-toggle').forEach((button) => {
|
||
button.textContent = isChinese ? 'EN' : '中文';
|
||
button.setAttribute('aria-label', isChinese ? 'Switch to English' : '切换为中文');
|
||
});
|
||
const nameInput = document.querySelector('input[name="name"]');
|
||
if (nameInput) nameInput.placeholder = isChinese ? '姓名' : 'Name';
|
||
const activePlanCopy = document.querySelector('.plan-tab.active')?.dataset.copy;
|
||
const planDescription = document.querySelector('#plan-copy');
|
||
if (activePlanCopy && planDescription) planDescription.textContent = isChinese ? (EN_TO_ZH[activePlanCopy] || activePlanCopy) : activePlanCopy;
|
||
const formStatus = document.querySelector('.form-status');
|
||
if (formStatus) formStatus.textContent = '';
|
||
localStorage.setItem('site-language', language);
|
||
}
|
||
|
||
document.querySelectorAll('.language-toggle').forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
setLanguage(document.documentElement.lang.startsWith('zh') ? 'en' : 'zh');
|
||
});
|
||
});
|
||
|
||
setLanguage(localStorage.getItem('site-language') === 'zh' ? 'zh' : 'en');
|
||
|
||
menuButton?.addEventListener('click', () => {
|
||
const open = nav.classList.toggle('open');
|
||
menuButton.setAttribute('aria-expanded', String(open));
|
||
});
|
||
|
||
nav?.querySelectorAll('a').forEach((link) => {
|
||
link.addEventListener('click', () => {
|
||
nav.classList.remove('open');
|
||
menuButton?.setAttribute('aria-expanded', 'false');
|
||
});
|
||
});
|
||
|
||
let headerAnchor = header?.offsetTop ?? 0;
|
||
window.addEventListener('scroll', () => {
|
||
if (!header) return;
|
||
header.classList.toggle('is-sticky', window.scrollY > headerAnchor + 160);
|
||
document.body.style.paddingTop = header.classList.contains('is-sticky') ? `${header.offsetHeight}px` : '0';
|
||
const scrollable = document.documentElement.scrollHeight - window.innerHeight;
|
||
const progress = scrollable > 0 ? Math.min(1, Math.max(0, window.scrollY / scrollable)) : 0;
|
||
if (scrollProgress) scrollProgress.style.transform = `scaleX(${progress})`;
|
||
}, { passive: true });
|
||
|
||
const planImage = document.querySelector('#plan-image');
|
||
const planCopy = document.querySelector('#plan-copy');
|
||
document.querySelectorAll('.plan-tab').forEach((tab) => {
|
||
tab.addEventListener('click', () => {
|
||
document.querySelectorAll('.plan-tab').forEach((item) => {
|
||
item.classList.remove('active');
|
||
item.setAttribute('aria-selected', 'false');
|
||
});
|
||
tab.classList.add('active');
|
||
tab.setAttribute('aria-selected', 'true');
|
||
planImage.style.opacity = '0';
|
||
setTimeout(() => {
|
||
planImage.src = tab.dataset.plan;
|
||
planImage.alt = `${tab.querySelector('strong').textContent} venue plan`;
|
||
const planText = tab.dataset.copy;
|
||
planCopy.textContent = document.documentElement.lang.startsWith('zh') ? (EN_TO_ZH[planText] || planText) : planText;
|
||
planImage.style.opacity = '1';
|
||
}, 160);
|
||
});
|
||
});
|
||
|
||
const lightbox = document.querySelector('#lightbox');
|
||
const lightboxImage = lightbox?.querySelector('img');
|
||
document.querySelectorAll('.gallery-item').forEach((item) => {
|
||
item.addEventListener('click', () => {
|
||
lightboxImage.src = item.dataset.image;
|
||
lightbox.showModal();
|
||
});
|
||
});
|
||
lightbox?.querySelector('button').addEventListener('click', () => lightbox.close());
|
||
lightbox?.addEventListener('click', (event) => {
|
||
if (event.target === lightbox) lightbox.close();
|
||
});
|
||
|
||
const revealObserver = new IntersectionObserver((entries) => {
|
||
entries.forEach((entry) => {
|
||
if (entry.isIntersecting) {
|
||
entry.target.classList.add('visible');
|
||
revealObserver.unobserve(entry.target);
|
||
}
|
||
});
|
||
}, { threshold: 0.08, rootMargin: '0px 0px -30px' });
|
||
document.querySelectorAll('.reveal').forEach((element) => revealObserver.observe(element));
|
||
|
||
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
const countSection = document.querySelector('.court-counts');
|
||
const countNumbers = [...document.querySelectorAll('.court-count strong')];
|
||
if (countSection && !reducedMotion) {
|
||
const countObserver = new IntersectionObserver((entries) => {
|
||
if (!entries.some((entry) => entry.isIntersecting)) return;
|
||
countNumbers.forEach((number) => {
|
||
const target = Number(number.textContent);
|
||
const start = performance.now();
|
||
const duration = 850;
|
||
number.classList.add('counting');
|
||
const update = (now) => {
|
||
const elapsed = Math.min(1, (now - start) / duration);
|
||
const eased = 1 - Math.pow(1 - elapsed, 3);
|
||
number.textContent = String(Math.round(target * eased));
|
||
if (elapsed < 1) requestAnimationFrame(update);
|
||
else number.classList.remove('counting');
|
||
};
|
||
requestAnimationFrame(update);
|
||
});
|
||
countObserver.disconnect();
|
||
}, { threshold: .35 });
|
||
countObserver.observe(countSection);
|
||
}
|
||
|
||
const contactModal = document.querySelector('#contact-modal');
|
||
const copyStatus = contactModal?.querySelector('.copy-status');
|
||
|
||
document.querySelectorAll('.contact-modal-trigger').forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
if (!contactModal?.open) contactModal.showModal();
|
||
});
|
||
});
|
||
|
||
contactModal?.querySelector('.modal-close').addEventListener('click', () => contactModal.close());
|
||
contactModal?.addEventListener('click', (event) => {
|
||
if (event.target === contactModal) contactModal.close();
|
||
});
|
||
|
||
async function copyContactText(text) {
|
||
if (navigator.clipboard?.writeText) {
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
return true;
|
||
} catch (_) {
|
||
// Fall through to the file:// compatible copy method.
|
||
}
|
||
}
|
||
const field = document.createElement('textarea');
|
||
field.value = text;
|
||
field.setAttribute('readonly', '');
|
||
field.style.position = 'fixed';
|
||
field.style.opacity = '0';
|
||
document.body.appendChild(field);
|
||
field.select();
|
||
const copied = document.execCommand('copy');
|
||
field.remove();
|
||
return copied;
|
||
}
|
||
|
||
document.querySelectorAll('.copy-contact').forEach((button) => {
|
||
button.addEventListener('click', async () => {
|
||
const copied = await copyContactText(button.dataset.copy);
|
||
const isChinese = document.documentElement.lang.startsWith('zh');
|
||
if (copyStatus) {
|
||
copyStatus.textContent = copied
|
||
? (isChinese ? `已复制:${button.dataset.copy}` : `Copied: ${button.dataset.copy}`)
|
||
: (isChinese ? '复制失败,请手动选择文字复制。' : 'Copy failed. Please select and copy the text manually.');
|
||
}
|
||
if (!copied) return;
|
||
button.textContent = isChinese ? '已复制' : 'Copied';
|
||
button.classList.add('copied');
|
||
setTimeout(() => {
|
||
button.textContent = isChinese ? '复制' : 'Copy';
|
||
button.classList.remove('copied');
|
||
}, 1600);
|
||
});
|
||
});
|