/** * LemonLink - Interactive Landing Page * Modern animations and interactions */ document.addEventListener('DOMContentLoaded', () => { // Initialize all modules initNavbar(); initSmoothScroll(); initCounters(); initServiceCards(); initScrollReveal(); initMobileMenu(); initParallax(); }); /** * Navbar scroll effect */ function initNavbar() { const navbar = document.querySelector('.navbar'); let lastScroll = 0; window.addEventListener('scroll', () => { const currentScroll = window.pageYOffset; // Add/remove scrolled class if (currentScroll > 50) { navbar.classList.add('scrolled'); } else { navbar.classList.remove('scrolled'); } // Hide/show on scroll direction if (currentScroll > lastScroll && currentScroll > 100) { navbar.style.transform = 'translateY(-100%)'; } else { navbar.style.transform = 'translateY(0)'; } lastScroll = currentScroll; }); // Smooth transition for navbar navbar.style.transition = 'transform 0.3s ease, background 0.3s ease'; } /** * Smooth scroll for anchor links */ function initSmoothScroll() { 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' }); } }); }); } /** * Animated counters for stats */ function initCounters() { const counters = document.querySelectorAll('.stat-number'); const animateCounter = (counter) => { const target = parseFloat(counter.getAttribute('data-target')); const duration = 2000; const startTime = performance.now(); const updateCounter = (currentTime) => { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); // Easing function for smooth animation const easeOutQuart = 1 - Math.pow(1 - progress, 4); const current = target * easeOutQuart; // Format based on target value if (target % 1 === 0) { counter.textContent = Math.round(current); } else { counter.textContent = current.toFixed(1); } if (progress < 1) { requestAnimationFrame(updateCounter); } else { counter.textContent = target; } }; requestAnimationFrame(updateCounter); }; // Use Intersection Observer to trigger animation const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { animateCounter(entry.target); observer.unobserve(entry.target); } }); }, { threshold: 0.5 }); counters.forEach(counter => observer.observe(counter)); } /** * Service cards mouse follow glow effect */ function initServiceCards() { const cards = document.querySelectorAll('.service-card'); cards.forEach(card => { const glow = card.querySelector('.service-glow'); card.addEventListener('mousemove', (e) => { const rect = card.getBoundingClientRect(); const x = ((e.clientX - rect.left) / rect.width) * 100; const y = ((e.clientY - rect.top) / rect.height) * 100; card.style.setProperty('--mouse-x', `${x}%`); card.style.setProperty('--mouse-y', `${y}%`); }); }); } /** * Scroll reveal animations */ function initScrollReveal() { const reveals = document.querySelectorAll('.section, .service-card, .infra-card, .project-card, .domain-node'); const revealOnScroll = (entries, observer) => { entries.forEach((entry, index) => { if (entry.isIntersecting) { // Add stagger delay for cards if (entry.target.classList.contains('service-card') || entry.target.classList.contains('domain-node')) { entry.target.style.animationDelay = `${index * 0.1}s`; } entry.target.style.opacity = '1'; entry.target.style.transform = 'translateY(0)'; observer.unobserve(entry.target); } }); }; const observer = new IntersectionObserver(revealOnScroll, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' }); reveals.forEach(el => { el.style.opacity = '0'; el.style.transform = 'translateY(30px)'; el.style.transition = 'opacity 0.6s ease, transform 0.6s ease'; observer.observe(el); }); } /** * Mobile menu toggle */ function initMobileMenu() { const menuBtn = document.querySelector('.mobile-menu-btn'); const navLinks = document.querySelector('.nav-links'); if (!menuBtn) return; menuBtn.addEventListener('click', () => { menuBtn.classList.toggle('active'); navLinks.classList.toggle('active'); // Animate hamburger const spans = menuBtn.querySelectorAll('span'); if (menuBtn.classList.contains('active')) { spans[0].style.transform = 'rotate(45deg) translate(5px, 5px)'; spans[1].style.opacity = '0'; spans[2].style.transform = 'rotate(-45deg) translate(5px, -5px)'; // Show mobile menu navLinks.style.display = 'flex'; navLinks.style.flexDirection = 'column'; navLinks.style.position = 'absolute'; navLinks.style.top = '100%'; navLinks.style.left = '0'; navLinks.style.right = '0'; navLinks.style.background = 'rgba(10, 10, 15, 0.98)'; navLinks.style.padding = '2rem'; navLinks.style.backdropFilter = 'blur(20px)'; navLinks.style.borderBottom = '1px solid rgba(255,255,255,0.1)'; } else { spans[0].style.transform = 'none'; spans[1].style.opacity = '1'; spans[2].style.transform = 'none'; navLinks.style.display = ''; } }); // Close menu on link click navLinks.querySelectorAll('a').forEach(link => { link.addEventListener('click', () => { menuBtn.classList.remove('active'); navLinks.classList.remove('active'); const spans = menuBtn.querySelectorAll('span'); spans[0].style.transform = 'none'; spans[1].style.opacity = '1'; spans[2].style.transform = 'none'; navLinks.style.display = ''; }); }); } /** * Parallax effect for hero section */ function initParallax() { const hero = document.querySelector('.hero'); const blobs = document.querySelectorAll('.bg-blob'); if (!hero) return; // Check for touch device const isTouchDevice = window.matchMedia('(pointer: coarse)').matches; if (isTouchDevice) return; let ticking = false; document.addEventListener('mousemove', (e) => { if (!ticking) { requestAnimationFrame(() => { const x = (e.clientX / window.innerWidth - 0.5) * 2; const y = (e.clientY / window.innerHeight - 0.5) * 2; blobs.forEach((blob, index) => { const speed = (index + 1) * 20; blob.style.transform = `translate(${x * speed}px, ${y * speed}px)`; }); ticking = false; }); ticking = true; } }); } /** * Typing effect for terminal cursor */ function initTerminalTyping() { const terminal = document.querySelector('.terminal-body'); if (!terminal) return; const commands = [ { text: 'docker ps', output: 'CONTAINER ID IMAGE STATUS\nabc123 nginx Up 3 days' }, { text: 'kubectl get pods', output: 'NAME READY STATUS\nlemonlink-app-7d9f4 1/1 Running' }, { text: 'systemctl status lemonlink', output: '● lemonlink.service - LemonLink Platform\n Active: active (running)' } ]; let currentCommand = 0; let isTyping = false; const typeCommand = () => { if (isTyping) return; isTyping = true; const cmd = commands[currentCommand]; const lines = terminal.querySelectorAll('.terminal-line'); const lastLine = lines[lines.length - 1]; // Clear cursor from previous line const cursor = terminal.querySelector('.cursor'); if (cursor) cursor.remove(); // Create new command line const newLine = document.createElement('div'); newLine.className = 'terminal-line'; newLine.innerHTML = ` ~ | `; terminal.appendChild(newLine); const commandSpan = newLine.querySelector('.command'); let charIndex = 0; const typeChar = () => { if (charIndex < cmd.text.length) { commandSpan.textContent += cmd.text[charIndex]; charIndex++; setTimeout(typeChar, 50 + Math.random() * 50); } else { // Show output after typing setTimeout(() => { const outputDiv = document.createElement('div'); outputDiv.className = 'terminal-output'; outputDiv.innerHTML = cmd.output.replace(/\n/g, '
'); terminal.appendChild(outputDiv); // Move cursor to new line const nextLine = document.createElement('div'); nextLine.className = 'terminal-line'; nextLine.innerHTML = ` ~ | `; terminal.appendChild(nextLine); // Scroll to bottom terminal.scrollTop = terminal.scrollHeight; isTyping = false; currentCommand = (currentCommand + 1) % commands.length; }, 500); } }; typeChar(); }; // Start typing effect every 8 seconds setInterval(typeCommand, 8000); } // Initialize terminal typing when contact section is visible - limited to 3 cycles const terminalObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { initLimitedTerminalTyping(); terminalObserver.unobserve(entry.target); } }); }, { threshold: 0.5 }); const contactSection = document.querySelector('.contact-section'); if (contactSection) { terminalObserver.observe(contactSection); } /** * Limited Terminal Typing Effect (3 cycles max) */ function initLimitedTerminalTyping() { const terminal = document.querySelector('.terminal-body'); if (!terminal) return; const commands = [ { text: 'docker ps', output: 'CONTAINER ID IMAGE STATUS\nabc123 nginx Up 3 days\ndef456 nextcloud Up 5 days' }, { text: 'uptime', output: 'Load: 1.06, 1.06, 1.14 | RAM: 18% of 96GB' }, { text: 'tailscale status', output: '100.x.x.x compute-01 linux -' } ]; let currentCommand = 0; let cycleCount = 0; const maxCycles = 3; let isTyping = false; const typeCommand = () => { if (isTyping || cycleCount >= maxCycles) return; isTyping = true; const cmd = commands[currentCommand]; const lines = terminal.querySelectorAll('.terminal-line'); // Remove cursor from previous line const cursor = terminal.querySelector('.cursor'); if (cursor) cursor.remove(); // Create new command line const newLine = document.createElement('div'); newLine.className = 'terminal-line'; newLine.innerHTML = ` ~ | `; terminal.appendChild(newLine); const commandSpan = newLine.querySelector('.command'); let charIndex = 0; const typeChar = () => { if (charIndex < cmd.text.length) { commandSpan.textContent += cmd.text[charIndex]; charIndex++; setTimeout(typeChar, 50 + Math.random() * 50); } else { setTimeout(() => { const outputDiv = document.createElement('div'); outputDiv.className = 'terminal-output'; outputDiv.innerHTML = cmd.output.replace(/\n/g, '
'); terminal.appendChild(outputDiv); const nextLine = document.createElement('div'); nextLine.className = 'terminal-line'; nextLine.innerHTML = ` ~ | `; terminal.appendChild(nextLine); terminal.scrollTop = terminal.scrollHeight; isTyping = false; currentCommand = (currentCommand + 1) % commands.length; // Count completed cycles if (currentCommand === 0) { cycleCount++; } }, 500); } }; typeChar(); }; // Start typing effect every 6 seconds, but only for 3 cycles const intervalId = setInterval(() => { if (cycleCount >= maxCycles) { clearInterval(intervalId); // Keep cursor blinking at the end return; } typeCommand(); }, 6000); } /** * Network connection lines animation */ function drawConnectionLines() { const hub = document.querySelector('.hub-center'); const nodes = document.querySelectorAll('.domain-node'); const svg = document.querySelector('.connections-svg'); if (!hub || !svg || nodes.length === 0) return; const hubRect = hub.getBoundingClientRect(); const hubX = hubRect.left + hubRect.width / 2; const hubY = hubRect.top + hubRect.height / 2; // Clear existing lines svg.innerHTML = svg.querySelector('defs').outerHTML; nodes.forEach(node => { const nodeRect = node.getBoundingClientRect(); const nodeX = nodeRect.left + nodeRect.width / 2; const nodeY = nodeRect.top + nodeRect.height / 2; const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); line.setAttribute('x1', hubX); line.setAttribute('y1', hubY); line.setAttribute('x2', nodeX); line.setAttribute('y2', nodeY); line.setAttribute('stroke', 'url(#line-gradient)'); line.setAttribute('stroke-width', '1'); line.setAttribute('opacity', '0.3'); svg.appendChild(line); }); } // Draw lines on load and resize window.addEventListener('load', drawConnectionLines); window.addEventListener('resize', drawConnectionLines); /** * Easter egg: Konami code */ let konamiCode = []; const konamiSequence = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a']; document.addEventListener('keydown', (e) => { konamiCode.push(e.key); konamiCode = konamiCode.slice(-10); if (konamiCode.join(',') === konamiSequence.join(',')) { // Activate lemon mode! document.body.style.filter = 'hue-rotate(180deg)'; setTimeout(() => { document.body.style.filter = ''; }, 3000); // Create floating lemons for (let i = 0; i < 20; i++) { createFloatingLemon(); } } }); function createFloatingLemon() { const lemon = document.createElement('div'); lemon.textContent = '🍋'; lemon.style.cssText = ` position: fixed; font-size: 2rem; pointer-events: none; z-index: 9999; left: ${Math.random() * 100}vw; top: -50px; animation: fall ${3 + Math.random() * 2}s linear forwards; `; document.body.appendChild(lemon); setTimeout(() => lemon.remove(), 5000); } // Add falling animation const style = document.createElement('style'); style.textContent = ` @keyframes fall { to { transform: translateY(110vh) rotate(${Math.random() * 360}deg); } } `; document.head.appendChild(style); /** * Service status check simulation */ function checkServiceStatus() { const statusIndicators = document.querySelectorAll('.service-status'); statusIndicators.forEach(indicator => { // Random status update for demo purposes const isOnline = Math.random() > 0.1; // 90% online const dot = indicator.querySelector('.status-dot'); if (isOnline) { indicator.className = 'service-status online'; indicator.querySelector('span:last-child').textContent = 'Online'; dot.style.background = '#22c55e'; } else { indicator.className = 'service-status maintenance'; indicator.querySelector('span:last-child').textContent = 'Maintenance'; dot.style.background = '#f59e0b'; } }); } // Simulate status check every 30 seconds setInterval(checkServiceStatus, 30000); /** * Prefers reduced motion */ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { // Disable animations document.documentElement.style.setProperty('--transition-fast', '0s'); document.documentElement.style.setProperty('--transition-normal', '0s'); document.documentElement.style.setProperty('--transition-slow', '0s'); } /** * Service Status Checker - Real-time health checks */ function initServiceStatusChecker() { const statusElements = document.querySelectorAll('.service-status[data-status-url]'); statusElements.forEach(element => { const url = element.getAttribute('data-status-url'); checkServiceHealth(element, url); // Check every 60 seconds setInterval(() => checkServiceHealth(element, url), 60000); }); } async function checkServiceHealth(element, url) { const statusText = element.querySelector('.status-text'); const originalText = statusText.textContent; try { // Try to fetch with a short timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); const response = await fetch(url, { method: 'HEAD', mode: 'no-cors', signal: controller.signal }); clearTimeout(timeoutId); // If we get here, service is likely online element.className = 'service-status online'; statusText.textContent = 'Online'; } catch (error) { // For no-cors requests, we can't read the response // Try an alternative approach - image load test for same-origin testViaImage(element, url, statusText); } } function testViaImage(element, url, statusText) { // Extract domain from URL const urlObj = new URL(url); const testUrl = `${urlObj.protocol}//${urlObj.hostname}/favicon.ico`; const img = new Image(); img.onload = () => { element.className = 'service-status online'; statusText.textContent = 'Online'; }; img.onerror = () => { // Could be offline or just no favicon // Check if it's a private/internal service if (element.closest('.private-service')) { element.className = 'service-status online'; statusText.textContent = 'Restricted'; } else { element.className = 'service-status offline'; statusText.textContent = 'Offline'; } }; img.src = testUrl + '?t=' + Date.now(); // Timeout after 3 seconds setTimeout(() => { if (!img.complete) { img.src = ''; if (element.closest('.private-service')) { element.className = 'service-status online'; statusText.textContent = 'Restricted'; } else { element.className = 'service-status maintenance'; statusText.textContent = 'Unknown'; } } }, 3000); } // Initialize status checker document.addEventListener('DOMContentLoaded', initServiceStatusChecker); /** * Service Info Modals */ function initServiceModals() { const privateServices = document.querySelectorAll('.private-service'); const modals = document.querySelectorAll('.service-modal'); const closeButtons = document.querySelectorAll('.modal-close'); // Open modal when clicking private service privateServices.forEach(service => { service.addEventListener('click', (e) => { e.preventDefault(); const serviceName = service.getAttribute('data-service'); const modal = document.getElementById(`modal-${serviceName}`); if (modal) { modal.classList.add('active'); document.body.style.overflow = 'hidden'; } }); }); // Close modal when clicking close button closeButtons.forEach(button => { button.addEventListener('click', () => { const modal = button.closest('.service-modal'); modal.classList.remove('active'); document.body.style.overflow = ''; }); }); // Close modal when clicking outside modals.forEach(modal => { modal.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.remove('active'); document.body.style.overflow = ''; } }); }); // Close on Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { modals.forEach(modal => { modal.classList.remove('active'); }); document.body.style.overflow = ''; } }); } // Initialize modals document.addEventListener('DOMContentLoaded', initServiceModals);