/**
* 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);
}
// Simple Terminal Animation - Pre-defined content, no expanding
document.addEventListener('DOMContentLoaded', () => {
const terminal = document.querySelector('.terminal-body');
if (!terminal) return;
// Pre-defined terminal content (no dynamic expansion)
const terminalContent = `