- Modern, conversion-focused redesign for novizun.com - Dual-audience positioning: schools (B2B) and parents (B2C) - Government vendor credentials section (GeBIZ/VCG, UEN, tax invoices) - Government claims reference (Edusave, PGS, ECDA, MOE schemes) - 4-step school order process with claim-ready documentation - Product catalog with category filtering - Dark mode, responsive design, scroll animations - Floating mobile CTA for school enquiries
158 lines
5.1 KiB
JavaScript
158 lines
5.1 KiB
JavaScript
/* Novizun — Interactive Script */
|
|
|
|
// Initialize Lucide icons
|
|
lucide.createIcons();
|
|
|
|
// Dark mode toggle
|
|
(function () {
|
|
const toggle = document.querySelector('[data-theme-toggle]');
|
|
const root = document.documentElement;
|
|
let dark = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
root.setAttribute('data-theme', dark);
|
|
|
|
if (toggle) {
|
|
toggle.addEventListener('click', function () {
|
|
dark = dark === 'dark' ? 'light' : 'dark';
|
|
root.setAttribute('data-theme', dark);
|
|
toggle.setAttribute('aria-label', 'Switch to ' + (dark === 'dark' ? 'light' : 'dark') + ' mode');
|
|
toggle.innerHTML = dark === 'dark'
|
|
? '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>'
|
|
: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>';
|
|
});
|
|
}
|
|
})();
|
|
|
|
// Mobile menu toggle
|
|
(function () {
|
|
const btn = document.getElementById('mobile-menu-btn');
|
|
const nav = document.getElementById('nav');
|
|
if (btn && nav) {
|
|
btn.addEventListener('click', function () {
|
|
const expanded = btn.getAttribute('aria-expanded') === 'true';
|
|
btn.setAttribute('aria-expanded', String(!expanded));
|
|
nav.classList.toggle('open');
|
|
});
|
|
// Close on link click
|
|
nav.querySelectorAll('.nav-link').forEach(function (link) {
|
|
link.addEventListener('click', function () {
|
|
btn.setAttribute('aria-expanded', 'false');
|
|
nav.classList.remove('open');
|
|
});
|
|
});
|
|
}
|
|
})();
|
|
|
|
// Header scroll behavior
|
|
(function () {
|
|
const header = document.getElementById('header');
|
|
let lastScroll = 0;
|
|
window.addEventListener('scroll', function () {
|
|
const scrollY = window.scrollY;
|
|
if (scrollY > 10) {
|
|
header.classList.add('header--scrolled');
|
|
} else {
|
|
header.classList.remove('header--scrolled');
|
|
}
|
|
lastScroll = scrollY;
|
|
}, { passive: true });
|
|
})();
|
|
|
|
// Product category filtering
|
|
(function () {
|
|
const tabs = document.querySelectorAll('.product-tab');
|
|
const cards = document.querySelectorAll('.product-card');
|
|
|
|
tabs.forEach(function (tab) {
|
|
tab.addEventListener('click', function () {
|
|
// Update active tab
|
|
tabs.forEach(function (t) {
|
|
t.classList.remove('active');
|
|
t.setAttribute('aria-selected', 'false');
|
|
});
|
|
tab.classList.add('active');
|
|
tab.setAttribute('aria-selected', 'true');
|
|
|
|
const filter = tab.getAttribute('data-filter');
|
|
|
|
cards.forEach(function (card) {
|
|
if (filter === 'all') {
|
|
card.classList.remove('hidden');
|
|
} else {
|
|
const category = card.getAttribute('data-category') || '';
|
|
if (category.includes(filter)) {
|
|
card.classList.remove('hidden');
|
|
} else {
|
|
card.classList.add('hidden');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
})();
|
|
|
|
// Smooth scroll for anchor links
|
|
document.querySelectorAll('a[href^="#"]').forEach(function (anchor) {
|
|
anchor.addEventListener('click', function (e) {
|
|
const target = document.querySelector(this.getAttribute('href'));
|
|
if (target) {
|
|
e.preventDefault();
|
|
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
});
|
|
});
|
|
|
|
// Intersection Observer for scroll reveal
|
|
(function () {
|
|
const sections = document.querySelectorAll('.section');
|
|
if (!('IntersectionObserver' in window)) return;
|
|
|
|
const observer = new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (entry) {
|
|
if (entry.isIntersecting) {
|
|
entry.target.style.opacity = '1';
|
|
entry.target.style.transform = 'translateY(0)';
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' });
|
|
|
|
sections.forEach(function (section) {
|
|
section.style.opacity = '0';
|
|
section.style.transform = 'translateY(20px)';
|
|
section.style.transition = 'opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), transform 0.6s cubic-bezier(0.16, 1, 0.3, 1)';
|
|
observer.observe(section);
|
|
});
|
|
})();
|
|
|
|
// Floating CTA — show after scrolling past hero
|
|
(function () {
|
|
var floatingCta = document.getElementById('floating-cta');
|
|
if (!floatingCta) return;
|
|
|
|
var hero = document.querySelector('.hero');
|
|
if (!hero) return;
|
|
|
|
// Check if mobile (floating CTA hidden on desktop via CSS, but let's be safe)
|
|
function checkMobile() {
|
|
return window.innerWidth < 768;
|
|
}
|
|
|
|
function toggleFloatingCta() {
|
|
if (!checkMobile()) {
|
|
floatingCta.classList.remove('visible');
|
|
return;
|
|
}
|
|
var heroBottom = hero.getBoundingClientRect().bottom;
|
|
if (heroBottom < 0) {
|
|
floatingCta.classList.add('visible');
|
|
} else {
|
|
floatingCta.classList.remove('visible');
|
|
}
|
|
}
|
|
|
|
window.addEventListener('scroll', toggleFloatingCta, { passive: true });
|
|
window.addEventListener('resize', toggleFloatingCta, { passive: true });
|
|
// Initial check
|
|
toggleFloatingCta();
|
|
})();
|