659 lines
20 KiB
Plaintext
659 lines
20 KiB
Plaintext
---
|
|
// Terminal.astro
|
|
// A component that displays terminal-like interface with animated commands and outputs
|
|
|
|
interface Command {
|
|
prompt: string;
|
|
command: string;
|
|
output?: string[];
|
|
delay?: number;
|
|
}
|
|
|
|
interface Props {
|
|
commands: Command[];
|
|
title?: string;
|
|
theme?: 'dark' | 'light';
|
|
interactive?: boolean;
|
|
showTitleBar?: boolean;
|
|
}
|
|
|
|
const {
|
|
commands,
|
|
title = "argobox:~/homelab",
|
|
theme = "dark",
|
|
interactive = false,
|
|
showTitleBar = true
|
|
} = Astro.props;
|
|
|
|
// Make the last command have the typing effect
|
|
const lastIndex = commands.length - 1;
|
|
|
|
// Conditionally add classes based on props
|
|
const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : ''}`;
|
|
---
|
|
|
|
<div class={terminalClasses}>
|
|
{showTitleBar && (
|
|
<div class="terminal-header">
|
|
<div class="terminal-dots">
|
|
<div class="terminal-dot terminal-dot-red"></div>
|
|
<div class="terminal-dot terminal-dot-yellow"></div>
|
|
<div class="terminal-dot terminal-dot-green"></div>
|
|
</div>
|
|
<div class="terminal-title">{title}</div>
|
|
<div class="terminal-actions">
|
|
<button class="terminal-button terminal-button-minimize" aria-label="Minimize">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
</svg>
|
|
</button>
|
|
<button class="terminal-button terminal-button-maximize" aria-label="Maximize">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="15 3 21 3 21 9"></polyline>
|
|
<polyline points="9 21 3 21 3 15"></polyline>
|
|
<line x1="21" y1="3" x2="14" y2="10"></line>
|
|
<line x1="3" y1="21" x2="10" y2="14"></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div class="terminal-content">
|
|
{commands.map((cmd, index) => (
|
|
<div class="terminal-block">
|
|
<div class="terminal-line">
|
|
<span class="terminal-prompt">{cmd.prompt}</span>
|
|
<span class={index === lastIndex ? "terminal-command terminal-typing" : "terminal-command"} data-delay={cmd.delay || 50}>
|
|
{cmd.command}
|
|
</span>
|
|
</div>
|
|
{cmd.output && cmd.output.length > 0 && (
|
|
<div class="terminal-output">
|
|
{cmd.output.map((line) => (
|
|
<div class="terminal-output-line" set:html={line} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{interactive && (
|
|
<div class="terminal-block terminal-interactive">
|
|
<div class="terminal-line">
|
|
<span class="terminal-prompt">guest@argobox:~$</span>
|
|
<input type="text" class="terminal-input" placeholder="Type 'help' for available commands" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div class="terminal-cursor"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.terminal-box {
|
|
width: 100%;
|
|
height: 340px;
|
|
background: var(--bg-secondary, #0d1529);
|
|
border-radius: 10px;
|
|
border: 1px solid var(--border-primary, rgba(255, 255, 255, 0.1));
|
|
box-shadow: 0 0 30px rgba(6, 182, 212, 0.1);
|
|
font-family: var(--font-mono, 'JetBrains Mono', monospace);
|
|
font-size: 0.9rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
/* Light theme */
|
|
.terminal-light {
|
|
background: #f0f4f8;
|
|
border-color: #d1dce5;
|
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.07);
|
|
color: #1a202c;
|
|
}
|
|
|
|
.terminal-light .terminal-prompt {
|
|
color: #2a7ac0;
|
|
}
|
|
|
|
.terminal-light .terminal-command {
|
|
color: #1a202c;
|
|
}
|
|
|
|
.terminal-light .terminal-output {
|
|
color: #4a5568;
|
|
}
|
|
|
|
.terminal-light .terminal-header {
|
|
border-bottom: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.terminal-light .terminal-title {
|
|
color: #4a5568;
|
|
}
|
|
|
|
/* Header */
|
|
.terminal-header {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0.75rem 1rem;
|
|
margin-bottom: 0.5rem;
|
|
border-bottom: 1px solid var(--border-secondary, rgba(255, 255, 255, 0.05));
|
|
height: 40px;
|
|
}
|
|
|
|
.terminal-dots {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.terminal-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.terminal-dot-red {
|
|
background: #ef4444;
|
|
}
|
|
|
|
.terminal-dot-yellow {
|
|
background: #eab308;
|
|
}
|
|
|
|
.terminal-dot-green {
|
|
background: #22c55e;
|
|
}
|
|
|
|
.terminal-title {
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
color: var(--text-secondary, #a0aec0);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.terminal-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.terminal-button {
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-secondary, #a0aec0);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0.25rem;
|
|
border-radius: 2px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.terminal-button:hover {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: var(--text-primary, #e2e8f0);
|
|
}
|
|
|
|
/* Content */
|
|
.terminal-content {
|
|
flex: 1;
|
|
color: var(--text-secondary, #a0aec0);
|
|
overflow-y: auto;
|
|
padding: 0.5rem 1.5rem 1.5rem;
|
|
opacity: 0.9;
|
|
position: relative;
|
|
}
|
|
|
|
.terminal-block {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.terminal-line {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
|
|
.terminal-prompt {
|
|
color: var(--accent-primary, #06b6d4);
|
|
margin-right: 0.5rem;
|
|
font-weight: bold;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.terminal-command {
|
|
color: var(--text-primary, #e2e8f0);
|
|
word-break: break-word;
|
|
}
|
|
|
|
.terminal-output {
|
|
margin-top: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
padding-left: 1.25rem;
|
|
color: var(--text-secondary, #a0aec0);
|
|
font-size: 0.85rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.terminal-output-line {
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
/* Highlight syntax in output */
|
|
.terminal-output :global(.highlight) {
|
|
color: var(--accent-primary, #06b6d4);
|
|
}
|
|
|
|
.terminal-output :global(.success) {
|
|
color: #22c55e;
|
|
}
|
|
|
|
.terminal-output :global(.warning) {
|
|
color: #eab308;
|
|
}
|
|
|
|
.terminal-output :global(.error) {
|
|
color: #ef4444;
|
|
}
|
|
|
|
/* Interactive elements */
|
|
.terminal-interactive {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.terminal-input {
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-primary, #e2e8f0);
|
|
font-family: var(--font-mono, 'JetBrains Mono', monospace);
|
|
font-size: 0.9rem;
|
|
flex: 1;
|
|
outline: none;
|
|
caret-color: transparent; /* Hide default cursor */
|
|
}
|
|
|
|
/* Blinking cursor */
|
|
.terminal-cursor {
|
|
position: absolute;
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 16px;
|
|
background: var(--accent-primary, #06b6d4);
|
|
animation: blink 1s infinite;
|
|
bottom: 2rem;
|
|
left: calc(1.5rem + 200px); /* Adjustable using JS */
|
|
opacity: 0;
|
|
}
|
|
|
|
.terminal-interactive:has(.terminal-input:focus) ~ .terminal-cursor {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Typing effect */
|
|
.terminal-typing {
|
|
position: relative;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0; }
|
|
}
|
|
|
|
/* Scrollbar styling */
|
|
.terminal-content::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.terminal-content::-webkit-scrollbar-track {
|
|
background: rgba(15, 23, 42, 0.3);
|
|
}
|
|
|
|
.terminal-content::-webkit-scrollbar-thumb {
|
|
background: rgba(226, 232, 240, 0.2);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.terminal-content::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(226, 232, 240, 0.3);
|
|
}
|
|
|
|
/* Fixed dot hovered appearance */
|
|
.terminal-box:hover .terminal-dot-red {
|
|
background: #f87171;
|
|
}
|
|
|
|
.terminal-box:hover .terminal-dot-yellow {
|
|
background: #fbbf24;
|
|
}
|
|
|
|
.terminal-box:hover .terminal-dot-green {
|
|
background: #34d399;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.terminal-box {
|
|
height: 300px;
|
|
font-size: 0.8rem;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
// Terminal typing effect
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Typing effect for commands
|
|
const typingElements = document.querySelectorAll('.terminal-typing');
|
|
|
|
typingElements.forEach((typingElement, elementIndex) => {
|
|
const text = typingElement.textContent || '';
|
|
const delay = parseInt(typingElement.getAttribute('data-delay') || '50', 10);
|
|
|
|
// Clear the element
|
|
typingElement.textContent = '';
|
|
|
|
let i = 0;
|
|
|
|
// Random delay before starting to type (sequential if there are multiple)
|
|
setTimeout(() => {
|
|
function typeWriter() {
|
|
if (i < text.length) {
|
|
typingElement.textContent += text.charAt(i);
|
|
i++;
|
|
|
|
// Random typing speed for realistic effect
|
|
const randomVariation = Math.random() * 30 - 15; // -15 to +15ms variation
|
|
const speed = delay + randomVariation;
|
|
|
|
setTimeout(typeWriter, speed);
|
|
} else {
|
|
// When done typing, scroll terminal content to bottom
|
|
const terminalContent = typingElement.closest('.terminal-content');
|
|
if (terminalContent) {
|
|
terminalContent.scrollTop = terminalContent.scrollHeight;
|
|
}
|
|
|
|
// Add blinking cursor after the last command
|
|
if (elementIndex === typingElements.length - 1) {
|
|
const cursor = typingElement.closest('.terminal-box').querySelector('.terminal-cursor');
|
|
if (cursor) {
|
|
const rect = typingElement.getBoundingClientRect();
|
|
const parentRect = typingElement.closest('.terminal-content').getBoundingClientRect();
|
|
|
|
// Position cursor after the last character
|
|
cursor.style.opacity = '1';
|
|
cursor.style.left = `${rect.left - parentRect.left + typingElement.offsetWidth}px`;
|
|
cursor.style.top = `${rect.top - parentRect.top}px`;
|
|
cursor.style.height = `${rect.height}px`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
typeWriter();
|
|
}, 1000 * elementIndex); // Sequential delay for multiple typing elements
|
|
});
|
|
|
|
// Interactive terminal functionality
|
|
const interactiveTerminals = document.querySelectorAll('.terminal-interactive');
|
|
|
|
interactiveTerminals.forEach(terminal => {
|
|
const input = terminal.querySelector('.terminal-input');
|
|
const terminalContent = terminal.closest('.terminal-content');
|
|
const prompt = terminal.querySelector('.terminal-prompt').textContent;
|
|
|
|
if (!input || !terminalContent) return;
|
|
|
|
// Position cursor when input is focused
|
|
input.addEventListener('focus', () => {
|
|
const cursor = terminal.closest('.terminal-box').querySelector('.terminal-cursor');
|
|
if (cursor) {
|
|
const rect = input.getBoundingClientRect();
|
|
const parentRect = terminalContent.getBoundingClientRect();
|
|
|
|
cursor.style.left = `${rect.left - parentRect.left + input.value.length * 8}px`;
|
|
cursor.style.top = `${rect.top - parentRect.top}px`;
|
|
cursor.style.height = `${rect.height}px`;
|
|
}
|
|
});
|
|
|
|
// Update cursor position as user types
|
|
input.addEventListener('input', () => {
|
|
const cursor = terminal.closest('.terminal-box').querySelector('.terminal-cursor');
|
|
if (cursor) {
|
|
const rect = input.getBoundingClientRect();
|
|
const parentRect = terminalContent.getBoundingClientRect();
|
|
|
|
cursor.style.left = `${rect.left - parentRect.left + 8 * (prompt.length + input.value.length) + 8}px`;
|
|
}
|
|
});
|
|
|
|
// Process command on Enter
|
|
input.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
|
|
const command = input.value.trim();
|
|
if (!command) return;
|
|
|
|
// Create new command block
|
|
const commandBlock = document.createElement('div');
|
|
commandBlock.className = 'terminal-block';
|
|
|
|
const commandLine = document.createElement('div');
|
|
commandLine.className = 'terminal-line';
|
|
|
|
const promptSpan = document.createElement('span');
|
|
promptSpan.className = 'terminal-prompt';
|
|
promptSpan.textContent = prompt;
|
|
|
|
const commandSpan = document.createElement('span');
|
|
commandSpan.className = 'terminal-command';
|
|
commandSpan.textContent = command;
|
|
|
|
commandLine.appendChild(promptSpan);
|
|
commandLine.appendChild(commandSpan);
|
|
commandBlock.appendChild(commandLine);
|
|
|
|
// Process command and add output
|
|
const output = processCommand(command);
|
|
|
|
if (output && output.length > 0) {
|
|
const outputDiv = document.createElement('div');
|
|
outputDiv.className = 'terminal-output';
|
|
|
|
output.forEach(line => {
|
|
const lineDiv = document.createElement('div');
|
|
lineDiv.className = 'terminal-output-line';
|
|
lineDiv.innerHTML = line;
|
|
outputDiv.appendChild(lineDiv);
|
|
});
|
|
|
|
commandBlock.appendChild(outputDiv);
|
|
}
|
|
|
|
// Insert before the interactive block
|
|
terminal.parentNode.insertBefore(commandBlock, terminal);
|
|
|
|
// Clear input
|
|
input.value = '';
|
|
|
|
// Scroll to bottom
|
|
terminalContent.scrollTop = terminalContent.scrollHeight;
|
|
}
|
|
});
|
|
|
|
// Define available commands and their outputs
|
|
function processCommand(cmd) {
|
|
const commands = {
|
|
'help': [
|
|
'<span class="highlight">Available commands:</span>',
|
|
' help - Display this help message',
|
|
' clear - Clear the terminal',
|
|
' ls - List available resources',
|
|
' cat [file] - View file contents',
|
|
' about - About this site',
|
|
' status - Check system status',
|
|
' uname -a - Display system information'
|
|
],
|
|
'clear': [],
|
|
'about': [
|
|
'<span class="highlight">LaForceIT</span>',
|
|
'A tech blog focused on home lab infrastructure, Kubernetes,',
|
|
'Docker, and DevOps best practices.',
|
|
'',
|
|
'Created by Daniel LaForce',
|
|
'Type <span class="highlight">\'help\'</span> to see available commands'
|
|
],
|
|
'uname -a': [
|
|
'ArgoBox-Lite 5.15.0-69-generic #76-Ubuntu SMP Fri Mar 17 17:19:29 UTC 2023 x86_64',
|
|
'Hardware: ProxmoxVE 8.0.4 | Intel(R) Core(TM) i7-12700K | 64GB RAM'
|
|
],
|
|
'status': [
|
|
'<span class="highlight">System Status:</span>',
|
|
'<span class="success">✓</span> ArgoBox: Online',
|
|
'<span class="success">✓</span> Kubernetes: Running',
|
|
'<span class="success">✓</span> Docker Registry: Active',
|
|
'<span class="success">✓</span> Gitea: Online',
|
|
'<span class="warning">⚠</span> Monitoring: Degraded - Check Grafana instance'
|
|
],
|
|
'ls': [
|
|
'<span class="highlight">Available resources:</span>',
|
|
'kubernetes/ docker/ networking/',
|
|
'homelab.md configs.yaml setup-guide.md',
|
|
'resources.json projects.md'
|
|
]
|
|
};
|
|
|
|
// Check for cat command
|
|
if (cmd.startsWith('cat ')) {
|
|
const file = cmd.split(' ')[1];
|
|
|
|
const fileContents = {
|
|
'homelab.md': [
|
|
'<span class="highlight">## HomeLab Setup Guide</span>',
|
|
'This document outlines my personal home lab setup,',
|
|
'including hardware specifications, network configuration,',
|
|
'and installed services.',
|
|
'',
|
|
'See the full guide at: /homelab'
|
|
],
|
|
'configs.yaml': [
|
|
'apiVersion: v1',
|
|
'kind: ConfigMap',
|
|
'metadata:',
|
|
' name: argobox-config',
|
|
' namespace: default',
|
|
'data:',
|
|
' POSTGRES_HOST: "db.local"',
|
|
' REDIS_HOST: "cache.local"',
|
|
' ...'
|
|
],
|
|
'setup-guide.md': [
|
|
'<span class="highlight">## Quick Start Guide</span>',
|
|
'1. Install Proxmox on bare metal hardware',
|
|
'2. Deploy K3s cluster using Ansible playbooks',
|
|
'3. Configure storage using Longhorn',
|
|
'4. Deploy ArgoCD for GitOps workflow',
|
|
'...'
|
|
],
|
|
'resources.json': [
|
|
'{',
|
|
' "cpu": "12 cores",',
|
|
' "memory": "64GB",',
|
|
' "storage": "8TB",',
|
|
' "network": "10Gbit"',
|
|
'}'
|
|
],
|
|
'projects.md': [
|
|
'<span class="highlight">## Current Projects</span>',
|
|
'- <span class="success">ArgoBox</span>: Self-hosted deployment platform',
|
|
'- <span class="success">K8s Monitor</span>: Custom Kubernetes dashboard',
|
|
'- <span class="warning">Media Server</span>: In progress',
|
|
'- <span class="highlight">See all projects at:</span> /projects'
|
|
]
|
|
};
|
|
|
|
if (fileContents[file]) {
|
|
return fileContents[file];
|
|
} else {
|
|
return [`<span class="error">Error: File '${file}' not found.</span>`];
|
|
}
|
|
}
|
|
|
|
// Handle unknown commands
|
|
if (!commands[cmd]) {
|
|
return [`<span class="error">Command not found: ${cmd}</span>`, 'Type <span class="highlight">\'help\'</span> to see available commands'];
|
|
}
|
|
|
|
// Handle clear command
|
|
if (cmd === 'clear') {
|
|
// Remove all blocks except the interactive one
|
|
const blocks = terminalContent.querySelectorAll('.terminal-block:not(.terminal-interactive)');
|
|
blocks.forEach(block => block.remove());
|
|
return [];
|
|
}
|
|
|
|
return commands[cmd];
|
|
}
|
|
});
|
|
|
|
// Button interactions
|
|
const minButtons = document.querySelectorAll('.terminal-button-minimize');
|
|
const maxButtons = document.querySelectorAll('.terminal-button-maximize');
|
|
|
|
minButtons.forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
const terminalBox = button.closest('.terminal-box');
|
|
terminalBox.classList.toggle('minimized');
|
|
|
|
if (terminalBox.classList.contains('minimized')) {
|
|
const content = terminalBox.querySelector('.terminal-content');
|
|
terminalBox.dataset.prevHeight = terminalBox.style.height;
|
|
terminalBox.style.height = '40px';
|
|
content.style.display = 'none';
|
|
} else {
|
|
const content = terminalBox.querySelector('.terminal-content');
|
|
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px';
|
|
content.style.display = 'block';
|
|
}
|
|
});
|
|
});
|
|
|
|
maxButtons.forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
const terminalBox = button.closest('.terminal-box');
|
|
terminalBox.classList.toggle('maximized');
|
|
|
|
if (terminalBox.classList.contains('maximized')) {
|
|
const content = terminalBox.querySelector('.terminal-content');
|
|
terminalBox.dataset.prevHeight = terminalBox.style.height;
|
|
terminalBox.dataset.prevWidth = terminalBox.style.width;
|
|
terminalBox.dataset.prevPosition = terminalBox.style.position;
|
|
|
|
terminalBox.style.position = 'fixed';
|
|
terminalBox.style.top = '0';
|
|
terminalBox.style.left = '0';
|
|
terminalBox.style.width = '100%';
|
|
terminalBox.style.height = '100%';
|
|
terminalBox.style.zIndex = '9999';
|
|
terminalBox.style.borderRadius = '0';
|
|
} else {
|
|
const content = terminalBox.querySelector('.terminal-content');
|
|
terminalBox.style.position = terminalBox.dataset.prevPosition || 'relative';
|
|
terminalBox.style.width = terminalBox.dataset.prevWidth || '100%';
|
|
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px';
|
|
terminalBox.style.zIndex = 'auto';
|
|
terminalBox.style.borderRadius = '10px';
|
|
terminalBox.style.top = 'auto';
|
|
terminalBox.style.left = 'auto';
|
|
}
|
|
});
|
|
});
|
|
});
|
|
</script> |