840 lines
24 KiB
Plaintext
840 lines
24 KiB
Plaintext
---
|
|
import { getCollection } from 'astro:content';
|
|
import BaseLayout from '../layouts/BaseLayout.astro'; // Corrected path
|
|
import KnowledgeGraph from '../components/KnowledgeGraph.astro'; // Corrected path
|
|
import Terminal from '../components/Terminal.astro'; // Corrected path
|
|
import Header from '../components/Header.astro'; // Import Header
|
|
import Footer from '../components/Footer.astro'; // Import Footer
|
|
|
|
// Get all blog entries
|
|
const allPosts = await getCollection('posts');
|
|
|
|
// Sort by publication date
|
|
const sortedPosts = allPosts.sort((a, b) => {
|
|
const dateA = a.data.pubDate ? new Date(a.data.pubDate) : new Date(0);
|
|
const dateB = b.data.pubDate ? new Date(b.data.pubDate) : new Date(0);
|
|
return dateB.getTime() - dateA.getTime();
|
|
});
|
|
|
|
// Get all unique tags
|
|
const allTags = [...new Set(allPosts.flatMap(post => post.data.tags || []))].sort();
|
|
|
|
// Get all unique categories
|
|
const allCategories = [...new Set(allPosts.map(post => post.data.category).filter(Boolean))].sort();
|
|
|
|
// Prepare post data for client-side filtering and knowledge graph
|
|
const postsData = sortedPosts.map(post => ({
|
|
slug: post.slug,
|
|
title: post.data.title,
|
|
description: post.data.description || '',
|
|
pubDate: post.data.pubDate ? new Date(post.data.pubDate).toLocaleDateString('en-us', { year: 'numeric', month: 'short', day: 'numeric' }) : 'No date',
|
|
pubDateISO: post.data.pubDate ? new Date(post.data.pubDate).toISOString() : '',
|
|
category: post.data.category || 'Uncategorized',
|
|
tags: post.data.tags || [],
|
|
heroImage: post.data.heroImage || '/images/placeholders/default.jpg',
|
|
readTime: post.data.readTime || '5 min read',
|
|
isDraft: post.data.draft || false
|
|
}));
|
|
|
|
// Prepare graph data (Obsidian-style: Posts and Tags)
|
|
const graphNodes = [];
|
|
const graphEdges = [];
|
|
const tagNodes = new Map(); // To avoid duplicate tag nodes
|
|
|
|
// Add post nodes
|
|
sortedPosts.forEach(post => {
|
|
if (!post.data.draft) { // Exclude drafts from graph
|
|
graphNodes.push({
|
|
id: post.slug,
|
|
label: post.data.title,
|
|
type: 'post', // Add type for styling/interaction
|
|
url: `/posts/${post.slug}/` // Add URL for linking
|
|
});
|
|
|
|
// Add tag nodes and edges
|
|
(post.data.tags || []).forEach(tag => {
|
|
const tagId = `tag-${tag}`;
|
|
// Add tag node only if it doesn't exist
|
|
if (!tagNodes.has(tagId)) {
|
|
graphNodes.push({
|
|
id: tagId,
|
|
label: `#${tag}`, // Prefix with # for clarity
|
|
type: 'tag' // Add type
|
|
});
|
|
tagNodes.set(tagId, true);
|
|
}
|
|
// Add edge connecting post to tag
|
|
graphEdges.push({
|
|
source: post.slug,
|
|
target: tagId,
|
|
type: 'tag-connection' // Add type
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
const graphData = { nodes: graphNodes, edges: graphEdges };
|
|
|
|
// Terminal commands for tech effect
|
|
const commands = [
|
|
{
|
|
prompt: "[laforceit@argobox]$ ",
|
|
command: "find ./posts -type f -name \"*.md\" | sort -n | wc -l",
|
|
output: [`${allPosts.length} posts found`]
|
|
},
|
|
{
|
|
prompt: "[laforceit@argobox]$ ",
|
|
command: "ls -la ./categories",
|
|
output: allCategories.map(cat => `${cat}`)
|
|
},
|
|
{
|
|
prompt: "[laforceit@argobox]$ ",
|
|
command: "grep -r \"kubernetes\" --include=\"*.md\" ./posts | wc -l",
|
|
output: [`${allPosts.filter(post =>
|
|
post.data.tags?.includes('kubernetes') ||
|
|
post.data.category === 'Kubernetes' ||
|
|
post.data.title?.toLowerCase().includes('kubernetes') ||
|
|
post.data.description?.toLowerCase().includes('kubernetes')
|
|
).length} matches found`]
|
|
}
|
|
];
|
|
---
|
|
|
|
<BaseLayout title="Blog | LaForce IT - Home Lab & DevOps Insights" description="Explore articles about Kubernetes, Infrastructure, DevOps, and Home Lab setups">
|
|
<Header slot="header" /> {/* Pass Header to slot */}
|
|
<main>
|
|
{/* Hero Section with Terminal */}
|
|
<section class="hero-section">
|
|
<div class="container">
|
|
<div class="hero-content">
|
|
<div class="hero-text">
|
|
<div class="hero-subtitle">Technical Articles & Guides</div>
|
|
<h1 class="hero-title">Exploring <span>advanced infrastructure</span> and automation</h1>
|
|
<p class="hero-description">
|
|
Dive into enterprise-grade home lab setups, Kubernetes deployments, and DevOps best practices for the modern tech enthusiast.
|
|
</p>
|
|
</div>
|
|
<div class="terminal-container">
|
|
<Terminal commands={commands} title="argobox:~/blog" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Blog Posts Section */}
|
|
<section class="blog-posts-section">
|
|
<div class="container">
|
|
<div class="section-header">
|
|
<h2 class="section-title">Latest Articles</h2>
|
|
<p class="section-description">
|
|
Technical insights, infrastructure guides, and DevOps best practices
|
|
</p>
|
|
</div>
|
|
|
|
{/* Search and Filter Section */}
|
|
<div class="search-filter-section">
|
|
<div class="search-bar">
|
|
<input type="search" id="search-input" placeholder="Search posts..." class="search-input" />
|
|
</div>
|
|
<span class="filter-label">Filter by Tag:</span>
|
|
<button class="tag-filter-btn active" data-tag="all">All</button>
|
|
{allTags.map(tag => (
|
|
<button class="tag-filter-btn" data-tag={tag}>{tag}</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Integrated Knowledge Graph */}
|
|
<div class="integrated-graph-container">
|
|
<KnowledgeGraph graphData={graphData} />
|
|
{/* We will update graphData generation later */}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Blog Grid (will be populated by JS) */}
|
|
<div class="blog-grid" id="blog-grid">
|
|
<div class="loading-indicator">
|
|
<div class="loading-spinner"></div>
|
|
<span>Loading articles...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
<Footer slot="footer" /> {/* Pass Footer to slot */}
|
|
|
|
<!-- Client-side script for filtering and graph interactions -->
|
|
<script define:vars={{ postsData, graphData }}>
|
|
// DOM Elements
|
|
const searchInput = document.getElementById('search-input');
|
|
const tagButtons = document.querySelectorAll('.tag-filter-btn');
|
|
const blogGrid = document.getElementById('blog-grid');
|
|
// Removed graphFilters as category filtering is removed from graph
|
|
|
|
// State variables
|
|
let currentFilterTag = 'all';
|
|
let currentSearchTerm = '';
|
|
// Removed currentGraphFilter
|
|
let cy; // Cytoscape instance will be set by KnowledgeGraph component
|
|
|
|
// Wait for cytoscape instance to be available
|
|
document.addEventListener('graphReady', (e) => {
|
|
cy = e.detail.cy;
|
|
setupGraphInteractions();
|
|
});
|
|
|
|
// Setup graph interactions (Post and Tag nodes)
|
|
function setupGraphInteractions() {
|
|
if (!cy) {
|
|
console.error("Cytoscape instance not ready.");
|
|
return;
|
|
}
|
|
|
|
// Remove previous category filter logic if any existed
|
|
// graphFilters.forEach(...) logic removed
|
|
|
|
// Handle clicks on graph nodes
|
|
cy.on('tap', 'node', function(evt) {
|
|
const node = evt.target;
|
|
const nodeId = node.id();
|
|
const nodeType = node.data('type'); // Get type ('post' or 'tag')
|
|
|
|
console.log(`Node clicked: ID=${nodeId}, Type=${nodeType}`); // Debug log
|
|
|
|
if (nodeType === 'post') {
|
|
// Handle post node click: Find post, update search, filter grid, scroll
|
|
const post = postsData.find(p => p.slug === nodeId);
|
|
if (post) {
|
|
console.log(`Post node clicked: ${post.title}`);
|
|
// Reset tag filter to 'all' when a specific post is selected via graph
|
|
currentFilterTag = 'all';
|
|
tagButtons.forEach(btn => btn.classList.remove('active'));
|
|
const allButton = document.querySelector('.tag-filter-btn[data-tag="all"]');
|
|
if (allButton) allButton.classList.add('active');
|
|
|
|
// Update search bar and term
|
|
searchInput.value = post.title; // Show post title in search
|
|
currentSearchTerm = post.title; // Filter grid by title
|
|
|
|
// Update grid to show only this post (or matching search term)
|
|
updateGrid();
|
|
|
|
// Scroll to the blog section smoothly
|
|
const blogSection = document.querySelector('.blog-posts-section');
|
|
if (blogSection) {
|
|
blogSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
} else {
|
|
console.warn(`Post data not found for slug: ${nodeId}`);
|
|
}
|
|
|
|
} else if (nodeType === 'tag') {
|
|
// Handle tag node click: Simulate click on corresponding tag filter button
|
|
const tagName = nodeId.replace(/^tag-/, ''); // Extract tag name (remove 'tag-' prefix)
|
|
console.log(`Tag node clicked: ${tagName}`);
|
|
|
|
const correspondingButton = document.querySelector(`.tag-filter-btn[data-tag="${tagName}"]`);
|
|
|
|
if (correspondingButton) {
|
|
console.log(`Found corresponding button for tag: ${tagName}`);
|
|
// Simulate click on the button
|
|
correspondingButton.click();
|
|
// Scroll to blog section smoothly
|
|
const blogSection = document.querySelector('.blog-posts-section');
|
|
if (blogSection) {
|
|
blogSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
} else {
|
|
console.warn(`Could not find tag filter button for tag: ${tagName}`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Function to create HTML for a single post card
|
|
function createPostCardHTML(post) {
|
|
// Make sure tags is an array before stringifying
|
|
const tagsString = JSON.stringify(post.tags || []);
|
|
|
|
// Create tag pills HTML
|
|
const tagPills = post.tags.map(tag =>
|
|
`<span class="post-tag">${tag}</span>`
|
|
).join('');
|
|
|
|
return `
|
|
<article class="post-card" data-tags='${tagsString}' data-slug="${post.slug}">
|
|
<div class="post-card-inner">
|
|
<div class="post-image-container">
|
|
<img
|
|
width="720"
|
|
height="360"
|
|
src="${post.heroImage}"
|
|
alt=""
|
|
class="post-image"
|
|
loading="lazy"
|
|
/>
|
|
<div class="post-category-badge">${post.category}</div>
|
|
</div>
|
|
<div class="post-content">
|
|
<div class="post-meta">
|
|
<time datetime="${post.pubDateISO}">${post.pubDate}</time>
|
|
<span class="post-read-time">${post.readTime}</span>
|
|
</div>
|
|
<h3 class="post-title">
|
|
<a href="/posts/${post.slug}/">${post.title}</a>
|
|
${post.isDraft ? '<span class="draft-badge">Draft</span>' : ''}
|
|
</h3>
|
|
<p class="post-excerpt">${post.description}</p>
|
|
<div class="post-footer">
|
|
<div class="post-tags">
|
|
${tagPills}
|
|
</div>
|
|
<a href="/posts/${post.slug}/" class="read-more">Read More</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
`;
|
|
}
|
|
|
|
// Function to filter and update the grid
|
|
function updateGrid() {
|
|
const searchTermLower = currentSearchTerm.toLowerCase();
|
|
|
|
// Ensure postsData is available
|
|
if (typeof postsData === 'undefined' || !postsData) {
|
|
console.error("postsData is not available in updateGrid");
|
|
if(blogGrid) blogGrid.innerHTML = '<p class="no-results">Error loading post data.</p>';
|
|
return;
|
|
}
|
|
|
|
const filteredPosts = postsData.filter(post => {
|
|
const postTags = post.tags || []; // Ensure tags is an array
|
|
const matchesTag = currentFilterTag === 'all' || postTags.includes(currentFilterTag);
|
|
const matchesSearch = searchTermLower === '' ||
|
|
post.title.toLowerCase().includes(searchTermLower) ||
|
|
post.description.toLowerCase().includes(searchTermLower) ||
|
|
postTags.some(tag => tag.toLowerCase().includes(searchTermLower));
|
|
return matchesTag && matchesSearch && !post.isDraft; // Exclude drafts
|
|
});
|
|
|
|
// Update the grid HTML
|
|
if (blogGrid) {
|
|
if (filteredPosts.length > 0) {
|
|
blogGrid.innerHTML = filteredPosts.map(createPostCardHTML).join('');
|
|
|
|
// If graph is available, highlight post nodes shown in the grid
|
|
if (cy) {
|
|
const matchingPostSlugs = filteredPosts.map(post => post.slug);
|
|
|
|
// Reset styles on all nodes first
|
|
cy.nodes().removeClass('highlighted').removeClass('faded');
|
|
|
|
// Highlight post nodes that are currently visible in the grid
|
|
cy.nodes('[type="post"]').forEach(node => {
|
|
if (matchingPostSlugs.includes(node.id())) {
|
|
node.removeClass('faded').addClass('highlighted');
|
|
} else {
|
|
node.removeClass('highlighted').addClass('faded'); // Fade non-matching posts
|
|
}
|
|
});
|
|
|
|
// Highlight tag nodes connected to visible posts OR the currently selected tag
|
|
cy.nodes('[type="tag"]').forEach(tagNode => {
|
|
const tagName = tagNode.id().replace(/^tag-/, '');
|
|
const isSelectedTag = tagName === currentFilterTag;
|
|
const isConnectedToVisiblePost = tagNode.connectedEdges().sources().some(postNode => matchingPostSlugs.includes(postNode.id()));
|
|
|
|
if (isSelectedTag || (currentFilterTag === 'all' && isConnectedToVisiblePost)) {
|
|
tagNode.removeClass('faded').addClass('highlighted');
|
|
} else {
|
|
tagNode.removeClass('highlighted').addClass('faded');
|
|
}
|
|
});
|
|
|
|
// Adjust edge visibility based on connected highlighted nodes
|
|
cy.edges().forEach(edge => {
|
|
if (edge.source().hasClass('highlighted') && edge.target().hasClass('highlighted')) {
|
|
edge.removeClass('faded').addClass('highlighted');
|
|
} else {
|
|
edge.removeClass('highlighted').addClass('faded');
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
blogGrid.innerHTML = '<p class="no-results">No posts found matching your criteria.</p>';
|
|
}
|
|
} else {
|
|
console.error("Blog grid element not found!");
|
|
}
|
|
}
|
|
|
|
// Event listener for search input
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', (e) => {
|
|
currentSearchTerm = e.target.value;
|
|
updateGrid();
|
|
});
|
|
} else {
|
|
console.error("Search input element not found!");
|
|
}
|
|
|
|
// Event listeners for tag buttons
|
|
tagButtons.forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
// Update active button style
|
|
tagButtons.forEach(btn => btn.classList.remove('active'));
|
|
button.classList.add('active');
|
|
|
|
// Update filter and grid
|
|
currentFilterTag = button.dataset.tag;
|
|
updateGrid();
|
|
});
|
|
});
|
|
|
|
// Initial grid population on client side
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
updateGrid(); // Call after DOM is fully loaded
|
|
});
|
|
</script>
|
|
</BaseLayout>
|
|
|
|
<style>
|
|
/* Hero Section */
|
|
.hero-section {
|
|
padding: 6rem 0 4rem;
|
|
position: relative;
|
|
overflow: hidden;
|
|
background: linear-gradient(180deg, var(--bg-secondary), var(--bg-primary));
|
|
}
|
|
|
|
.container {
|
|
max-width: 1280px;
|
|
margin: 0 auto;
|
|
padding: 0 clamp(1rem, 5vw, 3rem);
|
|
}
|
|
|
|
.hero-content {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4rem;
|
|
}
|
|
|
|
.hero-text {
|
|
flex: 1;
|
|
}
|
|
|
|
.hero-subtitle {
|
|
font-family: var(--font-mono);
|
|
color: var(--accent-primary);
|
|
font-size: var(--font-size-sm);
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
margin-bottom: 1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.hero-subtitle::before {
|
|
content: '>';
|
|
font-weight: bold;
|
|
}
|
|
|
|
.hero-title {
|
|
font-size: clamp(2.5rem, 5vw, 4rem);
|
|
line-height: 1.1;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.hero-title span {
|
|
background: linear-gradient(90deg, var(--accent-primary), var(--accent-tertiary));
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
color: transparent;
|
|
}
|
|
|
|
.hero-description {
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-lg);
|
|
margin-bottom: 1.5rem;
|
|
max-width: 540px;
|
|
}
|
|
|
|
.terminal-container {
|
|
flex: 1;
|
|
max-width: 560px;
|
|
}
|
|
|
|
/* Graph Section */
|
|
.graph-section {
|
|
padding: 5rem 0;
|
|
position: relative;
|
|
background: linear-gradient(0deg, var(--bg-primary), var(--bg-secondary), var(--bg-primary));
|
|
}
|
|
|
|
.section-header {
|
|
text-align: center;
|
|
max-width: 800px;
|
|
margin: 0 auto 3rem;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.section-description {
|
|
color: var(--text-secondary);
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.graph-container {
|
|
position: relative;
|
|
height: 60vh;
|
|
min-height: 500px;
|
|
max-height: 800px;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
border: 1px solid var(--card-border);
|
|
background: rgba(15, 23, 42, 0.2);
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.graph-controls {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
justify-content: center;
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
.graph-filter {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border-primary);
|
|
background: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--font-size-sm);
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.graph-filter:hover {
|
|
border-color: var(--accent-primary);
|
|
box-shadow: 0 0 10px var(--glow-primary);
|
|
}
|
|
|
|
.graph-filter.active {
|
|
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
|
border-color: transparent;
|
|
color: var(--bg-primary);
|
|
}
|
|
|
|
/* Blog Posts Section */
|
|
.blog-posts-section {
|
|
padding: 5rem 0;
|
|
}
|
|
|
|
.search-filter-section {
|
|
margin-bottom: 3rem;
|
|
padding: 1.5rem;
|
|
background: rgba(13, 21, 41, 0.5);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.search-bar {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.search-input {
|
|
width: 100%;
|
|
padding: 0.75rem 1rem;
|
|
background-color: var(--bg-primary);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 6px;
|
|
color: var(--text-primary);
|
|
font-size: 1rem;
|
|
font-family: var(--font-sans);
|
|
}
|
|
|
|
.search-input::placeholder {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.tag-filters {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.75rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.filter-label {
|
|
font-size: 0.9rem;
|
|
color: var(--text-secondary);
|
|
margin-right: 0.5rem;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.tag-filter-btn {
|
|
background-color: rgba(226, 232, 240, 0.05);
|
|
color: var(--text-secondary);
|
|
border: 1px solid var(--card-border);
|
|
padding: 0.4rem 0.8rem;
|
|
border-radius: 20px;
|
|
cursor: pointer;
|
|
font-size: 0.85rem;
|
|
transition: all 0.2s ease;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.tag-filter-btn:hover {
|
|
background-color: rgba(226, 232, 240, 0.1);
|
|
color: var(--text-primary);
|
|
border-color: rgba(56, 189, 248, 0.4);
|
|
}
|
|
|
|
.tag-filter-btn.active {
|
|
background-color: var(--accent-primary);
|
|
color: var(--bg-primary);
|
|
border-color: var(--accent-primary);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Styles for the integrated graph container */
|
|
.integrated-graph-container {
|
|
margin-top: 2rem; /* Add space above the graph */
|
|
height: 400px; /* Adjust height as needed */
|
|
border: 1px solid var(--border-primary);
|
|
border-radius: 8px;
|
|
background: rgba(15, 23, 42, 0.3); /* Slightly different background */
|
|
position: relative; /* Needed for Cytoscape */
|
|
overflow: hidden; /* Hide scrollbars if graph overflows */
|
|
}
|
|
|
|
.blog-grid {
|
|
margin: 2rem 0 4rem;
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 2rem;
|
|
}
|
|
|
|
/* Enhanced Post Card Styles */
|
|
.post-card {
|
|
height: 100%;
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.post-card-inner {
|
|
height: 100%;
|
|
background: var(--card-bg);
|
|
border-radius: 10px;
|
|
border: 1px solid var(--card-border);
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.post-card:hover .post-card-inner {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 10px 30px rgba(6, 182, 212, 0.15);
|
|
border-color: rgba(56, 189, 248, 0.4);
|
|
}
|
|
|
|
.post-image-container {
|
|
position: relative;
|
|
}
|
|
|
|
.post-image {
|
|
width: 100%;
|
|
height: 200px;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.post-category-badge {
|
|
position: absolute;
|
|
top: 15px;
|
|
right: 15px;
|
|
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
|
color: var(--bg-primary);
|
|
padding: 0.35rem 0.8rem;
|
|
border-radius: 20px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
box-shadow: 0 2px 10px rgba(6, 182, 212, 0.25);
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.post-content {
|
|
padding: 1.5rem;
|
|
flex-grow: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.post-meta {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
font-size: 0.85rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.post-title {
|
|
font-size: 1.25rem;
|
|
margin-bottom: 0.75rem;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.post-title a {
|
|
color: var(--text-primary);
|
|
text-decoration: none;
|
|
transition: color 0.3s ease;
|
|
}
|
|
|
|
.post-title a:hover {
|
|
color: var(--accent-primary);
|
|
}
|
|
|
|
.post-excerpt {
|
|
color: var(--text-secondary);
|
|
font-size: 0.9rem;
|
|
margin-bottom: 1.5rem;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.post-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-top: auto;
|
|
}
|
|
|
|
.post-tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.post-tag {
|
|
background: rgba(226, 232, 240, 0.05);
|
|
color: var(--text-secondary);
|
|
padding: 0.2rem 0.5rem;
|
|
border-radius: 4px;
|
|
font-size: 0.7rem;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.read-more {
|
|
color: var(--accent-primary);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
transition: color 0.3s ease;
|
|
}
|
|
|
|
.read-more:hover {
|
|
color: var(--accent-secondary);
|
|
}
|
|
|
|
.read-more::after {
|
|
content: '→';
|
|
}
|
|
|
|
.no-results {
|
|
grid-column: 1 / -1;
|
|
text-align: center;
|
|
padding: 3rem;
|
|
color: var(--text-secondary);
|
|
background: rgba(15, 23, 42, 0.2);
|
|
border-radius: 10px;
|
|
border: 1px dashed var(--card-border);
|
|
}
|
|
|
|
/* Loading Indicator */
|
|
.loading-indicator {
|
|
grid-column: 1 / -1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 3rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 3px solid rgba(6, 182, 212, 0.3);
|
|
border-radius: 50%;
|
|
border-top-color: var(--accent-primary);
|
|
animation: spin 1s linear infinite;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Responsive Adjustments */
|
|
@media (max-width: 1024px) {
|
|
.hero-content {
|
|
flex-direction: column;
|
|
gap: 2rem;
|
|
}
|
|
|
|
.terminal-container {
|
|
max-width: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
.graph-container {
|
|
height: 50vh;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.hero-section {
|
|
padding: 4rem 0 3rem;
|
|
}
|
|
|
|
.section-header {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.search-filter-section {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.blog-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style> |