Fixed multiple issues with the docs page card-based document view: **Card Overflow Fixed:** - Added overflow-x-hidden to #document-content container - Added w-full max-w-full to card-grid-container - Added w-full to grid itself - Added max-w-full overflow-hidden to individual cards - Cards now stay within container boundaries at all viewport sizes **Long Title Wrapping:** - Added insertSoftHyphens() method to break CamelCase words - Inserts soft hyphens (­) before capitals in compound words - Examples: "InstructionPersistenceClassifier" → "InstructionPersistenceClassifier" - Titles now wrap intelligently without being cut off **Colour Legend (Option C):** - Added toggle button (ℹ️) next to ToC and PDF buttons - Popup shows all 5 colour codes with descriptions - Translated to EN ("Colour Guide"), DE ("Farbcode"), FR ("Guide des couleurs") - Fixed colour square visibility (bg-500 with borders instead of bg-400) - Click outside to close functionality **Card Sequencing:** - Cards now display in original markdown document order - Removed groupByCategory() grouping logic - Removed category header sections - Color coding preserved based on section category **Category Fallback Bug:** - Fixed invalid fallback category 'downloads-resources' → 'resources' - Ensures uncategorized documents go to valid category **Database Migration:** - Added scripts/move-guides-to-resources.js - Moved 3 implementation guides from getting-started to resources - Getting Started now contains only: Introduction, Core Concepts - Resources now contains: Implementation guides **Result:** ✅ Cards respect container width (no overflow) ✅ Long titles wrap with hyphens (no cutoff) ✅ Colour legend accessible and translated ✅ Cards in logical reading order from markdown ✅ Implementation guides in correct category 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
885 lines
31 KiB
JavaScript
885 lines
31 KiB
JavaScript
let documents = [];
|
|
let currentDocument = null;
|
|
let documentCards = null;
|
|
let currentLanguage = 'en'; // Default language
|
|
|
|
// UI translations for full page i18n
|
|
const UI_TRANSLATIONS = {
|
|
en: {
|
|
pageTitle: 'Framework Documentation',
|
|
pageSubtitle: 'Technical specifications, guides, and reference materials',
|
|
documentsHeading: 'Documents',
|
|
searchButton: 'Search',
|
|
backToDocuments: 'Back to Documents',
|
|
selectDocument: 'Select a Document',
|
|
selectDocumentDesc: 'Choose a document from the sidebar to begin reading',
|
|
loadingDocument: 'Loading document...',
|
|
errorLoadingDoc: 'Error loading document',
|
|
tableOfContents: 'Table of Contents',
|
|
downloadPdf: 'Download PDF',
|
|
github: 'GitHub',
|
|
publicRepository: 'Public Repository',
|
|
publicRepositoryDesc: 'Source code, examples & contributions',
|
|
readmeQuickStart: 'README & Quick Start',
|
|
readmeQuickStartDesc: 'Installation and getting started guide',
|
|
categories: {
|
|
'getting-started': 'Getting Started',
|
|
'resources': 'Resources',
|
|
'research-theory': 'Research & Theory',
|
|
'technical-reference': 'Technical Reference',
|
|
'advanced-topics': 'Advanced Topics',
|
|
'business-leadership': 'Business & Leadership'
|
|
}
|
|
},
|
|
de: {
|
|
pageTitle: 'Framework-Dokumentation',
|
|
pageSubtitle: 'Technische Spezifikationen, Leitfäden und Referenzmaterialien',
|
|
documentsHeading: 'Dokumente',
|
|
searchButton: 'Suchen',
|
|
backToDocuments: 'Zurück zu Dokumenten',
|
|
selectDocument: 'Dokument auswählen',
|
|
selectDocumentDesc: 'Wählen Sie ein Dokument aus der Seitenleiste, um mit dem Lesen zu beginnen',
|
|
loadingDocument: 'Dokument wird geladen...',
|
|
errorLoadingDoc: 'Fehler beim Laden des Dokuments',
|
|
tableOfContents: 'Inhaltsverzeichnis',
|
|
downloadPdf: 'PDF herunterladen',
|
|
github: 'GitHub',
|
|
publicRepository: 'Öffentliches Repository',
|
|
publicRepositoryDesc: 'Quellcode, Beispiele und Beiträge',
|
|
readmeQuickStart: 'README & Schnellstart',
|
|
readmeQuickStartDesc: 'Installation und Einstiegsanleitung',
|
|
categories: {
|
|
'getting-started': 'Erste Schritte',
|
|
'resources': 'Ressourcen',
|
|
'research-theory': 'Forschung & Theorie',
|
|
'technical-reference': 'Technische Referenz',
|
|
'advanced-topics': 'Fortgeschrittene Themen',
|
|
'business-leadership': 'Business & Führung'
|
|
}
|
|
},
|
|
fr: {
|
|
pageTitle: 'Documentation du Framework',
|
|
pageSubtitle: 'Spécifications techniques, guides et matériels de référence',
|
|
documentsHeading: 'Documents',
|
|
searchButton: 'Rechercher',
|
|
backToDocuments: 'Retour aux documents',
|
|
selectDocument: 'Sélectionner un document',
|
|
selectDocumentDesc: 'Choisissez un document dans la barre latérale pour commencer la lecture',
|
|
loadingDocument: 'Chargement du document...',
|
|
errorLoadingDoc: 'Erreur lors du chargement du document',
|
|
tableOfContents: 'Table des matières',
|
|
downloadPdf: 'Télécharger PDF',
|
|
github: 'GitHub',
|
|
publicRepository: 'Dépôt public',
|
|
publicRepositoryDesc: 'Code source, exemples et contributions',
|
|
readmeQuickStart: 'README & Démarrage rapide',
|
|
readmeQuickStartDesc: 'Guide d\'installation et de démarrage',
|
|
categories: {
|
|
'getting-started': 'Premiers pas',
|
|
'resources': 'Ressources',
|
|
'research-theory': 'Recherche & Théorie',
|
|
'technical-reference': 'Référence technique',
|
|
'advanced-topics': 'Sujets avancés',
|
|
'business-leadership': 'Business & Leadership'
|
|
}
|
|
}
|
|
};
|
|
|
|
// Get current UI translations
|
|
function getUITranslations(lang = currentLanguage) {
|
|
return UI_TRANSLATIONS[lang] || UI_TRANSLATIONS.en;
|
|
}
|
|
|
|
// Update all page UI elements with current language translations
|
|
function updatePageUI(lang = currentLanguage) {
|
|
const t = getUITranslations(lang);
|
|
|
|
// Update page header
|
|
const pageTitle = document.querySelector('h1');
|
|
if (pageTitle && pageTitle.textContent === UI_TRANSLATIONS.en.pageTitle ||
|
|
pageTitle.textContent === UI_TRANSLATIONS.de.pageTitle ||
|
|
pageTitle.textContent === UI_TRANSLATIONS.fr.pageTitle) {
|
|
pageTitle.textContent = t.pageTitle;
|
|
}
|
|
|
|
const pageSubtitle = document.querySelector('.text-gray-600.mt-2');
|
|
if (pageSubtitle && (pageSubtitle.textContent === UI_TRANSLATIONS.en.pageSubtitle ||
|
|
pageSubtitle.textContent === UI_TRANSLATIONS.de.pageSubtitle ||
|
|
pageSubtitle.textContent === UI_TRANSLATIONS.fr.pageSubtitle)) {
|
|
pageSubtitle.textContent = t.pageSubtitle;
|
|
}
|
|
|
|
// Update search button
|
|
const searchBtn = document.querySelector('#open-search-modal-btn span');
|
|
if (searchBtn) {
|
|
searchBtn.textContent = t.searchButton;
|
|
}
|
|
|
|
// Update sidebar Documents heading
|
|
const docsHeading = document.querySelector('aside h3');
|
|
if (docsHeading && (docsHeading.textContent === UI_TRANSLATIONS.en.documentsHeading ||
|
|
docsHeading.textContent === UI_TRANSLATIONS.de.documentsHeading ||
|
|
docsHeading.textContent === UI_TRANSLATIONS.fr.documentsHeading)) {
|
|
docsHeading.textContent = t.documentsHeading;
|
|
}
|
|
|
|
// Update GitHub section heading
|
|
const githubHeadings = document.querySelectorAll('aside h3');
|
|
githubHeadings.forEach(heading => {
|
|
if (heading.textContent.trim() === UI_TRANSLATIONS.en.github ||
|
|
heading.textContent.trim() === UI_TRANSLATIONS.de.github ||
|
|
heading.textContent.trim() === UI_TRANSLATIONS.fr.github) {
|
|
// Keep the SVG icon, just update text
|
|
const textNode = Array.from(heading.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
|
|
if (textNode) {
|
|
textNode.textContent = t.github;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update GitHub links
|
|
const githubLinks = document.querySelectorAll('aside a[href*="github.com"]');
|
|
githubLinks.forEach(link => {
|
|
const titleDiv = link.querySelector('.text-sm.font-medium');
|
|
const descDiv = link.querySelector('.text-xs.text-gray-500');
|
|
|
|
if (titleDiv) {
|
|
if (titleDiv.textContent === UI_TRANSLATIONS.en.publicRepository ||
|
|
titleDiv.textContent === UI_TRANSLATIONS.de.publicRepository ||
|
|
titleDiv.textContent === UI_TRANSLATIONS.fr.publicRepository) {
|
|
titleDiv.textContent = t.publicRepository;
|
|
} else if (titleDiv.textContent === UI_TRANSLATIONS.en.readmeQuickStart ||
|
|
titleDiv.textContent === UI_TRANSLATIONS.de.readmeQuickStart ||
|
|
titleDiv.textContent === UI_TRANSLATIONS.fr.readmeQuickStart) {
|
|
titleDiv.textContent = t.readmeQuickStart;
|
|
}
|
|
}
|
|
|
|
if (descDiv) {
|
|
if (descDiv.textContent === UI_TRANSLATIONS.en.publicRepositoryDesc ||
|
|
descDiv.textContent === UI_TRANSLATIONS.de.publicRepositoryDesc ||
|
|
descDiv.textContent === UI_TRANSLATIONS.fr.publicRepositoryDesc) {
|
|
descDiv.textContent = t.publicRepositoryDesc;
|
|
} else if (descDiv.textContent === UI_TRANSLATIONS.en.readmeQuickStartDesc ||
|
|
descDiv.textContent === UI_TRANSLATIONS.de.readmeQuickStartDesc ||
|
|
descDiv.textContent === UI_TRANSLATIONS.fr.readmeQuickStartDesc) {
|
|
descDiv.textContent = t.readmeQuickStartDesc;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update Back to Documents button
|
|
const backBtn = document.querySelector('#back-to-docs-btn span');
|
|
if (backBtn) {
|
|
backBtn.textContent = t.backToDocuments;
|
|
}
|
|
|
|
// Update Select Document placeholder (if visible)
|
|
const selectDocHeading = document.getElementById('select-document-heading');
|
|
if (selectDocHeading) {
|
|
selectDocHeading.textContent = t.selectDocument;
|
|
}
|
|
|
|
const selectDocDesc = document.getElementById('select-document-desc');
|
|
if (selectDocDesc) {
|
|
selectDocDesc.textContent = t.selectDocumentDesc;
|
|
}
|
|
|
|
// Update page title tag
|
|
document.title = `${t.pageTitle} | Tractatus AI Safety`;
|
|
}
|
|
|
|
// Initialize card-based viewer
|
|
if (typeof DocumentCards !== 'undefined') {
|
|
documentCards = new DocumentCards('document-content');
|
|
}
|
|
|
|
// Detect language from URL, localStorage, or i18n system
|
|
function detectLanguage() {
|
|
// Priority 1: URL parameter
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const urlLang = urlParams.get('lang');
|
|
if (urlLang) {
|
|
return urlLang;
|
|
}
|
|
|
|
// Priority 2: localStorage
|
|
const storedLang = localStorage.getItem('tractatus-lang');
|
|
if (storedLang) {
|
|
return storedLang;
|
|
}
|
|
|
|
// Priority 3: i18n system
|
|
if (window.I18n && window.I18n.currentLang) {
|
|
return window.I18n.currentLang;
|
|
}
|
|
|
|
// Default: English
|
|
return 'en';
|
|
}
|
|
|
|
// Update URL with language parameter
|
|
function updateURL(slug, lang) {
|
|
const url = new URL(window.location);
|
|
url.searchParams.set('doc', slug);
|
|
if (lang && lang !== 'en') {
|
|
url.searchParams.set('lang', lang);
|
|
} else {
|
|
url.searchParams.delete('lang');
|
|
}
|
|
window.history.pushState({}, '', url);
|
|
}
|
|
|
|
// Listen for language changes from i18n system
|
|
if (typeof window !== 'undefined') {
|
|
window.addEventListener('languageChanged', async (e) => {
|
|
const newLang = e.detail.language;
|
|
currentLanguage = newLang;
|
|
|
|
// Update page UI (hero section, sidebar headings, etc.)
|
|
updatePageUI(newLang);
|
|
|
|
// Remember current document slug before reloading list
|
|
const currentSlug = currentDocument ? currentDocument.slug : null;
|
|
|
|
// Update URL lang parameter BEFORE reloading documents
|
|
// This ensures detectLanguage() reads the correct language from URL
|
|
if (currentSlug) {
|
|
updateURL(currentSlug, newLang);
|
|
}
|
|
|
|
// Reload document list to show translated category labels and document titles
|
|
await loadDocuments();
|
|
|
|
// Explicitly reload current document to ensure it updates
|
|
// (loadDocuments auto-loads from URL, but explicit call ensures it happens)
|
|
if (currentSlug) {
|
|
await loadDocument(currentSlug, newLang);
|
|
}
|
|
});
|
|
|
|
// Initialize language on i18n ready
|
|
window.addEventListener('i18nInitialized', (e) => {
|
|
currentLanguage = e.detail.language;
|
|
});
|
|
}
|
|
|
|
// Document categorization - Final 5 categories (curated for public docs)
|
|
const CATEGORIES = {
|
|
'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': {
|
|
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': {
|
|
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': {
|
|
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': {
|
|
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': {
|
|
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 || 'resources';
|
|
|
|
// Validate category exists in CATEGORIES constant
|
|
if (CATEGORIES[category]) {
|
|
return category;
|
|
}
|
|
|
|
// Fallback to resources for uncategorized
|
|
console.warn(`Document "${doc.title}" has invalid category "${category}", using fallback to resources`);
|
|
return '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' : '';
|
|
|
|
// Get translated title if available and language is not English
|
|
let displayTitle = doc.title;
|
|
if (currentLanguage !== 'en' && doc.translations && doc.translations[currentLanguage]) {
|
|
displayTitle = doc.translations[currentLanguage].title || doc.title;
|
|
}
|
|
|
|
// 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 `
|
|
<div class="relative mb-1">
|
|
<button class="doc-link w-full text-left px-3 py-2 ${paddingClass} rounded text-sm hover:bg-blue-50 transition ${highlightClass}"
|
|
data-slug="${doc.slug}">
|
|
<div class="font-medium text-gray-900">${displayTitle}</div>
|
|
</button>
|
|
${hasPDF ? `
|
|
<a href="${pdfPath}"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="doc-download-link"
|
|
title="Download PDF (opens in new tab)">
|
|
<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>
|
|
`;
|
|
}
|
|
|
|
// 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 = '<div class="text-sm text-gray-500">No documents available</div>';
|
|
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);
|
|
|
|
const t = getUITranslations(currentLanguage);
|
|
|
|
sortedCategories.forEach(([categoryId, category]) => {
|
|
const docs = grouped[categoryId] || [];
|
|
if (docs.length === 0) return;
|
|
|
|
const isCollapsed = category.collapsed || false;
|
|
const categoryLabel = t.categories[categoryId] || categoryId;
|
|
|
|
// Category header
|
|
html += `
|
|
<div class="category-section mb-4" data-category="${categoryId}">
|
|
<button class="category-toggle w-full flex items-center justify-between px-3 py-3 text-sm font-bold ${category.textColor} ${category.bgColor} ${category.borderColor} rounded-r hover:shadow-md transition-all"
|
|
data-category="${categoryId}"
|
|
data-collapsed="${isCollapsed}">
|
|
<span class="flex items-center gap-2">
|
|
<span class="category-icon">${category.icon}</span>
|
|
<span>${categoryLabel}</span>
|
|
</span>
|
|
<svg class="category-arrow w-5 h-5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
</svg>
|
|
</button>
|
|
<div class="category-docs mt-2 pl-2" data-category="${categoryId}" data-collapsed="${isCollapsed}">
|
|
`;
|
|
|
|
// 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 += `
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
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 =
|
|
'<div class="text-sm text-red-600">Error loading documents</div>';
|
|
}
|
|
}
|
|
|
|
// Load specific document
|
|
let isLoading = false;
|
|
|
|
async function loadDocument(slug, lang = null) {
|
|
// Prevent multiple simultaneous loads
|
|
if (isLoading) return;
|
|
|
|
// Use provided lang or detect from i18n system
|
|
const language = lang || detectLanguage();
|
|
|
|
try {
|
|
isLoading = true;
|
|
|
|
const t = getUITranslations(language);
|
|
|
|
// 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">${t.loadingDocument}</p>
|
|
</div>
|
|
`;
|
|
|
|
// Build API URL with language parameter
|
|
const apiUrl = language && language !== 'en'
|
|
? `/api/documents/${slug}?lang=${language}`
|
|
: `/api/documents/${slug}`;
|
|
|
|
const response = await fetch(apiUrl);
|
|
const data = await response.json();
|
|
|
|
if (!data.success) {
|
|
// If translation not available, fall back to English
|
|
if (response.status === 404 && language !== 'en') {
|
|
console.warn(`Translation not available for ${language}, falling back to English`);
|
|
const enResponse = await fetch(`/api/documents/${slug}`);
|
|
const enData = await enResponse.json();
|
|
|
|
if (enData.success) {
|
|
// Show notification that translation isn't available
|
|
showTranslationFallbackNotice(language);
|
|
const fallbackData = enData;
|
|
fallbackData.document.language = 'en';
|
|
fallbackData.document.fallback = true;
|
|
|
|
// Use English version
|
|
currentDocument = fallbackData.document;
|
|
currentLanguage = 'en';
|
|
updateURL(slug, 'en');
|
|
|
|
// Continue with rendering
|
|
data.success = true;
|
|
data.document = fallbackData.document;
|
|
} else {
|
|
throw new Error(data.error || 'Failed to load document');
|
|
}
|
|
} else {
|
|
throw new Error(data.error || 'Failed to load document');
|
|
}
|
|
} else {
|
|
currentDocument = data.document;
|
|
currentLanguage = language;
|
|
updateURL(slug, language);
|
|
}
|
|
|
|
// 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 = `
|
|
<div class="flex items-center justify-between mb-6 pb-4 border-b border-gray-200">
|
|
<h1 class="text-3xl font-bold text-gray-900">${currentDocument.title}</h1>
|
|
<div class="flex items-center gap-2">
|
|
${hasToC ? `
|
|
<button id="toc-button"
|
|
class="p-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded transition"
|
|
title="Table of Contents"
|
|
aria-label="Show table of contents">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7"/>
|
|
</svg>
|
|
</button>
|
|
` : ''}
|
|
${hasPDF ? `
|
|
<a href="${pdfPath}"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="p-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded transition"
|
|
title="Download PDF"
|
|
aria-label="Download PDF">
|
|
<svg class="w-6 h-6" 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>
|
|
`;
|
|
|
|
// Remove duplicate title H1 from content (it's already in header)
|
|
let contentHtml = currentDocument.content_html;
|
|
const firstH1Match = contentHtml.match(/<h1[^>]*>.*?<\/h1>/);
|
|
if (firstH1Match) {
|
|
contentHtml = contentHtml.replace(firstH1Match[0], '');
|
|
}
|
|
|
|
contentEl.innerHTML = headerHTML + `
|
|
<div class="prose max-w-none">
|
|
${contentHtml}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 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);
|
|
const t = getUITranslations(currentLanguage);
|
|
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">${t.errorLoadingDoc}</h3>
|
|
<p class="text-sm text-gray-600">${error.message}</p>
|
|
</div>
|
|
`;
|
|
} 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 `
|
|
<a href="#${item.slug}"
|
|
class="block py-2 px-3 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded transition toc-link toc-indent-${item.level}"
|
|
data-slug="${item.slug}">
|
|
${item.title}
|
|
</a>
|
|
`;
|
|
}).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' });
|
|
});
|
|
}
|