505 lines
13 KiB
Plaintext
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> |