Clean JavaScript - remove all blocking code, ensure clicks work on service cards

This commit is contained in:
Roberth Rajala 2026-02-02 13:46:59 +01:00
parent 1df28ba1b7
commit a8b173793d
2 changed files with 47 additions and 528 deletions

534
script.js
View File

@ -1,540 +1,30 @@
/**
* LemonLink - Interactive Landing Page
* Modern animations and interactions
* LemonLink - Clean JavaScript (No blocking)
*/
document.addEventListener('DOMContentLoaded', () => {
// Initialize all modules
initNavbar();
initSmoothScroll();
initCounters();
initServiceCards();
initScrollReveal();
initMobileMenu();
initParallax();
});
/**
* Navbar scroll effect
*/
function initNavbar() {
// Simple navbar scroll effect
const navbar = document.querySelector('.navbar');
let lastScroll = 0;
if (navbar) {
window.addEventListener('scroll', () => {
const currentScroll = window.pageYOffset;
// Add/remove scrolled class
if (currentScroll > 50) {
if (window.pageYOffset > 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 = `
<span class="prompt"></span>
<span class="path">~</span>
<span class="command"></span>
<span class="cursor">|</span>
`;
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, '<br>');
terminal.appendChild(outputDiv);
// Move cursor to new line
const nextLine = document.createElement('div');
nextLine.className = 'terminal-line';
nextLine.innerHTML = `
<span class="prompt"></span>
<span class="path">~</span>
<span class="cursor">|</span>
`;
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 = `
<div class="terminal-line">
<span class="prompt"></span>
<span class="path">~</span>
<span class="command">whoami</span>
</div>
<div class="terminal-output">lemon_admin</div>
<div class="terminal-line">
<span class="prompt"></span>
<span class="path">~</span>
<span class="command">uptime</span>
</div>
<div class="terminal-output">Load: 1.06 | RAM: 18% of 96GB | 50+ containers</div>
<div class="terminal-line">
<span class="prompt"></span>
<span class="path">~</span>
<span class="cursor">|</span>
</div>
`;
// Animate each line appearing
const lines = terminalContent.trim().split('</div>').filter(l => l.trim());
terminal.innerHTML = '';
let delay = 0;
lines.forEach((line, index) => {
const cleanLine = line + '</div>';
setTimeout(() => {
const div = document.createElement('div');
div.innerHTML = cleanLine;
terminal.appendChild(div.firstElementChild);
terminal.scrollTop = terminal.scrollHeight;
}, delay);
delay += 600;
});
});
/**
* 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 - Simplified version
* Just shows static statuses to avoid SSL/CORS errors
*/
function initServiceStatusChecker() {
const publicServices = document.querySelectorAll('.service-card.public .service-status');
const privateServices = document.querySelectorAll('.service-card.private-service .service-status');
// Public services show "Online" after a brief delay
publicServices.forEach((el, index) => {
setTimeout(() => {
// Set static statuses (no fetch calls)
document.querySelectorAll('.service-status.checking').forEach(el => {
el.className = 'service-status online';
el.innerHTML = '<span class="status-dot"></span><span>Online</span>';
}, 500 + (index * 200));
});
// Private services show "Restricted" after a brief delay
privateServices.forEach((el, index) => {
setTimeout(() => {
el.className = 'service-status online';
el.innerHTML = '<span class="status-dot"></span><span>Restricted</span>';
}, 500 + (index * 200));
// Simple counter animation
const counters = document.querySelectorAll('.stat-number');
counters.forEach(counter => {
const target = parseFloat(counter.getAttribute('data-target'));
counter.textContent = target;
});
}
// Initialize status checker
document.addEventListener('DOMContentLoaded', initServiceStatusChecker);
/**
* Service Info Modals
*/
// Modal functionality removed - links work normally
});

View File

@ -1957,3 +1957,32 @@ body {
grid-template-columns: 1fr;
}
}
/* Ensure service cards are clickable */
.service-card {
cursor: pointer !important;
position: relative !important;
z-index: 1 !important;
}
.service-card * {
pointer-events: none !important;
}
.service-card a,
.service-card[href] {
pointer-events: auto !important;
}
/* Fix service glow - don't block clicks */
.service-glow {
pointer-events: none !important;
}
/* Ensure links are on top */
a.service-card,
a.service-card:link,
a.service-card:visited {
display: block !important;
text-decoration: none !important;
z-index: 10 !important;
}