Spaces:
Running
Running
| const AppState = { | |
| currentTheme: 'dark', | |
| currentSection: 'home', | |
| isMenuOpen: false, | |
| isLoaded: false | |
| }; | |
| document.addEventListener('DOMContentLoaded', () => { | |
| initializeApp(); | |
| }); | |
| function initializeApp() { | |
| loadPreferences(); | |
| initTheme(); | |
| initNavigation(); | |
| initScrollEffects(); | |
| initFormHandlers(); | |
| initMobileMenu(); | |
| updateThemeUI(); | |
| AppState.isLoaded = true; | |
| } | |
| function loadPreferences() { | |
| const savedTheme = localStorage.getItem('portfolio-theme'); | |
| if (savedTheme) { | |
| AppState.currentTheme = savedTheme; | |
| } | |
| } | |
| function initTheme() { | |
| const themeToggle = document.getElementById('themeToggle'); | |
| if (themeToggle) { | |
| themeToggle.addEventListener('click', toggleTheme); | |
| } | |
| setTheme(AppState.currentTheme); | |
| } | |
| function toggleTheme() { | |
| const newTheme = AppState.currentTheme === 'dark' ? 'light' : 'dark'; | |
| setTheme(newTheme); | |
| localStorage.setItem('portfolio-theme', newTheme); | |
| } | |
| function setTheme(theme) { | |
| AppState.currentTheme = theme; | |
| document.body.setAttribute('data-theme', theme); | |
| updateThemeUI(); | |
| } | |
| function updateThemeUI() { | |
| const themeToggle = document.getElementById('themeToggle'); | |
| if (themeToggle) { | |
| const icon = themeToggle.querySelector('i'); | |
| if (icon) { | |
| icon.className = AppState.currentTheme === 'dark' ? 'fas fa-sun' : 'fas fa-moon'; | |
| } | |
| } | |
| } | |
| function initNavigation() { | |
| const navLinks = document.querySelectorAll('.nav-link[href^="#"]'); | |
| navLinks.forEach(link => { | |
| link.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const targetId = link.getAttribute('href'); | |
| const targetSection = document.querySelector(targetId); | |
| if (targetSection) { | |
| const headerHeight = document.querySelector('.main-header').offsetHeight; | |
| const targetPosition = targetSection.offsetTop - headerHeight; | |
| window.scrollTo({ | |
| top: targetPosition, | |
| behavior: 'smooth' | |
| }); | |
| updateActiveNavLink(link); | |
| if (AppState.isMenuOpen) { | |
| toggleMobileMenu(); | |
| } | |
| } | |
| }); | |
| }); | |
| window.addEventListener('scroll', handleScroll); | |
| window.addEventListener('scroll', updateHeaderOnScroll); | |
| } | |
| function handleScroll() { | |
| const sections = document.querySelectorAll('section[id]'); | |
| const scrollPosition = window.scrollY + 100; | |
| sections.forEach(section => { | |
| const sectionTop = section.offsetTop; | |
| const sectionHeight = section.offsetHeight; | |
| const sectionId = section.getAttribute('id'); | |
| if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) { | |
| AppState.currentSection = sectionId; | |
| updateActiveNavLink(null, sectionId); | |
| } | |
| }); | |
| } | |
| function updateActiveNavLink(clickedLink, sectionId = null) { | |
| const navLinks = document.querySelectorAll('.nav-link'); | |
| navLinks.forEach(link => { | |
| link.classList.remove('active'); | |
| if (clickedLink && link === clickedLink) { | |
| link.classList.add('active'); | |
| } else if (sectionId) { | |
| const linkSection = link.getAttribute('data-section'); | |
| if (linkSection === sectionId) { | |
| link.classList.add('active'); | |
| } | |
| } | |
| }); | |
| } | |
| function updateHeaderOnScroll() { | |
| const header = document.querySelector('.main-header'); | |
| if (window.scrollY > 50) { | |
| header.classList.add('scrolled'); | |
| } else { | |
| header.classList.remove('scrolled'); | |
| } | |
| } | |
| function initScrollEffects() { | |
| const observerOptions = { | |
| threshold: 0.1, | |
| rootMargin: '0px 0px -100px 0px' | |
| }; | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| entry.target.classList.add('visible'); | |
| observer.unobserve(entry.target); | |
| } | |
| }); | |
| }, observerOptions); | |
| const fadeElements = document.querySelectorAll('.fade-in'); | |
| fadeElements.forEach(element => observer.observe(element)); | |
| const sections = document.querySelectorAll('.section'); | |
| sections.forEach(section => observer.observe(section)); | |
| } | |
| function initFormHandlers() { | |
| const contactForm = document.getElementById('contactForm'); | |
| if (contactForm) { | |
| contactForm.addEventListener('submit', handleFormSubmit); | |
| } | |
| } | |
| function handleFormSubmit(e) { | |
| e.preventDefault(); | |
| const formData = new FormData(e.target); | |
| const data = Object.fromEntries(formData); | |
| console.log('Form submitted:', data); | |
| const message = 'Message sent successfully!'; | |
| alert(message); | |
| e.target.reset(); | |
| } | |
| function initMobileMenu() { | |
| const menuToggle = document.getElementById('menuToggle'); | |
| if (menuToggle) { | |
| menuToggle.addEventListener('click', toggleMobileMenu); | |
| } | |
| document.addEventListener('click', (e) => { | |
| const navMenu = document.getElementById('navMenu'); | |
| const menuToggle = document.getElementById('menuToggle'); | |
| if (AppState.isMenuOpen && | |
| !navMenu.contains(e.target) && | |
| !menuToggle.contains(e.target)) { | |
| toggleMobileMenu(); | |
| } | |
| }); | |
| } | |
| function toggleMobileMenu() { | |
| AppState.isMenuOpen = !AppState.isMenuOpen; | |
| const navMenu = document.getElementById('navMenu'); | |
| const menuToggle = document.getElementById('menuToggle'); | |
| if (navMenu) { | |
| navMenu.classList.toggle('active', AppState.isMenuOpen); | |
| } | |
| if (menuToggle) { | |
| menuToggle.classList.toggle('active', AppState.isMenuOpen); | |
| } | |
| } | |
| function generateParticles() { | |
| const particlesContainer = document.getElementById('particles'); | |
| if (!particlesContainer) return; | |
| const codeSymbols = ['{', '}', '[', ']', '(', ')', '<', '>', '/', '*', '=', '+', '-', ';', ':', '&', '|', '%', '$', '#', '@']; | |
| const particleCount = 20; | |
| for (let i = 0; i < particleCount; i++) { | |
| const particle = document.createElement('div'); | |
| particle.className = 'particle'; | |
| particle.textContent = codeSymbols[Math.floor(Math.random() * codeSymbols.length)]; | |
| particle.style.left = Math.random() * 100 + '%'; | |
| particle.style.animationDelay = Math.random() * 15 + 's'; | |
| particle.style.animationDuration = (10 + Math.random() * 10) + 's'; | |
| particlesContainer.appendChild(particle); | |
| } | |
| } | |
| document.addEventListener('DOMContentLoaded', () => { | |
| generateParticles(); | |
| }); | |
| //--------------animations.js----------------- | |
| function inView(element, callback, options = {}) { | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| callback(entry); | |
| if (options.once !== false) { | |
| observer.unobserve(entry.target); | |
| } | |
| } | |
| }); | |
| }, { | |
| threshold: options.amount || 0.1, | |
| rootMargin: options.rootMargin || '0px' | |
| }); | |
| observer.observe(element); | |
| return () => observer.unobserve(element); | |
| } | |
| function animateElement(element, props, options = {}) { | |
| if (typeof anime === 'undefined') return; | |
| const animeProps = {}; | |
| if (props.opacity) animeProps.opacity = props.opacity; | |
| if (props.x !== undefined) animeProps.translateX = props.x; | |
| if (props.y !== undefined) animeProps.translateY = props.y; | |
| if (props.scale) animeProps.scale = props.scale; | |
| return anime({ | |
| targets: element, | |
| ...animeProps, | |
| duration: (options.duration || 0.8) * 1000, | |
| delay: (options.delay || 0) * 1000, | |
| easing: options.easing || 'easeOutExpo' | |
| }); | |
| } | |
| window.addEventListener('load', () => { | |
| setTimeout(() => { | |
| initLoaderAnimation(); | |
| }, 100); | |
| }); | |
| function initLoaderAnimation() { | |
| const loader = document.getElementById('loader'); | |
| const loaderPercent = document.getElementById('loaderPercent'); | |
| if (!loader || !loaderPercent) return; | |
| let progress = 0; | |
| const progressInterval = setInterval(() => { | |
| progress += Math.random() * 15; | |
| if (progress >= 100) { | |
| progress = 100; | |
| clearInterval(progressInterval); | |
| setTimeout(() => { | |
| if (typeof anime !== 'undefined') { | |
| anime({ | |
| targets: loader, | |
| opacity: [1, 0], | |
| duration: 500, | |
| easing: 'easeInOutQuad', | |
| complete: () => { | |
| loader.classList.add('hidden'); | |
| initPageAnimations(); | |
| } | |
| }); | |
| } else { | |
| loader.classList.add('hidden'); | |
| initPageAnimations(); | |
| } | |
| }, 300); | |
| } | |
| if (loaderPercent) { | |
| loaderPercent.textContent = Math.floor(progress) + '%'; | |
| } | |
| }, 100); | |
| } | |
| function initPageAnimations() { | |
| setTimeout(() => { | |
| initHeroAnimations(); | |
| initSkillAnimations(); | |
| initTimelineAnimations(); | |
| initProjectAnimations(); | |
| initScrollAnimations(); | |
| initContactAnimations(); | |
| animateStats(); | |
| initParallax(); | |
| initSmoothScroll(); | |
| }, 300); | |
| } | |
| function initHeroAnimations() { | |
| if (typeof anime === 'undefined') return; | |
| const heroName = document.getElementById('heroName'); | |
| if (heroName) { | |
| const nameValue = heroName.querySelector('.name-value'); | |
| if (nameValue) { | |
| const originalText = nameValue.textContent; | |
| nameValue.textContent = ''; | |
| anime({ | |
| targets: { value: 0 }, | |
| value: originalText.length, | |
| duration: 1500, | |
| delay: 500, | |
| easing: 'easeInOutQuad', | |
| update: function(anim) { | |
| const length = Math.floor(anim.animatables[0].target.value); | |
| nameValue.textContent = originalText.substring(0, length); | |
| }, | |
| complete: () => { | |
| const cursor = document.createElement('span'); | |
| cursor.className = 'name-cursor'; | |
| cursor.textContent = '|'; | |
| cursor.style.animation = 'blink 1s infinite'; | |
| nameValue.appendChild(cursor); | |
| setTimeout(() => cursor.remove(), 2000); | |
| } | |
| }); | |
| } | |
| } | |
| const heroTitle = document.querySelector('.hero-title'); | |
| if (heroTitle) { | |
| anime({ | |
| targets: heroTitle, | |
| opacity: [0, 1], | |
| translateX: [-30, 0], | |
| delay: 800, | |
| duration: 1000, | |
| easing: 'easeOutExpo' | |
| }); | |
| } | |
| const heroDescription = document.querySelector('.hero-description'); | |
| if (heroDescription) { | |
| anime({ | |
| targets: heroDescription, | |
| opacity: [0, 1], | |
| translateY: [20, 0], | |
| delay: 1200, | |
| duration: 1000, | |
| easing: 'easeOutExpo' | |
| }); | |
| } | |
| const heroButtons = document.querySelectorAll('.hero-buttons .btn'); | |
| if (heroButtons.length > 0) { | |
| anime({ | |
| targets: heroButtons, | |
| opacity: [0, 1], | |
| scale: [0.8, 1], | |
| delay: anime.stagger(100, {start: 1500}), | |
| duration: 800, | |
| easing: 'easeOutBack' | |
| }); | |
| } | |
| const socialIcons = document.querySelectorAll('.hero-social .social-icon'); | |
| if (socialIcons.length > 0) { | |
| anime({ | |
| targets: socialIcons, | |
| opacity: [0, 1], | |
| scale: [0, 1], | |
| rotate: [180, 0], | |
| delay: anime.stagger(100, {start: 2000}), | |
| duration: 800, | |
| easing: 'easeOutBack' | |
| }); | |
| } | |
| const profileImage = document.getElementById('profileImage'); | |
| if (profileImage) { | |
| anime({ | |
| targets: profileImage, | |
| opacity: [0, 1], | |
| scale: [0.8, 1], | |
| rotate: [180, 0], | |
| delay: 1000, | |
| duration: 1500, | |
| easing: 'easeOutElastic(1, .8)' | |
| }); | |
| profileImage.addEventListener('mouseenter', () => { | |
| anime({ | |
| targets: profileImage, | |
| scale: [1, 1.1], | |
| rotate: [0, 5], | |
| duration: 500, | |
| easing: 'easeOutElastic(1, .8)' | |
| }); | |
| }); | |
| profileImage.addEventListener('mouseleave', () => { | |
| anime({ | |
| targets: profileImage, | |
| scale: [1.1, 1], | |
| rotate: [5, 0], | |
| duration: 500, | |
| easing: 'easeOutElastic(1, .8)' | |
| }); | |
| }); | |
| } | |
| const badges = document.querySelectorAll('.floating-badge'); | |
| if (badges.length > 0) { | |
| badges.forEach((badge, index) => { | |
| anime({ | |
| targets: badge, | |
| opacity: [0, 1], | |
| scale: [0, 1], | |
| delay: 1500 + (index * 200), | |
| duration: 800, | |
| easing: 'easeOutBack' | |
| }); | |
| }); | |
| } | |
| } | |
| function initSkillAnimations() { | |
| const skillsSection = document.getElementById('skills'); | |
| if (!skillsSection) return; | |
| const skillItems = skillsSection.querySelectorAll('.skill-item'); | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| const skillItem = entry.target; | |
| const progressBar = skillItem.querySelector('.skill-progress'); | |
| const percentElement = skillItem.querySelector('.skill-percent'); | |
| const percent = parseInt(skillItem.getAttribute('data-percent') || 0); | |
| if (progressBar && typeof anime !== 'undefined') { | |
| anime({ | |
| targets: progressBar, | |
| width: ['0%', percent + '%'], | |
| duration: 2000, | |
| easing: 'easeOutExpo', | |
| delay: 300 | |
| }); | |
| anime({ | |
| targets: { value: 0 }, | |
| value: percent, | |
| duration: 2000, | |
| easing: 'easeOutExpo', | |
| delay: 300, | |
| update: function(anim) { | |
| if (percentElement) { | |
| percentElement.textContent = Math.floor(anim.animatables[0].target.value) + '%'; | |
| } | |
| } | |
| }); | |
| } | |
| observer.unobserve(skillItem); | |
| } | |
| }); | |
| }, { threshold: 0.5 }); | |
| skillItems.forEach(item => observer.observe(item)); | |
| } | |
| function initTimelineAnimations() { | |
| const timelineItems = document.querySelectorAll('.timeline-item'); | |
| timelineItems.forEach((item, index) => { | |
| inView(item, () => { | |
| if (typeof anime !== 'undefined') { | |
| anime({ | |
| targets: item, | |
| opacity: [0, 1], | |
| translateX: [-50, 0], | |
| delay: index * 150, | |
| duration: 1000, | |
| easing: 'easeOutExpo' | |
| }); | |
| } else { | |
| animateElement(item, { opacity: [0, 1], x: [-50, 0] }, { duration: 0.8, delay: index * 0.1 }); | |
| } | |
| }, { amount: 0.3 }); | |
| }); | |
| } | |
| function initProjectAnimations() { | |
| const projectCards = document.querySelectorAll('.project-card'); | |
| projectCards.forEach((card, index) => { | |
| inView(card, () => { | |
| if (typeof anime !== 'undefined') { | |
| anime({ | |
| targets: card, | |
| opacity: [0, 1], | |
| translateY: [50, 0], | |
| scale: [0.9, 1], | |
| delay: index * 100, | |
| duration: 1000, | |
| easing: 'easeOutExpo' | |
| }); | |
| } else { | |
| animateElement(card, { opacity: [0, 1], y: [50, 0], scale: [0.9, 1] }, { duration: 0.8, delay: index * 0.1 }); | |
| } | |
| }, { amount: 0.2 }); | |
| card.addEventListener('mouseenter', () => { | |
| if (typeof anime !== 'undefined') { | |
| anime({ targets: card, scale: [1, 1.02], duration: 300, easing: 'easeOutQuad' }); | |
| } | |
| }); | |
| card.addEventListener('mouseleave', () => { | |
| if (typeof anime !== 'undefined') { | |
| anime({ targets: card, scale: [1.02, 1], duration: 300, easing: 'easeOutQuad' }); | |
| } | |
| }); | |
| }); | |
| } | |
| function initScrollAnimations() { | |
| const sections = document.querySelectorAll('.section'); | |
| sections.forEach(section => { | |
| inView(section, () => { | |
| const sectionHeader = section.querySelector('.section-header'); | |
| if (sectionHeader && typeof anime !== 'undefined') { | |
| anime({ | |
| targets: sectionHeader, | |
| opacity: [0, 1], | |
| translateY: [-20, 0], | |
| duration: 600, | |
| easing: 'easeOutExpo' | |
| }); | |
| } | |
| }, { amount: 0.2 }); | |
| }); | |
| const cards = document.querySelectorAll('.card, .project-card, .contact-item'); | |
| cards.forEach((card, index) => { | |
| inView(card, () => { | |
| if (typeof anime !== 'undefined') { | |
| anime({ | |
| targets: card, | |
| opacity: [0, 1], | |
| translateY: [30, 0], | |
| delay: index * 30, | |
| duration: 500, | |
| easing: 'easeOutExpo' | |
| }); | |
| } else { | |
| animateElement(card, { opacity: [0, 1], y: [50, 0] }, { duration: 0.6, delay: index * 0.05 }); | |
| } | |
| }, { amount: 0.2 }); | |
| }); | |
| } | |
| function animateStats() { | |
| const statNumbers = document.querySelectorAll('.stat-number'); | |
| statNumbers.forEach(stat => { | |
| const target = parseInt(stat.getAttribute('data-count') || 0); | |
| inView(stat, () => { | |
| if (typeof anime !== 'undefined') { | |
| anime({ | |
| targets: { value: 0 }, | |
| value: target, | |
| duration: 2000, | |
| easing: 'easeOutExpo', | |
| update: function(anim) { | |
| stat.textContent = Math.floor(anim.animatables[0].target.value); | |
| } | |
| }); | |
| } | |
| }, { amount: 0.5 }); | |
| }); | |
| } | |
| function initContactAnimations() { | |
| const contactItems = document.querySelectorAll('.contact-item'); | |
| contactItems.forEach(item => { | |
| item.addEventListener('mouseenter', () => { | |
| if (typeof anime !== 'undefined') { | |
| anime({ targets: item, scale: [1, 1.02], duration: 200, easing: 'easeOutQuad' }); | |
| } | |
| }); | |
| item.addEventListener('mouseleave', () => { | |
| if (typeof anime !== 'undefined') { | |
| anime({ targets: item, scale: [1.02, 1], duration: 200, easing: 'easeOutQuad' }); | |
| } | |
| }); | |
| }); | |
| } | |
| function initParallax() { | |
| const profileImage = document.getElementById('profileImage'); | |
| if (!profileImage) return; | |
| let ticking = false; | |
| window.addEventListener('scroll', () => { | |
| if (!ticking) { | |
| window.requestAnimationFrame(() => { | |
| const scrolled = window.pageYOffset; | |
| const parallaxSpeed = 0.3; | |
| const maxOffset = 100; | |
| const offset = Math.min(scrolled * parallaxSpeed, maxOffset); | |
| if (profileImage) { | |
| profileImage.style.transform = `translateY(${offset}px)`; | |
| } | |
| const gridBg = document.querySelector('.code-grid-bg'); | |
| if (gridBg) { | |
| gridBg.style.transform = `translateY(${scrolled * 0.2}px)`; | |
| } | |
| ticking = false; | |
| }); | |
| ticking = true; | |
| } | |
| }); | |
| } | |
| function initSmoothScroll() { | |
| const sections = document.querySelectorAll('section[id]'); | |
| const navLinks = document.querySelectorAll('.nav-link[href^="#"]'); | |
| navLinks.forEach(link => { | |
| link.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const targetId = link.getAttribute('href'); | |
| const targetSection = document.querySelector(targetId); | |
| if (targetSection) { | |
| const headerHeight = document.querySelector('.main-header').offsetHeight; | |
| const targetPosition = targetSection.offsetTop - headerHeight; | |
| if (typeof anime !== 'undefined') { | |
| anime({ | |
| targets: window, | |
| scrollTop: targetPosition, | |
| duration: 800, | |
| easing: 'easeInOutQuad' | |
| }); | |
| } else { | |
| window.scrollTo({ | |
| top: targetPosition, | |
| behavior: 'smooth' | |
| }); | |
| } | |
| } | |
| }); | |
| }); | |
| let currentSection = ''; | |
| window.addEventListener('scroll', () => { | |
| const scrollPos = window.scrollY + 150; | |
| sections.forEach(section => { | |
| const sectionTop = section.offsetTop; | |
| const sectionHeight = section.offsetHeight; | |
| const sectionId = section.getAttribute('id'); | |
| if (scrollPos >= sectionTop && scrollPos < sectionTop + sectionHeight) { | |
| if (currentSection !== sectionId) { | |
| currentSection = sectionId; | |
| navLinks.forEach(link => { | |
| link.classList.remove('active'); | |
| if (link.getAttribute('href') === `#${sectionId}`) { | |
| link.classList.add('active'); | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| }); | |
| } | |
| window.Animations = { | |
| initParallax, | |
| initSmoothScroll | |
| }; | |