Add hearts to marketplace assets

This commit is contained in:
2026-01-13 23:46:09 +01:00
parent e3580f950d
commit a267f9b5ec
12 changed files with 404 additions and 17 deletions

View File

@@ -302,12 +302,30 @@
white-space: nowrap;
}
.modal .modal-inner .marketplace-hearts {
display: inline-flex;
align-items: center;
gap: 6px;
color: rgba(248, 113, 113, 0.9);
}
.modal .modal-inner .marketplace-hearts i {
font-size: 12px;
}
.modal .modal-inner .marketplace-actions {
display: flex;
align-items: center;
margin-top: auto;
width: 100%;
justify-content: flex-end;
gap: 8px;
}
.modal .modal-inner .marketplace-heart-button.active {
border-color: rgba(248, 113, 113, 0.5);
background: rgba(248, 113, 113, 0.12);
color: #fecdd3;
}
.modal .modal-inner .marketplace-empty,

View File

@@ -750,7 +750,14 @@ export function createCustomAssetModal({
marketplaceList.innerHTML = '<div class="marketplace-empty">No scripts found.</div>';
return;
}
marketplaceEntries.forEach((entry) => {
const sortedEntries = [...marketplaceEntries].sort((a, b) => {
const heartsDelta = (b.heartsCount ?? 0) - (a.heartsCount ?? 0);
if (heartsDelta !== 0) {
return heartsDelta;
}
return (a.name || "").localeCompare(b.name || "", undefined, { sensitivity: "base" });
});
sortedEntries.forEach((entry) => {
const card = document.createElement("div");
card.className = "marketplace-card";
@@ -775,17 +782,34 @@ export function createCustomAssetModal({
description.textContent = entry.description || "No description provided.";
const meta = document.createElement("small");
meta.textContent = entry.broadcaster ? `By ${entry.broadcaster}` : "";
const hearts = document.createElement("small");
hearts.className = "marketplace-hearts";
const heartIcon = document.createElement("i");
heartIcon.className = "fa-solid fa-heart";
const heartCount = document.createElement("span");
heartCount.textContent = String(entry.heartsCount ?? 0);
hearts.appendChild(heartIcon);
hearts.appendChild(heartCount);
content.appendChild(title);
content.appendChild(description);
content.appendChild(meta);
content.appendChild(hearts);
const actions = document.createElement("div");
actions.className = "marketplace-actions";
const heartButton = document.createElement("button");
heartButton.type = "button";
heartButton.className = "icon-button marketplace-heart-button";
heartButton.setAttribute("aria-label", "Heart script");
updateMarketplaceHeartButton(heartButton, entry);
heartButton.addEventListener("click", () => toggleMarketplaceHeart(entry, heartCount));
const importButton = document.createElement("button");
importButton.type = "button";
importButton.className = "primary";
importButton.textContent = "Import";
importButton.className = "icon-button";
importButton.setAttribute("aria-label", "Import script");
importButton.innerHTML = '<i class="icon fa-solid fa-download"></i>';
importButton.addEventListener("click", () => importMarketplaceScript(entry));
actions.appendChild(heartButton);
actions.appendChild(importButton);
card.appendChild(content);
@@ -821,6 +845,44 @@ export function createCustomAssetModal({
});
}
function updateMarketplaceHeartButton(button, entry) {
if (!button || !entry) {
return;
}
button.classList.toggle("active", Boolean(entry.hearted));
button.setAttribute("aria-pressed", entry.hearted ? "true" : "false");
const iconClass = entry.hearted ? "fa-solid fa-heart" : "fa-regular fa-heart";
button.innerHTML = `<i class="icon ${iconClass}"></i>`;
}
function toggleMarketplaceHeart(entry, countElement) {
if (!entry?.id) {
return;
}
fetch(`/api/marketplace/scripts/${entry.id}/heart`, {
method: "POST",
headers: { "Content-Type": "application/json" },
})
.then((response) => {
if (!response.ok) {
throw new Error("Failed to update heart");
}
return response.json();
})
.then((updated) => {
entry.heartsCount = updated.heartsCount ?? entry.heartsCount ?? 0;
entry.hearted = updated.hearted ?? entry.hearted;
if (countElement) {
countElement.textContent = String(entry.heartsCount ?? 0);
}
renderMarketplace();
})
.catch((error) => {
console.error(error);
showToast?.("Unable to update heart. Please try again.", "error");
});
}
function debounce(fn, wait = 150) {
let timeout;
return (...args) => {