// Main JavaScript file for Alex Chen Portfolio Website // Handles all interactive functionality across pages // Global variables let currentProjectIndex = 0; let projects = []; let skillsChart = null; // Initialize everything when DOM is loaded document.addEventListener('DOMContentLoaded', function() { initializeCommonFeatures(); initializePageSpecificFeatures(); }); // Common features across all pages function initializeCommonFeatures() { initializeScrollReveal(); initializeMobileMenu(); initializeSmoothScroll(); } // Page-specific feature initialization function initializePageSpecificFeatures() { const currentPage = getCurrentPage(); switch(currentPage) { case 'index': initializeHomePage(); break; case 'about': initializeAboutPage(); break; case 'portfolio': initializePortfolioPage(); break; case 'contact': initializeContactPage(); break; } } // Get current page identifier function getCurrentPage() { const path = window.location.pathname; if (path.includes('about')) return 'about'; if (path.includes('portfolio')) return 'portfolio'; if (path.includes('contact')) return 'contact'; return 'index'; } // Scroll reveal animation function initializeScrollReveal() { const revealElements = document.querySelectorAll('.reveal-element'); const revealObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('revealed'); } }); }, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' }); revealElements.forEach(element => { revealObserver.observe(element); }); } // Mobile menu functionality function initializeMobileMenu() { const mobileMenuBtn = document.getElementById('mobile-menu-btn'); if (!mobileMenuBtn) return; mobileMenuBtn.addEventListener('click', function() { // Mobile menu implementation would go here alert('Mobile menu functionality - would show/hide navigation menu'); }); } // Smooth scroll for navigation links function initializeSmoothScroll() { const navLinks = document.querySelectorAll('.nav-link[href^="#"]'); navLinks.forEach(link => { link.addEventListener('click', function(e) { e.preventDefault(); const targetId = this.getAttribute('href').substring(1); const targetElement = document.getElementById(targetId); if (targetElement) { targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); } // Home page specific initialization function initializeHomePage() { initializeTypedText(); initializeParticles(); initializeTestimonialSlider(); initializeSkillCircles(); } // Typed text animation for hero section function initializeTypedText() { const typedElement = document.getElementById('typed-text'); if (!typedElement) return; const typed = new Typed('#typed-text', { strings: [ 'Creative solutions for digital products', 'User-centered design that drives results', 'Brand experiences that tell compelling stories', 'Strategic thinking meets beautiful execution' ], typeSpeed: 50, backSpeed: 30, backDelay: 2000, loop: true, showCursor: true, cursorChar: '|' }); } // Particle system for hero background function initializeParticles() { const canvas = document.getElementById('particles-canvas'); if (!canvas) return; // Simple particle system using p5.js would go here // For now, we'll create a basic implementation const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; const particles = []; const particleCount = 50; // Create particles for (let i = 0; i < particleCount; i++) { particles.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, vx: (Math.random() - 0.5) * 0.5, vy: (Math.random() - 0.5) * 0.5, size: Math.random() * 2 + 1, opacity: Math.random() * 0.5 + 0.2 }); } function animateParticles() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(particle => { particle.x += particle.vx; particle.y += particle.vy; // Wrap around edges if (particle.x < 0) particle.x = canvas.width; if (particle.x > canvas.width) particle.x = 0; if (particle.y < 0) particle.y = canvas.height; if (particle.y > canvas.height) particle.y = 0; // Draw particle ctx.beginPath(); ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); ctx.fillStyle = `rgba(224, 122, 95, ${particle.opacity})`; ctx.fill(); }); requestAnimationFrame(animateParticles); } animateParticles(); // Resize handler window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }); } // Testimonial slider initialization function initializeTestimonialSlider() { const slider = document.getElementById('testimonials-slider'); if (!slider) return; new Splide('#testimonials-slider', { type: 'loop', perPage: 1, autoplay: true, interval: 5000, arrows: false, pagination: true, gap: '2rem' }).mount(); } // Skill circles animation function initializeSkillCircles() { const skillCircles = document.querySelectorAll('.skill-circle'); const skillObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const circle = entry.target; const percentage = circle.getAttribute('data-percentage'); const circumference = 2 * Math.PI * 45; // radius = 45 const offset = circumference - (percentage / 100) * circumference; anime({ targets: circle, strokeDashoffset: offset, duration: 2000, easing: 'easeOutCubic' }); } }); }, { threshold: 0.5 }); skillCircles.forEach(circle => { skillObserver.observe(circle); }); } // About page specific initialization function initializeAboutPage() { initializeSkillsChart(); initializeSkillBars(); } // Skills chart initialization using ECharts function initializeSkillsChart() { const chartElement = document.getElementById('skills-chart'); if (!chartElement) return; skillsChart = echarts.init(chartElement); const option = { tooltip: { trigger: 'item' }, radar: { indicator: [ { name: 'UI/UX Design', max: 100 }, { name: 'Brand Identity', max: 100 }, { name: 'Web Design', max: 100 }, { name: 'Frontend Dev', max: 100 }, { name: 'Strategy', max: 100 }, { name: 'Project Management', max: 100 } ], radius: '70%', axisLine: { lineStyle: { color: '#F4F4F4' } }, splitLine: { lineStyle: { color: '#F4F4F4' } } }, series: [{ name: 'Skills', type: 'radar', data: [{ value: [90, 85, 80, 70, 75, 85], name: 'Technical Skills', areaStyle: { color: 'rgba(224, 122, 95, 0.3)' }, lineStyle: { color: '#E07A5F' }, itemStyle: { color: '#E07A5F' } }] }] }; skillsChart.setOption(option); // Resize handler window.addEventListener('resize', () => { if (skillsChart) { skillsChart.resize(); } }); } // Skill bars animation function initializeSkillBars() { const skillBars = document.querySelectorAll('.skill-progress'); const skillBarObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const bar = entry.target; const width = bar.getAttribute('data-width'); setTimeout(() => { bar.style.width = width + '%'; }, 200); } }); }, { threshold: 0.5 }); skillBars.forEach(bar => { skillBarObserver.observe(bar); }); } // Portfolio page specific initialization function initializePortfolioPage() { initializePortfolioFilter(); initializeProjectModals(); loadProjectData(); } // Portfolio filter functionality function initializePortfolioFilter() { const filterButtons = document.querySelectorAll('.filter-btn'); const projectCards = document.querySelectorAll('.project-card'); filterButtons.forEach(button => { button.addEventListener('click', function() { const filter = this.getAttribute('data-filter'); // Update active button filterButtons.forEach(btn => btn.classList.remove('active')); this.classList.add('active'); // Filter projects projectCards.forEach(card => { const category = card.getAttribute('data-category'); if (filter === 'all' || category === filter) { card.classList.remove('hidden'); anime({ targets: card, opacity: [0, 1], scale: [0.8, 1], duration: 600, easing: 'easeOutCubic', delay: anime.stagger(100) }); } else { anime({ targets: card, opacity: 0, scale: 0.8, duration: 300, easing: 'easeInCubic', complete: () => { card.classList.add('hidden'); } }); } }); }); }); } // Project modal functionality function initializeProjectModals() { const modal = document.getElementById('project-modal'); const modalClose = document.getElementById('modal-close'); const modalPrev = document.getElementById('modal-prev'); const modalNext = document.getElementById('modal-next'); const projectCards = document.querySelectorAll('.project-card'); // Open modal when project card is clicked projectCards.forEach((card, index) => { card.addEventListener('click', function() { const projectId = this.getAttribute('data-project'); currentProjectIndex = index; openProjectModal(projectId); }); }); // Close modal if (modalClose) { modalClose.addEventListener('click', closeProjectModal); } // Close modal when clicking outside if (modal) { modal.addEventListener('click', function(e) { if (e.target === modal) { closeProjectModal(); } }); } // Navigation buttons if (modalPrev) { modalPrev.addEventListener('click', () => { currentProjectIndex = (currentProjectIndex - 1 + projects.length) % projects.length; const projectId = projects[currentProjectIndex].id; openProjectModal(projectId); }); } if (modalNext) { modalNext.addEventListener('click', () => { currentProjectIndex = (currentProjectIndex + 1) % projects.length; const projectId = projects[currentProjectIndex].id; openProjectModal(projectId); }); } // Keyboard navigation document.addEventListener('keydown', function(e) { if (modal && modal.classList.contains('active')) { if (e.key === 'Escape') { closeProjectModal(); } else if (e.key === 'ArrowLeft' && modalPrev) { modalPrev.click(); } else if (e.key === 'ArrowRight' && modalNext) { modalNext.click(); } } }); } // Load project data for modals function loadProjectData() { projects = [ { id: 'fintech-app', title: 'Fintech Mobile Banking App', category: 'UI/UX Design', description: 'A comprehensive mobile banking application designed with accessibility and user experience at its core.', challenge: 'The client needed to improve their mobile banking app\'s user onboarding process, which had a 60% dropout rate.', solution: 'Through extensive user research and iterative testing, we redesigned the onboarding flow, simplified the interface, and implemented progressive disclosure.', impact: '40% improvement in user onboarding completion, 25% increase in daily active users', tools: ['Figma', 'Principle', 'Maze', 'UserTesting'], duration: '12 weeks', team: '2 designers, 1 researcher, 3 developers' }, { id: 'sustainable-fashion', title: 'EcoWear Brand Identity', category: 'Branding', description: 'Complete brand identity for a sustainable fashion startup targeting environmentally conscious consumers.', challenge: 'Create a brand identity that communicates sustainability without appearing overly "earthy" or sacrificing style appeal.', solution: 'Developed a modern, minimalist brand identity using natural color palettes and sustainable materials in all touchpoints.', impact: 'Successful launch with 25K social media followers in first 6 months, featured in 5 major fashion publications', tools: ['Adobe Creative Suite', 'Procreate', 'Keynote'], duration: '8 weeks', team: '1 designer (me), 1 brand strategist' } // Add more projects as needed ]; } // Open project modal with specific project data function openProjectModal(projectId) { const project = projects.find(p => p.id === projectId); if (!project) return; const modal = document.getElementById('project-modal'); const modalBody = document.getElementById('modal-body'); if (!modal || !modalBody) return; // Create modal content modalBody.innerHTML = `

