let documents = []; let currentDocument = null; let documentCards = null; // Initialize card-based viewer if (typeof DocumentCards !== 'undefined') { documentCards = new DocumentCards('document-content'); } // Document categorization const CATEGORIES = { 'getting-started': { label: '📘 Getting Started', icon: '📘', categories: ['conceptual', 'reference'], // Matches document.category field slugs: ['architectural-overview', 'core-concepts', 'implementation-guide'], order: 1, color: 'blue', bgColor: 'bg-blue-50', borderColor: 'border-l-4 border-blue-500', textColor: 'text-blue-700', collapsed: false }, 'framework-details': { label: '📗 Framework Details', icon: '📗', categories: ['conceptual', 'practical'], slugs: ['core-values', 'case-studies', 'business-case'], order: 2, color: 'green', bgColor: 'bg-green-50', borderColor: 'border-l-4 border-green-500', textColor: 'text-green-700', collapsed: false }, 'reference': { label: '📕 Reference', icon: '📕', categories: ['reference'], slugs: ['glossary'], order: 3, color: 'purple', bgColor: 'bg-purple-50', borderColor: 'border-l-4 border-purple-500', textColor: 'text-purple-700', collapsed: false } }; // Documents to hide (internal/confidential) const HIDDEN_DOCS = [ 'security-audit-report', 'koha-production-deployment', 'koha-stripe-payment', 'appendix-e-contact', 'cover-letter' ]; // Categorize a document based on order and category field function categorizeDocument(doc) { const slug = doc.slug.toLowerCase(); // Skip hidden documents if (HIDDEN_DOCS.some(hidden => slug.includes(hidden))) { return null; } // Documents are pre-ordered by 'order' field (1-7 for public docs) // Group by logical UI sections based on order const order = doc.order || 999; if (order >= 1 && order <= 3) { return 'getting-started'; } else if (order >= 4 && order <= 6) { return 'framework-details'; } else if (order >= 7 && order <= 8) { return 'reference'; } // Fallback to getting-started for uncategorized return 'getting-started'; } // Group documents by category function groupDocuments(docs) { const grouped = {}; // Initialize all categories Object.keys(CATEGORIES).forEach(key => { grouped[key] = []; }); // Categorize each document (already sorted by order from API) docs.forEach(doc => { const category = categorizeDocument(doc); if (category && grouped[category]) { grouped[category].push(doc); } }); return grouped; } // Render document link with download button function renderDocLink(doc, isHighlighted = false) { const highlightClass = isHighlighted ? 'text-blue-700 bg-blue-50 border border-blue-200' : ''; return `
`; } // Load document list async function loadDocuments() { try { // Fetch public documents const response = await fetch('/api/documents'); const data = await response.json(); documents = data.documents || []; // Fetch archived documents const archivedResponse = await fetch('/api/documents/archived'); const archivedData = await archivedResponse.json(); const archivedDocuments = archivedData.documents || []; const listEl = document.getElementById('document-list'); if (documents.length === 0 && archivedDocuments.length === 0) { listEl.innerHTML = '
No documents available
'; return; } // Group documents by category const grouped = groupDocuments(documents); let html = ''; // Render categories in order const sortedCategories = Object.entries(CATEGORIES) .sort((a, b) => a[1].order - b[1].order); sortedCategories.forEach(([categoryId, category]) => { const docs = grouped[categoryId] || []; if (docs.length === 0) return; const isCollapsed = category.collapsed || false; // Category header html += `
`; // Render documents in category docs.forEach(doc => { const isHighlighted = categoryId === 'getting-started' && doc.order === 1; html += renderDocLink(doc, isHighlighted); }); html += `
`; }); // Add Archives section if there are archived documents if (archivedDocuments.length > 0) { html += `
`; } listEl.innerHTML = html; // Add event delegation for document links listEl.addEventListener('click', function(e) { // Check for download link first (prevent document load when clicking download) const downloadLink = e.target.closest('.doc-download-link'); if (downloadLink) { e.stopPropagation(); return; } const button = e.target.closest('.doc-link'); if (button && button.dataset.slug) { e.preventDefault(); loadDocument(button.dataset.slug); return; } // Category toggle const toggle = e.target.closest('.category-toggle'); if (toggle) { const categoryId = toggle.dataset.category; const docsEl = listEl.querySelector(`.category-docs[data-category="${categoryId}"]`); const arrowEl = toggle.querySelector('.category-arrow'); if (docsEl.style.display === 'none') { docsEl.style.display = 'block'; arrowEl.style.transform = 'rotate(0deg)'; } else { docsEl.style.display = 'none'; arrowEl.style.transform = 'rotate(-90deg)'; } } }); // Auto-load first document in "Getting Started" category (order: 1) const gettingStartedDocs = grouped['getting-started'] || []; if (gettingStartedDocs.length > 0) { // Load the architectural overview (order: 1) if available const archOverview = gettingStartedDocs.find(d => d.order === 1); if (archOverview) { loadDocument(archOverview.slug); } else { loadDocument(gettingStartedDocs[0].slug); } } else if (documents.length > 0) { // Fallback to first available document const firstCategory = sortedCategories.find(([catId]) => grouped[catId] && grouped[catId].length > 0); if (firstCategory) { loadDocument(grouped[firstCategory[0]][0].slug); } } } catch (error) { console.error('Error loading documents:', error); document.getElementById('document-list').innerHTML = '
Error loading documents
'; } } // 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 = `

