fresh-main #7

Merged
argonaut merged 4 commits from fresh-main into main 2025-04-26 20:46:13 +00:00
14 changed files with 5106 additions and 1798 deletions
Showing only changes of commit 81008d7b03 - Show all commits

View File

@ -423,9 +423,9 @@ const currentPath = Astro.url.pathname;
} }
}); });
// Search functionality - client-side post filtering // Search functionality - client-side site-wide filtering (User provided version)
const searchResults = document.getElementById('search-results'); const searchResults = document.getElementById('search-results'); // Assuming this ID exists in your dropdown HTML
// Function to perform search // Function to perform search
const performSearch = async (query) => { const performSearch = async (query) => {
if (!query || query.length < 2) { if (!query || query.length < 2) {
@ -437,39 +437,68 @@ const currentPath = Astro.url.pathname;
} }
try { try {
// This would ideally be a server-side search or a pre-built index // Fetch the search index that contains all site content
// For now, we'll just fetch all posts and filter client-side const response = await fetch('/search-index.json'); // Ensure this path is correct based on your build output
const response = await fetch('/search-index.json');
if (!response.ok) throw new Error('Failed to fetch search data'); if (!response.ok) throw new Error('Failed to fetch search data');
const posts = await response.json(); const allContent = await response.json();
const results = posts.filter(post => { const results = allContent.filter(item => {
const lowerQuery = query.toLowerCase(); const lowerQuery = query.toLowerCase();
return ( return (
post.title.toLowerCase().includes(lowerQuery) || item.title.toLowerCase().includes(lowerQuery) ||
post.description?.toLowerCase().includes(lowerQuery) || item.description?.toLowerCase().includes(lowerQuery) ||
post.tags?.some(tag => tag.toLowerCase().includes(lowerQuery)) item.tags?.some(tag => tag.toLowerCase().includes(lowerQuery)) ||
item.category?.toLowerCase().includes(lowerQuery)
); );
}).slice(0, 5); // Limit to 5 results }).slice(0, 8); // Limit to 8 results for better UI
// Display results // Display results
if (searchResults) { if (searchResults) {
if (results.length > 0) { if (results.length > 0) {
searchResults.innerHTML = results.map(post => ` searchResults.innerHTML = results.map(item => {
<div class="search-result-item" data-url="/posts/${post.slug}/"> // Create type badge
<div class="search-result-title">${post.title}</div> let typeBadge = '';
<div class="search-result-snippet">${post.description || ''}</div> switch(item.type) {
</div> case 'post':
`).join(''); typeBadge = '<span class="result-type post">Blog</span>';
break;
case 'project':
typeBadge = '<span class="result-type project">Project</span>';
break;
case 'configuration':
typeBadge = '<span class="result-type config">Config</span>';
break;
case 'external':
typeBadge = '<span class="result-type external">External</span>';
break;
default:
typeBadge = '<span class="result-type">Content</span>';
}
return `
<div class="search-result-item" data-url="${item.url}">
<div class="search-result-header">
<div class="search-result-title">${item.title}</div>
${typeBadge}
</div>
<div class="search-result-snippet">${item.description || ''}</div>
${item.tags && item.tags.length > 0 ?
`<div class="search-result-tags">
${item.tags.slice(0, 3).map(tag => `<span class="search-tag">${tag}</span>`).join('')}
</div>` : ''
}
</div>
`;
}).join('');
// Add click handlers to results // Add click handlers to results
document.querySelectorAll('.search-result-item').forEach(item => { document.querySelectorAll('.search-result-item').forEach(item => {
item.addEventListener('click', () => { item.addEventListener('click', () => {
window.location.href = item.dataset.url; window.location.href = item.dataset.url; // Navigate to the item's URL
}); });
}); });
} else { } else {
searchResults.innerHTML = '<div class="no-results">No matching posts found</div>'; searchResults.innerHTML = '<div class="no-results">No matching content found</div>';
} }
} }
} catch (error) { } catch (error) {
@ -479,8 +508,8 @@ const currentPath = Astro.url.pathname;
} }
} }
}; };
// Search input event handler // Search input event handler with debounce
let searchTimeout; let searchTimeout;
searchInput?.addEventListener('input', (e) => { searchInput?.addEventListener('input', (e) => {
clearTimeout(searchTimeout); clearTimeout(searchTimeout);
@ -488,18 +517,19 @@ const currentPath = Astro.url.pathname;
performSearch(e.target.value); performSearch(e.target.value);
}, 300); // Debounce to avoid too many searches while typing }, 300); // Debounce to avoid too many searches while typing
}); });
// Handle search form submission // Handle search form submission (if your input is inside a form)
const searchForm = searchInput?.closest('form'); const searchForm = searchInput?.closest('form');
searchForm?.addEventListener('submit', (e) => { searchForm?.addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault(); // Prevent default form submission
performSearch(searchInput.value); performSearch(searchInput.value);
}); });
// Handle search-submit button click // Handle search-submit button click (if you have a separate submit button)
const searchSubmit = document.querySelector('.search-submit'); const searchSubmit = document.querySelector('.search-submit'); // Adjust selector if needed
searchSubmit?.addEventListener('click', () => { searchSubmit?.addEventListener('click', () => {
performSearch(searchInput?.value || ''); performSearch(searchInput?.value || '');
}); });
});
}); // End of DOMContentLoaded
</script> </script>

View File

