373 lines
18 KiB
JavaScript
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;
|
|
})(); |