Spaces:
Running
Running
| // Main JavaScript for Deployr Landing Page | |
| // Initialize when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', function() { | |
| initializeAnimations(); | |
| initializeScrollEffects(); | |
| initializeParallax(); | |
| initializeParticles(); | |
| initializeTypingEffect(); | |
| initializeCountUp(); | |
| initializeModalHandlers(); | |
| }); | |
| // Initialize animations | |
| function initializeAnimations() { | |
| // Intersection Observer for fade-in animations | |
| 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'); | |
| observer.unobserve(entry.target); | |
| } | |
| }); | |
| }, observerOptions); | |
| // Observe all service cards and sections | |
| document.querySelectorAll('.service-card, section > div').forEach(el => { | |
| observer.observe(el); | |
| }); | |
| } | |
| // Scroll effects | |
| function initializeScrollEffects() { | |
| let lastScrollY = window.scrollY; | |
| let ticking = false; | |
| function updateScrollEffects() { | |
| const scrollY = window.scrollY; | |
| const navbar = document.querySelector('nav'); | |
| // Navbar background on scroll | |
| if (scrollY > 50) { | |
| navbar.classList.add('bg-dark/95', 'backdrop-blur-lg', 'shadow-lg'); | |
| } else { | |
| navbar.classList.remove('bg-dark/95', 'backdrop-blur-lg', 'shadow-lg'); | |
| } | |
| lastScrollY = scrollY; | |
| ticking = false; | |
| } | |
| function requestTick() { | |
| if (!ticking) { | |
| window.requestAnimationFrame(updateScrollEffects); | |
| ticking = true; | |
| } | |
| } | |
| window.addEventListener('scroll', requestTick); | |
| } | |
| // Parallax effect | |
| function initializeParallax() { | |
| const parallaxElements = document.querySelectorAll('[data-parallax]'); | |
| function updateParallax() { | |
| const scrolled = window.pageYOffset; | |
| parallaxElements.forEach(element => { | |
| const speed = element.dataset.speed || 0.5; | |
| const yPos = -(scrolled * speed); | |
| element.style.transform = `translateY(${yPos}px)`; | |
| }); | |
| } | |
| window.addEventListener('scroll', updateParallax); | |
| } | |
| // Particle effects | |
| function initializeParticles() { | |
| const particleContainer = document.createElement('div'); | |
| particleContainer.className = 'fixed inset-0 pointer-events-none z-0'; | |
| document.body.appendChild(particleContainer); | |
| for (let i = 0; i < 20; i++) { | |
| setTimeout(() => { | |
| const particle = document.createElement('div'); | |
| particle.className = 'particle'; | |
| particle.style.left = Math.random() * 100 + '%'; | |
| particle.style.animationDelay = Math.random() * 10 + 's'; | |
| particle.style.animationDuration = (10 + Math.random() * 10) + 's'; | |
| particleContainer.appendChild(particle); | |
| }, i * 200); | |
| } | |
| } | |
| // Typing effect | |
| function initializeTypingEffect() { | |
| const typingElement = document.querySelector('[data-typing]'); | |
| if (!typingElement) return; | |
| const text = typingElement.getAttribute('data-typing'); | |
| let index = 0; | |
| function type() { | |
| if (index < text.length) { | |
| typingElement.textContent += text.charAt(index); | |
| index++; | |
| setTimeout(type, 100); | |
| } | |
| } | |
| type(); | |
| } | |
| // Count up animation | |
| function initializeCountUp() { | |
| const countElements = document.querySelectorAll('[data-count]'); | |
| const countObserver = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| const target = parseInt(entry.target.getAttribute('data-count')); | |
| const duration = 2000; | |
| const increment = target / (duration / 16); | |
| let current = 0; | |
| const updateCount = () => { | |
| if (current < target) { | |
| current += increment; | |
| entry.target.textContent = Math.floor(current); | |
| requestAnimationFrame(updateCount); | |
| } else { | |
| entry.target.textContent = target; | |
| } | |
| }; | |
| updateCount(); | |
| countObserver.unobserve(entry.target); | |
| } | |
| }); | |
| }); | |
| countElements.forEach(el => countObserver.observe(el)); | |
| } | |
| // Modal handlers | |
| function initializeModalHandlers() { | |
| // Open modal buttons | |
| document.querySelectorAll('[data-modal]').forEach(button => { | |
| button.addEventListener('click', () => { | |
| const modalId = button.getAttribute('data-modal'); | |
| const modal = document.getElementById(modalId); | |
| if (modal) { | |
| modal.classList.remove('hidden'); | |
| document.body.style.overflow = 'hidden'; | |
| } | |
| }); | |
| }); | |
| // Close modal buttons | |
| document.querySelectorAll('[data-close-modal]').forEach(button => { | |
| button.addEventListener('click', () => { | |
| const modal = button.closest('.modal'); | |
| if (modal) { | |
| modal.classList.add('hidden'); | |
| document.body.style.overflow = 'auto'; | |
| } | |
| }); | |
| }); | |
| // Close modal on backdrop click | |
| document.querySelectorAll('.modal').forEach(modal => { | |
| modal.addEventListener('click', (e) => { | |
| if (e.target === modal) { | |
| modal.classList.add('hidden'); | |
| document.body.style.overflow = 'auto'; | |
| } | |
| }); | |
| }); | |
| } | |
| // Smooth scroll for anchor links | |
| document.querySelectorAll('a[href^="#"]').forEach(anchor => { | |
| anchor.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| const target = document.querySelector(this.getAttribute('href')); | |
| if (target) { | |
| target.scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'start' | |
| }); | |
| } | |
| }); | |
| }); | |
| // Form validation | |
| function validateForm(formId) { | |
| const form = document.getElementById(formId); | |
| if (!form) return false; | |
| const inputs = form.querySelectorAll('input[required], textarea[required]'); | |
| let isValid = true; | |
| inputs.forEach(input => { | |
| if (!input.value.trim()) { | |
| input.classList.add('border-red-500'); | |
| isValid = false; | |
| } else { | |
| input.classList.remove('border-red-500'); | |
| } | |
| }); | |
| return isValid; | |
| } | |
| // Loading state for buttons | |
| function setLoading(buttonId, loading = true) { | |
| const button = document.getElementById(buttonId); | |
| if (!button) return; | |
| if (loading) { | |
| button.disabled = true; | |
| button.classList.add('opacity-50', 'cursor-not-allowed'); | |
| button.innerHTML = '<span class="loading-spinner inline-block mr-2"></span>Loading...'; | |
| } else { | |
| button.disabled = false; | |
| button.classList.remove('opacity-50', 'cursor-not-allowed'); | |
| button.innerHTML = button.getAttribute('data-original-text') || 'Submit'; | |
| } | |
| } | |
| // Toast notification | |
| function showToast(message, type = 'success') { | |
| const toast = document.createElement('div'); | |
| toast.className = `fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg transform transition-all duration-300 z-50 ${ | |
| type === 'success' ? 'bg-green-500' : 'bg-red-500' | |
| } text-white`; | |
| toast.textContent = message; | |
| document.body.appendChild(toast); | |
| setTimeout(() => { | |
| toast.classList.add('translate-y-0', 'opacity-100'); | |
| }, 100); | |
| setTimeout(() => { | |
| toast.classList.add('translate-y-full', 'opacity-0'); | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| // API call helper | |
| async function apiCall(url, options = {}) { | |
| try { | |
| const response = await fetch(url, { | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| ...options.headers | |
| }, | |
| ...options | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('API call failed:', error); | |
| showToast('An error occurred. Please try again.', 'error'); | |
| throw error; | |
| } | |
| } | |
| // Initialize GSAP animations if available | |
| if (typeof gsap !== 'undefined') { | |
| gsap.registerPlugin(ScrollTrigger); | |
| // Hero section animation | |
| gsap.timeline() | |
| .from('.hero h1', { y: 50, opacity: 0, duration: 1 }) | |
| .from('.hero p', { y: 30, opacity: 0, duration: 0.8 }, '-=0.5') | |
| .from('.hero .cta', { y: 20, opacity: 0, duration: 0.6 }, '-=0.3'); | |
| } | |
| // Theme toggle (if implemented) | |
| function toggleTheme() { | |
| document.body.classList.toggle('light-theme'); | |
| const isLight = document.body.classList.contains('light-theme'); | |
| localStorage.setItem('theme', isLight ? 'light' : 'dark'); | |
| } | |
| // Load saved theme | |
| const savedTheme = localStorage.getItem('theme'); | |
| if (savedTheme === 'light') { | |
| document.body.classList.add('light-theme'); | |
| } | |
| // Analytics tracking | |
| function trackEvent(eventName, properties = {}) { | |
| if (typeof gtag !== 'undefined') { | |
| gtag('event', eventName, properties); | |
| } | |
| } | |
| // Utility debouncing function | |
| function debounce(func, wait) { | |
| let timeout; | |
| return function executedFunction(...args) { | |
| const later = () => { | |
| clearTimeout(timeout); | |
| func(...args); | |
| }; | |
| clearTimeout(timeout); | |
| timeout = setTimeout(later, wait); | |
| }; | |
| } | |
| // Optimized resize handler | |
| const optimizedResize = debounce(() => { | |
| // Handle resize events | |
| }, 100); | |
| window.addEventListener('resize', optimizedResize); | |
| // Keyboard navigation | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') { | |
| const openModal = document.querySelector('.modal:not(.hidden)'); | |
| if (openModal) { | |
| openModal.classList.add('hidden'); | |
| document.body.style.overflow = 'auto'; | |
| } | |
| } | |
| }); | |
| // Initialize tooltips | |
| function initializeTooltips() { | |
| document.querySelectorAll('[data-tooltip]').forEach(element => { | |
| element.addEventListener('mouseenter', (e) => { | |
| const tooltip = document.createElement('div'); | |
| tooltip.className = 'absolute z-50 px-2 py-1 text-xs bg-gray-800 text-white rounded'; | |
| tooltip.textContent = e.target.getAttribute('data-tooltip'); | |
| tooltip.style.bottom = '100%'; | |
| tooltip.style.left = '50%'; | |
| tooltip.style.transform = 'translateX(-50%)'; | |
| e.target.style.position = 'relative'; | |
| e.target.appendChild(tooltip); | |
| }); | |
| element.addEventListener('mouseleave', (e) => { | |
| const tooltip = e.target.querySelector('.absolute'); | |
| if (tooltip) tooltip.remove(); | |
| }); | |
| }); | |
| } | |
| // Initialize everything | |
| initializeTooltips(); |