File manager - Edit - /home/webapp69.cm.in.th/u69319090026/portfolio/69319090026/assets/js/script.js
Back
/** * Frontend JavaScript for Han Portfolio * Handles: Loader, Dark/Light Mode, Sticky Nav, Active Section Nav, * Typed.js, AOS, Skill Animation, Counters, Lightbox, and Contact Form. */ document.addEventListener('DOMContentLoaded', () => { /* ========================================================================== 1. Loader Screen Hiding ========================================================================== */ const loader = document.getElementById('loader'); if (loader) { window.addEventListener('load', () => { setTimeout(() => { loader.style.opacity = '0'; loader.style.visibility = 'hidden'; }, 600); // Small delay for seamless loading transition }); } /* ========================================================================== 2. Dark / Light Mode Switching ========================================================================== */ const themeToggle = document.getElementById('themeToggle'); const htmlElement = document.documentElement; const moonIcon = document.querySelector('.theme-icon-dark'); const sunIcon = document.querySelector('.theme-icon-light'); // Retrieve saved theme or default to light const currentTheme = localStorage.getItem('theme') || 'light'; htmlElement.setAttribute('data-theme', currentTheme); if (currentTheme === 'light') { moonIcon.classList.add('d-none'); sunIcon.classList.remove('d-none'); } else { moonIcon.classList.remove('d-none'); sunIcon.classList.add('d-none'); } themeToggle.addEventListener('click', () => { const theme = htmlElement.getAttribute('data-theme'); if (theme === 'dark') { htmlElement.setAttribute('data-theme', 'light'); localStorage.setItem('theme', 'light'); moonIcon.classList.add('d-none'); sunIcon.classList.remove('d-none'); } else { htmlElement.setAttribute('data-theme', 'dark'); localStorage.setItem('theme', 'dark'); moonIcon.classList.remove('d-none'); sunIcon.classList.add('d-none'); } }); /* ========================================================================== 3. Sticky Navbar & Active Menu Highlighting ========================================================================== */ const navbar = document.getElementById('mainNavbar'); const navLinks = document.querySelectorAll('.navbar-nav .nav-link'); const sections = document.querySelectorAll('section'); const handleScroll = () => { // Sticky Navbar Toggle if (window.scrollY > 50) { navbar.classList.add('scrolled'); } else { navbar.classList.remove('scrolled'); } // Active Link Highlighting let currentSectionId = ''; sections.forEach(section => { const sectionTop = section.offsetTop - 120; const sectionHeight = section.offsetHeight; if (window.scrollY >= sectionTop && window.scrollY < sectionTop + sectionHeight) { currentSectionId = section.getAttribute('id'); } }); navLinks.forEach(link => { link.classList.remove('active'); if (link.getAttribute('href') === `#${currentSectionId}`) { link.classList.add('active'); } }); }; window.addEventListener('scroll', handleScroll); handleScroll(); // Trigger once on load // Collapse Navbar on Mobile menu item click const navbarCollapse = document.querySelector('.navbar-collapse'); const navbarToggler = document.querySelector('.navbar-toggler'); navLinks.forEach(link => { link.addEventListener('click', () => { if (navbarCollapse.classList.contains('show')) { navbarToggler.click(); } }); }); /* ========================================================================== 4. Typed.js Typing Animation ========================================================================== */ const typedTarget = document.querySelector('.typed-text'); if (typedTarget) { new Typed('.typed-text', { strings: ['YouTuber', 'Streamer', 'Digital Artist', 'Content Creator'], typeSpeed: 70, backSpeed: 45, backDelay: 2000, loop: true, cursorChar: '|' }); } /* ========================================================================== 5. AOS (Animate on Scroll) Initialization ========================================================================== */ if (typeof AOS !== 'undefined') { AOS.init({ duration: 900, easing: 'ease-out-cubic', once: true, offset: 100 }); } /* ========================================================================== 6. Skill Progress Bars Animation ========================================================================== */ const skillBars = document.querySelectorAll('.progress-bar-custom'); const animateSkills = (entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const bar = entry.target; const percent = bar.getAttribute('data-width'); bar.style.width = percent + '%'; observer.unobserve(bar); // Stop observing once animated } }); }; const skillObserver = new IntersectionObserver(animateSkills, { threshold: 0.1 }); skillBars.forEach(bar => { skillObserver.observe(bar); }); /* ========================================================================== 7. Stats Counter Animation ========================================================================== */ const counters = document.querySelectorAll('.counter-num'); const animateCounters = (entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const counter = entry.target; const target = parseFloat(counter.getAttribute('data-target')); const duration = 2000; // 2 seconds const start = 0; const startTime = performance.now(); const updateCount = (timestamp) => { const elapsed = timestamp - startTime; const progress = Math.min(elapsed / duration, 1); // Check if integer or float if (Number.isInteger(target)) { counter.innerText = Math.floor(progress * target); } else { // Float format (e.g. GPA) counter.innerText = (progress * target).toFixed(2); } if (progress < 1) { requestAnimationFrame(updateCount); } else { counter.innerText = target; } }; requestAnimationFrame(updateCount); observer.unobserve(counter); } }); }; const counterObserver = new IntersectionObserver(animateCounters, { threshold: 0.5 }); counters.forEach(counter => { counterObserver.observe(counter); }); /* ========================================================================== 8. Custom Modal Lightbox for Portfolio Gallery ========================================================================== */ const galleryItems = document.querySelectorAll('.lightbox-trigger'); // Create lightbox modal elements dynamically if they don't exist let lightbox = document.getElementById('customLightbox'); if (!lightbox && galleryItems.length > 0) { lightbox = document.createElement('div'); lightbox.id = 'customLightbox'; lightbox.className = 'lightbox-modal'; lightbox.innerHTML = ` <div class="lightbox-content-wrapper"> <button class="lightbox-close-btn" aria-label="Close Lightbox"><i class="fa-solid fa-xmark"></i></button> <img src="" alt="Enlarged Image" class="lightbox-img"> <div class="lightbox-caption"></div> </div> `; document.body.appendChild(lightbox); } if (lightbox) { const lightboxImg = lightbox.querySelector('.lightbox-img'); const lightboxCaption = lightbox.querySelector('.lightbox-caption'); const closeBtn = lightbox.querySelector('.lightbox-close-btn'); galleryItems.forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); const imgSrc = item.getAttribute('href') || item.getAttribute('data-img'); const captionText = item.getAttribute('data-caption') || ''; if (imgSrc) { lightboxImg.src = imgSrc; lightboxCaption.textContent = captionText; lightbox.classList.add('show'); document.body.style.overflow = 'hidden'; // Lock background scroll } }); }); const closeLightbox = () => { lightbox.classList.remove('show'); document.body.style.overflow = ''; // Restore scroll setTimeout(() => { lightboxImg.src = ''; }, 300); }; closeBtn.addEventListener('click', closeLightbox); lightbox.addEventListener('click', (e) => { if (e.target === lightbox || e.target.classList.contains('lightbox-content-wrapper')) { closeLightbox(); } }); // Close on ESC key document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && lightbox.classList.contains('show')) { closeLightbox(); } }); } /* ========================================================================== 9. Back to Top Button ========================================================================== */ const backToTopBtn = document.getElementById('backToTop'); if (backToTopBtn) { window.addEventListener('scroll', () => { if (window.scrollY > 400) { backToTopBtn.classList.add('show'); } else { backToTopBtn.classList.remove('show'); } }); backToTopBtn.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }); } /* ========================================================================== 10. Contact Form AJAX Submission ========================================================================== */ const contactForm = document.getElementById('contactForm'); const formAlert = document.getElementById('formAlert'); if (contactForm && formAlert) { contactForm.addEventListener('submit', (e) => { e.preventDefault(); // Show Loading State on Submit Button const submitBtn = contactForm.querySelector('button[type="submit"]'); const originalBtnText = submitBtn.innerHTML; submitBtn.disabled = true; submitBtn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin me-2"></i>กำลังส่งข้อความ...'; // Gather Form Data const formData = new FormData(contactForm); formData.append('ajax', '1'); // Add flag for AJAX detection in PHP // POST to index.php fetch('index.php', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { // Clear any old alert classes formAlert.className = 'form-alert'; formAlert.textContent = data.message; if (data.status === 'success') { formAlert.classList.add('success'); contactForm.reset(); // Clear form inputs } else { formAlert.classList.add('error'); } }) .catch(error => { formAlert.className = 'form-alert error'; formAlert.textContent = 'เกิดข้อผิดพลาดในการส่งข้อมูล กรุณาลองใหม่อีกครั้ง'; console.error('Error:', error); }) .finally(() => { // Restore button state submitBtn.disabled = false; submitBtn.innerHTML = originalBtnText; // Scroll alert into view formAlert.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }); }); } /* ========================================================================== 11. Lazy Loading for Gallery Images ========================================================================== */ const lazyImages = document.querySelectorAll('img[loading="lazy"]'); if ('IntersectionObserver' in window) { const imageObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const image = entry.target; image.src = image.getAttribute('data-src') || image.src; image.removeAttribute('data-src'); imageObserver.unobserve(image); } }); }); lazyImages.forEach(image => { imageObserver.observe(image); }); } });
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.34 |
proxy
|
phpinfo
|
Settings