/* ====================================================================== Wenty Spares & Performance -- site script Sections below: 1. Mobile nav toggle 2. Scroll reveal animations (staggered) 3. Animated stat counters 4. Card tilt + button/chip ripple effects 5. AI-style chat widget (local knowledge base, no server required) -- replies are linked to real page sections: asking about parts, the shop, testimonials or how to find us will smoothly scroll there and pulse-highlight that section, and part-related answers include a mini clickable photo gallery. ====================================================================== */ document.addEventListener('DOMContentLoaded', function () { document.getElementById('year').textContent = new Date().getFullYear(); /* ---------- 1. Mobile nav ---------- */ var navToggle = document.getElementById('navToggle'); var mainNav = document.getElementById('mainNav'); navToggle.addEventListener('click', function () { mainNav.classList.toggle('open'); }); mainNav.querySelectorAll('a').forEach(function (link) { link.addEventListener('click', function () { mainNav.classList.remove('open'); }); }); /* ---------- 2. Scroll reveal (staggered per group) ---------- */ var groupMap = new Map(); document.querySelectorAll('.reveal').forEach(function (el) { var parent = el.parentElement; var idx = groupMap.has(parent) ? groupMap.get(parent) : 0; el.style.setProperty('--reveal-delay', Math.min(idx, 6) * 0.09 + 's'); groupMap.set(parent, idx + 1); }); var revealEls = document.querySelectorAll('.reveal'); var observer = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { entry.target.classList.add('visible'); observer.unobserve(entry.target); } }); }, { threshold: 0.15 }); revealEls.forEach(function (el) { observer.observe(el); }); /* ---------- 3. Stat counters ---------- */ var statEls = document.querySelectorAll('.stat-num'); var statsObserver = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (!entry.isIntersecting) return; var el = entry.target; var target = parseInt(el.getAttribute('data-count'), 10) || 0; var current = 0; var step = Math.max(1, Math.round(target / 40)); var timer = setInterval(function () { current += step; if (current >= target) { current = target; clearInterval(timer); } el.textContent = current + (target === 5 ? '.0' : '+'); }, 30); statsObserver.unobserve(el); }); }, { threshold: 0.5 }); statEls.forEach(function (el) { statsObserver.observe(el); }); /* ---------- 4. Card tilt + ripple ---------- */ addTilt('.feature-card', 7); addTilt('.part-card', 6); addRippleTo(document.querySelectorAll('.btn, .btn-call')); /* ---------- 5. AI Chat Widget ---------- */ initChatWidget(); }); function addTilt(selector, intensity) { document.querySelectorAll(selector).forEach(function (card) { card.addEventListener('mousemove', function (e) { var rect = card.getBoundingClientRect(); var x = e.clientX - rect.left; var y = e.clientY - rect.top; var rotateY = ((x - rect.width / 2) / (rect.width / 2)) * intensity; var rotateX = -((y - rect.height / 2) / (rect.height / 2)) * intensity; card.style.transform = 'perspective(700px) rotateX(' + rotateX + 'deg) rotateY(' + rotateY + 'deg) translateY(-6px)'; }); card.addEventListener('mouseleave', function () { card.style.transform = ''; }); }); } function addRippleTo(nodeList) { nodeList.forEach(function (el) { el.addEventListener('click', spawnRipple); }); } function spawnRipple(e) { var btn = e.currentTarget; var rect = btn.getBoundingClientRect(); var size = Math.max(rect.width, rect.height); var circle = document.createElement('span'); circle.className = 'ripple'; circle.style.width = circle.style.height = size + 'px'; circle.style.left = (e.clientX - rect.left - size / 2) + 'px'; circle.style.top = (e.clientY - rect.top - size / 2) + 'px'; btn.appendChild(circle); setTimeout(function () { circle.remove(); }, 650); } /* Smoothly scroll to a page section and pulse-highlight it -- this is what links the chat widget's answers to the actual site content. */ function jumpToSection(id) { var el = document.getElementById(id); if (!el) return; el.scrollIntoView({ behavior: 'smooth', block: 'start' }); el.classList.remove('chat-highlight'); void el.offsetWidth; el.classList.add('chat-highlight'); setTimeout(function () { el.classList.remove('chat-highlight'); }, 1700); } function initChatWidget() { var widget = document.getElementById('chatWidget'); var toggleBtn = document.getElementById('chatToggle'); var messagesEl = document.getElementById('chatMessages'); var suggestionsEl = document.getElementById('chatSuggestions'); var form = document.getElementById('chatForm'); var input = document.getElementById('chatInput'); /* ---- Optional: connect a real AI provider later ---- Leave USE_REMOTE false to keep the built-in local assistant (recommended for a static site with no backend -- a key placed directly in this file would be visible to anyone who views source). If you later add a small server-side proxy, set USE_REMOTE = true and point REMOTE_ENDPOINT at your proxy endpoint. */ var CONFIG = { USE_REMOTE: false, REMOTE_ENDPOINT: '' }; var BUSINESS = { name: 'Wenty Spares & Performance', phone: '02 9636 7129', phoneHref: 'tel:+61296367129', address: '1/53 Station St, Wentworthville NSW 2145, Australia', mapsUrl: 'https://maps.google.com/maps?q=1/53+Station+St,+Wentworthville+NSW+2145,+Australia', facebook: 'https://www.facebook.com/profile.php?id=100057487808229', instagram: 'https://www.instagram.com/yaksmotorsport/', founded: 2008 }; var PART_THUMBS = [ 'https://wentyspares.com.au/wp-content/uploads/2024/04/SCA_662202-00_hi-res.jpg', 'https://wentyspares.com.au/wp-content/uploads/2024/04/SCA_340531_hi-res.jpg', 'https://wentyspares.com.au/wp-content/uploads/2024/04/SCA_550477_hi-res.jpg' ]; var KB = [ { keywords: ['hour', 'open', 'closing', 'close', 'time'], answer: 'For our current opening hours, the quickest way is to give us a call on ' + BUSINESS.phone + ' or check our Facebook page -- hours can vary on public holidays.', section: 'contact', sectionLabel: 'View contact details' }, { keywords: ['where', 'location', 'address', 'directions', 'find you'], answer: 'We\'re located at ' + BUSINESS.address + '. Get directions here.', section: 'contact', sectionLabel: 'Show map & directions' }, { keywords: ['phone', 'call', 'number'], answer: 'You can reach us on ' + BUSINESS.phone + ' -- the team is happy to check part availability over the phone.', section: 'contact', sectionLabel: 'Show contact details' }, { keywords: ['price', 'cost', 'cheap', 'expensive', 'quote'], answer: 'Pricing depends on the part and your vehicle. Give us a call on ' + BUSINESS.phone + ' with your rego or make/model and we\'ll get you a quick quote.', section: 'parts', sectionLabel: 'Browse our parts', gallery: PART_THUMBS }, { keywords: ['part', 'parts', 'brake', 'battery', 'muffler', 'spark', 'filter', 'engine', 'suspension', 'exhaust', 'stock'], answer: 'We stock a wide range of genuine and aftermarket parts -- brakes, batteries, filters, exhaust, suspension and more. Tell us your vehicle\'s make/model or call ' + BUSINESS.phone + ' and we\'ll check stock for you.', section: 'parts', sectionLabel: 'Browse our parts', gallery: PART_THUMBS }, { keywords: ['about', 'who are you', 'history', 'founded', 'since', 'company'], answer: 'Wenty Spares & Performance has been supplying reliable auto parts since ' + BUSINESS.founded + '. We\'re a local crew of car enthusiasts who genuinely love helping people keep their cars on the road.', section: 'about', sectionLabel: 'Read our story' }, { keywords: ['review', 'testimonial', 'feedback', 'trust'], answer: 'Our customers love our service and prices -- here\'s what a few of them had to say.', section: 'testimonials', sectionLabel: 'See testimonials' }, { keywords: ['facebook', 'instagram', 'social'], answer: 'Follow us on Facebook and Instagram for updates and new stock.' }, { keywords: ['human', 'person', 'agent', 'staff', 'mick'], answer: 'Our team (including Mick!) is best reached by phone at ' + BUSINESS.phone + ' for the fastest answer.', section: 'contact', sectionLabel: 'Show contact details' }, { keywords: ['hi', 'hello', 'hey', 'g\'day', 'gday'], answer: 'Hey there! Ask me about parts, pricing, our location or opening hours -- or call us directly on ' + BUSINESS.phone + '.' }, { keywords: ['thank', 'thanks', 'cheers'], answer: 'You\'re welcome! Anything else I can help with?' } ]; var FALLBACK = { answer: 'I\'m not 100% sure on that one -- the best way to get an accurate answer is to call the team on ' + BUSINESS.phone + '. You can also ask me about parts, pricing, location or hours.' }; var SUGGESTIONS = ['Opening hours?', 'Where are you located?', 'Do you have brake pads?', 'Show me reviews']; var hasGreeted = false; toggleBtn.addEventListener('click', function () { var isOpen = widget.classList.toggle('open'); if (isOpen && !hasGreeted) { hasGreeted = true; renderSuggestions(); setTimeout(function () { addBotMessage('Hi! I\'m the Wenty Spares assistant. Ask me about parts, pricing, hours or where to find us.'); }, 350); } if (isOpen) input.focus(); }); suggestionsEl.addEventListener('click', function (e) { if (e.target.classList.contains('chip')) { handleUserMessage(e.target.textContent); } }); form.addEventListener('submit', function (e) { e.preventDefault(); var text = input.value.trim(); if (!text) return; input.value = ''; handleUserMessage(text); }); function renderSuggestions() { suggestionsEl.innerHTML = ''; SUGGESTIONS.forEach(function (s) { var chip = document.createElement('button'); chip.type = 'button'; chip.className = 'chip'; chip.textContent = s; chip.addEventListener('click', spawnRipple); suggestionsEl.appendChild(chip); }); } function handleUserMessage(text) { addUserMessage(text); showTyping(); getResponse(text).then(function (reply) { hideTyping(); addBotMessage(reply); }); } function addUserMessage(text) { var div = document.createElement('div'); div.className = 'msg user'; div.textContent = text; messagesEl.appendChild(div); scrollToBottom(); } function addBotMessage(reply) { var data = (typeof reply === 'string') ? { answer: reply } : reply; var div = document.createElement('div'); div.className = 'msg bot'; div.innerHTML = data.answer; if (data.gallery && data.gallery.length) { var gallery = document.createElement('div'); gallery.className = 'msg-gallery'; data.gallery.forEach(function (src) { var img = document.createElement('img'); img.src = src; img.alt = 'Part in stock'; img.addEventListener('click', function () { jumpToSection('parts'); }); gallery.appendChild(img); }); div.appendChild(gallery); } if (data.section) { var jumpBtn = document.createElement('button'); jumpBtn.type = 'button'; jumpBtn.className = 'msg-jump'; jumpBtn.innerHTML = ' ' + (data.sectionLabel || 'View on page'); jumpBtn.addEventListener('click', function (e) { spawnRipple(e); jumpToSection(data.section); }); div.appendChild(jumpBtn); } messagesEl.appendChild(div); scrollToBottom(); } var typingEl = null; function showTyping() { typingEl = document.createElement('div'); typingEl.className = 'msg bot typing'; typingEl.innerHTML = ''; messagesEl.appendChild(typingEl); scrollToBottom(); } function hideTyping() { if (typingEl) { typingEl.remove(); typingEl = null; } } function scrollToBottom() { messagesEl.scrollTop = messagesEl.scrollHeight; } function localAnswer(text) { var lower = text.toLowerCase(); var best = null; var bestScore = 0; KB.forEach(function (entry) { var score = 0; entry.keywords.forEach(function (kw) { if (lower.indexOf(kw) !== -1) score++; }); if (score > bestScore) { bestScore = score; best = entry; } }); return best || FALLBACK; } function getResponse(text) { var delay = 500 + Math.random() * 500; if (CONFIG.USE_REMOTE && CONFIG.REMOTE_ENDPOINT) { return fetch(CONFIG.REMOTE_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text }) }).then(function (r) { return r.json(); }) .then(function (data) { return data.reply ? { answer: data.reply } : localAnswer(text); }) .catch(function () { return localAnswer(text); }); } return new Promise(function (resolve) { setTimeout(function () { resolve(localAnswer(text)); }, delay); }); } }