fresh-main #6
			
				
			
		
		
		
	|  | @ -1,439 +1,505 @@ | |||
| --- | ||||
| // Header.astro | ||||
| // Primary navigation component with premium design elements | ||||
| // src/components/Header.astro | ||||
| import ThemeToggler from './ThemeToggler.astro'; | ||||
| 
 | ||||
| // Get current path to highlight active nav item | ||||
| const pathname = new URL(Astro.request.url).pathname; | ||||
| const currentPath = pathname.split('/')[1]; // Get the first path segment | ||||
| 
 | ||||
| // Define navigation items | ||||
| // Define navigation items with proper URLs | ||||
| const navItems = [ | ||||
|   { name: 'Home', path: '/' }, | ||||
|   { name: 'Blog', path: '/blog/' }, | ||||
|   { name: 'Projects', path: '/projects/' }, | ||||
|   { name: 'Home Lab', path: '/homelab/' }, | ||||
|   { name: 'Resources', path: '/resources/' }, | ||||
|   { name: 'About', path: '/about/' }, | ||||
|   { name: 'Contact', path: '/contact/' } | ||||
|   { name: 'Home', url: '/' }, | ||||
|   { name: 'Blog', url: '/blog' }, | ||||
|   { name: 'Projects', url: '/projects' }, | ||||
|   { name: 'Home Lab', url: 'https://argobox.com' }, | ||||
|   { name: 'Resources', url: '/resources' }, | ||||
|   { name: 'About', url: 'https://laforceit.com' }, | ||||
|   { 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"> | ||||
|   <div class="nebula-bg"></div> | ||||
|   <div class="container"> | ||||
|     <div class="header-container"> | ||||
|       <a href="/" class="logo"> | ||||
|         <div class="logo-symbol"> | ||||
|           <span class="logo-text">LF</span> | ||||
|           <div class="logo-glow"></div> | ||||
|         </div> | ||||
|         <div class="logo-text-container"> | ||||
|           <span class="logo-name">LaForceIT</span> | ||||
|           <span class="logo-tagline">Infrastructure & Automation</span> | ||||
|   <div class="container header-container"> | ||||
|     <div class="logo-container"> | ||||
|       <a href="/" class="logo-link"> | ||||
|         <div class="logo">LF</div> | ||||
|         <div class="site-name"> | ||||
|           <span class="site-title">LaForceIT</span> | ||||
|           <span class="site-subtitle">Infrastructure & Automation</span> | ||||
|         </div> | ||||
|       </a> | ||||
|     </div> | ||||
| 
 | ||||
|       <nav> | ||||
|         <div class="main-nav"> | ||||
|           {navItems.map((item) => ( | ||||
|     <nav class="main-nav"> | ||||
|       <ul class="nav-list"> | ||||
|         {navItems.map(item => ( | ||||
|           <li class="nav-item"> | ||||
|             <a  | ||||
|               href={item.path}  | ||||
|               class={`nav-link ${currentPath === item.path.replace(/\//g, '') ? 'active' : ''}`} | ||||
|               href={item.url}  | ||||
|               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} | ||||
|               <div class="nav-highlight"></div> | ||||
|             </a> | ||||
|           </li> | ||||
|         ))} | ||||
|         </div> | ||||
|       </ul> | ||||
|     </nav> | ||||
| 
 | ||||
|     <div class="header-actions"> | ||||
|           <button id="theme-toggle" class="theme-toggle" aria-label="Toggle dark mode"> | ||||
|             <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"> | ||||
|               <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"> | ||||
|       <div class="search-container"> | ||||
|         <button class="search-toggle" aria-label="Toggle 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"> | ||||
|             <circle cx="11" cy="11" r="8"></circle> | ||||
|             <line x1="21" y1="21" x2="16.65" y2="16.65"></line> | ||||
|           </svg> | ||||
|         </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"> | ||||
|             <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"> | ||||
|     <button class="mobile-menu-toggle" aria-label="Toggle menu"> | ||||
|       <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="6" x2="21" y2="6"></line> | ||||
|         <line x1="3" y1="18" x2="21" y2="18"></line> | ||||
|       </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> | ||||
|   </div> | ||||
|       </nav> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <!-- Animated network lines effect --> | ||||
|   <div class="network-lines"></div> | ||||
| </header> | ||||
| 
 | ||||
| <style> | ||||
|   .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; | ||||
|     top: 0; | ||||
|     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; | ||||
|     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 { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     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 { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     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)); | ||||
|     width: 40px; | ||||
|     height: 40px; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     position: relative; | ||||
|     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; | ||||
|     background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); | ||||
|     color: var(--bg-primary); | ||||
|     z-index: 2; | ||||
|     font-weight: bold; | ||||
|     border-radius: 8px; | ||||
|     margin-right: 0.75rem; | ||||
|     font-family: var(--font-sans); | ||||
|   } | ||||
|    | ||||
|   .logo-glow { | ||||
|     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 { | ||||
|   .site-name { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|   } | ||||
|    | ||||
|   .logo-name { | ||||
|   .site-title { | ||||
|     font-weight: 600; | ||||
|     font-size: 1.4rem; | ||||
|     background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); | ||||
|     -webkit-background-clip: text; | ||||
|     background-clip: text; | ||||
|     color: transparent; | ||||
|     letter-spacing: 0.02em; | ||||
|     font-size: 1.25rem; | ||||
|     color: var(--text-primary); | ||||
|   } | ||||
|    | ||||
|   .logo-tagline { | ||||
|   .site-subtitle { | ||||
|     font-size: 0.75rem; | ||||
|     color: var(--text-secondary); | ||||
|     font-family: var(--font-mono); | ||||
|     letter-spacing: 0.05em; | ||||
|     color: var(--text-tertiary); | ||||
|     letter-spacing: 0.5px; | ||||
|   } | ||||
|    | ||||
|   .main-nav { | ||||
|     display: flex; | ||||
|     gap: 1.5rem; | ||||
|     align-items: center; | ||||
|     margin-left: auto; | ||||
|   } | ||||
|    | ||||
|   .nav-list { | ||||
|     display: flex; | ||||
|     list-style: none; | ||||
|     padding: 0; | ||||
|     margin: 0; | ||||
|     gap: 1rem; | ||||
|   } | ||||
|    | ||||
|   .nav-link { | ||||
|     color: var(--text-secondary); | ||||
|     position: relative; | ||||
|     text-decoration: none; | ||||
|     font-weight: 500; | ||||
|     transition: color 0.3s ease; | ||||
|     padding: 0.5rem 0; | ||||
|     font-size: 0.9rem; | ||||
|     padding: 0.5rem 0.75rem; | ||||
|     border-radius: 6px; | ||||
|     transition: all 0.2s ease; | ||||
|     position: relative; | ||||
|   } | ||||
|    | ||||
|   .nav-link:hover, .nav-link.active { | ||||
|   .nav-link:hover { | ||||
|     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; | ||||
|     bottom: -2px; | ||||
|     left: 0; | ||||
|     width: 0; | ||||
|     left: 0.75rem; | ||||
|     right: 0.75rem; | ||||
|     height: 2px; | ||||
|     background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); | ||||
|     transition: width 0.3s ease; | ||||
|     border-radius: 2px; | ||||
|   } | ||||
|    | ||||
|   .nav-link:hover .nav-highlight, | ||||
|   .nav-link.active .nav-highlight { | ||||
|     width: 100%; | ||||
|     background: var(--accent-primary); | ||||
|     border-radius: 1px; | ||||
|   } | ||||
|    | ||||
|   .header-actions { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 1rem; | ||||
|     margin-left: 2rem; | ||||
|     gap: 0.75rem; | ||||
|     margin-left: 1rem; | ||||
|   } | ||||
|    | ||||
|   .theme-toggle,  | ||||
|   .search-button { | ||||
|   /* Search Dropdown Styles */ | ||||
|   .search-container { | ||||
|     position: relative; | ||||
|   } | ||||
|    | ||||
|   .search-toggle { | ||||
|     background: none; | ||||
|     border: none; | ||||
|     color: var(--text-secondary); | ||||
|     padding: 0.5rem; | ||||
|     cursor: pointer; | ||||
|     border-radius: 50%; | ||||
|     transition: all 0.3s ease; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     color: var(--text-secondary); | ||||
|     cursor: pointer; | ||||
|     transition: all 0.2s ease; | ||||
|   } | ||||
|    | ||||
|   .theme-toggle:hover,  | ||||
|   .search-button:hover { | ||||
|   .search-toggle:hover { | ||||
|     color: var(--text-primary); | ||||
|     background: rgba(255, 255, 255, 0.1); | ||||
|     background: rgba(255, 255, 255, 0.05); | ||||
|   } | ||||
|    | ||||
|   .icon-sun { | ||||
|     display: none; | ||||
|   .search-dropdown { | ||||
|     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 { | ||||
|     display: none; | ||||
|   .search-dropdown.active { | ||||
|     transform: scale(1); | ||||
|     opacity: 1; | ||||
|     visibility: visible; | ||||
|   } | ||||
|    | ||||
|   :global(.light-mode) .icon-sun { | ||||
|     display: block; | ||||
|   .search-input-wrapper { | ||||
|     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; | ||||
|     background: none; | ||||
|     border: none; | ||||
|     color: var(--text-primary); | ||||
|     color: var(--text-secondary); | ||||
|     cursor: pointer; | ||||
|   } | ||||
|    | ||||
|   .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%); | ||||
|     } | ||||
|     padding: 0.5rem; | ||||
|   } | ||||
|    | ||||
|   /* Responsive Adjustments */ | ||||
|   @media (max-width: 1024px) { | ||||
|     .main-nav { | ||||
|       gap: 1rem; | ||||
|     } | ||||
|      | ||||
|     .header-actions { | ||||
|       margin-left: 1rem; | ||||
|     .nav-link { | ||||
|       padding: 0.5rem; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   @media (max-width: 768px) { | ||||
|     .mobile-menu-btn { | ||||
|       display: block; | ||||
|     } | ||||
|      | ||||
|     .main-nav { | ||||
|       display: none; | ||||
|       position: absolute; | ||||
|       top: 100%; | ||||
|       left: 0; | ||||
|       width: 100%; | ||||
|       right: 0; | ||||
|       background: var(--bg-secondary); | ||||
|       padding: 1rem; | ||||
|       flex-direction: column; | ||||
|       align-items: flex-start; | ||||
|       gap: 1rem; | ||||
|       border-bottom: 1px solid var(--border-primary); | ||||
|       z-index: 10; | ||||
|     } | ||||
|      | ||||
|     .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; | ||||
|     } | ||||
|      | ||||
|     .theme-toggle, .search-button { | ||||
|       padding: 0.4rem; | ||||
|     .mobile-menu-toggle { | ||||
|       display: block; | ||||
|     } | ||||
|      | ||||
|     .search-dropdown { | ||||
|       width: 260px; | ||||
|     } | ||||
|   } | ||||
| </style> | ||||
| 
 | ||||
| <script> | ||||
|   // Only define one DOMContentLoaded event handler | ||||
|   document.addEventListener('DOMContentLoaded', () => { | ||||
|     const menuBtn = document.getElementById('mobile-menu-btn'); | ||||
|     const mainNav = document.querySelector('.main-nav'); | ||||
|     const header = document.querySelector('.site-header'); | ||||
|      | ||||
|     // Mobile menu toggle | ||||
|     if (menuBtn && mainNav) { | ||||
|       menuBtn.addEventListener('click', () => { | ||||
|         mainNav.classList.toggle('active'); | ||||
|         menuBtn.classList.toggle('mobile-menu-active'); | ||||
|     const mobileMenuToggle = document.querySelector('.mobile-menu-toggle'); | ||||
|     const mainNav = document.querySelector('.main-nav'); | ||||
|      | ||||
|     mobileMenuToggle?.addEventListener('click', () => { | ||||
|       mainNav?.classList.toggle('active'); | ||||
|     }); | ||||
|      | ||||
|     // Search dropdown toggle | ||||
|     const searchToggle = document.querySelector('.search-toggle'); | ||||
|     const searchDropdown = document.querySelector('.search-dropdown'); | ||||
|     const searchInput = document.querySelector('#header-search'); | ||||
|      | ||||
|     searchToggle?.addEventListener('click', (e) => { | ||||
|       e.stopPropagation(); | ||||
|       searchDropdown?.classList.toggle('active'); | ||||
|        | ||||
|       if (searchDropdown?.classList.contains('active')) { | ||||
|         // Focus the search input when dropdown is shown | ||||
|         setTimeout(() => { | ||||
|           searchInput?.focus(); | ||||
|         }, 100); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Close search dropdown when clicking outside | ||||
|     document.addEventListener('click', (e) => { | ||||
|       if (!e.target.closest('.search-container')) { | ||||
|         searchDropdown?.classList.remove('active'); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // 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; | ||||
|       } | ||||
|        | ||||
|     // Header scroll effect | ||||
|     window.addEventListener('scroll', () => { | ||||
|       if (window.scrollY > 50) { | ||||
|         header?.classList.add('scrolled'); | ||||
|       try { | ||||
|         // This would ideally be a server-side search or a pre-built index | ||||
|         // For now, we'll just fetch all posts and filter client-side | ||||
|         const response = await fetch('/search-index.json'); | ||||
|         if (!response.ok) throw new Error('Failed to fetch search data'); | ||||
|          | ||||
|         const posts = await response.json(); | ||||
|         const results = posts.filter(post => { | ||||
|           const lowerQuery = query.toLowerCase(); | ||||
|           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 { | ||||
|         header?.classList.remove('scrolled'); | ||||
|             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 | ||||
|     }); | ||||
|      | ||||
|     // Theme toggle functionality - only define once | ||||
|     const themeToggle = document.getElementById('theme-toggle'); | ||||
|      | ||||
|     if (themeToggle) { | ||||
|       themeToggle.addEventListener('click', () => { | ||||
|         document.documentElement.classList.toggle('light-mode'); | ||||
|          | ||||
|         // Store preference in localStorage | ||||
|         const isLightMode = document.documentElement.classList.contains('light-mode'); | ||||
|         localStorage.setItem('theme', isLightMode ? 'light' : 'dark'); | ||||
|     // Handle search form submission | ||||
|     const searchForm = searchInput?.closest('form'); | ||||
|     searchForm?.addEventListener('submit', (e) => { | ||||
|       e.preventDefault(); | ||||
|       performSearch(searchInput.value); | ||||
|     }); | ||||
|      | ||||
|       // Apply saved theme preference | ||||
|       const savedTheme = localStorage.getItem('theme'); | ||||
|       if (savedTheme === 'light') { | ||||
|         document.documentElement.classList.add('light-mode'); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Add interactive network nodes animation | ||||
|     const header_el = document.querySelector('.site-header'); | ||||
|      | ||||
|     if (header_el) { | ||||
|       // Create animated nodes | ||||
|       for (let i = 0; i < 5; i++) { | ||||
|         const node = document.createElement('div'); | ||||
|         node.className = 'nav-node'; | ||||
|         node.style.left = `${Math.random() * 100}%`; | ||||
|         node.style.animationDelay = `${Math.random() * 5}s`; | ||||
|         node.style.animationDuration = `${5 + Math.random() * 5}s`; | ||||
|         header_el.appendChild(node); | ||||
|       } | ||||
|     } | ||||
|     // Handle search-submit button click | ||||
|     const searchSubmit = document.querySelector('.search-submit'); | ||||
|     searchSubmit?.addEventListener('click', () => { | ||||
|       performSearch(searchInput?.value || ''); | ||||
|     }); | ||||
|   }); | ||||
| </script> | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -47,25 +47,25 @@ | |||
|     transition: transform 0.5s ease, opacity 0.5s ease; | ||||
|   } | ||||
|    | ||||
|   html:not(.dark) .sun-icon { | ||||
|     opacity: 1; | ||||
|     transform: rotate(0); | ||||
|   } | ||||
|    | ||||
|   html:not(.dark) .moon-icon { | ||||
|     opacity: 0; | ||||
|     transform: rotate(90deg); | ||||
|   } | ||||
|    | ||||
|   html.dark .sun-icon { | ||||
|   :root:not(.light-mode) .sun-icon { | ||||
|     opacity: 0; | ||||
|     transform: rotate(-90deg); | ||||
|   } | ||||
|    | ||||
|   html.dark .moon-icon { | ||||
|   :root:not(.light-mode) .moon-icon { | ||||
|     opacity: 1; | ||||
|     transform: rotate(0); | ||||
|   } | ||||
|    | ||||
|   :root.light-mode .sun-icon { | ||||
|     opacity: 1; | ||||
|     transform: rotate(0); | ||||
|   } | ||||
|    | ||||
|   :root.light-mode .moon-icon { | ||||
|     opacity: 0; | ||||
|     transform: rotate(90deg); | ||||
|   } | ||||
| </style> | ||||
| 
 | ||||
| <script> | ||||
|  | @ -73,21 +73,34 @@ | |||
|   document.addEventListener('DOMContentLoaded', () => { | ||||
|     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 | ||||
|     const setTheme = (isDark) => { | ||||
|       if (isDark) { | ||||
|         document.documentElement.classList.add('dark'); | ||||
|         localStorage.setItem('theme', 'dark'); | ||||
|       } else { | ||||
|         document.documentElement.classList.remove('dark'); | ||||
|     const setTheme = (isLight) => { | ||||
|       if (isLight) { | ||||
|         document.documentElement.classList.add('light-mode'); | ||||
|         localStorage.setItem('theme', 'light'); | ||||
|       } else { | ||||
|         document.documentElement.classList.remove('light-mode'); | ||||
|         localStorage.setItem('theme', 'dark'); | ||||
|       } | ||||
|     }; | ||||
|      | ||||
|     // Apply initial theme | ||||
|     setTheme(getInitialTheme()); | ||||
|      | ||||
|     // Theme toggle click handler | ||||
|     themeToggle?.addEventListener('click', () => { | ||||
|       const isDark = document.documentElement.classList.contains('dark'); | ||||
|       setTheme(!isDark); | ||||
|       const isCurrentlyLight = document.documentElement.classList.contains('light-mode'); | ||||
|       setTheme(!isCurrentlyLight); | ||||
|     }); | ||||
|   }); | ||||
| </script> | ||||
|  | @ -1,98 +1,100 @@ | |||
| // src/content/config.ts
 | ||||
| import { defineCollection, z } from 'astro:content'; | ||||
| 
 | ||||
| // Define custom date validator that handles multiple formats
 | ||||
| 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
 | ||||
| // Define the post collection schema
 | ||||
| const postsCollection = defineCollection({ | ||||
|   type: 'content', | ||||
|   schema: baseSchema, | ||||
| }); | ||||
| 
 | ||||
| const configurationsCollection = defineCollection({ | ||||
|   type: 'content', | ||||
|   schema: baseSchema, | ||||
| }); | ||||
| 
 | ||||
| const projectsCollection = defineCollection({ | ||||
|   type: 'content', | ||||
|   schema: baseSchema, | ||||
|   schema: z.object({ | ||||
|     title: z.string(), | ||||
|     description: z.string().optional(), | ||||
|     pubDate: z.coerce.date(), | ||||
|     updatedDate: z.coerce.date().optional(), | ||||
|     heroImage: z.string().optional(), | ||||
|      | ||||
|     // Support both single category and categories array
 | ||||
|     category: z.string().optional(), | ||||
|     categories: z.array(z.string()).optional(), | ||||
|      | ||||
|     // 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({ | ||||
|   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 const collections = { | ||||
|   'posts': postsCollection, | ||||
|   'configurations': configurationsCollection, | ||||
|   'projects': projectsCollection, | ||||
|   posts: postsCollection, | ||||
|   'external-posts': externalPostsCollection, | ||||
|   configurations: configurationsCollection, | ||||
|   projects: projectsCollection, | ||||
| }; | ||||
|  | @ -3,7 +3,7 @@ title: "Projects Collection" | |||
| description: "A placeholder document for the projects collection" | ||||
| heroImage: "/blog/images/placeholders/default.jpg" | ||||
| pubDate: 2025-04-18 | ||||
| status: "planning" | ||||
| status: "concept" # Changed from 'planning' to match schema | ||||
| 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 BaseLayout from '../../layouts/BaseLayout.astro'; | ||||
| import KnowledgeGraph from '../../components/KnowledgeGraph.astro'; | ||||
|  | @ -37,38 +37,64 @@ const postsData = sortedPosts.map(post => ({ | |||
|   isDraft: post.data.draft || false | ||||
| })); | ||||
| 
 | ||||
| // Prepare graph data for visualization | ||||
| // Prepare enhanced graph data with both posts and tags | ||||
| const graphData = { | ||||
|   nodes: sortedPosts | ||||
|   nodes: [ | ||||
|     // Add post nodes | ||||
|     ...sortedPosts | ||||
|       .filter(post => !post.data.draft) | ||||
|       .map(post => ({ | ||||
|         id: post.slug, | ||||
|         label: post.data.title, | ||||
|         type: 'post', | ||||
|         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: [] | ||||
| }; | ||||
| 
 | ||||
| // Create edges between posts based on shared tags | ||||
| for (let i = 0; i < graphData.nodes.length; i++) { | ||||
|   const postA = graphData.nodes[i]; | ||||
| // Create edges between posts and their tags | ||||
| sortedPosts | ||||
|   .filter(post => !post.data.draft) | ||||
|   .forEach(post => { | ||||
|     const postTags = post.data.tags || []; | ||||
|      | ||||
|   for (let j = i + 1; j < graphData.nodes.length; j++) { | ||||
|     const postB = graphData.nodes[j]; | ||||
|      | ||||
|     // 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) { | ||||
|     // Add edges from post to tags | ||||
|     postTags.forEach(tag => { | ||||
|       graphData.edges.push({ | ||||
|         source: postA.id, | ||||
|         target: postB.id, | ||||
|         strength: sharedTags.length + (postA.category === postB.category ? 1 : 0) | ||||
|         source: post.slug, | ||||
|         target: `tag-${tag}`, | ||||
|         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 | ||||
| const commands = [ | ||||
|  | @ -79,8 +105,8 @@ const commands = [ | |||
|   }, | ||||
|   { | ||||
|     prompt: "[laforceit@argobox]$ ", | ||||
|     command: "ls -la ./categories", | ||||
|     output: allCategories.map(cat => `${cat}`) | ||||
|     command: "ls -la ./tags", | ||||
|     output: allTags.map(tag => `${tag}`) | ||||
|   }, | ||||
|   { | ||||
|     prompt: "[laforceit@argobox]$ ", | ||||
|  | @ -117,42 +143,26 @@ const commands = [ | |||
|       </div> | ||||
|     </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 --> | ||||
|     <section class="graph-section"> | ||||
|       <div class="container"> | ||||
|         <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 class="knowledge-graph-wrapper"> | ||||
|             <KnowledgeGraph graphData={graphData} height="500px" /> | ||||
|           </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-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" /> | ||||
|             </div> | ||||
|             <div class="tag-filters"> | ||||
|  | @ -163,8 +173,17 @@ const commands = [ | |||
|               ))} | ||||
|             </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="loading-indicator"> | ||||
|               <div class="loading-spinner"></div> | ||||
|  | @ -172,6 +191,7 @@ const commands = [ | |||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </section> | ||||
|   </main> | ||||
|   <Footer slot="footer" /> | ||||
|  | @ -182,81 +202,18 @@ const commands = [ | |||
|     const searchInput = document.getElementById('search-input'); | ||||
|     const tagButtons = document.querySelectorAll('.tag-filter-btn'); | ||||
|     const blogGrid = document.getElementById('blog-grid'); | ||||
|     const graphFilters = document.querySelectorAll('.graph-filter'); | ||||
|      | ||||
|     // State variables | ||||
|     let currentFilterTag = 'all'; | ||||
|     let currentSearchTerm = ''; | ||||
|     let currentGraphFilter = 'all'; | ||||
|     let cy; // Cytoscape instance will be set by KnowledgeGraph component | ||||
|    | ||||
|     // Wait for cytoscape instance to be available | ||||
|     document.addEventListener('graphReady', (e) => { | ||||
|       cy = e.detail.cy; | ||||
|       setupGraphInteractions(); | ||||
|       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 createPostCardHTML(post) { | ||||
|       // Make sure tags is an array before stringifying | ||||
|  | @ -264,12 +221,13 @@ const commands = [ | |||
|        | ||||
|       // Create tag pills HTML | ||||
|       const tagPills = post.tags.map(tag =>  | ||||
|         `<span class="post-tag">${tag}</span>` | ||||
|         `<span class="post-tag" data-tag="${tag}">${tag}</span>` | ||||
|       ).join(''); | ||||
|        | ||||
|       return ` | ||||
|         <article class="post-card" data-tags='${tagsString}' data-slug="${post.slug}"> | ||||
|           <div class="post-card-inner"> | ||||
|             <a href="/posts/${post.slug}/" class="post-image-link"> | ||||
|               <div class="post-image-container"> | ||||
|                 <img | ||||
|                   width="720" | ||||
|  | @ -281,6 +239,7 @@ const commands = [ | |||
|                 /> | ||||
|                 <div class="post-category-badge">${post.category}</div> | ||||
|               </div> | ||||
|             </a> | ||||
|             <div class="post-content"> | ||||
|               <div class="post-meta"> | ||||
|                 <time datetime="${post.pubDateISO}">${post.pubDate}</time> | ||||
|  | @ -321,7 +280,7 @@ const commands = [ | |||
|                             post.title.toLowerCase().includes(searchTermLower) || | ||||
|                             post.description.toLowerCase().includes(searchTermLower) || | ||||
|                             postTags.some(tag => tag.toLowerCase().includes(searchTermLower)); | ||||
|         return matchesTag && matchesSearch && !post.isDraft; // Exclude drafts | ||||
|         return matchesTag && matchesSearch; | ||||
|       }); | ||||
|    | ||||
|       // Update the grid HTML | ||||
|  | @ -329,29 +288,73 @@ const commands = [ | |||
|         if (filteredPosts.length > 0) { | ||||
|           blogGrid.innerHTML = filteredPosts.map(createPostCardHTML).join(''); | ||||
|            | ||||
|           // If graph is available, highlight matching nodes | ||||
|           if (cy) { | ||||
|             const matchingSlugs = filteredPosts.map(post => post.slug); | ||||
|           // Add click handlers to post tag spans | ||||
|           document.querySelectorAll('.post-tag').forEach(tagSpan => { | ||||
|             tagSpan.addEventListener('click', (e) => { | ||||
|               e.preventDefault(); | ||||
|               const tag = tagSpan.dataset.tag; | ||||
|                | ||||
|             // Reset all nodes | ||||
|             cy.nodes().removeClass('highlighted').removeClass('filtered'); | ||||
|              | ||||
|             // Highlight matching nodes | ||||
|             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'); | ||||
|               // Find and click the matching tag filter button | ||||
|               const tagBtn = Array.from(tagButtons).find(btn => btn.dataset.tag === tag); | ||||
|               if (tagBtn) { | ||||
|                 tagBtn.click(); | ||||
|               } | ||||
|             }); | ||||
|           }); | ||||
|            | ||||
|           // 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 { | ||||
|           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 { | ||||
|         console.error("Blog grid element not found!"); | ||||
|  | @ -378,12 +381,53 @@ const commands = [ | |||
|         // Update filter and grid | ||||
|         currentFilterTag = button.dataset.tag; | ||||
|         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 | ||||
|     document.addEventListener('DOMContentLoaded', () => { | ||||
|       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> | ||||
| </BaseLayout> | ||||
|  | @ -481,17 +525,15 @@ const commands = [ | |||
|     max-width: 560px; | ||||
|   } | ||||
|    | ||||
|   /* Graph Section */ | ||||
|   .graph-section { | ||||
|     padding: 5rem 0; | ||||
|     position: relative; | ||||
|     background: linear-gradient(0deg, var(--bg-primary), var(--bg-secondary), var(--bg-primary)); | ||||
|   /* Blog Content Section */ | ||||
|   .blog-content-section { | ||||
|     padding: 2rem 0 5rem; | ||||
|   } | ||||
|    | ||||
|   .section-header { | ||||
|     text-align: center; | ||||
|     max-width: 800px; | ||||
|     margin: 0 auto 3rem; | ||||
|     margin: 0 auto 2rem; | ||||
|   } | ||||
|    | ||||
|   .section-title { | ||||
|  | @ -519,75 +561,56 @@ const commands = [ | |||
|     margin: 0 auto; | ||||
|   } | ||||
|    | ||||
|   .graph-container { | ||||
|     position: relative; | ||||
|     height: 60vh; | ||||
|     min-height: 500px; | ||||
|     max-height: 800px; | ||||
|   /* Search Filter Container with Knowledge Graph */ | ||||
|   .search-filter-container { | ||||
|     margin-bottom: 4rem; | ||||
|     background: rgba(15, 23, 42, 0.3); | ||||
|     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); | ||||
|     background: var(--bg-secondary); | ||||
|     color: var(--text-primary); | ||||
|     font-family: var(--font-mono); | ||||
|     font-size: var(--font-size-sm); | ||||
|     cursor: pointer; | ||||
|     transition: all 0.3s ease; | ||||
|     overflow: hidden; | ||||
|     box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); | ||||
|   } | ||||
|    | ||||
|   .graph-filter:hover { | ||||
|     border-color: var(--accent-primary); | ||||
|     box-shadow: 0 0 10px var(--glow-primary); | ||||
|   } | ||||
|    | ||||
|   .graph-filter.active { | ||||
|     background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); | ||||
|     border-color: transparent; | ||||
|     color: var(--bg-primary); | ||||
|   } | ||||
|    | ||||
|   /* Blog Posts Section */ | ||||
|   .blog-posts-section { | ||||
|     padding: 5rem 0; | ||||
|   .knowledge-graph-wrapper { | ||||
|     width: 100%; | ||||
|     padding: 0; | ||||
|     margin: 0 0 1.5rem; | ||||
|   } | ||||
|    | ||||
|   .search-filter-section { | ||||
|     margin-bottom: 3rem; | ||||
|     padding: 1.5rem; | ||||
|     background: rgba(13, 21, 41, 0.5); | ||||
|     border: 1px solid var(--card-border); | ||||
|     border-radius: 8px; | ||||
|     position: relative; | ||||
|   } | ||||
|    | ||||
|   .search-bar { | ||||
|     margin-bottom: 1.5rem; | ||||
|     position: relative; | ||||
|   } | ||||
|    | ||||
|   .search-icon { | ||||
|     position: absolute; | ||||
|     left: 1rem; | ||||
|     top: 50%; | ||||
|     transform: translateY(-50%); | ||||
|     color: var(--text-tertiary); | ||||
|   } | ||||
|    | ||||
|   .search-input { | ||||
|     width: 100%; | ||||
|     padding: 0.75rem 1rem; | ||||
|     padding: 0.75rem 1rem 0.75rem 2.5rem; | ||||
|     background-color: var(--bg-primary); | ||||
|     border: 1px solid var(--card-border); | ||||
|     border-radius: 6px; | ||||
|     border-radius: 8px; | ||||
|     color: var(--text-primary); | ||||
|     font-size: 1rem; | ||||
|     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 { | ||||
|  | @ -624,6 +647,7 @@ const commands = [ | |||
|     background-color: rgba(226, 232, 240, 0.1); | ||||
|     color: var(--text-primary); | ||||
|     border-color: rgba(56, 189, 248, 0.4); | ||||
|     transform: translateY(-2px); | ||||
|   } | ||||
|    | ||||
|   .tag-filter-btn.active { | ||||
|  | @ -633,8 +657,36 @@ const commands = [ | |||
|     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 { | ||||
|     margin: 2rem 0 4rem; | ||||
|     margin: 2rem 0; | ||||
|     display: grid; | ||||
|     grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); | ||||
|     gap: 2rem; | ||||
|  | @ -666,6 +718,11 @@ const commands = [ | |||
|     border-color: rgba(56, 189, 248, 0.4); | ||||
|   } | ||||
|    | ||||
|   .post-image-link { | ||||
|     display: block; | ||||
|     text-decoration: none; | ||||
|   } | ||||
|    | ||||
|   .post-image-container { | ||||
|     position: relative; | ||||
|   } | ||||
|  | @ -674,6 +731,11 @@ const commands = [ | |||
|     width: 100%; | ||||
|     height: 200px; | ||||
|     object-fit: cover; | ||||
|     transition: transform 0.5s ease; | ||||
|   } | ||||
|    | ||||
|   .post-card:hover .post-image { | ||||
|     transform: scale(1.05); | ||||
|   } | ||||
|    | ||||
|   .post-category-badge { | ||||
|  | @ -722,6 +784,18 @@ const commands = [ | |||
|     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 { | ||||
|     color: var(--text-secondary); | ||||
|     font-size: 0.9rem; | ||||
|  | @ -747,12 +821,19 @@ const commands = [ | |||
|   } | ||||
|    | ||||
|   .post-tag { | ||||
|     background: rgba(226, 232, 240, 0.05); | ||||
|     color: var(--text-secondary); | ||||
|     background: rgba(16, 185, 129, 0.1); | ||||
|     color: #10B981; | ||||
|     padding: 0.2rem 0.5rem; | ||||
|     border-radius: 4px; | ||||
|     font-size: 0.7rem; | ||||
|     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 { | ||||
|  | @ -834,8 +915,10 @@ const commands = [ | |||
|       max-width: 100%; | ||||
|     } | ||||
|      | ||||
|     .graph-container { | ||||
|       height: 50vh; | ||||
|     .graph-toggle-btn { | ||||
|       top: auto; | ||||
|       bottom: 1.5rem; | ||||
|       right: 1.5rem; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|  | @ -855,5 +938,15 @@ const commands = [ | |||
|     .blog-grid { | ||||
|       grid-template-columns: 1fr; | ||||
|     } | ||||
|      | ||||
|     .graph-toggle-btn { | ||||
|       padding: 0.5rem; | ||||
|       right: 1rem; | ||||
|       bottom: 1rem; | ||||
|     } | ||||
|      | ||||
|     .graph-toggle-btn span { | ||||
|       display: none; | ||||
|     } | ||||
|   } | ||||
| </style> | ||||
|  | @ -36,38 +36,44 @@ const postsData = sortedPosts.map(post => ({ | |||
|   isDraft: post.data.draft || false | ||||
| })); | ||||
| 
 | ||||
| // Prepare graph data for visualization | ||||
| const graphData = { | ||||
|   nodes: sortedPosts | ||||
|     .filter(post => !post.data.draft) | ||||
|     .map(post => ({ | ||||
| // Prepare graph data (Obsidian-style: Posts and Tags) | ||||
| const graphNodes = []; | ||||
| const graphEdges = []; | ||||
| const tagNodes = new Map(); // To avoid duplicate tag nodes | ||||
| 
 | ||||
| // Add post nodes | ||||
| sortedPosts.forEach(post => { | ||||
|   if (!post.data.draft) { // Exclude drafts from graph | ||||
|     graphNodes.push({ | ||||
|       id: post.slug, | ||||
|       label: post.data.title, | ||||
|       category: post.data.category || 'Uncategorized', | ||||
|       tags: post.data.tags || [] | ||||
|     })), | ||||
|   edges: [] | ||||
| }; | ||||
|       type: 'post', // Add type for styling/interaction | ||||
|       url: `/posts/${post.slug}/` // Add URL for linking | ||||
|     }); | ||||
| 
 | ||||
| // Create edges between posts based on shared tags | ||||
| for (let i = 0; i < graphData.nodes.length; i++) { | ||||
|   const postA = graphData.nodes[i]; | ||||
|    | ||||
|   for (let j = i + 1; j < graphData.nodes.length; j++) { | ||||
|     const postB = graphData.nodes[j]; | ||||
|      | ||||
|     // 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({ | ||||
|         source: postA.id, | ||||
|         target: postB.id, | ||||
|         strength: sharedTags.length + (postA.category === postB.category ? 1 : 0) | ||||
|     // Add tag nodes and edges | ||||
|     (post.data.tags || []).forEach(tag => { | ||||
|       const tagId = `tag-${tag}`; | ||||
|       // Add tag node only if it doesn't exist | ||||
|       if (!tagNodes.has(tagId)) { | ||||
|         graphNodes.push({ | ||||
|           id: tagId, | ||||
|           label: `#${tag}`, // Prefix with # for clarity | ||||
|           type: 'tag' // Add type | ||||
|         }); | ||||
|         tagNodes.set(tagId, true); | ||||
|       } | ||||
|       // Add edge connecting post to tag | ||||
|       graphEdges.push({ | ||||
|         source: post.slug, | ||||
|         target: tagId, | ||||
|         type: 'tag-connection' // Add type | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|   } | ||||
| } | ||||
| }); | ||||
| 
 | ||||
| const graphData = { nodes: graphNodes, edges: graphEdges }; | ||||
| 
 | ||||
| // Terminal commands for tech effect | ||||
| const commands = [ | ||||
|  | @ -115,29 +121,6 @@ const commands = [ | |||
|       </div> | ||||
|     </section> | ||||
| 
 | ||||
|     {/* Knowledge Graph Visualization */} | ||||
|     <section class="graph-section"> | ||||
|       <div class="container"> | ||||
|         <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 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"> | ||||
|  | @ -159,9 +142,15 @@ const commands = [ | |||
|               <button class="tag-filter-btn" data-tag={tag}>{tag}</button> | ||||
|             ))} | ||||
|           </div> | ||||
| 
 | ||||
|           {/* Integrated Knowledge Graph */} | ||||
|           <div class="integrated-graph-container"> | ||||
|             <KnowledgeGraph graphData={graphData} /> | ||||
|             {/* We will update graphData generation later */} | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Blog Grid (will be populated by JS) --> | ||||
|         {/* Blog Grid (will be populated by JS) */} | ||||
|         <div class="blog-grid" id="blog-grid"> | ||||
|           <div class="loading-indicator"> | ||||
|             <div class="loading-spinner"></div> | ||||
|  | @ -179,12 +168,12 @@ const commands = [ | |||
|     const searchInput = document.getElementById('search-input'); | ||||
|     const tagButtons = document.querySelectorAll('.tag-filter-btn'); | ||||
|     const blogGrid = document.getElementById('blog-grid'); | ||||
|     const graphFilters = document.querySelectorAll('.graph-filter'); | ||||
|     // Removed graphFilters as category filtering is removed from graph | ||||
|      | ||||
|     // State variables | ||||
|     let currentFilterTag = 'all'; | ||||
|     let currentSearchTerm = ''; | ||||
|     let currentGraphFilter = 'all'; | ||||
|     // Removed currentGraphFilter | ||||
|     let cy; // Cytoscape instance will be set by KnowledgeGraph component | ||||
|    | ||||
|     // Wait for cytoscape instance to be available | ||||
|  | @ -193,63 +182,70 @@ const commands = [ | |||
|       setupGraphInteractions(); | ||||
|     }); | ||||
|      | ||||
|     // Setup graph filtering and interactions | ||||
|     // Setup graph interactions (Post and Tag nodes) | ||||
|     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'); | ||||
|       if (!cy) { | ||||
|         console.error("Cytoscape instance not ready."); | ||||
|         return; | ||||
|       } | ||||
|         }); | ||||
|       }); | ||||
|        | ||||
|       // Click node to filter posts | ||||
|       // Remove previous category filter logic if any existed | ||||
|       // graphFilters.forEach(...) logic removed | ||||
| 
 | ||||
|       // Handle clicks on graph nodes | ||||
|       cy.on('tap', 'node', function(evt) { | ||||
|         const node = evt.target; | ||||
|         const slug = node.id(); | ||||
|         const nodeId = node.id(); | ||||
|         const nodeType = node.data('type'); // Get type ('post' or 'tag') | ||||
| 
 | ||||
|         // Scroll to the post in the blog grid | ||||
|         const post = postsData.find(p => p.slug === slug); | ||||
|         console.log(`Node clicked: ID=${nodeId}, Type=${nodeType}`); // Debug log | ||||
| 
 | ||||
|         if (nodeType === 'post') { | ||||
|           // Handle post node click: Find post, update search, filter grid, scroll | ||||
|           const post = postsData.find(p => p.slug === nodeId); | ||||
|           if (post) { | ||||
|           // Reset filters | ||||
|             console.log(`Post node clicked: ${post.title}`); | ||||
|             // Reset tag filter to 'all' when a specific post is selected via graph | ||||
|             currentFilterTag = 'all'; | ||||
|           searchInput.value = post.title; | ||||
|           currentSearchTerm = post.title; | ||||
|            | ||||
|           // Update UI | ||||
|             tagButtons.forEach(btn => btn.classList.remove('active')); | ||||
|           tagButtons[0].classList.add('active'); | ||||
|             const allButton = document.querySelector('.tag-filter-btn[data-tag="all"]'); | ||||
|             if (allButton) allButton.classList.add('active'); | ||||
| 
 | ||||
|           // Update grid with just this post | ||||
|             // Update search bar and term | ||||
|             searchInput.value = post.title; // Show post title in search | ||||
|             currentSearchTerm = post.title; // Filter grid by title | ||||
| 
 | ||||
|             // Update grid to show only this post (or matching search term) | ||||
|             updateGrid(); | ||||
|              | ||||
|           // Scroll to blog section | ||||
|           document.querySelector('.blog-posts-section').scrollIntoView({  | ||||
|             behavior: 'smooth', | ||||
|             block: 'start' | ||||
|           }); | ||||
|             // Scroll to the blog section smoothly | ||||
|             const blogSection = document.querySelector('.blog-posts-section'); | ||||
|             if (blogSection) { | ||||
|               blogSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); | ||||
|             } | ||||
|           } else { | ||||
|              console.warn(`Post data not found for slug: ${nodeId}`); | ||||
|           } | ||||
| 
 | ||||
|         } else if (nodeType === 'tag') { | ||||
|           // Handle tag node click: Simulate click on corresponding tag filter button | ||||
|           const tagName = nodeId.replace(/^tag-/, ''); // Extract tag name (remove 'tag-' prefix) | ||||
|           console.log(`Tag node clicked: ${tagName}`); | ||||
|            | ||||
|           const correspondingButton = document.querySelector(`.tag-filter-btn[data-tag="${tagName}"]`); | ||||
|            | ||||
|           if (correspondingButton) { | ||||
|             console.log(`Found corresponding button for tag: ${tagName}`); | ||||
|             // Simulate click on the button | ||||
|             correspondingButton.click(); | ||||
|             // Scroll to blog section smoothly | ||||
|             const blogSection = document.querySelector('.blog-posts-section'); | ||||
|             if (blogSection) { | ||||
|               blogSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); | ||||
|             } | ||||
|           } else { | ||||
|             console.warn(`Could not find tag filter button for tag: ${tagName}`); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|  | @ -326,26 +322,43 @@ const commands = [ | |||
|         if (filteredPosts.length > 0) { | ||||
|           blogGrid.innerHTML = filteredPosts.map(createPostCardHTML).join(''); | ||||
|            | ||||
|           // If graph is available, highlight matching nodes | ||||
|           // If graph is available, highlight post nodes shown in the grid | ||||
|           if (cy) { | ||||
|             const matchingSlugs = filteredPosts.map(post => post.slug); | ||||
|             const matchingPostSlugs = filteredPosts.map(post => post.slug); | ||||
|              | ||||
|             // Reset all nodes | ||||
|             cy.nodes().removeClass('highlighted').removeClass('filtered'); | ||||
|             // Reset styles on all nodes first | ||||
|             cy.nodes().removeClass('highlighted').removeClass('faded'); | ||||
|              | ||||
|             // Highlight matching nodes | ||||
|             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'); | ||||
|             // Highlight post nodes that are currently visible in the grid | ||||
|             cy.nodes('[type="post"]').forEach(node => { | ||||
|               if (matchingPostSlugs.includes(node.id())) { | ||||
|                 node.removeClass('faded').addClass('highlighted'); | ||||
|               } else { | ||||
|                 node.removeClass('highlighted').addClass('faded'); // Fade non-matching posts | ||||
|               } | ||||
|             }); | ||||
| 
 | ||||
|             // Highlight tag nodes connected to visible posts OR the currently selected tag | ||||
|             cy.nodes('[type="tag"]').forEach(tagNode => { | ||||
|               const tagName = tagNode.id().replace(/^tag-/, ''); | ||||
|               const isSelectedTag = tagName === currentFilterTag; | ||||
|               const isConnectedToVisiblePost = tagNode.connectedEdges().sources().some(postNode => matchingPostSlugs.includes(postNode.id())); | ||||
| 
 | ||||
|               if (isSelectedTag || (currentFilterTag === 'all' && isConnectedToVisiblePost)) { | ||||
|                  tagNode.removeClass('faded').addClass('highlighted'); | ||||
|               } else { | ||||
|                  tagNode.removeClass('highlighted').addClass('faded'); | ||||
|               } | ||||
|             }); | ||||
| 
 | ||||
|             // Adjust edge visibility based on connected highlighted nodes | ||||
|              cy.edges().forEach(edge => { | ||||
|                 if (edge.source().hasClass('highlighted') && edge.target().hasClass('highlighted')) { | ||||
|                     edge.removeClass('faded').addClass('highlighted'); | ||||
|                 } else { | ||||
|                     edge.removeClass('highlighted').addClass('faded'); | ||||
|                 } | ||||
|              }); | ||||
|           } | ||||
|         } else { | ||||
|           blogGrid.innerHTML = '<p class="no-results">No posts found matching your criteria.</p>'; | ||||
|  | @ -604,6 +617,17 @@ const commands = [ | |||
|     font-weight: 600; | ||||
|   } | ||||
| 
 | ||||
|   /* Styles for the integrated graph container */ | ||||
|   .integrated-graph-container { | ||||
|     margin-top: 2rem; /* Add space above the graph */ | ||||
|     height: 400px; /* Adjust height as needed */ | ||||
|     border: 1px solid var(--border-primary); | ||||
|     border-radius: 8px; | ||||
|     background: rgba(15, 23, 42, 0.3); /* Slightly different background */ | ||||
|     position: relative; /* Needed for Cytoscape */ | ||||
|     overflow: hidden; /* Hide scrollbars if graph overflows */ | ||||
|   } | ||||
|    | ||||
|   .blog-grid { | ||||
|     margin: 2rem 0 4rem; | ||||
|     display: grid; | ||||
|  |  | |||
|  | @ -1,17 +1,628 @@ | |||
| --- | ||||
| // src/pages/posts/[slug].astro | ||||
| 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() { | ||||
|   const postEntries = await getCollection('posts'); | ||||
|   return postEntries.map(entry => ({ | ||||
|     params: { slug: entry.slug }, props: { entry }, | ||||
|   try { | ||||
|     // Get posts from the posts collection | ||||
|     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 | ||||
| const { entry } = Astro.props; | ||||
| const { Content } = await entry.render(); | ||||
| // Get the post and all posts from props | ||||
| const { post, allPosts } = Astro.props; | ||||
| 
 | ||||
| // 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' | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -1,82 +1,132 @@ | |||
| /* Theme Variables - Dark/Light Mode Support */ | ||||
| /* src/styles/theme.css */ | ||||
| 
 | ||||
| /* Dark theme (default) */ | ||||
| html { | ||||
|   /* Keep the default dark theme as defined in BaseLayout */ | ||||
| /* Base Variables (Dark Mode Default) */ | ||||
| :root { | ||||
|   --bg-primary: #0f1219; | ||||
|   --bg-secondary: #161a24; | ||||
|   --bg-tertiary: #1e2330; | ||||
|   --bg-code: #1a1e2a; | ||||
|   --text-primary: #e2e8f0; | ||||
|   --text-secondary: #a0aec0; | ||||
|   --text-tertiary: #718096; | ||||
|   --accent-primary: #06b6d4; /* Cyan */ | ||||
|   --accent-secondary: #3b82f6; /* Blue */ | ||||
|   --accent-tertiary: #8b5cf6; /* Violet */ | ||||
|   --glow-primary: rgba(6, 182, 212, 0.2); | ||||
|   --glow-secondary: rgba(59, 130, 246, 0.2); | ||||
|   --glow-tertiary: rgba(139, 92, 246, 0.2); | ||||
|   --border-primary: rgba(255, 255, 255, 0.1); | ||||
|   --border-secondary: rgba(255, 255, 255, 0.05); | ||||
|   --card-bg: rgba(24, 28, 44, 0.5); | ||||
|   --card-border: rgba(56, 189, 248, 0.2); /* Cyan border */ | ||||
|   --ui-element: #1e293b; | ||||
|   --ui-element-hover: #334155; | ||||
|   --container-padding: clamp(1rem, 5vw, 3rem); | ||||
|   --font-size-xs: 0.75rem; | ||||
|   --font-size-sm: 0.875rem; | ||||
|   --font-size-md: 1rem; | ||||
|   --font-size-lg: 1.125rem; | ||||
|   --font-size-xl: 1.25rem; | ||||
|   --font-size-2xl: 1.5rem; | ||||
|   --font-size-3xl: 1.875rem; | ||||
|   --font-size-4xl: 2.25rem; | ||||
|   --font-size-5xl: 3rem; | ||||
|   --font-mono: 'JetBrains Mono', monospace; | ||||
|   --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | ||||
|   --bg-primary-rgb: 15, 18, 25; /* RGB for gradients */ | ||||
|   --bg-secondary-rgb: 22, 26, 36; /* RGB for gradients */ | ||||
| } | ||||
| 
 | ||||
| /* Light theme */ | ||||
| html.light-mode { | ||||
|   /* Primary Colors */ | ||||
|   --bg-primary: #f8fafc; | ||||
|   --bg-secondary: #f1f5f9; | ||||
|   --bg-tertiary: #e2e8f0; | ||||
| /* Light Mode Variables */ | ||||
| :root.light-mode { | ||||
|   --bg-primary: #ffffff; | ||||
|   --bg-secondary: #f8fafc; /* Lighter secondary */ | ||||
|   --bg-tertiary: #f1f5f9; /* Even lighter tertiary */ | ||||
|   --bg-code: #f1f5f9; | ||||
|   --text-primary: #0f172a; | ||||
|   --text-secondary: #334155; | ||||
|   --text-tertiary: #64748b; | ||||
|    | ||||
|   /* Accent Colors remain the same for brand consistency */ | ||||
|    | ||||
|   /* Glow Effects - lighter for light mode */ | ||||
|   --glow-primary: rgba(6, 182, 212, 0.1); | ||||
|   --glow-secondary: rgba(59, 130, 246, 0.1); | ||||
|   --glow-tertiary: rgba(139, 92, 246, 0.1); | ||||
|    | ||||
|   /* Border Colors */ | ||||
|   --border-primary: rgba(0, 0, 0, 0.1); | ||||
|   --text-primary: #1e293b; /* Darker primary text */ | ||||
|   --text-secondary: #475569; /* Darker secondary text */ | ||||
|   --text-tertiary: #64748b; /* Darker tertiary text */ | ||||
|   --accent-primary: #0891b2; /* Slightly darker cyan */ | ||||
|   --accent-secondary: #2563eb; /* Slightly darker blue */ | ||||
|   --accent-tertiary: #7c3aed; /* Slightly darker violet */ | ||||
|   --glow-primary: rgba(8, 145, 178, 0.15); | ||||
|   --glow-secondary: rgba(37, 99, 235, 0.15); | ||||
|   --glow-tertiary: rgba(124, 58, 237, 0.15); | ||||
|   --border-primary: rgba(0, 0, 0, 0.1); /* Darker borders */ | ||||
|   --border-secondary: rgba(0, 0, 0, 0.05); | ||||
|    | ||||
|   /* Card Background */ | ||||
|   --card-bg: rgba(255, 255, 255, 0.8); | ||||
|   --card-border: rgba(56, 189, 248, 0.3); /* Slightly stronger border */ | ||||
|    | ||||
|   /* UI Element Colors */ | ||||
|   --ui-element: #e2e8f0; | ||||
|   --card-bg: rgba(255, 255, 255, 0.8); /* White card with opacity */ | ||||
|   --card-border: rgba(37, 99, 235, 0.3); /* Blue border */ | ||||
|   --ui-element: #e2e8f0; /* Lighter UI elements */ | ||||
|   --ui-element-hover: #cbd5e1; | ||||
|   --bg-primary-rgb: 255, 255, 255; /* RGB for gradients */ | ||||
|   --bg-secondary-rgb: 248, 250, 252; /* RGB for gradients */ | ||||
| } | ||||
| 
 | ||||
| /* Background adjustments for light mode */ | ||||
| html.light-mode body { | ||||
|   background-image:  | ||||
|     radial-gradient(circle at 20% 35%, rgba(6, 182, 212, 0.05) 0%, transparent 50%), | ||||
|     radial-gradient(circle at 75% 15%, rgba(59, 130, 246, 0.05) 0%, transparent 45%), | ||||
|     radial-gradient(circle at 85% 70%, rgba(139, 92, 246, 0.05) 0%, transparent 40%); | ||||
| /* Ensure transitions for smooth theme changes */ | ||||
| *, *::before, *::after { | ||||
|   transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; | ||||
| } | ||||
| 
 | ||||
| /* Adding light mode grid overlay */ | ||||
| html.light-mode body::before { | ||||
|   background-image:  | ||||
|     linear-gradient(rgba(15, 23, 42, 0.03) 1px, transparent 1px), | ||||
|     linear-gradient(90deg, rgba(15, 23, 42, 0.03) 1px, transparent 1px); | ||||
| /* Knowledge Graph specific theme adjustments */ | ||||
| :root.light-mode .graph-container { | ||||
|   background: rgba(248, 250, 252, 0.3); | ||||
|   border: 1px solid var(--card-border); | ||||
| } | ||||
| 
 | ||||
| /* Theme transition for smooth switching */ | ||||
| html, body, * { | ||||
|   transition:  | ||||
|     background-color 0.3s ease, | ||||
|     color 0.3s ease, | ||||
|     border-color 0.3s ease, | ||||
|     box-shadow 0.3s ease; | ||||
| :root.light-mode .node-details { | ||||
|   background: rgba(255, 255, 255, 0.8); | ||||
|   box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); | ||||
| } | ||||
| 
 | ||||
| /* Knowledge Graph light mode adjustments */ | ||||
| html.light-mode .graph-container { | ||||
|   background: rgba(248, 250, 252, 0.6); | ||||
| :root.light-mode .graph-filters { | ||||
|   background: rgba(248, 250, 252, 0.7); | ||||
| } | ||||
| 
 | ||||
| html.light-mode .graph-loading { | ||||
|   background: rgba(241, 245, 249, 0.7); | ||||
| :root.light-mode .graph-filter { | ||||
|   color: var(--text-secondary); | ||||
|   border-color: var(--border-primary); | ||||
| } | ||||
| 
 | ||||
| html.light-mode .graph-filters { | ||||
|   background: rgba(241, 245, 249, 0.7); | ||||
| :root.light-mode .connections-list a { | ||||
|   color: var(--accent-secondary); | ||||
| } | ||||
| 
 | ||||
| html.light-mode .graph-legend { | ||||
|   background: rgba(241, 245, 249, 0.7); | ||||
| :root.light-mode .node-link { | ||||
|   box-shadow: 0 4px 10px rgba(8, 145, 178, 0.15); | ||||
| } | ||||
| 
 | ||||
| html.light-mode .node-details { | ||||
|   background: rgba(248, 250, 252, 0.9); | ||||
| /* 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 */ | ||||
| body { | ||||
|   background-color: var(--bg-primary); | ||||
|   color: var(--text-primary); | ||||
| } | ||||
| 
 | ||||
| a { | ||||
|   color: var(--accent-primary); | ||||
| } | ||||
| 
 | ||||
| /* Fix for inputs/selects */ | ||||
| input, select, textarea { | ||||
|   background-color: var(--bg-secondary); | ||||
|   color: var(--text-primary); | ||||
|   border-color: var(--border-primary); | ||||
| } | ||||
| 
 | ||||
| /* 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