Merge branch 'main' of ssh://git.linuxhg.com:2222/Bookhoard/bookhoard
This commit is contained in:
+203
-69
@@ -4,41 +4,22 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
templ DocsLayout(nav Navigation, doc Document, user User) {
|
||||
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">
|
||||
<title>{ doc.Title } - Bookhoard Documentation</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
|
||||
<link rel="stylesheet" href="/web/static/highlight-dark.min.css">
|
||||
<script src="/web/static/highlight.min.js"></script>
|
||||
<script src="/web/static/lunr.min.js"></script>
|
||||
<script src="/web/static/lunr-flex.min.js"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: {
|
||||
primary: '#1a1b26',
|
||||
secondary: '#24283b',
|
||||
},
|
||||
text: {
|
||||
primary: '#c0caf5',
|
||||
secondary: '#9aa5ce',
|
||||
},
|
||||
border: '#414868',
|
||||
accent: '#7aa2f7',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<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>
|
||||
<script src="/static/header.js"></script>
|
||||
</head>
|
||||
<body class="bg-background-primary text-text-primary font-sans antialiased">
|
||||
<body class={ "theme-" + user.Theme + " page-docs bg-background-primary text-text-primary font-sans antialiased" }>
|
||||
@Header(user, currentPath)
|
||||
<!-- Mobile Menu 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"
|
||||
@@ -54,7 +35,7 @@ templ DocsLayout(nav Navigation, doc Document, user User) {
|
||||
<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 z-40 lg:translate-x-0 transition-transform duration-300 ease-in-out">
|
||||
<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
|
||||
@@ -62,7 +43,7 @@ templ DocsLayout(nav Navigation, doc Document, user User) {
|
||||
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"
|
||||
oninput="searchDocs(this.value)"
|
||||
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -115,11 +96,14 @@ templ DocsLayout(nav Navigation, doc Document, user User) {
|
||||
|
||||
<!-- Title -->
|
||||
<h1 class="text-4xl font-bold mb-8 text-text-primary">{ doc.Title }</h1>
|
||||
|
||||
|
||||
<!-- Table of Contents -->
|
||||
if len(doc.TOC) > 0 {
|
||||
<div class="toc bg-background-secondary p-4 rounded-lg mb-8 border border-border">
|
||||
<strong class="block mb-2 text-text-primary font-semibold">On this page</strong>
|
||||
<details class="toc bg-background-secondary p-4 rounded-lg mb-8 border border-border">
|
||||
<summary class="cursor-pointer hover:opacity-80 transition-opacity">
|
||||
<strong class="text-text-primary font-semibold">On this page</strong>
|
||||
<span class="ml-2 text-text-secondary text-xs">▼</span>
|
||||
</summary>
|
||||
for _, item := range doc.TOC {
|
||||
<a
|
||||
href={ "#" + item.Anchor }
|
||||
@@ -129,7 +113,7 @@ templ DocsLayout(nav Navigation, doc Document, user User) {
|
||||
{ item.Title }
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
|
||||
<!-- Content -->
|
||||
@@ -162,14 +146,194 @@ templ DocsLayout(nav Navigation, doc Document, user User) {
|
||||
sidebar.classList.toggle('open');
|
||||
}
|
||||
|
||||
// Close sidebar when clicking main content on mobile
|
||||
document.querySelector('.main-content')?.addEventListener('click', () => {
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
if (window.innerWidth < 1024) {
|
||||
sidebar.classList.remove('open');
|
||||
// Highlight current page in nav
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize syntax highlighting
|
||||
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) => {
|
||||
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() {
|
||||
this.use(lunr.flex);
|
||||
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');
|
||||
})
|
||||
.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');
|
||||
|
||||
let debounceTimer;
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
const query = e.target.value.trim();
|
||||
|
||||
if (query.length < 2) {
|
||||
searchResultsDiv.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Search with fuzzy matching
|
||||
const results = idx.search(query, {
|
||||
fields: {title: {boost: 10}, content: 1},
|
||||
expand: true
|
||||
});
|
||||
|
||||
// Render results
|
||||
renderSearchResults(results, query);
|
||||
}, 150);
|
||||
});
|
||||
|
||||
// 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');
|
||||
return;
|
||||
}
|
||||
|
||||
searchResultsDiv.innerHTML = results.map(r => {
|
||||
const doc = searchDocs.find(d => d.id === r.ref);
|
||||
if (!doc) return '';
|
||||
|
||||
const snippet = getSnippet(doc.content, query);
|
||||
|
||||
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('');
|
||||
|
||||
searchResultsDiv.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function getSnippet(content, query) {
|
||||
const words = query.toLowerCase().split(/\s+/);
|
||||
const contentLower = content.toLowerCase();
|
||||
|
||||
for (const word of words) {
|
||||
const idx = contentLower.indexOf(word);
|
||||
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(0, 150) + '...';
|
||||
}
|
||||
|
||||
// Close search results when clicking outside
|
||||
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');
|
||||
searchInput.blur();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// Toggle sidebar on mobile
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
sidebar.classList.toggle('open');
|
||||
}
|
||||
|
||||
|
||||
// Highlight current page in nav
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const currentPath = window.location.pathname;
|
||||
@@ -353,33 +517,3 @@ templ DocsLayout(nav Navigation, doc Document, user User) {
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ DocsLayoutWithExplorer(nav Navigation, doc Document, user User, explorer APIExplorerData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{ doc.Title } - Bookhoard Documentation</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
|
||||
<link rel="stylesheet" href="/web/static/highlight-dark.min.css">
|
||||
<script src="/web/static/highlight.min.js"></script>
|
||||
<script src="/web/static/lunr.min.js"></script>
|
||||
<script src="/web/static/lunr-flex.min.js"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: {
|
||||
primary: '#1a1b26',
|
||||
secondary: '#24283b',
|
||||
},
|
||||
text: {
|
||||
primary: '#c0caf5',
|
||||
secondary: '#9aa5ce',
|
||||
},
|
||||
border: '#414868',
|
||||
accent: '#7aa2f7',
|
||||
},
|
||||
|
||||
+175
-463
File diff suppressed because one or more lines are too long
+53
-21
@@ -1,7 +1,7 @@
|
||||
package templates
|
||||
|
||||
templ Header(user User, currentPath string) {
|
||||
<nav class="border-b header-nav" style="border-color: var(--border); background-color: var(--bg-secondary)">
|
||||
<nav class="border-b header-nav" style="border-color: var(--border); background-color: var(--bg-secondary);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<!-- Left: App Title & Navigation -->
|
||||
@@ -104,28 +104,60 @@ templ Header(user User, currentPath string) {
|
||||
|
||||
<!-- User Icon with Dropdown -->
|
||||
<div class="relative">
|
||||
<button onclick="toggleUserMenu()" class="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-700 transition-colors" style="background-color: var(--bg-primary);">
|
||||
<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
<span class="text-sm hidden sm:block" style="color: var(--text-primary)">{ user.Username }</span>
|
||||
</button>
|
||||
<div id="user-menu" class="hidden absolute right-0 mt-2 w-48 rounded-lg shadow-lg z-50" style="background-color: var(--bg-secondary); border: 1px solid var(--border);">
|
||||
<div class="py-1">
|
||||
<a href="/settings" class="block px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary); text-decoration: none;">
|
||||
Settings
|
||||
</a>
|
||||
if user.Role == "admin" {
|
||||
<a href="/admin" class="block px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary); text-decoration: none;">
|
||||
Admin Panel
|
||||
if user.ID != "" {
|
||||
<!-- LOGGED IN: Show user menu with logout -->
|
||||
<button onclick="toggleUserMenu()" class="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-700 transition-colors" style="background-color: var(--bg-primary);">
|
||||
<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
<span class="text-sm hidden sm:block" style="color: var(--text-primary)">{ user.Username }</span>
|
||||
</button>
|
||||
<div id="user-menu" class="hidden absolute right-0 mt-2 w-48 rounded-lg shadow-lg z-50" style="background-color: var(--bg-secondary); border: 1px solid var(--border);">
|
||||
<div class="py-1">
|
||||
<a href="/settings" class="block px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary); text-decoration: none;">
|
||||
Settings
|
||||
</a>
|
||||
}
|
||||
<div class="border-t my-1" style="border-color: var(--border);"></div>
|
||||
<button onclick="logout()" class="block w-full text-left px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary);">
|
||||
Logout
|
||||
</button>
|
||||
if user.Role == "admin" {
|
||||
<a href="/admin" class="block px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary); text-decoration: none;">
|
||||
Admin Panel
|
||||
</a>
|
||||
}
|
||||
<div class="border-t my-1" style="border-color: var(--border);"></div>
|
||||
<button onclick="logout()" class="block w-full text-left px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary);">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} else {
|
||||
<!-- LOGGED OUT: Show login form -->
|
||||
<button onclick="toggleUserMenu()" class="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-700 transition-colors" style="background-color: var(--bg-primary);">
|
||||
<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
<span class="text-sm hidden sm:block" style="color: var(--text-primary)">Login</span>
|
||||
</button>
|
||||
<div id="user-menu" class="hidden absolute right-0 mt-2 w-64 rounded-lg shadow-lg z-50 p-4" style="background-color: var(--bg-secondary); border: 1px solid var(--border);">
|
||||
<form hx-post="/api/auth/login" hx-target="#login-result" hx-swap="innerHTML" class="space-y-3">
|
||||
<input type="hidden" name="redirect" value="{ currentPath }">
|
||||
<div>
|
||||
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Email or Username</label>
|
||||
<input type="text" name="login" class="w-full px-3 py-2 border rounded text-sm" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Password</label>
|
||||
<input type="password" name="password" class="w-full px-3 py-2 border rounded text-sm" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" required>
|
||||
</div>
|
||||
<button type="submit" class="w-full py-2 rounded text-sm" style="background-color: var(--accent); color: white;">
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
<div id="login-result"></div>
|
||||
<div class="border-t my-2" style="border-color: var(--border);"></div>
|
||||
<a href="/register" class="block text-center text-sm hover:opacity-80" style="color: var(--text-secondary); text-decoration: none;">
|
||||
Don't have an account? Sign Up
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+32
-17
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user