/**
* 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 step = target / (duration / 16);
let current = 0;
const updateCounter = () => {
current += step;
if (current < target) {
counter.textContent = current.toFixed(1);
requestAnimationFrame(updateCounter);
} else {
counter.textContent = target;
}
};
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);