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 -