Loading document...

`; 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 with header const hasToC = currentDocument.toc && currentDocument.toc.length > 0; let headerHTML = `

${currentDocument.title}

${hasToC ? ` ` : ''}
`; // Remove duplicate title H1 from content (it's already in header) let contentHtml = currentDocument.content_html; const firstH1Match = contentHtml.match(/]*>.*?<\/h1>/); if (firstH1Match) { contentHtml = contentHtml.replace(firstH1Match[0], ''); } contentEl.innerHTML = headerHTML + `
${contentHtml}
`; } // Add ToC button event listener (works for both card and traditional views) setTimeout(() => { const tocButton = document.getElementById('toc-button'); if (tocButton) { tocButton.addEventListener('click', () => openToCModal()); } }, 100); // Scroll to top window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (error) { console.error('Error loading document:', error); document.getElementById('document-content').innerHTML = `

Error loading document

${error.message}

`; } finally { isLoading = false; } } // Open ToC modal function openToCModal() { if (!currentDocument || !currentDocument.toc || currentDocument.toc.length === 0) { return; } const modal = document.getElementById('toc-modal'); if (!modal) return; // Render ToC content const tocContent = document.getElementById('toc-modal-content'); const tocHTML = currentDocument.toc .filter(item => item.level <= 3) // Only show H1, H2, H3 .map(item => { return ` ${item.title} `; }).join(''); tocContent.innerHTML = tocHTML; // Show modal modal.classList.add('show'); // Prevent body scroll and reset modal content scroll document.body.style.overflow = 'hidden'; tocContent.scrollTop = 0; // Add event listeners to ToC links tocContent.querySelectorAll('.toc-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetId = link.getAttribute('href').substring(1); const targetEl = document.getElementById(targetId); if (targetEl) { closeToCModal(); setTimeout(() => { targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 200); } }); }); } // Close ToC modal function closeToCModal() { const modal = document.getElementById('toc-modal'); if (modal) { modal.classList.remove('show'); document.body.style.overflow = ''; } } // Initialize loadDocuments(); // Add ESC key listener for closing modal document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeToCModal(); } }); // Add close button listener for ToC modal (script loads after DOM, so elements exist) const closeButton = document.getElementById('toc-close-button'); if (closeButton) { closeButton.addEventListener('click', closeToCModal); } // Click outside modal to close const modal = document.getElementById('toc-modal'); if (modal) { modal.addEventListener('click', function(e) { if (e.target === this) { closeToCModal(); } }); }