rspace-online/modules/rauctions/folk-auctions-hub.ts

135 lines
4.6 KiB
TypeScript

/**
* <folk-auctions-hub> — Displays active auctions from the rauctions.online API.
*
* Attributes:
* space — current space slug
*/
const RAUCTIONS_API = "https://rauctions.online";
class FolkAuctionsHub extends HTMLElement {
#shadow: ShadowRoot;
#space = "demo";
constructor() {
super();
this.#shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.#space = this.getAttribute("space") || "demo";
this.#render();
this.#fetchAuctions();
}
async #fetchAuctions() {
try {
const res = await fetch(`${RAUCTIONS_API}/api/auctions?status=active`, {
headers: { "Accept": "application/json" },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const auctions = Array.isArray(data) ? data : (data.auctions || []);
this.#renderAuctions(auctions);
} catch (err) {
this.#renderError(err instanceof Error ? err.message : "Failed to load auctions");
}
}
#renderAuctions(auctions: Array<{ id: string; title: string; currentBid?: number; endTime?: string; imageUrl?: string; status?: string }>) {
const list = this.#shadow.getElementById("auction-list");
if (!list) return;
if (auctions.length === 0) {
list.innerHTML = `
<div class="empty">
<div class="empty-icon">🏛</div>
<p>No active auctions right now.</p>
<a href="${RAUCTIONS_API}/auctions/create" target="_blank" rel="noopener" class="btn">Create an Auction</a>
</div>`;
return;
}
list.innerHTML = auctions.map((a) => {
const bid = a.currentBid != null ? `$${a.currentBid.toFixed(2)}` : "No bids";
const endStr = a.endTime ? new Date(a.endTime).toLocaleString() : "";
const img = a.imageUrl
? `<img src="${a.imageUrl}" alt="" class="thumb" />`
: `<div class="thumb-placeholder">🏛</div>`;
return `
<a href="${RAUCTIONS_API}/auctions/${a.id}" target="_blank" rel="noopener" class="auction-card">
${img}
<div class="card-body">
<div class="card-title">${this.#esc(a.title)}</div>
<div class="card-meta">
<span class="bid">${bid}</span>
${endStr ? `<span class="ends">Ends ${endStr}</span>` : ""}
</div>
</div>
</a>`;
}).join("");
}
#renderError(msg: string) {
const list = this.#shadow.getElementById("auction-list");
if (!list) return;
list.innerHTML = `
<div class="empty">
<p style="color:var(--rs-error,#ef4444);">⚠ ${this.#esc(msg)}</p>
<a href="${RAUCTIONS_API}" target="_blank" rel="noopener" class="btn">Open rAuctions</a>
</div>`;
}
#esc(s: string) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
#render() {
this.#shadow.innerHTML = `
<style>
:host { display: block; padding: 1rem; }
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
.header h2 { font-size: 1.1rem; font-weight: 700; margin: 0; }
.btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 14px; border-radius: 8px; border: none;
background: var(--rs-accent, #6366f1); color: #fff;
font-size: 0.8rem; font-weight: 600; cursor: pointer;
text-decoration: none; transition: opacity 0.15s;
}
.btn:hover { opacity: 0.85; }
#auction-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1rem; }
.auction-card {
display: flex; flex-direction: column;
border-radius: 10px; overflow: hidden;
border: 1px solid var(--rs-border-subtle, #333);
background: var(--rs-bg-surface, #1a1a2e);
text-decoration: none; color: inherit;
transition: border-color 0.15s, transform 0.1s;
}
.auction-card:hover { border-color: var(--rs-accent, #6366f1); transform: translateY(-1px); }
.thumb { width: 100%; height: 160px; object-fit: cover; }
.thumb-placeholder {
width: 100%; height: 160px; display: flex; align-items: center; justify-content: center;
font-size: 3rem; background: var(--rs-bg-hover, #222);
}
.card-body { padding: 0.75rem 1rem; }
.card-title { font-weight: 600; font-size: 0.9rem; margin-bottom: 4px; }
.card-meta { display: flex; gap: 8px; font-size: 0.75rem; opacity: 0.6; }
.bid { color: var(--rs-success, #22c55e); font-weight: 600; opacity: 1; }
.empty { text-align: center; padding: 3rem 1rem; opacity: 0.6; }
.empty-icon { font-size: 3rem; margin-bottom: 0.5rem; }
.loading { text-align: center; padding: 2rem; opacity: 0.5; }
</style>
<div class="header">
<h2>🏛 Active Auctions</h2>
<a href="${RAUCTIONS_API}/auctions/create" target="_blank" rel="noopener" class="btn">+ New Auction</a>
</div>
<div id="auction-list">
<div class="loading">Loading auctions…</div>
</div>`;
}
}
customElements.define("folk-auctions-hub", FolkAuctionsHub);