const PID_STAR_ICON =
'';
const PID_DEFAULT_APP_ICON =
'';
async function pidFetchFavorites() {
return (await pidApiRequest("/favoritos/")) || [];
}
async function pidToggleFavorite(appId, isFavorite) {
if (isFavorite) {
await pidApiRequest(`/favoritos/${encodeURIComponent(appId)}/`, { method: "DELETE" });
} else {
await pidApiRequest("/favoritos/", { method: "POST", body: { app_id: appId } });
}
}
async function pidUpdateFavoriteOrdem(appId, ordem) {
return pidApiRequest(`/favoritos/${encodeURIComponent(appId)}/`, { method: "PATCH", body: { ordem } });
}
function pidSlug(text) {
return text
.toLowerCase()
.normalize("NFD")
.replace(/[̀-ͯ]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}
function pidCollectFavoritableApps() {
const apps = [];
document.querySelectorAll(".sidebar a.nav-item, .sidebar a.nav-subitem").forEach((link) => {
const li = link.closest("li");
if (!li || li.getAttribute("data-section") === "principal") return;
const sectionLi = li.hasAttribute("data-section") ? li : li.closest("[data-section]");
if (!sectionLi) return;
const labelEl = link.querySelector(".nav-item__label");
const label = labelEl ? labelEl.textContent.trim() : link.textContent.trim();
const id = li.hasAttribute("data-section")
? li.getAttribute("data-section")
: `${sectionLi.getAttribute("data-section")}__${pidSlug(label)}`;
// Sub-itens (a.nav-subitem) não têm ícone próprio no menu — usa o ícone
// do módulo pai (o svg do próprio botão do nav-group, ex.: o lápis de
// "Utilitários") em vez de cair no ícone genérico de grade.
const iconNode = link.querySelector("svg") || sectionLi.querySelector(".nav-group__toggle svg") || null;
apps.push({
id,
label,
link,
iconNode,
});
});
return apps;
}
function pidIconHtmlFor(app) {
if (!app.iconNode) return PID_DEFAULT_APP_ICON;
const clone = app.iconNode.cloneNode(true);
clone.removeAttribute("width");
clone.removeAttribute("height");
clone.setAttribute("class", "app-card__icon");
return clone.outerHTML;
}
document.addEventListener("DOMContentLoaded", async () => {
const me = await pidGetMe();
if (!me) return;
const apps = pidCollectFavoritableApps();
const starButtons = [];
apps.forEach((app) => {
const row = document.createElement("div");
row.className = "nav-item-row";
app.link.replaceWith(row);
row.appendChild(app.link);
const starBtn = document.createElement("button");
starBtn.type = "button";
starBtn.className = "fav-toggle";
starBtn.innerHTML = PID_STAR_ICON;
row.appendChild(starBtn);
starButtons.push({ id: app.id, btn: starBtn });
starBtn.addEventListener("click", async () => {
const isFavorite = starBtn.classList.contains("is-favorite");
await pidToggleFavorite(app.id, isFavorite);
await refreshAll();
});
});
const grid = document.getElementById("app-card-grid");
const favoritesEmpty = document.getElementById("favorites-empty");
let favorites = [];
let favIds = [];
function syncStarButtons() {
starButtons.forEach(({ id, btn }) => {
const active = favIds.includes(id);
btn.classList.toggle("is-favorite", active);
btn.setAttribute("aria-pressed", String(active));
btn.setAttribute("aria-label", active ? "Remover dos favoritos" : "Adicionar aos favoritos");
});
}
function renderDashboard() {
if (!grid) return;
const favApps = favIds.map((id) => apps.find((a) => a.id === id)).filter(Boolean);
grid.querySelectorAll(".app-card").forEach((el) => el.remove());
const cardsHtml = favApps
.map(
(app) => `
${pidIconHtmlFor(app)}
${pidEscapeHtml(app.label)}
`
)
.join("");
if (favoritesEmpty) {
favoritesEmpty.insertAdjacentHTML("beforebegin", cardsHtml);
favoritesEmpty.hidden = favApps.length !== 0;
} else {
grid.insertAdjacentHTML("beforeend", cardsHtml);
}
}
async function refreshAll() {
favorites = await pidFetchFavorites();
favIds = favorites.map((f) => f.app_id);
syncStarButtons();
renderDashboard();
}
// Dispara o clique no próprio original do sidebar (ainda no DOM, só
// movido pra dentro do .nav-item-row) em vez de reimplementar a navegação
// a partir de href/target — assim um app favoritado herda automaticamente
// qualquer comportamento especial do item (ex.: "Calendário De Paula", que
// não navega mais, abre um modal com iframe via listener em sidebar.js).
function openApp(app) {
app.link.click();
}
if (grid) {
grid.addEventListener("click", async (event) => {
const removeBtn = event.target.closest("[data-fav-remove]");
if (removeBtn) {
const appId = removeBtn.getAttribute("data-fav-remove");
await pidToggleFavorite(appId, true);
await refreshAll();
return;
}
const card = event.target.closest(".app-card");
if (!card) return;
const app = apps.find((a) => a.id === card.getAttribute("data-fav-id"));
if (app) openApp(app);
});
grid.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") return;
if (event.target.closest("[data-fav-remove]")) return;
const card = event.target.closest(".app-card");
if (!card) return;
event.preventDefault();
const app = apps.find((a) => a.id === card.getAttribute("data-fav-id"));
if (app) openApp(app);
});
let draggedAppId = null;
let dropTargetAppId = null;
let dropBefore = true;
function clearDropHighlight() {
grid.querySelectorAll(".app-card--drop-before, .app-card--drop-after").forEach((el) => {
el.classList.remove("app-card--drop-before", "app-card--drop-after");
});
}
grid.addEventListener("dragstart", (event) => {
const card = event.target.closest(".app-card");
if (!card) return;
draggedAppId = card.getAttribute("data-fav-id");
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", draggedAppId);
requestAnimationFrame(() => card.classList.add("app-card--dragging"));
});
grid.addEventListener("dragover", (event) => {
if (draggedAppId === null) return;
const card = event.target.closest(".app-card");
if (!card || card.getAttribute("data-fav-id") === draggedAppId) return;
event.preventDefault();
const rect = card.getBoundingClientRect();
dropBefore = event.clientX < rect.left + rect.width / 2;
dropTargetAppId = card.getAttribute("data-fav-id");
clearDropHighlight();
card.classList.add(dropBefore ? "app-card--drop-before" : "app-card--drop-after");
});
grid.addEventListener("drop", async (event) => {
if (draggedAppId === null) return;
event.preventDefault();
clearDropHighlight();
const fromId = draggedAppId;
const toId = dropTargetAppId;
draggedAppId = null;
dropTargetAppId = null;
if (toId === null || fromId === toId) return;
const fromIndex = favorites.findIndex((f) => f.app_id === fromId);
if (fromIndex === -1) return;
const reordered = favorites.slice();
const [moved] = reordered.splice(fromIndex, 1);
let insertIndex = reordered.findIndex((f) => f.app_id === toId);
if (insertIndex === -1) return;
if (!dropBefore) insertIndex += 1;
reordered.splice(insertIndex, 0, moved);
const updates = reordered
.map((favorito, index) => ({ favorito, index }))
.filter(({ favorito, index }) => favorito.ordem !== index);
if (!updates.length) return;
favorites = reordered;
favIds = favorites.map((f) => f.app_id);
renderDashboard();
await Promise.all(updates.map(({ favorito, index }) => pidUpdateFavoriteOrdem(favorito.app_id, index)));
await refreshAll();
});
grid.addEventListener("dragend", () => {
draggedAppId = null;
dropTargetAppId = null;
clearDropHighlight();
grid.querySelectorAll(".app-card--dragging").forEach((el) => el.classList.remove("app-card--dragging"));
});
}
await refreshAll();
});