Improve "heart" ui

This commit is contained in:
2026-01-15 17:31:08 +01:00
parent 71f9e4ddaf
commit a682926104
3 changed files with 84 additions and 25 deletions

View File

@@ -619,6 +619,7 @@ public class ChannelDirectoryService {
return applyMarketplaceHearts(entries, sessionUsername);
}
@Transactional
public Optional<ScriptMarketplaceEntry> toggleMarketplaceHeart(String scriptId, String sessionUsername) {
if (scriptId == null || scriptId.isBlank() || sessionUsername == null || sessionUsername.isBlank()) {
return Optional.empty();

View File

@@ -302,17 +302,6 @@
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;
@@ -322,12 +311,57 @@
gap: 8px;
}
.modal .modal-inner .marketplace-heart-count {
display: inline-flex;
align-items: center;
gap: 6px;
color: rgba(248, 113, 113, 0.9);
font-size: 12px;
font-weight: 600;
}
.modal .modal-inner .marketplace-heart-count i {
font-size: 12px;
}
.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-heart-button.is-animating .icon {
animation: marketplace-heart-pop 320ms ease;
}
.modal .modal-inner .marketplace-heart-count.is-animating {
animation: marketplace-heart-count 260ms ease;
}
@keyframes marketplace-heart-pop {
0% {
transform: scale(1);
}
45% {
transform: scale(1.25) rotate(-8deg);
}
100% {
transform: scale(1);
}
}
@keyframes marketplace-heart-count {
0% {
transform: translateY(0);
}
45% {
transform: translateY(-3px);
}
100% {
transform: translateY(0);
}
}
.modal .modal-inner .marketplace-empty,
.modal .modal-inner .marketplace-loading {
padding: 14px;

View File

@@ -782,33 +782,39 @@ 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 heartCountWrapper = document.createElement("div");
heartCountWrapper.className = "marketplace-heart-count";
const heartCountIcon = document.createElement("i");
heartCountIcon.className = "fa-solid fa-heart";
const heartCount = document.createElement("span");
heartCount.textContent = String(entry.heartsCount ?? 0);
heartCountWrapper.appendChild(heartCountIcon);
heartCountWrapper.appendChild(heartCount);
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));
heartButton.addEventListener("click", () =>
toggleMarketplaceHeart(entry, {
button: heartButton,
count: heartCount,
countWrapper: heartCountWrapper,
})
);
const importButton = document.createElement("button");
importButton.type = "button";
importButton.className = "icon-button";
importButton.setAttribute("aria-label", "Import script");
importButton.innerHTML = '<i class="icon fa-solid fa-cloud-download"></i>';
importButton.addEventListener("click", () => importMarketplaceScript(entry));
actions.appendChild(heartCountWrapper);
actions.appendChild(heartButton);
actions.appendChild(importButton);
@@ -855,10 +861,11 @@ export function createCustomAssetModal({
button.innerHTML = `<i class="icon ${iconClass}"></i>`;
}
function toggleMarketplaceHeart(entry, countElement) {
function toggleMarketplaceHeart(entry, elements = {}) {
if (!entry?.id) {
return;
}
animateMarketplaceHeart(elements);
fetch(`/api/marketplace/scripts/${entry.id}/heart`, {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -872,10 +879,14 @@ export function createCustomAssetModal({
.then((updated) => {
entry.heartsCount = updated.heartsCount ?? entry.heartsCount ?? 0;
entry.hearted = updated.hearted ?? entry.hearted;
if (countElement) {
countElement.textContent = String(entry.heartsCount ?? 0);
if (elements.count) {
elements.count.textContent = String(entry.heartsCount ?? 0);
}
renderMarketplace();
if (elements.button) {
updateMarketplaceHeartButton(elements.button, entry);
}
animateMarketplaceHeart(elements);
setTimeout(() => renderMarketplace(), 300);
})
.catch((error) => {
console.error(error);
@@ -883,6 +894,19 @@ export function createCustomAssetModal({
});
}
function animateMarketplaceHeart({ button, countWrapper } = {}) {
if (button) {
button.classList.remove("is-animating");
void button.offsetWidth;
button.classList.add("is-animating");
}
if (countWrapper) {
countWrapper.classList.remove("is-animating");
void countWrapper.offsetWidth;
countWrapper.classList.add("is-animating");
}
}
function debounce(fn, wait = 150) {
let timeout;
return (...args) => {