|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
|
|
initSecurityHeaders(); |
|
|
initSmoothScrolling(); |
|
|
initMobileMenu(); |
|
|
initFormHandling(); |
|
|
initScrollAnimations(); |
|
|
initClassSchedule(); |
|
|
initImageLazyLoading(); |
|
|
initParallaxEffect(); |
|
|
initPrivacyControls(); |
|
|
initRateLimiting(); |
|
|
|
|
|
console.log('FitForge Pro Security-First initialized 🔒🔥'); |
|
|
}); |
|
|
|
|
|
|
|
|
function initSecurityHeaders() { |
|
|
|
|
|
const externalScripts = document.querySelectorAll('script[src]'); |
|
|
externalScripts.forEach(script => { |
|
|
const src = script.getAttribute('src'); |
|
|
|
|
|
if (src && !src.startsWith('/') && !src.includes('cdn.tailwindcss.com') && |
|
|
!src.includes('cdn.jsdelivr.net') && |
|
|
!src.includes('unpkg.com') && |
|
|
!src.includes('huggingface.co')) { |
|
|
console.warn('Untrusted script source detected:', src); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
addSecurityBadge(); |
|
|
} |
|
|
|
|
|
|
|
|
function addSecurityBadge() { |
|
|
const badge = document.createElement('div'); |
|
|
badge.className = 'fixed bottom-4 right-4 z-50 bg-green-600 text-white px-3 py-2 rounded-lg text-sm font-semibold hidden print:hidden'; |
|
|
badge.innerHTML = ` |
|
|
<div class="flex items-center gap-2"> |
|
|
<i data-feather="shield" class="w-4 h-4"></i> |
|
|
<span>Security-First Compliant</span> |
|
|
`; |
|
|
document.body.appendChild(badge); |
|
|
|
|
|
|
|
|
badge.addEventListener('mouseenter', () => { |
|
|
badge.classList.remove('hidden'); |
|
|
}); |
|
|
|
|
|
badge.addEventListener('mouseleave', () => { |
|
|
badge.classList.add('hidden'); |
|
|
}); |
|
|
} |
|
|
|
|
|
function initSmoothScrolling() { |
|
|
const links = document.querySelectorAll('a[href^="#"]'); |
|
|
|
|
|
links.forEach(link => { |
|
|
link.addEventListener('click', function(e) { |
|
|
e.preventDefault(); |
|
|
const targetId = this.getAttribute('href'); |
|
|
const targetSection = document.querySelector(targetId); |
|
|
|
|
|
if (targetSection) { |
|
|
targetSection.scrollIntoView({ |
|
|
behavior: 'smooth', |
|
|
block: 'start' |
|
|
}); |
|
|
|
|
|
|
|
|
updateActiveNavLink(targetId); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function updateActiveNavLink(targetId) { |
|
|
const navLinks = document.querySelectorAll('.nav-link'); |
|
|
navLinks.forEach(link => { |
|
|
link.classList.remove('active'); |
|
|
if (link.getAttribute('href') === targetId) { |
|
|
link.classList.add('active'); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function initMobileMenu() { |
|
|
const mobileMenuToggle = document.querySelector('.mobile-menu-toggle'); |
|
|
const mobileMenu = document.querySelector('.mobile-menu'); |
|
|
const mobileMenuClose = document.querySelector('.mobile-menu-close'); |
|
|
|
|
|
if (mobileMenuToggle && mobileMenu) { |
|
|
mobileMenuToggle.addEventListener('click', function() { |
|
|
mobileMenu.classList.add('active'); |
|
|
document.body.style.overflow = 'hidden'; |
|
|
}); |
|
|
|
|
|
mobileMenuClose.addEventListener('click', function() { |
|
|
mobileMenu.classList.remove('active'); |
|
|
document.body.style.overflow = ''; |
|
|
}); |
|
|
|
|
|
|
|
|
const mobileLinks = mobileMenu.querySelectorAll('a'); |
|
|
mobileLinks.forEach(link => { |
|
|
link.addEventListener('click', function() { |
|
|
mobileMenu.classList.remove('active'); |
|
|
document.body.style.overflow = ''; |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
function initFormHandling() { |
|
|
const forms = document.querySelectorAll('form'); |
|
|
|
|
|
forms.forEach(form => { |
|
|
|
|
|
form.setAttribute('novalidate', 'true'); |
|
|
form.setAttribute('autocomplete', 'on'); |
|
|
|
|
|
form.addEventListener('submit', function(e) { |
|
|
e.preventDefault(); |
|
|
|
|
|
|
|
|
const inputs = form.querySelectorAll('input[required], select[required]'); |
|
|
let isValid = true; |
|
|
|
|
|
inputs.forEach(input => { |
|
|
if (!validateInput(input)) { |
|
|
isValid = false; |
|
|
} |
|
|
}); |
|
|
|
|
|
if (!isValid) { |
|
|
showNotification('Please fill in all required fields correctly.', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const formData = new FormData(form); |
|
|
const formType = form.id || 'general'; |
|
|
|
|
|
|
|
|
const submitBtn = form.querySelector('button[type="submit"]'); |
|
|
const originalBtnText = submitBtn.textContent; |
|
|
submitBtn.textContent = 'Processing...'; |
|
|
submitBtn.disabled = true; |
|
|
|
|
|
|
|
|
if (!checkRateLimit('form-submission')) { |
|
|
showNotification('Please wait before submitting again.', 'warning'); |
|
|
submitBtn.textContent = originalBtnText; |
|
|
submitBtn.disabled = false; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
showNotification('Success! We\'ll contact you soon.', 'success'); |
|
|
form.reset(); |
|
|
submitBtn.textContent = originalBtnText; |
|
|
submitBtn.disabled = false; |
|
|
|
|
|
|
|
|
trackConversion(formType); |
|
|
}, 2000); |
|
|
}); |
|
|
|
|
|
|
|
|
const inputs = form.querySelectorAll('input, select, textarea'); |
|
|
inputs.forEach(input => { |
|
|
input.addEventListener('blur', function() { |
|
|
validateInput(this); |
|
|
}); |
|
|
|
|
|
input.addEventListener('input', function() { |
|
|
clearValidationError(this); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
function validateInput(input) { |
|
|
const value = input.value.trim(); |
|
|
const type = input.type; |
|
|
const name = input.name || input.getAttribute('aria-label') || 'field'; |
|
|
let isValid = true; |
|
|
let errorMessage = ''; |
|
|
|
|
|
|
|
|
if (input.hasAttribute('required') && !value) { |
|
|
isValid = false; |
|
|
errorMessage = 'This field is required'; |
|
|
} |
|
|
|
|
|
|
|
|
if ((type === 'email' || name.toLowerCase().includes('email')) && value) { |
|
|
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])+$/; |
|
|
if (!emailRegex.test(value)) { |
|
|
isValid = false; |
|
|
errorMessage = 'Please enter a valid email address'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if ((type === 'tel' || name.toLowerCase().includes('phone')) && value) { |
|
|
const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/; |
|
|
if (!phoneRegex.test(value.replace(/\s/g, ''))) { |
|
|
isValid = false; |
|
|
errorMessage = 'Please enter a valid phone number'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (value) { |
|
|
|
|
|
const sanitizedValue = value.replace(/</g, '<').replace(/>/g, '>'); |
|
|
input.value = sanitizedValue; |
|
|
} |
|
|
|
|
|
|
|
|
if (isValid) { |
|
|
input.classList.remove('input-error'); |
|
|
input.classList.add('input-success'); |
|
|
input.setAttribute('aria-invalid', 'false'); |
|
|
} else { |
|
|
input.classList.remove('input-success'); |
|
|
input.classList.add('input-error'); |
|
|
input.setAttribute('aria-invalid', 'true'); |
|
|
input.setAttribute('aria-describedby', `${input.id}-error`); |
|
|
showFieldError(input, errorMessage); |
|
|
} |
|
|
|
|
|
return isValid; |
|
|
} |
|
|
|
|
|
function showFieldError(input, message) { |
|
|
removeExistingError(input); |
|
|
|
|
|
const errorDiv = document.createElement('div'); |
|
|
errorDiv.className = 'field-error text-red-400 text-sm mt-1'; |
|
|
errorDiv.textContent = message; |
|
|
|
|
|
input.parentNode.appendChild(errorDiv); |
|
|
} |
|
|
|
|
|
|
|
|
function clearValidationError(input) { |
|
|
input.classList.remove('input-error', 'input-success'); |
|
|
removeExistingError(input); |
|
|
} |
|
|
|
|
|
|
|
|
function removeExistingError(input) { |
|
|
const existingError = input.parentNode.querySelector('.field-error'); |
|
|
if (existingError) { |
|
|
existingError.remove(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function initScrollAnimations() { |
|
|
const observerOptions = { |
|
|
threshold: 0.1, |
|
|
rootMargin: '0px 0px -50px 0px' |
|
|
}; |
|
|
|
|
|
const observer = new IntersectionObserver((entries) => { |
|
|
entries.forEach(entry => { |
|
|
if (entry.isIntersecting) { |
|
|
entry.target.classList.add('animate-fade-in-up'); |
|
|
} |
|
|
}); |
|
|
}, observerOptions); |
|
|
|
|
|
|
|
|
const animateElements = document.querySelectorAll('.class-card, .trainer-card, .pricing-card, .transformation-card, .facility-card'); |
|
|
animateElements.forEach(el => { |
|
|
observer.observe(el); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function initClassSchedule() { |
|
|
const classCards = document.querySelectorAll('.class-card'); |
|
|
|
|
|
classCards.forEach(card => { |
|
|
card.addEventListener('click', function() { |
|
|
const className = this.querySelector('h3').textContent; |
|
|
showClassDetails(className); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function showClassDetails(className) { |
|
|
const modal = document.createElement('div'); |
|
|
modal.className = 'fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4'; |
|
|
modal.innerHTML = ` |
|
|
<div class="bg-gray-800 rounded-lg p-8 max-w-md w-full"> |
|
|
<h3 class="text-2xl font-bold text-orange-400 mb-4">${className}</h3> |
|
|
<p class="text-gray-300 mb-6">Ready to join this class? Sign up for your free trial to get started!</p> |
|
|
<div class="flex gap-4"> |
|
|
<button class="flex-1 bg-orange-500 hover:bg-orange-600 py-2 px-4 rounded-lg font-semibold transition-all" onclick="closeModal(this)"> |
|
|
Sign Up Now |
|
|
</button> |
|
|
<button class="flex-1 border border-gray-600 hover:bg-gray-700 py-2 px-4 rounded-lg font-semibold transition-all" onclick="closeModal(this)"> |
|
|
Maybe Later |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
document.body.appendChild(modal); |
|
|
document.body.style.overflow = 'hidden'; |
|
|
|
|
|
|
|
|
modal.addEventListener('click', function(e) { |
|
|
if (e.target === modal) { |
|
|
closeModal(modal); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function closeModal(element) { |
|
|
const modal = element.closest('.fixed'); |
|
|
if (modal) { |
|
|
modal.remove(); |
|
|
document.body.style.overflow = ''; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function initImageLazyLoading() { |
|
|
const images = document.querySelectorAll('img[data-src]'); |
|
|
|
|
|
const imageObserver = new IntersectionObserver((entries, observer) => { |
|
|
entries.forEach(entry => { |
|
|
if (entry.isIntersecting) { |
|
|
const img = entry.target; |
|
|
img.src = img.dataset.src; |
|
|
img.classList.remove('lazy'); |
|
|
observer.unobserve(img); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
images.forEach(img => imageObserver.observe(img)); |
|
|
} |
|
|
|
|
|
|
|
|
function initParallaxEffect() { |
|
|
const parallaxElements = document.querySelectorAll('.parallax-bg'); |
|
|
|
|
|
window.addEventListener('scroll', () => { |
|
|
const scrolled = window.pageYOffset; |
|
|
|
|
|
parallaxElements.forEach(element => { |
|
|
const rate = scrolled * -0.5; |
|
|
element.style.transform = `translateY(${rate}px)`; |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function showNotification(message, type = 'info') { |
|
|
const notification = document.createElement('div'); |
|
|
notification.className = `fixed top-4 right-4 p-4 rounded-lg z-50 max-w-sm animate-fade-in-up`; |
|
|
|
|
|
const bgColor = { |
|
|
success: 'bg-green-600', |
|
|
error: 'bg-red-600', |
|
|
warning: 'bg-yellow-600', |
|
|
info: 'bg-blue-600' |
|
|
}[type]; |
|
|
|
|
|
notification.classList.add(bgColor); |
|
|
notification.innerHTML = ` |
|
|
<div class="flex items-center justify-between"> |
|
|
<span>${message}</span> |
|
|
<button class="ml-4 text-white hover:text-gray-200" onclick="this.parentElement.parentElement.remove()"> |
|
|
<i data-feather="x" class="w-5 h-5"></i> |
|
|
</button> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
document.body.appendChild(notification); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
if (notification.parentElement) { |
|
|
notification.remove(); |
|
|
} |
|
|
}, 5000); |
|
|
|
|
|
|
|
|
if (typeof feather !== 'undefined') { |
|
|
feather.replace(); |
|
|
} |
|
|
} |
|
|
|
|
|
function trackConversion(formType) { |
|
|
|
|
|
const timestamp = Date.now(); |
|
|
const eventId = `conversion_${formType}_${timestamp}`; |
|
|
console.log(`Secure conversion tracked: ${formType}`, eventId); |
|
|
|
|
|
|
|
|
|
|
|
if (typeof gtag !== 'undefined') { |
|
|
gtag('event', 'conversion', { |
|
|
'send_to': 'AW-XXXXXX/XXXXX', |
|
|
'anonymize_ip': true, |
|
|
'allow_ad_personalization_signals': false |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
function debounce(func, wait) { |
|
|
let timeout; |
|
|
return function executedFunction(...args) { |
|
|
const later = () => { |
|
|
clearTimeout(timeout); |
|
|
func(...args); |
|
|
}; |
|
|
clearTimeout(timeout); |
|
|
timeout = setTimeout(later, wait); |
|
|
}; |
|
|
} |
|
|
|
|
|
function throttle(func, limit) { |
|
|
let inThrottle; |
|
|
return function() { |
|
|
const args = arguments; |
|
|
const context = this; |
|
|
if (!inThrottle) { |
|
|
func.apply(context, args); |
|
|
inThrottle = true; |
|
|
setTimeout(() => inThrottle = false, limit); |
|
|
} |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
function updateFooterYear() { |
|
|
const yearElements = document.querySelectorAll('.current-year'); |
|
|
yearElements.forEach(el => { |
|
|
el.textContent = new Date().getFullYear(); |
|
|
}); |
|
|
} |
|
|
|
|
|
updateFooterYear(); |
|
|
|
|
|
window.addEventListener('load', function() { |
|
|
const loadTime = performance.timing.loadEventEnd - performance.timing.navigationStart; |
|
|
console.log(`Secure page loaded in ${loadTime}ms`); |
|
|
|
|
|
|
|
|
if ('webVitals' in window) { |
|
|
webVitals.getCLS(console.log); |
|
|
webVitals.getFID(console.log); |
|
|
webVitals.getFCP(console.log); |
|
|
webVitals.getLCP(console.log); |
|
|
webVitals.getFID(console.log); |
|
|
webVitals.getTTFB(console.log); |
|
|
} |
|
|
}); |
|
|
|
|
|
if ('serviceWorker' in navigator) { |
|
|
window.addEventListener('load', function() { |
|
|
navigator.serviceWorker.register('/sw.js').then(function(registration) { |
|
|
console.log('ServiceWorker registration successful'); |
|
|
}, function(err) { |
|
|
console.log('ServiceWorker registration failed: ', err); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { |
|
|
document.documentElement.classList.add('dark-mode'); |
|
|
} |
|
|
|
|
|
|
|
|
window.addEventListener('online', function() { |
|
|
showNotification('You\'re back online!', 'success'); |
|
|
}); |
|
|
|
|
|
window.addEventListener('offline', function() { |
|
|
showNotification('You\'re offline. Some features may not work.', 'warning'); |
|
|
}); |
|
|
|
|
|
function initPrivacyControls() { |
|
|
|
|
|
const originalConsoleLog = console.log; |
|
|
console.log = function(...args) { |
|
|
|
|
|
const sanitizedArgs = args.map(arg => { |
|
|
if (typeof arg === 'string') { |
|
|
|
|
|
return arg.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[EMAIL_REDACTED]'); |
|
|
} |
|
|
return arg; |
|
|
}); |
|
|
originalConsoleLog.apply(console, sanitizedArgs); |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
function initRateLimiting() { |
|
|
window.rateLimitStore = window.rateLimitStore || new Map(); |
|
|
|
|
|
|
|
|
setInterval(() => { |
|
|
const now = Date.now(); |
|
|
window.rateLimitStore.forEach((timestamp, key) => { |
|
|
if (now - timestamp > 60000) { |
|
|
window.rateLimitStore.delete(key); |
|
|
} |
|
|
}, 60000); |
|
|
} |
|
|
|
|
|
|
|
|
function checkRateLimit(action) { |
|
|
const key = `${action}_${getUserIdentifier()}`; |
|
|
const now = Date.now(); |
|
|
|
|
|
if (window.rateLimitStore.has(key)) { |
|
|
const lastAttempt = window.rateLimitStore.get(key); |
|
|
if (now - lastAttempt < 5000) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
window.rateLimitStore.set(key, now); |
|
|
return true; |
|
|
} |
|
|
|
|
|
function getUserIdentifier() { |
|
|
|
|
|
return 'session_' + (sessionStorage.getItem('sessionId') || 'default'); |
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
window.FitForgeApp = { |
|
|
showNotification, |
|
|
trackConversion, |
|
|
validateInput, |
|
|
closeModal, |
|
|
debounce, |
|
|
throttle, |
|
|
checkRateLimit |
|
|
}; |
|
|
|