@ -8,6 +8,8 @@ import Footer from '../components/Footer.astro';
import Terminal from '../components/Terminal.astro'; import Terminal from '../components/Terminal.astro';
import KnowledgeGraph from '../components/KnowledgeGraph.astro'; import KnowledgeGraph from '../components/KnowledgeGraph.astro';
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
import { Image } from 'astro:assets';
import { COMMON_COMMANDS, TERMINAL_CONTENT } from '../config/terminal.js';
// Get all blog entries // Get all blog entries
const allPosts = await getCollection('posts'); const allPosts = await getCollection('posts');
@ -22,38 +24,8 @@ const sortedPosts = allPosts.sort((a, b) => {
// Get recent posts (latest 4) // Get recent posts (latest 4)
const recentPosts = sortedPosts.slice(0, 4); const recentPosts = sortedPosts.slice(0, 4);
// Prepare terminal commands // Prepare terminal commands - now imported from central config
const terminalCommands = [ const terminalCommands = COMMON_COMMANDS;
{
prompt: "[laforceit@argobox]$ ",
command: "ls -la ./infrastructure",
output: [
"total 20",
"drwxr-xr-x 5 laforceit users 4096 Apr 23 09:15 <span class='highlight'>kubernetes/</span>",
"drwxr-xr-x 3 laforceit users 4096 Apr 20 17:22 <span class='highlight'>docker/</span>",
"drwxr-xr-x 2 laforceit users 4096 Apr 19 14:30 <span class='highlight'>networking/</span>",
"drwxr-xr-x 4 laforceit users 4096 Apr 22 21:10 <span class='highlight'>monitoring/</span>",
"drwxr-xr-x 3 laforceit users 4096 Apr 21 16:45 <span class='highlight'>storage/</span>",
]
},
{
prompt: "[laforceit@argobox]$ ",
command: "find ./posts -type f -name \"*.md\" | wc -l",
output: [`${allPosts.length} posts found`]
},
{
prompt: "[laforceit@argobox]$ ",
command: "kubectl get nodes",
output: [
"NAME STATUS ROLES AGE VERSION",
"argobox-cp1 Ready control-plane,master 92d v1.27.3",
"argobox-cp2 Ready control-plane,master 92d v1.27.3",
"argobox-cp3 Ready control-plane,master 92d v1.27.3",
"argobox-node1 Ready worker 92d v1.27.3",
"argobox-node2 Ready worker 92d v1.27.3"
]
}
];
// Prepare graph data for knowledge map // Prepare graph data for knowledge map
// Extract categories and tags from posts // Extract categories and tags from posts
@ -364,6 +336,38 @@ const techStack = [
</div> </div>
</div> </div>
</section> </section>
<!-- Terminal Section -->
<section class="terminal-section">
<div class="container">
<div class="row">
<div class="col-12 col-md-6">
<Terminal
title="argobox:~/kubernetes"
promptPrefix={TERMINAL_DEFAULTS.promptPrefix}
height="400px"
command="kubectl get pods -A | head -8"
output={`NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-66bff467f8-8p7z2 1/1 Running 0 15d
kube-system coredns-66bff467f8-v68vr 1/1 Running 0 15d
kube-system etcd-control-plane 1/1 Running 0 15d
kube-system kube-apiserver-control-plane 1/1 Running 0 15d
kube-system kube-controller-manager-control-plane 1/1 Running 0 15d
kube-system kube-proxy-c84qf 1/1 Running 0 15d
kube-system kube-scheduler-control-plane 1/1 Running 0 15d`}
/>
</div>
<div class="col-12 col-md-6">
<Terminal
title="argobox:~/system"
promptPrefix={TERMINAL_DEFAULTS.promptPrefix}
height="400px"
content={SYSTEM_MONITOR_SEQUENCE.map(item => `<div class="term-blue">${item.prompt}</div><span>$</span> <span class="term-bold">${item.command}</span>\n${item.output.join('\n')}`).join('\n\n')}
/>
</div>
</div>
</div>
</section>
</main> </main>
<Footer slot="footer" /> <Footer slot="footer" />
@ -993,6 +997,32 @@ const techStack = [
background: rgba(226, 232, 240, 0.2); background: rgba(226, 232, 240, 0.2);
} }
/* Terminal Section */
.terminal-section {
padding: 6rem 0;
background: var(--bg-primary);
position: relative;
}
.section-title {
font-size: clamp(1.75rem, 3vw, 2.5rem);
margin-bottom: 1rem;
position: relative;
display: inline-block;
}
.section-title::after {
content: '';
position: absolute;
height: 4px;
width: 60px;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
bottom: -10px;
left: 50%;
transform: translateX(-50%);
border-radius: 2px;
}
/* Responsive Design */ /* Responsive Design */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.hero-content { .hero-content {

View File

@ -77,6 +77,32 @@ const nodeTypeCounts = {
<script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script> <script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script>
<div class="graph-container-wrapper" style={`--graph-height: ${height};`}> <div class="graph-container-wrapper" style={`--graph-height: ${height};`}>
<!-- Physics Toggle Button -->
<button id="toggle-physics" class="physics-toggle" aria-label="Physics settings">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"></path><path d="M2 12h20"></path></svg>
</button>
<!-- Physics Controls Panel -->
<div id="physics-panel" class="physics-controls">
<div class="physics-control-group">
<label class="physics-label">Node Repulsion: <span id="repulsion-value">9000</span></label>
<input type="range" id="node-repulsion" class="physics-slider" min="1000" max="20000" step="1000" value="9000">
</div>
<div class="physics-control-group">
<label class="physics-label">Edge Elasticity: <span id="elasticity-value">50</span></label>
<input type="range" id="edge-elasticity" class="physics-slider" min="10" max="100" step="5" value="50">
</div>
<div class="physics-control-group">
<label class="physics-label">Gravity: <span id="gravity-value">10</span></label>
<input type="range" id="gravity" class="physics-slider" min="0" max="50" step="5" value="10">
</div>
<button id="apply-physics" class="physics-button">Apply Settings</button>
<button id="reset-physics" class="physics-button">Reset to Defaults</button>
</div>
<!-- Graph Instructions --> <!-- Graph Instructions -->
<div class="graph-instructions"> <div class="graph-instructions">
<details class="instructions-details"> <details class="instructions-details">
@ -160,8 +186,34 @@ const nodeTypeCounts = {
<button id="reset-graph" class="graph-action" aria-label="Reset View"> <button id="reset-graph" class="graph-action" aria-label="Reset View">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 2v6h6"></path><path d="M21 12A9 9 0 0 0 6 5.3L3 8"></path><path d="M21 22v-6h-6"></path><path d="M3 12a9 9 0 0 0 15 6.7l3-2.7"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 2v6h6"></path><path d="M21 12A9 9 0 0 0 6 5.3L3 8"></path><path d="M21 22v-6h-6"></path><path d="M3 12a9 9 0 0 0 15 6.7l3-2.7"></path></svg>
</button> </button>
<button id="fullscreen-graph" class="graph-action" aria-label="Fullscreen">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3"></path><path d="M21 8V5a2 2 0 0 0-2-2h-3"></path><path d="M3 16v3a2 2 0 0 0 2 2h3"></path><path d="M16 21h3a2 2 0 0 0 2-2v-3"></path></svg>
</button>
</div> </div>
</div> </div>
<!-- Fullscreen Mode Split View -->
<div id="fullscreen-container" class="fullscreen-container">
<div class="fullscreen-header">
<h2>Knowledge Graph Explorer</h2>
<div class="fullscreen-controls">
<button id="fullscreen-close" class="fullscreen-btn" aria-label="Close Fullscreen">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"></path><path d="M9 21H3v-6"></path><path d="M21 3l-7 7"></path><path d="M3 21l7-7"></path></svg>
Exit Fullscreen
</button>
</div>
</div>
<div class="fullscreen-content">
<div id="fullscreen-graph" class="fullscreen-graph-container"></div>
<div id="fullscreen-article" class="fullscreen-article-container">
<div class="article-placeholder">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg>
<p>Select a post in the graph to view its content here</p>
</div>
<div id="article-content" class="article-content"></div>
</div>
</div>
</div>
</div> </div>
<script define:vars={{ graphData, nodeTypeColors, nodeSizes, predefinedColors, initialFilter }}> <script define:vars={{ graphData, nodeTypeColors, nodeSizes, predefinedColors, initialFilter }}>
@ -318,27 +370,103 @@ const nodeTypeCounts = {
}} }}
], ],
// Update layout for better visualization of post-tag connections // Update layout for better visualization of post-tag connections
layout: { // Replace the existing layout configuration with this improved one
name: 'cose', layout: {
idealEdgeLength: 80, name: 'cose',
nodeOverlap: 20, idealEdgeLength: 75, // Increased from previous value
refresh: 20, nodeOverlap: 30, // Increased to prevent overlap
fit: true, refresh: 20,
padding: 30, fit: true,
randomize: false, padding: 30,
componentSpacing: 100, randomize: false,
nodeRepulsion: 450000, componentSpacing: 60,
edgeElasticity: 100, nodeRepulsion: 1000000, // Significantly increased repulsion
nestingFactor: 5, edgeElasticity: 150, // Increased elasticity
gravity: 80, nestingFactor: 7, // Adjusted nesting
numIter: 1000, gravity: 30, // Reduced gravity for more spread
initialTemp: 200, numIter: 2000, // Increased iterations for better settling
coolingFactor: 0.95, initialTemp: 250, // Higher initial temperature
minTemp: 1.0 coolingFactor: 0.95, // Slower cooling for better layout
minTemp: 1.0,
animate: true,
animationDuration: 800 // Longer animation
}, },
zoom: 1, minZoom: 0.1, maxZoom: 3, zoomingEnabled: true, userZoomingEnabled: true, panningEnabled: true, userPanningEnabled: true, boxSelectionEnabled: false, zoom: 1, minZoom: 0.1, maxZoom: 3, zoomingEnabled: true, userZoomingEnabled: true, panningEnabled: true, userPanningEnabled: true, boxSelectionEnabled: false,
}); });
// Add improved Obsidian-like dragging behavior
cy.on('drag', 'node', function(evt){
const node = evt.target;
node.lock(); // Lock node during drag to prevent physics interference
// Get immediate neighbors (directly connected nodes)
const directNeighbors = node.neighborhood('node');
// Apply a gentle pull to immediate neighbors
if (directNeighbors.length > 0) {
directNeighbors.forEach(neighbor => {
// Don't move nodes that are being manually dragged
if (!neighbor.grabbed()) {
// Calculate distance and direction
const dx = node.position('x') - neighbor.position('x');
const dy = node.position('y') - neighbor.position('y');
const distance = Math.sqrt(dx*dx + dy*dy);
// Apply gentle force (stronger for closer nodes)
const pullFactor = Math.min(0.05, 10/distance);
neighbor.position({
x: neighbor.position('x') + dx * pullFactor,
y: neighbor.position('y') + dy * pullFactor
});
}
});
}
});
cy.on('dragfree', 'node', function(evt){
const node = evt.target;
setTimeout(() => {
node.unlock(); // Unlock after drag complete
}, 50);
});
// Create physics controls object to store settings
const physicsSettings = {
nodeRepulsion: 9000,
edgeElasticity: 50,
gravity: 10,
animate: true
};
// Function to update physics settings
function updatePhysics(settings) {
cy.layout({
name: 'cose',
idealEdgeLength: 100,
nodeOverlap: 30,
refresh: 20,
fit: false, // Don't fit to viewport to maintain current view
padding: 40,
randomize: false,
componentSpacing: 120,
nodeRepulsion: settings.nodeRepulsion,
edgeElasticity: settings.edgeElasticity,
nestingFactor: 5,
gravity: settings.gravity,
numIter: 1000,
initialTemp: 200,
coolingFactor: 0.95,
minTemp: 1.0,
animate: settings.animate
}).run();
}
// Expose physics settings to global scope for external controls
window.graphPhysics = {
settings: physicsSettings,
update: updatePhysics
};
// Hide loading screen // Hide loading screen
if (loadingEl) { if (loadingEl) {
setTimeout(() => { loadingEl.classList.add('hidden'); }, 500); setTimeout(() => { loadingEl.classList.add('hidden'); }, 500);
@ -576,41 +704,6 @@ const nodeTypeCounts = {
} }
} }
// Add Obsidian-like dragging behavior
// When dragging a node, connected nodes follow with a damping effect
cy.on('drag', 'node', function(e) {
const node = e.target;
const neighbors = node.neighborhood('node');
// Add grabbed class for styling
node.addClass('grabbed');
if (neighbors.length > 0) {
neighbors.forEach(neighbor => {
// Don't move nodes that are being manually dragged by the user
if (!neighbor.grabbed()) {
// Calculate the position to move the neighbor node
// This creates a "pull" effect where neighbors follow but with resistance
// The 0.2 factor controls how much the neighbor follows (smaller = less movement)
const damping = 0.2;
const dx = node.position('x') - neighbor.position('x');
const dy = node.position('y') - neighbor.position('y');
// Apply the position change with damping
neighbor.position({
x: neighbor.position('x') + dx * damping,
y: neighbor.position('y') + dy * damping
});
}
});
}
});
cy.on('dragfree', 'node', function(e) {
// Remove grabbed class when drag ends
e.target.removeClass('grabbed');
});
// Connect search input if it exists // Connect search input if it exists
const searchInput = document.getElementById('search-input'); const searchInput = document.getElementById('search-input');
if (searchInput) { if (searchInput) {
@ -680,6 +773,235 @@ const nodeTypeCounts = {
// Dispatch graphReady event for external listeners // Dispatch graphReady event for external listeners
document.dispatchEvent(new CustomEvent('graphReady', { detail: { cy } })); document.dispatchEvent(new CustomEvent('graphReady', { detail: { cy } }));
// Fullscreen mode functionality
const fullscreenBtn = document.getElementById('fullscreen-graph');
const fullscreenContainer = document.getElementById('fullscreen-container');
const fullscreenClose = document.getElementById('fullscreen-close');
const fullscreenGraphContainer = document.getElementById('fullscreen-graph');
const fullscreenArticle = document.getElementById('fullscreen-article');
const articleContent = document.getElementById('article-content');
let fullscreenCy = null;
if (fullscreenBtn && fullscreenContainer) {
fullscreenBtn.addEventListener('click', () => {
// Show fullscreen container
fullscreenContainer.classList.add('active');
document.body.classList.add('graph-fullscreen-mode');
// Create a clone of the graph in fullscreen mode if it doesn't exist
if (!fullscreenCy) {
// Small delay to ensure container is visible first
setTimeout(() => {
// Clone the Cytoscape graph
fullscreenCy = cytoscape({
container: fullscreenGraphContainer,
elements: cy.json().elements,
style: cy.style().json(),
layout: { name: 'preset' }, // Use preset layout to maintain positions
zoom: cy.zoom(),
pan: cy.pan()
});
// Add same event listeners to the fullscreen graph
fullscreenCy.on('tap', 'node', function(e) {
const node = e.target;
const nodeData = node.data();
// Highlight the node and its connections
fullscreenCy.elements().removeClass('highlighted').addClass('faded');
node.removeClass('faded').addClass('highlighted');
node.neighborhood().removeClass('faded').addClass('highlighted');
// If it's a post node, load the content
if (nodeData.type === 'post' && nodeData.url && nodeData.url !== '#') {
// Show loading state
articleContent.innerHTML = '<div class="article-loading">Loading article...</div>';
// Remove placeholder if exists
const placeholder = fullscreenArticle.querySelector('.article-placeholder');
if (placeholder) placeholder.classList.add('hidden');
// Show content container
articleContent.classList.remove('hidden');
// Fetch and display article content
fetch(nodeData.url)
.then(response => response.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Extract the article content - adjust selector to match your blog structure
const articleElement = doc.querySelector('article') ||
doc.querySelector('.blog-post-content') ||
doc.querySelector('main');
if (articleElement) {
// Create header for the article
const articleHeader = document.createElement('div');
articleHeader.className = 'article-header';
articleHeader.innerHTML = `
<h1>${nodeData.label}</h1>
<a href="${nodeData.url}" class="article-link" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
View Full Article
</a>
`;
// Set content
articleContent.innerHTML = '';
articleContent.appendChild(articleHeader);
articleContent.appendChild(articleElement);
} else {
articleContent.innerHTML = `
<div class="article-error">
<h2>${nodeData.label}</h2>
<p>Unable to load article content directly.</p>
<a href="${nodeData.url}" class="article-link" target="_blank">
Open Article in New Tab
</a>
</div>
`;
}
})
.catch(error => {
console.error('Error loading article:', error);
articleContent.innerHTML = `
<div class="article-error">
<h2>Error Loading Content</h2>
<p>There was a problem loading the article content.</p>
<a href="${nodeData.url}" class="article-link" target="_blank">
Open Article in New Tab
</a>
</div>
`;
});
} else if (nodeData.type === 'tag') {
// For tag nodes, show related posts
const connectedPosts = node.neighborhood('node[type="post"]');
// Hide placeholder if exists
const placeholder = fullscreenArticle.querySelector('.article-placeholder');
if (placeholder) placeholder.classList.add('hidden');
// Show content container
articleContent.classList.remove('hidden');
articleContent.innerHTML = `
<div class="tag-posts">
<h2>Posts tagged with: ${nodeData.label}</h2>
<ul class="related-posts-list">
${connectedPosts.length === 0
? '<li>No posts found with this tag</li>'
: connectedPosts.map(post => {
const postData = post.data();
return `
<li>
<a href="${postData.url}" data-id="${postData.id}" class="post-link">
${postData.label}
</a>
</li>
`;
}).join('')
}
</ul>
</div>
`;
// Add click handlers for the post links
setTimeout(() => {
document.querySelectorAll('.post-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const postId = link.dataset.id;
const postNode = fullscreenCy.getElementById(postId);
if (postNode.length > 0) {
postNode.trigger('tap');
}
});
});
}, 100);
}
});
fullscreenCy.on('tap', function(e) {
if (e.target === fullscreenCy) {
fullscreenCy.elements().removeClass('highlighted faded');
}
});
// Add drag behavior
fullscreenCy.on('drag', 'node', function(evt){
const node = evt.target;
node.lock();
// Get immediate neighbors
const directNeighbors = node.neighborhood('node');
// Apply gentle pull to immediate neighbors
if (directNeighbors.length > 0) {
directNeighbors.forEach(neighbor => {
if (!neighbor.grabbed()) {
const dx = node.position('x') - neighbor.position('x');
const dy = node.position('y') - neighbor.position('y');
const distance = Math.sqrt(dx*dx + dy*dy);
const pullFactor = Math.min(0.05, 10/distance);
neighbor.position({
x: neighbor.position('x') + dx * pullFactor,
y: neighbor.position('y') + dy * pullFactor
});
}
});
}
});
fullscreenCy.on('dragfree', 'node', function(evt){
const node = evt.target;
setTimeout(() => {
node.unlock();
}, 50);
});
}, 100);
} else {
// If fullscreenCy already exists, update its elements and layout
fullscreenCy.json({ elements: cy.json().elements });
fullscreenCy.layout({ name: 'preset' }).run();
fullscreenCy.zoom(cy.zoom());
fullscreenCy.pan(cy.pan());
}
});
// Close fullscreen mode
if (fullscreenClose) {
fullscreenClose.addEventListener('click', () => {
fullscreenContainer.classList.remove('active');
document.body.classList.remove('graph-fullscreen-mode');
// Hide any loaded content
if (articleContent) {
articleContent.innerHTML = '';
articleContent.classList.add('hidden');
}
// Show placeholder again
const placeholder = fullscreenArticle.querySelector('.article-placeholder');
if (placeholder) placeholder.classList.remove('hidden');
// If the main graph exists, sync positions from fullscreen graph
if (cy && fullscreenCy) {
// Transfer node positions from fullscreen to main graph
fullscreenCy.nodes().forEach(node => {
const mainNode = cy.getElementById(node.id());
if (mainNode.length > 0) {
mainNode.position(node.position());
}
});
}
});
}
}
} }
// Initialize graph on DOMContentLoaded or if already loaded // Initialize graph on DOMContentLoaded or if already loaded
@ -718,7 +1040,7 @@ const nodeTypeCounts = {
#knowledge-graph { #knowledge-graph {
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 1; z-index: 4; /* Increase z-index to ensure nodes appear above instructions */
} }
/* Loading Animation */ /* Loading Animation */
@ -1068,18 +1390,30 @@ const nodeTypeCounts = {
.graph-instructions { .graph-instructions {
margin-bottom: 1rem; margin-bottom: 1rem;
width: 100%; width: 100%;
position: relative;
z-index: 5; /* Higher than graph for visibility but lower than open instructions */
} }
.instructions-details { .instructions-details {
background: rgba(15, 23, 42, 0.3); background: rgba(15, 23, 42, 0.9); /* More opaque background */
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--border-primary); border: 1px solid var(--border-primary);
overflow: hidden; overflow: hidden;
transition: all 0.3s ease; transition: all 0.3s ease;
backdrop-filter: blur(10px);
position: relative; /* Changed from absolute */
width: 100%;
} }
.instructions-details[open] { .instructions-details[open] {
padding-bottom: 1rem; padding-bottom: 1rem;
z-index: 20; /* Very high z-index when open */
}
.instructions-details:not([open]) {
background: rgba(15, 23, 42, 0.8);
border: 1px solid var(--border-primary);
backdrop-filter: blur(5px);
} }
.instructions-summary { .instructions-summary {
@ -1090,6 +1424,8 @@ const nodeTypeCounts = {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
position: relative;
z-index: 3;
} }
.instructions-summary::before { .instructions-summary::before {
@ -1098,8 +1434,16 @@ const nodeTypeCounts = {
} }
.instructions-content { .instructions-content {
padding: 0 1rem; padding: 0.5rem 1rem 1rem;
color: var(--text-secondary); color: var(--text-primary);
line-height: 1.5;
max-width: 800px;
position: relative;
z-index: 3;
}
.instructions-content p {
margin-bottom: 0.5rem;
} }
.instructions-content ul { .instructions-content ul {
@ -1158,4 +1502,437 @@ const nodeTypeCounts = {
font-size: 0.75rem; font-size: 0.75rem;
} }
} }
</style>
/* Fullscreen Mode Styles */
.fullscreen-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: var(--bg-primary, #0f172a);
z-index: 9999;
display: flex;
flex-direction: column;
visibility: hidden;
opacity: 0;
transition: all 0.3s ease;
color: var(--text-primary, #e2e8f0);
}
.fullscreen-container.active {
visibility: visible;
opacity: 1;
}
.fullscreen-header {
height: 60px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1.5rem;
background: var(--bg-secondary, #1e293b);
border-bottom: 1px solid var(--border-primary, #334155);
}
.fullscreen-header h2 {
font-size: 1.2rem;
margin: 0;
color: var(--text-primary, #e2e8f0);
}
.fullscreen-controls {
display: flex;
gap: 1rem;
}
.fullscreen-btn {
display: flex;
align-items: center;
gap: 0.5rem;
background: rgba(226, 232, 240, 0.05);
color: var(--text-primary, #e2e8f0);
border: 1px solid var(--border-primary, #334155);
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s ease;
}
.fullscreen-btn:hover {
background: rgba(226, 232, 240, 0.1);
border-color: var(--accent-primary, #38bdf8);
}
.fullscreen-content {
flex: 1;
display: flex;
overflow: hidden;
}
.fullscreen-graph-container {
width: 60%;
height: 100%;
background: var(--bg-primary, #0f172a);
border-right: 1px solid var(--border-primary, #334155);
position: relative;
}
.fullscreen-article-container {
width: 40%;
height: 100%;
overflow-y: auto;
padding: 0;
background: var(--bg-secondary, #1e293b);
}
.article-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary, #94a3b8);
text-align: center;
padding: 2rem;
}
.article-placeholder svg {
opacity: 0.4;
margin-bottom: 1rem;
}
.article-placeholder p {
font-size: 1.1rem;
max-width: 300px;
}
.article-content {
padding: 2rem;
}
.article-content.hidden {
display: none;
}
.article-header {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-primary, #334155);
}
.article-header h1 {
font-size: 1.8rem;
margin: 0 0 1rem 0;
line-height: 1.3;
color: var(--text-primary, #e2e8f0);
}
.article-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--accent-primary, #38bdf8);
text-decoration: none;
font-size: 0.9rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--accent-primary, #38bdf8);
border-radius: 6px;
transition: all 0.2s ease;
}
.article-link:hover {
background: rgba(56, 189, 248, 0.1);
transform: translateY(-2px);
}
.article-loading, .article-error {
padding: 2rem;
text-align: center;
color: var(--text-secondary, #94a3b8);
}
.tag-posts {
padding: 2rem;
}
.tag-posts h2 {
font-size: 1.4rem;
margin-bottom: 1.5rem;
color: var(--text-primary, #e2e8f0);
}
.related-posts-list {
list-style: none;
padding: 0;
margin: 0;
}
.related-posts-list li {
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border-secondary, #1f2937);
}
.related-posts-list .post-link {
color: var(--accent-primary, #38bdf8);
text-decoration: none;
font-size: 1rem;
transition: all 0.2s ease;
display: block;
padding: 0.5rem;
border-radius: 4px;
}
.related-posts-list .post-link:hover {
background: rgba(56, 189, 248, 0.05);
color: var(--accent-secondary, #0ea5e9);
transform: translateX(4px);
}
.hidden {
display: none !important;
}
body.graph-fullscreen-mode {
overflow: hidden;
}
/* Physics Controls Styles */
.physics-toggle {
position: absolute;
top: 20px;
right: 20px;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 15;
transition: all 0.2s ease;
}
.physics-toggle:hover {
transform: rotate(20deg);
background: var(--bg-tertiary);
}
.physics-controls {
position: absolute;
top: 70px;
right: 20px;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 8px;
padding: 1rem;
width: 250px;
z-index: 20;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
transform: translateX(120%);
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
opacity: 0;
backdrop-filter: blur(10px);
display: none;
}
.physics-controls.active {
transform: translateX(0);
opacity: 1;
display: block;
}
.physics-control-group {
margin-bottom: 1rem;
}
.physics-label {
display: block;
font-size: 0.8rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
font-family: var(--font-mono);
}
.physics-slider {
width: 100%;
height: 4px;
background: rgba(226, 232, 240, 0.1);
border-radius: 2px;
outline: none;
appearance: none;
}
.physics-slider::-webkit-slider-thumb {
appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--accent-primary);
cursor: pointer;
border: none;
}
.physics-slider::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--accent-primary);
cursor: pointer;
border: none;
}
.physics-button {
background: rgba(226, 232, 240, 0.05);
color: var(--text-primary);
border: 1px solid var(--border-primary);
padding: 0.5rem 0.75rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
width: 100%;
margin-top: 0.5rem;
transition: all 0.2s ease;
}
.physics-button:hover {
background: rgba(226, 232, 240, 0.1);
border-color: var(--accent-primary);
}
/* Media Queries for Fullscreen Mode */
@media screen and (max-width: 900px) {
.fullscreen-content {
flex-direction: column;
}
.fullscreen-graph-container,
.fullscreen-article-container {
width: 100%;
height: 50%;
}
.fullscreen-graph-container {
border-right: none;
border-bottom: 1px solid var(--border-primary);
}
.article-content {
padding: 1.5rem;
}
}
@media screen and (max-width: 768px) {
// ... existing media queries ...
.fullscreen-header {
padding: 0 1rem;
}
.fullscreen-header h2 {
font-size: 1rem;
}
.fullscreen-btn {
padding: 0.4rem 0.75rem;
font-size: 0.8rem;
}
.article-header h1 {
font-size: 1.5rem;
}
.physics-controls {
width: 200px;
right: 50px;
}
}
</style>
<script>
// Add Physics Controls UI interaction
document.addEventListener('DOMContentLoaded', () => {
const physicsToggle = document.getElementById('toggle-physics');
const physicsPanel = document.getElementById('physics-panel');
if (physicsToggle && physicsPanel) {
// Toggle physics panel visibility
physicsToggle.addEventListener('click', () => {
physicsPanel.classList.toggle('active');
});
// Update value displays as sliders change
document.getElementById('node-repulsion')?.addEventListener('input', (e) => {
document.getElementById('repulsion-value').textContent = e.target.value;
});
document.getElementById('edge-elasticity')?.addEventListener('input', (e) => {
document.getElementById('elasticity-value').textContent = e.target.value;
});
document.getElementById('gravity')?.addEventListener('input', (e) => {
document.getElementById('gravity-value').textContent = e.target.value;
});
// Apply physics settings when button is clicked
document.getElementById('apply-physics')?.addEventListener('click', () => {
if (window.graphPhysics) {
const newSettings = {
nodeRepulsion: parseInt(document.getElementById('node-repulsion').value),
edgeElasticity: parseInt(document.getElementById('edge-elasticity').value),
gravity: parseInt(document.getElementById('gravity').value),
animate: true
};
// Update settings object
Object.assign(window.graphPhysics.settings, newSettings);
// Apply settings
window.graphPhysics.update(newSettings);
// Close panel after applying
physicsPanel.classList.remove('active');
}
});
// Reset physics to defaults
document.getElementById('reset-physics')?.addEventListener('click', () => {
if (window.graphPhysics) {
const defaultSettings = {
nodeRepulsion: 9000,
edgeElasticity: 50,
gravity: 10,
animate: true
};
// Update UI
document.getElementById('node-repulsion').value = defaultSettings.nodeRepulsion;
document.getElementById('repulsion-value').textContent = defaultSettings.nodeRepulsion;
document.getElementById('edge-elasticity').value = defaultSettings.edgeElasticity;
document.getElementById('elasticity-value').textContent = defaultSettings.edgeElasticity;
document.getElementById('gravity').value = defaultSettings.gravity;
document.getElementById('gravity-value').textContent = defaultSettings.gravity;
// Update settings object
Object.assign(window.graphPhysics.settings, defaultSettings);
// Apply settings
window.graphPhysics.update(defaultSettings);
}
});
// Click outside to close physics panel
document.addEventListener('click', (e) => {
if (!physicsPanel.contains(e.target) && e.target !== physicsToggle) {
physicsPanel.classList.remove('active');
}
});
}
});
</script>

File diff suppressed because it is too large Load Diff

View File

@ -2,37 +2,32 @@
// Terminal.astro // Terminal.astro
// A component that displays terminal-like interface with animated commands and outputs // A component that displays terminal-like interface with animated commands and outputs
interface Command { export interface Props {
prompt: string;
command: string;
output?: string[];
delay?: number;
}
interface Props {
commands: Command[];
title?: string; title?: string;
theme?: 'dark' | 'light'; height?: string;
interactive?: boolean;
showTitleBar?: boolean; showTitleBar?: boolean;
showPrompt?: boolean;
commands?: {
prompt: string;
command: string;
output?: string[];
delay?: number;
}[];
} }
const { const {
commands, title = "terminal",
title = "argobox:~/homelab", height = "auto",
theme = "dark", showTitleBar = true,
interactive = false, showPrompt = true,
showTitleBar = true commands = []
} = Astro.props; } = Astro.props;
// Make the last command have the typing effect // Make the last command have the typing effect
const lastIndex = commands.length - 1; const lastIndex = commands.length - 1;
// Conditionally add classes based on props
const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : ''}`;
--- ---
<div class={terminalClasses}> <div class="terminal-box">
{showTitleBar && ( {showTitleBar && (
<div class="terminal-header"> <div class="terminal-header">
<div class="terminal-dots"> <div class="terminal-dots">
@ -59,7 +54,7 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
</div> </div>
)} )}
<div class="terminal-content"> <div class="terminal-content" style={`height: ${height};`}>
{commands.map((cmd, index) => ( {commands.map((cmd, index) => (
<div class="terminal-block"> <div class="terminal-block">
<div class="terminal-line"> <div class="terminal-line">
@ -78,15 +73,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
</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 class="terminal-cursor"></div>
</div> </div>
</div> </div>
@ -107,34 +93,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
position: relative; 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 */ /* Header */
.terminal-header { .terminal-header {
display: flex; display: flex;
@ -261,22 +219,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
color: #ef4444; 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 */ /* Blinking cursor */
.terminal-cursor { .terminal-cursor {
position: absolute; position: absolute;
@ -290,10 +232,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
opacity: 0; opacity: 0;
} }
.terminal-interactive:has(.terminal-input:focus) ~ .terminal-cursor {
opacity: 1;
}
/* Typing effect */ /* Typing effect */
.terminal-typing { .terminal-typing {
position: relative; position: relative;
@ -335,13 +273,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
.terminal-box:hover .terminal-dot-green { .terminal-box:hover .terminal-dot-green {
background: #34d399; background: #34d399;
} }
@media (max-width: 768px) {
.terminal-box {
height: 300px;
font-size: 0.8rem;
}
}
</style> </style>
<script> <script>
@ -383,13 +314,15 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
const cursor = typingElement.closest('.terminal-box').querySelector('.terminal-cursor'); const cursor = typingElement.closest('.terminal-box').querySelector('.terminal-cursor');
if (cursor) { if (cursor) {
const rect = typingElement.getBoundingClientRect(); const rect = typingElement.getBoundingClientRect();
const parentRect = typingElement.closest('.terminal-content').getBoundingClientRect(); if (terminalContent) {
const parentRect = terminalContent.getBoundingClientRect();
// Position cursor after the last character
cursor.style.opacity = '1'; // Position cursor after the last character
cursor.style.left = `${rect.left - parentRect.left + typingElement.offsetWidth}px`; cursor.style.opacity = '1';
cursor.style.top = `${rect.top - parentRect.top}px`; cursor.style.left = `${rect.left - parentRect.left + typingElement.offsetWidth}px`;
cursor.style.height = `${rect.height}px`; cursor.style.top = `${rect.top - parentRect.top}px`;
cursor.style.height = `${rect.height}px`;
}
} }
} }
} }
@ -399,210 +332,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
}, 1000 * elementIndex); // Sequential delay for multiple typing elements }, 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 // Button interactions
const minButtons = document.querySelectorAll('.terminal-button-minimize'); const minButtons = document.querySelectorAll('.terminal-button-minimize');
const maxButtons = document.querySelectorAll('.terminal-button-maximize'); const maxButtons = document.querySelectorAll('.terminal-button-maximize');
@ -610,17 +339,23 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
minButtons.forEach(button => { minButtons.forEach(button => {
button.addEventListener('click', () => { button.addEventListener('click', () => {
const terminalBox = button.closest('.terminal-box'); const terminalBox = button.closest('.terminal-box');
terminalBox.classList.toggle('minimized'); if (terminalBox) {
terminalBox.classList.toggle('minimized');
if (terminalBox.classList.contains('minimized')) {
const content = terminalBox.querySelector('.terminal-content'); if (terminalBox.classList.contains('minimized')) {
terminalBox.dataset.prevHeight = terminalBox.style.height; const content = terminalBox.querySelector('.terminal-content');
terminalBox.style.height = '40px'; if (content) {
content.style.display = 'none'; terminalBox.dataset.prevHeight = terminalBox.style.height;
} else { terminalBox.style.height = '40px';
const content = terminalBox.querySelector('.terminal-content'); content.style.display = 'none';
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px'; }
content.style.display = 'block'; } else {
const content = terminalBox.querySelector('.terminal-content');
if (content) {
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px';
content.style.display = 'block';
}
}
} }
}); });
}); });
@ -628,30 +363,31 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
maxButtons.forEach(button => { maxButtons.forEach(button => {
button.addEventListener('click', () => { button.addEventListener('click', () => {
const terminalBox = button.closest('.terminal-box'); const terminalBox = button.closest('.terminal-box');
terminalBox.classList.toggle('maximized'); if (terminalBox) {
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'; if (terminalBox.classList.contains('maximized')) {
terminalBox.style.top = '0'; const content = terminalBox.querySelector('.terminal-content');
terminalBox.style.left = '0'; terminalBox.dataset.prevHeight = terminalBox.style.height;
terminalBox.style.width = '100%'; terminalBox.dataset.prevWidth = terminalBox.style.width;
terminalBox.style.height = '100%'; terminalBox.dataset.prevPosition = terminalBox.style.position;
terminalBox.style.zIndex = '9999';
terminalBox.style.borderRadius = '0'; terminalBox.style.position = 'fixed';
} else { terminalBox.style.top = '0';
const content = terminalBox.querySelector('.terminal-content'); terminalBox.style.left = '0';
terminalBox.style.position = terminalBox.dataset.prevPosition || 'relative'; terminalBox.style.width = '100%';
terminalBox.style.width = terminalBox.dataset.prevWidth || '100%'; terminalBox.style.height = '100%';
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px'; terminalBox.style.zIndex = '9999';
terminalBox.style.zIndex = 'auto'; terminalBox.style.borderRadius = '0';
terminalBox.style.borderRadius = '10px'; } else {
terminalBox.style.top = 'auto'; terminalBox.style.position = terminalBox.dataset.prevPosition || 'relative';
terminalBox.style.left = 'auto'; 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';
}
} }
}); });
}); });

414
src/config/terminal.js Normal file
View File

@ -0,0 +1,414 @@
/**
* Terminal Configuration
* Central configuration for the Terminal component across the site
*/
// Default terminal prompt settings
export const TERMINAL_DEFAULTS = {
promptPrefix: "[laforceit@argobox]",
title: "argobox:~/blog",
theme: "dark", // Default theme (dark or light)
height: "auto",
showTitleBar: true,
showPrompt: true
};
// Commonly used commands
export const COMMON_COMMANDS = [
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "ls -la ./infrastructure",
output: [
"total 20",
"drwxr-xr-x 5 laforceit users 4096 Apr 23 09:15 <span class='highlight'>kubernetes/</span>",
"drwxr-xr-x 3 laforceit users 4096 Apr 20 17:22 <span class='highlight'>docker/</span>",
"drwxr-xr-x 2 laforceit users 4096 Apr 19 14:30 <span class='highlight'>networking/</span>",
"drwxr-xr-x 4 laforceit users 4096 Apr 22 21:10 <span class='highlight'>monitoring/</span>",
"drwxr-xr-x 3 laforceit users 4096 Apr 21 16:45 <span class='highlight'>storage/</span>",
]
},
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "grep -r \"kubernetes\" --include=\"*.md\" ./posts | wc -l",
output: ["7 matches found"]
},
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "kubectl get nodes",
output: [
"NAME STATUS ROLES AGE VERSION",
"argobox-cp1 Ready control-plane,master 92d v1.27.3",
"argobox-cp2 Ready control-plane,master 92d v1.27.3",
"argobox-cp3 Ready control-plane,master 92d v1.27.3",
"argobox-node1 Ready worker 92d v1.27.3",
"argobox-node2 Ready worker 92d v1.27.3"
]
}
];
// Advanced blog search command sequence
export const BLOG_SEARCH_SEQUENCE = [
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "cd ./posts && grep -r \"homelab\" --include=\"*.md\" | sort | head -5",
output: [
"<span class='term-green'>homelab-essentials.md</span>:<span class='term-blue'>title:</span> \"Essential Tools for Your Home Lab Setup\"",
"<span class='term-green'>homelab-essentials.md</span>:<span class='term-blue'>description:</span> \"A curated list of must-have tools for building your home lab infrastructure\"",
"<span class='term-green'>kubernetes-at-home.md</span>:<span class='term-blue'>title:</span> \"Running Kubernetes in Your Homelab\"",
"<span class='term-green'>proxmox-cluster.md</span>:<span class='term-blue'>description:</span> \"Building a resilient homelab foundation with Proxmox VE cluster\"",
"<span class='term-green'>storage-solutions.md</span>:<span class='term-blue'>body:</span> \"...affordable homelab storage solutions for a growing collection of VMs and containers...\""
]
},
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "find ./posts -type f -name \"*.md\" | xargs wc -l | sort -nr | head -3",
output: [
"2567 total",
" 842 ./posts/kubernetes-the-hard-way.md",
" 756 ./posts/home-automation-guide.md",
" 523 ./posts/proxmox-cluster.md"
]
}
];
// System monitoring sequence
export const SYSTEM_MONITOR_SEQUENCE = [
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "htop",
output: [
"<span class='term-purple'>Tasks:</span> <span class='term-cyan'>143</span> total, <span class='term-green'>4</span> running, <span class='term-yellow'>139</span> sleeping, <span class='term-red'>0</span> stopped, <span class='term-red'>0</span> zombie",
"<span class='term-purple'>%Cpu(s):</span> <span class='term-green'>12.5</span> us, <span class='term-blue'>4.2</span> sy, <span class='term-cyan'>0.0</span> ni, <span class='term-green'>82.3</span> id, <span class='term-yellow'>0.7</span> wa, <span class='term-red'>0.0</span> hi, <span class='term-red'>0.3</span> si, <span class='term-cyan'>0.0</span> st",
"<span class='term-purple'>MiB Mem:</span> <span class='term-cyan'>32102.3</span> total, <span class='term-green'>12023.4</span> free, <span class='term-yellow'>10654.8</span> used, <span class='term-blue'>9424.1</span> buff/cache",
"<span class='term-purple'>MiB Swap:</span> <span class='term-cyan'>16384.0</span> total, <span class='term-green'>16384.0</span> free, <span class='term-yellow'>0.0</span> used. <span class='term-green'>20223.3</span> avail Mem",
"",
" <span class='term-cyan'>PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND</span>",
"<span class='term-yellow'> 23741 laforcei 20 0 4926.0m 257.9m 142.1m S 25.0 0.8 42:36.76 node</span>",
" 22184 root 20 0 743.9m 27.7m 17.6m S 6.2 0.1 27:57.21 dockerd",
" 15532 root 20 0 1735.9m 203.5m 122.1m S 6.2 0.6 124:29.93 k3s-server",
" 1126 prometheu 20 0 1351.5m 113.9m 41.3m S 0.0 0.4 3:12.52 prometheus"
]
},
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "df -h",
output: [
"Filesystem Size Used Avail Use% Mounted on",
"/dev/nvme0n1p2 932G 423G 462G 48% /",
"/dev/nvme1n1 1.8T 1.1T 638G 64% /data",
"tmpfs 16G 12M 16G 1% /run",
"tmpfs 32G 0 32G 0% /dev/shm"
]
},
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "docker stats --no-stream",
output: [
"CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS",
"7d9915b1f946 blog-site 0.15% 145.6MiB / 32GiB 0.44% 648kB / 4.21MB 12.3MB / 0B 24",
"c7823beac704 prometheus 2.33% 175.2MiB / 32GiB 0.53% 15.5MB / 25.4MB 29.6MB / 12.4MB 15",
"db9d8512f471 postgres 0.03% 96.45MiB / 32GiB 0.29% 85.1kB / 106kB 21.9MB / 63.5MB 11",
"f3b1c9e2a147 grafana 0.42% 78.32MiB / 32GiB 0.24% 5.42MB / 12.7MB 86.4MB / 1.21MB 13"
]
}
];
// Blog deployment sequence
export const BLOG_DEPLOYMENT_SEQUENCE = [
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "git status",
output: [
"On branch <span class='term-cyan'>main</span>",
"Your branch is up to date with 'origin/main'.",
"",
"Changes not staged for commit:",
" (use \"git add <file>...\" to update what will be committed)",
" (use \"git restore <file>...\" to discard changes in working directory)",
" <span class='term-red'>modified: src/content/posts/kubernetes-at-home.md</span>",
" <span class='term-red'>modified: src/components/Terminal.astro</span>",
"",
"Untracked files:",
" (use \"git add <file>...\" to include in what will be committed)",
" <span class='term-red'>src/content/posts/new-homelab-upgrades.md</span>",
"",
"no changes added to commit (use \"git add\" and/or \"git commit -a\")"
]
},
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "git add . && git commit -m \"feat: add new post about homelab upgrades\"",
output: [
"[main <span class='term-green'>f92d47a</span>] <span class='term-cyan'>feat: add new post about homelab upgrades</span>",
" 3 files changed, 214 insertions(+), 12 deletions(-)",
" create mode 100644 src/content/posts/new-homelab-upgrades.md"
]
},
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "npm run build && npm run deploy",
output: [
"<span class='term-green'>✓</span> Building for production...",
"<span class='term-green'>✓</span> Generating static routes",
"<span class='term-green'>✓</span> Client side rendering with hydration",
"<span class='term-green'>✓</span> Applying optimizations",
"<span class='term-green'>✓</span> Complete! 187 pages generated in 43.2 seconds",
"",
"<span class='term-blue'>Deploying to production environment...</span>",
"<span class='term-green'>✓</span> Upload complete",
"<span class='term-green'>✓</span> CDN cache invalidated",
"<span class='term-green'>✓</span> DNS configuration verified",
"<span class='term-green'>✓</span> Blog is live at https://laforceit.com!"
]
}
];
// Kubernetes operation sequence
export const K8S_OPERATION_SEQUENCE = [
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "kubectl create namespace blog-prod",
output: [
"namespace/blog-prod created"
]
},
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "kubectl apply -f kubernetes/blog-deployment.yaml",
output: [
"deployment.apps/blog-frontend created",
"service/blog-frontend created",
"configmap/blog-config created",
"secret/blog-secrets created"
]
},
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "kubectl get pods -n blog-prod",
output: [
"NAME READY STATUS RESTARTS AGE",
"blog-frontend-7d9b5c7b8d-2xprm 1/1 Running 0 35s",
"blog-frontend-7d9b5c7b8d-8bkpl 1/1 Running 0 35s",
"blog-frontend-7d9b5c7b8d-f9j7s 1/1 Running 0 35s"
]
},
{
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
command: "kubectl get ingress -n blog-prod",
output: [
"NAME CLASS HOSTS ADDRESS PORTS AGE",
"blog-ingress <none> blog.laforceit.com 192.168.1.50 80, 443 42s"
]
}
];
// Predefined terminal content blocks
export const TERMINAL_CONTENT = {
fileExplorer: `<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">ls -la</span>
total 42
drwxr-xr-x 6 laforceit users 4096 Nov 7 22:15 .
drwxr-xr-x 12 laforceit users 4096 Nov 7 20:32 ..
-rw-r--r-- 1 laforceit users 182 Nov 7 22:15 .astro
drwxr-xr-x 2 laforceit users 4096 Nov 7 21:03 components
drwxr-xr-x 3 laforceit users 4096 Nov 7 21:14 content
drwxr-xr-x 4 laforceit users 4096 Nov 7 21:42 layouts
drwxr-xr-x 5 laforceit users 4096 Nov 7 22:10 pages
-rw-r--r-- 1 laforceit users 1325 Nov 7 22:12 package.json`,
tags: `<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">cat ./content/tags.txt</span>
cloudflare
coding
containers
devops
digital-garden
docker
file-management
filebrowser
flux
git
gitea
gitops
grafana
homelab
infrastructure
k3s
knowledge-management
kubernetes
learning-in-public
monitoring
networking
observability
obsidian
prometheus
proxmox
quartz
rancher
remote-development
security
self-hosted
terraform
test
tunnels
tutorial
virtualization
vscode`,
blogDeployment: `<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">git add src/content/posts/kubernetes-monitoring.md</span>
<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">git commit -m "feat: add new article on Kubernetes monitoring"</span>
[main <span class="term-green">8fd43a9</span>] <span class="term-cyan">feat: add new article on Kubernetes monitoring</span>
1 file changed, 147 insertions(+)
create mode 100644 src/content/posts/kubernetes-monitoring.md
<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">git push origin main</span>
Enumerating objects: 8, done.
Counting objects: 100% (8/8), done.
Delta compression using up to 8 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 2.12 KiB | 2.12 MiB/s, done.
Total 5 (delta 3), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (3/3), completed with 3 local objects.
<span class="term-green"></span> Deployed to https://laforceit.com
<span class="term-green"></span> Article published successfully`,
k8sInstall: `<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">curl -sfL https://get.k3s.io | sh -</span>
[INFO] Finding release for channel stable
[INFO] Using v1.27.4+k3s1 as release
[INFO] Downloading hash https://github.com/k3s-io/k3s/releases/download/v1.27.4+k3s1/sha256sum-amd64.txt
[INFO] Downloading binary https://github.com/k3s-io/k3s/releases/download/v1.27.4+k3s1/k3s
[INFO] Verifying binary download
[INFO] Installing k3s to /usr/local/bin/k3s
[INFO] Creating /usr/local/bin/kubectl symlink to k3s
[INFO] Creating /usr/local/bin/crictl symlink to k3s
[INFO] Creating /usr/local/bin/ctr symlink to k3s
[INFO] Creating killall script /usr/local/bin/k3s-killall.sh
[INFO] Creating uninstall script /usr/local/bin/k3s-uninstall.sh
[INFO] env: Creating environment file /etc/systemd/system/k3s.service.env
[INFO] systemd: Creating service file /etc/systemd/system/k3s.service
[INFO] systemd: Enabling k3s unit
Created symlink /etc/systemd/system/multi-user.target.wants/k3s.service /etc/systemd/system/k3s.service.
[INFO] systemd: Starting k3s
<span class="term-green"></span> K3s has been installed successfully
<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">kubectl get pods -A</span>
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system helm-install-traefik-crd-k7gxl 0/1 Completed 0 2m43s
kube-system helm-install-traefik-pvvhg 0/1 Completed 1 2m43s
kube-system metrics-server-67c658dc48-mxnxp 1/1 Running 0 2m43s
kube-system local-path-provisioner-7b7dc8d6f5-q99nl 1/1 Running 0 2m43s
kube-system coredns-b96499967-nkvnz 1/1 Running 0 2m43s
kube-system svclb-traefik-bd0bfb17-ht8gq 2/2 Running 0 96s
kube-system traefik-7d586bdc47-d6lzr 1/1 Running 0 96s`,
dockerCompose: `<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">cat docker-compose.yaml</span>
version: '3.8'
services:
blog:
image: node:18-alpine
restart: unless-stopped
volumes:
- ./:/app
working_dir: /app
command: sh -c "npm install && npm run dev"
ports:
- "3000:3000"
environment:
- NODE_ENV=development
db:
image: postgres:14-alpine
restart: unless-stopped
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=secure_password
- POSTGRES_USER=bloguser
- POSTGRES_DB=blogdb
ports:
- "5432:5432"
volumes:
postgres_data:
<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">docker-compose up -d</span>
Creating network "laforceit-blog_default" with the default driver
Creating volume "laforceit-blog_postgres_data" with default driver
Pulling blog (node:18-alpine)...
Pulling db (postgres:14-alpine)...
Creating laforceit-blog_db_1 ... done
Creating laforceit-blog_blog_1 ... done`
};
// Helper function to create terminal presets
export function createTerminalPreset(type) {
switch (type) {
case 'blog-search':
return BLOG_SEARCH_SEQUENCE[Math.floor(Math.random() * BLOG_SEARCH_SEQUENCE.length)];
case 'system-monitor':
return SYSTEM_MONITOR_SEQUENCE[Math.floor(Math.random() * SYSTEM_MONITOR_SEQUENCE.length)];
case 'blog-deploy':
return BLOG_DEPLOYMENT_SEQUENCE[Math.floor(Math.random() * BLOG_DEPLOYMENT_SEQUENCE.length)];
case 'k8s-ops':
return K8S_OPERATION_SEQUENCE[Math.floor(Math.random() * K8S_OPERATION_SEQUENCE.length)];
case 'k8s':
return {
title: "argobox:~/kubernetes",
command: "kubectl get pods -A",
output: `NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-66bff467f8-8p7z2 1/1 Running 0 15d
kube-system coredns-66bff467f8-v68vr 1/1 Running 0 15d
kube-system etcd-control-plane 1/1 Running 0 15d
kube-system kube-apiserver-control-plane 1/1 Running 0 15d
kube-system kube-controller-manager-control-plane 1/1 Running 0 15d
kube-system kube-proxy-c84qf 1/1 Running 0 15d
kube-system kube-scheduler-control-plane 1/1 Running 0 15d`
};
case 'docker':
return {
title: "argobox:~/docker",
command: "docker ps",
output: `CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
d834f0efcf2f nginx:latest "/docker-entrypoint.…" Up 2 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp web
0b292940b4c0 postgres:13 "docker-entrypoint.s…" Up 2 days 0.0.0.0:5432->5432/tcp db
a834fa3ede06 redis:6 "docker-entrypoint.s…" Up 2 days 0.0.0.0:6379->6379/tcp cache`
};
case 'search':
return {
title: "argobox:~/blog",
command: "grep -r \"kubernetes\" --include=\"*.md\" ./posts | wc -l",
output: "7 matches found"
};
case 'random-cool':
// Pick a random sequence for a cool effect
const sequences = [
TERMINAL_CONTENT.k8sInstall,
TERMINAL_CONTENT.blogDeployment,
TERMINAL_CONTENT.dockerCompose,
...BLOG_SEARCH_SEQUENCE.map(item => `<div class="term-blue">${item.prompt}</div><span>$</span> <span class="term-bold">${item.command}</span>\n${item.output.join('\n')}`),
...SYSTEM_MONITOR_SEQUENCE.map(item => `<div class="term-blue">${item.prompt}</div><span>$</span> <span class="term-bold">${item.command}</span>\n${item.output.join('\n')}`),
...BLOG_DEPLOYMENT_SEQUENCE.map(item => `<div class="term-blue">${item.prompt}</div><span>$</span> <span class="term-bold">${item.command}</span>\n${item.output.join('\n')}`),
...K8S_OPERATION_SEQUENCE.map(item => `<div class="term-blue">${item.prompt}</div><span>$</span> <span class="term-bold">${item.command}</span>\n${item.output.join('\n')}`)
];
return {
title: "argobox:~/cool-stuff",
content: sequences[Math.floor(Math.random() * sequences.length)]
};
default:
return {
title: TERMINAL_DEFAULTS.title,
command: "echo 'Hello from LaForceIT Terminal'",
output: "Hello from LaForceIT Terminal"
};
}
}

View File

@ -57,7 +57,7 @@ const {
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<!-- Theme CSS --> <!-- Theme CSS -->
<link rel="stylesheet" href="/styles/theme.css" /> <link rel="stylesheet" href="/src/styles/theme.css" />
<!-- Cytoscape Library for Knowledge Graph --> <!-- Cytoscape Library for Knowledge Graph -->
<script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script> <script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script>
@ -360,5 +360,90 @@ const {
} }
}); });
</script> </script>
<!-- Add copy to clipboard functionality for code blocks -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Find all code blocks
const codeBlocks = document.querySelectorAll('pre code');
// Add copy button to each
codeBlocks.forEach((codeBlock, index) => {
// Create container for copy button (to enable positioning)
const container = document.createElement('div');
container.className = 'code-block-container';
container.style.position = 'relative';
// Create copy button
const copyButton = document.createElement('button');
copyButton.className = 'copy-code-button';
copyButton.setAttribute('aria-label', 'Copy code to clipboard');
copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="copy-icon">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="check-icon" style="display: none;">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
`;
// Style the button
copyButton.style.position = 'absolute';
copyButton.style.top = '0.5rem';
copyButton.style.right = '0.5rem';
copyButton.style.padding = '0.25rem';
copyButton.style.background = 'rgba(45, 55, 72, 0.5)';
copyButton.style.border = '1px solid rgba(255, 255, 255, 0.2)';
copyButton.style.borderRadius = '0.25rem';
copyButton.style.cursor = 'pointer';
copyButton.style.zIndex = '10';
copyButton.style.opacity = '0';
copyButton.style.transition = 'opacity 0.2s';
// Add click handler
copyButton.addEventListener('click', () => {
// Get code text
const code = codeBlock.textContent;
// Copy to clipboard
navigator.clipboard.writeText(code).then(() => {
// Show success UI
copyButton.querySelector('.copy-icon').style.display = 'none';
copyButton.querySelector('.check-icon').style.display = 'block';
// Reset after 2 seconds
setTimeout(() => {
copyButton.querySelector('.copy-icon').style.display = 'block';
copyButton.querySelector('.check-icon').style.display = 'none';
}, 2000);
});
});
// Clone the code block
const preElement = codeBlock.parentElement;
const wrapper = preElement.parentElement;
// Create the container structure
container.appendChild(preElement.cloneNode(true));
container.appendChild(copyButton);
// Replace the original pre with our container
wrapper.replaceChild(container, preElement);
// Update the reference to the new code block
const newCodeBlock = container.querySelector('code');
// Add hover behavior
container.addEventListener('mouseenter', () => {
copyButton.style.opacity = '1';
});
container.addEventListener('mouseleave', () => {
copyButton.style.opacity = '0';
});
});
});
</script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,8 @@ import BaseLayout from './BaseLayout.astro';
import Header from '../components/Header.astro'; import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro'; import Footer from '../components/Footer.astro';
import Newsletter from '../components/Newsletter.astro'; import Newsletter from '../components/Newsletter.astro';
import MiniKnowledgeGraph from '../components/MiniKnowledgeGraph.astro';
import { getCollection } from 'astro:content';
interface Props { interface Props {
frontmatter: { frontmatter: {
@ -11,20 +13,21 @@ interface Props {
pubDate: Date; pubDate: Date;
updatedDate?: Date; updatedDate?: Date;
heroImage?: string; heroImage?: string;
category?: string; // Keep category for potential filtering, but don't display in header category?: string;
tags?: string[]; tags?: string[];
readTime?: string; readTime?: string;
draft?: boolean; draft?: boolean;
author?: string; // Keep author field if needed elsewhere author?: string;
// Add other potential frontmatter fields as optional
github?: string; github?: string;
live?: string; live?: string;
technologies?: string[]; technologies?: string[];
related_posts?: string[]; // Explicit related posts by slug
} }
} }
const { frontmatter } = Astro.props; const { frontmatter } = Astro.props;
// Format dates
const formattedPubDate = frontmatter.pubDate ? new Date(frontmatter.pubDate).toLocaleDateString('en-us', { const formattedPubDate = frontmatter.pubDate ? new Date(frontmatter.pubDate).toLocaleDateString('en-us', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
@ -39,6 +42,58 @@ const formattedUpdatedDate = frontmatter.updatedDate ? new Date(frontmatter.upda
// Default image if heroImage is missing // Default image if heroImage is missing
const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'; const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg';
// Get related posts for MiniKnowledgeGraph
// First get all posts
const allPosts = await getCollection('posts').catch(() => []);
// Find the current post in collection
const currentPost = allPosts.find(post =>
post.data.title === frontmatter.title ||
post.slug === frontmatter.title.toLowerCase().replace(/\s+/g, '-')
);
// Get related posts - first from explicit frontmatter relation, then by tag similarity
let relatedPosts = [];
// If related_posts is specified in frontmatter, use those first
if (frontmatter.related_posts && frontmatter.related_posts.length > 0) {
const explicitRelatedPosts = allPosts.filter(post =>
frontmatter.related_posts.includes(post.slug)
);
relatedPosts = [...explicitRelatedPosts];
}
// If we need more related posts, find them by tags
if (relatedPosts.length < 3 && frontmatter.tags && frontmatter.tags.length > 0) {
// Calculate tag similarity score for each post
const tagSimilarityPosts = allPosts
.filter(post =>
// Filter out current post and already included related posts
post.data.title !== frontmatter.title &&
!relatedPosts.some(rp => rp.slug === post.slug)
)
.map(post => {
// Count matching tags
const postTags = post.data.tags || [];
const matchingTags = postTags.filter(tag =>
frontmatter.tags.includes(tag)
);
return {
post,
score: matchingTags.length
};
})
.filter(item => item.score > 0) // Only consider posts with at least one matching tag
.sort((a, b) => b.score - a.score) // Sort by score descending
.map(item => item.post); // Extract just the post
// Add tag-related posts to fill up to 3 related posts
relatedPosts = [...relatedPosts, ...tagSimilarityPosts.slice(0, 3 - relatedPosts.length)];
}
// Limit to 3 related posts
relatedPosts = relatedPosts.slice(0, 3);
--- ---
<BaseLayout title={frontmatter.title} description={frontmatter.description} image={displayImage}> <BaseLayout title={frontmatter.title} description={frontmatter.description} image={displayImage}>
@ -63,7 +118,6 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
<span class="blog-post-updated">(Updated {formattedUpdatedDate})</span> <span class="blog-post-updated">(Updated {formattedUpdatedDate})</span>
)} )}
{frontmatter.readTime && <span class="blog-post-read-time">{frontmatter.readTime}</span>} {frontmatter.readTime && <span class="blog-post-read-time">{frontmatter.readTime}</span>}
{/* Category removed from display here */}
</div> </div>
{/* Tags */} {/* Tags */}
@ -76,6 +130,18 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
)} )}
</header> </header>
{/* Content Connections Graph - only show if we have the current post and related content */}
{currentPost && (frontmatter.tags?.length > 0 || relatedPosts.length > 0) && (
<div class="content-connections">
<h3 class="section-subtitle">Content Connections</h3>
<MiniKnowledgeGraph
currentPost={currentPost}
relatedPosts={relatedPosts}
height="250px"
/>
</div>
)}
{/* Display Hero Image */} {/* Display Hero Image */}
{displayImage && ( {displayImage && (
<div class="blog-post-hero"> <div class="blog-post-hero">
@ -88,9 +154,6 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
<slot /> {/* Renders the actual markdown content */} <slot /> {/* Renders the actual markdown content */}
</div> </div>
{/* Future Feature Placeholders remain commented out */}
{/* ... */}
</article> </article>
{/* Sidebar */} {/* Sidebar */}
@ -107,7 +170,6 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
<p class="author-bio"> <p class="author-bio">
Exploring enterprise-grade infrastructure, automation, Kubernetes, and zero-trust networking in the home lab and beyond. Exploring enterprise-grade infrastructure, automation, Kubernetes, and zero-trust networking in the home lab and beyond.
</p> </p>
{/* Social links removed */}
</div> </div>
{/* Table of Contents Card */} {/* Table of Contents Card */}
@ -118,8 +180,26 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
</nav> </nav>
</div> </div>
{/* Future Feature Placeholders remain commented out */} {/* Related Posts */}
{/* ... */} {relatedPosts.length > 0 && (
<div class="sidebar-card related-posts-card">
<h3>Related Articles</h3>
<div class="related-posts">
{relatedPosts.map(post => (
<a href={`/posts/${post.slug}/`} class="related-post-link">
<h4>{post.data.title}</h4>
{post.data.tags && post.data.tags.length > 0 && (
<div class="related-post-tags">
{post.data.tags.slice(0, 2).map(tag => (
<span class="related-tag">{tag}</span>
))}
</div>
)}
</a>
))}
</div>
</div>
)}
</aside> </aside>
</div> </div>
@ -168,7 +248,6 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
} }
</script> </script>
{/* Styles Updated */}
<style> <style>
.draft-badge { .draft-badge {
display: inline-block; display: inline-block;
@ -183,12 +262,11 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
} }
.blog-post-container { .blog-post-container {
display: grid; display: grid;
/* Adjusted grid for wider TOC/Sidebar */ grid-template-columns: minmax(0, 1fr) 300px;
grid-template-columns: 7fr 2fr; gap: 2rem;
gap: 3rem; /* Wider gap */ max-width: 1200px;
max-width: 1400px; /* Wider max width */ margin: 0 auto;
margin: 2rem auto; padding: 2rem 1rem;
padding: 0 1.5rem;
} }
.blog-post-header { .blog-post-header {
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
@ -216,7 +294,6 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
font-size: 0.85rem; font-size: 0.85rem;
color: var(--text-secondary); color: var(--text-secondary);
} }
/* Removed .blog-post-category style */
.blog-post-tags { .blog-post-tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -250,6 +327,26 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
height: auto; height: auto;
display: block; display: block;
} }
/* Content Connections Graph */
.content-connections {
margin: 1.5rem 0 2rem;
border-radius: 10px;
border: 1px solid var(--card-border, #334155);
background: rgba(15, 23, 42, 0.2);
overflow: hidden;
}
.section-subtitle {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-primary, #e2e8f0);
padding: 1rem 1.5rem;
margin: 0;
background: rgba(15, 23, 42, 0.5);
border-bottom: 1px solid var(--card-border, #334155);
}
.blog-post-content { .blog-post-content {
/* Styles inherited from prose */ /* Styles inherited from prose */
} }
@ -334,6 +431,54 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
font-size: 0.85rem; font-size: 0.85rem;
opacity: 0.9; opacity: 0.9;
} }
/* Related Posts */
.related-posts-card h3 {
margin-bottom: 1rem;
color: var(--text-primary);
}
.related-posts {
display: flex;
flex-direction: column;
gap: 1rem;
}
.related-post-link {
display: block;
padding: 0.75rem;
border-radius: 8px;
border: 1px solid var(--border-primary);
background: rgba(255, 255, 255, 0.03);
transition: all 0.3s ease;
text-decoration: none;
}
.related-post-link:hover {
background: rgba(6, 182, 212, 0.05);
border-color: var(--accent-primary);
transform: translateY(-2px);
}
.related-post-link h4 {
margin: 0 0 0.5rem;
font-size: 0.95rem;
color: var(--text-primary);
line-height: 1.3;
}
.related-post-tags {
display: flex;
gap: 0.5rem;
}
.related-tag {
font-size: 0.7rem;
padding: 0.1rem 0.4rem;
border-radius: 3px;
background: rgba(16, 185, 129, 0.1);
color: var(--text-secondary);
}
@media (max-width: 1024px) { @media (max-width: 1024px) {
.blog-post-container { .blog-post-container {

View File

@ -215,52 +215,55 @@ const commands = [
}); });
// Function to create HTML for a single post card // Function to create HTML for a single post card
function createPostCardHTML(post) { // Update the post card HTML creation function in the blog/index.astro file
// Make sure tags is an array before stringifying // Find the function that creates post cards (might be called createPostCardHTML)
const tagsString = JSON.stringify(post.tags || []);
function createPostCardHTML(post) {
// Create tag pills HTML // Make sure tags is an array before stringifying
const tagPills = post.tags.map(tag => const tagsString = JSON.stringify(post.tags || []);
`<span class="post-tag" data-tag="${tag}">${tag}</span>`
).join(''); // Create tag pills HTML
const tagPills = post.tags.map(tag =>
return ` `<span class="post-tag" data-tag="${tag}">${tag}</span>`
<article class="post-card" data-tags='${tagsString}' data-slug="${post.slug}"> ).join('');
<div class="post-card-inner">
<a href="/posts/${post.slug}/" class="post-image-link"> return `
<div class="post-image-container"> <article class="post-card" data-tags='${tagsString}' data-slug="${post.slug}">
<img <div class="post-card-inner">
width="720" <a href="/posts/${post.slug}/" class="post-image-link">
height="360" <div class="post-image-container">
src="${post.heroImage}" <img
alt="" width="720"
class="post-image" height="360"
loading="lazy" src="${post.heroImage}"
/> alt=""
<div class="post-category-badge">${post.category}</div> class="post-image"
</div> loading="lazy"
</a> />
<div class="post-content"> <div class="post-category-badge">${post.category}</div>
<div class="post-meta">
<time datetime="${post.pubDateISO}">${post.pubDate}</time>
<span class="post-read-time">${post.readTime}</span>
</div>
<h3 class="post-title">
<a href="/posts/${post.slug}/">${post.title}</a>
${post.isDraft ? '<span class="draft-badge">Draft</span>' : ''}
</h3>
<p class="post-excerpt">${post.description}</p>
<div class="post-footer">
<div class="post-tags">
${tagPills}
</div>
<a href="/posts/${post.slug}/" class="read-more">Read More</a>
</div>
</div>
</div> </div>
</article> </a>
`; <div class="post-content">
} <div class="post-meta">
<time datetime="${post.pubDateISO}">${post.pubDate}</time>
<span class="post-read-time">${post.readTime}</span>
</div>
<h3 class="post-title">
<a href="/posts/${post.slug}/">${post.title}</a>
${post.isDraft ? '<span class="draft-badge">Draft</span>' : ''}
</h3>
<p class="post-excerpt">${post.description}</p>
<div class="post-footer">
<div class="post-tags">
${tagPills}
</div>
<a href="/posts/${post.slug}/" class="read-more">Read More</a>
</div>
</div>
</div>
</article>
`;
}
// Function to filter and update the grid // Function to filter and update the grid
function updateGrid() { function updateGrid() {
@ -949,4 +952,34 @@ const commands = [
display: none; display: none;
} }
} }
/* Add CSS to make the image link more obvious on hover */
.post-image-link {
display: block;
position: relative;
overflow: hidden;
border-radius: 8px 8px 0 0; /* Match card radius */
}
.post-image-link:hover .post-image {
transform: scale(1.05);
transition: transform 0.5s ease;
}
.post-image-link::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(6, 182, 212, 0.1); /* Use accent color with alpha */
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none; /* Allow clicks through */
}
.post-image-link:hover::after {
opacity: 1;
}
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@ -1,265 +1,483 @@
--- ---
// src/pages/tag/[tag].astro // src/pages/tag/[tag].astro
// Dynamic route for tag pages // Dynamic route for tag pages with enhanced visualization
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
import KnowledgeGraph from '../../components/KnowledgeGraph.astro';
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
export async function getStaticPaths() { export async function getStaticPaths() {
const allPosts = await getCollection('blog'); try {
const uniqueTags = [...new Set(allPosts.map((post) => post.data.tags).flat())]; // Get all posts
const allPosts = await getCollection('posts', ({ data }) => {
return uniqueTags.map((tag) => { // Exclude draft posts in production
const filteredPosts = allPosts.filter((post) => post.data.tags.includes(tag)); return import.meta.env.PROD ? !data.draft : true;
return { });
params: { tag },
props: { posts: filteredPosts }, // Extract all unique tags
}; const allTags = [...new Set(allPosts.flatMap(post => post.data.tags || []))];
});
// Create a path for each tag
return allTags.map((tag) => {
// Filter posts to only those with this tag
const filteredPosts = allPosts.filter((post) =>
(post.data.tags || []).includes(tag)
);
return {
params: { tag },
props: {
posts: filteredPosts,
tag,
allPosts // Pass all posts for knowledge graph
},
};
});
} catch (error) {
console.error("Error in getStaticPaths:", error);
// Return empty array as fallback
return [];
}
} }
const { tag } = Astro.params; const { tag } = Astro.params;
const { posts } = Astro.props; const { posts, allPosts } = Astro.props;
// Format date // Format dates
const formatDate = (dateStr) => { const formatDate = (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr); const date = new Date(dateStr);
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}; };
// Sort posts by date (newest first) // Sort posts by date (newest first)
const sortedPosts = posts.sort((a, b) => { const sortedPosts = [...posts].sort((a, b) => {
const dateA = new Date(a.data.pubDate); const dateA = new Date(a.data.pubDate || 0);
const dateB = new Date(b.data.pubDate); const dateB = new Date(b.data.pubDate || 0);
return dateB.getTime() - dateA.getTime(); return dateB.getTime() - dateA.getTime();
}); });
// Prepare Knowledge Graph data
const graphData = {
nodes: [
// Add the current tag as a central node
{
id: `tag-${tag}`,
label: tag,
type: 'tag',
url: `/tag/${tag}`
},
// Add posts with this tag
...sortedPosts.map(post => ({
id: post.slug,
label: post.data.title,
type: 'post',
category: post.data.category || 'Uncategorized',
tags: post.data.tags || [],
url: `/posts/${post.slug}/`
})),
// Add related tags (tags that appear alongside this tag in posts)
...posts.flatMap(post =>
(post.data.tags || [])
.filter(t => t !== tag) // Don't include current tag
.map(relatedTag => ({
id: `tag-${relatedTag}`,
label: relatedTag,
type: 'tag',
url: `/tag/${relatedTag}`
}))
).filter((v, i, a) => a.findIndex(t => t.id === v.id) === i) // Deduplicate
],
edges: [
// Connect posts to the current tag
...sortedPosts.map(post => ({
source: post.slug,
target: `tag-${tag}`,
type: 'post-tag',
strength: 2
})),
// Connect related tags to their posts
...posts.flatMap(post =>
(post.data.tags || [])
.filter(t => t !== tag) // Skip current tag
.map(relatedTag => ({
source: post.slug,
target: `tag-${relatedTag}`,
type: 'post-tag',
strength: 1
}))
)
]
};
--- ---
<BaseLayout title={`Posts tagged with "${tag}" | LaForce IT Blog`} description={`Articles and guides related to ${tag}`}> <BaseLayout title={`Posts tagged with "${tag}" | LaForce IT Blog`} description={`Articles and guides related to ${tag}`}>
<div class="container tag-page"> <Header slot="header" />
<header class="tag-hero">
<h1>Posts tagged with <span class="tag-highlight">{tag}</span></h1> <main class="tag-page-container">
<p>Explore {sortedPosts.length} {sortedPosts.length === 1 ? 'article' : 'articles'} related to {tag}</p> <section class="tag-hero">
</header> <div class="container">
<h1>Posts tagged with <span class="tag-highlight">{tag}</span></h1>
<div class="posts-grid"> <p class="tag-description">
{sortedPosts.map((post) => ( Explore {sortedPosts.length} {sortedPosts.length === 1 ? 'article' : 'articles'} related to {tag}
<article class="post-card"> </p>
<!-- Simplified image rendering that works reliably --> </div>
<img </section>
width={720}
height={360} <section class="tag-content container">
src={post.data.heroImage || "/images/placeholders/default.jpg"} <div class="knowledge-graph-section">
alt="" <h2>Content Connections</h2>
class="post-image" <p class="section-description">
/> Explore how {tag} relates to other content and tags
<div class="post-content"> </p>
<time datetime={post.data.pubDate}>{formatDate(post.data.pubDate)}</time>
<h2 class="post-title"> <div class="graph-wrapper">
<a href={`/posts/${post.slug}/`}>{post.data.title}</a> <KnowledgeGraph graphData={graphData} height="400px" initialFilter="all" />
</h2> </div>
<p class="post-excerpt">{post.data.description}</p> </div>
<div class="post-meta">
<span class="reading-time">{post.data.minutesRead || '5 min'} read</span> <div class="posts-section">
<ul class="post-tags"> <h2>Articles</h2>
{post.data.tags.map((tagName) => ( <div class="posts-grid">
<li> {sortedPosts.length > 0 ? sortedPosts.map((post) => (
<a href={`/tag/${tagName}`} class={tagName === tag ? 'current-tag' : ''}> <article class="post-card">
{tagName} <a href={`/posts/${post.slug}/`} class="post-card-link">
</a> {post.data.heroImage && (
</li> <div class="post-image-wrapper">
))} <img
</ul> src={post.data.heroImage}
alt=""
class="post-image"
width="400"
height="225"
loading="lazy"
/>
</div>
)}
<div class="post-content">
<div class="post-meta">
<time datetime={post.data.pubDate?.toISOString()}>
{formatDate(post.data.pubDate)}
</time>
{post.data.readTime && (
<span class="read-time">{post.data.readTime}</span>
)}
</div>
<h3 class="post-title">{post.data.title}</h3>
{post.data.description && (
<p class="post-description">{post.data.description}</p>
)}
{post.data.tags && post.data.tags.length > 0 && (
<div class="post-tags">
{post.data.tags.map(postTag => (
<span class={`post-tag ${postTag === tag ? 'current-tag' : ''}`}>
#{postTag}
</span>
))}
</div>
)}
</div>
</a>
</article>
)) : (
<div class="no-posts">
<p>No posts found with the tag "{tag}".</p>
<a href="/blog" class="back-to-blog">Browse all posts</a>
</div> </div>
</div> )}
</article> </div>
))} </div>
</div>
<div class="tag-navigation">
<a href="/tags" class="all-tags-link"> <a href="/blog" class="back-button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"></line> <line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline> <polyline points="12 19 5 12 12 5"></polyline>
</svg> </svg>
View all tags Back to All Posts
</a> </a>
</div> </div>
</section>
</main>
<Footer slot="footer" />
</BaseLayout> </BaseLayout>
<style> <style>
.tag-page { .tag-page-container {
padding-top: 2rem;
padding-bottom: 4rem; padding-bottom: 4rem;
} }
.tag-hero { .container {
text-align: center; max-width: 1280px;
margin-bottom: 3rem; margin: 0 auto;
animation: fadeIn 0.5s ease-out; padding: 0 var(--container-padding, 1.5rem);
} }
.tag-hero {
padding: 5rem 0 3rem;
background: linear-gradient(to bottom, var(--bg-secondary), var(--bg-primary));
text-align: center;
position: relative;
overflow: hidden;
}
.tag-hero::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at 30% 50%, rgba(6, 182, 212, 0.05) 0%, transparent 50%);
pointer-events: none;
}
.tag-hero h1 {
font-size: clamp(1.8rem, 4vw, 3rem);
margin-bottom: 1rem;
animation: fadeInUp 0.6s ease-out;
}
.tag-highlight {
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 700;
}
.tag-description {
color: var(--text-secondary);
font-size: clamp(1rem, 2vw, 1.2rem);
max-width: 600px;
margin: 0 auto;
animation: fadeInUp 0.8s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.tag-content {
margin-top: 3rem;
}
.knowledge-graph-section,
.posts-section {
margin-bottom: 4rem;
}
.knowledge-graph-section h2,
.posts-section h2 {
font-size: 1.8rem;
margin-bottom: 0.5rem;
text-align: center;
}
.section-description {
text-align: center;
color: var(--text-secondary);
margin-bottom: 2rem;
}
.graph-wrapper {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 2rem;
}
.post-card {
height: 100%;
animation: fadeIn 0.6s ease-out forwards;
opacity: 0;
}
.post-card:nth-child(1) { animation-delay: 0.1s; }
.post-card:nth-child(2) { animation-delay: 0.2s; }
.post-card:nth-child(3) { animation-delay: 0.3s; }
.post-card:nth-child(4) { animation-delay: 0.4s; }
.post-card:nth-child(5) { animation-delay: 0.5s; }
.post-card:nth-child(6) { animation-delay: 0.6s; }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); } from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
.tag-hero h1 { .post-card-link {
font-size: var(--font-size-3xl);
margin-bottom: 0.5rem;
line-height: 1.2;
}
.tag-highlight {
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
color: transparent;
font-weight: 700;
}
.tag-hero p {
color: var(--text-secondary);
font-size: var(--font-size-lg);
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
margin-bottom: 3rem;
}
.post-card {
background: var(--card-bg);
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border-primary);
transition: transform 0.3s ease, box-shadow 0.3s ease;
animation: fadeIn 0.5s ease-out forwards;
animation-delay: calc(var(--animation-order, 0) * 0.1s);
opacity: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 12px;
overflow: hidden;
text-decoration: none;
transition: all 0.3s ease;
} }
.post-card:hover { .post-card-link:hover {
transform: translateY(-5px); transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
border-color: var(--accent-primary); border-color: var(--accent-primary);
} }
.post-image-wrapper {
height: 200px;
overflow: hidden;
border-bottom: 1px solid var(--card-border);
}
.post-image { .post-image {
width: 100%; width: 100%;
height: 200px; height: 100%;
object-fit: cover; object-fit: cover;
border-bottom: 1px solid var(--border-primary); transition: transform 0.5s ease;
}
.post-card-link:hover .post-image {
transform: scale(1.05);
} }
.post-content { .post-content {
padding: 1.5rem; padding: 1.5rem;
flex-grow: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
}
.post-content time {
color: var(--text-tertiary);
font-size: var(--font-size-sm);
font-family: var(--font-mono);
}
.post-title {
font-size: var(--font-size-xl);
margin: 0.5rem 0 1rem;
line-height: 1.3;
}
.post-title a {
color: var(--text-primary);
text-decoration: none;
transition: color 0.2s ease;
}
.post-title a:hover {
color: var(--accent-primary);
}
.post-excerpt {
color: var(--text-secondary);
font-size: var(--font-size-md);
margin-bottom: 1.5rem;
line-height: 1.6;
flex-grow: 1; flex-grow: 1;
} }
.post-meta { .post-meta {
display: flex; display: flex;
flex-direction: column; justify-content: space-between;
gap: 0.75rem; font-size: 0.85rem;
margin-top: auto; color: var(--text-tertiary);
margin-bottom: 1rem;
} }
.reading-time { .read-time {
color: var(--text-tertiary);
font-size: var(--font-size-sm);
font-family: var(--font-mono); font-family: var(--font-mono);
} }
.post-title {
font-size: 1.2rem;
color: var(--text-primary);
margin-bottom: 0.75rem;
line-height: 1.4;
}
.post-description {
color: var(--text-secondary);
font-size: 0.95rem;
margin-bottom: 1.5rem;
flex-grow: 1;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.post-tags { .post-tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5rem; gap: 0.5rem;
list-style: none; margin-top: auto;
padding: 0;
} }
.post-tags li a { .post-tag {
display: block; font-size: 0.75rem;
padding: 0.25rem 0.75rem; color: var(--text-secondary);
background: rgba(56, 189, 248, 0.1); background: rgba(226, 232, 240, 0.05);
border-radius: 20px; padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.post-tag.current-tag {
background: rgba(16, 185, 129, 0.2);
color: var(--accent-primary); color: var(--accent-primary);
font-size: var(--font-size-xs);
text-decoration: none;
transition: all 0.2s ease;
} }
.post-tags li a:hover { .no-posts {
background: rgba(56, 189, 248, 0.2); grid-column: 1 / -1;
transform: translateY(-2px); text-align: center;
padding: 3rem;
background: var(--card-bg);
border: 1px dashed var(--card-border);
border-radius: 12px;
color: var(--text-secondary);
} }
.post-tags li a.current-tag { .back-to-blog {
background: var(--accent-primary); display: inline-block;
margin-top: 1rem;
padding: 0.75rem 1.5rem;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
color: var(--bg-primary); color: var(--bg-primary);
border-radius: 6px;
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
} }
.all-tags-link { .back-to-blog:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(6, 182, 212, 0.2);
}
.tag-navigation {
display: flex;
justify-content: center;
margin-top: 2rem;
}
.back-button {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
margin: 0 auto; padding: 0.8rem 1.5rem;
padding: 0.75rem 1.5rem;
background: var(--bg-secondary); background: var(--bg-secondary);
border: 1px solid var(--border-primary); border: 1px solid var(--border-primary);
border-radius: 30px; border-radius: 30px;
color: var(--text-primary); color: var(--text-primary);
font-size: var(--font-size-md);
text-decoration: none; text-decoration: none;
transition: all 0.2s ease; transition: all 0.3s ease;
width: fit-content;
} }
.all-tags-link:hover { .back-button:hover {
background: var(--bg-tertiary); background: var(--bg-tertiary);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.tag-hero h1 { .tag-hero {
font-size: var(--font-size-2xl); padding: 4rem 0 2rem;
} }
.posts-grid { .posts-grid {

179
src/pages/test-graph.astro Normal file
View File

@ -0,0 +1,179 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import MiniKnowledgeGraph from '../components/MiniKnowledgeGraph.astro';
import { getCollection } from 'astro:content';
// Get all posts
const allPosts = await getCollection('posts').catch(error => {
console.error('Error fetching posts collection:', error);
return [];
});
// Try blog collection if posts doesn't exist
const blogPosts = allPosts.length === 0 ? await getCollection('blog').catch(() => []) : [];
const combinedPosts = [...allPosts, ...blogPosts];
// Use the first post as a test post
const testPost = combinedPosts.length > 0 ? combinedPosts[0] : {
slug: 'test-post',
data: {
title: 'Test Post',
tags: ['test', 'graph'],
category: 'Test'
}
};
// Create related posts - use the next 3 posts in the collection or create test posts
const relatedPosts = combinedPosts.length > 1
? combinedPosts.slice(1, 4)
: [
{
slug: 'related-1',
data: {
title: 'Related Post 1',
tags: ['test', 'graph'],
category: 'Test'
}
},
{
slug: 'related-2',
data: {
title: 'Related Post 2',
tags: ['test'],
category: 'Test'
}
}
];
---
<BaseLayout title="Test MiniKnowledgeGraph">
<Header slot="header" />
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-6">MiniKnowledgeGraph Test Page</h1>
<div class="bg-slate-800 rounded-lg p-6 mb-8">
<p class="mb-4">This is a test page to ensure the MiniKnowledgeGraph component is working properly.</p>
<div class="border border-slate-700 rounded-lg p-4 mb-6">
<h2 class="text-xl font-bold mb-4">Test Post Details:</h2>
<p><strong>Title:</strong> {testPost.data.title}</p>
<p><strong>Slug:</strong> {testPost.slug}</p>
<p><strong>Tags:</strong> {testPost.data.tags?.join(', ') || 'None'}</p>
<p><strong>Category:</strong> {testPost.data.category || 'None'}</p>
<p><strong>Related Posts:</strong> {relatedPosts.length}</p>
</div>
<div class="mini-knowledge-graph-area">
<h2 class="text-xl font-bold mb-4">MiniKnowledgeGraph Component:</h2>
<div class="mini-knowledge-graph-wrapper">
<MiniKnowledgeGraph
currentPost={testPost}
relatedPosts={relatedPosts}
height="300px"
title="Test Graph"
/>
</div>
<!-- Debug Information Display -->
<div class="debug-info mt-4 p-4 bg-gray-900 rounded-lg">
<h3 class="text-lg font-bold mb-2">Debug Info</h3>
<div id="debug-container">Loading debug info...</div>
</div>
</div>
</div>
</div>
<Footer slot="footer" />
</BaseLayout>
<style>
.mini-knowledge-graph-area {
margin-top: 2rem;
}
.mini-knowledge-graph-wrapper {
width: 100%;
border-radius: 10px;
overflow: hidden;
display: block !important;
position: relative;
min-height: 300px;
height: 300px;
background: var(--card-bg, #1e293b);
border: 1px solid var(--card-border, #334155);
visibility: visible !important;
}
.debug-info {
font-family: monospace;
font-size: 0.8rem;
line-height: 1.4;
}
</style>
<script>
// Debug utility for testing the knowledge graph
document.addEventListener('DOMContentLoaded', function() {
setTimeout(checkGraphStatus, 500);
// Also check after window load
window.addEventListener('load', function() {
setTimeout(checkGraphStatus, 1000);
});
});
function checkGraphStatus() {
const debugContainer = document.getElementById('debug-container');
if (!debugContainer) return;
// Get container info
const container = document.querySelector('.mini-knowledge-graph-wrapper');
const cyContainer = document.getElementById('mini-cy');
// Check for cytoscape instance
const cyInstance = window.miniCy;
let html = '<ul>';
// Container dimensions
if (container) {
html += `<li>Container: ${container.offsetWidth}x${container.offsetHeight}px</li>`;
html += `<li>Display: ${getComputedStyle(container).display}</li>`;
html += `<li>Visibility: ${getComputedStyle(container).visibility}</li>`;
} else {
html += '<li>Container: Not found</li>';
}
// Cytoscape container
if (cyContainer) {
html += `<li>Cy Container: ${cyContainer.offsetWidth}x${cyContainer.offsetHeight}px</li>`;
} else {
html += '<li>Cy Container: Not found</li>';
}
// Cytoscape instance
html += `<li>Cytoscape object: ${typeof cytoscape !== 'undefined' ? 'Available' : 'Not available'}</li>`;
html += `<li>Cytoscape instance: ${cyInstance ? 'Initialized' : 'Not initialized'}</li>`;
// If instance exists, get more details
if (cyInstance) {
html += `<li>Nodes: ${cyInstance.nodes().length}</li>`;
html += `<li>Edges: ${cyInstance.edges().length}</li>`;
}
html += '</ul>';
// Add refresh button
html += '<button id="refresh-debug" class="mt-2 px-3 py-1 bg-blue-700 text-white rounded hover:bg-blue-600">' +
'Refresh Debug Info</button>';
debugContainer.innerHTML = html;
// Add event listener to refresh button
document.getElementById('refresh-debug')?.addEventListener('click', checkGraphStatus);
}
</script>

View File

@ -37,12 +37,12 @@
--bg-secondary-rgb: 22, 26, 36; /* RGB for gradients */ --bg-secondary-rgb: 22, 26, 36; /* RGB for gradients */
} }
/* Light Mode Variables */ /* Enhanced Light Mode Variables - More tech-focused, less plain white */
:root.light-mode { :root.light-mode {
--bg-primary: #ffffff; --bg-primary: #f0f4f8; /* Subtle blue-gray instead of white */
--bg-secondary: #f8fafc; /* Lighter secondary */ --bg-secondary: #e5eaf2; /* Slightly darker secondary */
--bg-tertiary: #f1f5f9; /* Even lighter tertiary */ --bg-tertiary: #dae2ef; /* Even more blue tint for tertiary */
--bg-code: #f1f5f9; --bg-code: #e5edf7;
--text-primary: #1e293b; /* Darker primary text */ --text-primary: #1e293b; /* Darker primary text */
--text-secondary: #475569; /* Darker secondary text */ --text-secondary: #475569; /* Darker secondary text */
--text-tertiary: #64748b; /* Darker tertiary text */ --text-tertiary: #64748b; /* Darker tertiary text */
@ -52,14 +52,14 @@
--glow-primary: rgba(8, 145, 178, 0.15); --glow-primary: rgba(8, 145, 178, 0.15);
--glow-secondary: rgba(37, 99, 235, 0.15); --glow-secondary: rgba(37, 99, 235, 0.15);
--glow-tertiary: rgba(124, 58, 237, 0.15); --glow-tertiary: rgba(124, 58, 237, 0.15);
--border-primary: rgba(0, 0, 0, 0.1); /* Darker borders */ --border-primary: rgba(37, 99, 235, 0.15); /* More visible blue-tinted borders */
--border-secondary: rgba(0, 0, 0, 0.05); --border-secondary: rgba(8, 145, 178, 0.1);
--card-bg: rgba(255, 255, 255, 0.8); /* White card with opacity */ --card-bg: rgba(255, 255, 255, 0.6); /* More transparent card background */
--card-border: rgba(37, 99, 235, 0.3); /* Blue border */ --card-border: rgba(37, 99, 235, 0.2); /* Subtle blue border */
--ui-element: #e2e8f0; /* Lighter UI elements */ --ui-element: rgba(226, 232, 240, 0.7); /* More transparent UI elements */
--ui-element-hover: #cbd5e1; --ui-element-hover: rgba(203, 213, 225, 0.8);
--bg-primary-rgb: 255, 255, 255; /* RGB for gradients */ --bg-primary-rgb: 240, 244, 248; /* RGB for gradients */
--bg-secondary-rgb: 248, 250, 252; /* RGB for gradients */ --bg-secondary-rgb: 229, 234, 242; /* RGB for gradients */
} }
/* Ensure transitions for smooth theme changes */ /* Ensure transitions for smooth theme changes */
@ -67,24 +67,40 @@
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
} }
/* Knowledge Graph specific theme adjustments */ /* Knowledge Graph specific theme adjustments - More transparent in light mode */
:root.light-mode .graph-container { :root.light-mode .graph-container {
background: rgba(248, 250, 252, 0.3); background: rgba(248, 250, 252, 0.08); /* Much more transparent - lighter gray */
border: 1px solid var(--card-border); backdrop-filter: blur(2px);
border: 1px solid rgba(37, 99, 235, 0.15);
box-shadow: 0 4px 20px rgba(37, 99, 235, 0.05);
} }
:root.light-mode .node-details { :root.light-mode .node-details {
background: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.8); /* More opaque for readability */
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); backdrop-filter: blur(5px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(37, 99, 235, 0.1);
} }
:root.light-mode .graph-filters { :root.light-mode .graph-filters {
background: rgba(248, 250, 252, 0.7); background: rgba(248, 250, 252, 0.6); /* Slightly more opaque */
backdrop-filter: blur(3px);
border: 1px solid rgba(37, 99, 235, 0.1);
} }
:root.light-mode .graph-filter { :root.light-mode .graph-filter {
color: var(--text-secondary); color: var(--text-secondary);
border-color: var(--border-primary); border-color: var(--border-primary);
background: rgba(255, 255, 255, 0.5);
}
:root.light-mode .graph-filter:hover {
background: rgba(255, 255, 255, 0.7);
}
:root.light-mode .graph-filter.active {
background: linear-gradient(135deg, rgba(8, 145, 178, 0.1), rgba(37, 99, 235, 0.1));
border-color: rgba(37, 99, 235, 0.3);
} }
:root.light-mode .connections-list a { :root.light-mode .connections-list a {
@ -92,7 +108,12 @@
} }
:root.light-mode .node-link { :root.light-mode .node-link {
box-shadow: 0 4px 10px rgba(8, 145, 178, 0.15); box-shadow: 0 4px 10px rgba(8, 145, 178, 0.1);
background: linear-gradient(135deg, rgba(8, 145, 178, 0.1), rgba(37, 99, 235, 0.1));
}
:root.light-mode .node-link:hover {
background: linear-gradient(135deg, rgba(8, 145, 178, 0.2), rgba(37, 99, 235, 0.2));
} }
/* Fix for code blocks in light mode */ /* Fix for code blocks in light mode */
@ -100,6 +121,39 @@
:root.light-mode code { :root.light-mode code {
background-color: var(--bg-code); background-color: var(--bg-code);
color: var(--text-secondary); color: var(--text-secondary);
border: 1px solid rgba(37, 99, 235, 0.1);
}
/* Services and Newsletter sections - More transparent in light mode */
:root.light-mode .service-card {
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(5px);
border: 1px solid rgba(37, 99, 235, 0.1);
}
:root.light-mode .newsletter-container,
:root.light-mode .cta-container {
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(5px);
border: 1px solid rgba(37, 99, 235, 0.15);
}
:root.light-mode .newsletter-input {
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(37, 99, 235, 0.2);
}
/* Enhanced light mode body background with more pronounced grid pattern */
:root.light-mode body {
background-color: var(--bg-primary);
background-image:
radial-gradient(circle at 20% 35%, rgba(8, 145, 178, 0.08) 0%, transparent 50%),
radial-gradient(circle at 75% 15%, rgba(37, 99, 235, 0.08) 0%, transparent 45%),
radial-gradient(circle at 85% 70%, rgba(124, 58, 237, 0.08) 0%, transparent 40%),
linear-gradient(rgba(37, 99, 235, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(37, 99, 235, 0.05) 1px, transparent 1px);
background-size: auto, auto, auto, 16px 16px, 16px 16px;
background-position: 0 0, 0 0, 0 0, center center, center center;
} }
/* Apply base styles using variables */ /* Apply base styles using variables */
@ -129,4 +183,44 @@ input, select, textarea {
.post-card, .sidebar-block { .post-card, .sidebar-block {
background-color: var(--card-bg); background-color: var(--card-bg);
border-color: var(--card-border); border-color: var(--card-border);
}
/* Light mode buttons are more attractive */
:root.light-mode button {
background: linear-gradient(135deg, rgba(8, 145, 178, 0.05), rgba(37, 99, 235, 0.05));
border: 1px solid rgba(37, 99, 235, 0.1);
}
:root.light-mode button:hover {
background: linear-gradient(135deg, rgba(8, 145, 178, 0.1), rgba(37, 99, 235, 0.1));
border-color: rgba(37, 99, 235, 0.2);
}
/* Other light mode improvements */
:root.light-mode .primary-button,
:root.light-mode .cta-primary-button {
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
}
:root.light-mode .secondary-button,
:root.light-mode .cta-secondary-button {
background: rgba(255, 255, 255, 0.5);
border: 1px solid rgba(37, 99, 235, 0.2);
}
:root.light-mode .hero-section {
background: linear-gradient(to bottom, var(--bg-secondary), var(--bg-primary));
}
:root.light-mode .hero-bg {
background-image:
radial-gradient(circle at 20% 35%, rgba(8, 145, 178, 0.1) 0%, transparent 50%),
radial-gradient(circle at 75% 15%, rgba(37, 99, 235, 0.1) 0%, transparent 45%),
radial-gradient(circle at 85% 70%, rgba(124, 58, 237, 0.1) 0%, transparent 40%);
}
/* Fix for knowledge graph in both themes */
.graph-container {
backdrop-filter: blur(2px);
} }