feat: Project finished.
This commit is contained in:
parent
978e4f3819
commit
91a645af4b
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/Public/data
|
||||||
300
Public/ecma/Application/JudgePole.ecma.js
Normal file
300
Public/ecma/Application/JudgePole.ecma.js
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
"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;
|
||||||
|
})();
|
||||||
221
Public/ecma/Managers/DatabaseManager.ecma.js
Normal file
221
Public/ecma/Managers/DatabaseManager.ecma.js
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const DatabaseManager = (function(){
|
||||||
|
|
||||||
|
const DatabaseManager = function(judge_pole){
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
let data = {
|
||||||
|
users : [],
|
||||||
|
projects : [],
|
||||||
|
criteria : [],
|
||||||
|
votes : {} // { project_id: { user_id: { criterion_name: score } } }
|
||||||
|
};
|
||||||
|
|
||||||
|
const constructor = () => {
|
||||||
|
// Inicializar criterios por defecto si no existen
|
||||||
|
const default_crit = judge_pole.settings.get("default_criteria", null, []);
|
||||||
|
default_crit.forEach(crit => self.add_criterion(crit));
|
||||||
|
|
||||||
|
// Carga de base de datos externa inyectada
|
||||||
|
if(typeof database != "undefined" && typeof database == "string"){
|
||||||
|
self.import_base64(database);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.get_data = () => data;
|
||||||
|
|
||||||
|
this.add_criterion = (criterion_name) => {
|
||||||
|
criterion_name = ("" + criterion_name).trim();
|
||||||
|
if(criterion_name && !data.criteria.includes(criterion_name)){
|
||||||
|
data.criteria.push(criterion_name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update_criterion = (old_name, new_name) => {
|
||||||
|
old_name = ("" + old_name).trim();
|
||||||
|
new_name = ("" + new_name).trim();
|
||||||
|
|
||||||
|
if(!new_name || old_name === new_name) return false;
|
||||||
|
|
||||||
|
const index = data.criteria.indexOf(old_name);
|
||||||
|
if(index === -1) return false;
|
||||||
|
|
||||||
|
// Reemplazar en la lista maestra
|
||||||
|
data.criteria[index] = new_name;
|
||||||
|
|
||||||
|
// Propagar en cascada en todos los votos existentes
|
||||||
|
Object.keys(data.votes).forEach(project_id => {
|
||||||
|
const project_votes = data.votes[project_id];
|
||||||
|
if(project_votes){
|
||||||
|
Object.keys(project_votes).forEach(user_id => {
|
||||||
|
if(project_votes[user_id] && project_votes[user_id][old_name] !== undefined){
|
||||||
|
project_votes[user_id][new_name] = project_votes[user_id][old_name];
|
||||||
|
delete project_votes[user_id][old_name];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.delete_criterion = (criterion_name) => {
|
||||||
|
criterion_name = ("" + criterion_name).trim();
|
||||||
|
const initial_len = data.criteria.length;
|
||||||
|
data.criteria = data.criteria.filter(c => c !== criterion_name);
|
||||||
|
|
||||||
|
if(data.criteria.length !== initial_len){
|
||||||
|
// Purgar el criterio de todos los votos guardados
|
||||||
|
Object.keys(data.votes).forEach(project_id => {
|
||||||
|
const project_votes = data.votes[project_id];
|
||||||
|
if(project_votes){
|
||||||
|
Object.keys(project_votes).forEach(user_id => {
|
||||||
|
if(project_votes[user_id] && project_votes[user_id][criterion_name] !== undefined){
|
||||||
|
delete project_votes[user_id][criterion_name];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.save_votes = (project_id, vote_package) => {
|
||||||
|
// vote_package: { scores, judge_weights, criteria_weights }
|
||||||
|
data.votes[project_id] = vote_package;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.export_base64 = () => {
|
||||||
|
const json_string = JSON.stringify(data, null, 4);
|
||||||
|
return btoa(unescape(encodeURIComponent(json_string)));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.import_base64 = (base64_string) => {
|
||||||
|
try {
|
||||||
|
const json_string = decodeURIComponent(escape(atob(base64_string.trim())));
|
||||||
|
data = JSON.parse(json_string);
|
||||||
|
return true;
|
||||||
|
} catch(error) {
|
||||||
|
console.error("Error al importar Base64:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.delete_user = (user_id) => {
|
||||||
|
const initial_length = data.users.length;
|
||||||
|
data.users = data.users.filter(u => u.id !== user_id);
|
||||||
|
|
||||||
|
if(data.users.length !== initial_length){
|
||||||
|
// Limpieza de proyectos huérfanos asociados a este usuario
|
||||||
|
const removed_project_ids = data.projects
|
||||||
|
.filter(p => p.user_id === user_id)
|
||||||
|
.map(p => p.id);
|
||||||
|
|
||||||
|
data.projects = data.projects.filter(p => p.user_id !== user_id);
|
||||||
|
|
||||||
|
// Limpieza de votos del usuario o de proyectos eliminados
|
||||||
|
removed_project_ids.forEach(p_id => {
|
||||||
|
delete data.votes[p_id];
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(data.votes).forEach(p_id => {
|
||||||
|
if(data.votes[p_id] && data.votes[p_id][user_id]){
|
||||||
|
delete data.votes[p_id][user_id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.add_user = (user_data) => {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const user = {
|
||||||
|
id : "usr_" + Date.now() + "_" + Math.floor(Math.random() * 1000),
|
||||||
|
type : user_data.type || "human", // "human" | "ai"
|
||||||
|
nick : user_data.nick || "Anon",
|
||||||
|
password : user_data.password ? user_data.password.trim() : "",
|
||||||
|
description : user_data.description || "",
|
||||||
|
avatar : user_data.avatar || "",
|
||||||
|
links : user_data.links || [],
|
||||||
|
created_at : now,
|
||||||
|
updated_at : now
|
||||||
|
};
|
||||||
|
data.users.push(user);
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update_user = (user_id, updated_fields) => {
|
||||||
|
const user = data.users.find(u => u.id === user_id);
|
||||||
|
if(!user) return null;
|
||||||
|
|
||||||
|
user.type = updated_fields.type !== undefined ? updated_fields.type : (user.type || "human");
|
||||||
|
user.nick = updated_fields.nick !== undefined ? updated_fields.nick : user.nick;
|
||||||
|
user.password = updated_fields.password !== undefined ? updated_fields.password.trim() : (user.password || "");
|
||||||
|
user.description = updated_fields.description !== undefined ? updated_fields.description : user.description;
|
||||||
|
user.avatar = updated_fields.avatar !== undefined ? updated_fields.avatar : user.avatar;
|
||||||
|
user.links = updated_fields.links !== undefined ? updated_fields.links : user.links;
|
||||||
|
user.updated_at = new Date().toISOString();
|
||||||
|
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.add_project = (project_data) => {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const project = {
|
||||||
|
id : "prj_" + Date.now() + "_" + Math.floor(Math.random() * 1000),
|
||||||
|
user_ids : project_data.user_ids || (project_data.user_id ? [project_data.user_id] : []),
|
||||||
|
name : project_data.name,
|
||||||
|
description : project_data.description || "",
|
||||||
|
avatar : project_data.avatar || "",
|
||||||
|
tags : project_data.tags || [],
|
||||||
|
technologies : project_data.technologies || [],
|
||||||
|
links : project_data.links || [],
|
||||||
|
created_at : now,
|
||||||
|
updated_at : now
|
||||||
|
};
|
||||||
|
data.projects.push(project);
|
||||||
|
return project;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update_project = (project_id, updated_fields) => {
|
||||||
|
const project = data.projects.find(p => p.id === project_id);
|
||||||
|
if(!project) return null;
|
||||||
|
|
||||||
|
project.user_ids = updated_fields.user_ids !== undefined ? updated_fields.user_ids : (project.user_ids || (project.user_id ? [project.user_id] : []));
|
||||||
|
project.name = updated_fields.name !== undefined ? updated_fields.name : project.name;
|
||||||
|
project.description = updated_fields.description !== undefined ? updated_fields.description : project.description;
|
||||||
|
project.avatar = updated_fields.avatar !== undefined ? updated_fields.avatar : project.avatar;
|
||||||
|
project.tags = updated_fields.tags !== undefined ? updated_fields.tags : project.tags;
|
||||||
|
project.technologies = updated_fields.technologies !== undefined ? updated_fields.technologies : project.technologies;
|
||||||
|
project.links = updated_fields.links !== undefined ? updated_fields.links : project.links;
|
||||||
|
project.updated_at = new Date().toISOString();
|
||||||
|
|
||||||
|
return project;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.delete_project = (project_id) => {
|
||||||
|
const initial_length = data.projects.length;
|
||||||
|
data.projects = data.projects.filter(p => p.id !== project_id);
|
||||||
|
|
||||||
|
if(data.projects.length !== initial_length){
|
||||||
|
// Limpieza de las puntuaciones asociadas al proyecto eliminado
|
||||||
|
if(data.votes[project_id]){
|
||||||
|
delete data.votes[project_id];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return DatabaseManager;
|
||||||
|
})();
|
||||||
81
Public/ecma/Managers/I18NManager.ecma.js
Normal file
81
Public/ecma/Managers/I18NManager.ecma.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const I18NManager = (function(){
|
||||||
|
|
||||||
|
const I18NManager = function(judge_pole){
|
||||||
|
|
||||||
|
const self = this,
|
||||||
|
sentences = {};
|
||||||
|
let language = "espanol";
|
||||||
|
|
||||||
|
const constructor = () => {
|
||||||
|
[i18n, typeof custom_i18n != "undefined" ? custom_i18n : {}].forEach(block => {
|
||||||
|
self.add(block, true);
|
||||||
|
});
|
||||||
|
self.set_language(judge_pole.settings.get("language"), null, Object.keys(sentences)[0] || language);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.add = (new_sentences, overwrite = true) => {
|
||||||
|
for(const language in new_sentences){
|
||||||
|
if(!sentences[language])
|
||||||
|
sentences[language] = {};
|
||||||
|
for(const key in new_sentences[language])
|
||||||
|
if(overwrite || sentences[language][key] === undefined)
|
||||||
|
sentences[language][key] = new_sentences[language][key];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const get_sentence = (strings, _default) => {
|
||||||
|
|
||||||
|
const done = [];
|
||||||
|
|
||||||
|
if(typeof strings == "string")
|
||||||
|
strings = [strings];
|
||||||
|
|
||||||
|
for(const current of [language].concat(Object.keys(sentences)))
|
||||||
|
if(!done.includes(current)){
|
||||||
|
done.push(current);
|
||||||
|
for(const key of strings)
|
||||||
|
if(sentences[current][key] !== undefined)
|
||||||
|
return sentences[current][key];
|
||||||
|
};
|
||||||
|
if(_default !== null)
|
||||||
|
return _default;
|
||||||
|
for(const text of strings)
|
||||||
|
return text;
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
this.get = (strings, inputs = null, _default = null) => Common.string_variables(get_sentence(strings, _default), inputs);
|
||||||
|
|
||||||
|
this.update = () => {
|
||||||
|
document.querySelectorAll("[data-i18n]").forEach(element => {
|
||||||
|
|
||||||
|
const text = self.get(element.getAttribute("data-i18n"));
|
||||||
|
|
||||||
|
if(element.getAttribute("data-i18n-without") != "true")
|
||||||
|
element.textContent = text;
|
||||||
|
["title", "alt", "placeholder"].forEach(key => {
|
||||||
|
element.hasAttribute(key) && element.setAttribute(key, text);
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
this.set_language = new_language => {
|
||||||
|
if(language != new_language && sentences[new_language]){
|
||||||
|
language = new_language;
|
||||||
|
self.update();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
this.get_languages = () => Object.keys(sentences);
|
||||||
|
|
||||||
|
this.get_current_language = () => language;
|
||||||
|
|
||||||
|
constructor();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return I18NManager;
|
||||||
|
})();
|
||||||
45
Public/ecma/Managers/SessionManager.ecma.js
Normal file
45
Public/ecma/Managers/SessionManager.ecma.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const SessionManager = (function(){
|
||||||
|
|
||||||
|
const SessionManager = function(judge_pole){
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
let current_user = null;
|
||||||
|
|
||||||
|
// Comprueba si existe al menos un usuario con contraseña registrada
|
||||||
|
this.is_auth_required = () => {
|
||||||
|
const users = judge_pole.db.get_data().users || [];
|
||||||
|
return users.some(u => u.password && u.password.trim().length > 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Si no hay usuarios con contraseña, cualquiera es admin por defecto
|
||||||
|
this.is_admin = () => {
|
||||||
|
if(!self.is_auth_required()) return true;
|
||||||
|
return current_user !== null;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.get_user = () => current_user;
|
||||||
|
|
||||||
|
this.login = (nick, password) => {
|
||||||
|
const users = judge_pole.db.get_data().users || [];
|
||||||
|
const user = users.find(u =>
|
||||||
|
u.nick.trim().toLowerCase() === nick.trim().toLowerCase() &&
|
||||||
|
u.password === password
|
||||||
|
);
|
||||||
|
|
||||||
|
if(user){
|
||||||
|
current_user = user;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logout = () => {
|
||||||
|
current_user = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return SessionManager;
|
||||||
|
})();
|
||||||
27
Public/ecma/Managers/SettingsManager.ecma.js
Normal file
27
Public/ecma/Managers/SettingsManager.ecma.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const SettingsManager = (function(){
|
||||||
|
|
||||||
|
const SettingsManager = function(judge_pole){
|
||||||
|
|
||||||
|
const self = this,
|
||||||
|
settings = {};
|
||||||
|
|
||||||
|
const constructor = () => {
|
||||||
|
self.add(settings, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.add = (block, overwrite = true) => {
|
||||||
|
for(const key in block)
|
||||||
|
if(overwrite || settings[key] === undefined)
|
||||||
|
settings[key] = block[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
this.get = (keys, inputs = null, _default = null) => Common.get_value(keys, [inputs, settings], _default);
|
||||||
|
|
||||||
|
constructor();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return SettingsManager;
|
||||||
|
})();
|
||||||
127
Public/ecma/Utils/Common.ecma.js
Normal file
127
Public/ecma/Utils/Common.ecma.js
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const Common = (function(){
|
||||||
|
|
||||||
|
const Common = function(){};
|
||||||
|
|
||||||
|
Common.get_keys = (...items) => items.reduce((keys, item) => {
|
||||||
|
|
||||||
|
if(item){
|
||||||
|
if(typeof item == "string" && /^[a-z_][a-z0-9_]*$/i.test(item) && !keys.includes(item))
|
||||||
|
keys.push(item);
|
||||||
|
else if(item instanceof Array)
|
||||||
|
for(const key of Common.get_keys(...item))
|
||||||
|
keys.includes(item) || keys.push(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
Common.get_dictionaries = (...items) => items.reduce((dictionaries, item) => {
|
||||||
|
|
||||||
|
if(item){
|
||||||
|
if(item.constructor == Object)
|
||||||
|
dictionaries.push(item);
|
||||||
|
else if(item instanceof Array)
|
||||||
|
dictionaries.push(...Common.get_dictionaries(...item));
|
||||||
|
};
|
||||||
|
|
||||||
|
return dictionaries;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
Common.get_value = (keys, inputs, _default = null) => {
|
||||||
|
if((keys = Common.get_keys(keys)).length)
|
||||||
|
for(const subinputs of Common.get_dictionaries(inputs))
|
||||||
|
for(const key of keys)
|
||||||
|
if(subinputs[key] !== undefined)
|
||||||
|
return subinputs[key];
|
||||||
|
return _default;
|
||||||
|
};
|
||||||
|
|
||||||
|
Common.get_dictionary = (items, overwrite = false) => {
|
||||||
|
|
||||||
|
if(items){
|
||||||
|
if(items.constructor == Object)
|
||||||
|
return items;
|
||||||
|
else if(items instanceof Array)
|
||||||
|
return items.reduce((dictionary, item) => {
|
||||||
|
Object.entries(Common.get_dictionary(item)).forEach(([key, value]) => {
|
||||||
|
if(overwrite || dictionary[key] === undefined)
|
||||||
|
dictionary[key] = value;
|
||||||
|
});
|
||||||
|
return dictionary;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
Common.string_variables = (string, variables, _default = null) => {
|
||||||
|
|
||||||
|
variables = Common.get_dictionary(variables);
|
||||||
|
|
||||||
|
return ("" + string).replace(/\{([^\{\}]+)\}/g, (all, key) => (
|
||||||
|
variables[key] !== undefined ? variables[key] :
|
||||||
|
_default !== null ? _default :
|
||||||
|
all));
|
||||||
|
};
|
||||||
|
|
||||||
|
Common.execute_array = (array, each_callback, end_callback, i = 0) => {
|
||||||
|
if(i == array.length)
|
||||||
|
end_callback();
|
||||||
|
else
|
||||||
|
each_callback(array[i], () => {
|
||||||
|
Common.execute_array(array, each_callback, end_callback, i + 1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Common.SCSS = (data, selector = "") => {
|
||||||
|
|
||||||
|
/** @type {Arrya.<string>} */
|
||||||
|
const childs = [];
|
||||||
|
/** @type {string} */
|
||||||
|
let css = ``;
|
||||||
|
|
||||||
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
|
if(value && value.constructor == Object)
|
||||||
|
key.split(",").forEach(key => {
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
|
const subselector = `${(selector ? selector + (
|
||||||
|
key[0] == "&" ? "" : " "
|
||||||
|
) : "") + (
|
||||||
|
key[0] == "&" ? key.substring(1) : key
|
||||||
|
)}`,
|
||||||
|
/** @type {Array<string>} */
|
||||||
|
subcss = Common.SCSS(value, subselector);
|
||||||
|
|
||||||
|
childs.push(`\n\n${subselector}{${subcss[0]}\n}`, ...subcss.slice(1));
|
||||||
|
|
||||||
|
});
|
||||||
|
else
|
||||||
|
css += `\n ${key.replace(/[^a-z0-9]+/gi, "-")} : ${value === null ? "none" : value};`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!selector){
|
||||||
|
|
||||||
|
/** @type {HTMLStyleElement} */
|
||||||
|
let style = document.querySelector("head>style");
|
||||||
|
|
||||||
|
if(!style){
|
||||||
|
style = document.querySelector("head").appendChild(document.createElement("style"));
|
||||||
|
[
|
||||||
|
["data-type", "text/css;charset=utf-8"],
|
||||||
|
["data-language", "CSS3"],
|
||||||
|
["charset", "utf-8"]
|
||||||
|
].forEach(([key, value]) => style.setAttribute(key, value === null ? "none" : value));
|
||||||
|
};
|
||||||
|
|
||||||
|
style.appendChild(document.createTextNode(css + childs.join("")));
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return [css, ...childs];
|
||||||
|
};
|
||||||
|
|
||||||
|
return Common;
|
||||||
|
})();
|
||||||
187
Public/ecma/Utils/I18N.ecma.js
Normal file
187
Public/ecma/Utils/I18N.ecma.js
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/** @type {Dictionary.<string, Dictionary.<string, string>>} */
|
||||||
|
const i18n = {
|
||||||
|
espanol : {
|
||||||
|
|
||||||
|
"judge_pole" : "JudgePole",
|
||||||
|
"lang_espanol" : "Español 🇪🇸",
|
||||||
|
"lang_english" : "English 🇬🇧",
|
||||||
|
"nav_users" : "Usuarios",
|
||||||
|
"nav_projects" : "Proyectos",
|
||||||
|
"nav_votes" : "Votaciones",
|
||||||
|
"nav_export" : "Base64 DB",
|
||||||
|
|
||||||
|
"users_title" : "Gestión de Jueces y Creadores",
|
||||||
|
"user_nick" : "Nick:",
|
||||||
|
"user_desc" : "Descripción / Bio:",
|
||||||
|
"user_avatar" : "Avatar (Ruta local / URL):",
|
||||||
|
"user_links" : "Enlaces (Nombre y URL):",
|
||||||
|
"btn_add_link" : "+ Enlace",
|
||||||
|
"btn_save_user" : "Guardar Usuario",
|
||||||
|
"no_users" : "No hay participantes registrados todavía.",
|
||||||
|
"btn_edit" : "Editar",
|
||||||
|
"btn_delete" : "Eliminar",
|
||||||
|
"btn_update_user" : "Actualizar Usuario",
|
||||||
|
"btn_cancel_edit" : "Cancelar Edición",
|
||||||
|
"user_editing_mode" : "Editando a: {nick}",
|
||||||
|
"confirm_delete_user" : "¿Estás seguro de eliminar a '{nick}'? Se eliminarán también sus proyectos y votos asociados.",
|
||||||
|
"btn_new_user" : "+ Añadir Usuario",
|
||||||
|
"modal_new_user_title" : "Nuevo Participante",
|
||||||
|
"modal_edit_user_title" : "Editar Participante: {nick}",
|
||||||
|
"btn_close_modal" : "Cerrar",
|
||||||
|
"search_users_placeholder" : "Buscar usuarios por nick o bio...",
|
||||||
|
"no_users_found" : "No se encontraron participantes que coincidan con la búsqueda.",
|
||||||
|
|
||||||
|
"projects_title" : "Proyectos de la Jam",
|
||||||
|
"project_author" : "Autor del proyecto:",
|
||||||
|
"project_name" : "Nombre del Proyecto:",
|
||||||
|
"project_desc" : "Descripción:",
|
||||||
|
"project_avatar" : "Imagen / Logo del proyecto:",
|
||||||
|
"project_tags" : "Tags (separados por coma):",
|
||||||
|
"project_tech" : "Tecnologías (separadas por coma):",
|
||||||
|
"project_links" : "Enlaces del proyecto:",
|
||||||
|
"btn_save_project" : "Guardar Proyecto",
|
||||||
|
"no_projects" : "No hay proyectos registrados.",
|
||||||
|
"select_author" : "-- Selecciona un autor --",
|
||||||
|
"btn_update_project" : "Actualizar Proyecto",
|
||||||
|
"project_editing_mode" : "Editando proyecto: {name}",
|
||||||
|
"confirm_delete_project" : "¿Estás seguro de eliminar el proyecto '{name}'? Se perderán también sus calificaciones registradas.",
|
||||||
|
"btn_new_project" : "+ Añadir Proyecto",
|
||||||
|
"modal_new_project_title" : "Nuevo Proyecto Jam",
|
||||||
|
"modal_edit_project_title" : "Editar Proyecto: {name}",
|
||||||
|
"search_projects_placeholder" : "Buscar proyectos por nombre, autor, tags o tech...",
|
||||||
|
"no_projects_found" : "No se encontraron proyectos que coincidan con la búsqueda.",
|
||||||
|
"search_authors_placeholder" : "Filtrar participantes...",
|
||||||
|
|
||||||
|
"user_type" : "Tipo de participante:",
|
||||||
|
"user_type_human" : "Humano 👤",
|
||||||
|
"user_type_ai" : "Inteligencia Artificial 🤖",
|
||||||
|
"project_authors" : "Autores del proyecto:",
|
||||||
|
"no_authors_selected" : "Debes seleccionar al menos un autor para el proyecto.",
|
||||||
|
|
||||||
|
"votes_title" : "Tribunal de Evaluación",
|
||||||
|
"select_project" : "Proyecto a Juzgar:",
|
||||||
|
"select_project_option" : "-- Seleccionar proyecto --",
|
||||||
|
"participating_judges" : "Jueces que votarán (incluido el autor):",
|
||||||
|
"evaluation_criteria" : "Criterios de Evaluación:",
|
||||||
|
"btn_new_criterion" : "+ Crear Criterio Personalizado",
|
||||||
|
"new_criterion_prompt" : "Nombre del nuevo criterio:",
|
||||||
|
"btn_save_scores" : "Registrar Veredicto",
|
||||||
|
"score_matrix_title" : "Papeleta de Calificaciones (0 a 10):",
|
||||||
|
"final_ranking" : "Ranking y Puntuaciones Finales",
|
||||||
|
"score_total" : "Media General:",
|
||||||
|
"created_at" : "Alta:",
|
||||||
|
"updated_at" : "Modif:",
|
||||||
|
|
||||||
|
"db_modal_title" : "Base de Datos en Base64 (database.ecma.js)",
|
||||||
|
"db_copy_btn" : "Copiar al Portapapeles",
|
||||||
|
"db_import_btn" : "Cargar Base64",
|
||||||
|
"db_copied" : "¡Copiado con éxito!",
|
||||||
|
"db_imported" : "¡Datos cargados con éxito!",
|
||||||
|
|
||||||
|
"btn_new_vote" : "+ Registrar Votación",
|
||||||
|
"search_rankings_placeholder" : "Buscar proyecto en el ranking...",
|
||||||
|
"rank_global" : "Global",
|
||||||
|
"rank_filtered" : "Filtro",
|
||||||
|
"rank_project" : "Proyecto",
|
||||||
|
"rank_average" : "Media General",
|
||||||
|
"rank_actions" : "Acciones",
|
||||||
|
"rank_criteria_breakdown" : "Desglose de medias por criterio:",
|
||||||
|
"search_criteria_placeholder" : "Filtrar criterios...",
|
||||||
|
"modal_new_vote_title" : "Tribunal: Evaluar Proyecto",
|
||||||
|
"modal_edit_vote_title" : "Tribunal: Modificar Notas de {project}",
|
||||||
|
"user_average_row" : "Media Juez",
|
||||||
|
"criterion_average_col" : "Media Criterio",
|
||||||
|
"confirm_delete_vote" : "¿Eliminar todas las calificaciones del proyecto '{project}'?",
|
||||||
|
"no_votes_registered" : "Aún no se ha calificado ningún proyecto.",
|
||||||
|
"edit_criterion_prompt" : "Nuevo nombre para el criterio:",
|
||||||
|
"confirm_delete_criterion" : "¿Seguro que quieres eliminar el criterio '{criterion}'? Se borrará de todas las calificaciones existentes.",
|
||||||
|
"weight_label" : "Peso:",
|
||||||
|
"crit_avg_natural" : "Media Nat.",
|
||||||
|
"crit_avg_weighted" : "Media Pond.",
|
||||||
|
"judge_avg_natural" : "Media Juez (Nat.)",
|
||||||
|
"judge_avg_weighted" : "Media Juez (Pond.)",
|
||||||
|
"global_natural_label" : "Nat:",
|
||||||
|
"global_weighted_label" : "Pond:",
|
||||||
|
|
||||||
|
"nav_login" : "Acceder 🔐",
|
||||||
|
"nav_logout" : "Salir 🚪",
|
||||||
|
"login_modal_title" : "Acceso Administrativo",
|
||||||
|
"login_nick" : "Nick de Usuario:",
|
||||||
|
"login_pass" : "Contraseña:",
|
||||||
|
"login_btn" : "Iniciar Sesión",
|
||||||
|
"login_error" : "Credenciales incorrectas o usuario sin permisos.",
|
||||||
|
"user_pass_label" : "Contraseña (dejar vacío si no requiere login):",
|
||||||
|
"user_pass_placeholder" : "Opcional: define clave admin",
|
||||||
|
"rank_view_full_matrix" : "Ver Matriz Completa",
|
||||||
|
"rank_hide_full_matrix" : "Ocultar Matriz Completa"
|
||||||
|
|
||||||
|
},
|
||||||
|
english : {
|
||||||
|
"judge_pole" : "JudgePole",
|
||||||
|
"lang_espanol" : "Español 🇪🇸",
|
||||||
|
"lang_english" : "English 🇬🇧",
|
||||||
|
"nav_users" : "Judges",
|
||||||
|
"nav_projects" : "Projects",
|
||||||
|
"nav_votes" : "Voting",
|
||||||
|
"nav_export" : "Base64 DB",
|
||||||
|
|
||||||
|
"users_title" : "Judges & Creators Management",
|
||||||
|
"user_nick" : "Nick:",
|
||||||
|
"user_desc" : "Description / Bio:",
|
||||||
|
"user_avatar" : "Avatar (Local path / URL):",
|
||||||
|
"user_links" : "Links (Name and URL):",
|
||||||
|
"user_type" : "Participant Type:",
|
||||||
|
"user_type_human" : "Human 👤",
|
||||||
|
"user_type_ai" : "Artificial Intelligence 🤖",
|
||||||
|
"btn_add_link" : "+ Link",
|
||||||
|
"btn_save_user" : "Save Judge",
|
||||||
|
"btn_new_user" : "+ Add Judge",
|
||||||
|
"btn_edit" : "Edit",
|
||||||
|
"btn_delete" : "Delete",
|
||||||
|
"btn_update_user" : "Update Judge",
|
||||||
|
"btn_cancel_edit" : "Cancel",
|
||||||
|
"no_users" : "No registered participants yet.",
|
||||||
|
"search_users_placeholder" : "Search judges by nick or bio...",
|
||||||
|
"no_users_found" : "No participants match your search.",
|
||||||
|
"confirm_delete_user" : "Delete '{nick}'? Their projects and votes will be removed.",
|
||||||
|
|
||||||
|
"projects_title" : "Jam Projects",
|
||||||
|
"project_authors" : "Project Authors:",
|
||||||
|
"project_name" : "Project Name:",
|
||||||
|
"project_desc" : "Description:",
|
||||||
|
"project_avatar" : "Project Logo / Image:",
|
||||||
|
"project_tags" : "Tags (comma separated):",
|
||||||
|
"project_tech" : "Technologies (comma separated):",
|
||||||
|
"project_links" : "Project Links:",
|
||||||
|
"btn_save_project" : "Save Project",
|
||||||
|
"btn_new_project" : "+ Add Project",
|
||||||
|
"btn_update_project" : "Update Project",
|
||||||
|
"no_projects" : "No registered projects.",
|
||||||
|
"search_projects_placeholder" : "Search projects...",
|
||||||
|
"no_projects_found" : "No projects found.",
|
||||||
|
"confirm_delete_project" : "Delete project '{name}'?",
|
||||||
|
"search_authors_placeholder" : "Filter authors...",
|
||||||
|
"no_authors_selected" : "Please select at least one author.",
|
||||||
|
|
||||||
|
"votes_title" : "Evaluation Tribunal",
|
||||||
|
"select_project" : "Project to Judge:",
|
||||||
|
"select_project_option" : "-- Select project --",
|
||||||
|
"participating_judges" : "Voting Judges:",
|
||||||
|
"evaluation_criteria" : "Evaluation Criteria:",
|
||||||
|
"btn_new_criterion" : "+ New Criterion",
|
||||||
|
"new_criterion_prompt" : "Criterion name:",
|
||||||
|
"btn_save_scores" : "Save Verdict",
|
||||||
|
"score_matrix_title" : "Scorecard (0 to 10):",
|
||||||
|
"final_ranking" : "Final Standings",
|
||||||
|
"created_at" : "Created:",
|
||||||
|
"updated_at" : "Updated:",
|
||||||
|
|
||||||
|
"db_modal_title" : "Base64 Database",
|
||||||
|
"db_copy_btn" : "Copy to Clipboard",
|
||||||
|
"db_import_btn" : "Load Base64",
|
||||||
|
"db_copied" : "Copied successfully!",
|
||||||
|
"db_imported" : "Data loaded successfully!"
|
||||||
|
}
|
||||||
|
};
|
||||||
20
Public/ecma/Utils/Settings.ecma.js
Normal file
20
Public/ecma/Utils/Settings.ecma.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/** @type {Dictionary.<string, any|null>} */
|
||||||
|
const settings = {
|
||||||
|
cells : 40,
|
||||||
|
position : "body",
|
||||||
|
class_name : "judge-pole",
|
||||||
|
frames_per_second : 12,
|
||||||
|
language : "espanol",
|
||||||
|
|
||||||
|
db_storage_key : "judge_pole_db_data",
|
||||||
|
default_criteria : [
|
||||||
|
"Vibe & Originalidad",
|
||||||
|
"Calidad Técnica / No Explota",
|
||||||
|
"Factor Troll / Meme",
|
||||||
|
"Diseño / Estética"
|
||||||
|
],
|
||||||
|
|
||||||
|
...(typeof custom_settings == "undefined" ? {} : custom_settings)
|
||||||
|
};
|
||||||
567
Public/ecma/Utils/Styles.ecma.js
Normal file
567
Public/ecma/Utils/Styles.ecma.js
Normal file
@ -0,0 +1,567 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/** @type {Dictionary.<string, any|null>} */
|
||||||
|
const SCSS = {
|
||||||
|
...{
|
||||||
|
"body,html" : {
|
||||||
|
height : "100%",
|
||||||
|
margin : "0em",
|
||||||
|
background : "#0f111a",
|
||||||
|
color : "#e6edf3",
|
||||||
|
font_family : "system-ui, -apple-system, sans-serif"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...{
|
||||||
|
".judge-pole" : {
|
||||||
|
position : "relative",
|
||||||
|
top : "0em",
|
||||||
|
left : "0em",
|
||||||
|
width : "100%",
|
||||||
|
height : "100%",
|
||||||
|
box_sizing : "border-box",
|
||||||
|
display : "flex",
|
||||||
|
flex_direction : "column",
|
||||||
|
overflow : "hidden",
|
||||||
|
|
||||||
|
".jp-header" : {
|
||||||
|
display : "flex",
|
||||||
|
align_items : "center",
|
||||||
|
justify_content : "space-between",
|
||||||
|
padding : "0.5em 1em",
|
||||||
|
background : "#161b22",
|
||||||
|
border_bottom : "0.05em solid #30363d",
|
||||||
|
|
||||||
|
".jp-title" : {
|
||||||
|
margin : "0em",
|
||||||
|
font_size : "1.2em",
|
||||||
|
color : "#58a6ff"
|
||||||
|
},
|
||||||
|
".jp-nav" : {
|
||||||
|
display : "flex",
|
||||||
|
gap : "0.5em"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-content-area" : {
|
||||||
|
flex : "1",
|
||||||
|
overflow_y : "auto",
|
||||||
|
padding : "1em",
|
||||||
|
box_sizing : "border-box"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-btn, button" : {
|
||||||
|
background : "#21262d",
|
||||||
|
color : "#c9d1d9",
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
padding : "0.4em 0.8em",
|
||||||
|
font_size : "0.8em",
|
||||||
|
cursor : "pointer",
|
||||||
|
border_radius : "0.2em",
|
||||||
|
"&:hover" : {
|
||||||
|
background : "#30363d"
|
||||||
|
},
|
||||||
|
"&.is-active" : {
|
||||||
|
background : "#1f6feb",
|
||||||
|
color : "#ffffff",
|
||||||
|
border_color : "#388bfd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-btn-primary" : {
|
||||||
|
background : "#238636",
|
||||||
|
color : "#ffffff",
|
||||||
|
border_color : "#2ea043",
|
||||||
|
"&:hover" : { background : "#2ea043" }
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-btn-danger" : {
|
||||||
|
background : "#da3633",
|
||||||
|
color : "#ffffff",
|
||||||
|
border_color : "#f85149"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-form-card, .jp-panel-controls, .jp-matrix-card, .jp-ranking-box" : {
|
||||||
|
background : "#161b22",
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
padding : "1em",
|
||||||
|
margin_bottom : "1em",
|
||||||
|
border_radius : "0.3em"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-field" : {
|
||||||
|
margin_bottom : "0.8em",
|
||||||
|
display : "flex",
|
||||||
|
flex_direction : "column",
|
||||||
|
gap : "0.3em",
|
||||||
|
|
||||||
|
"label" : {
|
||||||
|
font_size : "0.8em",
|
||||||
|
color : "#8b949e",
|
||||||
|
font_weight : "bold"
|
||||||
|
},
|
||||||
|
"input, textarea, select" : {
|
||||||
|
background : "#0d1117",
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
color : "#e6edf3",
|
||||||
|
padding : "0.4em",
|
||||||
|
font_size : "0.8em",
|
||||||
|
border_radius : "0.2em"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-cards-grid" : {
|
||||||
|
display : "flex",
|
||||||
|
flex_wrap : "wrap",
|
||||||
|
justify_content : "center", // Centra la lista de tarjetas en la pantalla
|
||||||
|
gap : "1em",
|
||||||
|
width : "100%",
|
||||||
|
box_sizing : "border-box"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-card" : {
|
||||||
|
flex : "1 1 12em",
|
||||||
|
max_width : "16em", // Evita que una sola tarjeta se vuelva gigantesca a lo ancho
|
||||||
|
position : "relative",
|
||||||
|
background : "#161b22",
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
padding : "0.8em",
|
||||||
|
border_radius : "0.3em",
|
||||||
|
box_sizing : "border-box",
|
||||||
|
|
||||||
|
".jp-avatar, .jp-avatar-placeholder" : {
|
||||||
|
width : "100%",
|
||||||
|
height : "5em",
|
||||||
|
border_radius : "0.2em",
|
||||||
|
margin_bottom : "0.5em",
|
||||||
|
box_sizing : "border-box"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-avatar" : {
|
||||||
|
object_fit : "cover"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-avatar-placeholder" : {
|
||||||
|
display : "flex",
|
||||||
|
align_items : "center",
|
||||||
|
justify_content : "center",
|
||||||
|
background : "#0d1117",
|
||||||
|
border : "0.05em dashed #30363d",
|
||||||
|
color : "#484f58",
|
||||||
|
font_size : "1.2em",
|
||||||
|
font_weight : "bold",
|
||||||
|
letter_spacing : "0.05em",
|
||||||
|
user_select : "none"
|
||||||
|
},
|
||||||
|
"h3" : { margin : "0em 0em 0.3em 0em", font_size : "1em", padding_right : "1.5em" },
|
||||||
|
".jp-desc" : { font_size : "0.75em", color : "#8b949e" },
|
||||||
|
|
||||||
|
// Botón Eliminar: Esquina superior derecha
|
||||||
|
".jp-card-btn-delete" : {
|
||||||
|
position : "absolute",
|
||||||
|
top : "0.4em",
|
||||||
|
right : "0.4em",
|
||||||
|
width : "1.6em",
|
||||||
|
height : "1.6em",
|
||||||
|
border_radius : "50%",
|
||||||
|
background : "#da3633",
|
||||||
|
color : "#ffffff",
|
||||||
|
border : "0.05em solid #f85149",
|
||||||
|
display : "flex",
|
||||||
|
align_items : "center",
|
||||||
|
justify_content : "center",
|
||||||
|
font_size : "0.75em",
|
||||||
|
padding : "0em",
|
||||||
|
cursor : "pointer",
|
||||||
|
box_shadow : "0 0.1em 0.3em rgba(0,0,0,0.5)",
|
||||||
|
"&:hover" : {
|
||||||
|
background : "#f85149",
|
||||||
|
transform : "scale(1.1)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Botón Editar: Esquina inferior derecha
|
||||||
|
".jp-card-btn-edit" : {
|
||||||
|
position : "absolute",
|
||||||
|
bottom : "0.4em",
|
||||||
|
right : "0.4em",
|
||||||
|
width : "1.6em",
|
||||||
|
height : "1.6em",
|
||||||
|
border_radius : "50%",
|
||||||
|
background : "#21262d",
|
||||||
|
color : "#58a6ff",
|
||||||
|
border : "0.05em solid #388bfd",
|
||||||
|
display : "flex",
|
||||||
|
align_items : "center",
|
||||||
|
justify_content : "center",
|
||||||
|
font_size : "0.75em",
|
||||||
|
padding : "0em",
|
||||||
|
cursor : "pointer",
|
||||||
|
box_shadow : "0 0.1em 0.3em rgba(0,0,0,0.5)",
|
||||||
|
"&:hover" : {
|
||||||
|
background : "#1f6feb",
|
||||||
|
color : "#ffffff",
|
||||||
|
transform : "scale(1.1)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-tags-group" : {
|
||||||
|
display : "flex",
|
||||||
|
flex_wrap : "wrap",
|
||||||
|
gap : "0.3em",
|
||||||
|
margin : "0.4em 0em"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-badge" : {
|
||||||
|
font_size : "0.65em",
|
||||||
|
padding : "0.1em 0.4em",
|
||||||
|
border_radius : "0.2em",
|
||||||
|
"&.jp-badge-tag" : { background : "#388bfd33", color : "#58a6ff" },
|
||||||
|
"&.jp-badge-tech" : { background : "#a371f733", color : "#bc8cff" }
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-tag-link" : {
|
||||||
|
color : "#58a6ff",
|
||||||
|
text_decoration : "none",
|
||||||
|
font_size : "0.75em",
|
||||||
|
border_bottom : "0.05em dotted #58a6ff"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-table" : {
|
||||||
|
width : "100%",
|
||||||
|
border_collapse : "collapse",
|
||||||
|
margin : "0.8em 0em",
|
||||||
|
"th, td" : {
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
padding : "0.4em",
|
||||||
|
text_align : "center",
|
||||||
|
font_size : "0.8em"
|
||||||
|
},
|
||||||
|
"th" : { background : "#21262d" },
|
||||||
|
".jp-score-input" : {
|
||||||
|
width : "3.5em",
|
||||||
|
text_align : "center"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-rank-row" : {
|
||||||
|
display : "flex",
|
||||||
|
justify_content : "space-between",
|
||||||
|
padding : "0.4em",
|
||||||
|
border_bottom : "0.05em solid #30363d",
|
||||||
|
font_size : "0.85em",
|
||||||
|
".jp-rank-avg" : { color : "#3fb950", font_weight : "bold" }
|
||||||
|
},
|
||||||
|
".jp-card-actions" : {
|
||||||
|
display : "flex",
|
||||||
|
gap : "0.5em",
|
||||||
|
margin_top : "0.8em"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-meta" : {
|
||||||
|
display : "flex",
|
||||||
|
flex_direction : "column",
|
||||||
|
gap : "0.2em",
|
||||||
|
margin_top : "0.5em",
|
||||||
|
color : "#8b949e",
|
||||||
|
font_size : "0.7em"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-notice-badge" : {
|
||||||
|
display : "flex",
|
||||||
|
justify_content : "space-between",
|
||||||
|
align_items : "center",
|
||||||
|
background : "#1f6feb22",
|
||||||
|
border : "0.05em solid #388bfd",
|
||||||
|
padding : "0.4em 0.8em",
|
||||||
|
margin_bottom : "0.8em",
|
||||||
|
border_radius : "0.2em",
|
||||||
|
font_size : "0.8em",
|
||||||
|
color : "#58a6ff"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-author-tag" : {
|
||||||
|
margin : "0em 0em 0.4em 0em",
|
||||||
|
font_size : "0.75em",
|
||||||
|
color : "#58a6ff",
|
||||||
|
font_weight : "normal"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-modal-overlay" : {
|
||||||
|
position : "fixed",
|
||||||
|
top : "0em",
|
||||||
|
left : "0em",
|
||||||
|
width : "100%",
|
||||||
|
height : "100%",
|
||||||
|
background : "rgba(0, 0, 0, 0.75)",
|
||||||
|
display : "flex",
|
||||||
|
align_items : "center",
|
||||||
|
justify_content : "center",
|
||||||
|
z_index : "1000",
|
||||||
|
padding : "1em",
|
||||||
|
box_sizing : "border-box",
|
||||||
|
|
||||||
|
".jp-modal-window" : {
|
||||||
|
background : "#161b22",
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
border_radius : "0.4em",
|
||||||
|
width : "100%",
|
||||||
|
max_width : "28em",
|
||||||
|
max_height : "90%",
|
||||||
|
display : "flex",
|
||||||
|
flex_direction : "column",
|
||||||
|
box_shadow : "0 0.5em 2em rgba(0,0,0,0.8)",
|
||||||
|
overflow : "hidden",
|
||||||
|
|
||||||
|
".jp-modal-header" : {
|
||||||
|
display : "flex",
|
||||||
|
align_items : "center",
|
||||||
|
justify_content : "space-between",
|
||||||
|
padding : "0.8em 1em",
|
||||||
|
border_bottom : "0.05em solid #30363d",
|
||||||
|
background : "#0d1117",
|
||||||
|
"h3" : { margin : "0em", font_size : "1em", color : "#58a6ff" }
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-modal-body" : {
|
||||||
|
padding : "1em",
|
||||||
|
overflow_y : "auto",
|
||||||
|
flex : "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-view-topbar" : {
|
||||||
|
display : "flex",
|
||||||
|
justify_content : "space-between",
|
||||||
|
align_items : "center",
|
||||||
|
margin_bottom : "1em"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-search-input" : {
|
||||||
|
background : "#0d1117",
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
color : "#e6edf3",
|
||||||
|
padding : "0.4em 0.8em",
|
||||||
|
font_size : "0.8em",
|
||||||
|
border_radius : "0.2em",
|
||||||
|
width : "16em",
|
||||||
|
box_sizing : "border-box",
|
||||||
|
"&:focus" : {
|
||||||
|
border_color : "#58a6ff",
|
||||||
|
outline : "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-badge-type-human" : {
|
||||||
|
background : "#23863633",
|
||||||
|
color : "#3fb950",
|
||||||
|
border : "0.05em solid #2ea04366"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-badge-type-ai" : {
|
||||||
|
background : "#a371f733",
|
||||||
|
color : "#d2a8ff",
|
||||||
|
border : "0.05em solid #a371f766"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-authors-select-box" : {
|
||||||
|
display : "flex",
|
||||||
|
flex_wrap : "wrap",
|
||||||
|
gap : "0.5em",
|
||||||
|
background : "#0d1117",
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
padding : "0.5em",
|
||||||
|
border_radius : "0.2em",
|
||||||
|
max_height : "6em",
|
||||||
|
overflow_y : "auto"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-check-label" : {
|
||||||
|
display : "flex",
|
||||||
|
align_items : "center",
|
||||||
|
gap : "0.3em",
|
||||||
|
font_size : "0.75em",
|
||||||
|
color : "#c9d1d9",
|
||||||
|
cursor : "pointer",
|
||||||
|
user_select : "none",
|
||||||
|
"input" : {
|
||||||
|
cursor : "pointer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-authors-picker" : {
|
||||||
|
display : "flex",
|
||||||
|
flex_direction : "column",
|
||||||
|
background : "#0d1117",
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
border_radius : "0.2em",
|
||||||
|
overflow : "hidden",
|
||||||
|
|
||||||
|
".jp-authors-search-input" : {
|
||||||
|
background : "#161b22",
|
||||||
|
border : "none",
|
||||||
|
border_bottom : "0.05em solid #30363d",
|
||||||
|
color : "#e6edf3",
|
||||||
|
padding : "0.4em 0.6em",
|
||||||
|
font_size : "0.75em",
|
||||||
|
box_sizing : "border-box",
|
||||||
|
width : "100%",
|
||||||
|
outline : "none",
|
||||||
|
"&:focus" : {
|
||||||
|
background : "#1c2128"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-authors-select-box" : {
|
||||||
|
display : "flex",
|
||||||
|
flex_direction : "column",
|
||||||
|
gap : "0.4em",
|
||||||
|
padding : "0.5em",
|
||||||
|
max_height : "7em", // Altura máxima fija
|
||||||
|
overflow_y : "auto", // Scroll vertical si excede
|
||||||
|
box_sizing : "border-box"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-lang-select" : {
|
||||||
|
background : "#0d1117",
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
color : "#e6edf3",
|
||||||
|
padding : "0.3em 0.6em",
|
||||||
|
font_size : "0.75em",
|
||||||
|
border_radius : "0.2em",
|
||||||
|
cursor : "pointer",
|
||||||
|
outline : "none",
|
||||||
|
"&:hover, &:focus" : {
|
||||||
|
border_color : "#58a6ff"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-rank-table" : {
|
||||||
|
width : "100%",
|
||||||
|
border_collapse : "collapse",
|
||||||
|
background : "#161b22",
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
border_radius : "0.3em",
|
||||||
|
overflow : "hidden",
|
||||||
|
|
||||||
|
"th, td" : {
|
||||||
|
padding : "0.6em 0.8em",
|
||||||
|
border_bottom : "0.05em solid #30363d",
|
||||||
|
font_size : "0.8em"
|
||||||
|
},
|
||||||
|
"th" : {
|
||||||
|
background : "#0d1117",
|
||||||
|
color : "#8b949e",
|
||||||
|
text_align : "left"
|
||||||
|
},
|
||||||
|
"tbody tr" : {
|
||||||
|
cursor : "pointer",
|
||||||
|
"&:hover" : {
|
||||||
|
background : "#1c2128"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
".jp-rank-badge-global" : {
|
||||||
|
font_weight : "bold",
|
||||||
|
color : "#f1e05a"
|
||||||
|
},
|
||||||
|
".jp-rank-badge-filter" : {
|
||||||
|
color : "#58a6ff"
|
||||||
|
},
|
||||||
|
".jp-rank-avg-score" : {
|
||||||
|
color : "#3fb950",
|
||||||
|
font_weight : "bold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-breakdown-row" : {
|
||||||
|
background : "#0d1117 !important",
|
||||||
|
cursor : "default !important",
|
||||||
|
"td" : {
|
||||||
|
padding : "0.8em 1.2em !important"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-breakdown-grid" : {
|
||||||
|
display : "grid",
|
||||||
|
grid_template_columns : "repeat(auto-fill, minmax(10em, 1fr))",
|
||||||
|
gap : "0.5em",
|
||||||
|
margin_top : "0.4em"
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-breakdown-item" : {
|
||||||
|
background : "#161b22",
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
padding : "0.4em 0.6em",
|
||||||
|
border_radius : "0.2em",
|
||||||
|
display : "flex",
|
||||||
|
justify_content : "space-between",
|
||||||
|
font_size : "0.75em",
|
||||||
|
".jp-crit-name" : { color : "#c9d1d9" },
|
||||||
|
".jp-crit-val" : { color : "#58a6ff", font_weight : "bold" }
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-matrix-table" : {
|
||||||
|
width : "100%",
|
||||||
|
border_collapse : "collapse",
|
||||||
|
margin_top : "0.8em",
|
||||||
|
"th, td" : {
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
padding : "0.4em",
|
||||||
|
text_align : "center",
|
||||||
|
font_size : "0.75em"
|
||||||
|
},
|
||||||
|
"th" : { background : "#0d1117", color : "#c9d1d9" },
|
||||||
|
".jp-crit-cell" : { text_align : "left", font_weight : "bold", color : "#58a6ff" },
|
||||||
|
".jp-summary-cell" : { background : "#161b22", color : "#3fb950", font_weight : "bold" },
|
||||||
|
".jp-score-input" : {
|
||||||
|
width : "3.5em",
|
||||||
|
text_align : "center",
|
||||||
|
padding : "0.2em",
|
||||||
|
background : "#0d1117",
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
color : "#e6edf3",
|
||||||
|
border_radius : "0.2em"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-criterion-row" : {
|
||||||
|
display : "flex",
|
||||||
|
align_items : "center",
|
||||||
|
justify_content : "space-between",
|
||||||
|
padding : "0.2em 0em",
|
||||||
|
border_bottom : "0.05em solid #21262d",
|
||||||
|
|
||||||
|
".jp-crit-actions" : {
|
||||||
|
display : "flex",
|
||||||
|
gap : "0.3em"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-weight-input" : {
|
||||||
|
width : "3.2em",
|
||||||
|
padding : "0.15em 0.3em",
|
||||||
|
background : "#0d1117",
|
||||||
|
border : "0.05em solid #30363d",
|
||||||
|
color : "#f1e05a",
|
||||||
|
font_size : "0.75em",
|
||||||
|
text_align : "center",
|
||||||
|
border_radius : "0.2em",
|
||||||
|
margin_left : "0.4em",
|
||||||
|
"&:disabled" : {
|
||||||
|
opacity : "0.3",
|
||||||
|
cursor : "not-allowed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".jp-summary-subcell" : {
|
||||||
|
background : "#161b22",
|
||||||
|
font_weight : "bold",
|
||||||
|
font_size : "0.7em"
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...(typeof custom_styles == "undefined" ? {} : custom_styles)
|
||||||
|
};
|
||||||
373
Public/ecma/Views/ProjectsView.ecma.js
Normal file
373
Public/ecma/Views/ProjectsView.ecma.js
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const ProjectsView = (function(){
|
||||||
|
|
||||||
|
const ProjectsView = function(judge_pole){
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
let container = null;
|
||||||
|
let cards_wrapper = null;
|
||||||
|
let filter_query = "";
|
||||||
|
|
||||||
|
this.go = () => {
|
||||||
|
judge_pole.content_area.innerHTML = "";
|
||||||
|
judge_pole.html(judge_pole.content_area, build());
|
||||||
|
};
|
||||||
|
|
||||||
|
const build = () => {
|
||||||
|
const is_admin = judge_pole.session.is_admin();
|
||||||
|
|
||||||
|
const topbar_actions = [
|
||||||
|
["input", {
|
||||||
|
"type" : "search",
|
||||||
|
"class" : "jp-search-input",
|
||||||
|
"data-i18n" : "search_projects_placeholder",
|
||||||
|
"data-i18n-without" : "true",
|
||||||
|
"placeholder" : judge_pole.i18n.get("search_projects_placeholder"),
|
||||||
|
"value" : filter_query,
|
||||||
|
"oninput" : (input) => {
|
||||||
|
filter_query = input.value.trim().toLowerCase();
|
||||||
|
render_cards();
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
];
|
||||||
|
|
||||||
|
if(is_admin){
|
||||||
|
topbar_actions.push(["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-btn-primary",
|
||||||
|
"data-i18n" : "btn_new_project",
|
||||||
|
"onclick" : () => show_project_modal()
|
||||||
|
}, judge_pole.i18n.get("btn_new_project")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
container = judge_pole.html(["div", { "class" : "jp-view jp-projects-view" }, [
|
||||||
|
["div", { "class" : "jp-view-topbar" }, [
|
||||||
|
["h2", { "data-i18n" : "projects_title" }, judge_pole.i18n.get("projects_title")],
|
||||||
|
["div", { "style" : "display:flex; gap:0.5em; align-items:center;" }, topbar_actions]
|
||||||
|
]],
|
||||||
|
cards_wrapper = judge_pole.html(["div", { "class" : "jp-cards-grid", "id" : "jp-projects-cards" }])[0]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
render_cards();
|
||||||
|
|
||||||
|
return container;
|
||||||
|
};
|
||||||
|
|
||||||
|
const render_cards = () => {
|
||||||
|
if(!cards_wrapper) return;
|
||||||
|
cards_wrapper.innerHTML = "";
|
||||||
|
|
||||||
|
const db_data = judge_pole.db.get_data();
|
||||||
|
const projects = db_data.projects;
|
||||||
|
const users = db_data.users;
|
||||||
|
const is_admin = judge_pole.session.is_admin();
|
||||||
|
|
||||||
|
if(!projects.length){
|
||||||
|
cards_wrapper.appendChild(judge_pole.html(["p", { "class" : "jp-empty", "data-i18n" : "no_projects" }, judge_pole.i18n.get("no_projects")])[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered_projects = projects.filter(project => {
|
||||||
|
if(!filter_query) return true;
|
||||||
|
|
||||||
|
const project_user_ids = project.user_ids || (project.user_id ? [project.user_id] : []);
|
||||||
|
const authors = users.filter(u => project_user_ids.includes(u.id));
|
||||||
|
const authors_str = authors.map(a => a.nick.toLowerCase()).join(" ");
|
||||||
|
|
||||||
|
const match_name = project.name.toLowerCase().includes(filter_query);
|
||||||
|
const match_author = authors_str.includes(filter_query);
|
||||||
|
const match_desc = project.description ? project.description.toLowerCase().includes(filter_query) : false;
|
||||||
|
const match_tags = project.tags ? project.tags.some(t => t.toLowerCase().includes(filter_query)) : false;
|
||||||
|
const match_tech = project.technologies ? project.technologies.some(t => t.toLowerCase().includes(filter_query)) : false;
|
||||||
|
const match_links = project.links ? project.links.some(l => l.name.toLowerCase().includes(filter_query)) : false;
|
||||||
|
|
||||||
|
return match_name || match_author || match_desc || match_tags || match_tech || match_links;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!filtered_projects.length){
|
||||||
|
cards_wrapper.appendChild(judge_pole.html(["p", { "class" : "jp-empty", "data-i18n" : "no_projects_found" }, judge_pole.i18n.get("no_projects_found")])[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered_projects.forEach(project => {
|
||||||
|
const project_user_ids = project.user_ids || (project.user_id ? [project.user_id] : []);
|
||||||
|
const authors = users.filter(u => project_user_ids.includes(u.id));
|
||||||
|
const authors_str = authors.length
|
||||||
|
? authors.map(a => `${a.nick} ${a.type === 'ai' ? '🤖' : '👤'}`).join(", ")
|
||||||
|
: "Unknown";
|
||||||
|
|
||||||
|
const tags_html = project.tags.map(t => ["span", { "class" : "jp-badge jp-badge-tag" }, "#" + t]);
|
||||||
|
const tech_html = project.technologies.map(t => ["span", { "class" : "jp-badge jp-badge-tech" }, t]);
|
||||||
|
const links_html = project.links.map(link => ["a", { "href" : link.url, "target" : "_blank", "class" : "jp-tag-link" }, link.name]);
|
||||||
|
|
||||||
|
const avatar_element = project.avatar
|
||||||
|
? ["img", { "src" : project.avatar, "class" : "jp-avatar", "alt" : project.name }]
|
||||||
|
: ["div", { "class" : "jp-avatar-placeholder" }, (project.name ? project.name.substring(0, 2).toUpperCase() : "</>")];
|
||||||
|
|
||||||
|
const card_elements = [];
|
||||||
|
|
||||||
|
// Botones sólo si es Admin
|
||||||
|
if(is_admin){
|
||||||
|
card_elements.push(["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-card-btn-delete",
|
||||||
|
"data-i18n" : "btn_delete",
|
||||||
|
"data-i18n-without" : "true",
|
||||||
|
"title" : judge_pole.i18n.get("btn_delete"),
|
||||||
|
"onclick" : () => {
|
||||||
|
const confirm_msg = judge_pole.i18n.get("confirm_delete_project", { name : project.name });
|
||||||
|
if(confirm(confirm_msg)){
|
||||||
|
judge_pole.db.delete_project(project.id);
|
||||||
|
render_cards();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "✕"]);
|
||||||
|
|
||||||
|
card_elements.push(["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-card-btn-edit",
|
||||||
|
"data-i18n" : "btn_edit",
|
||||||
|
"data-i18n-without" : "true",
|
||||||
|
"title" : judge_pole.i18n.get("btn_edit"),
|
||||||
|
"onclick" : () => show_project_modal(project.id)
|
||||||
|
}, "✏️"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
card_elements.push(
|
||||||
|
avatar_element,
|
||||||
|
["h3", {}, project.name],
|
||||||
|
["h4", { "class" : "jp-author-tag" }, "By: " + authors_str],
|
||||||
|
["p", { "class" : "jp-desc" }, project.description],
|
||||||
|
["div", { "class" : "jp-tags-group" }, tags_html],
|
||||||
|
["div", { "class" : "jp-tags-group" }, tech_html],
|
||||||
|
["div", { "class" : "jp-tags-group" }, links_html],
|
||||||
|
["div", { "class" : "jp-meta" }, [
|
||||||
|
["small", {}, judge_pole.i18n.get("created_at") + " " + project.created_at.substring(0, 10)],
|
||||||
|
["small", {}, judge_pole.i18n.get("updated_at") + " " + project.updated_at.substring(0, 10)]
|
||||||
|
]]
|
||||||
|
);
|
||||||
|
|
||||||
|
const card = judge_pole.html(["div", { "class" : "jp-card" }, card_elements])[0];
|
||||||
|
cards_wrapper.appendChild(card);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const show_project_modal = (project_id = null) => {
|
||||||
|
const db_data = judge_pole.db.get_data();
|
||||||
|
const users = db_data.users;
|
||||||
|
const current_project = project_id
|
||||||
|
? db_data.projects.find(p => p.id === project_id)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let links_container = null;
|
||||||
|
|
||||||
|
const add_link_row = (name = "", url = "") => {
|
||||||
|
const row = judge_pole.html(["div", { "class" : "jp-link-row" }, [
|
||||||
|
["input", { "type" : "text", "placeholder" : "Nombre (Repo, Demo...)", "class" : "link-name", "value" : name }],
|
||||||
|
["input", { "type" : "url", "placeholder" : "https://...", "class" : "link-url", "value" : url }],
|
||||||
|
["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-btn-danger",
|
||||||
|
"onclick" : (btn) => btn.parentNode.remove()
|
||||||
|
}, "✕"]
|
||||||
|
]])[0];
|
||||||
|
links_container.appendChild(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close_modal = () => {
|
||||||
|
if(modal_overlay && modal_overlay.parentNode)
|
||||||
|
modal_overlay.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
const modal_title = current_project
|
||||||
|
? judge_pole.i18n.get("modal_edit_project_title", { name : current_project.name })
|
||||||
|
: judge_pole.i18n.get("modal_new_project_title");
|
||||||
|
|
||||||
|
// 1. Overlay base
|
||||||
|
const modal_overlay = judge_pole.html(["div", {
|
||||||
|
"class" : "jp-modal-overlay",
|
||||||
|
"onclick" : (overlay, e) => {
|
||||||
|
if(e.target === overlay) close_modal();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
["div", { "class" : "jp-modal-window" }, [
|
||||||
|
["div", { "class" : "jp-modal-header" }, [
|
||||||
|
["h3", {}, modal_title],
|
||||||
|
["button", { "type" : "button", "class" : "jp-btn-danger", "onclick" : () => close_modal() }, "✕"]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-modal-body" }]
|
||||||
|
]]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
const modal_body = modal_overlay.querySelector(".jp-modal-body");
|
||||||
|
|
||||||
|
// 2. Construcción de la caja de autores (Picker)
|
||||||
|
const current_author_ids = current_project
|
||||||
|
? (current_project.user_ids || (current_project.user_id ? [current_project.user_id] : []))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const authors_picker = judge_pole.html(["div", { "class" : "jp-authors-picker" }, [
|
||||||
|
["input", {
|
||||||
|
"type" : "search",
|
||||||
|
"class" : "jp-authors-search-input",
|
||||||
|
"data-i18n" : "search_authors_placeholder",
|
||||||
|
"data-i18n-without" : "true",
|
||||||
|
"placeholder" : judge_pole.i18n.get("search_authors_placeholder"),
|
||||||
|
"oninput" : (input) => {
|
||||||
|
const query = input.value.trim().toLowerCase();
|
||||||
|
authors_list_box.querySelectorAll(".jp-check-label").forEach(label => {
|
||||||
|
const nick = label.getAttribute("data-nick") || "";
|
||||||
|
label.style.display = (!query || nick.includes(query)) ? "flex" : "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
["div", { "class" : "jp-authors-select-box" }]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
const authors_list_box = authors_picker.querySelector(".jp-authors-select-box");
|
||||||
|
|
||||||
|
users.forEach(user => {
|
||||||
|
const is_checked = current_author_ids.includes(user.id);
|
||||||
|
const is_ai = user.type === "ai";
|
||||||
|
const user_label_text = (is_ai ? "🤖 " : "👤 ") + user.nick;
|
||||||
|
|
||||||
|
const row = judge_pole.html(["label", {
|
||||||
|
"class" : "jp-check-label",
|
||||||
|
"data-nick" : user.nick.toLowerCase()
|
||||||
|
}, [
|
||||||
|
["input", {
|
||||||
|
"type" : "checkbox",
|
||||||
|
"name" : "author_ids",
|
||||||
|
"value" : user.id,
|
||||||
|
...(is_checked ? { "checked" : "checked" } : {})
|
||||||
|
}],
|
||||||
|
user_label_text
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
authors_list_box.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Formulario principal
|
||||||
|
const form = judge_pole.html(["form", {
|
||||||
|
"onsubmit" : (form_element, event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const selected_authors = [];
|
||||||
|
form_element.querySelectorAll("[name='author_ids']:checked").forEach(input => {
|
||||||
|
selected_authors.push(input.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!selected_authors.length){
|
||||||
|
alert(judge_pole.i18n.get("no_authors_selected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const links = [];
|
||||||
|
links_container.querySelectorAll(".jp-link-row").forEach(row => {
|
||||||
|
const name = row.querySelector(".link-name").value.trim();
|
||||||
|
const url = row.querySelector(".link-url").value.trim();
|
||||||
|
if(name && url) links.push({ name : name, url : url });
|
||||||
|
});
|
||||||
|
|
||||||
|
const tags_raw = form_element.querySelector("[name='tags']").value;
|
||||||
|
const tech_raw = form_element.querySelector("[name='technologies']").value;
|
||||||
|
|
||||||
|
const project_payload = {
|
||||||
|
user_ids : selected_authors,
|
||||||
|
name : form_element.querySelector("[name='name']").value.trim(),
|
||||||
|
avatar : form_element.querySelector("[name='avatar']").value.trim(),
|
||||||
|
description : form_element.querySelector("[name='description']").value.trim(),
|
||||||
|
tags : tags_raw.split(",").map(t => t.trim()).filter(t => t),
|
||||||
|
technologies : tech_raw.split(",").map(t => t.trim()).filter(t => t),
|
||||||
|
links : links
|
||||||
|
};
|
||||||
|
|
||||||
|
if(current_project) {
|
||||||
|
judge_pole.db.update_project(current_project.id, project_payload);
|
||||||
|
} else {
|
||||||
|
judge_pole.db.add_project(project_payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
close_modal();
|
||||||
|
render_cards();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
["div", { "class" : "jp-field jp-field-authors" }, [
|
||||||
|
["label", {"data-i18n" : "project_authors"}, judge_pole.i18n.get("project_authors")]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-field" }, [
|
||||||
|
["label", {"data-i18n" : "project_name"}, judge_pole.i18n.get("project_name")],
|
||||||
|
["input", {
|
||||||
|
"type" : "text",
|
||||||
|
"name" : "name",
|
||||||
|
"required" : "required",
|
||||||
|
"value" : current_project ? current_project.name : ""
|
||||||
|
}]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-field" }, [
|
||||||
|
["label", {"data-i18n" : "project_avatar"}, judge_pole.i18n.get("project_avatar")],
|
||||||
|
["input", {
|
||||||
|
"type" : "text",
|
||||||
|
"name" : "avatar",
|
||||||
|
"placeholder" : "./data/projects/logo.png",
|
||||||
|
"value" : current_project ? current_project.avatar : ""
|
||||||
|
}]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-field" }, [
|
||||||
|
["label", {"data-i18n" : "project_desc"}, judge_pole.i18n.get("project_desc")],
|
||||||
|
["textarea", { "name" : "description", "rows" : "3" }, current_project ? current_project.description : ""]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-field" }, [
|
||||||
|
["label", {"data-i18n" : "project_tags"}, judge_pole.i18n.get("project_tags")],
|
||||||
|
["input", {
|
||||||
|
"type" : "text",
|
||||||
|
"name" : "tags",
|
||||||
|
"placeholder" : "retro, troll, shader",
|
||||||
|
"value" : current_project ? current_project.tags.join(", ") : ""
|
||||||
|
}]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-field" }, [
|
||||||
|
["label", {"data-i18n" : "project_tech"}, judge_pole.i18n.get("project_tech")],
|
||||||
|
["input", {
|
||||||
|
"type" : "text",
|
||||||
|
"name" : "technologies",
|
||||||
|
"placeholder" : "C++, Raylib, WebGL",
|
||||||
|
"value" : current_project ? current_project.technologies.join(", ") : ""
|
||||||
|
}]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-field" }, [
|
||||||
|
["label", {"data-i18n" : "project_links"}, judge_pole.i18n.get("project_links")],
|
||||||
|
links_container = judge_pole.html(["div", { "class" : "jp-links-list" }])[0],
|
||||||
|
["button", { "type" : "button", "class" : "jp-btn-secondary", "data-i18n" : "btn_add_link", "onclick" : () => add_link_row() }, judge_pole.i18n.get("btn_add_link")]
|
||||||
|
]],
|
||||||
|
["div", { "style" : "display:flex; justify-content:flex-end; gap:0.5em; margin-top:1em;" }, [
|
||||||
|
["button", { "type" : "button", "class" : "jp-btn", "data-i18n" : "btn_cancel_edit", "onclick" : () => close_modal() }, judge_pole.i18n.get("btn_cancel_edit")],
|
||||||
|
["button", {
|
||||||
|
"type" : "submit",
|
||||||
|
"class" : "jp-btn-primary",
|
||||||
|
"data-i18n" : current_project ? "btn_update_project" : "btn_save_project"
|
||||||
|
},
|
||||||
|
current_project ? judge_pole.i18n.get("btn_update_project") : judge_pole.i18n.get("btn_save_project")
|
||||||
|
]
|
||||||
|
]]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
// 4. Inyección manual de authors_picker en su campo del form
|
||||||
|
const authors_field = form.querySelector(".jp-field-authors");
|
||||||
|
authors_field.appendChild(authors_picker);
|
||||||
|
|
||||||
|
// 5. Inyección del form en el modal body
|
||||||
|
modal_body.appendChild(form);
|
||||||
|
|
||||||
|
if(current_project && current_project.links && current_project.links.length) {
|
||||||
|
current_project.links.forEach(link => add_link_row(link.name, link.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
judge_pole.item_self.appendChild(modal_overlay);
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return ProjectsView;
|
||||||
|
})();
|
||||||
289
Public/ecma/Views/UsersView.ecma.js
Normal file
289
Public/ecma/Views/UsersView.ecma.js
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const UsersView = (function(){
|
||||||
|
|
||||||
|
const UsersView = function(judge_pole){
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
let container = null;
|
||||||
|
let cards_wrapper = null;
|
||||||
|
let filter_query = "";
|
||||||
|
|
||||||
|
this.go = () => {
|
||||||
|
judge_pole.content_area.innerHTML = "";
|
||||||
|
judge_pole.html(judge_pole.content_area, build());
|
||||||
|
};
|
||||||
|
|
||||||
|
const build = () => {
|
||||||
|
const is_admin = judge_pole.session.is_admin();
|
||||||
|
|
||||||
|
const topbar_actions = [
|
||||||
|
["input", {
|
||||||
|
"type" : "search",
|
||||||
|
"class" : "jp-search-input",
|
||||||
|
"data-i18n" : "search_users_placeholder",
|
||||||
|
"data-i18n-without" : "true",
|
||||||
|
"placeholder" : judge_pole.i18n.get("search_users_placeholder"),
|
||||||
|
"value" : filter_query,
|
||||||
|
"oninput" : (input) => {
|
||||||
|
filter_query = input.value.trim().toLowerCase();
|
||||||
|
render_cards();
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sólo añadimos el botón si es admin
|
||||||
|
if(is_admin){
|
||||||
|
topbar_actions.push(["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-btn-primary",
|
||||||
|
"data-i18n" : "btn_new_user",
|
||||||
|
"onclick" : () => show_user_modal()
|
||||||
|
}, judge_pole.i18n.get("btn_new_user")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
container = judge_pole.html(["div", { "class" : "jp-view jp-users-view" }, [
|
||||||
|
["div", { "class" : "jp-view-topbar" }, [
|
||||||
|
["h2", { "data-i18n" : "users_title" }, judge_pole.i18n.get("users_title")],
|
||||||
|
["div", { "style" : "display:flex; gap:0.5em; align-items:center;" }, topbar_actions]
|
||||||
|
]],
|
||||||
|
cards_wrapper = judge_pole.html(["div", { "class" : "jp-cards-grid", "id" : "jp-users-cards" }])[0]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
render_cards();
|
||||||
|
|
||||||
|
return container;
|
||||||
|
};
|
||||||
|
|
||||||
|
const render_cards = () => {
|
||||||
|
if(!cards_wrapper) return;
|
||||||
|
cards_wrapper.innerHTML = "";
|
||||||
|
|
||||||
|
const users = judge_pole.db.get_data().users;
|
||||||
|
const is_admin = judge_pole.session.is_admin();
|
||||||
|
|
||||||
|
if(!users.length){
|
||||||
|
cards_wrapper.appendChild(judge_pole.html(["p", { "class" : "jp-empty", "data-i18n" : "no_users" }, judge_pole.i18n.get("no_users")])[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered_users = users.filter(user => {
|
||||||
|
if(!filter_query) return true;
|
||||||
|
const match_nick = user.nick.toLowerCase().includes(filter_query);
|
||||||
|
const match_desc = user.description ? user.description.toLowerCase().includes(filter_query) : false;
|
||||||
|
const match_links = user.links ? user.links.some(l => l.name.toLowerCase().includes(filter_query)) : false;
|
||||||
|
return match_nick || match_desc || match_links;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!filtered_users.length){
|
||||||
|
cards_wrapper.appendChild(judge_pole.html(["p", { "class" : "jp-empty", "data-i18n" : "no_users_found" }, judge_pole.i18n.get("no_users_found")])[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered_users.forEach(user => {
|
||||||
|
const links_html = user.links.map(link => ["a", { "href" : link.url, "target" : "_blank", "class" : "jp-tag-link" }, link.name]);
|
||||||
|
const is_ai = user.type === "ai";
|
||||||
|
const type_badge = ["span", {
|
||||||
|
"class" : "jp-badge " + (is_ai ? "jp-badge-type-ai" : "jp-badge-type-human")
|
||||||
|
}, is_ai ? "🤖 IA" : "👤 Humano"];
|
||||||
|
|
||||||
|
const avatar_element = user.avatar
|
||||||
|
? ["img", { "src" : user.avatar, "class" : "jp-avatar", "alt" : user.nick }]
|
||||||
|
: ["div", { "class" : "jp-avatar-placeholder" }, (user.nick ? user.nick[0].toUpperCase() : (is_ai ? "🤖" : "👤"))];
|
||||||
|
|
||||||
|
const card_elements = [];
|
||||||
|
|
||||||
|
// Botones sólo para Administradores
|
||||||
|
if(is_admin){
|
||||||
|
card_elements.push(["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-card-btn-delete",
|
||||||
|
"data-i18n" : "btn_delete",
|
||||||
|
"data-i18n-without" : "true",
|
||||||
|
"title" : judge_pole.i18n.get("btn_delete"),
|
||||||
|
"onclick" : () => {
|
||||||
|
const confirm_msg = judge_pole.i18n.get("confirm_delete_user", { nick : user.nick });
|
||||||
|
if(confirm(confirm_msg)){
|
||||||
|
judge_pole.db.delete_user(user.id);
|
||||||
|
render_cards();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "✕"]);
|
||||||
|
|
||||||
|
card_elements.push(["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-card-btn-edit",
|
||||||
|
"data-i18n" : "btn_edit",
|
||||||
|
"data-i18n-without" : "true",
|
||||||
|
"title" : judge_pole.i18n.get("btn_edit"),
|
||||||
|
"onclick" : () => show_user_modal(user.id)
|
||||||
|
}, "✏️"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
card_elements.push(
|
||||||
|
avatar_element,
|
||||||
|
["div", { "style" : "display:flex; justify-content:space-between; align-items:center; margin-bottom:0.3em;" }, [
|
||||||
|
["h3", { "style" : "margin:0;" }, user.nick],
|
||||||
|
type_badge
|
||||||
|
]],
|
||||||
|
["p", { "class" : "jp-desc" }, user.description],
|
||||||
|
["div", { "class" : "jp-tags-group" }, links_html],
|
||||||
|
["div", { "class" : "jp-meta" }, [
|
||||||
|
["small", {}, judge_pole.i18n.get("created_at") + " " + user.created_at.substring(0, 10)],
|
||||||
|
["small", {}, judge_pole.i18n.get("updated_at") + " " + user.updated_at.substring(0, 10)]
|
||||||
|
]]
|
||||||
|
);
|
||||||
|
|
||||||
|
const card = judge_pole.html(["div", { "class" : "jp-card" }, card_elements])[0];
|
||||||
|
cards_wrapper.appendChild(card);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const show_user_modal = (user_id = null) => {
|
||||||
|
const current_user = user_id
|
||||||
|
? judge_pole.db.get_data().users.find(u => u.id === user_id)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let links_container = null;
|
||||||
|
|
||||||
|
const add_link_row = (name = "", url = "") => {
|
||||||
|
const row = judge_pole.html(["div", { "class" : "jp-link-row" }, [
|
||||||
|
["input", { "type" : "text", "placeholder" : "Nombre (ej: GitHub)", "class" : "link-name", "value" : name }],
|
||||||
|
["input", { "type" : "url", "placeholder" : "https://...", "class" : "link-url", "value" : url }],
|
||||||
|
["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-btn-danger",
|
||||||
|
"onclick" : (btn) => btn.parentNode.remove()
|
||||||
|
}, "✕"]
|
||||||
|
]])[0];
|
||||||
|
links_container.appendChild(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close_modal = () => {
|
||||||
|
if(modal_overlay && modal_overlay.parentNode)
|
||||||
|
modal_overlay.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
const modal_title = current_user
|
||||||
|
? judge_pole.i18n.get("modal_edit_user_title", { nick : current_user.nick })
|
||||||
|
: judge_pole.i18n.get("modal_new_user_title");
|
||||||
|
|
||||||
|
const modal_overlay = judge_pole.html(["div", {
|
||||||
|
"class" : "jp-modal-overlay",
|
||||||
|
"onclick" : (overlay, e) => {
|
||||||
|
if(e.target === overlay) close_modal();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
["div", { "class" : "jp-modal-window" }, [
|
||||||
|
["div", { "class" : "jp-modal-header" }, [
|
||||||
|
["h3", {}, modal_title],
|
||||||
|
["button", { "type" : "button", "class" : "jp-btn-danger", "onclick" : () => close_modal() }, "✕"]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-modal-body" }]
|
||||||
|
]]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
const modal_body = modal_overlay.querySelector(".jp-modal-body");
|
||||||
|
|
||||||
|
const form = judge_pole.html(["form", {
|
||||||
|
"onsubmit" : (form_element, event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const links = [];
|
||||||
|
links_container.querySelectorAll(".jp-link-row").forEach(row => {
|
||||||
|
const name = row.querySelector(".link-name").value.trim();
|
||||||
|
const url = row.querySelector(".link-url").value.trim();
|
||||||
|
if(name && url) links.push({ name : name, url : url });
|
||||||
|
});
|
||||||
|
|
||||||
|
const select_type = form_element.querySelector("[name='type']");
|
||||||
|
const pass_input = form_element.querySelector("[name='password']"); // <--- Extraer password
|
||||||
|
|
||||||
|
const user_payload = {
|
||||||
|
type : select_type ? select_type.value : "human",
|
||||||
|
nick : form_element.querySelector("[name='nick']").value.trim(),
|
||||||
|
password : pass_input ? pass_input.value.trim() : "", // <--- Incluir en payload
|
||||||
|
description : form_element.querySelector("[name='description']").value.trim(),
|
||||||
|
avatar : form_element.querySelector("[name='avatar']").value.trim(),
|
||||||
|
links : links
|
||||||
|
};
|
||||||
|
|
||||||
|
if(current_user) {
|
||||||
|
judge_pole.db.update_user(current_user.id, user_payload);
|
||||||
|
} else {
|
||||||
|
judge_pole.db.add_user(user_payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
close_modal();
|
||||||
|
judge_pole.render_header(); // <--- Forzar refresco del header por si cambió el estado de seguridad
|
||||||
|
render_cards();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
["div", { "class" : "jp-field" }, [
|
||||||
|
["label", {"data-i18n" : "user_type"}, judge_pole.i18n.get("user_type")],
|
||||||
|
["select", { "name" : "type" }, [
|
||||||
|
["option", { "value" : "human", "data-i18n" : "user_type_human", ...(current_user && current_user.type === "human" ? { "selected" : "selected" } : {}) }, judge_pole.i18n.get("user_type_human")],
|
||||||
|
["option", { "value" : "ai", "data-i18n" : "user_type_ai", ...(current_user && current_user.type === "ai" ? { "selected" : "selected" } : {}) }, judge_pole.i18n.get("user_type_ai")]
|
||||||
|
]]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-field" }, [
|
||||||
|
["label", {"data-i18n" : "user_nick"}, judge_pole.i18n.get("user_nick")],
|
||||||
|
["input", {
|
||||||
|
"type" : "text",
|
||||||
|
"name" : "nick",
|
||||||
|
"required" : "required",
|
||||||
|
"value" : current_user ? current_user.nick : ""
|
||||||
|
}]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-field" }, [
|
||||||
|
["label", { "data-i18n" : "user_pass_label" }, judge_pole.i18n.get("user_pass_label")],
|
||||||
|
["input", {
|
||||||
|
"type" : "password",
|
||||||
|
"name" : "password",
|
||||||
|
"placeholder" : judge_pole.i18n.get("user_pass_placeholder"),
|
||||||
|
"value" : current_user ? (current_user.password || "") : ""
|
||||||
|
}]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-field" }, [
|
||||||
|
["label", {"data-i18n" : "user_avatar"}, judge_pole.i18n.get("user_avatar")],
|
||||||
|
["input", {
|
||||||
|
"type" : "text",
|
||||||
|
"name" : "avatar",
|
||||||
|
"placeholder" : "./data/avatars/yo.png",
|
||||||
|
"value" : current_user ? current_user.avatar : ""
|
||||||
|
}]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-field" }, [
|
||||||
|
["label", {"data-i18n" : "user_desc"}, judge_pole.i18n.get("user_desc")],
|
||||||
|
["textarea", { "name" : "description", "rows" : "3" }, current_user ? current_user.description : ""]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-field" }, [
|
||||||
|
["label", {"data-i18n" : "user_links"}, judge_pole.i18n.get("user_links")],
|
||||||
|
links_container = judge_pole.html(["div", { "class" : "jp-links-list" }])[0],
|
||||||
|
["button", { "type" : "button", "class" : "jp-btn-secondary", "data-i18n" : "btn_add_link", "onclick" : () => add_link_row() }, judge_pole.i18n.get("btn_add_link")]
|
||||||
|
]],
|
||||||
|
["div", { "style" : "display:flex; justify-content:flex-end; gap:0.5em; margin-top:1em;" }, [
|
||||||
|
["button", { "type" : "button", "class" : "jp-btn", "data-i18n" : "btn_cancel_edit", "onclick" : () => close_modal() }, judge_pole.i18n.get("btn_cancel_edit")],
|
||||||
|
["button", {
|
||||||
|
"type" : "submit",
|
||||||
|
"class" : "jp-btn-primary",
|
||||||
|
"data-i18n" : current_user ? "btn_update_user" : "btn_save_user"
|
||||||
|
},
|
||||||
|
current_user ? judge_pole.i18n.get("btn_update_user") : judge_pole.i18n.get("btn_save_user")
|
||||||
|
]
|
||||||
|
]]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
modal_body.appendChild(form);
|
||||||
|
|
||||||
|
if(current_user && current_user.links && current_user.links.length) {
|
||||||
|
current_user.links.forEach(link => add_link_row(link.name, link.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
judge_pole.item_self.appendChild(modal_overlay);
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return UsersView;
|
||||||
|
})();
|
||||||
785
Public/ecma/Views/VotesView.ecma.js
Normal file
785
Public/ecma/Views/VotesView.ecma.js
Normal file
@ -0,0 +1,785 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const VotesView = (function(){
|
||||||
|
|
||||||
|
const VotesView = function(judge_pole){
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
let container = null;
|
||||||
|
let rank_wrapper = null;
|
||||||
|
let filter_query = "";
|
||||||
|
let expanded_project_id = null;
|
||||||
|
|
||||||
|
this.go = () => {
|
||||||
|
judge_pole.content_area.innerHTML = "";
|
||||||
|
judge_pole.html(judge_pole.content_area, build());
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Extrae las puntuaciones y pesos normalizados (con retrocompatibilidad)
|
||||||
|
const normalize_project_data = (raw_data) => {
|
||||||
|
if(!raw_data) return null;
|
||||||
|
// Si el objeto fue guardado con la nueva estructura
|
||||||
|
if(raw_data.scores !== undefined){
|
||||||
|
return {
|
||||||
|
scores : raw_data.scores || {},
|
||||||
|
judge_weights : raw_data.judge_weights || {},
|
||||||
|
criteria_weights : raw_data.criteria_weights || {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Compatibilidad hacia atrás
|
||||||
|
return {
|
||||||
|
scores : raw_data,
|
||||||
|
judge_weights : {},
|
||||||
|
criteria_weights : {}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calcula métricas naturales y ponderadas completas
|
||||||
|
const calculate_project_metrics = (project_id) => {
|
||||||
|
const raw_entry = judge_pole.db.get_data().votes[project_id];
|
||||||
|
const pack = normalize_project_data(raw_entry);
|
||||||
|
if(!pack) return null;
|
||||||
|
|
||||||
|
const { scores, judge_weights, criteria_weights } = pack;
|
||||||
|
const user_ids = Object.keys(scores);
|
||||||
|
if(!user_ids.length) return null;
|
||||||
|
|
||||||
|
const criteria_stats = {};
|
||||||
|
let natural_sum = 0, natural_count = 0;
|
||||||
|
let weighted_sum = 0, total_weight_mass = 0;
|
||||||
|
|
||||||
|
user_ids.forEach(u_id => {
|
||||||
|
const u_weight = parseInt(judge_weights[u_id]) || 20;
|
||||||
|
const u_scores = scores[u_id];
|
||||||
|
|
||||||
|
Object.keys(u_scores).forEach(crit => {
|
||||||
|
const val = u_scores[crit];
|
||||||
|
const c_weight = parseInt(criteria_weights[crit]) || 20;
|
||||||
|
|
||||||
|
if(!criteria_stats[crit]) {
|
||||||
|
criteria_stats[crit] = {
|
||||||
|
nat_sum : 0,
|
||||||
|
nat_count : 0,
|
||||||
|
pond_sum : 0,
|
||||||
|
pond_weights : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Natural por criterio
|
||||||
|
criteria_stats[crit].nat_sum += val;
|
||||||
|
criteria_stats[crit].nat_count++;
|
||||||
|
|
||||||
|
// Ponderado de criterio según peso de juez
|
||||||
|
criteria_stats[crit].pond_sum += (val * u_weight);
|
||||||
|
criteria_stats[crit].pond_weights += u_weight;
|
||||||
|
|
||||||
|
// Globales
|
||||||
|
natural_sum += val;
|
||||||
|
natural_count++;
|
||||||
|
|
||||||
|
const joint_weight = u_weight * c_weight;
|
||||||
|
weighted_sum += (val * joint_weight);
|
||||||
|
total_weight_mass += joint_weight;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const criteria_averages = {};
|
||||||
|
Object.keys(criteria_stats).forEach(crit => {
|
||||||
|
const stat = criteria_stats[crit];
|
||||||
|
criteria_averages[crit] = {
|
||||||
|
natural : stat.nat_count > 0 ? (stat.nat_sum / stat.nat_count).toFixed(2) : "0.00",
|
||||||
|
weighted : stat.pond_weights > 0 ? (stat.pond_sum / stat.pond_weights).toFixed(2) : "0.00"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
natural_avg : natural_count > 0 ? (natural_sum / natural_count).toFixed(2) : "0.00",
|
||||||
|
weighted_avg : total_weight_mass > 0 ? (weighted_sum / total_weight_mass).toFixed(2) : "0.00",
|
||||||
|
criteria : criteria_averages
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const build = () => {
|
||||||
|
const is_admin = judge_pole.session.is_admin();
|
||||||
|
|
||||||
|
const topbar_actions = [
|
||||||
|
["input", {
|
||||||
|
"type" : "search",
|
||||||
|
"class" : "jp-search-input",
|
||||||
|
"data-i18n" : "search_rankings_placeholder",
|
||||||
|
"data-i18n-without" : "true",
|
||||||
|
"placeholder" : judge_pole.i18n.get("search_rankings_placeholder"),
|
||||||
|
"value" : filter_query,
|
||||||
|
"oninput" : (input) => {
|
||||||
|
filter_query = input.value.trim().toLowerCase();
|
||||||
|
render_rankings();
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
];
|
||||||
|
|
||||||
|
if(is_admin){
|
||||||
|
topbar_actions.push(["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-btn-primary",
|
||||||
|
"data-i18n" : "btn_new_vote",
|
||||||
|
"onclick" : () => show_vote_modal()
|
||||||
|
}, judge_pole.i18n.get("btn_new_vote")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
container = judge_pole.html(["div", { "class" : "jp-view jp-votes-view" }, [
|
||||||
|
["div", { "class" : "jp-view-topbar" }, [
|
||||||
|
["h2", { "data-i18n" : "votes_title" }, judge_pole.i18n.get("votes_title")],
|
||||||
|
["div", { "style" : "display:flex; gap:0.5em; align-items:center;" }, topbar_actions]
|
||||||
|
]],
|
||||||
|
rank_wrapper = judge_pole.html(["div", { "class" : "jp-rank-table-wrapper" }])[0]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
render_rankings();
|
||||||
|
|
||||||
|
return container;
|
||||||
|
};
|
||||||
|
|
||||||
|
const render_rankings = () => {
|
||||||
|
if(!rank_wrapper) return;
|
||||||
|
rank_wrapper.innerHTML = "";
|
||||||
|
|
||||||
|
const db_data = judge_pole.db.get_data();
|
||||||
|
const projects = db_data.projects;
|
||||||
|
const is_admin = judge_pole.session.is_admin();
|
||||||
|
|
||||||
|
const scored_projects = [];
|
||||||
|
projects.forEach(project => {
|
||||||
|
const metrics = calculate_project_metrics(project.id);
|
||||||
|
if(metrics){
|
||||||
|
scored_projects.push({
|
||||||
|
project : project,
|
||||||
|
natural_avg : metrics.natural_avg,
|
||||||
|
weighted_avg : metrics.weighted_avg,
|
||||||
|
weighted_num : parseFloat(metrics.weighted_avg),
|
||||||
|
criteria : metrics.criteria
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scored_projects.sort((a, b) => b.weighted_num - a.weighted_num);
|
||||||
|
|
||||||
|
scored_projects.forEach((item, index) => {
|
||||||
|
item.global_rank = index + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!scored_projects.length){
|
||||||
|
rank_wrapper.appendChild(judge_pole.html(["p", { "class" : "jp-empty", "data-i18n" : "no_votes_registered" }, judge_pole.i18n.get("no_votes_registered")])[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered_list = scored_projects.filter(item => {
|
||||||
|
if(!filter_query) return true;
|
||||||
|
return item.project.name.toLowerCase().includes(filter_query);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cabeceras de tabla: la columna acciones sólo se muestra si es admin
|
||||||
|
const thead_cols = [
|
||||||
|
["th", { "data-i18n" : "rank_global" }, judge_pole.i18n.get("rank_global")],
|
||||||
|
["th", { "data-i18n" : "rank_filtered" }, judge_pole.i18n.get("rank_filtered")],
|
||||||
|
["th", { "data-i18n" : "rank_project" }, judge_pole.i18n.get("rank_project")],
|
||||||
|
["th", { "data-i18n" : "rank_average" }, judge_pole.i18n.get("rank_average")]
|
||||||
|
];
|
||||||
|
|
||||||
|
if(is_admin){
|
||||||
|
thead_cols.push(["th", { "data-i18n" : "rank_actions", "style" : "text-align:right;" }, judge_pole.i18n.get("rank_actions")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = judge_pole.html(["table", { "class" : "jp-rank-table" }, [
|
||||||
|
["thead", {}, [
|
||||||
|
["tr", {}, thead_cols]
|
||||||
|
]],
|
||||||
|
["tbody", {}]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
const tbody = table.querySelector("tbody");
|
||||||
|
|
||||||
|
filtered_list.forEach((item, filter_idx) => {
|
||||||
|
const is_expanded = expanded_project_id === item.project.id;
|
||||||
|
|
||||||
|
const row_cols = [
|
||||||
|
["td", { "class" : "jp-rank-badge-global" }, "#" + item.global_rank],
|
||||||
|
["td", { "class" : "jp-rank-badge-filter" }, "#" + (filter_idx + 1)],
|
||||||
|
["td", { "style" : "font-weight:bold;" }, item.project.name],
|
||||||
|
["td", {}, [
|
||||||
|
["span", { "class" : "jp-rank-avg-score" }, item.weighted_avg + " pts "],
|
||||||
|
["small", { "style" : "color:#8b949e;" }, "(Nat: " + item.natural_avg + ")"]
|
||||||
|
]]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Celda de acciones sólo si es admin
|
||||||
|
if(is_admin){
|
||||||
|
row_cols.push(["td", { "style" : "text-align:right;" }, [
|
||||||
|
["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-btn",
|
||||||
|
"style" : "margin-right:0.4em; padding:0.2em 0.5em;",
|
||||||
|
"data-i18n" : "btn_edit",
|
||||||
|
"data-i18n-without" : "true",
|
||||||
|
"title" : judge_pole.i18n.get("btn_edit"),
|
||||||
|
"onclick" : () => show_vote_modal(item.project.id)
|
||||||
|
}, "✏️"],
|
||||||
|
["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-btn-danger",
|
||||||
|
"style" : "padding:0.2em 0.5em;",
|
||||||
|
"data-i18n" : "btn_delete",
|
||||||
|
"data-i18n-without" : "true",
|
||||||
|
"title" : judge_pole.i18n.get("btn_delete"),
|
||||||
|
"onclick" : () => {
|
||||||
|
const msg = judge_pole.i18n.get("confirm_delete_vote", { project : item.project.name });
|
||||||
|
if(confirm(msg)){
|
||||||
|
delete db_data.votes[item.project.id];
|
||||||
|
if(expanded_project_id === item.project.id) expanded_project_id = null;
|
||||||
|
render_rankings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "✕"]
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tr = judge_pole.html(["tr", {
|
||||||
|
"onclick" : (row, event) => {
|
||||||
|
if(event.target.closest("button")) return;
|
||||||
|
expanded_project_id = is_expanded ? null : item.project.id;
|
||||||
|
render_rankings();
|
||||||
|
}
|
||||||
|
}, row_cols])[0];
|
||||||
|
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
|
||||||
|
// Desglose expandido (visible para todos en solo lectura)
|
||||||
|
if(is_expanded){
|
||||||
|
const breakdown_items = Object.keys(item.criteria).map(crit => ["div", { "class" : "jp-breakdown-item" }, [
|
||||||
|
["span", { "class" : "jp-crit-name" }, crit],
|
||||||
|
["div", { "style" : "display:flex; gap:0.4em;" }, [
|
||||||
|
["span", { "style" : "color:#8b949e;" }, "N: " + item.criteria[crit].natural],
|
||||||
|
["span", { "class" : "jp-crit-val" }, "P: " + item.criteria[crit].weighted]
|
||||||
|
]]
|
||||||
|
]]);
|
||||||
|
|
||||||
|
const raw_pack = normalize_project_data(db_data.votes[item.project.id]) || { scores:{}, judge_weights:{}, criteria_weights:{} };
|
||||||
|
const participating_users = db_data.users.filter(u => Object.keys(raw_pack.scores).includes(u.id));
|
||||||
|
const active_criteria_keys = Object.keys(item.criteria);
|
||||||
|
|
||||||
|
const static_table = judge_pole.html(["table", { "class" : "jp-matrix-table", "style" : "margin-top:0.8em;" }, [
|
||||||
|
["thead", {}, [
|
||||||
|
["tr", {}, [
|
||||||
|
["th", { "style" : "text-align:left;" }, "Criterio"]
|
||||||
|
].concat(participating_users.map(u => [
|
||||||
|
"th", {}, [
|
||||||
|
["div", {}, (u.type === 'ai' ? '🤖 ' : '👤 ') + u.nick],
|
||||||
|
["small", { "style" : "color:#f1e05a;" }, "(W: " + (raw_pack.judge_weights[u.id] || 20) + ")"]
|
||||||
|
]
|
||||||
|
])).concat([
|
||||||
|
["th", { "class" : "jp-summary-cell" }, "Media Nat."],
|
||||||
|
["th", { "class" : "jp-summary-cell", "style" : "color:#f1e05a;" }, "Media Pond."]
|
||||||
|
])]
|
||||||
|
]],
|
||||||
|
["tbody", {}]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
const static_tbody = static_table.querySelector("tbody");
|
||||||
|
|
||||||
|
active_criteria_keys.forEach(crit => {
|
||||||
|
const crit_w = raw_pack.criteria_weights[crit] || 20;
|
||||||
|
const cells = [
|
||||||
|
["td", { "class" : "jp-crit-cell" }, [
|
||||||
|
["span", {}, crit + " "],
|
||||||
|
["small", { "style" : "color:#f1e05a;" }, "(W: " + crit_w + ")"]
|
||||||
|
]]
|
||||||
|
];
|
||||||
|
|
||||||
|
participating_users.forEach(u => {
|
||||||
|
const score = (raw_pack.scores[u.id] && raw_pack.scores[u.id][crit] !== undefined)
|
||||||
|
? raw_pack.scores[u.id][crit]
|
||||||
|
: "-";
|
||||||
|
cells.push(["td", { "style" : "font-weight:bold;" }, "" + score]);
|
||||||
|
});
|
||||||
|
|
||||||
|
cells.push(["td", { "class" : "jp-summary-subcell" }, item.criteria[crit].natural]);
|
||||||
|
cells.push(["td", { "class" : "jp-summary-subcell", "style" : "color:#f1e05a;" }, item.criteria[crit].weighted]);
|
||||||
|
|
||||||
|
static_tbody.appendChild(judge_pole.html(["tr", {}, cells])[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const col_count = is_admin ? "5" : "4";
|
||||||
|
|
||||||
|
const tr_detail = judge_pole.html(["tr", { "class" : "jp-breakdown-row" }, [
|
||||||
|
["td", { "colspan" : col_count }, [
|
||||||
|
["div", { "style" : "font-size:0.75em; color:#8b949e;", "data-i18n" : "rank_criteria_breakdown" }, judge_pole.i18n.get("rank_criteria_breakdown")],
|
||||||
|
["div", { "class" : "jp-breakdown-grid" }, breakdown_items],
|
||||||
|
static_table
|
||||||
|
]]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
tbody.appendChild(tr_detail);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rank_wrapper.appendChild(table);
|
||||||
|
};
|
||||||
|
|
||||||
|
const show_vote_modal = (target_project_id = null) => {
|
||||||
|
const db_data = judge_pole.db.get_data();
|
||||||
|
let selected_p_id = target_project_id || (db_data.projects[0] ? db_data.projects[0].id : null);
|
||||||
|
|
||||||
|
const existing_pack = normalize_project_data(db_data.votes[selected_p_id]) || {
|
||||||
|
scores : {},
|
||||||
|
judge_weights : {},
|
||||||
|
criteria_weights : {}
|
||||||
|
};
|
||||||
|
|
||||||
|
let chosen_judges = Object.keys(existing_pack.scores).length
|
||||||
|
? Object.keys(existing_pack.scores)
|
||||||
|
: db_data.users.map(u => u.id);
|
||||||
|
|
||||||
|
let chosen_criteria = [...db_data.criteria];
|
||||||
|
|
||||||
|
// Pesos en memoria
|
||||||
|
const active_judge_weights = { ...existing_pack.judge_weights };
|
||||||
|
const active_criteria_weights = { ...existing_pack.criteria_weights };
|
||||||
|
|
||||||
|
const close_modal = () => {
|
||||||
|
if(modal_overlay && modal_overlay.parentNode)
|
||||||
|
modal_overlay.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
const modal_overlay = judge_pole.html(["div", {
|
||||||
|
"class" : "jp-modal-overlay",
|
||||||
|
"onclick" : (overlay, e) => {
|
||||||
|
if(e.target === overlay) close_modal();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
["div", { "class" : "jp-modal-window", "style" : "max-width:50em;" }, [
|
||||||
|
["div", { "class" : "jp-modal-header" }, [
|
||||||
|
["h3", {}, target_project_id ? judge_pole.i18n.get("modal_edit_vote_title", { project : db_data.projects.find(p => p.id === target_project_id).name }) : judge_pole.i18n.get("modal_new_vote_title")],
|
||||||
|
["button", { "type" : "button", "class" : "jp-btn-danger", "onclick" : () => close_modal() }, "✕"]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-modal-body" }]
|
||||||
|
]]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
const modal_body = modal_overlay.querySelector(".jp-modal-body");
|
||||||
|
|
||||||
|
// --- 1. Selector de Proyecto ---
|
||||||
|
const project_options = db_data.projects.map(p => ["option", {
|
||||||
|
"value" : p.id,
|
||||||
|
...(p.id === selected_p_id ? { "selected" : "selected" } : {})
|
||||||
|
}, p.name]);
|
||||||
|
|
||||||
|
const project_select = judge_pole.html(["div", { "class" : "jp-field" }, [
|
||||||
|
["label", { "data-i18n" : "select_project" }, judge_pole.i18n.get("select_project")],
|
||||||
|
["select", {
|
||||||
|
"onchange" : (sel) => {
|
||||||
|
selected_p_id = sel.value;
|
||||||
|
rebuild_matrix();
|
||||||
|
}
|
||||||
|
}, project_options]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
// --- 2. Picker de Jueces con Pesos Numéricos ---
|
||||||
|
const judges_picker = judge_pole.html(["div", { "class" : "jp-field" }, [
|
||||||
|
["label", { "data-i18n" : "participating_judges" }, judge_pole.i18n.get("participating_judges")],
|
||||||
|
["div", { "class" : "jp-authors-picker" }, [
|
||||||
|
["input", {
|
||||||
|
"type" : "search",
|
||||||
|
"class" : "jp-authors-search-input",
|
||||||
|
"data-i18n" : "search_authors_placeholder",
|
||||||
|
"data-i18n-without" : "true",
|
||||||
|
"placeholder" : judge_pole.i18n.get("search_authors_placeholder"),
|
||||||
|
"oninput" : (input) => {
|
||||||
|
const q = input.value.trim().toLowerCase();
|
||||||
|
judges_list_box.querySelectorAll(".jp-criterion-row").forEach(lbl => {
|
||||||
|
const nick = lbl.getAttribute("data-name") || "";
|
||||||
|
lbl.style.display = (!q || nick.includes(q)) ? "flex" : "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
["div", { "class" : "jp-authors-select-box" }]
|
||||||
|
]]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
const judges_list_box = judges_picker.querySelector(".jp-authors-select-box");
|
||||||
|
|
||||||
|
db_data.users.forEach(user => {
|
||||||
|
const is_checked = chosen_judges.includes(user.id);
|
||||||
|
const current_w = active_judge_weights[user.id] !== undefined ? active_judge_weights[user.id] : 20;
|
||||||
|
active_judge_weights[user.id] = current_w;
|
||||||
|
|
||||||
|
const row = judge_pole.html(["div", {
|
||||||
|
"class" : "jp-criterion-row",
|
||||||
|
"data-name" : user.nick.toLowerCase()
|
||||||
|
}, [
|
||||||
|
["label", { "class" : "jp-check-label", "style" : "flex:1; cursor:pointer;" }, [
|
||||||
|
["input", {
|
||||||
|
"type" : "checkbox",
|
||||||
|
"value" : user.id,
|
||||||
|
...(is_checked ? { "checked" : "checked" } : {}),
|
||||||
|
"onchange" : (chk) => {
|
||||||
|
// Buscamos el input de peso relativo a esta fila de forma segura
|
||||||
|
const row_parent = chk.closest(".jp-criterion-row");
|
||||||
|
const input_target = row_parent ? row_parent.querySelector(".jp-weight-input") : null;
|
||||||
|
if(input_target) input_target.disabled = !chk.checked;
|
||||||
|
|
||||||
|
if(chk.checked) chosen_judges.push(user.id);
|
||||||
|
else chosen_judges = chosen_judges.filter(id => id !== user.id);
|
||||||
|
rebuild_matrix();
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
(user.type === "ai" ? "🤖 " : "👤 ") + user.nick
|
||||||
|
]],
|
||||||
|
["div", { "style" : "display:flex; align-items:center;" }, [
|
||||||
|
["small", { "style" : "color:#8b949e; font-size:0.7em;", "data-i18n" : "weight_label" }, judge_pole.i18n.get("weight_label")],
|
||||||
|
["input", {
|
||||||
|
"type" : "number",
|
||||||
|
"min" : "1",
|
||||||
|
"step" : "1",
|
||||||
|
"class" : "jp-weight-input",
|
||||||
|
"value" : current_w,
|
||||||
|
...(!is_checked ? { "disabled" : "disabled" } : {}),
|
||||||
|
"oninput" : (inp) => {
|
||||||
|
const val = parseInt(inp.value) || 1;
|
||||||
|
active_judge_weights[user.id] = Math.max(1, Math.floor(val));
|
||||||
|
rebuild_matrix();
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
]]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
judges_list_box.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 3. Picker de Criterios con Pesos Numéricos ---
|
||||||
|
const criteria_picker = judge_pole.html(["div", { "class" : "jp-field" }, [
|
||||||
|
["div", { "style" : "display:flex; justify-content:space-between; align-items:center;" }, [
|
||||||
|
["label", { "data-i18n" : "evaluation_criteria" }, judge_pole.i18n.get("evaluation_criteria")],
|
||||||
|
["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-btn-secondary",
|
||||||
|
"style" : "padding:0.2em 0.5em; font-size:0.7em;",
|
||||||
|
"data-i18n" : "btn_new_criterion",
|
||||||
|
"onclick" : () => {
|
||||||
|
const new_c = prompt(judge_pole.i18n.get("new_criterion_prompt"));
|
||||||
|
if(new_c && judge_pole.db.add_criterion(new_c)){
|
||||||
|
chosen_criteria.push(new_c.trim());
|
||||||
|
active_criteria_weights[new_c.trim()] = 20;
|
||||||
|
render_criteria_rows();
|
||||||
|
rebuild_matrix();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, judge_pole.i18n.get("btn_new_criterion")]
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-authors-picker" }, [
|
||||||
|
["input", {
|
||||||
|
"type" : "search",
|
||||||
|
"class" : "jp-authors-search-input",
|
||||||
|
"data-i18n" : "search_criteria_placeholder",
|
||||||
|
"data-i18n-without" : "true",
|
||||||
|
"placeholder" : judge_pole.i18n.get("search_criteria_placeholder"),
|
||||||
|
"oninput" : (input) => {
|
||||||
|
const q = input.value.trim().toLowerCase();
|
||||||
|
criteria_list_box.querySelectorAll(".jp-criterion-row").forEach(lbl => {
|
||||||
|
const name = lbl.getAttribute("data-name") || "";
|
||||||
|
lbl.style.display = (!q || name.includes(q)) ? "flex" : "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
["div", { "class" : "jp-authors-select-box" }]
|
||||||
|
]]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
const criteria_list_box = criteria_picker.querySelector(".jp-authors-select-box");
|
||||||
|
|
||||||
|
const render_criteria_rows = () => {
|
||||||
|
criteria_list_box.innerHTML = "";
|
||||||
|
db_data.criteria.forEach(crit => {
|
||||||
|
const is_checked = chosen_criteria.includes(crit);
|
||||||
|
const current_w = active_criteria_weights[crit] !== undefined ? active_criteria_weights[crit] : 20;
|
||||||
|
active_criteria_weights[crit] = current_w;
|
||||||
|
|
||||||
|
const row = judge_pole.html(["div", {
|
||||||
|
"class" : "jp-criterion-row",
|
||||||
|
"data-name" : crit.toLowerCase()
|
||||||
|
}, [
|
||||||
|
["label", { "class" : "jp-check-label", "style" : "flex:1; cursor:pointer;" }, [
|
||||||
|
["input", {
|
||||||
|
"type" : "checkbox",
|
||||||
|
"value" : crit,
|
||||||
|
...(is_checked ? { "checked" : "checked" } : {}),
|
||||||
|
"onchange" : (chk) => {
|
||||||
|
const row_parent = chk.closest(".jp-criterion-row");
|
||||||
|
const input_target = row_parent ? row_parent.querySelector(".jp-weight-input") : null;
|
||||||
|
if(input_target) input_target.disabled = !chk.checked;
|
||||||
|
|
||||||
|
if(chk.checked) chosen_criteria.push(crit);
|
||||||
|
else chosen_criteria = chosen_criteria.filter(c => c !== crit);
|
||||||
|
rebuild_matrix();
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
crit
|
||||||
|
]],
|
||||||
|
["div", { "class" : "jp-crit-actions", "style" : "display:flex; align-items:center;" }, [
|
||||||
|
["small", { "style" : "color:#8b949e; font-size:0.7em;", "data-i18n" : "weight_label" }, judge_pole.i18n.get("weight_label")],
|
||||||
|
["input", {
|
||||||
|
"type" : "number",
|
||||||
|
"min" : "1",
|
||||||
|
"step" : "1",
|
||||||
|
"class" : "jp-weight-input",
|
||||||
|
"value" : current_w,
|
||||||
|
...(!is_checked ? { "disabled" : "disabled" } : {}),
|
||||||
|
"oninput" : (inp) => {
|
||||||
|
const val = parseInt(inp.value) || 1;
|
||||||
|
active_criteria_weights[crit] = Math.max(1, Math.floor(val));
|
||||||
|
rebuild_matrix();
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-btn",
|
||||||
|
"style" : "padding:0.1em 0.3em; font-size:0.65em; margin-left:0.4em;",
|
||||||
|
"title" : judge_pole.i18n.get("btn_edit"),
|
||||||
|
"onclick" : () => {
|
||||||
|
const updated_name = prompt(judge_pole.i18n.get("edit_criterion_prompt"), crit);
|
||||||
|
if(updated_name && judge_pole.db.update_criterion(crit, updated_name)){
|
||||||
|
const idx = chosen_criteria.indexOf(crit);
|
||||||
|
if(idx !== -1) chosen_criteria[idx] = updated_name.trim();
|
||||||
|
active_criteria_weights[updated_name.trim()] = active_criteria_weights[crit];
|
||||||
|
delete active_criteria_weights[crit];
|
||||||
|
render_criteria_rows();
|
||||||
|
rebuild_matrix();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "✏️"],
|
||||||
|
["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-btn-danger",
|
||||||
|
"style" : "padding:0.1em 0.3em; font-size:0.65em; margin-left:0.2em;",
|
||||||
|
"title" : judge_pole.i18n.get("btn_delete"),
|
||||||
|
"onclick" : () => {
|
||||||
|
const confirm_msg = judge_pole.i18n.get("confirm_delete_criterion", { criterion : crit });
|
||||||
|
if(confirm(confirm_msg)){
|
||||||
|
judge_pole.db.delete_criterion(crit);
|
||||||
|
chosen_criteria = chosen_criteria.filter(c => c !== crit);
|
||||||
|
delete active_criteria_weights[crit];
|
||||||
|
render_criteria_rows();
|
||||||
|
rebuild_matrix();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "✕"]
|
||||||
|
]]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
criteria_list_box.appendChild(row);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render_criteria_rows();
|
||||||
|
|
||||||
|
// --- 4. Matriz Transpuesta con Doble Medias ---
|
||||||
|
const matrix_container = judge_pole.html(["div", { "style" : "overflow-x:auto; margin-top:1em;" }])[0];
|
||||||
|
|
||||||
|
const rebuild_matrix = () => {
|
||||||
|
matrix_container.innerHTML = "";
|
||||||
|
if(!selected_p_id || !chosen_judges.length || !chosen_criteria.length) return;
|
||||||
|
|
||||||
|
const current_scores = existing_pack.scores || {};
|
||||||
|
const participating_users = db_data.users.filter(u => chosen_judges.includes(u.id));
|
||||||
|
|
||||||
|
const table = judge_pole.html(["table", { "class" : "jp-matrix-table" }, [
|
||||||
|
["thead", {}, [
|
||||||
|
["tr", {}, [
|
||||||
|
["th", { "style" : "text-align:left;" }, "Criterio (Peso)"]
|
||||||
|
].concat(participating_users.map(u => [
|
||||||
|
"th", {}, [
|
||||||
|
["div", {}, (u.type === 'ai' ? '🤖 ' : '👤 ') + u.nick],
|
||||||
|
["small", { "style" : "color:#f1e05a;" }, "(W: " + (active_judge_weights[u.id] || 20) + ")"]
|
||||||
|
]
|
||||||
|
])).concat([
|
||||||
|
["th", { "class" : "jp-summary-cell", "data-i18n" : "crit_avg_natural" }, judge_pole.i18n.get("crit_avg_natural")],
|
||||||
|
["th", { "class" : "jp-summary-cell", "style" : "color:#f1e05a;", "data-i18n" : "crit_avg_weighted" }, judge_pole.i18n.get("crit_avg_weighted")]
|
||||||
|
])]
|
||||||
|
]],
|
||||||
|
["tbody", {}],
|
||||||
|
["tfoot", {}, [
|
||||||
|
// Tupla 1: Media Natural de Jueces
|
||||||
|
["tr", { "class" : "jp-summary-row" }, [
|
||||||
|
["td", { "class" : "jp-crit-cell", "data-i18n" : "judge_avg_natural" }, judge_pole.i18n.get("judge_avg_natural")]
|
||||||
|
].concat(participating_users.map(u => ["td", {
|
||||||
|
"class" : "jp-summary-cell",
|
||||||
|
"data-judge-nat" : u.id
|
||||||
|
}, "0.00"])).concat([
|
||||||
|
["td", { "class" : "jp-summary-cell", "data-total-nat" : "true" }, "0.00"],
|
||||||
|
["td", { "class" : "jp-summary-cell", "style" : "background:#0d1117;" }, "-"]
|
||||||
|
])],
|
||||||
|
// Tupla 2: Media Ponderada de Jueces (según peso de criterios)
|
||||||
|
["tr", { "class" : "jp-summary-row" }, [
|
||||||
|
["td", { "class" : "jp-crit-cell", "style" : "color:#f1e05a;", "data-i18n" : "judge_avg_weighted" }, judge_pole.i18n.get("judge_avg_weighted")]
|
||||||
|
].concat(participating_users.map(u => ["td", {
|
||||||
|
"class" : "jp-summary-cell",
|
||||||
|
"style" : "color:#f1e05a;",
|
||||||
|
"data-judge-pond" : u.id
|
||||||
|
}, "0.00"])).concat([
|
||||||
|
["td", { "class" : "jp-summary-cell", "style" : "background:#0d1117;" }, "-"],
|
||||||
|
["td", { "class" : "jp-summary-cell", "style" : "color:#f1e05a;", "data-total-pond" : "true" }, "0.00"]
|
||||||
|
])]
|
||||||
|
]]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
const tbody = table.querySelector("tbody");
|
||||||
|
|
||||||
|
chosen_criteria.forEach(crit => {
|
||||||
|
const c_w = active_criteria_weights[crit] || 20;
|
||||||
|
|
||||||
|
const row_cells = [
|
||||||
|
["td", { "class" : "jp-crit-cell" }, [
|
||||||
|
["span", {}, crit + " "],
|
||||||
|
["small", { "style" : "color:#f1e05a;" }, "(W: " + c_w + ")"]
|
||||||
|
]]
|
||||||
|
];
|
||||||
|
|
||||||
|
participating_users.forEach(user => {
|
||||||
|
const saved_score = (current_scores[user.id] && current_scores[user.id][crit] !== undefined)
|
||||||
|
? current_scores[user.id][crit]
|
||||||
|
: 5;
|
||||||
|
|
||||||
|
row_cells.push(["td", {}, [
|
||||||
|
["input", {
|
||||||
|
"type" : "number",
|
||||||
|
"min" : "0",
|
||||||
|
"max" : "10",
|
||||||
|
"step" : "0.5",
|
||||||
|
"value" : saved_score,
|
||||||
|
"class" : "jp-score-input",
|
||||||
|
"data-judge" : user.id,
|
||||||
|
"data-crit" : crit,
|
||||||
|
"oninput" : () => calculate_live_averages(table)
|
||||||
|
}]
|
||||||
|
]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2 Columnas de media por criterio: Natural y Ponderada (por peso de juez)
|
||||||
|
row_cells.push(["td", { "class" : "jp-summary-subcell", "data-crit-nat" : crit }, "0.00"]);
|
||||||
|
row_cells.push(["td", { "class" : "jp-summary-subcell", "style" : "color:#f1e05a;", "data-crit-pond" : crit }, "0.00"]);
|
||||||
|
|
||||||
|
tbody.appendChild(judge_pole.html(["tr", {}, row_cells])[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
matrix_container.appendChild(table);
|
||||||
|
calculate_live_averages(table);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculate_live_averages = (table_element) => {
|
||||||
|
const inputs = table_element.querySelectorAll(".jp-score-input");
|
||||||
|
|
||||||
|
const crit_data = {};
|
||||||
|
const judge_data = {};
|
||||||
|
let overall_nat_sum = 0, overall_nat_count = 0;
|
||||||
|
let overall_pond_sum = 0, overall_pond_mass = 0;
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
const judge = input.getAttribute("data-judge");
|
||||||
|
const crit = input.getAttribute("data-crit");
|
||||||
|
const val = parseFloat(input.value) || 0;
|
||||||
|
|
||||||
|
const j_w = active_judge_weights[judge] || 20;
|
||||||
|
const c_w = active_criteria_weights[crit] || 20;
|
||||||
|
|
||||||
|
// Acumuladores de Criterio
|
||||||
|
if(!crit_data[crit]) crit_data[crit] = { nat_sum : 0, count : 0, pond_sum : 0, j_weight_sum : 0 };
|
||||||
|
crit_data[crit].nat_sum += val;
|
||||||
|
crit_data[crit].count++;
|
||||||
|
crit_data[crit].pond_sum += (val * j_w);
|
||||||
|
crit_data[crit].j_weight_sum += j_w;
|
||||||
|
|
||||||
|
// Acumuladores de Juez
|
||||||
|
if(!judge_data[judge]) judge_data[judge] = { nat_sum : 0, count : 0, pond_sum : 0, c_weight_sum : 0 };
|
||||||
|
judge_data[judge].nat_sum += val;
|
||||||
|
judge_data[judge].count++;
|
||||||
|
judge_data[judge].pond_sum += (val * c_w);
|
||||||
|
judge_data[judge].c_weight_sum += c_w;
|
||||||
|
|
||||||
|
// Totales globales
|
||||||
|
overall_nat_sum += val;
|
||||||
|
overall_nat_count++;
|
||||||
|
|
||||||
|
const joint_weight = j_w * c_w;
|
||||||
|
overall_pond_sum += (val * joint_weight);
|
||||||
|
overall_pond_mass += joint_weight;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actualizar columnas por criterio
|
||||||
|
Object.keys(crit_data).forEach(crit => {
|
||||||
|
const d = crit_data[crit];
|
||||||
|
const nat_cell = table_element.querySelector(`[data-crit-nat="${crit}"]`);
|
||||||
|
const pond_cell = table_element.querySelector(`[data-crit-pond="${crit}"]`);
|
||||||
|
if(nat_cell) nat_cell.textContent = (d.nat_sum / (d.count || 1)).toFixed(2);
|
||||||
|
if(pond_cell) pond_cell.textContent = (d.pond_sum / (d.j_weight_sum || 1)).toFixed(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actualizar tuplas por juez
|
||||||
|
Object.keys(judge_data).forEach(judge => {
|
||||||
|
const d = judge_data[judge];
|
||||||
|
const nat_cell = table_element.querySelector(`[data-judge-nat="${judge}"]`);
|
||||||
|
const pond_cell = table_element.querySelector(`[data-judge-pond="${judge}"]`);
|
||||||
|
if(nat_cell) nat_cell.textContent = (d.nat_sum / (d.count || 1)).toFixed(2);
|
||||||
|
if(pond_cell) pond_cell.textContent = (d.pond_sum / (d.c_weight_sum || 1)).toFixed(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Esquinas globales
|
||||||
|
const total_nat_cell = table_element.querySelector(`[data-total-nat="true"]`);
|
||||||
|
const total_pond_cell = table_element.querySelector(`[data-total-pond="true"]`);
|
||||||
|
if(total_nat_cell) total_nat_cell.textContent = (overall_nat_sum / (overall_nat_count || 1)).toFixed(2);
|
||||||
|
if(total_pond_cell) total_pond_cell.textContent = (overall_pond_sum / (overall_pond_mass || 1)).toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = judge_pole.html(["div", { "style" : "display:flex; justify-content:flex-end; gap:0.5em; margin-top:1em;" }, [
|
||||||
|
["button", { "type" : "button", "class" : "jp-btn", "onclick" : () => close_modal() }, judge_pole.i18n.get("btn_cancel_edit")],
|
||||||
|
["button", {
|
||||||
|
"type" : "button",
|
||||||
|
"class" : "jp-btn-primary",
|
||||||
|
"data-i18n" : "btn_save_scores",
|
||||||
|
"onclick" : () => {
|
||||||
|
const scores = {};
|
||||||
|
matrix_container.querySelectorAll(".jp-score-input").forEach(input => {
|
||||||
|
const j_id = input.getAttribute("data-judge");
|
||||||
|
const c_name = input.getAttribute("data-crit");
|
||||||
|
const val = parseFloat(input.value) || 0;
|
||||||
|
if(!scores[j_id]) scores[j_id] = {};
|
||||||
|
scores[j_id][c_name] = Math.max(0, Math.min(10, val));
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
scores : scores,
|
||||||
|
judge_weights : active_judge_weights,
|
||||||
|
criteria_weights : active_criteria_weights
|
||||||
|
};
|
||||||
|
|
||||||
|
judge_pole.db.save_votes(selected_p_id, payload);
|
||||||
|
close_modal();
|
||||||
|
render_rankings();
|
||||||
|
}
|
||||||
|
}, judge_pole.i18n.get("btn_save_scores")]
|
||||||
|
]])[0];
|
||||||
|
|
||||||
|
modal_body.appendChild(project_select);
|
||||||
|
modal_body.appendChild(judges_picker);
|
||||||
|
modal_body.appendChild(criteria_picker);
|
||||||
|
modal_body.appendChild(matrix_container);
|
||||||
|
modal_body.appendChild(actions);
|
||||||
|
|
||||||
|
rebuild_matrix();
|
||||||
|
|
||||||
|
judge_pole.item_self.appendChild(modal_overlay);
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return VotesView;
|
||||||
|
})();
|
||||||
47
Public/index.html
Normal file
47
Public/index.html
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<title data-i18n="judge_pole">JudgePole</title>
|
||||||
|
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
|
|
||||||
|
<meta name="xdoc:link" content="https://judgepole.k3y.pw/" />
|
||||||
|
<meta name="xdoc:git" content="https://git.k3y.pw/KyMAN/JudgePole" />
|
||||||
|
<meta name="xdoc:authors" content="KyMAN" />
|
||||||
|
<meta name="xdoc:ai-authors" content="Gemini" />
|
||||||
|
<meta name="xdoc:since" content="20260919" />
|
||||||
|
<meta name="xdoc:version" content="20260920" />
|
||||||
|
<meta name="xdoc:license" content=" Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International" />
|
||||||
|
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./data/database.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./data/i18n.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./data/settings.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./data/styles.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Utils/Styles.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Utils/Common.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Utils/I18N.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Utils/Settings.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Managers/DatabaseManager.ecma.js" charset="utf-8"></script>
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Managers/SettingsManager.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Managers/I18NManager.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||||
|
<script src="./ecma/Managers/SessionManager.ecma.js"></script>
|
||||||
|
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Views/UsersView.ecma.js" charset="utf-8"></script>
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Views/ProjectsView.ecma.js" charset="utf-8"></script>
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Views/VotesView.ecma.js" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" src="./ecma/Application/JudgePole.ecma.js" charset="utf-8" data-crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<script data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" charset="utf-8">
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const judge_pole = new JudgePole();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
||||||
13
Public/md/ai-dev-rules.md
Normal file
13
Public/md/ai-dev-rules.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
El proyecto tiene un raíz privado con los archivos `LICENSE`, `README.md` y `.gitignore`; luego tiene un lado público el cual es `/Public`. Dentro de éste un `index.html` y los directorios `data` y `ecma`. En el directorio `data` se meterá todo lo que no se meta en el repositorio de Git, como configuraciones personalizadas, la base de datos, etc. Mientras que los archivos de Script se meten dentro de `ecma` el cual tiene la estructura `Application` para los archivos que funcionen sobre el proyecto de forma diferente o independiente; `Utils` para archivos totalmente autónomos; `Managers` para gestores como `SettingsManager` o `I18NManager`; `Views` para las vistas; y `Models` para los modelos si hicieren falta.
|
||||||
|
|
||||||
|
Las reglas de desarrollo son:
|
||||||
|
|
||||||
|
- La sangría es de 4 espacios.
|
||||||
|
- Las variables y nombres de funciones, métodos y atributos son Snake Lower Case; el nombre de las clases es en Pascal Case; las constantes de clase o constantes sueltas estarán en Snake Upper Case.
|
||||||
|
- El GUI HTML se basa en un sistema que simula DPIs a partir de la unidad de medida `em`, el cual divide el lado más estrecho del GUI en 40 celdas por defecto. `1em` es equivalente a una celda. Cada celda es cuadrada.
|
||||||
|
- Todas las clases que se generen han de ser Funciones Constructoras. Los métodos y atributos estáticos tienen que ir vinculados al nombre sin `prototype`.
|
||||||
|
- Todo valor susceptible de ser de configuración ha de estar integrado en el archivo `/Public/ecma/Utils/Settings.ecma.js`.
|
||||||
|
- En HTML, todos los elementos que contengan textos internacionalizados han de llevar el atributo `data-i18n` con la clave del texto, y en caso de ser campos sin contenido como `input`, `textarea`, etc. Han de llevar a mayores el atributo `data-i18n-without="true"` para indicar que no se añada la traducción a su contenido, sólo a los atributos, si los tuviere, como `title`, `placeholder`, etc.
|
||||||
|
- Las vistas estará en el directorio `/Public/ecma/Views` y tendrán como nombre `NombreDeVistaView.ecma.js` y nombre de clase función constructora `NombreDeVistaView` y se implementarán en el objeto `judge_pole.views` con el nombre `nombre_de_vista`. Cada vista ha de tener un método `go` para construir y visualizar dicha vista cada vez que se realiza la acción de ir a dicha vista. Cada texto que se muestre tiene que recogerse del método `judge_pole.i18n.get` y sus claves y valores en el archivo `/Public/ecma/Utils/I18N.ecma.js` en cada uno de los idiomas deseados.
|
||||||
|
- Cualquier HTML que se haga, salvo que no se pueda realizar por dicho método, ha de estar hecho con el método `judge_pole.html`.
|
||||||
|
- Los estilos han de agregarse por bloques que se deseen al archivo `/Public/ecma/Utils/Styles.ecma.js` en formato del método `Common.SCSS`.
|
||||||
145
README.md
145
README.md
@ -1,3 +1,144 @@
|
|||||||
# JudgePole
|
# ⚖️ Judge Pole - Manual de Usuario y Arquitectura
|
||||||
|
|
||||||
|
**Judge Pole** es una aplicación web *serverless*, ligera y portátil diseñada para arbitrar y evaluar proyectos durante "vibe coding" jams, hackathons y competiciones de desarrollo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 1. Filosofía y Arquitectura Técnica
|
||||||
|
|
||||||
|
* **Cero Infraestructura**: No requiere backend, bases de datos SQL/NoSQL ni servicios en la nube.
|
||||||
|
* **Persistencia Portátil (Base64)**: Todo el estado de la aplicación reside en `/Public/data/database.ecma.js` codificado en una cadena Base64 que contiene un JSON plano.
|
||||||
|
* **Construcción DOM Nativa**: Construida íntegramente con Vanilla JS mediante el helper `judge_pole.html()`, evitando frameworks pesados y virtual DOMs.
|
||||||
|
* **Sistema de Virtual DPI (Grid de 40 celdas)**: El diseño escala de manera uniforme adaptando la unidad base `em` al tamaño de la pantalla mediante un cálculo dinámico de `fontSize`.
|
||||||
|
* **Internacionalización Dinámica**: Soporte reactivo multilingüe basado en atributos `data-i18n` y `data-i18n-without="true"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 2. Estructura del Proyecto
|
||||||
|
|
||||||
|
```text
|
||||||
|
judge-pole/
|
||||||
|
├── Dockerfile # Entorno de compilación SASS
|
||||||
|
├── build.sh / build.bat # Scripts de compilación de estilos
|
||||||
|
└── Public/
|
||||||
|
├── index.html # Punto de entrada HTML
|
||||||
|
├── data/
|
||||||
|
│ └── database.ecma.js # Archivo físico de la BDD en Base64
|
||||||
|
└── ecma/
|
||||||
|
├── Application/
|
||||||
|
│ └── JudgePole.ecma.js # Orquestador del ciclo de vida y UI
|
||||||
|
├── Managers/
|
||||||
|
│ ├── DatabaseManager.ecma.js # Operaciones CRUD y serialización
|
||||||
|
│ ├── I18NManager.ecma.js # Motor de traducción
|
||||||
|
│ ├── SessionManager.ecma.js # Control de acceso y roles
|
||||||
|
│ └── SettingsManager.ecma.js # Ajustes de visualización y FPS
|
||||||
|
├── Models/ # Definiciones de datos
|
||||||
|
├── Utils/
|
||||||
|
│ ├── Common.ecma.js # Inyección de estilos y helpers
|
||||||
|
│ ├── I18N.ecma.js # Diccionarios de idiomas
|
||||||
|
│ └── Styles.ecma.js # Estilos SCSS en JavaScript
|
||||||
|
└── Views/
|
||||||
|
├── UsersView.ecma.js # Gestión y filtrado de jueces/creadores
|
||||||
|
├── ProjectsView.ecma.js # Gestión y autoría de proyectos
|
||||||
|
└── VotesView.ecma.js # Matriz de notas ponderada y ranking
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 3. Gestión de Participantes (Usuarios)
|
||||||
|
|
||||||
|
La vista de **Usuarios** permite registrar tanto jueces como participantes:
|
||||||
|
|
||||||
|
* **Tipos de Usuario**:
|
||||||
|
* **Humano (`👤`)**: Por defecto.
|
||||||
|
* **Inteligencia Artificial (`🤖`)**: Modelos LLM o agentes participantes.
|
||||||
|
|
||||||
|
|
||||||
|
* **Avatar Adaptativo**: Si no se define una URL o ruta local, se genera automáticamente un placeholder centrado con las iniciales o el icono de tipo.
|
||||||
|
* **Buscador Reactivo**: Filtra en tiempo real por nick, biografía o plataformas vinculadas.
|
||||||
|
* **Seguridad / Contraseña**:
|
||||||
|
* Puedes definir una contraseña opcional en cualquier participante.
|
||||||
|
* En cuanto al menos un usuario tenga contraseña, la web bloqueará la edición pública.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 4. Gestión de Proyectos
|
||||||
|
|
||||||
|
La vista de **Proyectos** permite administrar los trabajos presentados a la Jam:
|
||||||
|
|
||||||
|
* **Coautoría Múltiple**: Los proyectos pueden pertenecer a uno o varios creadores simultáneamente (equipos humanos, equipos de IAs o formatos híbridos).
|
||||||
|
* **Selector con Scroll y Filtro**: Al crear o editar un proyecto, la lista de autores cuenta con un buscador en vivo y altura restringida para no desbordar el formulario.
|
||||||
|
* **Metadatos y Enlaces**: Soporta etiquetas (#tags), tecnologías utilizadas y enlaces externos (repositorios, demos, webs).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚖️ 5. Tribunal de Evaluación y Ranking
|
||||||
|
|
||||||
|
La vista de **Votaciones** es el núcleo de la aplicación:
|
||||||
|
|
||||||
|
### 5.1. Ranking en Vivo
|
||||||
|
|
||||||
|
* **Posición Doble**: Muestra la posición **Global** (absoluta en la Jam) y la posición en el **Filtro** (según la búsqueda tecleada).
|
||||||
|
* **Criterio de Orden**: Ordenado oficialmente por la **Media Ponderada**.
|
||||||
|
* **Acordeón Desplegable**: Al hacer clic sobre cualquier fila del ranking, se despliegan:
|
||||||
|
* El resumen de medias naturales y ponderadas de cada criterio.
|
||||||
|
* La **Matriz Completa de Solo Lectura**, visible para cualquier visitante.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 5.2. Matriz de Puntuaciones Transpuesta
|
||||||
|
|
||||||
|
Al pulsar en **"+ Registrar Votación"** o al **Editar (✏️)**:
|
||||||
|
|
||||||
|
* **Estructura Transpuesta**:
|
||||||
|
* **Filas (Tuplas)**: Criterios de evaluación.
|
||||||
|
* **Columnas**: Jueces participantes.
|
||||||
|
* **Columnas Finales**: Media Natural y Media Ponderada por Criterio.
|
||||||
|
* **Filas Inferiores (`tfoot`)**: Media Natural y Media Ponderada por Juez.
|
||||||
|
* **Esquina Inferior Derecha**: Media Global Doblemente Ponderada.
|
||||||
|
|
||||||
|
|
||||||
|
* **Cálculo de Pesos (Weights)**:
|
||||||
|
* Tanto jueces como criterios admiten un **Peso entero** (por defecto `20`, mínimo `1`).
|
||||||
|
* Las medias se recalculan automáticamente en el cliente a medida que se teclean notas o se modifican los pesos.
|
||||||
|
|
||||||
|
|
||||||
|
* **Gestión de Criterios en Caliente**:
|
||||||
|
* Es posible añadir nuevos criterios con `+ Nuevo Criterio`.
|
||||||
|
* Los criterios existentes pueden renombrarse (✏️) o eliminarse (✕), propagándose el cambio en cascada sobre todas las notas históricas guardadas.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 6. Control de Acceso (Modo Admin vs. Público)
|
||||||
|
|
||||||
|
El sistema opera bajo dos estados dinámicos gestionados por `SessionManager`:
|
||||||
|
|
||||||
|
1. **Modo Libre / Desarrollo**:
|
||||||
|
* Si ningún usuario de la base de datos tiene contraseña asignada, la web asume que estás en local y muestra todas las opciones administrativas (`+ Añadir`, `✏️ Editar`, `✕ Borrar`, exportar Base64).
|
||||||
|
|
||||||
|
|
||||||
|
2. **Modo Protegido (Visitante)**:
|
||||||
|
* Se activa en cuanto un usuario recibe una contraseña.
|
||||||
|
* La interfaz oculta todos los botones de creación, modificación y borrado.
|
||||||
|
* La cabecera muestra el botón **"Acceder 🔐"**.
|
||||||
|
* Al iniciar sesión con nick y clave, la sesión pasa a ser administradora y se restauran todos los botones de control. Al pulsar **"Salir 🚪"**, vuelve al modo protegido.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💾 7. Flujo de Guardado y Persistencia
|
||||||
|
|
||||||
|
Al ser una aplicación sin servidor backend, los cambios realizados en el navegador se almacenan en la memoria local activa. Para hacerlos permanentes:
|
||||||
|
|
||||||
|
1. Inicia sesión como administrador.
|
||||||
|
2. Pulsa en el botón **"Base64 DB"** en la barra superior.
|
||||||
|
3. Copia el contenido generado (`const database = "...";`).
|
||||||
|
4. Abre el archivo `/Public/data/database.ecma.js` en tu editor y sobreescribe su contenido.
|
||||||
|
5. Haz commit o despliega los cambios en tu servidor estático (GitHub Pages, Vercel, Netlify o servidor local).
|
||||||
|
|
||||||
Jam project for Jams valorations.
|
|
||||||
Loading…
Reference in New Issue
Block a user