diff --git a/hugo.toml b/hugo.toml index f602571..df28889 100644 --- a/hugo.toml +++ b/hugo.toml @@ -74,3 +74,6 @@ weight = 4 posts = '/posts/:year/:month/:slug/' [permalinks.section] posts = '/posts/' + +[outputs] + home = ["HTML", "RSS", "JSON"] diff --git a/themes/aottr/assets/css/search.css b/themes/aottr/assets/css/search.css new file mode 100644 index 0000000..93869be --- /dev/null +++ b/themes/aottr/assets/css/search.css @@ -0,0 +1,206 @@ +/* Search Container - no longer needed, modal is directly in body */ + +/* Search Toggle Button in Menu - styled like menu items */ +.search-toggle-menu { + background: none; + border: none; + cursor: pointer; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + color: rgb(249 168 212); + border-bottom-width: 4px; + border-color: transparent; + text-decoration: none; + width: 100%; + height: 100%; + pointer-events: auto; + position: relative; + z-index: 1; +} + +.search-toggle-menu svg { + pointer-events: none; +} + +.search-toggle-menu:hover { + background-color: rgb(249 168 212); + color: rgb(28 25 23); +} + +.search-toggle-menu svg { + height: 1.75rem; + width: 1.75rem; +} + +/* Make sure the search button li behaves like other menu items */ +header nav ul li:has(.search-toggle-menu) { + display: flex; + align-items: center; +} + +/* Search Modal */ +.search-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); + z-index: 1000; + display: flex; + align-items: flex-start; + justify-content: center; + padding: 2rem 1rem; + overflow-y: auto; +} + +.search-modal-hidden { + display: none !important; +} + +.search-modal-content { + background: rgb(28 25 23); + border-radius: 8px; + width: 100%; + max-width: 700px; + max-height: 80vh; + display: flex; + flex-direction: column; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2); + border: 1px solid rgb(68 64 60); +} + +/* Search Header */ +.search-header { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 1rem; + border-bottom: 1px solid rgb(68 64 60); +} + +.search-input { + flex: 1; + padding: 0.75rem 1rem; + border: 1px solid rgb(68 64 60); + border-radius: 6px; + font-size: 1rem; + background: rgb(41 37 36); + color: rgb(250 250 249); + outline: none; +} + +.search-input:focus { + border-color: rgb(249 168 212); + box-shadow: 0 0 0 3px rgba(249, 168, 212, 0.1); +} + +.search-close { + background: none; + border: none; + cursor: pointer; + padding: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + color: rgb(249 168 212); +} + +.search-close:hover { + background-color: rgb(249 168 212); + color: rgb(28 25 23); +} + +/* Search Results */ +.search-results { + padding: 1rem; + overflow-y: auto; + flex: 1; +} + +.search-result-item { + display: block; + padding: 1rem; + margin-bottom: 0.5rem; + border-radius: 6px; + text-decoration: none; + color: rgb(250 250 249); + border: 1px solid transparent; +} + +.search-result-title { + margin: 0 0 0.5rem 0; + font-size: 1.125rem; + font-weight: 600; + line-height: 1.4; + color: rgb(249 168 212); +} + +.search-result-item:hover { + background-color: rgba(249, 168, 212, 0.25); + border-color: rgba(249, 168, 212, 0.3); +} + +.search-result-item:hover .search-result-title, +.search-result-item:hover .search-result-date, +.search-result-item:hover .search-result-summary { + color: rgb(249 168 212); +} + +.search-result-item:hover .search-tag { + background-color: rgb(28 25 23); + color: rgb(249 168 212); +} + +.search-result-title mark { + background-color: rgb(217 119 6); + color: rgb(255 255 255); + padding: 0.125rem 0.25rem; + border-radius: 3px; + font-weight: 700; +} + +.search-result-item:hover .search-result-title mark { + background-color: rgb(28 25 23); + color: rgb(249 168 212); +} + +.search-result-date { + display: inline-block; + font-size: 0.875rem; + color: rgb(168 162 158); + margin-bottom: 0.5rem; +} + +.search-result-summary { + margin: 0.5rem 0 0 0; + font-size: 0.9375rem; + line-height: 1.6; + color: rgb(168 162 158); +} + +.search-result-tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.75rem; +} + +.search-tag { + display: inline-block; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + background-color: rgb(68 64 60); + color: rgb(250 250 249); + border-radius: 4px; +} + +.search-no-results, +.search-error { + padding: 2rem; + text-align: center; + color: rgb(168 162 158); +} diff --git a/themes/aottr/assets/js/search.js b/themes/aottr/assets/js/search.js new file mode 100644 index 0000000..5b15d55 --- /dev/null +++ b/themes/aottr/assets/js/search.js @@ -0,0 +1,171 @@ +// Search functionality using Fuse.js +let searchIndex = null; +let fuse = null; + +function openSearchModal() { + const searchModal = document.getElementById('search-modal'); + const searchInput = document.getElementById('search-input'); + + if (!searchModal || !searchInput) { + return; + } + + searchModal.classList.remove('search-modal-hidden'); + searchModal.style.display = 'flex'; + + searchInput.focus(); + document.body.style.overflow = 'hidden'; +} + +// Initialize search when DOM is ready +document.addEventListener('DOMContentLoaded', async function() { + const searchToggle = document.getElementById('search-toggle'); + const searchModal = document.getElementById('search-modal'); + const searchClose = document.getElementById('search-close'); + const searchInput = document.getElementById('search-input'); + const searchResults = document.getElementById('search-results'); + + if (!searchToggle || !searchModal || !searchInput) { + return; + } + + // Open search modal + searchToggle.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + openSearchModal(); + }); + + // Close search modal + function closeSearch() { + searchModal.classList.add('search-modal-hidden'); + searchModal.style.display = 'none'; + searchInput.value = ''; + searchResults.innerHTML = ''; + document.body.style.overflow = ''; + } + + searchClose.addEventListener('click', closeSearch); + + // Close on Escape key + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape' && searchModal.style.display === 'flex') { + closeSearch(); + } + }); + + // Close on background click + searchModal.addEventListener('click', function(e) { + if (e.target === searchModal) { + closeSearch(); + } + }); + + // Load search index and initialize Fuse.js + async function initializeSearch() { + // Check if Fuse.js is loaded + if (typeof Fuse === 'undefined') { + console.error('Fuse.js is not loaded. Please ensure the Fuse.js script is loaded before search.js'); + return false; + } + + try { + const response = await fetch('/index.json'); + if (!response.ok) { + throw new Error('Failed to load search index'); + } + const data = await response.json(); + searchIndex = Array.isArray(data) ? data : (data.index || []); + + if (searchIndex.length === 0) { + console.warn('Search index is empty'); + return false; + } + + // Initialize Fuse.js + fuse = new Fuse(searchIndex, { + keys: [ + { name: 'title', weight: 0.7 }, + { name: 'content', weight: 0.3 }, + { name: 'tags', weight: 0.2 } + ], + threshold: 0.3, // 0.0 = exact match, 1.0 = match anything + includeScore: true, + minMatchCharLength: 2, + ignoreLocation: true, + findAllMatches: true + }); + return true; + } catch (error) { + console.error('Error loading search index:', error); + return false; + } + } + + // Initialize search + const searchInitialized = await initializeSearch(); + if (!searchInitialized) { + console.warn('Search initialization failed, but modal can still be opened'); + } + + // Perform search on input + let searchTimeout; + searchInput.addEventListener('input', function(e) { + const query = e.target.value.trim(); + + clearTimeout(searchTimeout); + + if (!query) { + searchResults.innerHTML = ''; + return; + } + + if (!fuse) { + searchResults.innerHTML = '
Search is not available. Please refresh the page.
'; + return; + } + + // Debounce search + searchTimeout = setTimeout(() => { + performSearch(query); + }, 150); + }); + + function performSearch(query) { + const results = fuse.search(query); + + if (results.length === 0) { + searchResults.innerHTML = 'No articles found matching your search.
'; + return; + } + + // Display results (limit to 10) + const resultsHTML = results.slice(0, 10).map(result => { + const item = result.item; + const score = result.score; + const title = highlightMatch(item.title, query); + const summary = item.summary || item.content.substring(0, 150) + '...'; + const date = item.date ? new Date(item.date).toLocaleDateString() : ''; + const tags = item.tags && item.tags.length > 0 + ? `` + : ''; + + return ` + +${summary}
+ ${tags} + + `; + }).join(''); + + searchResults.innerHTML = resultsHTML; + } + + function highlightMatch(text, query) { + if (!query) return text; + const regex = new RegExp(`(${query})`, 'gi'); + return text.replace(regex, '$1'); + } +}); diff --git a/themes/aottr/layouts/index.json b/themes/aottr/layouts/index.json new file mode 100644 index 0000000..0f9d066 --- /dev/null +++ b/themes/aottr/layouts/index.json @@ -0,0 +1,16 @@ +{{- $.Scratch.Add "index" slice -}} +{{- $pages := where site.RegularPages "Type" "posts" -}} +{{- if not $pages -}} + {{- $pages = site.RegularPages -}} +{{- end -}} +{{- range $pages -}} + {{- $.Scratch.Add "index" (dict + "title" .Title + "content" (.Plain | htmlUnescape) + "summary" .Summary + "permalink" .Permalink + "date" (.Date.Format "2006-01-02") + "tags" (.Params.tags | default slice) + ) -}} +{{- end -}} +{{- $.Scratch.Get "index" | jsonify -}} diff --git a/themes/aottr/layouts/partials/head.html b/themes/aottr/layouts/partials/head.html index e15c318..6c75f9b 100644 --- a/themes/aottr/layouts/partials/head.html +++ b/themes/aottr/layouts/partials/head.html @@ -3,4 +3,6 @@