Files
bookhoard/templates/docs.templ
T
john-okeefe afdcc7b589 docs: add API explorer template component and fix rendering
Phase 3 part 1: Add interactive API explorer
- Create templates/api_explorer.templ with mock and real modes
- Add EndpointInfo and APIExplorerData types to templates/types.go
- Fix markdown rendering with UnsafeHTML.ToComponent()
- Add templ import for Component support
- API explorer supports:
  - Mock data mode for non-authenticated users
  - Real execution mode for logged-in users
  - Request/response display
  - cURL generation
  - Copy to clipboard
2026-02-02 08:56:23 -05:00

292 lines
7.1 KiB
Templ
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package templates
import (
"fmt"
"html/template"
)
templ DocsLayout(nav Navigation, doc Document, user User) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{ doc.Title } - Bookhoard Documentation</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<style>
:root {
--bg-primary: #1a1b26;
--bg-secondary: #24283b;
--text-primary: #c0caf5;
--text-secondary: #9aa5ce;
--border: #414868;
--accent: #7aa2f7;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
margin: 0;
padding: 0;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 280px;
background-color: var(--bg-secondary);
border-right: 1px solid var(--border);
overflow-y: auto;
z-index: 50;
}
.main-content {
margin-left: 280px;
padding: 2rem;
max-width: 900px;
}
.nav-section {
margin-bottom: 1.5rem;
}
.nav-section-title {
font-weight: 600;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
padding: 0.75rem 1rem;
cursor: pointer;
user-select: none;
}
.nav-item {
display: block;
padding: 0.5rem 1rem 0.5rem 2rem;
color: var(--text-secondary);
text-decoration: none;
font-size: 0.875rem;
transition: color 0.2s;
}
.nav-item:hover {
color: var(--accent);
}
.nav-item.active {
color: var(--accent);
background-color: rgba(122, 162, 247, 0.1);
border-right: 2px solid var(--accent);
}
pre {
background-color: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1rem;
overflow-x: auto;
}
code {
background-color: rgba(122, 162, 247, 0.1);
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-size: 0.875em;
}
.breadcrumb {
display: flex;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 2rem;
}
.breadcrumb a {
color: var(--accent);
text-decoration: none;
}
.toc {
background-color: var(--bg-secondary);
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 2rem;
border: 1px solid var(--border);
}
.toc-item {
padding: 0.25rem 0;
display: block;
color: var(--text-secondary);
text-decoration: none;
font-size: 0.875rem;
}
.toc-item:hover {
color: var(--accent);
}
.search-box {
padding: 1rem;
border-bottom: 1px solid var(--border);
}
.search-input {
width: 100%;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
background-color: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border);
font-size: 0.875rem;
}
.search-input:focus {
outline: none;
border-color: var(--accent);
}
.mobile-menu-button {
display: none;
position: fixed;
top: 1rem;
left: 1rem;
z-index: 100;
background-color: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 0.375rem;
padding: 0.5rem;
color: var(--text-primary);
cursor: pointer;
}
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s;
}
.sidebar.open {
transform: translateX(0);
}
.main-content {
margin-left: 0;
padding: 1rem;
}
.mobile-menu-button {
display: block;
}
}
</style>
</head>
<body>
<button class="mobile-menu-button" onclick="toggleSidebar()">
</button>
<!-- Sidebar -->
<div class="sidebar" id="sidebar">
<!-- Search -->
<div class="search-box">
<input type="text"
class="search-input"
placeholder="Search documentation..."
id="docs-search"
oninput="searchDocs(this.value)">
</div>
<!-- Navigation Sections -->
for _, section := range nav.Sections {
<div class="nav-section">
<div class="nav-section-title" onclick="toggleSection(this)">
{ section.Title }
</div>
if section.Collapsed {
<div class="nav-items" style="display: none;">
for _, item := range section.Items {
<a href={ item.URL } class="nav-item">
{ item.Icon } { item.Title }
</a>
}
</div>
} else {
<div class="nav-items">
for _, item := range section.Items {
<a href={ item.URL } class="nav-item">
{ item.Icon } { item.Title }
</a>
}
</div>
}
</div>
}
</div>
<!-- Main Content -->
<div class="main-content">
<!-- Breadcrumb -->
if len(doc.Breadcrumb) > 0 {
<div class="breadcrumb">
for i, crumb := range doc.Breadcrumb {
if i > 0 {
<span></span>
}
<a href={ crumb.URL }>{ crumb.Title }</a>
}
</div>
}
<!-- Title -->
<h1 style="margin-bottom: 2rem;">{ doc.Title }</h1>
<!-- Table of Contents -->
if len(doc.TOC) > 0 {
<div class="toc">
<strong style="display: block; margin-bottom: 0.5rem; color: var(--text-primary);">On this page</strong>
for _, item := range doc.TOC {
<a href={ "#" + item.Anchor } class="toc-item" style={ "margin-left: " + fmt.Sprintf("%drem", item.Level) }>
{ item.Title }
</a>
}
</div>
}
<!-- Content -->
<div>
@UnsafeHTML(doc.Content).ToComponent()
</div>
</div>
<script>
// Toggle sidebar section
function toggleSection(el) {
const items = el.nextElementSibling;
if (items.style.display === 'none') {
items.style.display = 'block';
} else {
items.style.display = 'none';
}
}
// Toggle sidebar on mobile
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('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('active');
}
});
// Initialize syntax highlighting
hljs.highlightAll();
});
// Search functionality
async function searchDocs(query) {
if (query.length < 2) return;
try {
const response = await fetch('/docs/api/search?q=' + encodeURIComponent(query));
const data = await response.json();
// Display search results (you can enhance this)
console.log('Search results:', data.results);
} catch (error) {
console.error('Search failed:', error);
}
}
</script>
</body>
</html>
}