Files
Mahi 80566bf0a2 feat: Goa GEL Blockchain e-Licensing Platform - Full Stack Implementation
Complete implementation of the Goa Government e-Licensing platform with:

Backend:
- NestJS API with JWT authentication
- PostgreSQL database with Knex ORM
- Redis caching and session management
- MinIO document storage
- Hyperledger Besu blockchain integration
- Multi-department workflow system
- Comprehensive API tests (266/282 passing)

Frontend:
- Angular 21 with standalone components
- Angular Material + TailwindCSS UI
- Visual workflow builder
- Document upload with progress tracking
- Blockchain explorer integration
- Role-based dashboards (Admin, Department, Citizen)
- E2E tests with Playwright (37 tests)

Infrastructure:
- Docker Compose orchestration
- Blockscout blockchain explorer
- Development and production configurations
2026-02-07 10:23:29 -04:00

323 lines
9.3 KiB
JavaScript

// Viewer.js - Document viewer functionality
// Configuration
const DOC_MAP = {
'USER_GUIDE': '/docs/USER_GUIDE.md',
'E2E_TESTING_GUIDE': '/docs/E2E_TESTING_GUIDE.md',
'IMPLEMENTATION_COMPLETE': '/docs/IMPLEMENTATION_COMPLETE.md',
'ARCHITECTURE_GUIDE': '/docs/ARCHITECTURE_GUIDE.md',
'QUICK_START': '/docs/QUICK_START.md',
'DOCUMENTATION_INDEX': '/docs/DOCUMENTATION_INDEX.md',
'IMPLEMENTATION_SUMMARY': '/docs/IMPLEMENTATION_SUMMARY.md'
};
let currentDoc = null;
let sidebarOpen = true;
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
initViewer();
setupEventListeners();
loadDocumentFromURL();
});
function initViewer() {
// Configure marked.js
if (typeof marked !== 'undefined') {
marked.setOptions({
breaks: true,
gfm: true,
highlight: function(code, lang) {
if (typeof hljs !== 'undefined' && lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(code, { language: lang }).value;
} catch (err) {
console.error('Highlight error:', err);
}
}
return code;
}
});
}
}
function setupEventListeners() {
// Sidebar toggle
const toggleBtn = document.getElementById('toggle-sidebar');
if (toggleBtn) {
toggleBtn.addEventListener('click', toggleSidebar);
}
// Document selector
const docSelector = document.getElementById('doc-selector');
if (docSelector) {
docSelector.addEventListener('change', function() {
if (this.value) {
loadDocument(this.value);
}
});
}
// Print button
const printBtn = document.getElementById('print-doc');
if (printBtn) {
printBtn.addEventListener('click', function() {
window.print();
});
}
// Download button
const downloadBtn = document.getElementById('download-doc');
if (downloadBtn) {
downloadBtn.addEventListener('click', downloadCurrentDoc);
}
// Sidebar navigation links
document.querySelectorAll('.sidebar-nav .nav-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const url = this.getAttribute('href');
const params = new URLSearchParams(url.split('?')[1]);
const doc = params.get('doc');
if (doc) {
loadDocument(doc);
updateURL(doc);
}
});
});
}
function loadDocumentFromURL() {
const params = new URLSearchParams(window.location.search);
const doc = params.get('doc');
if (doc && DOC_MAP[doc]) {
loadDocument(doc);
} else {
showError('No document specified. Please select a document from the navigation.');
}
}
async function loadDocument(docKey) {
const docPath = DOC_MAP[docKey];
if (!docPath) {
showError(`Document "${docKey}" not found`);
return;
}
currentDoc = docKey;
showLoading();
hideError();
try {
const response = await fetch(docPath);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const markdown = await response.text();
renderMarkdown(markdown);
generateTOC();
updateActiveNav(docKey);
updateDocSelector(docKey);
updateURL(docKey);
// Scroll to top
window.scrollTo(0, 0);
} catch (error) {
console.error('Error loading document:', error);
showError(`Failed to load document: ${error.message}`);
} finally {
hideLoading();
}
}
function renderMarkdown(markdown) {
const contentDiv = document.getElementById('content');
if (!contentDiv) return;
try {
// Parse markdown to HTML
const html = marked.parse(markdown);
// Sanitize HTML using DOMPurify if available, otherwise use trusted content
const safeHTML = (typeof DOMPurify !== 'undefined')
? DOMPurify.sanitize(html, { ADD_ATTR: ['target'] })
: html;
contentDiv.innerHTML = safeHTML;
// Syntax highlighting for code blocks
if (typeof hljs !== 'undefined') {
contentDiv.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
}
// Make external links open in new tab
contentDiv.querySelectorAll('a[href^="http"]').forEach(link => {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
});
// Add IDs to headings for anchor links
contentDiv.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((heading, index) => {
if (!heading.id) {
heading.id = `heading-${index}`;
}
});
} catch (error) {
console.error('Error rendering markdown:', error);
showError(`Failed to render document: ${error.message}`);
}
}
function generateTOC() {
const tocContent = document.getElementById('toc-content');
const content = document.getElementById('content');
if (!tocContent || !content) return;
const headings = content.querySelectorAll('h2, h3');
if (headings.length === 0) {
tocContent.textContent = 'No headings found';
return;
}
// Clear existing content
tocContent.innerHTML = '';
// Create TOC links using DOM methods
headings.forEach(heading => {
const level = heading.tagName.toLowerCase();
const text = heading.textContent;
const id = heading.id || `heading-${text.replace(/\s+/g, '-').toLowerCase()}`;
heading.id = id;
const link = document.createElement('a');
link.href = `#${id}`;
link.textContent = text;
if (level === 'h3') {
link.style.marginLeft = '1rem';
}
link.addEventListener('click', function(e) {
e.preventDefault();
const target = document.getElementById(id);
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
tocContent.appendChild(link);
});
}
function toggleSidebar() {
const sidebar = document.querySelector('.sidebar');
const icon = document.getElementById('sidebar-icon');
if (sidebar) {
sidebarOpen = !sidebarOpen;
sidebar.classList.toggle('active');
if (icon) {
icon.textContent = sidebarOpen ? '☰' : '✕';
}
}
}
function updateActiveNav(docKey) {
document.querySelectorAll('.sidebar-nav .nav-link').forEach(link => {
link.classList.remove('active');
const url = link.getAttribute('href');
if (url && url.includes(`doc=${docKey}`)) {
link.classList.add('active');
}
});
}
function updateDocSelector(docKey) {
const selector = document.getElementById('doc-selector');
if (selector) {
selector.value = docKey;
}
}
function updateURL(docKey) {
const newURL = `${window.location.pathname}?doc=${docKey}`;
window.history.pushState({ doc: docKey }, '', newURL);
}
function downloadCurrentDoc() {
if (!currentDoc) {
alert('No document loaded');
return;
}
const docPath = DOC_MAP[currentDoc];
const filename = docPath.split('/').pop();
fetch(docPath)
.then(response => response.text())
.then(text => {
const blob = new Blob([text], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
})
.catch(error => {
console.error('Download error:', error);
alert('Failed to download document');
});
}
function showLoading() {
const loading = document.getElementById('loading');
const content = document.getElementById('content');
if (loading) loading.style.display = 'block';
if (content) content.style.display = 'none';
}
function hideLoading() {
const loading = document.getElementById('loading');
const content = document.getElementById('content');
if (loading) loading.style.display = 'none';
if (content) content.style.display = 'block';
}
function showError(message) {
const error = document.getElementById('error');
const errorMessage = document.getElementById('error-message');
const content = document.getElementById('content');
if (error) error.style.display = 'block';
if (errorMessage) errorMessage.textContent = message;
if (content) content.style.display = 'none';
}
function hideError() {
const error = document.getElementById('error');
if (error) error.style.display = 'none';
}
// Handle browser back/forward buttons
window.addEventListener('popstate', function(event) {
if (event.state && event.state.doc) {
loadDocument(event.state.doc);
}
});
// Search functionality (future enhancement)
function searchDocumentation(query) {
// TODO: Implement search across all documentation
console.log('Search query:', query);
}