- Add comprehensive UI translations object for EN, DE, FR - Translate page header, category labels, sidebar headings - Translate search button, GitHub section, all UI elements - Update category rendering to use translated labels - Display translated document titles from database in sidebar - Add updatePageUI function to apply translations dynamically - Update docs.html with IDs for dynamic translation - Language selector now updates entire page UI and document list All UI elements now fully support German and French translations.
906 lines
31 KiB
JavaScript
906 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_language');
|
|
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', (e) => {
|
|
const newLang = e.detail.language;
|
|
currentLanguage = newLang;
|
|
|
|
// Reload current document in new language
|
|
if (currentDocument) {
|
|
loadDocument(currentDocument.slug, 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 || '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' : '';
|
|
|
|
// 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();
|
|
|
|
// Initialize language selector and detect initial language
|
|
(function initLanguageSelector() {
|
|
const selector = document.getElementById('language-selector');
|
|
if (!selector) return;
|
|
|
|
// Set initial value from current language
|
|
const initialLang = detectLanguage();
|
|
selector.value = initialLang;
|
|
currentLanguage = initialLang;
|
|
|
|
// Update page UI with initial language
|
|
updatePageUI(initialLang);
|
|
|
|
// Handle language change
|
|
selector.addEventListener('change', async (e) => {
|
|
const newLang = e.target.value;
|
|
|
|
// Save to localStorage
|
|
localStorage.setItem('tractatus_language', newLang);
|
|
|
|
// Update current language
|
|
currentLanguage = newLang;
|
|
|
|
// Update all page UI elements
|
|
updatePageUI(newLang);
|
|
|
|
// Reload document list to show translated category labels and document titles
|
|
const currentSlug = currentDocument ? currentDocument.slug : null;
|
|
await loadDocuments();
|
|
|
|
// If a document was loaded, reload it in the new language
|
|
if (currentSlug) {
|
|
loadDocument(currentSlug, newLang);
|
|
}
|
|
});
|
|
})();
|
|
|
|
// 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' });
|
|
});
|
|
}
|