JudgePole/Public/ecma/Application/JudgePole.ecma.js
2026-09-20 17:12:09 +02:00

300 lines
14 KiB
JavaScript

"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.<any|null>)} inputs
* @returns {Array.<HTMLElement>}
* @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;
})();