"use strict"; const JudgePole = (function(){ const JudgePole = function(){ const self = this, screen = {x : 0, y : 0}; let cells = 40, thread_interval = null, fps = 12; this.item_self = document; this.settings = new SettingsManager(self); this.i18n = new I18NManager(self); this.db = new DatabaseManager(self); this.session = new SessionManager(self); this.views = {}; this.content_area = null; const constructor = () => { const position = self.settings.get("position", null, "body"); let target, preload = setInterval(() => { if(target = document.querySelector(position)){ clearInterval(preload); self.item_self = target.appendChild(document.createElement("div")); self.item_self.className = self.item_self.id = self.settings.get("class_name", null, "judge-pole"); self.item_self.setAttribute("data-cells", cells = self.settings.get("cells", null, cells)); thread_interval = setInterval(thread, 1000 / fps); build(); }; }, fps = self.settings.get(["frames_per_second", "fps"], null, fps)); Common.SCSS(SCSS); }; const thread = () => { if(screen.x != self.item_self.offsetWidth || screen.y != self.item_self.offsetHeight){ screen.x = self.item_self.offsetWidth; screen.y = self.item_self.offsetHeight; self.item_self.style.fontSize = ((screen.x < screen.y ? screen.x : screen.y) / cells) + "px"; }; }; const build = () => { self.views.users = new UsersView(self); self.views.projects = new ProjectsView(self); self.views.votes = new VotesView(self); self.render_header(); self.content_area = self.html(["main", { "class" : "jp-content-area" }])[0]; self.item_self.appendChild(self.content_area); switch_view("votes"); }; // 1. Declarar render_header expuesto en 'this' / 'self' this.render_header = () => { const existing_header = self.item_self.querySelector("header.jp-header"); if(existing_header) existing_header.remove(); const is_admin = self.session.is_admin(); const auth_required = self.session.is_auth_required(); const nav_buttons = [ ["button", { "class" : "jp-btn" + (self.current_view === "users" ? " is-active" : ""), "data-i18n" : "nav_users", "onclick" : (btn) => switch_view("users", btn) }, self.i18n.get("nav_users")], ["button", { "class" : "jp-btn" + (self.current_view === "projects" ? " is-active" : ""), "data-i18n" : "nav_projects", "onclick" : (btn) => switch_view("projects", btn) }, self.i18n.get("nav_projects")], ["button", { "class" : "jp-btn" + (self.current_view === "votes" ? " is-active" : ""), "data-i18n" : "nav_votes", "onclick" : (btn) => switch_view("votes", btn) }, self.i18n.get("nav_votes")] ]; // Botón de Base64 sólo para administradores if(is_admin){ nav_buttons.push(["button", { "class" : "jp-btn jp-btn-secondary", "data-i18n" : "nav_export", "onclick" : () => show_export_modal() }, self.i18n.get("nav_export")]); } // Botón de Login / Logout si la base de datos requiere autenticación if(auth_required){ if(is_admin){ nav_buttons.push(["button", { "type" : "button", "class" : "jp-btn-danger", "data-i18n" : "nav_logout", "style" : "margin-left:0.5em;", "onclick" : () => { self.session.logout(); self.render_header(); self.views[self.current_view].go(); } }, self.i18n.get("nav_logout")]); } else { nav_buttons.push(["button", { "type" : "button", "class" : "jp-btn-primary", "data-i18n" : "nav_login", "style" : "margin-left:0.5em;", "onclick" : () => show_login_modal() }, self.i18n.get("nav_login")]); } } const current_lang = self.i18n.get_current_language(); const lang_options = self.i18n.get_languages().map(lang_key => { const label_key = "lang_" + lang_key; return ["option", { "value" : lang_key, "data-i18n" : label_key, ...(lang_key === current_lang ? { "selected" : "selected" } : {}) }, self.i18n.get(label_key, null, lang_key)]; }); const lang_select = self.html(["select", { "class" : "jp-lang-select", "onchange" : (select) => { self.i18n.set_language(select.value); if(self.current_view && self.views[self.current_view]){ self.views[self.current_view].go(); } } }, lang_options])[0]; const header = self.html(["header", { "class" : "jp-header" }, [ ["h1", { "class" : "jp-title", "data-i18n" : "judge_pole" }, self.i18n.get("judge_pole")], ["div", { "style" : "display:flex; align-items:center; gap:0.6em;" }, [ ["nav", { "class" : "jp-nav" }, nav_buttons], lang_select ]] ]])[0]; self.item_self.insertBefore(header, self.content_area || null); }; const show_login_modal = () => { const close_modal = () => { if(modal_overlay && modal_overlay.parentNode) modal_overlay.remove(); }; const modal_overlay = self.html(["div", { "class" : "jp-modal-overlay", "onclick" : (overlay, e) => { if(e.target === overlay) close_modal(); } }, [ ["div", { "class" : "jp-modal-window", "style" : "max-width:24em;" }, [ ["div", { "class" : "jp-modal-header" }, [ ["h3", { "data-i18n" : "login_modal_title" }, self.i18n.get("login_modal_title")], ["button", { "type" : "button", "class" : "jp-btn-danger", "onclick" : () => close_modal() }, "✕"] ]], ["div", { "class" : "jp-modal-body" }, [ ["form", { "onsubmit" : (f, e) => { e.preventDefault(); const nick = f.querySelector("[name='nick']").value; const pass = f.querySelector("[name='password']").value; if(self.session.login(nick, pass)){ close_modal(); self.render_header(); self.views[self.current_view].go(); } else { alert(self.i18n.get("login_error")); } } }, [ ["div", { "class" : "jp-field" }, [ ["label", { "data-i18n" : "login_nick" }, self.i18n.get("login_nick")], ["input", { "type" : "text", "name" : "nick", "required" : "required" }] ]], ["div", { "class" : "jp-field" }, [ ["label", { "data-i18n" : "login_pass" }, self.i18n.get("login_pass")], ["input", { "type" : "password", "name" : "password", "required" : "required" }] ]], ["div", { "style" : "display:flex; justify-content:flex-end; gap:0.5em; margin-top:1em;" }, [ ["button", { "type" : "button", "class" : "jp-btn", "onclick" : () => close_modal() }, "Cancelar"], ["button", { "type" : "submit", "class" : "jp-btn-primary", "data-i18n" : "login_btn" }, self.i18n.get("login_btn")] ]] ]] ]] ]] ]])[0]; self.item_self.appendChild(modal_overlay); }; const switch_view = (view_name, active_btn) => { self.current_view = view_name; self.item_self.querySelectorAll(".jp-nav .jp-btn").forEach(b => b.classList.remove("is-active")); if(active_btn) active_btn.classList.add("is-active"); if(self.views[view_name]) self.views[view_name].go(); }; const show_export_modal = () => { const b64 = self.db.export_base64(); const close_modal = () => { if(modal_overlay && modal_overlay.parentNode) modal_overlay.remove(); }; const modal_overlay = self.html(["div", { "class" : "jp-modal-overlay", "onclick" : (overlay, event) => { // Cerrar sólo si se hace clic sobre el fondo oscuro exterior if(event.target === overlay) close_modal(); } }, [ ["div", { "class" : "jp-modal-window", "style" : "max-width:32em;" }, [ ["div", { "class" : "jp-modal-header" }, [ ["h3", {"data-i18n" : "db_modal_title"}, self.i18n.get("db_modal_title")], ["button", { "type" : "button", "class" : "jp-btn-danger", "onclick" : () => close_modal() }, "✕"] ]], ["div", { "class" : "jp-modal-body", "style" : "display:flex; flex-direction:column; gap:0.6em;" }, [ ["p", { "class" : "jp-desc", "style" : "margin:0;" }, "Copia esta constante para sobreescribir /Public/data/database.ecma.js:"], ["textarea", { "style" : "width:100%; height:8em; box-sizing:border-box; font-family:monospace; font-size:0.75em; background:#0d1117; color:#58a6ff; border:0.05em solid #30363d; padding:0.5em; border-radius:0.2em; resize:none;", "readonly" : "readonly" }, `const database = "${b64}";`], ["div", { "style" : "display:flex; justify-content:flex-end; gap:0.5em; margin-top:0.5em;" }, [ ["button", { "type" : "button", "class" : "jp-btn-primary", "data-i18n" : "db_copy_btn", "onclick" : () => { navigator.clipboard.writeText(`const database = "${b64}";`); alert(self.i18n.get("db_copied")); }}, self.i18n.get("db_copy_btn")], ["button", { "type" : "button", "class" : "jp-btn-secondary", "data-i18n" : "db_import_btn", "onclick" : () => { const input = prompt("Pega el Base64:"); if(input && self.db.import_base64(input)){ alert(self.i18n.get("db_imported")); self.views.users.go(); close_modal(); } }}, self.i18n.get("db_import_btn")], ["button", { "type" : "button", "class" : "jp-btn", "onclick" : () => close_modal() }, "Cerrar"] ]] ]] ]] ]])[0]; self.item_self.appendChild(modal_overlay); }; /** * @param {...(HTMLElement|string|Array.)} inputs * @returns {Array.} * @access public * @static */ this.html = (...inputs) => { const fragment = new DocumentFragment(), master = ( inputs[0] && (inputs[0].tagName || inputs[0].nodeName) ? inputs[0] : typeof inputs[0] == "string" ? document.querySelector(inputs[0]) : null), items = []; (master ? inputs.slice(1) : inputs).forEach(subinputs => { if(subinputs && (subinputs.tagName || subinputs.nodeName)) fragment.appendChild(subinputs); else if(typeof subinputs == "string") fragment.appendChild(document.createTextNode(subinputs)); else if(subinputs instanceof Array){ const [tag, attributes, children] = subinputs.concat([null, null]).slice(0, 3), item = document.createElement(tag); attributes && attributes.constructor == Object && Object.entries(attributes).forEach(([key, value]) => { if(/^on[_\-]?/i.test(key) && typeof value == "function") item.addEventListener(key.replace(/^on[_\-]?/i, "").replace(/[^a-z0-9]+/gi, "").toLowerCase(), event => { value(item, event); }); else if(value !== null) item.setAttribute(key.replace(/[^a-z0-9]+/gi, "-"), value); }); if(children instanceof Array) self.html(...children).forEach(child => item.appendChild(child)); else if(typeof children == "string") item.innerHTML += children; fragment.appendChild(item); }; }); items.push(...fragment.childNodes); master && master.appendChild(fragment); return items; }; constructor(); }; return JudgePole; })();