fix(dashboard): character inventory loads fast with thousands of items

The inventory modal drew every item of every tab into the page, and one
item details request for all of them (over the 500 a request allows, so
no tooltips at all). Now the items come from the summary API when the
modal opens and are drawn one tab at a time, 240 at a time as the tab
scrolls, with a filter by name or LOT. Each chunk's item details are
fetched with it (at most 500 a request) and a tooltip is made on first
hover. Player-built items get their UGC icon as their chunk is drawn
(UgcLinks.inventoryLinks and decorateItem).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 20:38:38 -05:00
parent 6fda92c50d
commit 455d6693f6
2 changed files with 183 additions and 52 deletions

View File

@@ -75,6 +75,8 @@
UgcLinks.details(box.dataset.ugcDetails, box.dataset.via, box.dataset.model).then(function (html) { body.innerHTML = html; });
}, true);
var linkCache = {}; // character id -> promise of its item links
window.UgcLinks = {
// A badge for the UGC server's state; a failure's reason as its tooltip
state: function (item) {
@@ -108,32 +110,49 @@
}).catch(function () { return {}; });
},
// Shows the creations among a character page's items (.inv-item[data-id]) with their own icon and a link
// Itemid -> the UGC link of each of a character's player-built items (fetched once per character)
inventoryLinks: function (characterId) {
if (!linkCache[characterId]) {
linkCache[characterId] = api.get('/api/ugc_links/character/' + characterId).then(function (d) {
var map = {};
((d && d.items) || []).forEach(function (item) { map[item.itemId] = item; });
return map;
});
}
return linkCache[characterId];
},
// Shows a player-built item's UGC icon and a link to the UGC viewer on one inventory item element
decorateItem: function (el, item) {
var img = el.querySelector('img');
if (img) {
var own = img.src;
img.onerror = function () { img.onerror = null; img.src = own; };
img.src = item.icon;
}
el.classList.add('inv-ugc');
el.title = (el.title ? el.title + ' - ' : '') + 'Player-built ' + (item.kind === 'modular' ? 'car or rocket' : 'model') + ' (UGC ' + item.ugcId + ', ' + item.state + ')';
if (item.link && !el.querySelector('.inv-ugc-link')) {
// Items may sit inside a link already (the item trace), so this is a button, not a nested link
var a = document.createElement('span');
a.className = 'inv-ugc-link';
a.setAttribute('role', 'link');
a.tabIndex = 0;
a.textContent = 'UGC';
a.title = 'Open in the UGC viewer';
var open = function (e) { e.preventDefault(); e.stopPropagation(); window.location.href = item.link; };
a.addEventListener('click', open);
a.addEventListener('keydown', function (e) { if (e.key === 'Enter') open(e); });
el.appendChild(a);
}
},
// Decorates the inventory items already on the page
decorateInventory: function (characterId, root) {
return api.get('/api/ugc_links/character/' + characterId).then(function (d) {
((d && d.items) || []).forEach(function (item) {
(root || document).querySelectorAll('.inv-item[data-id="' + item.itemId + '"]').forEach(function (el) {
var img = el.querySelector('img');
if (img) {
var own = img.src;
img.onerror = function () { img.onerror = null; img.src = own; };
img.src = item.icon;
}
el.classList.add('inv-ugc');
el.title = (el.title ? el.title + ' - ' : '') + 'Player-built ' + (item.kind === 'modular' ? 'car or rocket' : 'model') + ' (UGC ' + item.ugcId + ', ' + item.state + ')';
if (item.link && !el.querySelector('.inv-ugc-link')) {
// Items may sit inside a link already (the item trace), so this is a button, not a nested link
var a = document.createElement('span');
a.className = 'inv-ugc-link';
a.setAttribute('role', 'link');
a.tabIndex = 0;
a.textContent = 'UGC';
a.title = 'Open in the UGC viewer';
var open = function (e) { e.preventDefault(); e.stopPropagation(); window.location.href = item.link; };
a.addEventListener('click', open);
a.addEventListener('keydown', function (e) { if (e.key === 'Enter') open(e); });
el.appendChild(a);
}
});
var self = this;
return this.inventoryLinks(characterId).then(function (map) {
Object.keys(map).forEach(function (id) {
(root || document).querySelectorAll('.inv-item[data-id="' + id + '"]').forEach(function (el) { self.decorateItem(el, map[id]); });
});
}).catch(function () {});
}