updated the makefile, added new org files (2026), and started working on adding a search feature for the website.
This commit is contained in:
3475
assets/scripts/lunr.js
Normal file
3475
assets/scripts/lunr.js
Normal file
File diff suppressed because it is too large
Load Diff
210
assets/scripts/search.js
Normal file
210
assets/scripts/search.js
Normal file
@@ -0,0 +1,210 @@
|
||||
let lunrIndex;
|
||||
let documents = [];
|
||||
|
||||
/* ------------------------------
|
||||
JSON → documents
|
||||
-------------------------------- */
|
||||
function extractDocuments(node, currentPath = "") {
|
||||
if (node.type === "folder" && node.children) {
|
||||
const nextPath = currentPath
|
||||
? `${currentPath}/${node.name}`
|
||||
: node.name;
|
||||
|
||||
node.children.forEach(child => {
|
||||
extractDocuments(child, nextPath);
|
||||
});
|
||||
}
|
||||
|
||||
if (node.type === "file") {
|
||||
const filePath = currentPath
|
||||
? `${currentPath}/${node.name}`
|
||||
: node.name;
|
||||
|
||||
documents.push({
|
||||
id: documents.length.toString(),
|
||||
title: node.name,
|
||||
content: node.content || "",
|
||||
path: "/" + filePath.replace(/^output\//, "")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Lunr index
|
||||
-------------------------------- */
|
||||
function buildIndex() {
|
||||
lunrIndex = lunr(function () {
|
||||
this.ref("id");
|
||||
this.field("title", { boost: 10 });
|
||||
this.field("content");
|
||||
|
||||
documents.forEach(doc => this.add(doc));
|
||||
});
|
||||
|
||||
console.log("Lunr index built with", documents.length, "documents");
|
||||
}
|
||||
|
||||
async function initSearch() {
|
||||
const response = await fetch("test.json");
|
||||
const json = await response.json();
|
||||
|
||||
extractDocuments(json);
|
||||
buildIndex();
|
||||
}
|
||||
|
||||
initSearch();
|
||||
|
||||
/* ------------------------------
|
||||
Fuzzy search
|
||||
-------------------------------- */
|
||||
function buildFuzzyQuery(query) {
|
||||
return query
|
||||
.split(/\s+/)
|
||||
.map(term => `${term}~1`)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function search(query) {
|
||||
if (!lunrIndex) return [];
|
||||
|
||||
const results = lunrIndex.search(buildFuzzyQuery(query));
|
||||
|
||||
return results.map(r => {
|
||||
const doc = documents.find(d => d.id === r.ref);
|
||||
return {
|
||||
title: doc.title,
|
||||
path: doc.path,
|
||||
score: r.score,
|
||||
preview: doc.content.slice(0, 200) + "…"
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
DOM elements
|
||||
-------------------------------- */
|
||||
const searchInput = document.getElementById("search-input");
|
||||
const searchBtn = document.getElementById("search-btn");
|
||||
|
||||
/* ------------------------------
|
||||
Live dropdown results
|
||||
-------------------------------- */
|
||||
const resultsBox = document.createElement("div");
|
||||
resultsBox.id = "search-results";
|
||||
document.body.appendChild(resultsBox);
|
||||
|
||||
function showResults(results) {
|
||||
resultsBox.innerHTML = "";
|
||||
|
||||
if (results.length === 0) {
|
||||
resultsBox.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = searchInput.getBoundingClientRect();
|
||||
resultsBox.style.top = `${rect.bottom + window.scrollY}px`;
|
||||
resultsBox.style.left = `${rect.left + window.scrollX}px`;
|
||||
resultsBox.style.width = `${rect.width}px`;
|
||||
|
||||
results.slice(0, 5).forEach(result => {
|
||||
const item = document.createElement("div");
|
||||
item.className = "search-result";
|
||||
item.innerHTML = `
|
||||
<div class="search-result-title">${result.title}</div>
|
||||
<div class="search-result-preview">${result.preview}</div>
|
||||
`;
|
||||
item.onclick = () => window.location.href = result.path;
|
||||
resultsBox.appendChild(item);
|
||||
});
|
||||
|
||||
resultsBox.style.display = "block";
|
||||
}
|
||||
|
||||
searchInput.addEventListener("input", () => {
|
||||
const q = searchInput.value.trim();
|
||||
if (!q) {
|
||||
resultsBox.style.display = "none";
|
||||
return;
|
||||
}
|
||||
showResults(search(q));
|
||||
});
|
||||
|
||||
/* ------------------------------
|
||||
Modal popup
|
||||
-------------------------------- */
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "search-modal";
|
||||
modal.innerHTML = `
|
||||
<div class="search-modal-backdrop"></div>
|
||||
<div class="search-modal-content">
|
||||
<h2>Search results</h2>
|
||||
<div id="search-modal-results"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const modalResults = modal.querySelector("#search-modal-results");
|
||||
|
||||
function openSearchModal(results) {
|
||||
modalResults.innerHTML = "";
|
||||
|
||||
if (results.length === 0) {
|
||||
modalResults.innerHTML = "<p>No results found.</p>";
|
||||
}
|
||||
|
||||
results.forEach(result => {
|
||||
const item = document.createElement("div");
|
||||
item.className = "search-modal-result";
|
||||
item.innerHTML = `
|
||||
<div class="search-modal-title">${result.title}</div>
|
||||
<div class="search-modal-preview">${result.preview}</div>
|
||||
`;
|
||||
item.onclick = () => window.location.href = result.path;
|
||||
modalResults.appendChild(item);
|
||||
});
|
||||
|
||||
modal.style.display = "block";
|
||||
}
|
||||
|
||||
/* ------------------------------
|
||||
Submit search (Enter / 🔍)
|
||||
-------------------------------- */
|
||||
function submitSearch() {
|
||||
const q = searchInput.value.trim();
|
||||
if (!q) return;
|
||||
openSearchModal(search(q));
|
||||
}
|
||||
|
||||
searchInput.addEventListener("keydown", e => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
submitSearch();
|
||||
}
|
||||
});
|
||||
|
||||
searchBtn.addEventListener("click", submitSearch);
|
||||
|
||||
/* ------------------------------
|
||||
Close behaviours
|
||||
-------------------------------- */
|
||||
document.addEventListener("click", e => {
|
||||
if (
|
||||
!resultsBox.contains(e.target) &&
|
||||
e.target !== searchInput &&
|
||||
e.target !== searchBtn
|
||||
) {
|
||||
resultsBox.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
modal.addEventListener("click", e => {
|
||||
if (e.target.classList.contains("search-modal-backdrop")) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", e => {
|
||||
if (e.key === "Escape") {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
});
|
||||
@@ -225,3 +225,123 @@ time.countdown.expired {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/* Make nav a flex container */
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Push search to the far right */
|
||||
#search-input,
|
||||
#search-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Input styling */
|
||||
#search-input {
|
||||
width: 12rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid var(--border-color, #ccc);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-color, #fff);
|
||||
color: var(--text-color, #000);
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
#search-btn {
|
||||
margin-left: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Focus state */
|
||||
#search-input:focus {
|
||||
outline: 2px solid var(--accent-color, #5b8cff);
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
#search-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#search-btn {
|
||||
font-size: 1.4rem;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
}
|
||||
#search-results {
|
||||
position: absolute;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-color, #fff);
|
||||
border: 1px solid var(--border-color, #ccc);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.12);
|
||||
display: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
.search-result {
|
||||
padding: 0.6rem 0.8rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.search-result:hover {
|
||||
background: var(--accent-bg, #f2f6ff);
|
||||
}
|
||||
|
||||
.search-result-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.search-result-preview {
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
}
|
||||
#search-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: none;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.search-modal-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.45);
|
||||
}
|
||||
|
||||
.search-modal-content {
|
||||
position: relative;
|
||||
max-width: 700px;
|
||||
margin: 10vh auto;
|
||||
padding: 1.2rem;
|
||||
background: var(--bg-color, #fff);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.25);
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-modal-result {
|
||||
padding: 0.8rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-modal-result:hover {
|
||||
background: var(--accent-bg, #f2f6ff);
|
||||
}
|
||||
|
||||
.search-modal-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-modal-preview {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user