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:
Kunthawat Greethong
2026-06-23 11:40:37 +07:00
parent e279119f97
commit f827afb33f
188 changed files with 4577 additions and 15483 deletions

181
src/scripts/home.js Normal file
View 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');
}
});