fresh-main #6
|
@ -1,458 +1,505 @@
|
||||||
---
|
---
|
||||||
// Header.astro
|
// src/components/Header.astro
|
||||||
// Primary navigation component with premium design elements
|
import ThemeToggler from './ThemeToggler.astro';
|
||||||
|
|
||||||
// Get current path to highlight active nav item
|
// Define navigation items with proper URLs
|
||||||
const pathname = new URL(Astro.request.url).pathname;
|
|
||||||
const currentPath = pathname.split('/')[1]; // Get the first path segment
|
|
||||||
|
|
||||||
// Define navigation items
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ name: 'Home', path: '/' },
|
{ name: 'Home', url: '/' },
|
||||||
{ name: 'Blog', path: '/blog/' },
|
{ name: 'Blog', url: '/blog' },
|
||||||
{ name: 'Projects', path: '/projects/' },
|
{ name: 'Projects', url: '/projects' },
|
||||||
{ name: 'Home Lab', path: '/homelab/' },
|
{ name: 'Home Lab', url: 'https://argobox.com' },
|
||||||
{ name: 'Resources', path: '/resources/' },
|
{ name: 'Resources', url: '/resources' },
|
||||||
{ name: 'About', path: '/about/' },
|
{ name: 'About', url: 'https://laforceit.com' },
|
||||||
{ name: 'Contact', path: '/contact/' }
|
{ name: 'Contact', url: 'https://laforceit.com/index.html#contact' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Get current URL path for active nav item highlighting
|
||||||
|
const currentPath = Astro.url.pathname;
|
||||||
---
|
---
|
||||||
|
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<div class="nebula-bg"></div>
|
<div class="container header-container">
|
||||||
<div class="container">
|
<div class="logo-container">
|
||||||
<div class="header-container">
|
<a href="/" class="logo-link">
|
||||||
<a href="/" class="logo">
|
<div class="logo">LF</div>
|
||||||
<div class="logo-symbol">
|
<div class="site-name">
|
||||||
<span class="logo-text">LF</span>
|
<span class="site-title">LaForceIT</span>
|
||||||
<div class="logo-glow"></div>
|
<span class="site-subtitle">Infrastructure & Automation</span>
|
||||||
</div>
|
|
||||||
<div class="logo-text-container">
|
|
||||||
<span class="logo-name">LaForceIT</span>
|
|
||||||
<span class="logo-tagline">Infrastructure & Automation</span>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<nav>
|
<nav class="main-nav">
|
||||||
<div class="main-nav">
|
<ul class="nav-list">
|
||||||
{navItems.map((item) => (
|
{navItems.map(item => (
|
||||||
|
<li class="nav-item">
|
||||||
<a
|
<a
|
||||||
href={item.path}
|
href={item.url}
|
||||||
class={`nav-link ${currentPath === item.path.replace(/\//g, '') ? 'active' : ''}`}
|
class={`nav-link ${currentPath === item.url ||
|
||||||
|
(currentPath.startsWith(item.url) && item.url !== '/') ? 'active' : ''}`}
|
||||||
|
target={item.url.startsWith('http') ? '_blank' : undefined}
|
||||||
|
rel={item.url.startsWith('http') ? 'noopener noreferrer' : undefined}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
<div class="nav-highlight"></div>
|
|
||||||
</a>
|
</a>
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle dark mode">
|
<div class="search-container">
|
||||||
<svg class="icon-sun" 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">
|
<button class="search-toggle" aria-label="Toggle search">
|
||||||
<circle cx="12" cy="12" r="5"></circle>
|
|
||||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
|
||||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
|
||||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
|
||||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
|
||||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
|
||||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
|
||||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
|
||||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
|
||||||
</svg>
|
|
||||||
<svg class="icon-moon" 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">
|
|
||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button id="search-button" class="search-button" aria-label="Search">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="search-dropdown">
|
||||||
|
<div class="search-input-wrapper">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
id="header-search"
|
||||||
|
placeholder="Search blog posts..."
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
<button class="search-submit">
|
||||||
|
<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">
|
||||||
|
<polyline points="9 18 15 12 9 6"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="search-results" id="search-results">
|
||||||
|
<!-- Results will be populated by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ThemeToggler />
|
||||||
|
</div>
|
||||||
|
|
||||||
<button id="mobile-menu-btn" class="mobile-menu-btn" aria-label="Toggle menu">
|
<button class="mobile-menu-toggle" aria-label="Toggle menu">
|
||||||
<svg class="icon-menu" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<svg class="icon-close" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Animated network lines effect -->
|
|
||||||
<div class="network-lines"></div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.site-header {
|
.site-header {
|
||||||
background: linear-gradient(180deg, rgba(15, 18, 25, 0.9), rgba(13, 16, 23, 0.8));
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
padding: 1rem 0;
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
background: rgba(var(--bg-primary-rgb), 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid var(--border-primary);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-header.scrolled {
|
|
||||||
padding: 0.6rem 0;
|
|
||||||
background: rgba(10, 12, 20, 0.95);
|
|
||||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-container {
|
.header-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 var(--container-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
width: 40px;
|
||||||
align-items: center;
|
height: 40px;
|
||||||
gap: 0.75rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-symbol {
|
|
||||||
width: 2.75rem;
|
|
||||||
height: 2.75rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||||
overflow: hidden;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo:hover .logo-symbol {
|
|
||||||
transform: translateY(-3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-text {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
color: var(--bg-primary);
|
color: var(--bg-primary);
|
||||||
z-index: 2;
|
font-weight: bold;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
font-family: var(--font-sans);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-glow {
|
.site-name {
|
||||||
position: absolute;
|
|
||||||
width: 150%;
|
|
||||||
height: 150%;
|
|
||||||
background: radial-gradient(circle, var(--accent-primary) 0%, transparent 70%);
|
|
||||||
opacity: 0.5;
|
|
||||||
filter: blur(15px);
|
|
||||||
z-index: 1;
|
|
||||||
animation: pulse 4s infinite alternate ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-text-container {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-name {
|
.site-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.4rem;
|
font-size: 1.25rem;
|
||||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
color: var(--text-primary);
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-tagline {
|
.site-subtitle {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-tertiary);
|
||||||
font-family: var(--font-mono);
|
letter-spacing: 0.5px;
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-nav {
|
.main-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.5rem;
|
margin-left: auto;
|
||||||
align-items: center;
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
position: relative;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 500;
|
font-size: 0.9rem;
|
||||||
transition: color 0.3s ease;
|
padding: 0.5rem 0.75rem;
|
||||||
padding: 0.5rem 0;
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link:hover, .nav-link.active {
|
.nav-link:hover {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-highlight {
|
.nav-link.active {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active::after {
|
||||||
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -2px;
|
bottom: -2px;
|
||||||
left: 0;
|
left: 0.75rem;
|
||||||
width: 0;
|
right: 0.75rem;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
background: var(--accent-primary);
|
||||||
transition: width 0.3s ease;
|
border-radius: 1px;
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover .nav-highlight,
|
|
||||||
.nav-link.active .nav-highlight {
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 0.75rem;
|
||||||
margin-left: 2rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle,
|
/* Search Dropdown Styles */
|
||||||
.search-button {
|
.search-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-toggle {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-secondary);
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: all 0.3s ease;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle:hover,
|
.search-toggle:hover {
|
||||||
.search-button:hover {
|
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-sun {
|
.search-dropdown {
|
||||||
display: none;
|
position: absolute;
|
||||||
|
top: calc(100% + 0.5rem);
|
||||||
|
right: 0;
|
||||||
|
width: 300px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 0.75rem;
|
||||||
|
z-index: 10;
|
||||||
|
transform-origin: top right;
|
||||||
|
transform: scale(0.95);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.2s cubic-bezier(0.5, 0, 0, 1.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.light-mode) .icon-moon {
|
.search-dropdown.active {
|
||||||
display: none;
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.light-mode) .icon-sun {
|
.search-input-wrapper {
|
||||||
display: block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-menu-btn {
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.6rem 2.5rem 0.6rem 0.75rem;
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
box-shadow: 0 0 0 3px var(--glow-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-submit {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.5rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-submit:hover {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--border-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item:hover {
|
||||||
|
background: rgba(var(--bg-tertiary-rgb), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-snippet {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Menu Toggle */
|
||||||
|
.mobile-menu-toggle {
|
||||||
display: none;
|
display: none;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-primary);
|
color: var(--text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
padding: 0.5rem;
|
||||||
|
|
||||||
.icon-close {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu-active .icon-menu {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu-active .icon-close {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animated network lines effect */
|
|
||||||
.network-lines {
|
|
||||||
position: absolute;
|
|
||||||
bottom: -1px;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 1px;
|
|
||||||
overflow: hidden;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-lines::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, transparent 0%, var(--accent-primary) 50%, transparent 100%);
|
|
||||||
animation: network-scan 8s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Nebula background */
|
|
||||||
.nebula-bg {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-image:
|
|
||||||
radial-gradient(circle at 20% 50%, rgba(6, 182, 212, 0.1) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 80% 50%, rgba(139, 92, 246, 0.1) 0%, transparent 50%);
|
|
||||||
opacity: 0.3;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
opacity: 0.4;
|
|
||||||
transform: scale(0.8);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0.8;
|
|
||||||
transform: scale(1.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes network-scan {
|
|
||||||
0% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
/* Responsive Adjustments */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.main-nav {
|
.nav-link {
|
||||||
gap: 1rem;
|
padding: 0.5rem;
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.mobile-menu-btn {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-nav {
|
.main-nav {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
right: 0;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 1rem;
|
|
||||||
border-bottom: 1px solid var(--border-primary);
|
border-bottom: 1px solid var(--border-primary);
|
||||||
z-index: 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-nav.active {
|
.main-nav.active {
|
||||||
display: flex;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-tagline {
|
.nav-list {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
display: block;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle, .search-button {
|
.mobile-menu-toggle {
|
||||||
padding: 0.4rem;
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-dropdown {
|
||||||
|
width: 260px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Updated theme toggle script for Header.astro
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
// Mobile menu toggle
|
||||||
const menuBtn = document.getElementById('mobile-menu-btn'); // Added for mobile menu
|
const mobileMenuToggle = document.querySelector('.mobile-menu-toggle');
|
||||||
const mainNav = document.querySelector('.main-nav'); // Added for mobile menu
|
const mainNav = document.querySelector('.main-nav');
|
||||||
const header = document.querySelector('.site-header'); // Added for scroll effect
|
|
||||||
|
|
||||||
// Mobile menu toggle (Keep existing functionality)
|
mobileMenuToggle?.addEventListener('click', () => {
|
||||||
if (menuBtn && mainNav) {
|
mainNav?.classList.toggle('active');
|
||||||
menuBtn.addEventListener('click', () => {
|
|
||||||
mainNav.classList.toggle('active');
|
|
||||||
menuBtn.classList.toggle('mobile-menu-active');
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Header scroll effect (Keep existing functionality)
|
// Search dropdown toggle
|
||||||
if (header) { // Check if header exists
|
const searchToggle = document.querySelector('.search-toggle');
|
||||||
window.addEventListener('scroll', () => {
|
const searchDropdown = document.querySelector('.search-dropdown');
|
||||||
if (window.scrollY > 50) {
|
const searchInput = document.querySelector('#header-search');
|
||||||
header.classList.add('scrolled');
|
|
||||||
} else {
|
searchToggle?.addEventListener('click', (e) => {
|
||||||
header.classList.remove('scrolled');
|
e.stopPropagation();
|
||||||
|
searchDropdown?.classList.toggle('active');
|
||||||
|
|
||||||
|
if (searchDropdown?.classList.contains('active')) {
|
||||||
|
// Focus the search input when dropdown is shown
|
||||||
|
setTimeout(() => {
|
||||||
|
searchInput?.focus();
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Theme Toggle Logic
|
// Close search dropdown when clicking outside
|
||||||
if (themeToggle) {
|
document.addEventListener('click', (e) => {
|
||||||
// Force initial theme class application based on localStorage or preference
|
if (!e.target.closest('.search-container')) {
|
||||||
const savedTheme = localStorage.getItem('theme');
|
searchDropdown?.classList.remove('active');
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
|
|
||||||
// Apply light mode only if explicitly set or if preference is light
|
|
||||||
if (savedTheme === 'light' || (!savedTheme && !prefersDark)) {
|
|
||||||
document.documentElement.classList.add('light-mode');
|
|
||||||
console.log('Initial theme set to light');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('light-mode'); // Default to dark
|
|
||||||
console.log('Initial theme set to dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add click event that logs for debugging
|
|
||||||
themeToggle.addEventListener('click', () => {
|
|
||||||
// Check if light-mode class is *currently* present before toggling
|
|
||||||
const isCurrentlyLight = document.documentElement.classList.contains('light-mode');
|
|
||||||
console.log('Theme toggle clicked, current mode:', isCurrentlyLight ? 'light' : 'dark');
|
|
||||||
|
|
||||||
if (isCurrentlyLight) {
|
|
||||||
// Switch to dark
|
|
||||||
document.documentElement.classList.remove('light-mode');
|
|
||||||
localStorage.setItem('theme', 'dark');
|
|
||||||
console.log('Switched to dark mode');
|
|
||||||
} else {
|
|
||||||
// Switch to light
|
|
||||||
document.documentElement.classList.add('light-mode');
|
|
||||||
localStorage.setItem('theme', 'light');
|
|
||||||
console.log('Switched to light mode');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
console.warn("Theme toggle button not found.");
|
// Search functionality - client-side post filtering
|
||||||
|
const searchResults = document.getElementById('search-results');
|
||||||
|
|
||||||
|
// Function to perform search
|
||||||
|
const performSearch = async (query) => {
|
||||||
|
if (!query || query.length < 2) {
|
||||||
|
// Clear results if query is too short
|
||||||
|
if (searchResults) {
|
||||||
|
searchResults.innerHTML = '';
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add interactive network nodes animation (Keep existing functionality)
|
try {
|
||||||
const header_el = document.querySelector('.site-header');
|
// This would ideally be a server-side search or a pre-built index
|
||||||
if (header_el) {
|
// For now, we'll just fetch all posts and filter client-side
|
||||||
for (let i = 0; i < 5; i++) {
|
const response = await fetch('/search-index.json');
|
||||||
const node = document.createElement('div');
|
if (!response.ok) throw new Error('Failed to fetch search data');
|
||||||
node.className = 'nav-node'; // Ensure this class exists in your CSS
|
|
||||||
node.style.left = `${Math.random() * 100}%`;
|
const posts = await response.json();
|
||||||
node.style.animationDelay = `${Math.random() * 5}s`;
|
const results = posts.filter(post => {
|
||||||
node.style.animationDuration = `${5 + Math.random() * 5}s`;
|
const lowerQuery = query.toLowerCase();
|
||||||
header_el.appendChild(node);
|
return (
|
||||||
|
post.title.toLowerCase().includes(lowerQuery) ||
|
||||||
|
post.description?.toLowerCase().includes(lowerQuery) ||
|
||||||
|
post.tags?.some(tag => tag.toLowerCase().includes(lowerQuery))
|
||||||
|
);
|
||||||
|
}).slice(0, 5); // Limit to 5 results
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Add click handlers to results
|
||||||
|
document.querySelectorAll('.search-result-item').forEach(item => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
window.location.href = item.dataset.url;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
searchResults.innerHTML = '<div class="no-results">No matching posts found</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Search error:', error);
|
||||||
|
if (searchResults) {
|
||||||
|
searchResults.innerHTML = '<div class="no-results">Error performing search</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Search input event handler
|
||||||
|
let searchTimeout;
|
||||||
|
searchInput?.addEventListener('input', (e) => {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
performSearch(e.target.value);
|
||||||
|
}, 300); // Debounce to avoid too many searches while typing
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle search form submission
|
||||||
|
const searchForm = searchInput?.closest('form');
|
||||||
|
searchForm?.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
performSearch(searchInput.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle search-submit button click
|
||||||
|
const searchSubmit = document.querySelector('.search-submit');
|
||||||
|
searchSubmit?.addEventListener('click', () => {
|
||||||
|
performSearch(searchInput?.value || '');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
File diff suppressed because it is too large
Load Diff
|
@ -47,25 +47,25 @@
|
||||||
transition: transform 0.5s ease, opacity 0.5s ease;
|
transition: transform 0.5s ease, opacity 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
html:not(.dark) .sun-icon {
|
:root:not(.light-mode) .sun-icon {
|
||||||
opacity: 1;
|
|
||||||
transform: rotate(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
html:not(.dark) .moon-icon {
|
|
||||||
opacity: 0;
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .sun-icon {
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark .moon-icon {
|
:root:not(.light-mode) .moon-icon {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: rotate(0);
|
transform: rotate(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root.light-mode .sun-icon {
|
||||||
|
opacity: 1;
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode .moon-icon {
|
||||||
|
opacity: 0;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -73,21 +73,34 @@
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
|
||||||
|
// Check for saved theme preference or prefer-color-scheme
|
||||||
|
const getInitialTheme = () => {
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) {
|
||||||
|
return savedTheme === 'light';
|
||||||
|
}
|
||||||
|
// If no saved preference, check system preference
|
||||||
|
return window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||||
|
};
|
||||||
|
|
||||||
// Function to set theme
|
// Function to set theme
|
||||||
const setTheme = (isDark) => {
|
const setTheme = (isLight) => {
|
||||||
if (isDark) {
|
if (isLight) {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('light-mode');
|
||||||
localStorage.setItem('theme', 'dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
localStorage.setItem('theme', 'light');
|
localStorage.setItem('theme', 'light');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('light-mode');
|
||||||
|
localStorage.setItem('theme', 'dark');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Apply initial theme
|
||||||
|
setTheme(getInitialTheme());
|
||||||
|
|
||||||
// Theme toggle click handler
|
// Theme toggle click handler
|
||||||
themeToggle?.addEventListener('click', () => {
|
themeToggle?.addEventListener('click', () => {
|
||||||
const isDark = document.documentElement.classList.contains('dark');
|
const isCurrentlyLight = document.documentElement.classList.contains('light-mode');
|
||||||
setTheme(!isDark);
|
setTheme(!isCurrentlyLight);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
|
@ -1,98 +1,100 @@
|
||||||
|
// src/content/config.ts
|
||||||
import { defineCollection, z } from 'astro:content';
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
|
||||||
// Define custom date validator that handles multiple formats
|
// Define the post collection schema
|
||||||
const customDateParser = (dateString: string | Date | null | undefined) => {
|
|
||||||
// Handle null/undefined
|
|
||||||
if (dateString === null || dateString === undefined) {
|
|
||||||
return new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If date is already a Date object, return it
|
|
||||||
if (dateString instanceof Date) {
|
|
||||||
return dateString;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to parse the date as is
|
|
||||||
let date = new Date(dateString);
|
|
||||||
|
|
||||||
// For format like "Jul 22 2023"
|
|
||||||
if (isNaN(date.getTime()) && typeof dateString === 'string') {
|
|
||||||
try {
|
|
||||||
// Try various formats
|
|
||||||
if (dateString.match(/^\d{2}\/\d{2}\/\d{4}$/)) {
|
|
||||||
const [month, day, year] = dateString.split('/').map(Number);
|
|
||||||
date = new Date(year, month - 1, day);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Default to current date if all parsing fails
|
|
||||||
date = new Date();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return date;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define the base schema for all content
|
|
||||||
const baseSchema = z.object({
|
|
||||||
title: z.string(),
|
|
||||||
description: z.string().optional(),
|
|
||||||
pubDate: z.union([z.string(), z.date(), z.null()]).optional().default(() => new Date()).transform(customDateParser),
|
|
||||||
updatedDate: z.union([z.string(), z.date(), z.null()]).optional().transform(val => val ? customDateParser(val) : undefined),
|
|
||||||
heroImage: z.string().optional().nullable(),
|
|
||||||
// Add categories array that falls back to the single category field
|
|
||||||
categories: z.union([
|
|
||||||
z.array(z.string()),
|
|
||||||
z.string().transform(val => [val]),
|
|
||||||
z.null()
|
|
||||||
]).optional().transform(val => {
|
|
||||||
if (val === null || val === undefined) return ['Uncategorized'];
|
|
||||||
return val;
|
|
||||||
}),
|
|
||||||
// Keep the original category field for backward compatibility
|
|
||||||
category: z.string().optional().default('Uncategorized'),
|
|
||||||
tags: z.union([z.array(z.string()), z.null()]).optional().default([]),
|
|
||||||
draft: z.boolean().optional().default(false),
|
|
||||||
readTime: z.union([z.string(), z.number()]).optional(),
|
|
||||||
image: z.string().optional(),
|
|
||||||
excerpt: z.string().optional(),
|
|
||||||
author: z.string().optional(),
|
|
||||||
github: z.string().optional(),
|
|
||||||
live: z.string().optional(),
|
|
||||||
technologies: z.array(z.string()).optional(),
|
|
||||||
}).passthrough() // Allow any other frontmatter properties
|
|
||||||
.transform(data => {
|
|
||||||
// If categories isn't set but category is, use category value to populate categories
|
|
||||||
if ((!data.categories || data.categories.length === 0) && data.category) {
|
|
||||||
data.categories = [data.category];
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define collections using the same base schema
|
|
||||||
const postsCollection = defineCollection({
|
const postsCollection = defineCollection({
|
||||||
type: 'content',
|
type: 'content',
|
||||||
schema: baseSchema,
|
schema: z.object({
|
||||||
});
|
title: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
const configurationsCollection = defineCollection({
|
pubDate: z.coerce.date(),
|
||||||
type: 'content',
|
updatedDate: z.coerce.date().optional(),
|
||||||
schema: baseSchema,
|
heroImage: z.string().optional(),
|
||||||
});
|
|
||||||
|
// Support both single category and categories array
|
||||||
const projectsCollection = defineCollection({
|
category: z.string().optional(),
|
||||||
type: 'content',
|
categories: z.array(z.string()).optional(),
|
||||||
schema: baseSchema,
|
|
||||||
|
// Tags as an array
|
||||||
|
tags: z.array(z.string()).default([]),
|
||||||
|
|
||||||
|
// Author and reading time
|
||||||
|
author: z.string().optional(),
|
||||||
|
readTime: z.string().optional(),
|
||||||
|
|
||||||
|
// Draft status
|
||||||
|
draft: z.boolean().optional().default(false),
|
||||||
|
|
||||||
|
// Related posts by slug
|
||||||
|
related_posts: z.array(z.string()).optional(),
|
||||||
|
|
||||||
|
// Additional metadata
|
||||||
|
featured: z.boolean().optional().default(false),
|
||||||
|
technologies: z.array(z.string()).optional(),
|
||||||
|
complexity: z.enum(['beginner', 'intermediate', 'advanced']).optional(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Define the external posts collection (for external articles)
|
||||||
const externalPostsCollection = defineCollection({
|
const externalPostsCollection = defineCollection({
|
||||||
type: 'content',
|
type: 'content',
|
||||||
schema: baseSchema,
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
pubDate: z.coerce.date(),
|
||||||
|
url: z.string().url(),
|
||||||
|
heroImage: z.string().optional(),
|
||||||
|
source: z.string().optional(),
|
||||||
|
category: z.string().optional(),
|
||||||
|
categories: z.array(z.string()).optional(),
|
||||||
|
tags: z.array(z.string()).default([]),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define the configurations collection (for config files)
|
||||||
|
const configurationsCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
pubDate: z.coerce.date(),
|
||||||
|
updatedDate: z.coerce.date().optional(),
|
||||||
|
heroImage: z.string().optional(),
|
||||||
|
category: z.string().optional(),
|
||||||
|
categories: z.array(z.string()).optional(),
|
||||||
|
tags: z.array(z.string()).default([]),
|
||||||
|
technologies: z.array(z.string()).optional(),
|
||||||
|
complexity: z.enum(['beginner', 'intermediate', 'advanced']).optional(),
|
||||||
|
draft: z.boolean().optional().default(false),
|
||||||
|
version: z.string().optional(),
|
||||||
|
github: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define the projects collection
|
||||||
|
const projectsCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
pubDate: z.coerce.date(),
|
||||||
|
updatedDate: z.coerce.date().optional(),
|
||||||
|
heroImage: z.string().optional(),
|
||||||
|
category: z.string().optional(),
|
||||||
|
categories: z.array(z.string()).optional(),
|
||||||
|
tags: z.array(z.string()).default([]),
|
||||||
|
technologies: z.array(z.string()).optional(),
|
||||||
|
github: z.string().optional(),
|
||||||
|
website: z.string().optional(),
|
||||||
|
status: z.enum(['concept', 'in-progress', 'completed', 'maintained']).optional(),
|
||||||
|
draft: z.boolean().optional().default(false),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Export the collections
|
// Export the collections
|
||||||
export const collections = {
|
export const collections = {
|
||||||
'posts': postsCollection,
|
posts: postsCollection,
|
||||||
'configurations': configurationsCollection,
|
|
||||||
'projects': projectsCollection,
|
|
||||||
'external-posts': externalPostsCollection,
|
'external-posts': externalPostsCollection,
|
||||||
|
configurations: configurationsCollection,
|
||||||
|
projects: projectsCollection,
|
||||||
};
|
};
|
|
@ -3,7 +3,7 @@ title: "Projects Collection"
|
||||||
description: "A placeholder document for the projects collection"
|
description: "A placeholder document for the projects collection"
|
||||||
heroImage: "/blog/images/placeholders/default.jpg"
|
heroImage: "/blog/images/placeholders/default.jpg"
|
||||||
pubDate: 2025-04-18
|
pubDate: 2025-04-18
|
||||||
status: "planning"
|
status: "concept" # Changed from 'planning' to match schema
|
||||||
tech: ["astro", "markdown"]
|
tech: ["astro", "markdown"]
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
// blog/index.astro - Blog page with knowledge graph and filtering
|
// src/pages/blog/index.astro - Blog page with enhanced knowledge graph and filtering
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
import KnowledgeGraph from '../../components/KnowledgeGraph.astro';
|
import KnowledgeGraph from '../../components/KnowledgeGraph.astro';
|
||||||
|
@ -37,38 +37,64 @@ const postsData = sortedPosts.map(post => ({
|
||||||
isDraft: post.data.draft || false
|
isDraft: post.data.draft || false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Prepare graph data for visualization
|
// Prepare enhanced graph data with both posts and tags
|
||||||
const graphData = {
|
const graphData = {
|
||||||
nodes: sortedPosts
|
nodes: [
|
||||||
|
// Add post nodes
|
||||||
|
...sortedPosts
|
||||||
.filter(post => !post.data.draft)
|
.filter(post => !post.data.draft)
|
||||||
.map(post => ({
|
.map(post => ({
|
||||||
id: post.slug,
|
id: post.slug,
|
||||||
label: post.data.title,
|
label: post.data.title,
|
||||||
|
type: 'post',
|
||||||
category: post.data.category || 'Uncategorized',
|
category: post.data.category || 'Uncategorized',
|
||||||
tags: post.data.tags || []
|
tags: post.data.tags || [],
|
||||||
|
url: `/posts/${post.slug}/`
|
||||||
})),
|
})),
|
||||||
|
|
||||||
|
// Add tag nodes
|
||||||
|
...allTags.map(tag => ({
|
||||||
|
id: `tag-${tag}`,
|
||||||
|
label: tag,
|
||||||
|
type: 'tag',
|
||||||
|
url: `/tag/${tag}/`
|
||||||
|
}))
|
||||||
|
],
|
||||||
edges: []
|
edges: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create edges between posts based on shared tags
|
// Create edges between posts and their tags
|
||||||
for (let i = 0; i < graphData.nodes.length; i++) {
|
sortedPosts
|
||||||
const postA = graphData.nodes[i];
|
.filter(post => !post.data.draft)
|
||||||
|
.forEach(post => {
|
||||||
|
const postTags = post.data.tags || [];
|
||||||
|
|
||||||
for (let j = i + 1; j < graphData.nodes.length; j++) {
|
// Add edges from post to tags
|
||||||
const postB = graphData.nodes[j];
|
postTags.forEach(tag => {
|
||||||
|
|
||||||
// Create edge if posts share at least one tag or same category
|
|
||||||
const sharedTags = postA.tags.filter(tag => postB.tags.includes(tag));
|
|
||||||
|
|
||||||
if (sharedTags.length > 0 || postA.category === postB.category) {
|
|
||||||
graphData.edges.push({
|
graphData.edges.push({
|
||||||
source: postA.id,
|
source: post.slug,
|
||||||
target: postB.id,
|
target: `tag-${tag}`,
|
||||||
strength: sharedTags.length + (postA.category === postB.category ? 1 : 0)
|
type: 'post-tag',
|
||||||
|
strength: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if post references other posts (optional)
|
||||||
|
// This requires a related_posts field in frontmatter
|
||||||
|
if (post.data.related_posts && Array.isArray(post.data.related_posts)) {
|
||||||
|
post.data.related_posts.forEach(relatedSlug => {
|
||||||
|
// Make sure related post exists
|
||||||
|
if (sortedPosts.some(p => p.slug === relatedSlug)) {
|
||||||
|
graphData.edges.push({
|
||||||
|
source: post.slug,
|
||||||
|
target: relatedSlug,
|
||||||
|
type: 'post-post',
|
||||||
|
strength: 2
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// Terminal commands for tech effect
|
// Terminal commands for tech effect
|
||||||
const commands = [
|
const commands = [
|
||||||
|
@ -79,8 +105,8 @@ const commands = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prompt: "[laforceit@argobox]$ ",
|
prompt: "[laforceit@argobox]$ ",
|
||||||
command: "ls -la ./categories",
|
command: "ls -la ./tags",
|
||||||
output: allCategories.map(cat => `${cat}`)
|
output: allTags.map(tag => `${tag}`)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prompt: "[laforceit@argobox]$ ",
|
prompt: "[laforceit@argobox]$ ",
|
||||||
|
@ -117,42 +143,26 @@ const commands = [
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Blog Content Section -->
|
||||||
|
<section class="blog-content-section">
|
||||||
|
<div class="container">
|
||||||
|
<!-- Search and Filter Section with integrated Knowledge Graph -->
|
||||||
|
<div class="search-filter-container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Knowledge Graph & Content Explorer</h2>
|
||||||
|
<p class="section-description">
|
||||||
|
Explore connections between articles and topics, or search by keyword
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Knowledge Graph Visualization -->
|
<!-- Knowledge Graph Visualization -->
|
||||||
<section class="graph-section">
|
<div class="knowledge-graph-wrapper">
|
||||||
<div class="container">
|
<KnowledgeGraph graphData={graphData} height="500px" />
|
||||||
<div class="section-header">
|
|
||||||
<h2 class="section-title">Knowledge Graph</h2>
|
|
||||||
<p class="section-description">
|
|
||||||
Explore connections between articles based on topics and categories
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="graph-container">
|
|
||||||
<KnowledgeGraph graphData={graphData} />
|
|
||||||
|
|
||||||
<div class="graph-controls">
|
|
||||||
<button class="graph-filter active" data-filter="all">All Topics</button>
|
|
||||||
{allCategories.map(category => (
|
|
||||||
<button class="graph-filter" data-filter={category}>{category}</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Blog Posts Section -->
|
|
||||||
<section class="blog-posts-section">
|
|
||||||
<div class="container">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2 class="section-title">All 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-filter-section">
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
|
<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" class="search-icon"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||||
<input type="search" id="search-input" placeholder="Search posts..." class="search-input" />
|
<input type="search" id="search-input" placeholder="Search posts..." class="search-input" />
|
||||||
</div>
|
</div>
|
||||||
<div class="tag-filters">
|
<div class="tag-filters">
|
||||||
|
@ -163,8 +173,17 @@ const commands = [
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Blog Grid (populated by JS) -->
|
||||||
|
<div class="blog-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">All Articles</h2>
|
||||||
|
<p class="section-description">
|
||||||
|
Technical insights, infrastructure guides, and DevOps best practices
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Blog Grid (will be populated by JS) -->
|
|
||||||
<div class="blog-grid" id="blog-grid">
|
<div class="blog-grid" id="blog-grid">
|
||||||
<div class="loading-indicator">
|
<div class="loading-indicator">
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"></div>
|
||||||
|
@ -172,6 +191,7 @@ const commands = [
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<Footer slot="footer" />
|
<Footer slot="footer" />
|
||||||
|
@ -182,81 +202,18 @@ const commands = [
|
||||||
const searchInput = document.getElementById('search-input');
|
const searchInput = document.getElementById('search-input');
|
||||||
const tagButtons = document.querySelectorAll('.tag-filter-btn');
|
const tagButtons = document.querySelectorAll('.tag-filter-btn');
|
||||||
const blogGrid = document.getElementById('blog-grid');
|
const blogGrid = document.getElementById('blog-grid');
|
||||||
const graphFilters = document.querySelectorAll('.graph-filter');
|
|
||||||
|
|
||||||
// State variables
|
// State variables
|
||||||
let currentFilterTag = 'all';
|
let currentFilterTag = 'all';
|
||||||
let currentSearchTerm = '';
|
let currentSearchTerm = '';
|
||||||
let currentGraphFilter = 'all';
|
|
||||||
let cy; // Cytoscape instance will be set by KnowledgeGraph component
|
let cy; // Cytoscape instance will be set by KnowledgeGraph component
|
||||||
|
|
||||||
// Wait for cytoscape instance to be available
|
// Wait for cytoscape instance to be available
|
||||||
document.addEventListener('graphReady', (e) => {
|
document.addEventListener('graphReady', (e) => {
|
||||||
cy = e.detail.cy;
|
cy = e.detail.cy;
|
||||||
setupGraphInteractions();
|
console.log('Graph ready and connected to filtering system');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup graph filtering and interactions
|
|
||||||
function setupGraphInteractions() {
|
|
||||||
if (!cy) return;
|
|
||||||
|
|
||||||
// Graph filtering by category
|
|
||||||
graphFilters.forEach(button => {
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
// Update active button style
|
|
||||||
graphFilters.forEach(btn => btn.classList.remove('active'));
|
|
||||||
button.classList.add('active');
|
|
||||||
|
|
||||||
// Update filter
|
|
||||||
currentGraphFilter = button.dataset.filter;
|
|
||||||
|
|
||||||
// Apply filter to graph
|
|
||||||
if (currentGraphFilter === 'all') {
|
|
||||||
cy.elements().removeClass('faded').removeClass('highlighted');
|
|
||||||
} else {
|
|
||||||
// Fade all nodes/edges
|
|
||||||
cy.elements().addClass('faded');
|
|
||||||
|
|
||||||
// Highlight nodes with matching category and their edges
|
|
||||||
const matchingNodes = cy.nodes().filter(node =>
|
|
||||||
node.data('category') === currentGraphFilter
|
|
||||||
);
|
|
||||||
|
|
||||||
matchingNodes.removeClass('faded').addClass('highlighted');
|
|
||||||
matchingNodes.connectedEdges().removeClass('faded').addClass('highlighted');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click node to filter posts
|
|
||||||
cy.on('tap', 'node', function(evt) {
|
|
||||||
const node = evt.target;
|
|
||||||
const slug = node.id();
|
|
||||||
|
|
||||||
// Scroll to the post in the blog grid
|
|
||||||
const post = postsData.find(p => p.slug === slug);
|
|
||||||
if (post) {
|
|
||||||
// Reset filters
|
|
||||||
currentFilterTag = 'all';
|
|
||||||
searchInput.value = post.title;
|
|
||||||
currentSearchTerm = post.title;
|
|
||||||
|
|
||||||
// Update UI
|
|
||||||
tagButtons.forEach(btn => btn.classList.remove('active'));
|
|
||||||
tagButtons[0].classList.add('active');
|
|
||||||
|
|
||||||
// Update grid with just this post
|
|
||||||
updateGrid();
|
|
||||||
|
|
||||||
// Scroll to blog section
|
|
||||||
document.querySelector('.blog-posts-section').scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'start'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to create HTML for a single post card
|
// Function to create HTML for a single post card
|
||||||
function createPostCardHTML(post) {
|
function createPostCardHTML(post) {
|
||||||
// Make sure tags is an array before stringifying
|
// Make sure tags is an array before stringifying
|
||||||
|
@ -264,12 +221,13 @@ const commands = [
|
||||||
|
|
||||||
// Create tag pills HTML
|
// Create tag pills HTML
|
||||||
const tagPills = post.tags.map(tag =>
|
const tagPills = post.tags.map(tag =>
|
||||||
`<span class="post-tag">${tag}</span>`
|
`<span class="post-tag" data-tag="${tag}">${tag}</span>`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<article class="post-card" data-tags='${tagsString}' data-slug="${post.slug}">
|
<article class="post-card" data-tags='${tagsString}' data-slug="${post.slug}">
|
||||||
<div class="post-card-inner">
|
<div class="post-card-inner">
|
||||||
|
<a href="/posts/${post.slug}/" class="post-image-link">
|
||||||
<div class="post-image-container">
|
<div class="post-image-container">
|
||||||
<img
|
<img
|
||||||
width="720"
|
width="720"
|
||||||
|
@ -281,6 +239,7 @@ const commands = [
|
||||||
/>
|
/>
|
||||||
<div class="post-category-badge">${post.category}</div>
|
<div class="post-category-badge">${post.category}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
<div class="post-content">
|
<div class="post-content">
|
||||||
<div class="post-meta">
|
<div class="post-meta">
|
||||||
<time datetime="${post.pubDateISO}">${post.pubDate}</time>
|
<time datetime="${post.pubDateISO}">${post.pubDate}</time>
|
||||||
|
@ -321,7 +280,7 @@ const commands = [
|
||||||
post.title.toLowerCase().includes(searchTermLower) ||
|
post.title.toLowerCase().includes(searchTermLower) ||
|
||||||
post.description.toLowerCase().includes(searchTermLower) ||
|
post.description.toLowerCase().includes(searchTermLower) ||
|
||||||
postTags.some(tag => tag.toLowerCase().includes(searchTermLower));
|
postTags.some(tag => tag.toLowerCase().includes(searchTermLower));
|
||||||
return matchesTag && matchesSearch && !post.isDraft; // Exclude drafts
|
return matchesTag && matchesSearch;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the grid HTML
|
// Update the grid HTML
|
||||||
|
@ -329,29 +288,73 @@ const commands = [
|
||||||
if (filteredPosts.length > 0) {
|
if (filteredPosts.length > 0) {
|
||||||
blogGrid.innerHTML = filteredPosts.map(createPostCardHTML).join('');
|
blogGrid.innerHTML = filteredPosts.map(createPostCardHTML).join('');
|
||||||
|
|
||||||
// If graph is available, highlight matching nodes
|
// Add click handlers to post tag spans
|
||||||
if (cy) {
|
document.querySelectorAll('.post-tag').forEach(tagSpan => {
|
||||||
const matchingSlugs = filteredPosts.map(post => post.slug);
|
tagSpan.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const tag = tagSpan.dataset.tag;
|
||||||
|
|
||||||
// Reset all nodes
|
// Find and click the matching tag filter button
|
||||||
cy.nodes().removeClass('highlighted').removeClass('filtered');
|
const tagBtn = Array.from(tagButtons).find(btn => btn.dataset.tag === tag);
|
||||||
|
if (tagBtn) {
|
||||||
// Highlight matching nodes
|
tagBtn.click();
|
||||||
matchingSlugs.forEach(slug => {
|
|
||||||
cy.getElementById(slug).addClass('highlighted');
|
|
||||||
});
|
|
||||||
|
|
||||||
// If filtering by tag, also highlight connected nodes
|
|
||||||
if (currentFilterTag !== 'all') {
|
|
||||||
cy.nodes().forEach(node => {
|
|
||||||
if (node.data('tags')?.includes(currentFilterTag)) {
|
|
||||||
node.addClass('filtered');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// If graph is available, highlight matching nodes
|
||||||
|
if (cy) {
|
||||||
|
// Get matching slugs for posts
|
||||||
|
const matchingSlugs = filteredPosts.map(post => post.slug);
|
||||||
|
|
||||||
|
if (currentFilterTag !== 'all') {
|
||||||
|
// We're filtering by tag - highlight tag node and connected posts
|
||||||
|
cy.elements().addClass('faded').removeClass('highlighted filtered');
|
||||||
|
|
||||||
|
// Highlight the tag node
|
||||||
|
const tagNode = cy.getElementById(`tag-${currentFilterTag}`);
|
||||||
|
if (tagNode.length > 0) {
|
||||||
|
tagNode.removeClass('faded').addClass('highlighted');
|
||||||
|
|
||||||
|
// Get connected posts and highlight them
|
||||||
|
const connectedPosts = tagNode.neighborhood('node[type="post"]');
|
||||||
|
connectedPosts.removeClass('faded').addClass('filtered');
|
||||||
|
|
||||||
|
// Highlight connecting edges
|
||||||
|
tagNode.connectedEdges().removeClass('faded').addClass('highlighted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (currentSearchTerm) {
|
||||||
|
// We're searching - highlight matching posts
|
||||||
|
cy.elements().addClass('faded').removeClass('highlighted filtered');
|
||||||
|
|
||||||
|
// Find and highlight matching post nodes
|
||||||
|
matchingSlugs.forEach(slug => {
|
||||||
|
const node = cy.getElementById(slug);
|
||||||
|
if (node.length > 0) {
|
||||||
|
node.removeClass('faded').addClass('highlighted');
|
||||||
|
|
||||||
|
// Also show connected tags
|
||||||
|
const connectedTags = node.neighborhood('node[type="tag"]');
|
||||||
|
connectedTags.removeClass('faded').addClass('filtered');
|
||||||
|
|
||||||
|
// And highlight edges
|
||||||
|
node.connectedEdges().removeClass('faded');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Reset graph view
|
||||||
|
cy.elements().removeClass('faded highlighted filtered');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blogGrid.innerHTML = '<p class="no-results">No posts found matching your criteria.</p>';
|
blogGrid.innerHTML = '<p class="no-results">No posts found matching your criteria. Try adjusting your search or filters.</p>';
|
||||||
|
|
||||||
|
// Reset graph view
|
||||||
|
if (cy) {
|
||||||
|
cy.elements().removeClass('faded highlighted filtered');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error("Blog grid element not found!");
|
console.error("Blog grid element not found!");
|
||||||
|
@ -378,12 +381,53 @@ const commands = [
|
||||||
// Update filter and grid
|
// Update filter and grid
|
||||||
currentFilterTag = button.dataset.tag;
|
currentFilterTag = button.dataset.tag;
|
||||||
updateGrid();
|
updateGrid();
|
||||||
|
|
||||||
|
// If tag changes but search is active, keep it integrated
|
||||||
|
if (cy && currentFilterTag !== 'all') {
|
||||||
|
// Find the tag node
|
||||||
|
const tagNode = cy.getElementById(`tag-${currentFilterTag}`);
|
||||||
|
if (tagNode.length > 0) {
|
||||||
|
// Center the view on this tag
|
||||||
|
cy.animate({
|
||||||
|
center: { eles: tagNode },
|
||||||
|
zoom: 1.5
|
||||||
|
}, {
|
||||||
|
duration: 500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial grid population on client side
|
// Initial grid population on client side
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
updateGrid(); // Call after DOM is fully loaded
|
updateGrid(); // Call after DOM is fully loaded
|
||||||
|
|
||||||
|
// Create link between graph and grid
|
||||||
|
document.addEventListener('graphReady', (e) => {
|
||||||
|
// Add a scroll-to-graph button
|
||||||
|
const searchSection = document.querySelector('.search-filter-section');
|
||||||
|
if (searchSection) {
|
||||||
|
const graphButton = document.createElement('button');
|
||||||
|
graphButton.className = 'graph-toggle-btn';
|
||||||
|
graphButton.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">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
<line x1="11" y1="8" x2="11" y2="14"></line>
|
||||||
|
<line x1="8" y1="11" x2="14" y2="11"></line>
|
||||||
|
</svg>
|
||||||
|
Explore Knowledge Graph
|
||||||
|
`;
|
||||||
|
graphButton.addEventListener('click', () => {
|
||||||
|
document.querySelector('.knowledge-graph-wrapper').scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
searchSection.appendChild(graphButton);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
@ -481,17 +525,15 @@ const commands = [
|
||||||
max-width: 560px;
|
max-width: 560px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Graph Section */
|
/* Blog Content Section */
|
||||||
.graph-section {
|
.blog-content-section {
|
||||||
padding: 5rem 0;
|
padding: 2rem 0 5rem;
|
||||||
position: relative;
|
|
||||||
background: linear-gradient(0deg, var(--bg-primary), var(--bg-secondary), var(--bg-primary));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto 3rem;
|
margin: 0 auto 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
|
@ -519,75 +561,56 @@ const commands = [
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-container {
|
/* Search Filter Container with Knowledge Graph */
|
||||||
position: relative;
|
.search-filter-container {
|
||||||
height: 60vh;
|
margin-bottom: 4rem;
|
||||||
min-height: 500px;
|
background: rgba(15, 23, 42, 0.3);
|
||||||
max-height: 800px;
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
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);
|
border: 1px solid var(--border-primary);
|
||||||
background: var(--bg-secondary);
|
overflow: hidden;
|
||||||
color: var(--text-primary);
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-filter:hover {
|
.knowledge-graph-wrapper {
|
||||||
border-color: var(--accent-primary);
|
width: 100%;
|
||||||
box-shadow: 0 0 10px var(--glow-primary);
|
padding: 0;
|
||||||
}
|
margin: 0 0 1.5rem;
|
||||||
|
|
||||||
.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 {
|
.search-filter-section {
|
||||||
margin-bottom: 3rem;
|
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
background: rgba(13, 21, 41, 0.5);
|
position: relative;
|
||||||
border: 1px solid var(--card-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 1rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem 0.75rem 2.5rem;
|
||||||
background-color: var(--bg-primary);
|
background-color: var(--bg-primary);
|
||||||
border: 1px solid var(--card-border);
|
border: 1px solid var(--card-border);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.2);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input::placeholder {
|
.search-input::placeholder {
|
||||||
|
@ -624,6 +647,7 @@ const commands = [
|
||||||
background-color: rgba(226, 232, 240, 0.1);
|
background-color: rgba(226, 232, 240, 0.1);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border-color: rgba(56, 189, 248, 0.4);
|
border-color: rgba(56, 189, 248, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-filter-btn.active {
|
.tag-filter-btn.active {
|
||||||
|
@ -633,8 +657,36 @@ const commands = [
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-toggle-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.5rem;
|
||||||
|
right: 1.5rem;
|
||||||
|
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||||
|
color: var(--bg-primary);
|
||||||
|
border: none;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 10px rgba(6, 182, 212, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-toggle-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 15px rgba(6, 182, 212, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blog Section and Grid */
|
||||||
|
.blog-section {
|
||||||
|
margin-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
.blog-grid {
|
.blog-grid {
|
||||||
margin: 2rem 0 4rem;
|
margin: 2rem 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
|
@ -666,6 +718,11 @@ const commands = [
|
||||||
border-color: rgba(56, 189, 248, 0.4);
|
border-color: rgba(56, 189, 248, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-image-link {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.post-image-container {
|
.post-image-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -674,6 +731,11 @@ const commands = [
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card:hover .post-image {
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-category-badge {
|
.post-category-badge {
|
||||||
|
@ -722,6 +784,18 @@ const commands = [
|
||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.draft-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: rgba(245, 158, 11, 0.2);
|
||||||
|
color: #F59E0B;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
.post-excerpt {
|
.post-excerpt {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
@ -747,12 +821,19 @@ const commands = [
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-tag {
|
.post-tag {
|
||||||
background: rgba(226, 232, 240, 0.05);
|
background: rgba(16, 185, 129, 0.1);
|
||||||
color: var(--text-secondary);
|
color: #10B981;
|
||||||
padding: 0.2rem 0.5rem;
|
padding: 0.2rem 0.5rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-tag:hover {
|
||||||
|
background: rgba(16, 185, 129, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.read-more {
|
.read-more {
|
||||||
|
@ -834,8 +915,10 @@ const commands = [
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-container {
|
.graph-toggle-btn {
|
||||||
height: 50vh;
|
top: auto;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
right: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -855,5 +938,15 @@ const commands = [
|
||||||
.blog-grid {
|
.blog-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-toggle-btn {
|
||||||
|
padding: 0.5rem;
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-toggle-btn span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,17 +1,628 @@
|
||||||
---
|
---
|
||||||
|
// src/pages/posts/[slug].astro
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import BlogPostLayout from '../../layouts/BlogPostLayout.astro'; // Using BlogPostLayout
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
|
||||||
// 1. Generate a path for every blog post
|
// Required getStaticPaths function for dynamic routes
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const postEntries = await getCollection('posts');
|
try {
|
||||||
return postEntries.map(entry => ({
|
// Get posts from the posts collection
|
||||||
params: { slug: entry.slug }, props: { entry },
|
const allPosts = await getCollection('posts', ({ data }) => {
|
||||||
|
return import.meta.env.PROD ? !data.draft : true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return allPosts.map(post => ({
|
||||||
|
params: { slug: post.slug },
|
||||||
|
props: { post, allPosts },
|
||||||
}));
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching posts:', error);
|
||||||
|
// Return empty array if collection doesn't exist
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. For your template, you can get the entry directly from the prop
|
// Get the post and all posts from props
|
||||||
const { entry } = Astro.props;
|
const { post, allPosts } = Astro.props;
|
||||||
const { Content } = await entry.render();
|
|
||||||
|
// Format date helper
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return '';
|
||||||
|
const d = new Date(date);
|
||||||
|
return d.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate datetime attribute safely
|
||||||
|
const getISODate = (date) => {
|
||||||
|
if (!date) return '';
|
||||||
|
// Handle various date formats
|
||||||
|
try {
|
||||||
|
// If already a Date object
|
||||||
|
if (date instanceof Date) {
|
||||||
|
return date.toISOString();
|
||||||
|
}
|
||||||
|
// If it's a string or number, convert to Date
|
||||||
|
return new Date(date).toISOString();
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback if date is invalid
|
||||||
|
console.error('Invalid date format:', date);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find related posts by tags
|
||||||
|
const getRelatedPosts = (currentPost, allPosts, maxPosts = 3) => {
|
||||||
|
if (!currentPost || !allPosts) return [];
|
||||||
|
|
||||||
|
// Get current post tags
|
||||||
|
const postTags = currentPost.data.tags || [];
|
||||||
|
|
||||||
|
// If no tags, just return recent posts
|
||||||
|
if (postTags.length === 0) {
|
||||||
|
return allPosts
|
||||||
|
.filter(p => p.slug !== currentPost.slug && !p.data.draft)
|
||||||
|
.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();
|
||||||
|
})
|
||||||
|
.slice(0, maxPosts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score posts by matching tags
|
||||||
|
const scoredPosts = allPosts
|
||||||
|
.filter(p => p.slug !== currentPost.slug && !p.data.draft)
|
||||||
|
.map(p => {
|
||||||
|
const pTags = p.data.tags || [];
|
||||||
|
const matchCount = pTags.filter(tag => postTags.includes(tag)).length;
|
||||||
|
return { post: p, score: matchCount };
|
||||||
|
})
|
||||||
|
.filter(item => item.score > 0)
|
||||||
|
.sort((a, b) => {
|
||||||
|
// Sort by score first
|
||||||
|
if (b.score !== a.score) return b.score - a.score;
|
||||||
|
|
||||||
|
// If scores are equal, sort by date
|
||||||
|
const dateA = a.post.data.pubDate ? new Date(a.post.data.pubDate) : new Date(0);
|
||||||
|
const dateB = b.post.data.pubDate ? new Date(b.post.data.pubDate) : new Date(0);
|
||||||
|
return dateB.getTime() - dateA.getTime();
|
||||||
|
})
|
||||||
|
.slice(0, maxPosts);
|
||||||
|
|
||||||
|
// If we don't have enough related posts by tags, add recent posts
|
||||||
|
if (scoredPosts.length < maxPosts) {
|
||||||
|
const recentPosts = allPosts
|
||||||
|
.filter(p => {
|
||||||
|
return p.slug !== currentPost.slug &&
|
||||||
|
!p.data.draft &&
|
||||||
|
!scoredPosts.some(sp => sp.post.slug === p.slug);
|
||||||
|
})
|
||||||
|
.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();
|
||||||
|
})
|
||||||
|
.slice(0, maxPosts - scoredPosts.length);
|
||||||
|
|
||||||
|
return [...scoredPosts.map(sp => sp.post), ...recentPosts];
|
||||||
|
}
|
||||||
|
|
||||||
|
return scoredPosts.map(sp => sp.post);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get related posts
|
||||||
|
const relatedPosts = getRelatedPosts(post, allPosts);
|
||||||
|
|
||||||
|
// Check for explicitly related posts in frontmatter
|
||||||
|
const explicitRelatedPosts = post.data.related_posts
|
||||||
|
? allPosts.filter(p => post.data.related_posts.includes(p.slug))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Combine explicit and tag-based related posts, with explicit ones first
|
||||||
|
const combinedRelatedPosts = [
|
||||||
|
...explicitRelatedPosts,
|
||||||
|
...relatedPosts.filter(p => !explicitRelatedPosts.some(ep => ep.slug === p.slug))
|
||||||
|
].slice(0, 3);
|
||||||
|
|
||||||
|
// Get the Content component for rendering markdown
|
||||||
|
const { Content } = await post.render();
|
||||||
---
|
---
|
||||||
<BlogPostLayout frontmatter={entry.data}> <Content /> </BlogPostLayout>
|
|
||||||
|
<BaseLayout title={post.data.title} description={post.data.description || ''}>
|
||||||
|
<article class="container blog-post">
|
||||||
|
<header class="post-header">
|
||||||
|
<h1>{post.data.title}</h1>
|
||||||
|
<div class="post-meta">
|
||||||
|
{post.data.pubDate && <time datetime={getISODate(post.data.pubDate)}>{formatDate(post.data.pubDate)}</time>}
|
||||||
|
{post.data.updatedDate && <div class="updated-date">Updated: {formatDate(post.data.updatedDate)}</div>}
|
||||||
|
{post.data.readTime && <div class="read-time">{post.data.readTime}</div>}
|
||||||
|
{post.data.author && <div class="author">By {post.data.author}</div>}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{post.data.heroImage && (
|
||||||
|
<div class="hero-image">
|
||||||
|
<img src={post.data.heroImage} alt={post.data.title} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="post-content">
|
||||||
|
<div class="post-body">
|
||||||
|
<Content />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="post-sidebar">
|
||||||
|
{post.data.tags && post.data.tags.length > 0 && (
|
||||||
|
<div class="tags-section sidebar-block">
|
||||||
|
<h3>Tags</h3>
|
||||||
|
<div class="tags">
|
||||||
|
{post.data.tags.map(tag => (
|
||||||
|
<a href={`/tag/${tag}`} class="tag">{tag}</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{post.data.category && (
|
||||||
|
<div class="category-section sidebar-block">
|
||||||
|
<h3>Category</h3>
|
||||||
|
<a href={`/categories/${post.data.category}`} class="category">
|
||||||
|
{post.data.category}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{post.data.categories && post.data.categories.length > 0 && (
|
||||||
|
<div class="categories-section sidebar-block">
|
||||||
|
<h3>Categories</h3>
|
||||||
|
<div class="categories">
|
||||||
|
{post.data.categories.map(category => (
|
||||||
|
<a href={`/categories/${category}`} class="category">
|
||||||
|
{category}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{combinedRelatedPosts.length > 0 && (
|
||||||
|
<div class="related-posts-section sidebar-block">
|
||||||
|
<h3>Related Articles</h3>
|
||||||
|
<ul class="related-posts">
|
||||||
|
{combinedRelatedPosts.map(relatedPost => (
|
||||||
|
<li>
|
||||||
|
<a href={`/posts/${relatedPost.slug}/`} class="related-post">
|
||||||
|
<div class="related-post-title">{relatedPost.data.title}</div>
|
||||||
|
<div class="related-post-meta">
|
||||||
|
<span class="related-post-date">{formatDate(relatedPost.data.pubDate)}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="post-navigation">
|
||||||
|
<a href="/blog" class="back-to-blog">
|
||||||
|
<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">
|
||||||
|
<line x1="19" y1="12" x2="5" y2="12"></line>
|
||||||
|
<polyline points="12 19 5 12 12 5"></polyline>
|
||||||
|
</svg>
|
||||||
|
Back to Blog
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 var(--container-padding, 1.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post {
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-header h1 {
|
||||||
|
font-size: var(--font-size-4xl, 2.25rem);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: var(--font-size-sm, 0.875rem);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-image {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 3fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-block {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-block h3 {
|
||||||
|
font-size: var(--font-size-lg, 1.125rem);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
border-radius: 20px;
|
||||||
|
color: #10B981;
|
||||||
|
font-size: var(--font-size-xs, 0.75rem);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag:hover {
|
||||||
|
background: rgba(16, 185, 129, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category, .categories {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||||
|
border-radius: 20px;
|
||||||
|
color: var(--bg-primary);
|
||||||
|
font-size: var(--font-size-sm, 0.875rem);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Related Posts */
|
||||||
|
.related-posts {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-posts li {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-posts li:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-post {
|
||||||
|
display: block;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-post:hover {
|
||||||
|
background: rgba(6, 182, 212, 0.05);
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-post-title {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-post-meta {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Post Navigation */
|
||||||
|
.post-navigation {
|
||||||
|
margin-top: 3rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-blog {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 30px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-blog:hover {
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Styling */
|
||||||
|
.post-body {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
margin: 2rem 0 1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body h3 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin: 1.75rem 0 0.75rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body p {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body a {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px dashed var(--accent-primary);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body a:hover {
|
||||||
|
color: var(--accent-secondary);
|
||||||
|
border-bottom-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body ul, .post-body ol {
|
||||||
|
margin: 1rem 0 1.5rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body blockquote {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-left: 4px solid var(--accent-primary);
|
||||||
|
background: rgba(6, 182, 212, 0.05);
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
background: rgba(15, 23, 42, 0.3);
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body pre {
|
||||||
|
background: rgba(15, 23, 42, 0.3);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body pre code {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body th, .post-body td {
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body th {
|
||||||
|
background: rgba(15, 23, 42, 0.3);
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Adjustments */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.post-header h1 {
|
||||||
|
font-size: var(--font-size-3xl, 1.875rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.post-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-header h1 {
|
||||||
|
font-size: var(--font-size-2xl, 1.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Add functionality to handle post sharing and tag clicks
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Add click functionality to tags
|
||||||
|
document.querySelectorAll('.tag').forEach(tag => {
|
||||||
|
tag.addEventListener('click', (e) => {
|
||||||
|
// Already links, no need for additional JS
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add scroll-to-top button when scrolling down
|
||||||
|
const scrollToTop = document.createElement('button');
|
||||||
|
scrollToTop.className = 'scroll-to-top';
|
||||||
|
scrollToTop.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="18 15 12 9 6 15"></polyline>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(scrollToTop);
|
||||||
|
|
||||||
|
// Show/hide scroll-to-top button
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
if (window.scrollY > 500) {
|
||||||
|
scrollToTop.classList.add('visible');
|
||||||
|
} else {
|
||||||
|
scrollToTop.classList.remove('visible');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scroll to top when clicked
|
||||||
|
scrollToTop.addEventListener('click', () => {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
/* Scroll to top button */
|
||||||
|
.scroll-to-top {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30px;
|
||||||
|
right: 30px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
transform: translateY(20px);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-to-top.visible {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-to-top:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.scroll-to-top {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-to-top svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,32 @@
|
||||||
|
// src/pages/search-index.json.js
|
||||||
|
// Generates a JSON file with all posts for client-side search
|
||||||
|
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
|
||||||
|
export async function get() {
|
||||||
|
// Get all posts
|
||||||
|
const allPosts = await getCollection('posts', ({ data }) => {
|
||||||
|
// Exclude draft posts in production
|
||||||
|
return import.meta.env.PROD ? !data.draft : true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transform posts into search-friendly format
|
||||||
|
const searchablePosts = allPosts.map(post => ({
|
||||||
|
slug: post.slug,
|
||||||
|
title: post.data.title,
|
||||||
|
description: post.data.description || '',
|
||||||
|
pubDate: post.data.pubDate ? new Date(post.data.pubDate).toISOString() : '',
|
||||||
|
category: post.data.category || 'Uncategorized',
|
||||||
|
tags: post.data.tags || [],
|
||||||
|
readTime: post.data.readTime || '5 min read',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Return JSON
|
||||||
|
return {
|
||||||
|
body: JSON.stringify(searchablePosts),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'max-age=3600'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,8 +33,8 @@
|
||||||
--font-size-5xl: 3rem;
|
--font-size-5xl: 3rem;
|
||||||
--font-mono: 'JetBrains Mono', monospace;
|
--font-mono: 'JetBrains Mono', monospace;
|
||||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
--bg-primary-rgb: 15, 18, 25; /* Added RGB for gradients */
|
--bg-primary-rgb: 15, 18, 25; /* RGB for gradients */
|
||||||
--bg-secondary-rgb: 22, 26, 36; /* Added RGB for gradients */
|
--bg-secondary-rgb: 22, 26, 36; /* RGB for gradients */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light Mode Variables */
|
/* Light Mode Variables */
|
||||||
|
@ -58,49 +58,75 @@
|
||||||
--card-border: rgba(37, 99, 235, 0.3); /* Blue border */
|
--card-border: rgba(37, 99, 235, 0.3); /* Blue border */
|
||||||
--ui-element: #e2e8f0; /* Lighter UI elements */
|
--ui-element: #e2e8f0; /* Lighter UI elements */
|
||||||
--ui-element-hover: #cbd5e1;
|
--ui-element-hover: #cbd5e1;
|
||||||
--bg-primary-rgb: 255, 255, 255; /* Added RGB for gradients */
|
--bg-primary-rgb: 255, 255, 255; /* RGB for gradients */
|
||||||
--bg-secondary-rgb: 248, 250, 252; /* Added RGB for gradients */
|
--bg-secondary-rgb: 248, 250, 252; /* RGB for gradients */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure transitions for smooth theme changes */
|
/* Ensure transitions for smooth theme changes */
|
||||||
body, .site-header, .site-footer, .post-card, input, select, button, pre, code {
|
*, *::before, *::after {
|
||||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Knowledge Graph specific theme adjustments */
|
||||||
|
:root.light-mode .graph-container {
|
||||||
|
background: rgba(248, 250, 252, 0.3);
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode .node-details {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode .graph-filters {
|
||||||
|
background: rgba(248, 250, 252, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode .graph-filter {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-color: var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode .connections-list a {
|
||||||
|
color: var(--accent-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode .node-link {
|
||||||
|
box-shadow: 0 4px 10px rgba(8, 145, 178, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for code blocks in light mode */
|
||||||
|
:root.light-mode pre,
|
||||||
|
:root.light-mode code {
|
||||||
|
background-color: var(--bg-code);
|
||||||
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply base styles using variables */
|
/* Apply base styles using variables */
|
||||||
/* (Keep existing base styles from BaseLayout.astro's <style is:global> here if they are not theme-dependent) */
|
body {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
/* Example: Ensure header background uses variables */
|
color: var(--text-primary);
|
||||||
.site-header {
|
|
||||||
/* Use RGB variables for background gradients with opacity */
|
|
||||||
background: linear-gradient(180deg, rgba(var(--bg-secondary-rgb), 0.9), rgba(var(--bg-primary-rgb), 0.8));
|
|
||||||
border-bottom-color: var(--border-primary);
|
|
||||||
/* Other header styles */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-header.scrolled {
|
a {
|
||||||
background: rgba(var(--bg-primary-rgb), 0.95);
|
color: var(--accent-primary);
|
||||||
/* Other scrolled styles */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Example: Ensure card styles use variables */
|
/* Fix for inputs/selects */
|
||||||
.post-card {
|
input, select, textarea {
|
||||||
background: var(--card-bg);
|
|
||||||
border-color: var(--card-border);
|
|
||||||
/* Other card styles */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure code blocks adapt */
|
|
||||||
pre, code {
|
|
||||||
background-color: var(--bg-code);
|
|
||||||
color: var(--text-secondary); /* Or adjust as needed */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure inputs/selects adapt */
|
|
||||||
input, select {
|
|
||||||
background-color: var(--bg-secondary);
|
background-color: var(--bg-secondary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border-color: var(--border-primary);
|
border-color: var(--border-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add any other theme-dependent styles here */
|
/* Ensure header and footer adapt to theme */
|
||||||
|
.site-header, .site-footer {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-color: var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix card styles */
|
||||||
|
.post-card, .sidebar-block {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
border-color: var(--card-border);
|
||||||
|
}
|
Loading…
Reference in New Issue