255 lines
9.2 KiB
JavaScript
255 lines
9.2 KiB
JavaScript
const PID_STAR_ICON =
|
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l2.6 5.6 6.1.8-4.5 4.3 1.2 6.1L12 17l-5.4 2.8 1.2-6.1-4.5-4.3 6.1-.8L12 3z" stroke-linejoin="round"/></svg>';
|
|
|
|
const PID_DEFAULT_APP_ICON =
|
|
'<svg class="app-card__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="4" y="4" width="7" height="7" rx="1"/><rect x="13" y="4" width="7" height="7" rx="1"/><rect x="4" y="13" width="7" height="7" rx="1"/><rect x="13" y="13" width="7" height="7" rx="1"/></svg>';
|
|
|
|
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) => `
|
|
<article class="app-card" data-fav-id="${pidEscapeHtml(app.id)}" role="button" tabindex="0" draggable="true">
|
|
${pidIconHtmlFor(app)}
|
|
<span class="app-card__label">${pidEscapeHtml(app.label)}</span>
|
|
<button type="button" class="app-card__remove" data-fav-remove="${pidEscapeHtml(app.id)}" aria-label="Remover dos favoritos" draggable="false">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2m-8 0 1 12a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1l1-12" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
</button>
|
|
</article>`
|
|
)
|
|
.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 <a> 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();
|
|
});
|