feat: liquid glass UI, blob background, redesign home/portfolio/about pages
- 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
This commit is contained in:
181
src/scripts/home.js
Normal file
181
src/scripts/home.js
Normal file
@@ -0,0 +1,181 @@
|
||||
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');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user