Restructure services into Public/Private sections, fix modals, limit terminal animation

This commit is contained in:
Roberth Rajala 2026-02-01 17:25:48 +01:00
parent 79170ffa21
commit 1524944b52
4 changed files with 338 additions and 105 deletions

View File

@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

View File

@ -255,11 +255,46 @@
<div class="section-header">
<span class="section-badge">⚡ Available Now</span>
<h2 class="section-title">Services</h2>
<p class="section-desc">Self-hosted applications and services available on the network</p>
<p class="section-desc">Self-hosted applications running on the LemonLink infrastructure</p>
</div>
<!-- Public Services -->
<div class="services-category">
<h3 class="category-title">
<span class="category-icon">🌐</span>
Public Services
<span class="category-badge">Open Access</span>
</h3>
<div class="services-grid">
<a href="https://cloud.lemonlink.eu" class="service-card" target="_blank" rel="noopener">
<a href="https://git.lemonlink.eu" class="service-card public" target="_blank" rel="noopener">
<div class="service-glow"></div>
<div class="service-icon" style="--icon-color: #609926;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C6.477 0 2 4.477 2 10c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.341-3.369-1.341-.454-1.155-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z"/></svg>
</div>
<h3 class="service-name">Gitea</h3>
<p class="service-desc">Self-hosted Git service for code repositories</p>
<div class="service-status checking" data-status-url="https://git.lemonlink.eu/api/v1/version">
<span class="status-dot"></span>
<span class="status-text">Checking...</span>
</div>
<div class="service-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 17L17 7M17 7H7M17 7V17"/>
</svg>
</div>
</a>
</div>
</div>
<!-- Private Services -->
<div class="services-category">
<h3 class="category-title">
<span class="category-icon">🔒</span>
Private Services
<span class="category-badge">Restricted Access</span>
</h3>
<div class="services-grid">
<div class="service-card private-service" data-service="nextcloud">
<div class="service-glow"></div>
<div class="service-icon" style="--icon-color: #0082c9;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
@ -272,30 +307,12 @@
</div>
<div class="service-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 17L17 7M17 7H7M17 7V17"/>
<path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</a>
</div>
<a href="https://git.lemonlink.eu" class="service-card" target="_blank" rel="noopener">
<div class="service-glow"></div>
<div class="service-icon" style="--icon-color: #609926;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.341-3.369-1.341-.454-1.155-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z"/></svg>
</div>
<h3 class="service-name">Gitea</h3>
<p class="service-desc">Self-hosted Git service for code repositories</p>
<div class="service-status online">
<span class="status-dot"></span>
<span>Online</span>
</div>
<div class="service-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 17L17 7M17 7H7M17 7V17"/>
</svg>
</div>
</a>
<a href="https://stats.lemonlink.eu" class="service-card" target="_blank" rel="noopener">
<div class="service-card private-service" data-service="netdata">
<div class="service-glow"></div>
<div class="service-icon" style="--icon-color: #00ab44;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 3v18h18v-2H5V3H3zm11 12.5l-4-4-5 5L4 16l5-5 4 4 6.5-6.5 1.5 1.5-8 8z"/></svg>
@ -308,10 +325,10 @@
</div>
<div class="service-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 17L17 7M17 7H7M17 7V17"/>
<path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</a>
</div>
<div class="service-card private-service" data-service="immich">
<div class="service-glow"></div>
@ -359,16 +376,14 @@
<div class="service-status checking" data-status-url="https://login.tailscale.com">
<span class="status-dot"></span>
<span class="status-text">Checking...</span>
</div>
<span class="status-dot"></span>
<span>Online</span>
</div>
<div class="service-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 17L17 7M17 7H7M17 7V17"/>
<path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</a>
</div>
</div>
</div>
</div>
</section>
@ -632,6 +647,44 @@
</footer>
<!-- Service Info Modals -->
<div id="modal-nextcloud" class="service-modal">
<div class="modal-content">
<button class="modal-close">&times;</button>
<h3>☁️ Nextcloud</h3>
<p>Private cloud storage platform - your data stays under your control.</p>
<div class="modal-details">
<h4>Features:</h4>
<ul>
<li>File storage and sync</li>
<li>Calendar and contacts</li>
<li>Document collaboration</li>
<li>End-to-end encryption</li>
</ul>
<h4>Access:</h4>
<p>This is a <strong>private service</strong>. User account required.</p>
</div>
</div>
</div>
<div id="modal-netdata" class="service-modal">
<div class="modal-content">
<button class="modal-close">&times;</button>
<h3>📊 Netdata Monitoring</h3>
<p>Real-time infrastructure monitoring with beautiful dashboards.</p>
<div class="modal-details">
<h4>Features:</h4>
<ul>
<li>Real-time metrics</li>
<li>System performance monitoring</li>
<li>Custom alerts</li>
<li>Historical data</li>
</ul>
<h4>Access:</h4>
<p>This is a <strong>private service</strong>. VPN or local network access required.</p>
</div>
</div>
</div>
<div id="modal-immich" class="service-modal">
<div class="modal-content">
<button class="modal-close">&times;</button>

View File

@ -323,11 +323,11 @@ function initTerminalTyping() {
setInterval(typeCommand, 8000);
}
// Initialize terminal typing when contact section is visible
// Initialize terminal typing when contact section is visible - limited to 3 cycles
const terminalObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
initTerminalTyping();
initLimitedTerminalTyping();
terminalObserver.unobserve(entry.target);
}
});
@ -338,6 +338,97 @@ 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 = `
<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 {
setTimeout(() => {
const outputDiv = document.createElement('div');
outputDiv.className = 'terminal-output';
outputDiv.innerHTML = cmd.output.replace(/\n/g, '<br>');
terminal.appendChild(outputDiv);
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);
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
*/

View File

@ -1518,6 +1518,17 @@ body {
color: var(--color-text);
}
/* ========================================
Service Modals - Hide by default
======================================== */
.service-modal {
display: none !important;
}
.service-modal.active {
display: flex !important;
}
/* ========================================
Service Status Checking
======================================== */
@ -1698,3 +1709,73 @@ body {
padding: 1.5rem;
}
}
/* ========================================
Services Categories (Public/Private)
======================================== */
.services-category {
margin-bottom: 3rem;
}
.category-title {
font-family: 'Space Grotesk', sans-serif;
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--color-border);
}
.category-icon {
font-size: 1.5rem;
}
.category-badge {
margin-left: auto;
padding: 0.35rem 0.75rem;
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
border-radius: 100px;
font-size: 0.75rem;
font-weight: 600;
color: #22c55e;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.services-category:last-child .category-badge {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
color: #ef4444;
}
/* Public Service Styling */
.service-card.public {
border-color: rgba(34, 197, 94, 0.2);
}
.service-card.public:hover {
border-color: rgba(34, 197, 94, 0.5);
box-shadow: 0 8px 32px rgba(34, 197, 94, 0.1);
}
/* Private Service Styling */
.service-card.private-service {
border-color: rgba(239, 68, 68, 0.2);
cursor: pointer;
}
.service-card.private-service:hover {
border-color: rgba(239, 68, 68, 0.5);
box-shadow: 0 8px 32px rgba(239, 68, 68, 0.1);
}
.service-card.private-service .service-arrow svg {
width: 20px;
height: 20px;
}