fixed remove issue, made items avaiable for available view
This commit is contained in:
@@ -18,6 +18,7 @@ const state = {
|
||||
view: "catalog",
|
||||
user: null,
|
||||
inventory: { search: "", category: "", sort: "name" },
|
||||
available: { search: "", category: "", sort: "updated" },
|
||||
};
|
||||
|
||||
let authMode = "login";
|
||||
@@ -34,9 +35,13 @@ function cacheElements() {
|
||||
"modal-backdrop", "modal-close", "modal-title", "modal-body",
|
||||
"catalog-view", "inventory-view", "inv-search", "inv-category", "inv-sort",
|
||||
"inv-summary", "inv-empty", "inv-table-wrap", "inv-tbody", "tab-mine",
|
||||
"available-view", "avail-search", "avail-category", "avail-sort",
|
||||
"avail-summary", "avail-empty", "avail-table-wrap", "avail-tbody",
|
||||
"tab-available",
|
||||
"auth-backdrop", "auth-close", "auth-title", "auth-form",
|
||||
"auth-username", "auth-password", "auth-error", "auth-submit",
|
||||
"auth-toggle", "toast",
|
||||
"confirm-backdrop", "confirm-message", "confirm-ok", "confirm-cancel",
|
||||
];
|
||||
for (const id of ids) els[id] = document.getElementById(id);
|
||||
}
|
||||
@@ -369,6 +374,26 @@ function closeModal() {
|
||||
els["modal-backdrop"].classList.add("hidden");
|
||||
}
|
||||
|
||||
let confirmResolve = null;
|
||||
|
||||
function confirmAction(message, label = "Remove") {
|
||||
if (confirmResolve) closeConfirm(false);
|
||||
els["confirm-message"].textContent = message;
|
||||
els["confirm-ok"].textContent = label;
|
||||
els["confirm-backdrop"].classList.remove("hidden");
|
||||
els["confirm-ok"].focus();
|
||||
return new Promise((resolve) => {
|
||||
confirmResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
function closeConfirm(result) {
|
||||
els["confirm-backdrop"].classList.add("hidden");
|
||||
const resolve = confirmResolve;
|
||||
confirmResolve = null;
|
||||
if (resolve) resolve(result);
|
||||
}
|
||||
|
||||
/* ---------- personal inventory ---------- */
|
||||
|
||||
async function switchToMine() {
|
||||
@@ -380,6 +405,7 @@ async function switchToMine() {
|
||||
setActiveTab(els["tab-mine"]);
|
||||
state.view = "mine";
|
||||
els["catalog-view"].classList.add("hidden");
|
||||
els["available-view"].classList.add("hidden");
|
||||
els["inventory-view"].classList.remove("hidden");
|
||||
await loadInventory();
|
||||
}
|
||||
@@ -387,6 +413,7 @@ async function switchToMine() {
|
||||
function showCatalogView() {
|
||||
state.view = "catalog";
|
||||
els["inventory-view"].classList.add("hidden");
|
||||
els["available-view"].classList.add("hidden");
|
||||
els["catalog-view"].classList.remove("hidden");
|
||||
}
|
||||
|
||||
@@ -443,6 +470,29 @@ function renderInventorySummary(summary) {
|
||||
(parts ? ` · ${parts}` : "") + unavailable;
|
||||
}
|
||||
|
||||
function itemNameCell(entry) {
|
||||
const cell = document.createElement("td");
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "inv-item-link";
|
||||
button.innerHTML =
|
||||
`<div class="inv-item-name">${escapeHtml(entry.name)}</div>` +
|
||||
`<div class="inv-item-sub">${escapeHtml(entry.class_name)}</div>`;
|
||||
if (entry.available === false) {
|
||||
button.disabled = true;
|
||||
button.classList.add("is-static");
|
||||
button.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
'<div class="unavailable-note">Not in current catalog</div>'
|
||||
);
|
||||
} else {
|
||||
button.title = "View item details";
|
||||
button.addEventListener("click", () => openModal(entry.class_name));
|
||||
}
|
||||
cell.appendChild(button);
|
||||
return cell;
|
||||
}
|
||||
|
||||
function renderInventory(listing) {
|
||||
const tbody = els["inv-tbody"];
|
||||
tbody.innerHTML = "";
|
||||
@@ -452,11 +502,7 @@ function renderInventory(listing) {
|
||||
for (const entry of listing.items) {
|
||||
const row = document.createElement("tr");
|
||||
|
||||
const nameCell = document.createElement("td");
|
||||
nameCell.innerHTML =
|
||||
`<div class="inv-item-name">${escapeHtml(entry.name)}</div>` +
|
||||
`<div class="inv-item-sub">${escapeHtml(entry.class_name)}</div>` +
|
||||
(entry.available ? "" : '<div class="unavailable-note">Not in current catalog</div>');
|
||||
const nameCell = itemNameCell(entry);
|
||||
|
||||
const categoryCell = document.createElement("td");
|
||||
categoryCell.innerHTML = entry.category
|
||||
@@ -503,11 +549,48 @@ function renderInventory(listing) {
|
||||
remove.addEventListener("click", () => deleteEntry(entry.class_name, entry.name));
|
||||
actionCell.appendChild(remove);
|
||||
|
||||
row.append(nameCell, categoryCell, qtyCell, notesCell, actionCell);
|
||||
const visibilityCell = document.createElement("td");
|
||||
const visibility = document.createElement("button");
|
||||
visibility.type = "button";
|
||||
visibility.className = "visibility-toggle";
|
||||
visibility.addEventListener("click", () => toggleVisibility(entry, visibility));
|
||||
visibilityCell.appendChild(visibility);
|
||||
applyVisibility(visibility, entry.visibility);
|
||||
|
||||
row.append(nameCell, categoryCell, qtyCell, notesCell, visibilityCell, actionCell);
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function applyVisibility(button, value) {
|
||||
const isAvailable = value === "available";
|
||||
button.textContent = isAvailable ? "Available" : "Private";
|
||||
button.classList.toggle("is-available", isAvailable);
|
||||
button.setAttribute("aria-pressed", String(isAvailable));
|
||||
button.title = isAvailable
|
||||
? "Marked available — click to make private"
|
||||
: "Private to you — click to mark available";
|
||||
}
|
||||
|
||||
async function toggleVisibility(entry, button) {
|
||||
const next = entry.visibility === "available" ? "private" : "available";
|
||||
button.disabled = true;
|
||||
try {
|
||||
const updated = await apiFetch(
|
||||
`/api/v1/me/inventory/${encodeURIComponent(entry.class_name)}`,
|
||||
{ method: "PATCH", body: { visibility: next } }
|
||||
);
|
||||
entry.visibility = updated.visibility;
|
||||
applyVisibility(button, updated.visibility);
|
||||
showToast(updated.visibility === "available" ? "Marked available" : "Marked private");
|
||||
} catch (err) {
|
||||
if (!handleUnauthorized(err)) showToast(err.message, true);
|
||||
loadInventory();
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function patchEntry(class_name, payload) {
|
||||
try {
|
||||
await apiFetch(`/api/v1/me/inventory/${encodeURIComponent(class_name)}`, {
|
||||
@@ -523,13 +606,18 @@ async function patchEntry(class_name, payload) {
|
||||
}
|
||||
|
||||
async function deleteEntry(class_name, name) {
|
||||
if (!window.confirm(`Remove "${name}" from your inventory?`)) return;
|
||||
const confirmed = await confirmAction(
|
||||
`Remove "${name}" from your inventory? If it is marked available it will ` +
|
||||
"disappear from everyone else's view.",
|
||||
"Remove"
|
||||
);
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await apiFetch(`/api/v1/me/inventory/${encodeURIComponent(class_name)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
showToast("Removed from your inventory");
|
||||
loadInventory();
|
||||
await loadInventory();
|
||||
} catch (err) {
|
||||
if (!handleUnauthorized(err)) showToast(err.message, true);
|
||||
}
|
||||
@@ -557,6 +645,74 @@ async function addToInventory(class_name, quantity = 1) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- available items (public) ---------- */
|
||||
|
||||
async function switchToAvailable() {
|
||||
setActiveTab(els["tab-available"]);
|
||||
state.view = "available";
|
||||
els["catalog-view"].classList.add("hidden");
|
||||
els["inventory-view"].classList.add("hidden");
|
||||
els["available-view"].classList.remove("hidden");
|
||||
await loadAvailable();
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return "—";
|
||||
return date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
|
||||
}
|
||||
|
||||
async function loadAvailable() {
|
||||
const params = new URLSearchParams();
|
||||
if (state.available.search) params.set("search", state.available.search);
|
||||
if (state.available.category) params.set("category", state.available.category);
|
||||
params.set("sort", state.available.sort);
|
||||
params.set("page_size", String(INVENTORY_PAGE_SIZE));
|
||||
|
||||
els["avail-summary"].textContent = "Loading…";
|
||||
try {
|
||||
const listing = await apiFetch(`/api/v1/available?${params}`);
|
||||
renderAvailable(listing);
|
||||
} catch (err) {
|
||||
els["avail-summary"].textContent = "";
|
||||
showToast(err.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function renderAvailable(listing) {
|
||||
const tbody = els["avail-tbody"];
|
||||
tbody.innerHTML = "";
|
||||
els["avail-summary"].textContent =
|
||||
`${listing.total} item${listing.total === 1 ? "" : "s"} offered`;
|
||||
els["avail-empty"].classList.toggle("hidden", listing.total !== 0);
|
||||
els["avail-table-wrap"].classList.toggle("hidden", listing.total === 0);
|
||||
|
||||
for (const entry of listing.items) {
|
||||
const row = document.createElement("tr");
|
||||
|
||||
const nameCell = itemNameCell(entry);
|
||||
|
||||
const categoryCell = document.createElement("td");
|
||||
categoryCell.innerHTML = entry.category
|
||||
? categoryBadge(entry.category)
|
||||
: '<span class="badge size">—</span>';
|
||||
|
||||
const qtyCell = document.createElement("td");
|
||||
qtyCell.textContent = String(entry.quantity);
|
||||
|
||||
const userCell = document.createElement("td");
|
||||
userCell.className = "avail-user";
|
||||
userCell.textContent = entry.username;
|
||||
|
||||
const updatedCell = document.createElement("td");
|
||||
updatedCell.className = "avail-updated";
|
||||
updatedCell.textContent = formatDate(entry.updated_at);
|
||||
|
||||
row.append(nameCell, categoryCell, qtyCell, userCell, updatedCell);
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- events ---------- */
|
||||
|
||||
function debounce(fn, wait) {
|
||||
@@ -585,6 +741,10 @@ function bindEvents() {
|
||||
switchToMine();
|
||||
return;
|
||||
}
|
||||
if (tab.dataset.view === "available") {
|
||||
switchToAvailable();
|
||||
return;
|
||||
}
|
||||
setActiveTab(tab);
|
||||
showCatalogView();
|
||||
state.category = tab.dataset.category;
|
||||
@@ -647,8 +807,14 @@ function bindEvents() {
|
||||
els["modal-backdrop"].addEventListener("click", (event) => {
|
||||
if (event.target === els["modal-backdrop"]) closeModal();
|
||||
});
|
||||
els["confirm-ok"].addEventListener("click", () => closeConfirm(true));
|
||||
els["confirm-cancel"].addEventListener("click", () => closeConfirm(false));
|
||||
els["confirm-backdrop"].addEventListener("click", (event) => {
|
||||
if (event.target === els["confirm-backdrop"]) closeConfirm(false);
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "Escape") return;
|
||||
closeConfirm(false);
|
||||
closeModal();
|
||||
closeAuth();
|
||||
});
|
||||
@@ -683,6 +849,24 @@ function bindInventoryEvents() {
|
||||
});
|
||||
}
|
||||
|
||||
function bindAvailableEvents() {
|
||||
els["avail-search"].addEventListener(
|
||||
"input",
|
||||
debounce(() => {
|
||||
state.available.search = els["avail-search"].value.trim();
|
||||
loadAvailable();
|
||||
}, 300)
|
||||
);
|
||||
els["avail-category"].addEventListener("change", () => {
|
||||
state.available.category = els["avail-category"].value;
|
||||
loadAvailable();
|
||||
});
|
||||
els["avail-sort"].addEventListener("change", () => {
|
||||
state.available.sort = els["avail-sort"].value;
|
||||
loadAvailable();
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- boot ---------- */
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
@@ -690,6 +874,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
bindEvents();
|
||||
bindAuthEvents();
|
||||
bindInventoryEvents();
|
||||
bindAvailableEvents();
|
||||
initHeader();
|
||||
await initAuth();
|
||||
loadItems();
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<button class="tab" data-view="catalog" data-category="ship-items">Ship Equipment</button>
|
||||
<button class="tab" data-view="catalog" data-category="fps-items">FPS Items</button>
|
||||
<button class="tab tab-mine" id="tab-mine" data-view="mine">My Inventory</button>
|
||||
<button class="tab" id="tab-available" data-view="available">Available</button>
|
||||
</nav>
|
||||
|
||||
<div id="catalog-view">
|
||||
@@ -88,6 +89,7 @@
|
||||
<th>Category</th>
|
||||
<th>Qty</th>
|
||||
<th>Notes</th>
|
||||
<th>Visibility</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -96,6 +98,43 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="available-view" class="hidden">
|
||||
<div class="inventory-controls">
|
||||
<input type="search" id="avail-search" placeholder="Search item or pilot…" autocomplete="off">
|
||||
<select id="avail-category">
|
||||
<option value="">All categories</option>
|
||||
<option value="ships">Ships</option>
|
||||
<option value="ship-items">Ship Equipment</option>
|
||||
<option value="fps-items">FPS Items</option>
|
||||
</select>
|
||||
<select id="avail-sort" aria-label="Sort available items">
|
||||
<option value="updated">Sort: Recently updated</option>
|
||||
<option value="name">Sort: Name</option>
|
||||
<option value="category">Sort: Category</option>
|
||||
<option value="quantity">Sort: Quantity</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="avail-summary" class="inv-summary"></div>
|
||||
<div id="avail-empty" class="empty-state hidden">
|
||||
<p>Nothing is being offered right now.</p>
|
||||
<p class="hint">Mark items as available in My Inventory to list them here.</p>
|
||||
</div>
|
||||
<div id="avail-table-wrap" class="inv-table-wrap hidden">
|
||||
<table class="inv-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Category</th>
|
||||
<th>Qty</th>
|
||||
<th>Offered by</th>
|
||||
<th>Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="avail-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="site-footer">
|
||||
<p>Data source:
|
||||
<a href="https://github.com/richardthombs/scunpacked" target="_blank" rel="noopener noreferrer">scunpacked</a>
|
||||
@@ -111,6 +150,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="confirm-backdrop" class="modal-backdrop hidden">
|
||||
<div class="modal modal-narrow" role="alertdialog" aria-modal="true"
|
||||
aria-labelledby="confirm-title" aria-describedby="confirm-message">
|
||||
<h2 id="confirm-title">Remove item</h2>
|
||||
<p id="confirm-message" class="modal-desc"></p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="confirm-cancel" class="btn-secondary">Cancel</button>
|
||||
<button type="button" id="confirm-ok" class="btn-danger">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="auth-backdrop" class="modal-backdrop hidden">
|
||||
<div class="modal modal-narrow" role="dialog" aria-modal="true" aria-labelledby="auth-title">
|
||||
<button class="modal-close" id="auth-close" aria-label="Close">×</button>
|
||||
|
||||
@@ -132,6 +132,23 @@ body {
|
||||
}
|
||||
.btn-secondary:hover:not(:disabled) { color: var(--text); border-color: var(--accent); }
|
||||
.btn-secondary:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.btn-danger {
|
||||
background: var(--danger);
|
||||
color: #1b0505;
|
||||
border: 1px solid var(--danger);
|
||||
border-radius: 6px;
|
||||
padding: 0.55rem 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-danger:hover { filter: brightness(1.1); }
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.6rem;
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
|
||||
/* Main */
|
||||
main {
|
||||
@@ -350,7 +367,8 @@ main {
|
||||
#inv-sort { flex: 1 1 170px; }
|
||||
.inventory-controls input:focus,
|
||||
.inventory-controls select:focus { outline: none; border-color: var(--accent); }
|
||||
#inventory-view {
|
||||
#inventory-view,
|
||||
#available-view {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 1.25rem 0;
|
||||
@@ -381,6 +399,21 @@ main {
|
||||
}
|
||||
.inv-table tr:last-child td { border-bottom: none; }
|
||||
.inv-item-name { font-weight: 600; }
|
||||
.inv-item-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.inv-item-link:hover .inv-item-name { text-decoration: underline; }
|
||||
.inv-item-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
.inv-item-link.is-static { cursor: default; }
|
||||
.inv-item-link.is-static:hover .inv-item-name { text-decoration: none; }
|
||||
.inv-item-sub {
|
||||
color: var(--text-dim);
|
||||
font-size: 0.75rem;
|
||||
@@ -414,6 +447,25 @@ main {
|
||||
cursor: pointer;
|
||||
}
|
||||
.inv-remove:hover { border-color: var(--danger); }
|
||||
.visibility-toggle {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-dim);
|
||||
border-radius: 999px;
|
||||
padding: 0.3rem 0.7rem;
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.visibility-toggle:hover { border-color: var(--accent); color: var(--text); }
|
||||
.visibility-toggle.is-available {
|
||||
border-color: var(--success);
|
||||
color: var(--success);
|
||||
background: rgba(34, 197, 94, 0.12);
|
||||
}
|
||||
.visibility-toggle:disabled { opacity: 0.6; cursor: progress; }
|
||||
.avail-user { font-weight: 600; }
|
||||
.avail-updated { color: var(--text-dim); font-size: 0.8rem; white-space: nowrap; }
|
||||
.unavailable-note { color: var(--accent-warm); font-size: 0.75rem; }
|
||||
|
||||
/* Auth modal */
|
||||
|
||||
Reference in New Issue
Block a user