import { store } from '@/state/index.ts'; import { Router } from '@/router/index.ts'; import { fetchTags, fetchHstTags, fetchGroups, fetchLibraries, fetchLibraryMembers } from '@/api/index.ts'; import { GridView, TreeView, GraphView, DetailPanel } from '@/views/index.ts'; import { $, $$, delegateEvent, toast, createNameMap, resolveGroupName } from '@/utils/index.ts'; import { CATS, EDGE_COLORS } from '@/config/index.ts'; import type { BaseType, ViewType, CategoryKey, EdgeType } from '@/types/index.ts'; import './styles/main.css'; class App { private router: Router; private currentView: GridView | TreeView | GraphView | null = null; private detailPanel: DetailPanel | null = null; constructor() { this.router = new Router(store, () => this.init()); } async start(): Promise { this.router.parseHash(); await this.init(); this.bindEvents(); } private async init(): Promise { const contentArea = $('#content-area'); const detailPanelEl = $('#detail-panel'); if (!contentArea || !detailPanelEl) return; // Update UI this.updateBaseButtons(); this.updateViewTabs(); // Show loading contentArea.innerHTML = '
Cargando...
'; // Fetch data const state = store.getState(); const [tags, hstTags, groups, libraries] = await Promise.all([ fetchTags(state.base), fetchHstTags(), // Always load HST for group name resolution fetchGroups(), fetchLibraries(state.base) // Load libraries for current base ]); store.setState({ tags, hstTags, groups, libraries }); // Render groups this.renderGroups(); this.renderLibraries(); // Setup detail panel if (!this.detailPanel) { this.detailPanel = new DetailPanel(detailPanelEl, store); } // Render view this.renderView(); } private renderView(): void { const contentArea = $('#content-area'); if (!contentArea) return; const state = store.getState(); const showDetail = (mrf: string) => this.detailPanel?.showDetail(mrf); // Unmount current view this.currentView?.unmount(); // Clear and set class contentArea.innerHTML = ''; contentArea.className = `content-area ${state.view}-view`; // Mount new view switch (state.view) { case 'grid': this.currentView = new GridView(contentArea, store, showDetail); this.currentView.mount(); break; case 'tree': this.currentView = new TreeView(contentArea, store, showDetail); this.currentView.mount(); break; case 'graph': this.currentView = new GraphView(contentArea, store, showDetail); (this.currentView as GraphView).mount(); break; } } private renderGroups(): void { const container = $('#groups-bar'); if (!container) return; const state = store.getState(); // Use hstTags for group name resolution (set_hst points to hst tags) const nameMap = createNameMap(state.hstTags, state.lang); // Count tags per group const counts = new Map(); state.tags.forEach(tag => { const group = tag.set_hst || 'sin-grupo'; counts.set(group, (counts.get(group) || 0) + 1); }); // Sort by count and take top 20 const sorted = Array.from(counts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 20); container.innerHTML = ` ${sorted.map(([groupMrf, count]) => { const groupName = resolveGroupName(groupMrf === 'sin-grupo' ? undefined : groupMrf, nameMap); return ` `; }).join('')} `; delegateEvent(container, '.group-btn', 'click', (_, target) => { const group = target.dataset.group || 'all'; store.setState({ group }); this.renderGroups(); this.renderView(); }); } private renderLibraries(): void { const container = $('#left-panel'); if (!container) return; const state = store.getState(); // Show graph options when in graph view if (state.view === 'graph') { container.classList.add('graph-mode'); this.renderGraphOptions(container); return; } container.classList.remove('graph-mode'); container.innerHTML = `
ALL
${state.libraries.map(lib => { const icon = lib.img_thumb_url || lib.icon_url || ''; const name = lib.name || lib.name_es || lib.alias || lib.ref || lib.mrf.slice(0, 6); return `
${icon ? `` : ''} ${name.slice(0, 8)}
`; }).join('')} `; delegateEvent(container, '.lib-icon', 'click', async (_, target) => { const library = target.dataset.lib || 'all'; const currentBase = store.getState().base; if (library === 'all') { store.setState({ library: 'all', libraryMembers: new Set() }); } else { const members = await fetchLibraryMembers(library, currentBase); store.setState({ library, libraryMembers: new Set(members) }); } this.renderLibraries(); this.renderView(); }); } private renderGraphOptions(container: HTMLElement): void { const state = store.getState(); const { graphFilters, graphSettings, tags, graphEdges } = state; // Count nodes and edges const nodeCount = tags.length; const edgeCount = graphEdges.length; container.innerHTML = `
Nodos ${nodeCount}
Edges ${edgeCount}
Categorias
${Object.entries(CATS).map(([key, config]) => ` `).join('')}
Relaciones
${Object.entries(EDGE_COLORS).map(([key, color]) => ` `).join('')}
Visualizacion
Nodo ${graphSettings.nodeSize}px
Distancia ${graphSettings.linkDist}px
`; // Bind events this.bindGraphOptionEvents(container); } private bindGraphOptionEvents(container: HTMLElement): void { // Category checkboxes container.querySelectorAll('[data-cat]').forEach(cb => { cb.addEventListener('change', () => { const cat = cb.dataset.cat as CategoryKey; const state = store.getState(); const newCats = new Set(state.graphFilters.cats); if (cb.checked) { newCats.add(cat); } else { newCats.delete(cat); } store.setState({ graphFilters: { ...state.graphFilters, cats: newCats } }); this.renderView(); }); }); // Edge checkboxes container.querySelectorAll('[data-edge]').forEach(cb => { cb.addEventListener('change', () => { const edge = cb.dataset.edge as EdgeType; const state = store.getState(); const newEdges = new Set(state.graphFilters.edges); if (cb.checked) { newEdges.add(edge); } else { newEdges.delete(edge); } store.setState({ graphFilters: { ...state.graphFilters, edges: newEdges } }); this.renderView(); }); }); // Show images checkbox const showImgCb = container.querySelector('#graph-show-img'); showImgCb?.addEventListener('change', () => { const state = store.getState(); store.setState({ graphSettings: { ...state.graphSettings, showImg: showImgCb.checked } }); this.renderView(); }); // Show labels checkbox const showLblCb = container.querySelector('#graph-show-lbl'); showLblCb?.addEventListener('change', () => { const state = store.getState(); store.setState({ graphSettings: { ...state.graphSettings, showLbl: showLblCb.checked } }); this.renderView(); }); // Node size slider const nodeSizeSlider = container.querySelector('#graph-node-size'); const nodeSizeVal = container.querySelector('#node-size-val'); nodeSizeSlider?.addEventListener('input', () => { const size = parseInt(nodeSizeSlider.value, 10); if (nodeSizeVal) nodeSizeVal.textContent = `${size}px`; const state = store.getState(); store.setState({ graphSettings: { ...state.graphSettings, nodeSize: size } }); this.renderView(); }); // Link distance slider const linkDistSlider = container.querySelector('#graph-link-dist'); const linkDistVal = container.querySelector('#link-dist-val'); linkDistSlider?.addEventListener('input', () => { const dist = parseInt(linkDistSlider.value, 10); if (linkDistVal) linkDistVal.textContent = `${dist}px`; const state = store.getState(); store.setState({ graphSettings: { ...state.graphSettings, linkDist: dist } }); this.renderView(); }); } private updateBaseButtons(): void { const state = store.getState(); $$('.base-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.base === state.base); }); } private updateViewTabs(): void { const state = store.getState(); $$('.view-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.view === state.view); }); } private bindEvents(): void { // Base buttons delegateEvent(document.body, '.base-btn', 'click', async (_, target) => { const base = target.dataset.base as BaseType; if (!base) return; store.setState({ base, group: 'all', library: 'all', libraryMembers: new Set(), search: '', graphEdges: [], treeEdges: [], selected: new Set(), selectionMode: false }); this.router.updateHash(); await this.init(); }); // View tabs delegateEvent(document.body, '.view-tab', 'click', (_, target) => { const view = target.dataset.view as ViewType; if (!view) return; store.setState({ view }); this.router.updateHash(); this.detailPanel?.close(); this.updateViewTabs(); this.renderLibraries(); // Update left panel (graph options vs libraries) this.renderView(); }); // Search const searchInput = $('#search') as HTMLInputElement; if (searchInput) { let timeout: number; searchInput.addEventListener('input', () => { clearTimeout(timeout); timeout = window.setTimeout(() => { store.setState({ search: searchInput.value }); this.renderView(); }, 200); }); } // Language select const langSelect = $('#lang-select') as HTMLSelectElement; if (langSelect) { langSelect.addEventListener('change', () => { store.setState({ lang: langSelect.value as 'es' | 'en' | 'ch' }); this.renderView(); }); } // Selection mode const selBtn = $('#btn-sel'); if (selBtn) { selBtn.addEventListener('click', () => { const state = store.getState(); store.setState({ selectionMode: !state.selectionMode, selected: state.selectionMode ? new Set() : state.selected }); selBtn.classList.toggle('active', !state.selectionMode); this.updateSelectionCount(); this.renderView(); }); } // Get selected const getBtn = $('#btn-get'); if (getBtn) { getBtn.addEventListener('click', () => { const state = store.getState(); if (state.selected.size === 0) { toast('No hay seleccionados'); return; } navigator.clipboard.writeText([...state.selected].join('\n')) .then(() => toast(`Copiados ${state.selected.size} mrfs`)); }); } // API modal const apiBtn = $('#btn-api'); const apiModal = $('#api-modal'); if (apiBtn && apiModal) { apiBtn.addEventListener('click', () => apiModal.classList.add('open')); apiModal.addEventListener('click', (e) => { if (e.target === apiModal) apiModal.classList.remove('open'); }); const closeBtn = apiModal.querySelector('.modal-close'); closeBtn?.addEventListener('click', () => apiModal.classList.remove('open')); } // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { this.detailPanel?.close(); $('#api-modal')?.classList.remove('open'); if (store.getState().selectionMode) { store.setState({ selectionMode: false, selected: new Set() }); $('#btn-sel')?.classList.remove('active'); this.renderView(); } } if (e.key === '/' && (e.target as HTMLElement).tagName !== 'INPUT') { e.preventDefault(); ($('#search') as HTMLInputElement)?.focus(); } }); } private updateSelectionCount(): void { const counter = $('#sel-count'); if (counter) { const count = store.getState().selected.size; counter.textContent = count > 0 ? `(${count})` : ''; } } } // Bootstrap document.addEventListener('DOMContentLoaded', () => { new App().start(); });