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

373 lines
18 KiB
JavaScript

"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;
})();