/** * Document Cards Component * Renders document sections as interactive cards */ class DocumentCards { constructor(containerId) { this.container = document.getElementById(containerId); this.currentDocument = null; this.modalViewer = new ModalViewer(); } /** * Render document as card grid */ render(document) { if (!document || !document.sections || document.sections.length === 0) { this.renderTraditionalView(document); return; } this.currentDocument = document; // Create document header const headerHtml = this.renderHeader(document); // Group sections by category const sectionsByCategory = this.groupByCategory(document.sections); // Render card grid const cardsHtml = this.renderCardGrid(sectionsByCategory); this.container.innerHTML = ` ${headerHtml} ${cardsHtml} `; // Add event listeners after a brief delay to ensure DOM is ready setTimeout(() => { this.attachEventListeners(); }, 0); } /** * Render document header */ renderHeader(document) { const version = document.metadata?.version || ''; const dateUpdated = document.metadata?.date_updated ? new Date(document.metadata.date_updated).toLocaleDateString('en-US', { year: 'numeric', month: 'short' }) : ''; const versionText = version ? `v${version}` : ''; const metaText = [versionText, dateUpdated ? `Updated ${dateUpdated}` : ''] .filter(Boolean) .join(' | '); const hasToC = document.toc && document.toc.length > 0; return `

${document.title}

${metaText ? `

${metaText}

` : ''} ${document.sections ? `

${document.sections.length} sections

` : ''}
${hasToC ? ` ` : ''}
`; } /** * Group sections by category */ groupByCategory(sections) { const groups = { conceptual: [], practical: [], technical: [], reference: [], critical: [] }; sections.forEach(section => { const category = section.category || 'conceptual'; if (groups[category]) { groups[category].push(section); } else { groups.conceptual.push(section); } }); return groups; } /** * Render card grid */ renderCardGrid(sectionsByCategory) { const categoryConfig = { critical: { icon: '⚠️', label: 'Critical', color: 'red', order: 1 }, conceptual: { icon: '📘', label: 'Conceptual', color: 'blue', order: 2 }, practical: { icon: '✨', label: 'Practical', color: 'green', order: 3 }, technical: { icon: '🔧', label: 'Technical', color: 'purple', order: 4 }, reference: { icon: '📋', label: 'Reference', color: 'gray', order: 5 } }; let html = '
'; // Render categories in priority order (critical first) const orderedCategories = Object.entries(sectionsByCategory) .filter(([category, sections]) => sections.length > 0) .sort((a, b) => { const orderA = categoryConfig[a[0]]?.order || 999; const orderB = categoryConfig[b[0]]?.order || 999; return orderA - orderB; }); // Render each category that has sections for (const [category, sections] of orderedCategories) { const config = categoryConfig[category]; html += `

${config.icon} ${config.label}

${sections.map(section => this.renderCard(section, config.color)).join('')}
`; } html += '
'; return html; } /** * Render individual card */ renderCard(section, color) { const levelIcons = { basic: '○', intermediate: '◐', advanced: '●' }; const levelIcon = levelIcons[section.technicalLevel] || '○'; const levelLabel = section.technicalLevel.charAt(0).toUpperCase() + section.technicalLevel.slice(1); const borderColor = { red: 'border-red-500', blue: 'border-blue-400', green: 'border-green-400', purple: 'border-purple-400', gray: 'border-gray-400' }[color] || 'border-blue-400'; const hoverColor = { red: 'hover:border-red-700 hover:shadow-red-100', blue: 'hover:border-blue-600 hover:shadow-blue-100', green: 'hover:border-green-600 hover:shadow-green-100', purple: 'hover:border-purple-600 hover:shadow-purple-100', gray: 'hover:border-gray-600 hover:shadow-gray-100' }[color] || 'hover:border-blue-600'; const bgColor = { red: 'bg-red-50', blue: 'bg-blue-50', green: 'bg-green-50', purple: 'bg-purple-50', gray: 'bg-gray-50' }[color] || 'bg-blue-50'; return `

${section.title}

${section.excerpt}

${section.readingTime} min read ${levelIcon} ${levelLabel}
`; } /** * Fallback: render traditional view for documents without sections */ renderTraditionalView(document) { if (!document) return; this.container.innerHTML = `
${document.content_html}
`; } /** * Attach event listeners to cards */ attachEventListeners() { const cards = this.container.querySelectorAll('.doc-card'); cards.forEach(card => { card.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const sectionSlug = card.dataset.sectionSlug; const section = this.currentDocument.sections.find(s => s.slug === sectionSlug); if (section) { this.modalViewer.show(section, this.currentDocument.sections); } }); }); // Attach ToC button listener const tocButton = document.getElementById('toc-button'); if (tocButton) { tocButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (typeof openToCModal === 'function') { openToCModal(); } }); } } } /** * Modal Viewer Component * Displays section content in a modal */ class ModalViewer { constructor() { this.modal = null; this.currentSection = null; this.allSections = []; this.currentIndex = 0; this.createModal(); } /** * Create modal structure */ createModal() { const modalHtml = `
`; document.body.insertAdjacentHTML('beforeend', modalHtml); this.modal = document.getElementById('section-modal'); this.attachModalListeners(); } /** * Show modal with section content */ show(section, allSections) { this.currentSection = section; this.allSections = allSections; this.currentIndex = allSections.findIndex(s => s.slug === section.slug); // Update content const titleEl = document.getElementById('modal-title'); const contentEl = document.getElementById('modal-content'); if (!titleEl || !contentEl) { return; } titleEl.textContent = section.title; // Remove duplicate title (H1 or H2) from content (it's already in modal header) let contentHtml = section.content_html; // Try removing h1 first, then h2 const firstH1Match = contentHtml.match(/]*>.*?<\/h1>/); if (firstH1Match) { contentHtml = contentHtml.replace(firstH1Match[0], ''); } else { const firstH2Match = contentHtml.match(/]*>.*?<\/h2>/); if (firstH2Match) { contentHtml = contentHtml.replace(firstH2Match[0], ''); } } contentEl.innerHTML = contentHtml; // Update navigation this.updateNavigation(); // Show modal this.modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; // Scroll to top of content contentEl.scrollTop = 0; } /** * Hide modal */ hide() { this.modal.style.display = 'none'; document.body.style.overflow = ''; } /** * Update navigation buttons */ updateNavigation() { const prevBtn = document.getElementById('modal-prev'); const nextBtn = document.getElementById('modal-next'); const progress = document.getElementById('modal-progress'); prevBtn.disabled = this.currentIndex === 0; nextBtn.disabled = this.currentIndex === this.allSections.length - 1; progress.textContent = `${this.currentIndex + 1} of ${this.allSections.length}`; } /** * Navigate to previous section */ showPrevious() { if (this.currentIndex > 0) { this.show(this.allSections[this.currentIndex - 1], this.allSections); } } /** * Navigate to next section */ showNext() { if (this.currentIndex < this.allSections.length - 1) { this.show(this.allSections[this.currentIndex + 1], this.allSections); } } /** * Attach modal event listeners */ attachModalListeners() { // Close button document.getElementById('modal-close').addEventListener('click', () => this.hide()); // Navigation buttons document.getElementById('modal-prev').addEventListener('click', () => this.showPrevious()); document.getElementById('modal-next').addEventListener('click', () => this.showNext()); // Close on background click this.modal.addEventListener('click', (e) => { if (e.target === this.modal) { this.hide(); } }); // Keyboard navigation document.addEventListener('keydown', (e) => { // Check if modal is visible using display style instead of hidden class if (this.modal.style.display === 'flex') { if (e.key === 'Escape') { this.hide(); } else if (e.key === 'ArrowLeft') { this.showPrevious(); } else if (e.key === 'ArrowRight') { this.showNext(); } } }); } }