let documents = []; let currentDocument = null; let documentCards = null; // Initialize card-based viewer if (typeof DocumentCards !== 'undefined') { documentCards = new DocumentCards('document-content'); } // Document categorization - Final 5 categories (curated for public docs) const CATEGORIES = { 'getting-started': { label: '📚 Getting Started', icon: '📚', description: 'Introduction, core concepts, and implementation guides', order: 1, color: 'blue', bgColor: 'bg-blue-50', borderColor: 'border-l-4 border-blue-500', textColor: 'text-blue-700', collapsed: false }, 'research-theory': { label: '🔬 Research & Theory', icon: '🔬', description: 'Research papers, case studies, theoretical foundations', order: 2, color: 'purple', bgColor: 'bg-purple-50', borderColor: 'border-l-4 border-purple-500', textColor: 'text-purple-700', collapsed: false // Expanded to show Working Paper v0.1 }, 'technical-reference': { label: '🔌 Technical Reference', icon: '🔌', description: 'API documentation, code examples, architecture', order: 3, color: 'green', bgColor: 'bg-green-50', borderColor: 'border-l-4 border-green-500', textColor: 'text-green-700', collapsed: true }, 'advanced-topics': { label: '🎓 Advanced Topics', icon: '🎓', description: 'Value pluralism, organizational theory, advanced concepts', order: 4, color: 'teal', bgColor: 'bg-teal-50', borderColor: 'border-l-4 border-teal-500', textColor: 'text-teal-700', collapsed: true }, 'business-leadership': { label: '💼 Business & Leadership', icon: '💼', description: 'Business cases, ROI analysis, executive briefs', order: 5, color: 'pink', bgColor: 'bg-pink-50', borderColor: 'border-l-4 border-pink-500', textColor: 'text-pink-700', collapsed: true } }; // 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 using database category field // New granular category system for better document organization function categorizeDocument(doc) { const slug = doc.slug.toLowerCase(); // Skip hidden documents if (HIDDEN_DOCS.some(hidden => slug.includes(hidden))) { return null; } // Use category from database const category = doc.category || 'downloads-resources'; // Validate category exists in CATEGORIES constant if (CATEGORIES[category]) { return category; } // Fallback to downloads-resources for uncategorized console.warn(`Document "${doc.title}" has invalid category "${category}", using fallback`); return 'downloads-resources'; } // 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' : ''; // Determine if PDF download is available and get PDF path // First check if document has explicit download_formats.pdf let pdfPath = null; let hasPDF = false; if (doc.download_formats && doc.download_formats.pdf) { pdfPath = doc.download_formats.pdf; hasPDF = true; } else if (!doc.slug.includes('api-reference-complete') && !doc.slug.includes('openapi-specification') && !doc.slug.includes('api-javascript-examples') && !doc.slug.includes('api-python-examples') && !doc.slug.includes('technical-architecture-diagram')) { // Fallback to default /downloads/ path for documents that typically have PDFs pdfPath = `/downloads/${doc.slug}.pdf`; hasPDF = true; } // Add download button styling const paddingClass = hasPDF ? 'pr-10' : 'pr-3'; return `
${hasPDF ? ` ` : ''}
`; } // 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 => { // Highlight the first document in Getting Started category 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 += `
`; // Render archived documents archivedDocuments.forEach(doc => { html += renderDocLink(doc, false); // Add archive note if available if (doc.archiveNote) { html += `
${doc.archiveNote}
`; } }); html += `
`; } listEl.innerHTML = html; // Apply collapsed state to categories (CSP-compliant - no inline styles) listEl.querySelectorAll('.category-docs[data-collapsed="true"]').forEach(docsEl => { docsEl.style.display = 'none'; }); listEl.querySelectorAll('.category-toggle[data-collapsed="true"] .category-arrow').forEach(arrowEl => { arrowEl.style.transform = 'rotate(-90deg)'; }); // 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)'; } } }); // Check for URL parameter to auto-load document or category const urlParams = new URLSearchParams(window.location.search); const docParam = urlParams.get('doc'); const categoryParam = urlParams.get('category'); // Priority 1: Load specific document by slug if provided if (docParam) { const doc = documents.find(d => d.slug === docParam); if (doc) { // Find and expand the category containing this document const docCategory = categorizeDocument(doc); if (docCategory) { const categoryDocsEl = listEl.querySelector(`.category-docs[data-category="${docCategory}"]`); const categoryArrowEl = listEl.querySelector(`.category-toggle[data-category="${docCategory}"] .category-arrow`); if (categoryDocsEl) { categoryDocsEl.style.display = 'block'; if (categoryArrowEl) { categoryArrowEl.style.transform = 'rotate(0deg)'; } } } // Load the requested document loadDocument(docParam); } else { console.warn(`Document with slug "${docParam}" not found`); } } // Priority 2: Load category if provided but no specific document else if (categoryParam && grouped[categoryParam] && grouped[categoryParam].length > 0) { // Expand the specified category const categoryDocsEl = listEl.querySelector(`.category-docs[data-category="${categoryParam}"]`); const categoryArrowEl = listEl.querySelector(`.category-toggle[data-category="${categoryParam}"] .category-arrow`); if (categoryDocsEl) { categoryDocsEl.style.display = 'block'; if (categoryArrowEl) { categoryArrowEl.style.transform = 'rotate(0deg)'; } } // Load first document in the category const firstDoc = grouped[categoryParam][0]; if (firstDoc) { loadDocument(firstDoc.slug); } } // Priority 3: Default behavior else { // Default: Auto-load first document in "Getting Started" category (order: 1) const gettingStartedDocs = grouped['getting-started'] || []; if (gettingStartedDocs.length > 0) { // Load the first document (order: 1) if available const firstDoc = gettingStartedDocs.find(d => d.order === 1); if (firstDoc) { loadDocument(firstDoc.slug); } else { loadDocument(gettingStartedDocs[0].slug); } } else if (documents.length > 0) { // Fallback to first available document in any category 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; // Check if PDF is available and get PDF path let pdfPath = null; let hasPDF = false; if (currentDocument.download_formats && currentDocument.download_formats.pdf) { pdfPath = currentDocument.download_formats.pdf; hasPDF = true; } else if (!currentDocument.slug.includes('api-reference-complete') && !currentDocument.slug.includes('openapi-specification') && !currentDocument.slug.includes('api-javascript-examples') && !currentDocument.slug.includes('api-python-examples') && !currentDocument.slug.includes('technical-architecture-diagram')) { pdfPath = `/downloads/${currentDocument.slug}.pdf`; hasPDF = true; } let headerHTML = `

${currentDocument.title}

${hasToC ? ` ` : ''} ${hasPDF ? ` ` : ''}
`; // 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); // Mobile navigation: Add document-active class to show document view document.body.classList.add('document-active'); // 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(); } }); } // Mobile navigation: Back to documents button const backButton = document.getElementById('back-to-docs-btn'); if (backButton) { backButton.addEventListener('click', function() { // Remove document-active class to show sidebar document.body.classList.remove('document-active'); // Scroll to top window.scrollTo({ top: 0, behavior: 'smooth' }); }); }