File manager - Edit - /home/webapp69.cm.in.th/u69319090014/Portfolio/app.js
Back
document.addEventListener('DOMContentLoaded', () => { // Initialize Lucide Icons if (typeof lucide !== 'undefined') { lucide.createIcons(); } /* ========================================================================== HEADER SCROLL EFFECT ========================================================================== */ const header = document.querySelector('.header'); window.addEventListener('scroll', () => { if (window.scrollY > 50) { header.classList.add('scrolled'); } else { header.classList.remove('scrolled'); } }); /* ========================================================================== MOBILE BOTTOM BAR & BOTTOM SHEET DRAWER ========================================================================== */ const hamburger = document.getElementById('hamburger'); // top header hamburger const bottomHamburger = document.getElementById('bottom-hamburger'); // center bottom bar btn const navMenu = document.getElementById('nav-menu'); // slide-in desktop drawer const bottomSheet = document.getElementById('bottom-sheet'); const sheetOverlay = document.getElementById('bottom-sheet-overlay'); // Create dim overlay for the desktop side-drawer (top hamburger) const navOverlay = document.createElement('div'); navOverlay.classList.add('nav-overlay'); document.body.appendChild(navOverlay); /* ---- Top header hamburger toggles the side drawer (mobile drawer) ---- */ const closeNavMenu = () => { hamburger.classList.remove('active'); navMenu.classList.remove('active'); navOverlay.classList.remove('active'); hamburger.setAttribute('aria-expanded', 'false'); }; const toggleNavMenu = () => { const isOpen = navMenu.classList.contains('active'); if (isOpen) { closeNavMenu(); } else { hamburger.classList.add('active'); navMenu.classList.add('active'); navOverlay.classList.add('active'); hamburger.setAttribute('aria-expanded', 'true'); } }; hamburger.addEventListener('click', toggleNavMenu); navOverlay.addEventListener('click', closeNavMenu); // Close side-drawer when a desktop nav link is clicked const navLinks = document.querySelectorAll('.nav-link'); navLinks.forEach(link => link.addEventListener('click', closeNavMenu)); /* ---- Bottom hamburger toggles the bottom sheet ---- */ const openSheet = () => { bottomSheet.classList.add('active'); sheetOverlay.classList.add('active'); bottomSheet.setAttribute('aria-hidden', 'false'); bottomHamburger.classList.add('active'); bottomHamburger.setAttribute('aria-expanded', 'true'); }; const closeSheet = () => { bottomSheet.classList.remove('active'); sheetOverlay.classList.remove('active'); bottomSheet.setAttribute('aria-hidden', 'true'); bottomHamburger.classList.remove('active'); bottomHamburger.setAttribute('aria-expanded', 'false'); }; const toggleSheet = () => { bottomSheet.classList.contains('active') ? closeSheet() : openSheet(); }; if (bottomHamburger) bottomHamburger.addEventListener('click', toggleSheet); if (sheetOverlay) sheetOverlay.addEventListener('click', closeSheet); // Close sheet when a bottom-sheet nav link is clicked const sheetLinks = document.querySelectorAll('.bsn-link'); sheetLinks.forEach(link => link.addEventListener('click', closeSheet)); // Close sheet when the mbb-item shortcuts (Home / Contact) are clicked const mbbItems = document.querySelectorAll('.mbb-item'); mbbItems.forEach(item => item.addEventListener('click', closeSheet)); /* ========================================================================== NAVBAR LINK ACTIVE HIGHLIGHT (INTERSECTION OBSERVER) ========================================================================== */ const sections = document.querySelectorAll('section[id]'); const observerOptions = { root: null, rootMargin: '-20% 0px -60% 0px', threshold: 0 }; const observerCallback = (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const id = entry.target.getAttribute('id'); // Update top desktop nav navLinks.forEach(link => { link.classList.toggle('active', link.getAttribute('href') === `#${id}`); }); // Update bottom sheet links sheetLinks.forEach(link => { link.classList.toggle('active', link.getAttribute('href') === `#${id}`); }); // Update mbb shortcut items mbbItems.forEach(item => { item.classList.toggle('active', item.getAttribute('href') === `#${id}`); }); } }); }; const observer = new IntersectionObserver(observerCallback, observerOptions); sections.forEach(section => observer.observe(section)); /* ========================================================================== HERO TEXT TYPEWRITER EFFECT ========================================================================== */ const typeTarget = document.querySelector('.typewriter-text'); if (typeTarget) { const words = ["Full-Stack Developer", "Samurai Coder", "UI/UX Designer", "Problem Solver"]; let wordIndex = 0; let charIndex = 0; let isDeleting = false; let typeSpeed = 100; const type = () => { const currentWord = words[wordIndex]; if (isDeleting) { // Deleting text typeTarget.textContent = currentWord.substring(0, charIndex - 1); charIndex--; typeSpeed = 50; // Faster when deleting } else { // Writing text typeTarget.textContent = currentWord.substring(0, charIndex + 1); charIndex++; typeSpeed = 150; // Steady writing } // Word finished writing if (!isDeleting && charIndex === currentWord.length) { typeSpeed = 2000; // Pause at end of word isDeleting = true; } // Word finished deleting else if (isDeleting && charIndex === 0) { isDeleting = false; wordIndex = (wordIndex + 1) % words.length; typeSpeed = 500; // Small pause before writing next word } setTimeout(type, typeSpeed); }; // Start typewriter loop setTimeout(type, 1000); } /* ========================================================================== CONTACT FORM VALIDATION & SUBMISSION HANDLING ========================================================================== */ const contactForm = document.getElementById('contact-form'); const submitBtn = document.getElementById('submit-btn'); const feedbackModal = document.getElementById('feedback-modal'); const modalClose = document.getElementById('modal-close'); // Email regex validation helper const isValidEmail = (email) => { const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return regex.test(email); }; // Show error message const showError = (inputElement) => { const formGroup = inputElement.parentElement; formGroup.classList.add('error'); }; // Remove error message const clearError = (inputElement) => { const formGroup = inputElement.parentElement; formGroup.classList.remove('error'); }; // Input event listener to clear errors dynamically as user types const inputsToValidate = contactForm.querySelectorAll('input[required], textarea[required]'); inputsToValidate.forEach(input => { input.addEventListener('input', () => { if (input.value.trim() !== "") { if (input.type === 'email') { if (isValidEmail(input.value.trim())) { clearError(input); } } else { clearError(input); } } }); }); // Form submit listener contactForm.addEventListener('submit', (e) => { e.preventDefault(); let hasErrors = false; // Validate each required field inputsToValidate.forEach(input => { const val = input.value.trim(); if (val === "") { showError(input); hasErrors = true; } else if (input.type === 'email' && !isValidEmail(val)) { showError(input); hasErrors = true; } else { clearError(input); } }); // If validation succeeds, trigger beautiful loading state and modal if (!hasErrors) { // Set loading state const originalBtnContent = submitBtn.innerHTML; submitBtn.disabled = true; submitBtn.innerHTML = ` <span>กำลังส่งข้อความ...</span> <div class="spinner"></div> `; // Add simple inline style for CSS spinner const spinnerStyle = document.createElement('style'); spinnerStyle.id = 'temp-spinner-style'; spinnerStyle.innerHTML = ` .spinner { width: 18px; height: 18px; border: 2px solid rgba(255, 255, 255, 0.2); border-top: 2px solid var(--text-primary); border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(spinnerStyle); // Mock an API POST delay (e.g. 1.5 seconds) setTimeout(() => { // Restore submit button submitBtn.disabled = false; submitBtn.innerHTML = originalBtnContent; const styleToRemove = document.getElementById('temp-spinner-style'); if (styleToRemove) styleToRemove.remove(); // Open dynamic success modal popup feedbackModal.classList.add('active'); // Clear the form contactForm.reset(); }, 1500); } }); // Close feedback modal const closeModal = () => { feedbackModal.classList.remove('active'); }; modalClose.addEventListener('click', closeModal); feedbackModal.addEventListener('click', (e) => { if (e.target === feedbackModal) { closeModal(); } }); /* ========================================================================== CINEMATIC CYBERPUNK / SCI-FI INTRO LOGIC (VIDEO BACKGROUND) ========================================================================== */ const cutsceneContainer = document.getElementById('cutscene-container'); const cutsceneVideo = document.getElementById('cutscene-video'); const cyberPopupBox = document.getElementById('cyber-popup-box'); const btnLaunchSystem = document.getElementById('btn-launch-system'); const replayIntroBtn = document.getElementById('replay-intro-btn'); // HUD Real-time Clock const updateHUDClock = () => { const clockEl = document.getElementById('hud-clock'); if (clockEl) { const now = new Date(); const h = String(now.getHours()).padStart(2, '0'); const m = String(now.getMinutes()).padStart(2, '0'); const s = String(now.getSeconds()).padStart(2, '0'); clockEl.textContent = `${h}:${m}:${s}`; } }; updateHUDClock(); setInterval(updateHUDClock, 1000); // Entrance Animation Sequence const startCyberIntro = () => { // Lock page scrolling document.body.classList.add('cutscene-active'); document.documentElement.classList.remove('skip-intro-instantly'); if (cutsceneContainer) { cutsceneContainer.style.display = 'flex'; cutsceneContainer.classList.remove('fade-out'); } if (cyberPopupBox) { cyberPopupBox.classList.remove('exit-active'); // Force browser reflow to restart CSS animations cyberPopupBox.style.animation = 'none'; cyberPopupBox.offsetHeight; cyberPopupBox.style.animation = ''; } if (cutsceneVideo) { cutsceneVideo.currentTime = 0; // Play video muted automatically cutsceneVideo.play().catch(err => { console.warn("Autoplay blocked by browser. Retrying on user interaction...", err); }); } }; // Exit Animation Sequence (Fade out popup box, then fade out background video) const exitCyberIntro = () => { // 1. Trigger HUD pop-up box exit animation (Scale up and fade out) if (cyberPopupBox) { cyberPopupBox.classList.add('exit-active'); } // 2. Fade out the background video and allow body scroll slightly after setTimeout(() => { if (cutsceneContainer) { cutsceneContainer.classList.add('fade-out'); } document.body.classList.remove('cutscene-active'); sessionStorage.setItem('visited_portfolio', 'true'); }, 200); // 3. Pause video after 1.2 seconds and hide the container to free up resources setTimeout(() => { if (cutsceneVideo) { cutsceneVideo.pause(); } if (cutsceneContainer) { cutsceneContainer.style.display = 'none'; } }, 1200); }; // Launch Button Event if (btnLaunchSystem) { btnLaunchSystem.addEventListener('click', exitCyberIntro); } // Replay Button Event (in footer) if (replayIntroBtn) { replayIntroBtn.addEventListener('click', (e) => { e.preventDefault(); sessionStorage.removeItem('visited_portfolio'); startCyberIntro(); window.scrollTo({ top: 0, behavior: 'instant' }); }); } // Initialize/Auto-run logic if (sessionStorage.getItem('visited_portfolio') !== 'true') { startCyberIntro(); } else { document.body.classList.remove('cutscene-active'); if (cutsceneContainer) { cutsceneContainer.style.display = 'none'; } } });
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.24 |
proxy
|
phpinfo
|
Settings