tractatus/public/js/docs-app.js
TheFlow 09f706c51b feat: fix documentation system - cards, PDFs, TOC, and navigation
- Fixed download icon size (1.25rem instead of huge black icons)
- Uploaded all 12 PDFs to production server
- Restored table of contents rendering for all documents
- Fixed modal cards with proper CSS and event handlers
- Replaced all docs-viewer.html links with docs.html
- Added nginx redirect from /docs/* to /docs.html
- Fixed duplicate headers in modal sections
- Improved cache-busting with timestamp versioning

All documentation features now working correctly:
 Card-based document viewer with modals
 PDF downloads with proper icons
 Table of contents navigation
 Consistent URL structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 22:51:55 +13:00

224 lines
8.3 KiB
JavaScript

let documents = [];
let currentDocument = null;
let documentCards = null;
// Initialize card-based viewer
if (typeof DocumentCards !== 'undefined') {
documentCards = new DocumentCards('document-content');
}
// Load document list
async function loadDocuments() {
try {
const response = await fetch('/api/documents');
const data = await response.json();
documents = data.documents || [];
const listEl = document.getElementById('document-list');
if (documents.length === 0) {
listEl.innerHTML = '<div class="text-sm text-gray-500">No documents available</div>';
return;
}
console.log('Loaded documents:', documents.length);
// Find GLOSSARY and put it at the top
const glossary = documents.find(doc => doc.slug.includes('glossary'));
const otherDocs = documents.filter(doc => !doc.slug.includes('glossary'));
console.log('GLOSSARY found:', glossary ? glossary.title : 'NOT FOUND');
console.log('Other docs:', otherDocs.length);
let html = '';
// Add GLOSSARY prominently at top if it exists
if (glossary) {
html += `
<div class="mb-4 pb-4 border-b border-gray-200">
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">📚 Start Here</div>
<div class="relative">
<button class="doc-link w-full text-left px-3 py-2 pr-10 rounded text-sm font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 border border-blue-200"
data-slug="${glossary.slug}">
<div class="font-medium">${glossary.title}</div>
</button>
<a href="/downloads/${glossary.slug}.pdf"
download="${glossary.slug}.pdf"
class="doc-download-link"
title="Download PDF"
onclick="event.stopPropagation(); event.preventDefault(); window.location.href='/downloads/${glossary.slug}.pdf'; return false;">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</a>
</div>
</div>
`;
} else {
// If no glossary found, try case-insensitive fallback
const allGlossary = documents.find(doc => doc.slug.toLowerCase().includes('glossary'));
if (allGlossary) {
html += `
<div class="mb-4 pb-4 border-b border-gray-200">
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">📚 Start Here</div>
<button class="doc-link w-full text-left px-3 py-2 rounded text-sm font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 border border-blue-200"
data-slug="${allGlossary.slug}">
<div class="font-medium truncate">${allGlossary.title}</div>
</button>
</div>
`;
}
}
// Add other documents
const docsToShow = glossary ? otherDocs : documents.filter(doc => !doc.slug.toLowerCase().includes('glossary'));
if (docsToShow.length > 0) {
html += `<div class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Documentation</div>`;
html += docsToShow.map(doc => `
<div class="relative mb-1">
<button class="doc-link w-full text-left px-3 py-2 pr-10 rounded text-sm hover:bg-blue-50 transition"
data-slug="${doc.slug}">
<div class="font-medium text-gray-900">${doc.title}</div>
${doc.quadrant ? `<div class="text-xs text-gray-500 mt-1">${doc.quadrant}</div>` : ''}
</button>
<a href="/downloads/${doc.slug}.pdf"
download="${doc.slug}.pdf"
class="doc-download-link"
title="Download PDF"
onclick="event.stopPropagation(); event.preventDefault(); window.location.href='/downloads/${doc.slug}.pdf'; return false;">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</a>
</div>
`).join('');
}
listEl.innerHTML = html;
console.log('Navigation HTML updated');
// Add event delegation for document links
listEl.addEventListener('click', function(e) {
const button = e.target.closest('.doc-link');
if (button && button.dataset.slug) {
e.preventDefault();
loadDocument(button.dataset.slug);
}
});
// Auto-load GLOSSARY if it exists, otherwise first document
if (glossary) {
loadDocument(glossary.slug);
} else {
const allGlossary = documents.find(doc => doc.slug.toLowerCase().includes('glossary'));
if (allGlossary) {
loadDocument(allGlossary.slug);
} else if (documents.length > 0) {
loadDocument(documents[0].slug);
}
}
} catch (error) {
console.error('Error loading documents:', error);
document.getElementById('document-list').innerHTML =
'<div class="text-sm text-red-600">Error loading documents</div>';
}
}
// Load specific document
let isLoading = false;
async function loadDocument(slug) {
// Prevent multiple simultaneous loads
if (isLoading) return;
try {
isLoading = true;
// Show loading state
const contentEl = document.getElementById('document-content');
contentEl.innerHTML = `
<div class="text-center py-12">
<svg class="animate-spin h-8 w-8 text-blue-600 mx-auto mb-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p class="text-gray-600">Loading document...</p>
</div>
`;
const response = await fetch(`/api/documents/${slug}`);
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Failed to load document');
}
currentDocument = data.document;
// Update active state
document.querySelectorAll('.doc-link').forEach(el => {
if (el.dataset.slug === slug) {
el.classList.add('bg-blue-100', 'text-blue-900');
} else {
el.classList.remove('bg-blue-100', 'text-blue-900');
}
});
// Render with card-based viewer if available and document has sections
if (documentCards && currentDocument.sections && currentDocument.sections.length > 0) {
documentCards.render(currentDocument);
} else {
// Fallback to traditional view
contentEl.innerHTML = `
<div class="prose max-w-none">
${currentDocument.content_html}
</div>
`;
}
// Render table of contents
renderTOC(currentDocument.toc || []);
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
} catch (error) {
console.error('Error loading document:', error);
document.getElementById('document-content').innerHTML = `
<div class="text-center py-12">
<svg class="h-12 w-12 text-red-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<h3 class="text-lg font-medium text-gray-900 mb-2">Error loading document</h3>
<p class="text-sm text-gray-600">${error.message}</p>
</div>
`;
} finally {
isLoading = false;
}
}
// Render table of contents
function renderTOC(toc) {
const tocEl = document.getElementById('toc');
if (!toc || toc.length === 0) {
tocEl.innerHTML = '<div class="text-gray-500">No table of contents</div>';
return;
}
tocEl.innerHTML = toc
.filter(item => item.level <= 3) // Only show H1, H2, H3
.map(item => {
const indent = (item.level - 1) * 12;
return `
<a href="#${item.slug}"
class="block text-gray-600 hover:text-blue-600 transition"
style="padding-left: ${indent}px">
${item.title}
</a>
`;
}).join('');
}
// Initialize
loadDocuments();