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 glossary',
order: 1,
color: 'blue',
bgColor: 'bg-blue-50',
borderColor: 'border-l-4 border-blue-500',
textColor: 'text-blue-700',
collapsed: false
},
'resources': {
label: '📖 Resources',
icon: '📖',
description: 'Implementation guides and reference materials',
order: 2,
color: 'amber',
bgColor: 'bg-amber-50',
borderColor: 'border-l-4 border-amber-500',
textColor: 'text-amber-700',
collapsed: false
},
'research-theory': {
label: '🔬 Research & Theory',
icon: '🔬',
description: 'Research papers, case studies, theoretical foundations',
order: 3,
color: 'purple',
bgColor: 'bg-purple-50',
borderColor: 'border-l-4 border-purple-500',
textColor: 'text-purple-700',
collapsed: true
},
'technical-reference': {
label: '🔌 Technical Reference',
icon: '🔌',
description: 'API documentation, code examples, architecture',
order: 4,
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: 5,
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: 6,
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 || [];
const listEl = document.getElementById('document-list');
if (documents.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 += `
`;
});
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(/