Spaces:
Running
Running
| // Linear Portfolio Template JavaScript | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Mobile menu toggle | |
| const mobileMenuButton = document.querySelector('.mobile-menu-button'); | |
| const mobileMenu = document.querySelector('.mobile-menu'); | |
| if (mobileMenuButton && mobileMenu) { | |
| mobileMenuButton.addEventListener('click', function() { | |
| mobileMenu.classList.toggle('hidden'); | |
| }); | |
| } | |
| // Smooth scrolling for navigation 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' | |
| }); | |
| // Close mobile menu if open | |
| if (mobileMenu && !mobileMenu.classList.contains('hidden')) { | |
| mobileMenu.classList.add('hidden'); | |
| } | |
| } | |
| }); | |
| }); | |
| // Form submission handling | |
| const contactForm = document.querySelector('#contact form'); | |
| if (contactForm) { | |
| contactForm.addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| // Get form data | |
| const formData = new FormData(this); | |
| const name = formData.get('name'); | |
| const email = formData.get('email'); | |
| const company = formData.get('company'); | |
| const project = formData.get('project'); | |
| // Simple validation | |
| if (!name || !email || !project) { | |
| showNotification('Please fill in all required fields.', 'error'); | |
| return; | |
| } | |
| // Email validation | |
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
| if (!emailRegex.test(email)) { | |
| showNotification('Please enter a valid email address.', 'error'); | |
| return; | |
| } | |
| // Simulate form submission | |
| showNotification('Sending message...', 'info'); | |
| setTimeout(() => { | |
| showNotification(`Thanks ${name}! Your message has been sent.`, 'success'); | |
| this.reset(); | |
| }, 2000); | |
| }); | |
| } | |
| // Notification system | |
| function showNotification(message, type = 'info') { | |
| // Remove existing notification | |
| const existingNotification = document.querySelector('.notification'); | |
| if (existingNotification) { | |
| existingNotification.remove(); | |
| } | |
| // Create notification element | |
| const notification = document.createElement('div'); | |
| notification.className = 'notification'; | |
| // Set colors based on type | |
| let bgColor, borderColor; | |
| switch (type) { | |
| case 'success': | |
| bgColor = '#f0fdf4'; | |
| borderColor = '#bbf7d0'; | |
| break; | |
| case 'error': | |
| bgColor = '#fef2f2'; | |
| borderColor = '#fecaca'; | |
| break; | |
| default: | |
| bgColor = '#f8fafc'; | |
| borderColor = '#e2e8f0'; | |
| } | |
| notification.style.background = bgColor; | |
| notification.style.border = `1px solid ${borderColor}`; | |
| notification.innerHTML = ` | |
| <div class="flex items-center justify-between"> | |
| <span class="text-gray-900">${message}</span> | |
| <button onclick="this.parentElement.parentElement.remove()" class="ml-4 text-gray-400 hover:text-gray-600"> | |
| <i data-feather="x" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| `; | |
| // Add to page | |
| document.body.appendChild(notification); | |
| // Show notification | |
| setTimeout(() => notification.classList.add('show'), 100); | |
| // Auto hide after 5 seconds | |
| setTimeout(() => { | |
| if (notification && notification.parentElement) { | |
| notification.classList.remove('show'); | |
| setTimeout(() => notification.remove(), 300); | |
| } | |
| }, 5000); | |
| // Initialize feather icons in notification | |
| if (typeof feather !== 'undefined') { | |
| feather.replace(); | |
| } | |
| } | |
| // Intersection Observer for animations | |
| const observerOptions = { | |
| threshold: 0.1, | |
| rootMargin: '0px 0px -50px 0px' | |
| }; | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| entry.target.style.opacity = '1'; | |
| entry.target.style.transform = 'translateY(0)'; | |
| } | |
| }); | |
| }, observerOptions); | |
| // Observe elements for animation | |
| document.querySelectorAll('section').forEach(section => { | |
| section.style.opacity = '0'; | |
| section.style.transform = 'translateY(30px)'; | |
| section.style.transition = 'opacity 0.8s ease-out, transform 0.8s ease-out'; | |
| observer.observe(section); | |
| }); | |
| // Header scroll effect | |
| const header = document.querySelector('header'); | |
| let lastScrollTop = 0; | |
| window.addEventListener('scroll', function() { | |
| const scrollTop = window.pageYOffset || document.documentElement.scrollTop; | |
| if (scrollTop > 100) { | |
| header.classList.add('bg-white/95', 'backdrop-blur-sm', 'shadow-sm'); | |
| } else { | |
| header.classList.remove('shadow-sm'); | |
| } | |
| lastScrollTop = scrollTop; | |
| }); | |
| // Typing animation for hero text | |
| function typeWriter(element, text, speed = 50) { | |
| let i = 0; | |
| element.innerHTML = ''; | |
| function type() { | |
| if (i < text.length) { | |
| element.innerHTML += text.charAt(i); | |
| i++; | |
| setTimeout(type, speed); | |
| } | |
| } | |
| type(); | |
| } | |
| // Initialize typing animation on hero section | |
| const heroTitle = document.querySelector('#home h1'); | |
| if (heroTitle) { | |
| const originalText = heroTitle.innerHTML; | |
| const spans = heroTitle.querySelectorAll('span'); | |
| if (spans.length > 0) { | |
| typeWriter(spans[0], spans[0].textContent, 100); | |
| } | |
| } | |
| // Lazy loading for images | |
| const images = document.querySelectorAll('img'); | |
| const imageObserver = new IntersectionObserver((entries, observer) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| const img = entry.target; | |
| img.setAttribute('loaded', ''); | |
| observer.unobserve(img); | |
| } | |
| }); | |
| }); | |
| images.forEach(img => { | |
| img.setAttribute('loading', ''); | |
| imageObserver.observe(img); | |
| }); | |
| // Keyboard navigation support | |
| document.addEventListener('keydown', function(e) { | |
| // ESC key closes mobile menu | |
| if (e.key === 'Escape' && mobileMenu && !mobileMenu.classList.contains('hidden')) { | |
| mobileMenu.classList.add('hidden'); | |
| } | |
| }); | |
| // Performance optimization: Debounce scroll events | |
| function debounce(func, wait) { | |
| let timeout; | |
| return function executedFunction(...args) { | |
| const later = () => { | |
| clearTimeout(timeout); | |
| func(...args); | |
| }; | |
| clearTimeout(timeout); | |
| timeout = setTimeout(later, wait); | |
| }; | |
| } | |
| // Apply debounce to scroll handler | |
| const debouncedScrollHandler = debounce(function() { | |
| const scrollTop = window.pageYOffset || document.documentElement.scrollTop; | |
| if (scrollTop > 100) { | |
| header.classList.add('shadow-sm'); | |
| } else { | |
| header.classList.remove('shadow-sm'); | |
| } | |
| }, 10); | |
| window.addEventListener('scroll', debouncedScrollHandler); | |
| // Initialize feather icons | |
| if (typeof feather !== 'undefined') { | |
| feather.replace(); | |
| } | |
| console.log('Linear Portfolio Template initialized successfully!'); | |
| }); |