rspace-online/modules/cart/components/folk-cart-shop.ts

153 lines
5.9 KiB
TypeScript

/**
* <folk-cart-shop> — browse catalog, view orders, trigger fulfillment.
* Shows catalog items, order creation flow, and order status tracking.
*/
class FolkCartShop extends HTMLElement {
private shadow: ShadowRoot;
private catalog: any[] = [];
private orders: any[] = [];
private view: "catalog" | "orders" = "catalog";
private loading = true;
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.render();
this.loadData();
}
private getApiBase(): string {
const path = window.location.pathname;
const parts = path.split("/").filter(Boolean);
return parts.length >= 2 ? `/${parts[0]}/cart` : "/demo/cart";
}
private async loadData() {
this.loading = true;
this.render();
try {
const [catRes, ordRes] = await Promise.all([
fetch(`${this.getApiBase()}/api/catalog?limit=50`),
fetch(`${this.getApiBase()}/api/orders?limit=20`),
]);
const catData = await catRes.json();
const ordData = await ordRes.json();
this.catalog = catData.entries || [];
this.orders = ordData.orders || [];
} catch (e) {
console.error("Failed to load cart data:", e);
}
this.loading = false;
this.render();
}
private render() {
this.shadow.innerHTML = `
<style>
:host { display: block; padding: 1.5rem; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
.header h2 { margin: 0; color: #f1f5f9; font-size: 1.5rem; }
.tabs { display: flex; gap: 0.5rem; }
.tab { padding: 0.5rem 1rem; border-radius: 8px; border: 1px solid #334155; background: #1e293b; color: #94a3b8; cursor: pointer; font-size: 0.875rem; }
.tab:hover { border-color: #475569; color: #f1f5f9; }
.tab.active { background: #4f46e5; border-color: #6366f1; color: #fff; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; }
.card { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 1.25rem; }
.card:hover { border-color: #475569; }
.card-title { color: #f1f5f9; font-weight: 600; font-size: 1rem; margin: 0 0 0.5rem; }
.card-meta { color: #94a3b8; font-size: 0.8125rem; margin-bottom: 0.5rem; }
.tag { display: inline-block; padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.6875rem; margin-right: 0.25rem; }
.tag-type { background: rgba(99,102,241,0.1); color: #818cf8; }
.tag-cap { background: rgba(34,197,94,0.1); color: #4ade80; }
.dims { color: #64748b; font-size: 0.75rem; margin-top: 0.5rem; }
.status { padding: 0.125rem 0.5rem; border-radius: 999px; font-size: 0.6875rem; font-weight: 500; }
.status-pending { background: rgba(251,191,36,0.15); color: #fbbf24; }
.status-paid { background: rgba(34,197,94,0.15); color: #4ade80; }
.status-active { background: rgba(34,197,94,0.15); color: #4ade80; }
.status-completed { background: rgba(99,102,241,0.15); color: #a5b4fc; }
.status-cancelled { background: rgba(239,68,68,0.15); color: #f87171; }
.order-card { display: flex; justify-content: space-between; align-items: center; }
.order-info { flex: 1; }
.order-price { color: #f1f5f9; font-weight: 600; font-size: 1.125rem; }
.empty { text-align: center; padding: 3rem; color: #64748b; font-size: 0.875rem; }
.loading { text-align: center; padding: 3rem; color: #94a3b8; }
</style>
<div class="header">
<h2>\u{1F6D2} Community Shop</h2>
<div class="tabs">
<button class="tab ${this.view === 'catalog' ? 'active' : ''}" data-view="catalog">\u{1F4E6} Catalog (${this.catalog.length})</button>
<button class="tab ${this.view === 'orders' ? 'active' : ''}" data-view="orders">\u{1F4CB} Orders (${this.orders.length})</button>
</div>
</div>
${this.loading ? `<div class="loading">\u23F3 Loading...</div>` :
this.view === "catalog" ? this.renderCatalog() : this.renderOrders()}
`;
this.shadow.querySelectorAll(".tab").forEach((el) => {
el.addEventListener("click", () => {
this.view = ((el as HTMLElement).dataset.view || "catalog") as "catalog" | "orders";
this.render();
});
});
}
private renderCatalog(): string {
if (this.catalog.length === 0) {
return `<div class="empty">No items in the catalog yet. Ingest artifacts from rPubs or Swag Designer to list them here.</div>`;
}
return `<div class="grid">
${this.catalog.map((entry) => `
<div class="card">
<h3 class="card-title">${this.esc(entry.title || "Untitled")}</h3>
<div class="card-meta">
${entry.product_type ? `<span class="tag tag-type">${this.esc(entry.product_type)}</span>` : ""}
${(entry.required_capabilities || []).map((cap: string) => `<span class="tag tag-cap">${this.esc(cap)}</span>`).join("")}
</div>
${entry.description ? `<div class="card-meta">${this.esc(entry.description)}</div>` : ""}
${entry.dimensions ? `<div class="dims">${entry.dimensions.width_mm}x${entry.dimensions.height_mm}mm</div>` : ""}
<div style="margin-top:0.5rem"><span class="status status-${entry.status}">${entry.status}</span></div>
</div>
`).join("")}
</div>`;
}
private renderOrders(): string {
if (this.orders.length === 0) {
return `<div class="empty">No orders yet.</div>`;
}
return `<div class="grid">
${this.orders.map((order) => `
<div class="card">
<div class="order-card">
<div class="order-info">
<h3 class="card-title">${this.esc(order.artifact_title || "Order")}</h3>
<div class="card-meta">
${order.provider_name ? `Provider: ${this.esc(order.provider_name)}` : ""}
${order.quantity > 1 ? ` \u2022 Qty: ${order.quantity}` : ""}
</div>
<span class="status status-${order.status}">${order.status}</span>
</div>
<div class="order-price">$${parseFloat(order.total_price || 0).toFixed(2)}</div>
</div>
</div>
`).join("")}
</div>`;
}
private esc(s: string): string {
const d = document.createElement("div");
d.textContent = s;
return d.innerHTML;
}
}
customElements.define("folk-cart-shop", FolkCartShop);