fresh-main #7
|
@ -423,8 +423,8 @@ const currentPath = Astro.url.pathname;
|
|||
}
|
||||
});
|
||||
|
||||
// Search functionality - client-side post filtering
|
||||
const searchResults = document.getElementById('search-results');
|
||||
// Search functionality - client-side site-wide filtering (User provided version)
|
||||
const searchResults = document.getElementById('search-results'); // Assuming this ID exists in your dropdown HTML
|
||||
|
||||
// Function to perform search
|
||||
const performSearch = async (query) => {
|
||||
|
@ -437,39 +437,68 @@ const currentPath = Astro.url.pathname;
|
|||
}
|
||||
|
||||
try {
|
||||
// This would ideally be a server-side search or a pre-built index
|
||||
// For now, we'll just fetch all posts and filter client-side
|
||||
const response = await fetch('/search-index.json');
|
||||
// Fetch the search index that contains all site content
|
||||
const response = await fetch('/search-index.json'); // Ensure this path is correct based on your build output
|
||||
if (!response.ok) throw new Error('Failed to fetch search data');
|
||||
|
||||
const posts = await response.json();
|
||||
const results = posts.filter(post => {
|
||||
const allContent = await response.json();
|
||||
const results = allContent.filter(item => {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return (
|
||||
post.title.toLowerCase().includes(lowerQuery) ||
|
||||
post.description?.toLowerCase().includes(lowerQuery) ||
|
||||
post.tags?.some(tag => tag.toLowerCase().includes(lowerQuery))
|
||||
item.title.toLowerCase().includes(lowerQuery) ||
|
||||
item.description?.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
|
||||
if (searchResults) {
|
||||
if (results.length > 0) {
|
||||
searchResults.innerHTML = results.map(post => `
|
||||
<div class="search-result-item" data-url="/posts/${post.slug}/">
|
||||
<div class="search-result-title">${post.title}</div>
|
||||
<div class="search-result-snippet">${post.description || ''}</div>
|
||||
searchResults.innerHTML = results.map(item => {
|
||||
// Create type badge
|
||||
let typeBadge = '';
|
||||
switch(item.type) {
|
||||
case 'post':
|
||||
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>
|
||||
`).join('');
|
||||
<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
|
||||
document.querySelectorAll('.search-result-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
window.location.href = item.dataset.url;
|
||||
window.location.href = item.dataset.url; // Navigate to the item's URL
|
||||
});
|
||||
});
|
||||
} 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) {
|
||||
|
@ -480,7 +509,7 @@ const currentPath = Astro.url.pathname;
|
|||
}
|
||||
};
|
||||
|
||||
// Search input event handler
|
||||
// Search input event handler with debounce
|
||||
let searchTimeout;
|
||||
searchInput?.addEventListener('input', (e) => {
|
||||
clearTimeout(searchTimeout);
|
||||
|
@ -489,17 +518,18 @@ const currentPath = Astro.url.pathname;
|
|||
}, 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');
|
||||
searchForm?.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault(); // Prevent default form submission
|
||||
performSearch(searchInput.value);
|
||||
});
|
||||
|
||||
// Handle search-submit button click
|
||||
const searchSubmit = document.querySelector('.search-submit');
|
||||
// Handle search-submit button click (if you have a separate submit button)
|
||||
const searchSubmit = document.querySelector('.search-submit'); // Adjust selector if needed
|
||||
searchSubmit?.addEventListener('click', () => {
|
||||
performSearch(searchInput?.value || '');
|
||||
});
|
||||
});
|
||||
|
||||
}); // End of DOMContentLoaded
|
||||
</script>
|
|
@ -8,6 +8,8 @@ import Footer from '../components/Footer.astro';
|
|||
import Terminal from '../components/Terminal.astro';
|
||||
import KnowledgeGraph from '../components/KnowledgeGraph.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { Image } from 'astro:assets';
|
||||
import { COMMON_COMMANDS, TERMINAL_CONTENT } from '../config/terminal.js';
|
||||
|
||||
// Get all blog entries
|
||||
const allPosts = await getCollection('posts');
|
||||
|
@ -22,38 +24,8 @@ const sortedPosts = allPosts.sort((a, b) => {
|
|||
// Get recent posts (latest 4)
|
||||
const recentPosts = sortedPosts.slice(0, 4);
|
||||
|
||||
// Prepare terminal commands
|
||||
const terminalCommands = [
|
||||
{
|
||||
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 terminal commands - now imported from central config
|
||||
const terminalCommands = COMMON_COMMANDS;
|
||||
|
||||
// Prepare graph data for knowledge map
|
||||
// Extract categories and tags from posts
|
||||
|
@ -364,6 +336,38 @@ const techStack = [
|
|||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<Footer slot="footer" />
|
||||
|
@ -993,6 +997,32 @@ const techStack = [
|
|||
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 */
|
||||
@media (max-width: 1024px) {
|
||||
.hero-content {
|
||||
|
|
|
@ -77,6 +77,32 @@ const nodeTypeCounts = {
|
|||
<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};`}>
|
||||
<!-- 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 -->
|
||||
<div class="graph-instructions">
|
||||
<details class="instructions-details">
|
||||
|
@ -160,6 +186,32 @@ const nodeTypeCounts = {
|
|||
<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>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
|
@ -318,27 +370,103 @@ const nodeTypeCounts = {
|
|||
}}
|
||||
],
|
||||
// Update layout for better visualization of post-tag connections
|
||||
// Replace the existing layout configuration with this improved one
|
||||
layout: {
|
||||
name: 'cose',
|
||||
idealEdgeLength: 80,
|
||||
nodeOverlap: 20,
|
||||
idealEdgeLength: 75, // Increased from previous value
|
||||
nodeOverlap: 30, // Increased to prevent overlap
|
||||
refresh: 20,
|
||||
fit: true,
|
||||
padding: 30,
|
||||
randomize: false,
|
||||
componentSpacing: 100,
|
||||
nodeRepulsion: 450000,
|
||||
edgeElasticity: 100,
|
||||
nestingFactor: 5,
|
||||
gravity: 80,
|
||||
numIter: 1000,
|
||||
initialTemp: 200,
|
||||
coolingFactor: 0.95,
|
||||
minTemp: 1.0
|
||||
componentSpacing: 60,
|
||||
nodeRepulsion: 1000000, // Significantly increased repulsion
|
||||
edgeElasticity: 150, // Increased elasticity
|
||||
nestingFactor: 7, // Adjusted nesting
|
||||
gravity: 30, // Reduced gravity for more spread
|
||||
numIter: 2000, // Increased iterations for better settling
|
||||
initialTemp: 250, // Higher initial temperature
|
||||
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,
|
||||
});
|
||||
|
||||
// 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
|
||||
if (loadingEl) {
|
||||
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
|
||||
const searchInput = document.getElementById('search-input');
|
||||
if (searchInput) {
|
||||
|
@ -680,6 +773,235 @@ const nodeTypeCounts = {
|
|||
|
||||
// Dispatch graphReady event for external listeners
|
||||
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
|
||||
|
@ -718,7 +1040,7 @@ const nodeTypeCounts = {
|
|||
#knowledge-graph {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
z-index: 4; /* Increase z-index to ensure nodes appear above instructions */
|
||||
}
|
||||
|
||||
/* Loading Animation */
|
||||
|
@ -1068,18 +1390,30 @@ const nodeTypeCounts = {
|
|||
.graph-instructions {
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 5; /* Higher than graph for visibility but lower than open instructions */
|
||||
}
|
||||
|
||||
.instructions-details {
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
background: rgba(15, 23, 42, 0.9); /* More opaque background */
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-primary);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
position: relative; /* Changed from absolute */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.instructions-details[open] {
|
||||
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 {
|
||||
|
@ -1090,6 +1424,8 @@ const nodeTypeCounts = {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.instructions-summary::before {
|
||||
|
@ -1098,8 +1434,16 @@ const nodeTypeCounts = {
|
|||
}
|
||||
|
||||
.instructions-content {
|
||||
padding: 0 1rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 0.5rem 1rem 1rem;
|
||||
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 {
|
||||
|
@ -1158,4 +1502,437 @@ const nodeTypeCounts = {
|
|||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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
|
@ -2,37 +2,32 @@
|
|||
// Terminal.astro
|
||||
// A component that displays terminal-like interface with animated commands and outputs
|
||||
|
||||
interface Command {
|
||||
export interface Props {
|
||||
title?: string;
|
||||
height?: string;
|
||||
showTitleBar?: boolean;
|
||||
showPrompt?: boolean;
|
||||
commands?: {
|
||||
prompt: string;
|
||||
command: string;
|
||||
output?: string[];
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
commands: Command[];
|
||||
title?: string;
|
||||
theme?: 'dark' | 'light';
|
||||
interactive?: boolean;
|
||||
showTitleBar?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
const {
|
||||
commands,
|
||||
title = "argobox:~/homelab",
|
||||
theme = "dark",
|
||||
interactive = false,
|
||||
showTitleBar = true
|
||||
title = "terminal",
|
||||
height = "auto",
|
||||
showTitleBar = true,
|
||||
showPrompt = true,
|
||||
commands = []
|
||||
} = Astro.props;
|
||||
|
||||
// Make the last command have the typing effect
|
||||
const lastIndex = commands.length - 1;
|
||||
|
||||
// Conditionally add classes based on props
|
||||
const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : ''}`;
|
||||
---
|
||||
|
||||
<div class={terminalClasses}>
|
||||
<div class="terminal-box">
|
||||
{showTitleBar && (
|
||||
<div class="terminal-header">
|
||||
<div class="terminal-dots">
|
||||
|
@ -59,7 +54,7 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div class="terminal-content">
|
||||
<div class="terminal-content" style={`height: ${height};`}>
|
||||
{commands.map((cmd, index) => (
|
||||
<div class="terminal-block">
|
||||
<div class="terminal-line">
|
||||
|
@ -78,15 +73,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
</div>
|
||||
))}
|
||||
|
||||
{interactive && (
|
||||
<div class="terminal-block terminal-interactive">
|
||||
<div class="terminal-line">
|
||||
<span class="terminal-prompt">guest@argobox:~$</span>
|
||||
<input type="text" class="terminal-input" placeholder="Type 'help' for available commands" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="terminal-cursor"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -107,34 +93,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
position: relative;
|
||||
}
|
||||
|
||||
/* Light theme */
|
||||
.terminal-light {
|
||||
background: #f0f4f8;
|
||||
border-color: #d1dce5;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.07);
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.terminal-light .terminal-prompt {
|
||||
color: #2a7ac0;
|
||||
}
|
||||
|
||||
.terminal-light .terminal-command {
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.terminal-light .terminal-output {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.terminal-light .terminal-header {
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.terminal-light .terminal-title {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.terminal-header {
|
||||
display: flex;
|
||||
|
@ -261,22 +219,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Interactive elements */
|
||||
.terminal-interactive {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.terminal-input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-primary, #e2e8f0);
|
||||
font-family: var(--font-mono, 'JetBrains Mono', monospace);
|
||||
font-size: 0.9rem;
|
||||
flex: 1;
|
||||
outline: none;
|
||||
caret-color: transparent; /* Hide default cursor */
|
||||
}
|
||||
|
||||
/* Blinking cursor */
|
||||
.terminal-cursor {
|
||||
position: absolute;
|
||||
|
@ -290,10 +232,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
.terminal-interactive:has(.terminal-input:focus) ~ .terminal-cursor {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Typing effect */
|
||||
.terminal-typing {
|
||||
position: relative;
|
||||
|
@ -335,13 +273,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
.terminal-box:hover .terminal-dot-green {
|
||||
background: #34d399;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.terminal-box {
|
||||
height: 300px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
@ -383,7 +314,8 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
const cursor = typingElement.closest('.terminal-box').querySelector('.terminal-cursor');
|
||||
if (cursor) {
|
||||
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';
|
||||
|
@ -394,215 +326,12 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typeWriter();
|
||||
}, 1000 * elementIndex); // Sequential delay for multiple typing elements
|
||||
});
|
||||
|
||||
// Interactive terminal functionality
|
||||
const interactiveTerminals = document.querySelectorAll('.terminal-interactive');
|
||||
|
||||
interactiveTerminals.forEach(terminal => {
|
||||
const input = terminal.querySelector('.terminal-input');
|
||||
const terminalContent = terminal.closest('.terminal-content');
|
||||
const prompt = terminal.querySelector('.terminal-prompt').textContent;
|
||||
|
||||
if (!input || !terminalContent) return;
|
||||
|
||||
// Position cursor when input is focused
|
||||
input.addEventListener('focus', () => {
|
||||
const cursor = terminal.closest('.terminal-box').querySelector('.terminal-cursor');
|
||||
if (cursor) {
|
||||
const rect = input.getBoundingClientRect();
|
||||
const parentRect = terminalContent.getBoundingClientRect();
|
||||
|
||||
cursor.style.left = `${rect.left - parentRect.left + input.value.length * 8}px`;
|
||||
cursor.style.top = `${rect.top - parentRect.top}px`;
|
||||
cursor.style.height = `${rect.height}px`;
|
||||
}
|
||||
});
|
||||
|
||||
// Update cursor position as user types
|
||||
input.addEventListener('input', () => {
|
||||
const cursor = terminal.closest('.terminal-box').querySelector('.terminal-cursor');
|
||||
if (cursor) {
|
||||
const rect = input.getBoundingClientRect();
|
||||
const parentRect = terminalContent.getBoundingClientRect();
|
||||
|
||||
cursor.style.left = `${rect.left - parentRect.left + 8 * (prompt.length + input.value.length) + 8}px`;
|
||||
}
|
||||
});
|
||||
|
||||
// Process command on Enter
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
const command = input.value.trim();
|
||||
if (!command) return;
|
||||
|
||||
// Create new command block
|
||||
const commandBlock = document.createElement('div');
|
||||
commandBlock.className = 'terminal-block';
|
||||
|
||||
const commandLine = document.createElement('div');
|
||||
commandLine.className = 'terminal-line';
|
||||
|
||||
const promptSpan = document.createElement('span');
|
||||
promptSpan.className = 'terminal-prompt';
|
||||
promptSpan.textContent = prompt;
|
||||
|
||||
const commandSpan = document.createElement('span');
|
||||
commandSpan.className = 'terminal-command';
|
||||
commandSpan.textContent = command;
|
||||
|
||||
commandLine.appendChild(promptSpan);
|
||||
commandLine.appendChild(commandSpan);
|
||||
commandBlock.appendChild(commandLine);
|
||||
|
||||
// Process command and add output
|
||||
const output = processCommand(command);
|
||||
|
||||
if (output && output.length > 0) {
|
||||
const outputDiv = document.createElement('div');
|
||||
outputDiv.className = 'terminal-output';
|
||||
|
||||
output.forEach(line => {
|
||||
const lineDiv = document.createElement('div');
|
||||
lineDiv.className = 'terminal-output-line';
|
||||
lineDiv.innerHTML = line;
|
||||
outputDiv.appendChild(lineDiv);
|
||||
});
|
||||
|
||||
commandBlock.appendChild(outputDiv);
|
||||
}
|
||||
|
||||
// Insert before the interactive block
|
||||
terminal.parentNode.insertBefore(commandBlock, terminal);
|
||||
|
||||
// Clear input
|
||||
input.value = '';
|
||||
|
||||
// Scroll to bottom
|
||||
terminalContent.scrollTop = terminalContent.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
// Define available commands and their outputs
|
||||
function processCommand(cmd) {
|
||||
const commands = {
|
||||
'help': [
|
||||
'<span class="highlight">Available commands:</span>',
|
||||
' help - Display this help message',
|
||||
' clear - Clear the terminal',
|
||||
' ls - List available resources',
|
||||
' cat [file] - View file contents',
|
||||
' about - About this site',
|
||||
' status - Check system status',
|
||||
' uname -a - Display system information'
|
||||
],
|
||||
'clear': [],
|
||||
'about': [
|
||||
'<span class="highlight">LaForceIT</span>',
|
||||
'A tech blog focused on home lab infrastructure, Kubernetes,',
|
||||
'Docker, and DevOps best practices.',
|
||||
'',
|
||||
'Created by Daniel LaForce',
|
||||
'Type <span class="highlight">\'help\'</span> to see available commands'
|
||||
],
|
||||
'uname -a': [
|
||||
'ArgoBox-Lite 5.15.0-69-generic #76-Ubuntu SMP Fri Mar 17 17:19:29 UTC 2023 x86_64',
|
||||
'Hardware: ProxmoxVE 8.0.4 | Intel(R) Core(TM) i7-12700K | 64GB RAM'
|
||||
],
|
||||
'status': [
|
||||
'<span class="highlight">System Status:</span>',
|
||||
'<span class="success">✓</span> ArgoBox: Online',
|
||||
'<span class="success">✓</span> Kubernetes: Running',
|
||||
'<span class="success">✓</span> Docker Registry: Active',
|
||||
'<span class="success">✓</span> Gitea: Online',
|
||||
'<span class="warning">⚠</span> Monitoring: Degraded - Check Grafana instance'
|
||||
],
|
||||
'ls': [
|
||||
'<span class="highlight">Available resources:</span>',
|
||||
'kubernetes/ docker/ networking/',
|
||||
'homelab.md configs.yaml setup-guide.md',
|
||||
'resources.json projects.md'
|
||||
]
|
||||
};
|
||||
|
||||
// Check for cat command
|
||||
if (cmd.startsWith('cat ')) {
|
||||
const file = cmd.split(' ')[1];
|
||||
|
||||
const fileContents = {
|
||||
'homelab.md': [
|
||||
'<span class="highlight">## HomeLab Setup Guide</span>',
|
||||
'This document outlines my personal home lab setup,',
|
||||
'including hardware specifications, network configuration,',
|
||||
'and installed services.',
|
||||
'',
|
||||
'See the full guide at: /homelab'
|
||||
],
|
||||
'configs.yaml': [
|
||||
'apiVersion: v1',
|
||||
'kind: ConfigMap',
|
||||
'metadata:',
|
||||
' name: argobox-config',
|
||||
' namespace: default',
|
||||
'data:',
|
||||
' POSTGRES_HOST: "db.local"',
|
||||
' REDIS_HOST: "cache.local"',
|
||||
' ...'
|
||||
],
|
||||
'setup-guide.md': [
|
||||
'<span class="highlight">## Quick Start Guide</span>',
|
||||
'1. Install Proxmox on bare metal hardware',
|
||||
'2. Deploy K3s cluster using Ansible playbooks',
|
||||
'3. Configure storage using Longhorn',
|
||||
'4. Deploy ArgoCD for GitOps workflow',
|
||||
'...'
|
||||
],
|
||||
'resources.json': [
|
||||
'{',
|
||||
' "cpu": "12 cores",',
|
||||
' "memory": "64GB",',
|
||||
' "storage": "8TB",',
|
||||
' "network": "10Gbit"',
|
||||
'}'
|
||||
],
|
||||
'projects.md': [
|
||||
'<span class="highlight">## Current Projects</span>',
|
||||
'- <span class="success">ArgoBox</span>: Self-hosted deployment platform',
|
||||
'- <span class="success">K8s Monitor</span>: Custom Kubernetes dashboard',
|
||||
'- <span class="warning">Media Server</span>: In progress',
|
||||
'- <span class="highlight">See all projects at:</span> /projects'
|
||||
]
|
||||
};
|
||||
|
||||
if (fileContents[file]) {
|
||||
return fileContents[file];
|
||||
} else {
|
||||
return [`<span class="error">Error: File '${file}' not found.</span>`];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle unknown commands
|
||||
if (!commands[cmd]) {
|
||||
return [`<span class="error">Command not found: ${cmd}</span>`, 'Type <span class="highlight">\'help\'</span> to see available commands'];
|
||||
}
|
||||
|
||||
// Handle clear command
|
||||
if (cmd === 'clear') {
|
||||
// Remove all blocks except the interactive one
|
||||
const blocks = terminalContent.querySelectorAll('.terminal-block:not(.terminal-interactive)');
|
||||
blocks.forEach(block => block.remove());
|
||||
return [];
|
||||
}
|
||||
|
||||
return commands[cmd];
|
||||
}
|
||||
});
|
||||
|
||||
// Button interactions
|
||||
const minButtons = document.querySelectorAll('.terminal-button-minimize');
|
||||
const maxButtons = document.querySelectorAll('.terminal-button-maximize');
|
||||
|
@ -610,24 +339,31 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
minButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const terminalBox = button.closest('.terminal-box');
|
||||
if (terminalBox) {
|
||||
terminalBox.classList.toggle('minimized');
|
||||
|
||||
if (terminalBox.classList.contains('minimized')) {
|
||||
const content = terminalBox.querySelector('.terminal-content');
|
||||
if (content) {
|
||||
terminalBox.dataset.prevHeight = terminalBox.style.height;
|
||||
terminalBox.style.height = '40px';
|
||||
content.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
const content = terminalBox.querySelector('.terminal-content');
|
||||
if (content) {
|
||||
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px';
|
||||
content.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
maxButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const terminalBox = button.closest('.terminal-box');
|
||||
if (terminalBox) {
|
||||
terminalBox.classList.toggle('maximized');
|
||||
|
||||
if (terminalBox.classList.contains('maximized')) {
|
||||
|
@ -644,7 +380,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
terminalBox.style.zIndex = '9999';
|
||||
terminalBox.style.borderRadius = '0';
|
||||
} else {
|
||||
const content = terminalBox.querySelector('.terminal-content');
|
||||
terminalBox.style.position = terminalBox.dataset.prevPosition || 'relative';
|
||||
terminalBox.style.width = terminalBox.dataset.prevWidth || '100%';
|
||||
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px';
|
||||
|
@ -653,6 +388,7 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
terminalBox.style.top = 'auto';
|
||||
terminalBox.style.left = 'auto';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
};
|
||||
}
|
||||
}
|
|
@ -57,7 +57,7 @@ const {
|
|||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link rel="stylesheet" href="/styles/theme.css" />
|
||||
<link rel="stylesheet" href="/src/styles/theme.css" />
|
||||
|
||||
<!-- Cytoscape Library for Knowledge Graph -->
|
||||
<script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script>
|
||||
|
@ -360,5 +360,90 @@ const {
|
|||
}
|
||||
});
|
||||
</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>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -3,6 +3,8 @@ import BaseLayout from './BaseLayout.astro';
|
|||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import Newsletter from '../components/Newsletter.astro';
|
||||
import MiniKnowledgeGraph from '../components/MiniKnowledgeGraph.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
interface Props {
|
||||
frontmatter: {
|
||||
|
@ -11,20 +13,21 @@ interface Props {
|
|||
pubDate: Date;
|
||||
updatedDate?: Date;
|
||||
heroImage?: string;
|
||||
category?: string; // Keep category for potential filtering, but don't display in header
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
readTime?: string;
|
||||
draft?: boolean;
|
||||
author?: string; // Keep author field if needed elsewhere
|
||||
// Add other potential frontmatter fields as optional
|
||||
author?: string;
|
||||
github?: string;
|
||||
live?: string;
|
||||
technologies?: string[];
|
||||
related_posts?: string[]; // Explicit related posts by slug
|
||||
}
|
||||
}
|
||||
|
||||
const { frontmatter } = Astro.props;
|
||||
|
||||
// Format dates
|
||||
const formattedPubDate = frontmatter.pubDate ? new Date(frontmatter.pubDate).toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
|
@ -39,6 +42,58 @@ const formattedUpdatedDate = frontmatter.updatedDate ? new Date(frontmatter.upda
|
|||
|
||||
// Default image if heroImage is missing
|
||||
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}>
|
||||
|
@ -63,7 +118,6 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
<span class="blog-post-updated">(Updated {formattedUpdatedDate})</span>
|
||||
)}
|
||||
{frontmatter.readTime && <span class="blog-post-read-time">{frontmatter.readTime}</span>}
|
||||
{/* Category removed from display here */}
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
|
@ -76,6 +130,18 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
)}
|
||||
</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 */}
|
||||
{displayImage && (
|
||||
<div class="blog-post-hero">
|
||||
|
@ -88,9 +154,6 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
<slot /> {/* Renders the actual markdown content */}
|
||||
</div>
|
||||
|
||||
{/* Future Feature Placeholders remain commented out */}
|
||||
{/* ... */}
|
||||
|
||||
</article>
|
||||
|
||||
{/* Sidebar */}
|
||||
|
@ -107,7 +170,6 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
<p class="author-bio">
|
||||
Exploring enterprise-grade infrastructure, automation, Kubernetes, and zero-trust networking in the home lab and beyond.
|
||||
</p>
|
||||
{/* Social links removed */}
|
||||
</div>
|
||||
|
||||
{/* Table of Contents Card */}
|
||||
|
@ -118,8 +180,26 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
</nav>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
@ -168,7 +248,6 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
}
|
||||
</script>
|
||||
|
||||
{/* Styles Updated */}
|
||||
<style>
|
||||
.draft-badge {
|
||||
display: inline-block;
|
||||
|
@ -183,12 +262,11 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
}
|
||||
.blog-post-container {
|
||||
display: grid;
|
||||
/* Adjusted grid for wider TOC/Sidebar */
|
||||
grid-template-columns: 7fr 2fr;
|
||||
gap: 3rem; /* Wider gap */
|
||||
max-width: 1400px; /* Wider max width */
|
||||
margin: 2rem auto;
|
||||
padding: 0 1.5rem;
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
gap: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
.blog-post-header {
|
||||
margin-bottom: 2.5rem;
|
||||
|
@ -216,7 +294,6 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
/* Removed .blog-post-category style */
|
||||
.blog-post-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -250,6 +327,26 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
height: auto;
|
||||
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 {
|
||||
/* Styles inherited from prose */
|
||||
}
|
||||
|
@ -335,6 +432,54 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
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) {
|
||||
.blog-post-container {
|
||||
grid-template-columns: 1fr; /* Stack on smaller screens */
|
||||
|
|
|
@ -215,7 +215,10 @@ const commands = [
|
|||
});
|
||||
|
||||
// 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
|
||||
// Find the function that creates post cards (might be called createPostCardHTML)
|
||||
|
||||
function createPostCardHTML(post) {
|
||||
// Make sure tags is an array before stringifying
|
||||
const tagsString = JSON.stringify(post.tags || []);
|
||||
|
||||
|
@ -260,7 +263,7 @@ const commands = [
|
|||
</div>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to filter and update the grid
|
||||
function updateGrid() {
|
||||
|
@ -949,4 +952,34 @@ const commands = [
|
|||
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>
|
File diff suppressed because it is too large
Load Diff
|
@ -1,265 +1,483 @@
|
|||
---
|
||||
// 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 Header from '../../components/Header.astro';
|
||||
import Footer from '../../components/Footer.astro';
|
||||
import KnowledgeGraph from '../../components/KnowledgeGraph.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allPosts = await getCollection('blog');
|
||||
const uniqueTags = [...new Set(allPosts.map((post) => post.data.tags).flat())];
|
||||
try {
|
||||
// Get all posts
|
||||
const allPosts = await getCollection('posts', ({ data }) => {
|
||||
// Exclude draft posts in production
|
||||
return import.meta.env.PROD ? !data.draft : true;
|
||||
});
|
||||
|
||||
// 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 uniqueTags.map((tag) => {
|
||||
const filteredPosts = allPosts.filter((post) => post.data.tags.includes(tag));
|
||||
return {
|
||||
params: { tag },
|
||||
props: { posts: filteredPosts },
|
||||
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 { posts } = Astro.props;
|
||||
const { posts, allPosts } = Astro.props;
|
||||
|
||||
// Format date
|
||||
// Format dates
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '';
|
||||
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)
|
||||
const sortedPosts = posts.sort((a, b) => {
|
||||
const dateA = new Date(a.data.pubDate);
|
||||
const dateB = new Date(b.data.pubDate);
|
||||
const sortedPosts = [...posts].sort((a, b) => {
|
||||
const dateA = new Date(a.data.pubDate || 0);
|
||||
const dateB = new Date(b.data.pubDate || 0);
|
||||
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}`}>
|
||||
<div class="container tag-page">
|
||||
<header class="tag-hero">
|
||||
<h1>Posts tagged with <span class="tag-highlight">{tag}</span></h1>
|
||||
<p>Explore {sortedPosts.length} {sortedPosts.length === 1 ? 'article' : 'articles'} related to {tag}</p>
|
||||
</header>
|
||||
<Header slot="header" />
|
||||
|
||||
<main class="tag-page-container">
|
||||
<section class="tag-hero">
|
||||
<div class="container">
|
||||
<h1>Posts tagged with <span class="tag-highlight">{tag}</span></h1>
|
||||
<p class="tag-description">
|
||||
Explore {sortedPosts.length} {sortedPosts.length === 1 ? 'article' : 'articles'} related to {tag}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tag-content container">
|
||||
<div class="knowledge-graph-section">
|
||||
<h2>Content Connections</h2>
|
||||
<p class="section-description">
|
||||
Explore how {tag} relates to other content and tags
|
||||
</p>
|
||||
|
||||
<div class="graph-wrapper">
|
||||
<KnowledgeGraph graphData={graphData} height="400px" initialFilter="all" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="posts-section">
|
||||
<h2>Articles</h2>
|
||||
<div class="posts-grid">
|
||||
{sortedPosts.map((post) => (
|
||||
{sortedPosts.length > 0 ? sortedPosts.map((post) => (
|
||||
<article class="post-card">
|
||||
<!-- Simplified image rendering that works reliably -->
|
||||
<a href={`/posts/${post.slug}/`} class="post-card-link">
|
||||
{post.data.heroImage && (
|
||||
<div class="post-image-wrapper">
|
||||
<img
|
||||
width={720}
|
||||
height={360}
|
||||
src={post.data.heroImage || "/images/placeholders/default.jpg"}
|
||||
src={post.data.heroImage}
|
||||
alt=""
|
||||
class="post-image"
|
||||
width="400"
|
||||
height="225"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="post-content">
|
||||
<time datetime={post.data.pubDate}>{formatDate(post.data.pubDate)}</time>
|
||||
<h2 class="post-title">
|
||||
<a href={`/posts/${post.slug}/`}>{post.data.title}</a>
|
||||
</h2>
|
||||
<p class="post-excerpt">{post.data.description}</p>
|
||||
<div class="post-meta">
|
||||
<span class="reading-time">{post.data.minutesRead || '5 min'} read</span>
|
||||
<ul class="post-tags">
|
||||
{post.data.tags.map((tagName) => (
|
||||
<li>
|
||||
<a href={`/tag/${tagName}`} class={tagName === tag ? 'current-tag' : ''}>
|
||||
{tagName}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
<time datetime={post.data.pubDate?.toISOString()}>
|
||||
{formatDate(post.data.pubDate)}
|
||||
</time>
|
||||
{post.data.readTime && (
|
||||
<span class="read-time">{post.data.readTime}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<a href="/tags" class="all-tags-link">
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<div class="tag-navigation">
|
||||
<a href="/blog" class="back-button">
|
||||
<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>
|
||||
<polyline points="12 19 5 12 12 5"></polyline>
|
||||
</svg>
|
||||
View all tags
|
||||
Back to All Posts
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer slot="footer" />
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.tag-page {
|
||||
padding-top: 2rem;
|
||||
.tag-page-container {
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.tag-hero {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
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 {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.tag-hero h1 {
|
||||
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;
|
||||
.post-card-link {
|
||||
display: flex;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
.post-image-wrapper {
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
}
|
||||
|
||||
.post-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
height: 100%;
|
||||
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 {
|
||||
padding: 1.5rem;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-top: auto;
|
||||
justify-content: space-between;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.reading-time {
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-sm);
|
||||
.read-time {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.post-tags li a {
|
||||
display: block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: rgba(56, 189, 248, 0.1);
|
||||
border-radius: 20px;
|
||||
.post-tag {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
background: rgba(226, 232, 240, 0.05);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.post-tag.current-tag {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: var(--accent-primary);
|
||||
font-size: var(--font-size-xs);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.post-tags li a:hover {
|
||||
background: rgba(56, 189, 248, 0.2);
|
||||
transform: translateY(-2px);
|
||||
.no-posts {
|
||||
grid-column: 1 / -1;
|
||||
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 {
|
||||
background: var(--accent-primary);
|
||||
.back-to-blog {
|
||||
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);
|
||||
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;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0 auto;
|
||||
padding: 0.75rem 1.5rem;
|
||||
padding: 0.8rem 1.5rem;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 30px;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
width: fit-content;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.all-tags-link:hover {
|
||||
.back-button:hover {
|
||||
background: var(--bg-tertiary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tag-hero h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
.tag-hero {
|
||||
padding: 4rem 0 2rem;
|
||||
}
|
||||
|
||||
.posts-grid {
|
||||
|
|
|
@ -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>
|
|
@ -37,12 +37,12 @@
|
|||
--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 {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8fafc; /* Lighter secondary */
|
||||
--bg-tertiary: #f1f5f9; /* Even lighter tertiary */
|
||||
--bg-code: #f1f5f9;
|
||||
--bg-primary: #f0f4f8; /* Subtle blue-gray instead of white */
|
||||
--bg-secondary: #e5eaf2; /* Slightly darker secondary */
|
||||
--bg-tertiary: #dae2ef; /* Even more blue tint for tertiary */
|
||||
--bg-code: #e5edf7;
|
||||
--text-primary: #1e293b; /* Darker primary text */
|
||||
--text-secondary: #475569; /* Darker secondary text */
|
||||
--text-tertiary: #64748b; /* Darker tertiary text */
|
||||
|
@ -52,14 +52,14 @@
|
|||
--glow-primary: rgba(8, 145, 178, 0.15);
|
||||
--glow-secondary: rgba(37, 99, 235, 0.15);
|
||||
--glow-tertiary: rgba(124, 58, 237, 0.15);
|
||||
--border-primary: rgba(0, 0, 0, 0.1); /* Darker borders */
|
||||
--border-secondary: rgba(0, 0, 0, 0.05);
|
||||
--card-bg: rgba(255, 255, 255, 0.8); /* White card with opacity */
|
||||
--card-border: rgba(37, 99, 235, 0.3); /* Blue border */
|
||||
--ui-element: #e2e8f0; /* Lighter UI elements */
|
||||
--ui-element-hover: #cbd5e1;
|
||||
--bg-primary-rgb: 255, 255, 255; /* RGB for gradients */
|
||||
--bg-secondary-rgb: 248, 250, 252; /* RGB for gradients */
|
||||
--border-primary: rgba(37, 99, 235, 0.15); /* More visible blue-tinted borders */
|
||||
--border-secondary: rgba(8, 145, 178, 0.1);
|
||||
--card-bg: rgba(255, 255, 255, 0.6); /* More transparent card background */
|
||||
--card-border: rgba(37, 99, 235, 0.2); /* Subtle blue border */
|
||||
--ui-element: rgba(226, 232, 240, 0.7); /* More transparent UI elements */
|
||||
--ui-element-hover: rgba(203, 213, 225, 0.8);
|
||||
--bg-primary-rgb: 240, 244, 248; /* RGB for gradients */
|
||||
--bg-secondary-rgb: 229, 234, 242; /* RGB for gradients */
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Knowledge Graph specific theme adjustments */
|
||||
/* Knowledge Graph specific theme adjustments - More transparent in light mode */
|
||||
:root.light-mode .graph-container {
|
||||
background: rgba(248, 250, 252, 0.3);
|
||||
border: 1px solid var(--card-border);
|
||||
background: rgba(248, 250, 252, 0.08); /* Much more transparent - lighter gray */
|
||||
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 {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
background: rgba(255, 255, 255, 0.8); /* More opaque for readability */
|
||||
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 {
|
||||
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 {
|
||||
color: var(--text-secondary);
|
||||
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 {
|
||||
|
@ -92,7 +108,12 @@
|
|||
}
|
||||
|
||||
: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 */
|
||||
|
@ -100,6 +121,39 @@
|
|||
:root.light-mode code {
|
||||
background-color: var(--bg-code);
|
||||
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 */
|
||||
|
@ -130,3 +184,43 @@ input, select, textarea {
|
|||
background-color: var(--card-bg);
|
||||
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);
|
||||
}
|
Loading…
Reference in New Issue