${project.title}

${project.category}

${project.description}

Challenge

${project.challenge}

Solution

${project.solution}

Project Impact

${project.impact}

Tools Used

${project.tools.join(', ')}

Duration

${project.duration}

Team Size

${project.team}

`; modal.classList.add('active'); document.body.style.overflow = 'hidden'; } // Close project modal function closeProjectModal() { const modal = document.getElementById('project-modal'); if (modal) { modal.classList.remove('active'); document.body.style.overflow = ''; } } // Contact page specific initialization function initializeContactPage() { initializeContactForm(); } // Contact form functionality function initializeContactForm() { const form = document.getElementById('contact-form'); if (!form) return; form.addEventListener('submit', function(e) { e.preventDefault(); if (validateForm()) { submitForm(); } }); // Real-time validation const inputs = form.querySelectorAll('.form-input'); inputs.forEach(input => { input.addEventListener('blur', function() { validateField(this); }); input.addEventListener('input', function() { if (this.classList.contains('error')) { validateField(this); } }); }); } // Form validation function validateForm() { const form = document.getElementById('contact-form'); const inputs = form.querySelectorAll('.form-input[required]'); let isValid = true; inputs.forEach(input => { if (!validateField(input)) { isValid = false; } }); return isValid; } // Validate individual field function validateField(field) { const value = field.value.trim(); const fieldId = field.id; let isValid = true; let errorMessage = ''; // Clear previous errors field.classList.remove('error'); const errorElement = document.getElementById(fieldId + '-error'); if (errorElement) { errorElement.textContent = ''; errorElement.classList.remove('show'); } // Validation rules if (field.hasAttribute('required') && !value) { isValid = false; errorMessage = 'This field is required'; } else if (fieldId === 'email' && value) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { isValid = false; errorMessage = 'Please enter a valid email address'; } } // Show error if invalid if (!isValid) { field.classList.add('error'); if (errorElement) { errorElement.textContent = errorMessage; errorElement.classList.add('show'); } } return isValid; } // Form submission function submitForm() { const form = document.getElementById('contact-form'); const submitBtn = form.querySelector('.submit-btn'); const successMessage = document.getElementById('success-message'); // Show loading state submitBtn.classList.add('loading'); // Simulate form submission (replace with actual submission logic) setTimeout(() => { submitBtn.classList.remove('loading'); // Show success message if (successMessage) { successMessage.classList.add('show'); } // Reset form form.reset(); // Hide success message after 5 seconds setTimeout(() => { if (successMessage) { successMessage.classList.remove('show'); } }, 5000); }, 2000); } // Utility functions function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Export functions for global access window.portfolioApp = { openProjectModal, closeProjectModal, validateForm, submitForm };