argobox/src/components/Header.astro

536 lines
14 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: 'Tech Stack', url: '/tech-stack' },
{ name: 'Home Lab', url: '/homelab' }, // Updated URL
{ name: 'Resources', url: '/resources' },
{ name: 'About', url: 'https://ArgoBox.com' },
{ name: 'Contact', url: 'https://ArgoBox.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">ArgoBox</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 site-wide filtering (User provided version)
const searchResults = document.getElementById('search-results'); // Assuming this ID exists in your dropdown HTML
// 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 {
// Fetch the search index that contains all site content
const response = await fetch('/search-index.json'); // Ensure this path is correct based on your build output
if (!response.ok) throw new Error('Failed to fetch search data');
const allContent = await response.json();
const results = allContent.filter(item => {
const lowerQuery = query.toLowerCase();
return (
item.title.toLowerCase().includes(lowerQuery) ||
item.description?.toLowerCase().includes(lowerQuery) ||
item.tags?.some(tag => tag.toLowerCase().includes(lowerQuery)) ||
item.category?.toLowerCase().includes(lowerQuery)
);
}).slice(0, 8); // Limit to 8 results for better UI
// Display results
if (searchResults) {
if (results.length > 0) {
searchResults.innerHTML = results.map(item => {
// Create type badge
let typeBadge = '';
switch(item.type) {
case 'post':
typeBadge = '<span class="result-type post">Blog</span>';
break;
case 'project':
typeBadge = '<span class="result-type project">Project</span>';
break;
case 'configuration':
typeBadge = '<span class="result-type config">Config</span>';
break;
case 'external':
typeBadge = '<span class="result-type external">External</span>';
break;
default:
typeBadge = '<span class="result-type">Content</span>';
}
return `
<div class="search-result-item" data-url="${item.url}">
<div class="search-result-header">
<div class="search-result-title">${item.title}</div>
${typeBadge}
</div>
<div class="search-result-snippet">${item.description || ''}</div>
${item.tags && item.tags.length > 0 ?
`<div class="search-result-tags">
${item.tags.slice(0, 3).map(tag => `<span class="search-tag">${tag}</span>`).join('')}
</div>` : ''
}
</div>
`;
}).join('');
// Add click handlers to results
document.querySelectorAll('.search-result-item').forEach(item => {
item.addEventListener('click', () => {
window.location.href = item.dataset.url; // Navigate to the item's URL
});
});
} else {
searchResults.innerHTML = '<div class="no-results">No matching content 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 with debounce
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 (if your input is inside a form)
const searchForm = searchInput?.closest('form');
searchForm?.addEventListener('submit', (e) => {
e.preventDefault(); // Prevent default form submission
performSearch(searchInput.value);
});
// Handle search-submit button click (if you have a separate submit button)
const searchSubmit = document.querySelector('.search-submit'); // Adjust selector if needed
searchSubmit?.addEventListener('click', () => {
performSearch(searchInput?.value || '');
});
}); // End of DOMContentLoaded
</script>