Clean JavaScript - remove all blocking code, ensure clicks work on service cards
This commit is contained in:
parent
1df28ba1b7
commit
a8b173793d
546
script.js
546
script.js
|
|
@ -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;
|
||||
|
||||
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'
|
||||
});
|
||||
if (navbar) {
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.pageYOffset > 50) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animated counters for stats
|
||||
*/
|
||||
function initCounters() {
|
||||
// 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>';
|
||||
});
|
||||
|
||||
// Simple counter animation
|
||||
const counters = document.querySelectorAll('.stat-number');
|
||||
|
||||
const animateCounter = (counter) => {
|
||||
counters.forEach(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;
|
||||
counter.textContent = target;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 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(() => {
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize status checker
|
||||
document.addEventListener('DOMContentLoaded', initServiceStatusChecker);
|
||||
|
||||
/**
|
||||
* Service Info Modals
|
||||
*/
|
||||
// Modal functionality removed - links work normally
|
||||
|
|
|
|||
29
styles.css
29
styles.css
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue