/* ======================================================================
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);
});
}
}