- 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>
224 lines
8.3 KiB
JavaScript
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();
|