/** * Dynamic Content Loader * Handles lazy loading of content, interactive cards, and tabs */ document.addEventListener('DOMContentLoaded', function() { // Initialize lazy loading for images initLazyLoading(); // Initialize interactive cards initInteractiveCards(); // Initialize tabs if they exist initTabSystem(); // Initialize accordion components initAccordion(); // Initialize dynamic counters initCounters(); // Initialize dynamic content filter initContentFilter(); }); // Lazy load images as they enter viewport function initLazyLoading() { const lazyImages = document.querySelectorAll('.lazy-image'); if ('IntersectionObserver' in window) { const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; // Replace src with data-src if (img.dataset.src) { img.src = img.dataset.src; // Add fade-in effect img.classList.add('image-loaded'); } // Stop observing after loading imageObserver.unobserve(img); } }); }, { rootMargin: '50px', threshold: 0.1 }); // Observe all lazy images lazyImages.forEach(img => { imageObserver.observe(img); }); } else { // Fallback for browsers without IntersectionObserver lazyImages.forEach(img => { if (img.dataset.src) { img.src = img.dataset.src; } }); } } // Interactive hover and click effects for cards function initInteractiveCards() { const cards = document.querySelectorAll('.card, .service-card, .exp-item, .team-member'); cards.forEach(card => { // 3D tilt effect on mouse move card.addEventListener('mousemove', function(e) { const rect = this.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Calculate tilt based on mouse position const centerX = rect.width / 2; const centerY = rect.height / 2; const tiltX = (centerY - y) / 20; const tiltY = (x - centerX) / 20; // Apply 3D transform this.style.transform = `perspective(1000px) rotateX(${tiltX}deg) rotateY(${tiltY}deg) scale3d(1.02, 1.02, 1.02)`; // Add highlight effect to follow cursor const highlight = this.querySelector('.card-highlight') || document.createElement('div'); if (!this.querySelector('.card-highlight')) { highlight.className = 'card-highlight'; this.appendChild(highlight); } highlight.style.opacity = '1'; highlight.style.transform = `translate(${x}px, ${y}px)`; }); // Reset on mouse leave card.addEventListener('mouseleave', function() { this.style.transform = ''; const highlight = this.querySelector('.card-highlight'); if (highlight) { highlight.style.opacity = '0'; } }); // Click effect card.addEventListener('click', function() { this.classList.add('card-clicked'); setTimeout(() => { this.classList.remove('card-clicked'); }, 300); }); }); } // Tab system for content switching function initTabSystem() { const tabContainers = document.querySelectorAll('.tab-container'); tabContainers.forEach(container => { const tabsNav = container.querySelector('.tabs-nav'); const tabsContent = container.querySelector('.tabs-content'); if (!tabsNav || !tabsContent) return; const tabs = tabsNav.querySelectorAll('.tab'); const tabPanels = tabsContent.querySelectorAll('.tab-panel'); // Initialize first tab as active if (tabs.length > 0 && tabPanels.length > 0) { tabs[0].classList.add('active-tab'); tabPanels[0].classList.add('active-panel'); } // Add click handlers to tabs tabs.forEach((tab, index) => { tab.addEventListener('click', function() { // Remove active class from all tabs and panels tabs.forEach(t => t.classList.remove('active-tab')); tabPanels.forEach(panel => panel.classList.remove('active-panel')); // Add active class to current tab and panel this.classList.add('active-tab'); if (tabPanels[index]) { tabPanels[index].classList.add('active-panel'); } // Add entrance animation for tab content if (tabPanels[index]) { tabPanels[index].classList.add('panel-entering'); setTimeout(() => { tabPanels[index].classList.remove('panel-entering'); }, 500); } // Show active tab indicator const indicator = tabsNav.querySelector('.tab-indicator') || document.createElement('div'); if (!tabsNav.querySelector('.tab-indicator')) { indicator.className = 'tab-indicator'; tabsNav.appendChild(indicator); } // Position indicator below active tab const tabRect = this.getBoundingClientRect(); const navRect = tabsNav.getBoundingClientRect(); indicator.style.width = `${tabRect.width}px`; indicator.style.left = `${tabRect.left - navRect.left}px`; }); }); // Trigger indicator positioning for first tab if (tabs[0]) { setTimeout(() => { tabs[0].click(); }, 100); } }); } // Accordion functionality for FAQ or content sections function initAccordion() { const accordions = document.querySelectorAll('.accordion'); accordions.forEach(accordion => { const items = accordion.querySelectorAll('.accordion-item'); items.forEach(item => { const header = item.querySelector('.accordion-header'); const content = item.querySelector('.accordion-content'); if (!header || !content) return; // Set initial state content.style.maxHeight = '0px'; content.style.overflow = 'hidden'; content.style.transition = 'max-height 0.3s ease'; // Toggle accordion on click header.addEventListener('click', function() { // Check if this item is already open const isOpen = item.classList.contains('accordion-open'); // Close all items items.forEach(otherItem => { otherItem.classList.remove('accordion-open'); const otherContent = otherItem.querySelector('.accordion-content'); if (otherContent) { otherContent.style.maxHeight = '0px'; } }); // Open this item if it wasn't already open if (!isOpen) { item.classList.add('accordion-open'); content.style.maxHeight = `${content.scrollHeight}px`; } // Add indicator animation const indicator = header.querySelector('.accordion-indicator') || document.createElement('div'); if (!header.querySelector('.accordion-indicator')) { indicator.className = 'accordion-indicator'; indicator.innerHTML = '+'; header.appendChild(indicator); } // Update indicator indicator.innerHTML = item.classList.contains('accordion-open') ? '−' : '+'; }); }); // Open first item by default if specified if (accordion.classList.contains('first-open') && items[0]) { setTimeout(() => { items[0].querySelector('.accordion-header').click(); }, 100); } }); } // Animated counters that increment when in view function initCounters() { const counters = document.querySelectorAll('.counter'); if (!counters.length) return; const counterObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const counter = entry.target; const targetValue = parseInt(counter.getAttribute('data-target')); const duration = parseInt(counter.getAttribute('data-duration')) || 2000; // Prevent multiple animations if (counter.classList.contains('counting')) return; counter.classList.add('counting'); animateCounter(counter, targetValue, duration); // Stop observing after animation counterObserver.unobserve(counter); } }); }, { threshold: 0.2 }); // Observe all counters counters.forEach(counter => { counterObserver.observe(counter); }); // Counter animation function function animateCounter(counter, target, duration) { let startTime = null; const startValue = 0; function step(timestamp) { if (!startTime) startTime = timestamp; const progress = Math.min((timestamp - startTime) / duration, 1); const currentValue = Math.floor(progress * (target - startValue) + startValue); counter.textContent = formatNumber(currentValue); if (progress < 1) { window.requestAnimationFrame(step); } else { counter.textContent = formatNumber(target); counter.classList.add('counter-complete'); } } window.requestAnimationFrame(step); } // Format number with commas function formatNumber(num) { return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); } } // Content filtering system for portfolios or galleries function initContentFilter() { const filterContainers = document.querySelectorAll('.filter-container'); filterContainers.forEach(container => { const filterButtons = container.querySelectorAll('.filter-button'); const filterItems = container.querySelectorAll('.filter-item'); if (!filterButtons.length || !filterItems.length) return; // Click handler for filter buttons filterButtons.forEach(button => { button.addEventListener('click', function() { // Remove active class from all buttons filterButtons.forEach(btn => { btn.classList.remove('active-filter'); }); // Add active class to clicked button this.classList.add('active-filter'); // Get filter category const filterCategory = this.getAttribute('data-filter'); // Filter items filterItems.forEach(item => { const itemCategories = item.getAttribute('data-categories'); if (!itemCategories) return; // Check if item matches filter if (filterCategory === 'all' || itemCategories.includes(filterCategory)) { // Show item with animation item.style.opacity = '0'; item.style.transform = 'scale(0.8)'; item.style.display = 'block'; setTimeout(() => { item.style.opacity = '1'; item.style.transform = 'scale(1)'; }, 50); } else { // Hide item with animation item.style.opacity = '0'; item.style.transform = 'scale(0.8)'; setTimeout(() => { item.style.display = 'none'; }, 300); } }); }); }); // Set "All" filter as active by default const allFilterButton = container.querySelector('.filter-button[data-filter="all"]'); if (allFilterButton) { allFilterButton.click(); } }); }