352 lines
13 KiB
JavaScript
352 lines
13 KiB
JavaScript
const PID_LINK_DEFAULT_ICON =
|
|
'<svg class="lf-card__icon lf-card__icon--default" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1"/><path d="M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg>';
|
|
|
|
const PID_LINK_ICON_MAX_BYTES = 2 * 1024 * 1024;
|
|
|
|
async function pidFetchLinks() {
|
|
return (await pidApiRequest("/links-ferramentas/")) || [];
|
|
}
|
|
|
|
async function pidCreateLink(formData) {
|
|
return pidApiRequest("/links-ferramentas/", { method: "POST", body: formData });
|
|
}
|
|
|
|
async function pidUpdateLinkOrdem(id, ordem) {
|
|
return pidApiRequest(`/links-ferramentas/${id}/`, { method: "PATCH", body: { ordem } });
|
|
}
|
|
|
|
async function pidUpdateLink(id, formData) {
|
|
return pidApiRequest(`/links-ferramentas/${id}/`, { method: "PATCH", body: formData });
|
|
}
|
|
|
|
async function pidDeleteLink(id) {
|
|
return pidApiRequest(`/links-ferramentas/${id}/`, { method: "DELETE" });
|
|
}
|
|
|
|
async function pidFetchLinkFavoritos() {
|
|
const list = await pidApiRequest("/links-ferramentas-favoritos/");
|
|
return (list || []).map((f) => f.link);
|
|
}
|
|
|
|
async function pidAddLinkFavorito(linkId) {
|
|
return pidApiRequest("/links-ferramentas-favoritos/", { method: "POST", body: { link: linkId } });
|
|
}
|
|
|
|
async function pidRemoveLinkFavorito(linkId) {
|
|
return pidApiRequest(`/links-ferramentas-favoritos/${linkId}/`, { method: "DELETE" });
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", async () => {
|
|
const grid = document.getElementById("lf-grid");
|
|
if (!grid) return;
|
|
|
|
const me = await pidGetMe();
|
|
if (!me) return;
|
|
|
|
const linkPerms = (me.permissoes_efetivas && me.permissoes_efetivas["links-ferramentas"]) || {};
|
|
const canView = !!(linkPerms.apps && linkPerms.apps["links-ferramentas-visualizar"]);
|
|
const canManage = !!(linkPerms.apps && linkPerms.apps["links-ferramentas-editar"]);
|
|
|
|
const noAccessEl = document.getElementById("lf-no-access");
|
|
const contentEl = document.getElementById("lf-content");
|
|
if (noAccessEl) noAccessEl.hidden = canView;
|
|
if (contentEl) contentEl.hidden = !canView;
|
|
if (!canView) return;
|
|
|
|
const emptyEl = document.getElementById("lf-empty");
|
|
const addBtn = document.getElementById("lf-add-btn");
|
|
const modal = document.getElementById("lf-add-modal");
|
|
const formTitle = document.getElementById("lf-form-title");
|
|
const formNome = document.getElementById("lf-form-nome");
|
|
const formUrl = document.getElementById("lf-form-url");
|
|
const formIcone = document.getElementById("lf-form-icone");
|
|
const formIconeLabel = document.getElementById("lf-form-icone-label");
|
|
const formError = document.getElementById("lf-form-error");
|
|
const saveBtn = document.getElementById("lf-form-save-btn");
|
|
const cancelBtn = document.getElementById("lf-form-cancel-btn");
|
|
|
|
if (addBtn) addBtn.hidden = !canManage;
|
|
|
|
let rawLinks = [];
|
|
let links = [];
|
|
let favoritos = new Set();
|
|
let editingId = null;
|
|
|
|
function sortFavoritesFirst(list) {
|
|
const fav = [];
|
|
const rest = [];
|
|
list.forEach((link) => (favoritos.has(link.id) ? fav : rest).push(link));
|
|
return fav.concat(rest);
|
|
}
|
|
|
|
function cardHtml(link, index) {
|
|
const iconHtml = link.icone ? `<img class="lf-card__icon" src="${link.icone}" alt="" />` : PID_LINK_DEFAULT_ICON;
|
|
const isFavorite = favoritos.has(link.id);
|
|
|
|
const favoriteBtn = `
|
|
<button type="button" class="lf-card__favorite${isFavorite ? " is-active" : ""}" data-link-favorite="${link.id}" aria-label="${isFavorite ? "Remover dos favoritos" : "Adicionar aos favoritos"}" aria-pressed="${isFavorite}">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="${isFavorite ? "currentColor" : "none"}" stroke="currentColor" stroke-width="1.8"><path d="M12 3l2.9 6.2 6.8.7-5 4.7 1.4 6.7L12 17.9 5.9 21.3l1.4-6.7-5-4.7 6.8-.7z" stroke-linejoin="round"/></svg>
|
|
</button>`;
|
|
|
|
const adminHtml = canManage
|
|
? `
|
|
<button type="button" class="lf-card__move" data-link-move-up="${link.id}" ${index === 0 ? "disabled" : ""} aria-label="Mover para cima">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 15l-6-6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
</button>
|
|
<button type="button" class="lf-card__move" data-link-move-down="${link.id}" ${index === links.length - 1 ? "disabled" : ""} aria-label="Mover para baixo">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
</button>
|
|
<button type="button" class="lf-card__move" data-link-edit="${link.id}" aria-label="Editar link">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9" stroke-linecap="round"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z" stroke-linejoin="round"/></svg>
|
|
</button>
|
|
<button type="button" class="lf-card__remove" data-link-remove="${link.id}" aria-label="Remover link">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
|
|
</button>`
|
|
: "";
|
|
|
|
return `
|
|
<article class="lf-card${canManage ? " lf-card--draggable" : ""}" data-link-id="${link.id}" ${canManage ? 'draggable="true"' : ""}>
|
|
<div class="lf-card__admin">
|
|
${favoriteBtn}
|
|
${adminHtml}
|
|
</div>
|
|
<a class="lf-card__link" href="${link.url}" target="_blank" rel="noopener noreferrer" draggable="false">
|
|
${iconHtml}
|
|
<span class="lf-card__label">${link.nome}</span>
|
|
</a>
|
|
</article>`;
|
|
}
|
|
|
|
function render() {
|
|
grid.querySelectorAll(".lf-card").forEach((el) => el.remove());
|
|
const cardsHtml = links.map((link, index) => cardHtml(link, index)).join("");
|
|
|
|
if (emptyEl) {
|
|
emptyEl.insertAdjacentHTML("afterend", cardsHtml);
|
|
emptyEl.hidden = links.length !== 0;
|
|
} else {
|
|
grid.insertAdjacentHTML("beforeend", cardsHtml);
|
|
}
|
|
}
|
|
|
|
async function refresh() {
|
|
try {
|
|
const [fetchedLinks, favoriteIds] = await Promise.all([pidFetchLinks(), pidFetchLinkFavoritos()]);
|
|
rawLinks = fetchedLinks;
|
|
favoritos = new Set(favoriteIds);
|
|
} catch (e) {
|
|
rawLinks = [];
|
|
favoritos = new Set();
|
|
}
|
|
links = sortFavoritesFirst(rawLinks);
|
|
render();
|
|
}
|
|
|
|
function openModal(link) {
|
|
editingId = link ? link.id : null;
|
|
formNome.value = link ? link.nome : "";
|
|
formUrl.value = link ? link.url : "";
|
|
formIcone.value = "";
|
|
formError.textContent = "";
|
|
formTitle.textContent = link ? "Editar Link" : "Adicionar Link";
|
|
formIconeLabel.textContent = link
|
|
? "Ícone (deixe em branco para manter o atual, até 2MB)"
|
|
: "Ícone (opcional, até 2MB)";
|
|
saveBtn.textContent = link ? "Salvar" : "Adicionar";
|
|
modal.hidden = false;
|
|
formNome.focus();
|
|
}
|
|
|
|
function closeModal() {
|
|
modal.hidden = true;
|
|
editingId = null;
|
|
}
|
|
|
|
async function swapOrdem(a, b) {
|
|
await Promise.all([pidUpdateLinkOrdem(a.id, b.ordem), pidUpdateLinkOrdem(b.id, a.ordem)]);
|
|
await refresh();
|
|
}
|
|
|
|
if (addBtn) {
|
|
addBtn.addEventListener("click", () => openModal());
|
|
}
|
|
|
|
if (formIcone) {
|
|
formIcone.addEventListener("change", () => {
|
|
const file = formIcone.files[0];
|
|
if (file && file.size > PID_LINK_ICON_MAX_BYTES) {
|
|
formError.textContent = "O ícone deve ter no máximo 2MB.";
|
|
formIcone.value = "";
|
|
} else {
|
|
formError.textContent = "";
|
|
}
|
|
});
|
|
}
|
|
|
|
if (cancelBtn) cancelBtn.addEventListener("click", closeModal);
|
|
|
|
if (modal) {
|
|
modal.addEventListener("click", (event) => {
|
|
if (event.target === modal) closeModal();
|
|
});
|
|
}
|
|
|
|
if (saveBtn) {
|
|
saveBtn.addEventListener("click", async () => {
|
|
const nome = formNome.value.trim();
|
|
const url = formUrl.value.trim();
|
|
if (!nome || !url) {
|
|
formError.textContent = "Informe o nome e o link.";
|
|
return;
|
|
}
|
|
if (formIcone.files[0] && formIcone.files[0].size > PID_LINK_ICON_MAX_BYTES) {
|
|
formError.textContent = "O ícone deve ter no máximo 2MB.";
|
|
return;
|
|
}
|
|
formError.textContent = "";
|
|
|
|
const formData = new FormData();
|
|
formData.append("nome", nome);
|
|
formData.append("url", url);
|
|
if (formIcone.files[0]) formData.append("icone", formIcone.files[0]);
|
|
|
|
try {
|
|
if (editingId) {
|
|
await pidUpdateLink(editingId, formData);
|
|
} else {
|
|
await pidCreateLink(formData);
|
|
}
|
|
closeModal();
|
|
await refresh();
|
|
} catch (e) {
|
|
formError.textContent = e.message;
|
|
}
|
|
});
|
|
}
|
|
|
|
grid.addEventListener("click", async (event) => {
|
|
const favoriteBtn = event.target.closest("[data-link-favorite]");
|
|
if (favoriteBtn) {
|
|
const id = Number(favoriteBtn.getAttribute("data-link-favorite"));
|
|
try {
|
|
if (favoritos.has(id)) {
|
|
await pidRemoveLinkFavorito(id);
|
|
favoritos.delete(id);
|
|
} else {
|
|
await pidAddLinkFavorito(id);
|
|
favoritos.add(id);
|
|
}
|
|
links = sortFavoritesFirst(rawLinks);
|
|
render();
|
|
} catch (e) {
|
|
window.alert(e.message);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const editBtn = event.target.closest("[data-link-edit]");
|
|
if (editBtn) {
|
|
const id = Number(editBtn.getAttribute("data-link-edit"));
|
|
const link = links.find((l) => l.id === id);
|
|
if (link) openModal(link);
|
|
return;
|
|
}
|
|
|
|
const removeBtn = event.target.closest("[data-link-remove]");
|
|
if (removeBtn) {
|
|
const id = Number(removeBtn.getAttribute("data-link-remove"));
|
|
const link = links.find((l) => l.id === id);
|
|
if (link && window.confirm(`Remover o link "${link.nome}"?`)) {
|
|
await pidDeleteLink(id);
|
|
await refresh();
|
|
}
|
|
return;
|
|
}
|
|
|
|
const upBtn = event.target.closest("[data-link-move-up]");
|
|
if (upBtn) {
|
|
const id = Number(upBtn.getAttribute("data-link-move-up"));
|
|
const index = links.findIndex((l) => l.id === id);
|
|
if (index > 0) await swapOrdem(links[index], links[index - 1]);
|
|
return;
|
|
}
|
|
|
|
const downBtn = event.target.closest("[data-link-move-down]");
|
|
if (downBtn) {
|
|
const id = Number(downBtn.getAttribute("data-link-move-down"));
|
|
const index = links.findIndex((l) => l.id === id);
|
|
if (index !== -1 && index < links.length - 1) await swapOrdem(links[index], links[index + 1]);
|
|
}
|
|
});
|
|
|
|
let draggedId = null;
|
|
let dropTargetId = null;
|
|
let dropBefore = true;
|
|
|
|
function clearDropHighlight() {
|
|
grid.querySelectorAll(".lf-card--drop-before, .lf-card--drop-after").forEach((el) => {
|
|
el.classList.remove("lf-card--drop-before", "lf-card--drop-after");
|
|
});
|
|
}
|
|
|
|
grid.addEventListener("dragstart", (event) => {
|
|
const card = event.target.closest(".lf-card--draggable");
|
|
if (!card) return;
|
|
draggedId = Number(card.dataset.linkId);
|
|
event.dataTransfer.effectAllowed = "move";
|
|
event.dataTransfer.setData("text/plain", String(draggedId));
|
|
requestAnimationFrame(() => card.classList.add("lf-card--dragging"));
|
|
});
|
|
|
|
grid.addEventListener("dragover", (event) => {
|
|
if (draggedId === null) return;
|
|
const card = event.target.closest(".lf-card--draggable");
|
|
if (!card || Number(card.dataset.linkId) === draggedId) return;
|
|
event.preventDefault();
|
|
const rect = card.getBoundingClientRect();
|
|
dropBefore = event.clientX < rect.left + rect.width / 2;
|
|
dropTargetId = Number(card.dataset.linkId);
|
|
clearDropHighlight();
|
|
card.classList.add(dropBefore ? "lf-card--drop-before" : "lf-card--drop-after");
|
|
});
|
|
|
|
grid.addEventListener("drop", async (event) => {
|
|
if (draggedId === null) return;
|
|
event.preventDefault();
|
|
clearDropHighlight();
|
|
const fromId = draggedId;
|
|
const toId = dropTargetId;
|
|
draggedId = null;
|
|
dropTargetId = null;
|
|
if (toId === null || fromId === toId) return;
|
|
|
|
const fromIndex = links.findIndex((l) => l.id === fromId);
|
|
if (fromIndex === -1) return;
|
|
const reordered = links.slice();
|
|
const [moved] = reordered.splice(fromIndex, 1);
|
|
let insertIndex = reordered.findIndex((l) => l.id === toId);
|
|
if (insertIndex === -1) return;
|
|
if (!dropBefore) insertIndex += 1;
|
|
reordered.splice(insertIndex, 0, moved);
|
|
|
|
const updates = reordered
|
|
.map((link, index) => ({ link, index }))
|
|
.filter(({ link, index }) => link.ordem !== index);
|
|
if (!updates.length) return;
|
|
|
|
links = reordered;
|
|
render();
|
|
await Promise.all(updates.map(({ link, index }) => pidUpdateLinkOrdem(link.id, index)));
|
|
await refresh();
|
|
});
|
|
|
|
grid.addEventListener("dragend", () => {
|
|
draggedId = null;
|
|
dropTargetId = null;
|
|
clearDropHighlight();
|
|
grid.querySelectorAll(".lf-card--dragging").forEach((el) => el.classList.remove("lf-card--dragging"));
|
|
});
|
|
|
|
await refresh();
|
|
});
|