refactor: Consolidate script imports to use main.js bundle

- dashboard.templ: Replaced individual script tags with single main.js import
- admin_users.templ, custom_section.templ: Minor formatting/cleanup
- docs.templ: Updated script imports (excluded from Alpine conversion per user request)
- api.ts, docs.ts, password_validation.ts: Minor updates for compatibility
This commit is contained in:
2026-03-08 21:37:17 -04:00
parent c0290bb106
commit 0919698cf4
7 changed files with 568 additions and 510 deletions
+227 -213
View File
@@ -1,26 +1,22 @@
package templates
import (
"fmt"
)
import "fmt"
templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{ doc.Title } - Bookhoard Documentation</title>
<link href="/static/style.css" rel="stylesheet">
<link rel="stylesheet" href="/static/highlight-dark.min.css">
<script src="/static/highlight.min.js"></script>
<script src="/static/lunr.min.js"></script>
<script src="/static/lunr-flex.min.js"></script>
<link href="/static/style.css" rel="stylesheet"/>
<link rel="stylesheet" href="/static/highlight-dark.min.css"/>
<script src="/static/main.js"></script>
</head>
<body class={ "theme-" + user.Theme + " page-docs bg-background-primary text-text-primary font-sans antialiased" }>
@Header(user, currentPath)
<!-- Mobile Menu Button -->
<button
<button
class="lg:hidden fixed top-4 left-4 z-50 bg-background-secondary border border-border rounded p-2 text-text-primary hover:bg-background-secondary/80"
onclick="toggleSidebar()"
aria-label="Toggle menu"
@@ -29,27 +25,23 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
<!-- Search Results Overlay -->
<div id="search-results" class="hidden fixed inset-0 bg-black/95 z-50 overflow-y-auto p-8 transition-opacity duration-200 ease-in-out"></div>
<!-- Sidebar -->
<div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary border-r border-border overflow-y-auto mt-16 lg:translate-x-0 transition-transform duration-300 ease-in-out">
<!-- Search -->
<div class="p-4 border-b border-border">
<input
<input
type="text"
class="search-input w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent text-sm"
placeholder="Search documentation..."
id="docs-search"
>
/>
</div>
<!-- Navigation Sections -->
for _, section := range nav.Sections {
<div class="nav-section mb-6">
<div
<div
class="nav-section-title font-semibold text-xs uppercase tracking-wider text-text-secondary px-4 py-3 cursor-pointer select-none flex items-center justify-between"
onclick="toggleSection(this)"
>
@@ -78,7 +70,6 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
</div>
}
</div>
<!-- Main Content -->
<main class="main-content lg:ml-72 p-8 max-w-4xl mx-auto">
<!-- Breadcrumb -->
@@ -92,10 +83,8 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
}
</nav>
}
<!-- Title -->
<h1 class="text-4xl font-bold mb-8 text-text-primary">{ doc.Title }</h1>
<!-- Table of Contents -->
if len(doc.TOC) > 0 {
<details class="toc bg-background-secondary p-4 rounded-lg mb-8 border border-border">
@@ -104,8 +93,8 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
<span class="ml-2 text-text-secondary text-xs"></span>
</summary>
for _, item := range doc.TOC {
<a
href={ "#" + item.Anchor }
<a
href={ "#" + item.Anchor }
class="toc-item block py-1 text-text-secondary hover:text-accent text-sm no-underline"
style={ "margin-left: " + fmt.Sprintf("%drem", item.Level) }
>
@@ -114,155 +103,161 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
}
</details>
}
<!-- Content -->
<div class="prose prose-invert max-w-none">
@UnsafeHTML(doc.Content).ToComponent()
</div>
</main>
<script>
// Toggle sidebar section
function toggleSection(el) {
const items = el.nextElementSibling;
const arrow = el.querySelector('.section-arrow');
const isCollapsed = items.getAttribute('data-collapsed') === 'true';
const arrow = el.querySelector(".section-arrow");
const isCollapsed = items.getAttribute("data-collapsed") === "true";
if (isCollapsed) {
items.classList.remove('hidden');
items.setAttribute('data-collapsed', 'false');
arrow.style.transform = 'rotate(0deg)';
items.classList.remove("hidden");
items.setAttribute("data-collapsed", "false");
arrow.style.transform = "rotate(0deg)";
} else {
items.classList.add('hidden');
items.setAttribute('data-collapsed', 'true');
arrow.style.transform = 'rotate(-90deg)';
items.classList.add("hidden");
items.setAttribute("data-collapsed", "true");
arrow.style.transform = "rotate(-90deg)";
}
}
// Toggle sidebar on mobile
function toggleSidebar() {
const sidebar = document.querySelector('.sidebar');
sidebar.classList.toggle('open');
const sidebar = document.querySelector(".sidebar");
sidebar.classList.toggle("open");
}
// Highlight current page in nav
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener("DOMContentLoaded", function () {
const currentPath = window.location.pathname;
document.querySelectorAll('.nav-item').forEach(item => {
if (item.getAttribute('href') === currentPath) {
item.classList.add('text-accent', 'bg-accent/10', 'border-r-2', 'border-accent');
document.querySelectorAll(".nav-item").forEach((item) => {
if (item.getAttribute("href") === currentPath) {
item.classList.add(
"text-accent",
"bg-accent/10",
"border-r-2",
"border-accent",
);
}
});
// Initialize syntax highlighting
if (typeof hljs !== 'undefined') {
if (typeof hljs !== "undefined") {
hljs.highlightAll();
// Add copy buttons to all code blocks
document.querySelectorAll('pre code').forEach((block) => {
const pre = block.parentElement;
const wrapper = document.createElement('div');
wrapper.className = 'relative group';
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const button = document.createElement('button');
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
button.textContent = 'Copy';
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = 'Copied!';
setTimeout(() => {
button.textContent = 'Copy';
}, 2000);
};
wrapper.appendChild(button);
});
// Add copy buttons to all code blocks
document.querySelectorAll("pre code").forEach((block) => {
const pre = block.parentElement;
const wrapper = document.createElement("div");
wrapper.className = "relative group";
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const button = document.createElement("button");
button.className =
"absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
button.textContent = "Copy";
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = "Copy";
}, 2000);
};
wrapper.appendChild(button);
});
}
// Add copy buttons to all code blocks
document.querySelectorAll('pre code').forEach((block) => {
document.querySelectorAll("pre code").forEach((block) => {
const pre = block.parentElement;
const wrapper = document.createElement('div');
wrapper.className = 'relative group';
const wrapper = document.createElement("div");
wrapper.className = "relative group";
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const button = document.createElement('button');
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
button.textContent = 'Copy';
const button = document.createElement("button");
button.className =
"absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
button.textContent = "Copy";
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = 'Copied!';
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = 'Copy';
button.textContent = "Copy";
}, 2000);
};
wrapper.appendChild(button);
});
});
// Add copy buttons to all code blocks
document.querySelectorAll('pre code').forEach((block) => {
const pre = block.parentElement;
const wrapper = document.createElement('div');
wrapper.className = 'relative group';
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const button = document.createElement('button');
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
button.textContent = 'Copy';
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = 'Copied!';
setTimeout(() => {
button.textContent = 'Copy';
}, 2000);
};
wrapper.appendChild(button);
});
// Add copy buttons to all code blocks
document.querySelectorAll("pre code").forEach((block) => {
const pre = block.parentElement;
const wrapper = document.createElement("div");
wrapper.className = "relative group";
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const button = document.createElement("button");
button.className =
"absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
button.textContent = "Copy";
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = "Copy";
}, 2000);
};
wrapper.appendChild(button);
});
// Search state
let idx;
let searchDocs = [];
// Load index on page load
fetch('/docs/search-index.json')
.then(r => r.json())
.then(data => {
idx = lunr(function() {
fetch("/docs/search-index.json")
.then((r) => r.json())
.then((data) => {
idx = lunr(function () {
this.use(lunr.flex);
this.ref('id');
this.field('title', {boost: 10});
this.field('content', {boost: 1});
data.forEach(doc => this.add(doc));
this.ref("id");
this.field("title", { boost: 10 });
this.field("content", { boost: 1 });
data.forEach((doc) => this.add(doc));
});
searchDocs = data;
console.log('Search index loaded:', data.length, 'documents');
console.log("Search index loaded:", data.length, "documents");
})
.catch(err => console.error('Failed to load search index:', err));
.catch((err) => console.error("Failed to load search index:", err));
// Search input with debounce
const searchInput = document.getElementById('docs-search');
const searchResultsDiv = document.getElementById('search-results');
const searchInput = document.getElementById("docs-search");
const searchResultsDiv = document.getElementById("search-results");
let debounceTimer;
searchInput.addEventListener('input', (e) => {
searchInput.addEventListener("input", (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const query = e.target.value.trim();
if (query.length < 2) {
searchResultsDiv.classList.add('hidden');
searchResultsDiv.classList.add("hidden");
return;
}
// Search with fuzzy matching
const results = idx.search(query, {
fields: {title: {boost: 10}, content: 1},
expand: true
fields: { title: { boost: 10 }, content: 1 },
expand: true,
});
// Render results
@@ -273,26 +268,29 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
// Render search results
function renderSearchResults(results, query) {
if (results.length === 0) {
searchResultsDiv.innerHTML = '<div class="text-center py-8 text-text-secondary">No results found</div>';
searchResultsDiv.classList.remove('hidden');
searchResultsDiv.innerHTML =
'<div class="text-center py-8 text-text-secondary">No results found</div>';
searchResultsDiv.classList.remove("hidden");
return;
}
searchResultsDiv.innerHTML = results.map(r => {
const doc = searchDocs.find(d => d.id === r.ref);
if (!doc) return '';
searchResultsDiv.innerHTML = results
.map((r) => {
const doc = searchDocs.find((d) => d.id === r.ref);
if (!doc) return "";
const snippet = getSnippet(doc.content, query);
const snippet = getSnippet(doc.content, query);
return `
return `
<a href="${doc.url}" class="block p-4 border-b border-border hover:bg-background-secondary text-text-primary no-underline">
<div class="font-semibold mb-2">${doc.title}</div>
<div class="text-sm text-text-secondary">${snippet}...</div>
</a>
`;
}).join('');
})
.join("");
searchResultsDiv.classList.remove('hidden');
searchResultsDiv.classList.remove("hidden");
}
function getSnippet(content, query) {
@@ -304,23 +302,26 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
if (idx !== -1) {
const start = Math.max(0, idx - 50);
const end = Math.min(content.length, idx + 100);
return '...' + content.substring(start, end) + '...';
return "..." + content.substring(start, end) + "...";
}
}
return content.substring(0, 150) + '...';
return content.substring(0, 150) + "...";
}
// Close search results when clicking outside
document.addEventListener('click', (e) => {
if (!searchResultsDiv.contains(e.target) && e.target !== searchInput) {
searchResultsDiv.classList.add('hidden');
document.addEventListener("click", (e) => {
if (
!searchResultsDiv.contains(e.target) &&
e.target !== searchInput
) {
searchResultsDiv.classList.add("hidden");
}
});
// Close search on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
searchResultsDiv.classList.add('hidden');
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
searchResultsDiv.classList.add("hidden");
searchInput.blur();
}
});
@@ -328,129 +329,136 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
<script>
// Toggle sidebar on mobile
function toggleSidebar() {
const sidebar = document.querySelector('.sidebar');
sidebar.classList.toggle('open');
const sidebar = document.querySelector(".sidebar");
sidebar.classList.toggle("open");
}
// Highlight current page in nav
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener("DOMContentLoaded", function () {
const currentPath = window.location.pathname;
document.querySelectorAll('.nav-item').forEach(item => {
if (item.getAttribute('href') === currentPath) {
item.classList.add('text-accent', 'bg-accent/10', 'border-r-2', 'border-accent');
document.querySelectorAll(".nav-item").forEach((item) => {
if (item.getAttribute("href") === currentPath) {
item.classList.add(
"text-accent",
"bg-accent/10",
"border-r-2",
"border-accent",
);
}
});
// Initialize syntax highlighting
if (typeof hljs !== 'undefined') {
if (typeof hljs !== "undefined") {
hljs.highlightAll();
// Add copy buttons to all code blocks
document.querySelectorAll('pre code').forEach((block) => {
const pre = block.parentElement;
const wrapper = document.createElement('div');
wrapper.className = 'relative group';
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const button = document.createElement('button');
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
button.textContent = 'Copy';
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = 'Copied!';
setTimeout(() => {
button.textContent = 'Copy';
}, 2000);
};
wrapper.appendChild(button);
});
// Add copy buttons to all code blocks
document.querySelectorAll("pre code").forEach((block) => {
const pre = block.parentElement;
const wrapper = document.createElement("div");
wrapper.className = "relative group";
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const button = document.createElement("button");
button.className =
"absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
button.textContent = "Copy";
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = "Copy";
}, 2000);
};
wrapper.appendChild(button);
});
}
// Add copy buttons to all code blocks
document.querySelectorAll('pre code').forEach((block) => {
document.querySelectorAll("pre code").forEach((block) => {
const pre = block.parentElement;
const wrapper = document.createElement('div');
wrapper.className = 'relative group';
const wrapper = document.createElement("div");
wrapper.className = "relative group";
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const button = document.createElement('button');
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
button.textContent = 'Copy';
const button = document.createElement("button");
button.className =
"absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
button.textContent = "Copy";
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = 'Copied!';
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = 'Copy';
button.textContent = "Copy";
}, 2000);
};
wrapper.appendChild(button);
});
});
// Add copy buttons to all code blocks
document.querySelectorAll('pre code').forEach((block) => {
const pre = block.parentElement;
const wrapper = document.createElement('div');
wrapper.className = 'relative group';
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const button = document.createElement('button');
button.className = 'absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent';
button.textContent = 'Copy';
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = 'Copied!';
setTimeout(() => {
button.textContent = 'Copy';
}, 2000);
};
wrapper.appendChild(button);
});
// Add copy buttons to all code blocks
document.querySelectorAll("pre code").forEach((block) => {
const pre = block.parentElement;
const wrapper = document.createElement("div");
wrapper.className = "relative group";
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const button = document.createElement("button");
button.className =
"absolute top-2 right-2 bg-background-secondary text-text-secondary px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity border border-border hover:text-accent";
button.textContent = "Copy";
button.onclick = async () => {
await navigator.clipboard.writeText(block.textContent);
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = "Copy";
}, 2000);
};
wrapper.appendChild(button);
});
// Search state
let idx;
let searchDocs = [];
// Load index on page load
fetch('/docs/search-index.json')
.then(r => r.json())
.then(data => {
idx = lunr(function() {
fetch("/docs/search-index.json")
.then((r) => r.json())
.then((data) => {
idx = lunr(function () {
this.use(lunr.flex);
this.ref('id');
this.field('title', {boost: 10});
this.field('content', {boost: 1});
data.forEach(doc => this.add(doc));
this.ref("id");
this.field("title", { boost: 10 });
this.field("content", { boost: 1 });
data.forEach((doc) => this.add(doc));
});
searchDocs = data;
console.log('Search index loaded:', data.length, 'documents');
console.log("Search index loaded:", data.length, "documents");
})
.catch(err => console.error('Failed to load search index:', err));
.catch((err) => console.error("Failed to load search index:", err));
// Search input with debounce
const searchInput = document.getElementById('docs-search');
const searchResultsDiv = document.getElementById('search-results');
const searchInput = document.getElementById("docs-search");
const searchResultsDiv = document.getElementById("search-results");
let debounceTimer;
searchInput.addEventListener('input', (e) => {
searchInput.addEventListener("input", (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const query = e.target.value.trim();
if (query.length < 2) {
searchResultsDiv.classList.add('hidden');
searchResultsDiv.classList.add("hidden");
return;
}
// Search with fuzzy matching
const results = idx.search(query, {
fields: {title: {boost: 10}, content: 1},
expand: true
fields: { title: { boost: 10 }, content: 1 },
expand: true,
});
// Render results
@@ -461,26 +469,29 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
// Render search results
function renderSearchResults(results, query) {
if (results.length === 0) {
searchResultsDiv.innerHTML = '<div class="text-center py-8 text-text-secondary">No results found</div>';
searchResultsDiv.classList.remove('hidden');
searchResultsDiv.innerHTML =
'<div class="text-center py-8 text-text-secondary">No results found</div>';
searchResultsDiv.classList.remove("hidden");
return;
}
searchResultsDiv.innerHTML = results.map(r => {
const doc = searchDocs.find(d => d.id === r.ref);
if (!doc) return '';
searchResultsDiv.innerHTML = results
.map((r) => {
const doc = searchDocs.find((d) => d.id === r.ref);
if (!doc) return "";
const snippet = getSnippet(doc.content, query);
const snippet = getSnippet(doc.content, query);
return `
return `
<a href="${doc.url}" class="block p-4 border-b border-border hover:bg-background-secondary text-text-primary no-underline">
<div class="font-semibold mb-2">${doc.title}</div>
<div class="text-sm text-text-secondary">${snippet}...</div>
</a>
`;
}).join('');
})
.join("");
searchResultsDiv.classList.remove('hidden');
searchResultsDiv.classList.remove("hidden");
}
function getSnippet(content, query) {
@@ -492,23 +503,26 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
if (idx !== -1) {
const start = Math.max(0, idx - 50);
const end = Math.min(content.length, idx + 100);
return '...' + content.substring(start, end) + '...';
return "..." + content.substring(start, end) + "...";
}
}
return content.substring(0, 150) + '...';
return content.substring(0, 150) + "...";
}
// Close search results when clicking outside
document.addEventListener('click', (e) => {
if (!searchResultsDiv.contains(e.target) && e.target !== searchInput) {
searchResultsDiv.classList.add('hidden');
document.addEventListener("click", (e) => {
if (
!searchResultsDiv.contains(e.target) &&
e.target !== searchInput
) {
searchResultsDiv.classList.add("hidden");
}
});
// Close search on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
searchResultsDiv.classList.add('hidden');
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
searchResultsDiv.classList.add("hidden");
searchInput.blur();
}
});