- Liquid glass effect on navbar/cards with backdrop-filter invert - Animated blob gradient background (SVG-based) - Portfolio section: scene-dark invert, show 5 items on home - How Work section: step flow with numbers + connecting lines - Hero: Decision snapshot replacing problem selector - About page: inverted background with contrast fixes - Fix parallax JS bundling via Astro - Fix navbar fixed positioning after liquid glass CSS - Submenu hover fix - Clean up removed legacy files/assets
182 lines
7.8 KiB
JavaScript
182 lines
7.8 KiB
JavaScript
const root = document.documentElement;
|
|
const body = document.body;
|
|
const nav = document.querySelector('[data-nav]');
|
|
const navToggle = document.querySelector('[data-nav-toggle]');
|
|
const navMenu = document.querySelector('[data-nav-menu]');
|
|
const serviceToggle = document.querySelector('[data-service-toggle]');
|
|
const serviceWrap = serviceToggle?.closest('.nav-service');
|
|
const panel = document.querySelector('[data-lead-panel]');
|
|
const backdrop = document.querySelector('[data-panel-backdrop]');
|
|
const form = document.querySelector('[data-lead-form]');
|
|
const statusEl = document.querySelector('[data-form-status]');
|
|
const floatingCta = document.querySelector('[data-floating-cta]');
|
|
const openButtons = document.querySelectorAll('[data-open-lead]');
|
|
const closeButtons = document.querySelectorAll('[data-close-lead]');
|
|
|
|
const diagnosisByProblem = {
|
|
ads_not_worth_it: 'จากปัญหาที่เลือก เราน่าจะเริ่มจากการดูข้อมูลแอด กลุ่มเป้าหมาย และคุณภาพลูกค้าที่ทักเข้ามาก่อน',
|
|
wrong_leads: 'จากปัญหาที่เลือก เราน่าจะเริ่มจากการดูข้อมูลแอด กลุ่มเป้าหมาย และคุณภาพลูกค้าที่ทักเข้ามาก่อน',
|
|
website_no_leads: 'จากปัญหาที่เลือก เราน่าจะเริ่มจากการดูเว็บ เส้นทางลูกค้า และจุดที่ควรชวนให้ติดต่อก่อน',
|
|
slow_or_error_work: 'จากปัญหาที่เลือก เราน่าจะเริ่มจากขั้นตอนทำงานซ้ำ จุดที่ช้า และจุดที่ผิดพลาดบ่อยก่อน',
|
|
ai_not_sure: 'จากปัญหาที่เลือก เราน่าจะเริ่มจากงานจริงของทีม แล้วค่อยเลือกจุดที่ AI ช่วยได้อย่างเหมาะสม',
|
|
not_sure: 'ได้รับโจทย์แล้ว เราจะเริ่มจากการทำความเข้าใจธุรกิจและข้อมูลที่มีอยู่ก่อน',
|
|
};
|
|
|
|
function setStatus(message, tone = '') {
|
|
if (!statusEl) return;
|
|
statusEl.textContent = message;
|
|
statusEl.dataset.tone = tone;
|
|
}
|
|
|
|
function openPanel() {
|
|
body.classList.add('panel-open');
|
|
panel?.setAttribute('aria-hidden', 'false');
|
|
window.setTimeout(() => {
|
|
panel?.querySelector('input, textarea, button')?.focus();
|
|
}, 80);
|
|
}
|
|
|
|
function closePanel() {
|
|
body.classList.remove('panel-open');
|
|
panel?.setAttribute('aria-hidden', 'true');
|
|
}
|
|
|
|
openButtons.forEach((button) => button.addEventListener('click', openPanel));
|
|
closeButtons.forEach((button) => button.addEventListener('click', closePanel));
|
|
backdrop?.addEventListener('click', closePanel);
|
|
|
|
window.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Escape') {
|
|
closePanel();
|
|
serviceWrap?.classList.remove('is-open');
|
|
}
|
|
});
|
|
|
|
navToggle?.addEventListener('click', () => {
|
|
const isOpen = nav?.classList.toggle('is-open');
|
|
navToggle.setAttribute('aria-expanded', String(Boolean(isOpen)));
|
|
});
|
|
|
|
serviceToggle?.addEventListener('click', () => {
|
|
const isOpen = serviceWrap?.classList.toggle('is-open');
|
|
serviceToggle.setAttribute('aria-expanded', String(Boolean(isOpen)));
|
|
});
|
|
|
|
navMenu?.addEventListener('click', (event) => {
|
|
const target = event.target;
|
|
if (target instanceof HTMLAnchorElement) {
|
|
nav?.classList.remove('is-open');
|
|
navToggle?.setAttribute('aria-expanded', 'false');
|
|
}
|
|
});
|
|
|
|
let ticking = false;
|
|
|
|
function updateScrollState() {
|
|
const scrolled = window.scrollY > 28;
|
|
const pastHero = window.scrollY > window.innerHeight * 0.62;
|
|
const heroProgress = Math.min(Math.max(window.scrollY / Math.max(window.innerHeight, 1), 0), 1);
|
|
const maxScroll = Math.max(document.documentElement.scrollHeight - window.innerHeight, 1);
|
|
const pageProgress = Math.min(Math.max(window.scrollY / maxScroll, 0), 1);
|
|
const navProbeY = (nav?.getBoundingClientRect().bottom || 80) + 24;
|
|
const sceneAtNav = document.elementFromPoint(window.innerWidth / 2, navProbeY)?.closest('[data-scene]');
|
|
const isOverDark = sceneAtNav?.dataset.scene === 'dark';
|
|
nav?.classList.toggle('is-scrolled', scrolled);
|
|
nav?.classList.toggle('is-over-dark', isOverDark);
|
|
floatingCta?.classList.toggle('is-visible', pastHero);
|
|
root.style.setProperty('--scroll', heroProgress.toFixed(4));
|
|
root.style.setProperty('--page-scroll', pageProgress.toFixed(4));
|
|
root.style.setProperty('--scroll-y', `${Math.round(window.scrollY)}px`);
|
|
ticking = false;
|
|
}
|
|
|
|
function requestScrollUpdate() {
|
|
if (!ticking) {
|
|
ticking = true;
|
|
window.requestAnimationFrame(updateScrollState);
|
|
}
|
|
}
|
|
|
|
window.addEventListener('scroll', requestScrollUpdate, { passive: true });
|
|
window.addEventListener('resize', requestScrollUpdate, { passive: true });
|
|
updateScrollState();
|
|
|
|
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
let pointerFrame = 0;
|
|
window.addEventListener('pointermove', (event) => {
|
|
if (pointerFrame) return;
|
|
const { clientX, clientY } = event;
|
|
pointerFrame = window.requestAnimationFrame(() => {
|
|
const mx = (clientX / window.innerWidth - 0.5) * 2;
|
|
const my = (clientY / window.innerHeight - 0.5) * 2;
|
|
root.style.setProperty('--mx', mx.toFixed(4));
|
|
root.style.setProperty('--my', my.toFixed(4));
|
|
pointerFrame = 0;
|
|
});
|
|
}, { passive: true });
|
|
}
|
|
|
|
form?.addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
|
|
const data = new FormData(form);
|
|
const name = String(data.get('name') || '').trim();
|
|
const phone = String(data.get('phone') || '').trim();
|
|
const email = String(data.get('email') || '').trim();
|
|
const problems = data.getAll('problems').map(String);
|
|
const message = String(data.get('message') || '').trim();
|
|
const endpoint = panel?.dataset.endpoint || '';
|
|
|
|
if (!name) {
|
|
setStatus('กรุณาใส่ชื่อก่อนส่ง', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!phone && !email) {
|
|
setStatus('ใส่เบอร์โทรหรืออีเมลอย่างใดอย่างหนึ่งก็ได้', 'error');
|
|
return;
|
|
}
|
|
|
|
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
setStatus('รูปแบบอีเมลไม่ถูกต้อง', 'error');
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
name,
|
|
phone,
|
|
email,
|
|
problems,
|
|
message,
|
|
pageUrl: window.location.href,
|
|
userAgent: navigator.userAgent,
|
|
website: String(data.get('website') || ''),
|
|
};
|
|
|
|
const diagnosis = problems.map((key) => diagnosisByProblem[key]).find(Boolean)
|
|
|| 'ได้รับโจทย์แล้ว เราจะเริ่มจากการทำความเข้าใจธุรกิจและข้อมูลที่มีอยู่ก่อน';
|
|
|
|
if (!endpoint) {
|
|
setStatus('ฟอร์มพร้อมแล้ว เหลือใส่ Google Apps Script Web App URL ก่อนใช้งานจริง', 'error');
|
|
return;
|
|
}
|
|
|
|
setStatus('กำลังส่งโจทย์...', '');
|
|
|
|
try {
|
|
await fetch(endpoint, {
|
|
method: 'POST',
|
|
mode: 'no-cors',
|
|
headers: {
|
|
'Content-Type': 'text/plain;charset=utf-8',
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
form.reset();
|
|
setStatus(`ได้รับโจทย์แล้ว ${diagnosis} เราจะติดต่อกลับทางเบอร์หรืออีเมลที่ให้ไว้`, 'success');
|
|
} catch (error) {
|
|
setStatus('ส่งไม่สำเร็จ กรุณาลองใหม่อีกครั้ง', 'error');
|
|
}
|
|
});
|