- Fix JavaScript syntax errors preventing form submission - Update reCAPTCHA configuration with working test keys - Restore comprehensive spam protection (reCAPTCHA v3, AJAX validation, rate limiting) - Switch from minified to source JS file to apply critical fixes - Add missing security headers and form validation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
846 lines
31 KiB
JavaScript
846 lines
31 KiB
JavaScript
// Enhanced JavaScript for UK Data Services Website
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
// Rotating Hero Text Effect (like original UK Data Services site)
|
|
const rotatingText = document.getElementById('rotating-text');
|
|
const heroSubtitle = document.getElementById('hero-subtitle');
|
|
|
|
if (rotatingText && heroSubtitle) {
|
|
const slides = [
|
|
{
|
|
title: "Voted UK's No.1 Web Scraping Service",
|
|
subtitle: "We are experts in web scraping, data analysis and competitor price monitoring."
|
|
},
|
|
{
|
|
title: "UK-based Team",
|
|
subtitle: "Our team is based in the UK for a clearer, faster response."
|
|
},
|
|
{
|
|
title: "Professional Price Monitoring",
|
|
subtitle: "Let us monitor your competitor's pricing and product ranges."
|
|
},
|
|
{
|
|
title: "Bespoke Software Solutions",
|
|
subtitle: "Let our experts build your ideal scraping solution."
|
|
}
|
|
];
|
|
|
|
let currentSlide = 0;
|
|
|
|
function rotateSlide() {
|
|
// Fade out
|
|
rotatingText.style.opacity = '0';
|
|
heroSubtitle.style.opacity = '0';
|
|
|
|
setTimeout(() => {
|
|
// Change text
|
|
rotatingText.textContent = slides[currentSlide].title;
|
|
heroSubtitle.textContent = slides[currentSlide].subtitle;
|
|
|
|
// Fade in
|
|
rotatingText.style.opacity = '1';
|
|
heroSubtitle.style.opacity = '1';
|
|
|
|
currentSlide = (currentSlide + 1) % slides.length;
|
|
}, 500);
|
|
}
|
|
|
|
// Add transition styles immediately
|
|
rotatingText.style.transition = 'opacity 0.5s ease-in-out';
|
|
heroSubtitle.style.transition = 'opacity 0.5s ease-in-out';
|
|
|
|
// Start rotation after a short delay
|
|
setTimeout(() => {
|
|
rotateSlide();
|
|
setInterval(rotateSlide, 4000); // Change every 4 seconds
|
|
}, 2000); // Start after 2 seconds
|
|
|
|
console.log('Hero text rotation initialized');
|
|
} else {
|
|
console.log('Rotating text elements not found');
|
|
}
|
|
|
|
// Mobile Navigation Toggle
|
|
const navToggle = document.getElementById('nav-toggle');
|
|
const navMenu = document.getElementById('nav-menu');
|
|
|
|
if (navToggle && navMenu) {
|
|
navToggle.addEventListener('click', function() {
|
|
navMenu.classList.toggle('active');
|
|
navToggle.classList.toggle('active');
|
|
});
|
|
|
|
// Close mobile menu when clicking on a link
|
|
const navLinks = document.querySelectorAll('.nav-link');
|
|
navLinks.forEach(link => {
|
|
link.addEventListener('click', () => {
|
|
navMenu.classList.remove('active');
|
|
navToggle.classList.remove('active');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Navbar Scroll Effect
|
|
const navbar = document.getElementById('navbar');
|
|
|
|
function handleNavbarScroll() {
|
|
if (window.scrollY > 50) {
|
|
navbar.classList.add('scrolled');
|
|
} else {
|
|
navbar.classList.remove('scrolled');
|
|
}
|
|
}
|
|
|
|
window.addEventListener('scroll', handleNavbarScroll);
|
|
|
|
// Smooth Scrolling for Navigation Links
|
|
const smoothScrollLinks = document.querySelectorAll('a[href^="#"]');
|
|
|
|
smoothScrollLinks.forEach(link => {
|
|
link.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
|
|
const targetId = this.getAttribute('href');
|
|
const targetSection = document.querySelector(targetId);
|
|
|
|
if (targetSection) {
|
|
const headerOffset = 80;
|
|
const elementPosition = targetSection.getBoundingClientRect().top;
|
|
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
|
|
|
|
window.scrollTo({
|
|
top: offsetPosition,
|
|
behavior: 'smooth'
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Enhanced Scroll Animations
|
|
const animatedElements = document.querySelectorAll('.animate-on-scroll, .service-card, .feature, .step');
|
|
|
|
const observerOptions = {
|
|
threshold: 0.1,
|
|
rootMargin: '0px 0px -50px 0px'
|
|
};
|
|
|
|
const observer = new IntersectionObserver(function(entries) {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('animated');
|
|
entry.target.style.opacity = '1';
|
|
entry.target.style.transform = 'translateY(0)';
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, observerOptions);
|
|
|
|
animatedElements.forEach((element, index) => {
|
|
// Set initial state
|
|
element.style.opacity = '0';
|
|
element.style.transform = 'translateY(30px)';
|
|
element.style.transition = `opacity 0.8s ease-out ${index * 0.1}s, transform 0.8s ease-out ${index * 0.1}s`;
|
|
observer.observe(element);
|
|
});
|
|
|
|
// Add hover animations to service cards
|
|
const serviceCards = document.querySelectorAll('.service-card');
|
|
serviceCards.forEach(card => {
|
|
card.addEventListener('mouseenter', function() {
|
|
this.style.transform = 'translateY(-10px) scale(1.02)';
|
|
this.style.boxShadow = '0 20px 40px rgba(0, 0, 0, 0.15)';
|
|
});
|
|
|
|
card.addEventListener('mouseleave', function() {
|
|
this.style.transform = 'translateY(0) scale(1)';
|
|
this.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.08)';
|
|
});
|
|
});
|
|
|
|
// Add pulse animation to CTA buttons
|
|
const ctaButtons = document.querySelectorAll('.btn-primary');
|
|
ctaButtons.forEach(btn => {
|
|
btn.addEventListener('mouseenter', function() {
|
|
this.style.animation = 'pulse 0.5s ease-in-out';
|
|
});
|
|
|
|
btn.addEventListener('mouseleave', function() {
|
|
this.style.animation = 'none';
|
|
});
|
|
});
|
|
|
|
console.log('Enhanced animations initialized');
|
|
|
|
// Initialize reCAPTCHA and form tracking
|
|
let interactionScore = 0;
|
|
let formStartTime = Date.now();
|
|
|
|
// Track user interactions for bot detection
|
|
document.addEventListener('mousemove', () => interactionScore += 1);
|
|
document.addEventListener('keydown', () => interactionScore += 2);
|
|
document.addEventListener('click', () => interactionScore += 3);
|
|
|
|
// Set form timestamp
|
|
const timestampField = document.getElementById('form_timestamp');
|
|
if (timestampField) {
|
|
timestampField.value = formStartTime;
|
|
}
|
|
|
|
// Form Validation and Enhancement
|
|
const contactForm = document.querySelector('.contact-form form');
|
|
|
|
if (contactForm) {
|
|
contactForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
// Basic form validation
|
|
const formData = new FormData(this);
|
|
const name = formData.get('name');
|
|
const email = formData.get('email');
|
|
const message = formData.get('message');
|
|
|
|
// Validation
|
|
let isValid = true;
|
|
const errors = [];
|
|
|
|
if (!name || name.trim().length < 2) {
|
|
errors.push('Please enter a valid name');
|
|
isValid = false;
|
|
}
|
|
|
|
if (!email || !isValidEmail(email)) {
|
|
errors.push('Please enter a valid email address');
|
|
isValid = false;
|
|
}
|
|
|
|
if (!message || message.trim().length < 10) {
|
|
errors.push('Please provide more details about your project (minimum 10 characters)');
|
|
isValid = false;
|
|
}
|
|
|
|
if (isValid) {
|
|
// Show loading state
|
|
const submitButton = this.querySelector('button[type="submit"]');
|
|
const originalText = submitButton.textContent;
|
|
submitButton.textContent = 'Sending...';
|
|
submitButton.disabled = true;
|
|
|
|
// Execute reCAPTCHA and submit form
|
|
if (typeof grecaptcha !== 'undefined') {
|
|
grecaptcha.ready(() => {
|
|
grecaptcha.execute(window.recaptchaSiteKey, {action: 'contact_form'}).then((token) => {
|
|
// Add reCAPTCHA token and interaction data
|
|
formData.set('recaptcha_response', token);
|
|
formData.set('interaction_token', btoa(JSON.stringify({score: Math.min(interactionScore, 100), time: Date.now() - formStartTime})));
|
|
|
|
// Submit form
|
|
fetch('contact-handler.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
},
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showNotification('Message sent successfully! We\'ll get back to you soon.', 'success');
|
|
this.reset();
|
|
} else {
|
|
showNotification(data.message || 'There was an error sending your message. Please try again.', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showNotification('There was an error sending your message. Please try again.', 'error');
|
|
})
|
|
.finally(() => {
|
|
submitButton.textContent = originalText;
|
|
submitButton.disabled = false;
|
|
});
|
|
});
|
|
});
|
|
} else {
|
|
// Fallback if reCAPTCHA not loaded
|
|
showNotification('Security verification not available. Please refresh the page.', 'error');
|
|
submitButton.textContent = originalText;
|
|
submitButton.disabled = false;
|
|
}
|
|
} else {
|
|
showNotification(errors.join('<br>'), 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Email validation function
|
|
function isValidEmail(email) {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return emailRegex.test(email);
|
|
}
|
|
|
|
// Notification system
|
|
function showNotification(message, type = 'info') {
|
|
// Remove existing notifications
|
|
const existingNotification = document.querySelector('.notification');
|
|
if (existingNotification) {
|
|
existingNotification.remove();
|
|
}
|
|
|
|
// Create notification element
|
|
const notification = document.createElement('div');
|
|
notification.className = `notification notification-${type}`;
|
|
notification.innerHTML = `
|
|
<div class="notification-content">
|
|
<span class="notification-message">${message}</span>
|
|
<button class="notification-close">×</button>
|
|
</div>
|
|
`;
|
|
|
|
// Add styles
|
|
notification.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
z-index: 10000;
|
|
background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
|
|
color: white;
|
|
padding: 16px 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
max-width: 400px;
|
|
font-family: 'Inter', sans-serif;
|
|
font-size: 14px;
|
|
opacity: 0;
|
|
transform: translateX(100%);
|
|
transition: all 0.3s ease;
|
|
`;
|
|
|
|
notification.querySelector('.notification-content').style.cssText = `
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 12px;
|
|
`;
|
|
|
|
notification.querySelector('.notification-close').style.cssText = `
|
|
background: none;
|
|
border: none;
|
|
color: white;
|
|
font-size: 18px;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
width: 20px;
|
|
height: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
`;
|
|
|
|
// Add to page
|
|
document.body.appendChild(notification);
|
|
|
|
// Animate in
|
|
setTimeout(() => {
|
|
notification.style.opacity = '1';
|
|
notification.style.transform = 'translateX(0)';
|
|
}, 100);
|
|
|
|
// Handle close button
|
|
notification.querySelector('.notification-close').addEventListener('click', () => {
|
|
hideNotification(notification);
|
|
});
|
|
|
|
// Auto hide after 5 seconds
|
|
setTimeout(() => {
|
|
if (document.body.contains(notification)) {
|
|
hideNotification(notification);
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
function hideNotification(notification) {
|
|
notification.style.opacity = '0';
|
|
notification.style.transform = 'translateX(100%)';
|
|
setTimeout(() => {
|
|
if (document.body.contains(notification)) {
|
|
notification.remove();
|
|
}
|
|
}, 300);
|
|
}
|
|
|
|
// Stats Counter Animation
|
|
const stats = document.querySelectorAll('.stat-number');
|
|
|
|
function animateStats() {
|
|
stats.forEach(stat => {
|
|
const originalText = stat.textContent.trim();
|
|
console.log('Animating stat:', originalText);
|
|
|
|
// Handle different stat types
|
|
if (originalText.includes('£2.5M+')) {
|
|
return; // Keep as is, don't animate currency
|
|
} else if (originalText.includes('99.8%')) {
|
|
animateNumber(stat, 0, 99.8, '%');
|
|
} else if (originalText.includes('ISO 27001')) {
|
|
return; // Keep as is, don't animate certification
|
|
}
|
|
});
|
|
}
|
|
|
|
function animateNumber(element, start, end, suffix = '') {
|
|
let current = start;
|
|
const increment = (end - start) / 60; // 60 steps for smoother animation
|
|
|
|
const timer = setInterval(() => {
|
|
current += increment;
|
|
if (current >= end) {
|
|
current = end;
|
|
clearInterval(timer);
|
|
}
|
|
|
|
element.textContent = current.toFixed(1) + suffix;
|
|
}, 50); // Every 50ms
|
|
}
|
|
|
|
// Trigger stats animation when section is visible
|
|
const statsSection = document.querySelector('.hero-stats');
|
|
if (statsSection) {
|
|
const statsObserver = new IntersectionObserver(function(entries) {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
console.log('Stats section is visible, starting animation');
|
|
setTimeout(() => {
|
|
animateStats();
|
|
}, 500); // Small delay
|
|
statsObserver.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.3 });
|
|
|
|
statsObserver.observe(statsSection);
|
|
} else {
|
|
console.log('Stats section not found');
|
|
}
|
|
|
|
// Enhanced Lazy Loading for Images with WebP support
|
|
const images = document.querySelectorAll('img[loading="lazy"]');
|
|
|
|
// WebP support detection
|
|
function supportsWebP() {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 1;
|
|
canvas.height = 1;
|
|
return canvas.toDataURL('image/webp').indexOf('webp') !== -1;
|
|
}
|
|
|
|
if ('IntersectionObserver' in window) {
|
|
const imageObserver = new IntersectionObserver(function(entries) {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const img = entry.target;
|
|
|
|
// Handle data-src for lazy loading
|
|
if (img.dataset.src) {
|
|
img.src = img.dataset.src;
|
|
}
|
|
|
|
// Handle WebP support
|
|
if (img.dataset.webp && supportsWebP()) {
|
|
img.src = img.dataset.webp;
|
|
}
|
|
|
|
img.classList.add('loaded');
|
|
img.style.opacity = '1';
|
|
imageObserver.unobserve(img);
|
|
}
|
|
});
|
|
}, {
|
|
rootMargin: '50px 0px',
|
|
threshold: 0.1
|
|
});
|
|
|
|
images.forEach(img => {
|
|
// Set initial opacity for lazy images
|
|
if (img.loading === 'lazy') {
|
|
img.style.opacity = '0';
|
|
img.style.transition = 'opacity 0.3s ease';
|
|
}
|
|
imageObserver.observe(img);
|
|
});
|
|
}
|
|
|
|
// Scroll to Top Button
|
|
const scrollTopBtn = document.createElement('button');
|
|
scrollTopBtn.innerHTML = '↑';
|
|
scrollTopBtn.className = 'scroll-top-btn';
|
|
scrollTopBtn.style.cssText = `
|
|
position: fixed;
|
|
bottom: 30px;
|
|
right: 30px;
|
|
width: 50px;
|
|
height: 50px;
|
|
border: none;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
font-size: 20px;
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: all 0.3s ease;
|
|
z-index: 1000;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
`;
|
|
|
|
document.body.appendChild(scrollTopBtn);
|
|
|
|
scrollTopBtn.addEventListener('click', () => {
|
|
window.scrollTo({
|
|
top: 0,
|
|
behavior: 'smooth'
|
|
});
|
|
});
|
|
|
|
// Show/hide scroll to top button
|
|
function handleScrollTopButton() {
|
|
if (window.scrollY > 500) {
|
|
scrollTopBtn.style.opacity = '1';
|
|
scrollTopBtn.style.visibility = 'visible';
|
|
} else {
|
|
scrollTopBtn.style.opacity = '0';
|
|
scrollTopBtn.style.visibility = 'hidden';
|
|
}
|
|
}
|
|
|
|
window.addEventListener('scroll', handleScrollTopButton);
|
|
|
|
// Performance: Throttle scroll events
|
|
let scrollTimeout;
|
|
const originalHandlers = [handleNavbarScroll, handleScrollTopButton];
|
|
|
|
function throttledScrollHandler() {
|
|
if (!scrollTimeout) {
|
|
scrollTimeout = setTimeout(() => {
|
|
originalHandlers.forEach(handler => handler());
|
|
scrollTimeout = null;
|
|
}, 16); // ~60fps
|
|
}
|
|
}
|
|
|
|
window.removeEventListener('scroll', handleNavbarScroll);
|
|
window.removeEventListener('scroll', handleScrollTopButton);
|
|
window.addEventListener('scroll', throttledScrollHandler);
|
|
|
|
// Preload critical resources with WebP support
|
|
function preloadResource(href, as = 'image', type = null) {
|
|
const link = document.createElement('link');
|
|
link.rel = 'preload';
|
|
link.href = href;
|
|
link.as = as;
|
|
if (type) {
|
|
link.type = type;
|
|
}
|
|
document.head.appendChild(link);
|
|
}
|
|
|
|
// Preload critical images with WebP format preference
|
|
function preloadCriticalImages() {
|
|
const criticalImages = [
|
|
'assets/images/ukds-main-logo.png',
|
|
'assets/images/hero-data-analytics.svg'
|
|
];
|
|
|
|
criticalImages.forEach(imagePath => {
|
|
// Try WebP first if supported
|
|
if (supportsWebP()) {
|
|
const webpPath = imagePath.replace(/\.(jpg|jpeg|png)$/i, '.webp');
|
|
preloadResource(webpPath, 'image', 'image/webp');
|
|
} else {
|
|
preloadResource(imagePath, 'image');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize critical image preloading
|
|
preloadCriticalImages();
|
|
|
|
// Initialize tooltips (if needed)
|
|
const tooltipElements = document.querySelectorAll('[data-tooltip]');
|
|
|
|
tooltipElements.forEach(element => {
|
|
element.addEventListener('mouseenter', function() {
|
|
const tooltipText = this.getAttribute('data-tooltip');
|
|
const tooltip = document.createElement('div');
|
|
tooltip.className = 'tooltip';
|
|
tooltip.textContent = tooltipText;
|
|
tooltip.style.cssText = `
|
|
position: absolute;
|
|
background: #1a1a1a;
|
|
color: white;
|
|
padding: 8px 12px;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
white-space: nowrap;
|
|
z-index: 10000;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
pointer-events: none;
|
|
`;
|
|
|
|
document.body.appendChild(tooltip);
|
|
|
|
const rect = this.getBoundingClientRect();
|
|
tooltip.style.left = rect.left + (rect.width / 2) - (tooltip.offsetWidth / 2) + 'px';
|
|
tooltip.style.top = rect.top - tooltip.offsetHeight - 10 + 'px';
|
|
|
|
setTimeout(() => {
|
|
tooltip.style.opacity = '1';
|
|
}, 100);
|
|
|
|
this.addEventListener('mouseleave', function() {
|
|
tooltip.style.opacity = '0';
|
|
setTimeout(() => {
|
|
if (document.body.contains(tooltip)) {
|
|
tooltip.remove();
|
|
}
|
|
}, 300);
|
|
}, { once: true });
|
|
});
|
|
});
|
|
|
|
// Security: Prevent XSS in dynamic content
|
|
function sanitizeHTML(str) {
|
|
const temp = document.createElement('div');
|
|
temp.textContent = str;
|
|
return temp.innerHTML;
|
|
}
|
|
|
|
// Service Worker Registration (for PWA capabilities)
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', () => {
|
|
navigator.serviceWorker.register('/sw.js')
|
|
.then(registration => {
|
|
console.log('SW registered: ', registration);
|
|
})
|
|
.catch(registrationError => {
|
|
console.log('SW registration failed: ', registrationError);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Performance monitoring
|
|
if ('performance' in window) {
|
|
window.addEventListener('load', function() {
|
|
setTimeout(() => {
|
|
const perfData = performance.getEntriesByType('navigation')[0];
|
|
if (perfData) {
|
|
console.log('Page Load Performance:', {
|
|
'DNS Lookup': Math.round(perfData.domainLookupEnd - perfData.domainLookupStart),
|
|
'TCP Connection': Math.round(perfData.connectEnd - perfData.connectStart),
|
|
'Request/Response': Math.round(perfData.responseEnd - perfData.requestStart),
|
|
'DOM Processing': Math.round(perfData.domComplete - perfData.domLoading),
|
|
'Total Load Time': Math.round(perfData.loadEventEnd - perfData.navigationStart)
|
|
});
|
|
}
|
|
}, 0);
|
|
});
|
|
}
|
|
|
|
console.log('UK Data Services website initialized successfully');
|
|
console.log('Performance optimizations: Lazy loading, WebP support, and preloading enabled');
|
|
|
|
// Universal Blog Pagination System
|
|
initializeBlogPagination();
|
|
|
|
function initializeBlogPagination() {
|
|
const paginationContainer = document.querySelector('.blog-pagination');
|
|
const articlesGrid = document.querySelector('.articles-grid');
|
|
|
|
if (!paginationContainer || !articlesGrid) {
|
|
return; // No pagination on this page
|
|
}
|
|
|
|
const prevButton = paginationContainer.querySelector('button:first-child');
|
|
const nextButton = paginationContainer.querySelector('button:last-child');
|
|
const paginationInfo = paginationContainer.querySelector('.pagination-info');
|
|
|
|
if (!prevButton || !nextButton || !paginationInfo) {
|
|
return; // Invalid pagination structure
|
|
}
|
|
|
|
// Get current page from URL or default to 1
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
let currentPage = parseInt(urlParams.get('page')) || 1;
|
|
|
|
// Get all articles on the page
|
|
const allArticles = Array.from(articlesGrid.querySelectorAll('.article-card'));
|
|
const articlesPerPage = 6;
|
|
const totalPages = Math.ceil(allArticles.length / articlesPerPage);
|
|
|
|
// If we have actual multiple pages of content, use the original pagination logic
|
|
// Otherwise, implement client-side pagination
|
|
if (totalPages <= 1) {
|
|
// Hide pagination if not needed
|
|
paginationContainer.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
function renderPage(page, shouldScroll = false) {
|
|
// Hide all articles
|
|
allArticles.forEach(article => {
|
|
article.style.display = 'none';
|
|
});
|
|
|
|
// Show articles for current page
|
|
const startIndex = (page - 1) * articlesPerPage;
|
|
const endIndex = startIndex + articlesPerPage;
|
|
|
|
for (let i = startIndex; i < endIndex && i < allArticles.length; i++) {
|
|
allArticles[i].style.display = 'block';
|
|
allArticles[i].style.animation = 'fadeInUp 0.6s ease forwards';
|
|
}
|
|
|
|
// Update pagination info
|
|
paginationInfo.textContent = `Page ${page} of ${totalPages}`;
|
|
|
|
// Update button states
|
|
prevButton.disabled = (page <= 1);
|
|
nextButton.disabled = (page >= totalPages);
|
|
|
|
// Update URL without page reload
|
|
const newUrl = new URL(window.location);
|
|
if (page > 1) {
|
|
newUrl.searchParams.set('page', page);
|
|
} else {
|
|
newUrl.searchParams.delete('page');
|
|
}
|
|
window.history.replaceState({}, '', newUrl);
|
|
|
|
// Only scroll to articles section when navigating between pages
|
|
if (shouldScroll) {
|
|
articlesGrid.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Event listeners
|
|
prevButton.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
if (currentPage > 1) {
|
|
currentPage--;
|
|
renderPage(currentPage, true);
|
|
}
|
|
});
|
|
|
|
nextButton.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
if (currentPage < totalPages) {
|
|
currentPage++;
|
|
renderPage(currentPage, true);
|
|
}
|
|
});
|
|
|
|
// Initialize first page (don't scroll on initial load)
|
|
renderPage(currentPage, false);
|
|
|
|
// Add CSS animation for article transitions
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
@keyframes fadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.article-card {
|
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
}
|
|
|
|
.article-card[style*="display: none"] {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
console.log(`Blog pagination initialized: ${totalPages} pages, ${allArticles.length} articles`);
|
|
}
|
|
|
|
// Viewport-based Image Loading Optimization
|
|
function initializeViewportImageLoading() {
|
|
// Intersection Observer for lazy loading optimization
|
|
const imageObserver = new IntersectionObserver((entries, observer) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const img = entry.target;
|
|
|
|
// Load high-quality version when in viewport
|
|
if (img.dataset.src) {
|
|
img.src = img.dataset.src;
|
|
img.removeAttribute('data-src');
|
|
}
|
|
|
|
// Load WebP for supported browsers
|
|
if (img.dataset.webp && supportsWebP()) {
|
|
img.src = img.dataset.webp;
|
|
}
|
|
|
|
observer.unobserve(img);
|
|
}
|
|
});
|
|
}, {
|
|
rootMargin: '50px 0px',
|
|
threshold: 0.01
|
|
});
|
|
|
|
// Observe all images with data-src
|
|
document.querySelectorAll('img[data-src]').forEach(img => {
|
|
imageObserver.observe(img);
|
|
});
|
|
|
|
// WebP support detection
|
|
function supportsWebP() {
|
|
return new Promise(resolve => {
|
|
const webP = new Image();
|
|
webP.onload = webP.onerror = () => resolve(webP.height === 2);
|
|
webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
|
|
});
|
|
}
|
|
}
|
|
|
|
// Touch Target Optimization for Mobile
|
|
function optimizeTouchTargets() {
|
|
const minTouchSize = 44; // 44px minimum touch target
|
|
|
|
// Check and optimize button sizes
|
|
document.querySelectorAll('button, .btn, a[role="button"]').forEach(element => {
|
|
const rect = element.getBoundingClientRect();
|
|
if (rect.width < minTouchSize || rect.height < minTouchSize) {
|
|
element.style.minWidth = minTouchSize + 'px';
|
|
element.style.minHeight = minTouchSize + 'px';
|
|
element.style.display = 'inline-flex';
|
|
element.style.alignItems = 'center';
|
|
element.style.justifyContent = 'center';
|
|
}
|
|
});
|
|
|
|
// Add touch-friendly spacing
|
|
document.querySelectorAll('.nav-menu a, .social-links a').forEach(element => {
|
|
element.style.padding = '12px 16px';
|
|
element.style.margin = '4px';
|
|
});
|
|
}
|
|
|
|
// Initialize optimizations
|
|
initializeViewportImageLoading();
|
|
if ('ontouchstart' in window) {
|
|
optimizeTouchTargets();
|
|
}
|
|
}); |