laforceit-blog/src/components/Header.astro

505 lines
13 KiB
Plaintext

---
// src/components/Header.astro
import ThemeToggler from './ThemeToggler.astro';
// Define navigation items with proper URLs
const navItems = [
{ 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="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 class="main-nav">
<ul class="nav-list">
{navItems.map(item => (
<li class="nav-item">
<a
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}
</a>
</li>
))}
</ul>
</nav>
<div class="header-actions">
<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 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>
</button>
</div>
</header>
<style>
.site-header {
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;
}
.header-container {
display: flex;
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 {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
color: var(--bg-primary);
font-weight: bold;
border-radius: 8px;
margin-right: 0.75rem;
font-family: var(--font-sans);
}
.site-name {
display: flex;
flex-direction: column;
}
.site-title {
font-weight: 600;
font-size: 1.25rem;
color: var(--text-primary);
}
.site-subtitle {
font-size: 0.75rem;
color: var(--text-tertiary);
letter-spacing: 0.5px;
}
.main-nav {
display: flex;
margin-left: auto;
}
.nav-list {
display: flex;
list-style: none;
padding: 0;
margin: 0;
gap: 1rem;
}
.nav-link {
color: var(--text-secondary);
text-decoration: none;
font-size: 0.9rem;
padding: 0.5rem 0.75rem;
border-radius: 6px;
transition: all 0.2s ease;
position: relative;
}
.nav-link:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.05);
}
.nav-link.active {
color: var(--accent-primary);
font-weight: 500;
}
.nav-link.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0.75rem;
right: 0.75rem;
height: 2px;
background: var(--accent-primary);
border-radius: 1px;
}
.header-actions {
display: flex;
align-items: center;
gap: 0.75rem;
margin-left: 1rem;
}
/* Search Dropdown Styles */
.search-container {
position: relative;
}
.search-toggle {
background: none;
border: none;
padding: 0.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
}
.search-toggle:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.05);
}
.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);
}
.search-dropdown.active {
transform: scale(1);
opacity: 1;
visibility: visible;
}
.search-input-wrapper {
display: flex;
align-items: center;
position: relative;
}
.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-secondary);
cursor: pointer;
padding: 0.5rem;
}
/* Responsive Adjustments */
@media (max-width: 1024px) {
.nav-link {
padding: 0.5rem;
}
}
@media (max-width: 768px) {
.main-nav {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg-secondary);
padding: 1rem;
border-bottom: 1px solid var(--border-primary);
}
.main-nav.active {
display: block;
}
.nav-list {
flex-direction: column;
gap: 0.5rem;
}
.nav-link {
display: block;
padding: 0.75rem 1rem;
}
.nav-link.active::after {
display: none;
}
.mobile-menu-toggle {
display: block;
}
.search-dropdown {
width: 260px;
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Mobile menu toggle
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;
}
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 {
searchResults.innerHTML = '<div class="no-results">No matching posts found</div>';
}
}
} catch (error) {
console.error('Search error:', error);
if (searchResults) {
searchResults.innerHTML = '<div class="no-results">Error performing search</div>';
}
}
};
// Search input event handler
let searchTimeout;
searchInput?.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
performSearch(e.target.value);
}, 300); // Debounce to avoid too many searches while typing
});
// Handle search form submission
const searchForm = searchInput?.closest('form');
searchForm?.addEventListener('submit', (e) => {
e.preventDefault();
performSearch(searchInput.value);
});
// Handle search-submit button click
const searchSubmit = document.querySelector('.search-submit');
searchSubmit?.addEventListener('click', () => {
performSearch(searchInput?.value || '');
});
});
</script>