diff --git a/assets/js/main.min.js b/assets/js/main.min.js
index bf6f431..7b8e4c8 100644
--- a/assets/js/main.min.js
+++ b/assets/js/main.min.js
@@ -1,990 +1 @@
-// 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 with ARIA and Focus Trap
- const navToggle = document.getElementById('nav-toggle');
- const navMenu = document.getElementById('nav-menu');
-
- if (navToggle && navMenu) {
- // Get focusable elements in the menu
- const getFocusableElements = () => {
- return navMenu.querySelectorAll('a[href], button:not([disabled])');
- };
-
- // Focus trap handler
- const handleFocusTrap = (e) => {
- if (!navMenu.classList.contains('active')) return;
-
- const focusableElements = getFocusableElements();
- const firstElement = focusableElements[0];
- const lastElement = focusableElements[focusableElements.length - 1];
-
- if (e.key === 'Tab') {
- if (e.shiftKey && document.activeElement === firstElement) {
- e.preventDefault();
- lastElement.focus();
- } else if (!e.shiftKey && document.activeElement === lastElement) {
- e.preventDefault();
- firstElement.focus();
- }
- }
-
- // Close menu on Escape key
- if (e.key === 'Escape') {
- closeMenu();
- }
- };
-
- const openMenu = () => {
- navMenu.classList.add('active');
- navToggle.classList.add('active');
- navToggle.setAttribute('aria-expanded', 'true');
- document.addEventListener('keydown', handleFocusTrap);
- // Focus first menu item
- const firstFocusable = getFocusableElements()[0];
- if (firstFocusable) {
- setTimeout(() => firstFocusable.focus(), 100);
- }
- };
-
- const closeMenu = () => {
- navMenu.classList.remove('active');
- navToggle.classList.remove('active');
- navToggle.setAttribute('aria-expanded', 'false');
- document.removeEventListener('keydown', handleFocusTrap);
- navToggle.focus();
- };
-
- navToggle.addEventListener('click', function() {
- const isExpanded = navToggle.getAttribute('aria-expanded') === 'true';
- if (isExpanded) {
- closeMenu();
- } else {
- openMenu();
- }
- });
-
- // Close mobile menu when clicking on a link
- const navLinks = document.querySelectorAll('.nav-link');
- navLinks.forEach(link => {
- link.addEventListener('click', () => {
- closeMenu();
- });
- });
-
- // Close menu when clicking outside
- document.addEventListener('click', (e) => {
- if (navMenu.classList.contains('active') &&
- !navMenu.contains(e.target) &&
- !navToggle.contains(e.target)) {
- closeMenu();
- }
- });
- }
-
- // 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 with ARIA support
- const contactForm = document.querySelector('.contact-form form');
-
- // Helper function to set field error state
- function setFieldError(fieldId, errorMessage) {
- const field = document.getElementById(fieldId);
- const errorSpan = document.getElementById(fieldId + '-error');
- if (field && errorSpan) {
- field.setAttribute('aria-invalid', 'true');
- errorSpan.textContent = errorMessage;
- }
- }
-
- // Helper function to clear field error state
- function clearFieldError(fieldId) {
- const field = document.getElementById(fieldId);
- const errorSpan = document.getElementById(fieldId + '-error');
- if (field && errorSpan) {
- field.setAttribute('aria-invalid', 'false');
- errorSpan.textContent = '';
- }
- }
-
- // Clear all form errors
- function clearAllErrors() {
- ['name', 'email', 'company', 'message'].forEach(clearFieldError);
- }
-
- if (contactForm) {
- // Real-time validation on blur
- const requiredFields = contactForm.querySelectorAll('[required]');
- requiredFields.forEach(field => {
- field.addEventListener('blur', function() {
- validateField(this);
- });
- field.addEventListener('input', function() {
- if (this.getAttribute('aria-invalid') === 'true') {
- validateField(this);
- }
- });
- });
-
- function validateField(field) {
- const value = field.value.trim();
- const fieldId = field.id;
-
- if (fieldId === 'name' && value.length < 2) {
- setFieldError(fieldId, 'Please enter a valid name (at least 2 characters)');
- return false;
- } else if (fieldId === 'email' && !isValidEmail(value)) {
- setFieldError(fieldId, 'Please enter a valid email address');
- return false;
- } else if (fieldId === 'company' && value.length < 2) {
- setFieldError(fieldId, 'Please enter your organisation name');
- return false;
- } else if (fieldId === 'message' && value.length < 10) {
- setFieldError(fieldId, 'Please provide more details (at least 10 characters)');
- return false;
- }
-
- clearFieldError(fieldId);
- return true;
- }
-
- contactForm.addEventListener('submit', function(e) {
- e.preventDefault();
- clearAllErrors();
-
- // Basic form validation
- const formData = new FormData(this);
- const name = formData.get('name');
- const email = formData.get('email');
- const company = formData.get('company');
- const message = formData.get('message');
-
- // Validation
- let isValid = true;
- let firstErrorField = null;
-
- if (!name || name.trim().length < 2) {
- setFieldError('name', 'Please enter a valid name (at least 2 characters)');
- isValid = false;
- if (!firstErrorField) firstErrorField = document.getElementById('name');
- }
-
- if (!email || !isValidEmail(email)) {
- setFieldError('email', 'Please enter a valid email address');
- isValid = false;
- if (!firstErrorField) firstErrorField = document.getElementById('email');
- }
-
- if (!company || company.trim().length < 2) {
- setFieldError('company', 'Please enter your organisation name');
- isValid = false;
- if (!firstErrorField) firstErrorField = document.getElementById('company');
- }
-
- if (!message || message.trim().length < 10) {
- setFieldError('message', 'Please provide more details (at least 10 characters)');
- isValid = false;
- if (!firstErrorField) firstErrorField = document.getElementById('message');
- }
-
- // Focus first error field for accessibility
- if (!isValid && firstErrorField) {
- firstErrorField.focus();
- return;
- }
-
- if (isValid) {
- // Show loading state
- const submitButton = this.querySelector('button[type="submit"]');
- const originalText = submitButton.textContent;
- submitButton.textContent = 'Sending...';
- submitButton.disabled = true;
-
- // Update form timestamp to current time (ensures it's fresh at submission)
- const timestampField = document.getElementById('form_timestamp');
- if (timestampField) {
- timestampField.value = Date.now();
- }
- // Recreate formData after updating timestamp
- const freshFormData = new FormData(this);
-
- // 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
- freshFormData.set('recaptcha_response', token);
- freshFormData.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: freshFormData
- })
- .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('
'), '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 = `
-
- ${message}
-
-
- `;
-
- // 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, '%');
- }
- });
- }
-
- 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();
- }
-});
\ No newline at end of file
+document.addEventListener("DOMContentLoaded",function(){const e=document.getElementById("rotating-text"),t=document.getElementById("hero-subtitle");if(e&&t){const C=[{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 T=0;function n(){e.style.opacity="0",t.style.opacity="0",setTimeout(()=>{e.textContent=C[T].title,t.textContent=C[T].subtitle,e.style.opacity="1",t.style.opacity="1",T=(T+1)%C.length},500)}e.style.transition="opacity 0.5s ease-in-out",t.style.transition="opacity 0.5s ease-in-out",setTimeout(()=>{n(),setInterval(n,4e3)},2e3),console.log("Hero text rotation initialized")}else console.log("Rotating text elements not found");const o=document.getElementById("nav-toggle"),a=document.getElementById("nav-menu");if(o&&a){const P=()=>a.querySelectorAll("a[href], button:not([disabled])"),B=e=>{if(!a.classList.contains("active"))return;const t=P(),n=t[0],o=t[t.length-1];"Tab"===e.key&&(e.shiftKey&&document.activeElement===n?(e.preventDefault(),o.focus()):e.shiftKey||document.activeElement!==o||(e.preventDefault(),n.focus())),"Escape"===e.key&&D()},M=()=>{a.classList.add("active"),o.classList.add("active"),o.setAttribute("aria-expanded","true"),document.addEventListener("keydown",B);const e=P()[0];e&&setTimeout(()=>e.focus(),100)},D=()=>{a.classList.remove("active"),o.classList.remove("active"),o.setAttribute("aria-expanded","false"),document.removeEventListener("keydown",B),o.focus()};o.addEventListener("click",function(){"true"===o.getAttribute("aria-expanded")?D():M()});document.querySelectorAll(".nav-link").forEach(e=>{e.addEventListener("click",()=>{D()})}),document.addEventListener("click",e=>{!a.classList.contains("active")||a.contains(e.target)||o.contains(e.target)||D()})}const s=document.getElementById("navbar");function i(){window.scrollY>50?s.classList.add("scrolled"):s.classList.remove("scrolled")}window.addEventListener("scroll",i);document.querySelectorAll('a[href^="#"]').forEach(e=>{e.addEventListener("click",function(e){e.preventDefault();const t=this.getAttribute("href"),n=document.querySelector(t);if(n){const e=80,t=n.getBoundingClientRect().top+window.pageYOffset-e;window.scrollTo({top:t,behavior:"smooth"})}})});const r=document.querySelectorAll(".animate-on-scroll, .service-card, .feature, .step"),c=new IntersectionObserver(function(e){e.forEach(e=>{e.isIntersecting&&(e.target.classList.add("animated"),e.target.style.opacity="1",e.target.style.transform="translateY(0)",c.unobserve(e.target))})},{threshold:.1,rootMargin:"0px 0px -50px 0px"});r.forEach((e,t)=>{e.style.opacity="0",e.style.transform="translateY(30px)",e.style.transition=`opacity 0.8s ease-out ${.1*t}s, transform 0.8s ease-out ${.1*t}s`,c.observe(e)});document.querySelectorAll(".service-card").forEach(e=>{e.addEventListener("mouseenter",function(){this.style.transform="translateY(-10px) scale(1.02)",this.style.boxShadow="0 20px 40px rgba(0, 0, 0, 0.15)"}),e.addEventListener("mouseleave",function(){this.style.transform="translateY(0) scale(1)",this.style.boxShadow="0 4px 20px rgba(0, 0, 0, 0.08)"})});document.querySelectorAll(".btn-primary").forEach(e=>{e.addEventListener("mouseenter",function(){this.style.animation="pulse 0.5s ease-in-out"}),e.addEventListener("mouseleave",function(){this.style.animation="none"})}),console.log("Enhanced animations initialized");let l=0,d=Date.now();document.addEventListener("mousemove",()=>l+=1),document.addEventListener("keydown",()=>l+=2),document.addEventListener("click",()=>l+=3);const u=document.getElementById("form_timestamp");u&&(u.value=d);const m=document.querySelector(".contact-form form");function p(e,t){const n=document.getElementById(e),o=document.getElementById(e+"-error");n&&o&&(n.setAttribute("aria-invalid","true"),o.textContent=t)}function y(e){const t=document.getElementById(e),n=document.getElementById(e+"-error");t&&n&&(t.setAttribute("aria-invalid","false"),n.textContent="")}if(m){function g(e){const t=e.value.trim(),n=e.id;return"name"===n&&t.length<2?(p(n,"Please enter a valid name (at least 2 characters)"),!1):"email"!==n||f(t)?"company"===n&&t.length<2?(p(n,"Please enter your organisation name"),!1):"message"===n&&t.length<10?(p(n,"Please provide more details (at least 10 characters)"),!1):(y(n),!0):(p(n,"Please enter a valid email address"),!1)}m.querySelectorAll("[required]").forEach(e=>{e.addEventListener("blur",function(){g(this)}),e.addEventListener("input",function(){"true"===this.getAttribute("aria-invalid")&&g(this)})}),m.addEventListener("submit",function(e){e.preventDefault(),["name","email","company","message"].forEach(y);const t=new FormData(this),n=t.get("name"),o=t.get("email"),a=t.get("company"),s=t.get("message");let i=!0,r=null;if((!n||n.trim().length<2)&&(p("name","Please enter a valid name (at least 2 characters)"),i=!1,r||(r=document.getElementById("name"))),o&&f(o)||(p("email","Please enter a valid email address"),i=!1,r||(r=document.getElementById("email"))),(!a||a.trim().length<2)&&(p("company","Please enter your organisation name"),i=!1,r||(r=document.getElementById("company"))),(!s||s.trim().length<10)&&(p("message","Please provide more details (at least 10 characters)"),i=!1,r||(r=document.getElementById("message"))),i||!r)if(i){const e=this.querySelector('button[type="submit"]'),t=e.textContent;e.textContent="Sending...",e.disabled=!0;const n=document.getElementById("form_timestamp");n&&(n.value=Date.now());const o=new FormData(this);"undefined"!=typeof grecaptcha?grecaptcha.ready(()=>{grecaptcha.execute(window.recaptchaSiteKey,{action:"contact_form"}).then(n=>{o.set("recaptcha_response",n),o.set("interaction_token",btoa(JSON.stringify({score:Math.min(l,100),time:Date.now()-d}))),fetch("contact-handler.php",{method:"POST",headers:{"X-Requested-With":"XMLHttpRequest"},body:o}).then(e=>e.json()).then(e=>{e.success?(h("Message sent successfully! We'll get back to you soon.","success"),this.reset()):h(e.message||"There was an error sending your message. Please try again.","error")}).catch(e=>{console.error("Error:",e),h("There was an error sending your message. Please try again.","error")}).finally(()=>{e.textContent=t,e.disabled=!1})})}):(h("Security verification not available. Please refresh the page.","error"),e.textContent=t,e.disabled=!1)}else h(errors.join("
"),"error");else r.focus()})}function f(e){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}function h(e,t="info"){const n=document.querySelector(".notification");n&&n.remove();const o=document.createElement("div");o.className=`notification notification-${t}`,o.innerHTML=`\n \n ${e}\n \n
\n `,o.style.cssText=`\n position: fixed;\n top: 20px;\n right: 20px;\n z-index: 10000;\n background: ${"success"===t?"#10b981":"error"===t?"#ef4444":"#3b82f6"};\n color: white;\n padding: 16px 20px;\n border-radius: 8px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n max-width: 400px;\n font-family: 'Inter', sans-serif;\n font-size: 14px;\n opacity: 0;\n transform: translateX(100%);\n transition: all 0.3s ease;\n `,o.querySelector(".notification-content").style.cssText="\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 12px;\n ",o.querySelector(".notification-close").style.cssText="\n background: none;\n border: none;\n color: white;\n font-size: 18px;\n cursor: pointer;\n padding: 0;\n width: 20px;\n height: 20px;\n display: flex;\n align-items: center;\n justify-content: center;\n ",document.body.appendChild(o),setTimeout(()=>{o.style.opacity="1",o.style.transform="translateX(0)"},100),o.querySelector(".notification-close").addEventListener("click",()=>{v(o)}),setTimeout(()=>{document.body.contains(o)&&v(o)},5e3)}function v(e){e.style.opacity="0",e.style.transform="translateX(100%)",setTimeout(()=>{document.body.contains(e)&&e.remove()},300)}const b=document.querySelectorAll(".stat-number");function w(){b.forEach(e=>{const t=e.textContent.trim();console.log("Animating stat:",t),t.includes("£2.5M+")||t.includes("99.8%")&&function(e,t,n,o=""){let a=t;const s=(n-t)/60,i=setInterval(()=>{a+=s,a>=n&&(a=n,clearInterval(i)),e.textContent=a.toFixed(1)+o},50)}(e,0,99.8,"%")})}const x=document.querySelector(".hero-stats");if(x){const z=new IntersectionObserver(function(e){e.forEach(e=>{e.isIntersecting&&(console.log("Stats section is visible, starting animation"),setTimeout(()=>{w()},500),z.unobserve(e.target))})},{threshold:.3});z.observe(x)}else console.log("Stats section not found");const E=document.querySelectorAll('img[loading="lazy"]');function L(){const e=document.createElement("canvas");return e.width=1,e.height=1,-1!==e.toDataURL("image/webp").indexOf("webp")}if("IntersectionObserver"in window){const R=new IntersectionObserver(function(e){e.forEach(e=>{if(e.isIntersecting){const t=e.target;t.dataset.src&&(t.src=t.dataset.src),t.dataset.webp&&L()&&(t.src=t.dataset.webp),t.classList.add("loaded"),t.style.opacity="1",R.unobserve(t)}})},{rootMargin:"50px 0px",threshold:.1});E.forEach(e=>{"lazy"===e.loading&&(e.style.opacity="0",e.style.transition="opacity 0.3s ease"),R.observe(e)})}const S=document.createElement("button");function A(){window.scrollY>500?(S.style.opacity="1",S.style.visibility="visible"):(S.style.opacity="0",S.style.visibility="hidden")}let I;S.innerHTML="↑",S.className="scroll-top-btn",S.style.cssText="\n position: fixed;\n bottom: 30px;\n right: 30px;\n width: 50px;\n height: 50px;\n border: none;\n border-radius: 50%;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: white;\n font-size: 20px;\n cursor: pointer;\n opacity: 0;\n visibility: hidden;\n transition: all 0.3s ease;\n z-index: 1000;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n ",document.body.appendChild(S),S.addEventListener("click",()=>{window.scrollTo({top:0,behavior:"smooth"})}),window.addEventListener("scroll",A);const k=[i,A];function q(e,t="image",n=null){const o=document.createElement("link");o.rel="preload",o.href=e,o.as=t,n&&(o.type=n),document.head.appendChild(o)}window.removeEventListener("scroll",i),window.removeEventListener("scroll",A),window.addEventListener("scroll",function(){I||(I=setTimeout(()=>{k.forEach(e=>e()),I=null},16))}),["assets/images/ukds-main-logo.png","assets/images/hero-data-analytics.svg"].forEach(e=>{L()?q(e.replace(/\.(jpg|jpeg|png)$/i,".webp"),"image","image/webp"):q(e,"image")});document.querySelectorAll("[data-tooltip]").forEach(e=>{e.addEventListener("mouseenter",function(){const e=this.getAttribute("data-tooltip"),t=document.createElement("div");t.className="tooltip",t.textContent=e,t.style.cssText="\n position: absolute;\n background: #1a1a1a;\n color: white;\n padding: 8px 12px;\n border-radius: 6px;\n font-size: 14px;\n white-space: nowrap;\n z-index: 10000;\n opacity: 0;\n transition: opacity 0.3s ease;\n pointer-events: none;\n ",document.body.appendChild(t);const n=this.getBoundingClientRect();t.style.left=n.left+n.width/2-t.offsetWidth/2+"px",t.style.top=n.top-t.offsetHeight-10+"px",setTimeout(()=>{t.style.opacity="1"},100),this.addEventListener("mouseleave",function(){t.style.opacity="0",setTimeout(()=>{document.body.contains(t)&&t.remove()},300)},{once:!0})})}),"serviceWorker"in navigator&&window.addEventListener("load",()=>{navigator.serviceWorker.register("/sw.js").then(e=>{console.log("SW registered: ",e)}).catch(e=>{console.log("SW registration failed: ",e)})}),"performance"in window&&window.addEventListener("load",function(){setTimeout(()=>{const e=performance.getEntriesByType("navigation")[0];e&&console.log("Page Load Performance:",{"DNS Lookup":Math.round(e.domainLookupEnd-e.domainLookupStart),"TCP Connection":Math.round(e.connectEnd-e.connectStart),"Request/Response":Math.round(e.responseEnd-e.requestStart),"DOM Processing":Math.round(e.domComplete-e.domLoading),"Total Load Time":Math.round(e.loadEventEnd-e.navigationStart)})},0)}),console.log("UK Data Services website initialized successfully"),console.log("Performance optimizations: Lazy loading, WebP support, and preloading enabled"),function(){const e=document.querySelector(".blog-pagination"),t=document.querySelector(".articles-grid");if(!e||!t)return;const n=e.querySelector("button:first-child"),o=e.querySelector("button:last-child"),a=e.querySelector(".pagination-info");if(!n||!o||!a)return;const s=new URLSearchParams(window.location.search);let i=parseInt(s.get("page"))||1;const r=Array.from(t.querySelectorAll(".article-card")),c=Math.ceil(r.length/6);if(c<=1)return void(e.style.display="none");function l(e,s=!1){r.forEach(e=>{e.style.display="none"});const i=6*(e-1),l=i+6;for(let e=i;e=c;const d=new URL(window.location);e>1?d.searchParams.set("page",e):d.searchParams.delete("page"),window.history.replaceState({},"",d),s&&t.scrollIntoView({behavior:"smooth",block:"start"})}n.addEventListener("click",function(e){e.preventDefault(),i>1&&(i--,l(i,!0))}),o.addEventListener("click",function(e){e.preventDefault(),i{e.forEach(e=>{if(e.isIntersecting){const n=e.target;n.dataset.src&&(n.src=n.dataset.src,n.removeAttribute("data-src")),n.dataset.webp&&new Promise(e=>{const t=new Image;t.onload=t.onerror=()=>e(2===t.height),t.src="data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA"})&&(n.src=n.dataset.webp),t.unobserve(n)}})},{rootMargin:"50px 0px",threshold:.01});document.querySelectorAll("img[data-src]").forEach(t=>{e.observe(t)})}(),"ontouchstart"in window&&(document.querySelectorAll('button, .btn, a[role="button"]').forEach(e=>{const t=e.getBoundingClientRect();(t.width<44||t.height<44)&&(e.style.minWidth="44px",e.style.minHeight="44px",e.style.display="inline-flex",e.style.alignItems="center",e.style.justifyContent="center")}),document.querySelectorAll(".nav-menu a, .social-links a").forEach(e=>{e.style.padding="12px 16px",e.style.margin="4px"}))});
\ No newline at end of file
diff --git a/index.php b/index.php
index 2dc6b9e..8523d47 100644
--- a/index.php
+++ b/index.php
@@ -5,6 +5,7 @@ ini_set('session.cookie_samesite', 'Lax');
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1');
session_start();
+$nonce = base64_encode(random_bytes(16));
// Allow private caching with revalidation (CSRF token requires session)
header("Cache-Control: public, max-age=3600, stale-while-revalidate=86400");
@@ -12,7 +13,7 @@ if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
-header('Content-Security-Policy: default-src \'self\'; script-src \'self\' \'unsafe-inline\' https://cdnjs.cloudflare.com https://www.googletagmanager.com https://www.google-analytics.com https://www.clarity.ms https://www.google.com https://www.gstatic.com; style-src \'self\' \'unsafe-inline\' https://fonts.googleapis.com; font-src \'self\' https://fonts.gstatic.com; img-src \'self\' data: https://www.google-analytics.com; connect-src \'self\' https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://www.google.com; frame-src https://www.google.com;');
+header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{$nonce}' https://cdnjs.cloudflare.com https://www.googletagmanager.com https://www.google-analytics.com https://www.clarity.ms https://www.google.com https://www.gstatic.com; style-src 'self' 'nonce-{$nonce}' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://www.google-analytics.com; connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://region1.google-analytics.com https://www.google.com; frame-src https://www.google.com;");
// SEO and performance optimizations
$page_title = "AI Automation Consulting UK | Legal & Consultancy";
@@ -71,7 +72,7 @@ $twitter_card_image = "https://ukaiautomation.co.uk/assets/images/ukaiautomation
-
-
+
@@ -119,7 +120,7 @@ $twitter_card_image = "https://ukaiautomation.co.uk/assets/images/ukaiautomation
-