feat(rauctions): add rAuctions module with hub page and external app embed

Registers rauctions as an embedded rSpace module that proxies the
standalone rauctions.online Next.js app. Includes hub page with active
auction listings, landing page, and MODULE_META entry for canvas display.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-13 15:50:25 +00:00
parent cede2232b5
commit 18a688bade
4 changed files with 228 additions and 10 deletions

View File

@ -38,4 +38,5 @@ export const MODULE_META: Record<string, ModuleDisplayMeta> = {
rsocials: { badge: "rSo", color: "#f9a8d4", name: "rSocials", icon: "📱" },
rdesign: { badge: "rDe", color: "#7c3aed", name: "rDesign", icon: "🎨" },
rtime: { badge: "rTi", color: "#a78bfa", name: "rTime", icon: "⏳" },
rauctions: { badge: "rA", color: "#fca5a5", name: "rAuctions", icon: "🏛" },
};

View File

@ -0,0 +1,134 @@
/**
* <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);

View File

@ -0,0 +1,34 @@
/** Landing page body HTML for rAuctions */
export function renderLanding(): string {
return `
<div style="max-width:640px;margin:2rem auto;padding:0 1rem;text-align:center;">
<div style="font-size:3rem;margin-bottom:0.5rem;">🏛</div>
<h2 style="font-size:1.5rem;font-weight:700;margin-bottom:0.5rem;">rAuctions</h2>
<p style="color:var(--rs-text-secondary);margin-bottom:1.5rem;">
Live community auctions with real-time bidding and USDC settlement.
Create, bid on, and manage auctions for artwork, collectibles, and unique items.
</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;text-align:left;">
<div style="padding:1rem;border-radius:8px;background:var(--rs-bg-surface);border:1px solid var(--rs-border-subtle);">
<div style="font-size:1.5rem;margin-bottom:0.25rem;">🔨</div>
<strong>Live Bidding</strong>
<p style="font-size:0.8rem;opacity:0.6;margin:0;">Real-time bids with countdown timers</p>
</div>
<div style="padding:1rem;border-radius:8px;background:var(--rs-bg-surface);border:1px solid var(--rs-border-subtle);">
<div style="font-size:1.5rem;margin-bottom:0.25rem;">💰</div>
<strong>USDC Settlement</strong>
<p style="font-size:0.8rem;opacity:0.6;margin:0;">On-chain payments via Base</p>
</div>
<div style="padding:1rem;border-radius:8px;background:var(--rs-bg-surface);border:1px solid var(--rs-border-subtle);">
<div style="font-size:1.5rem;margin-bottom:0.25rem;">📸</div>
<strong>Rich Listings</strong>
<p style="font-size:0.8rem;opacity:0.6;margin:0;">Photos, descriptions, and reserve prices</p>
</div>
<div style="padding:1rem;border-radius:8px;background:var(--rs-bg-surface);border:1px solid var(--rs-border-subtle);">
<div style="font-size:1.5rem;margin-bottom:0.25rem;">🔐</div>
<strong>EncryptID Auth</strong>
<p style="font-size:0.8rem;opacity:0.6;margin:0;">Secure identity for bidders & sellers</p>
</div>
</div>
</div>`;
}

View File

@ -1,20 +1,69 @@
/**
* rAuctions module Community auctions with USDC (stub).
* TODO: Implement auction logic.
* rAuctions module community auctions with USDC bidding.
*
* Embeds the standalone rauctions.online Next.js app via externalApp iframe.
* Provides a hub page listing active auctions proxied from the rauctions API.
*/
import { Hono } from 'hono';
import type { RSpaceModule } from '../../shared/module';
import { Hono } from "hono";
import { renderShell, renderExternalAppShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
const RAUCTIONS_URL = process.env.RAUCTIONS_URL || "https://rauctions.online";
const routes = new Hono();
routes.get('/', (c) => c.text('rAuctions — coming soon'));
// ── Page route ──
routes.get("/", (c) => {
const spaceSlug = c.req.param("space") || "demo";
const view = c.req.query("view");
if (view === "app") {
return c.html(renderExternalAppShell({
title: `${spaceSlug} — Auctions | rSpace`,
moduleId: "rauctions",
spaceSlug,
modules: getModuleInfoList(),
appUrl: RAUCTIONS_URL,
appName: "rAuctions",
theme: "dark",
}));
}
return c.html(renderShell({
title: `${spaceSlug} — Auctions | rSpace`,
moduleId: "rauctions",
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
body: `<div class="rapp-nav" style="padding:0 1rem;margin-top:8px">
<span class="rapp-nav__title">Auctions</span>
<a href="?view=app" class="rapp-nav__btn--app-toggle">Open Full App</a>
</div>
<folk-auctions-hub space="${spaceSlug}"></folk-auctions-hub>`,
scripts: `<script type="module" src="/modules/rauctions/folk-auctions-hub.js"></script>`,
}));
});
// ── API: proxy health ──
routes.get("/api/health", (c) => {
return c.json({ status: "ok", service: "rauctions" });
});
export const auctionsModule: RSpaceModule = {
id: 'rauctions',
name: 'rAuctions',
icon: '🎭',
description: 'Community auctions with USDC',
routes,
id: "rauctions",
name: "rAuctions",
icon: "🏛",
description: "Live auctions with USDC bidding",
scoping: { defaultScope: 'space', userConfigurable: true },
routes,
landingPage: renderLanding,
standaloneDomain: "rauctions.online",
externalApp: { url: RAUCTIONS_URL, name: "rAuctions" },
outputPaths: [
{ path: "live", name: "Live Auctions", icon: "🔨", description: "Currently active auctions" },
{ path: "ended", name: "Ended", icon: "🏆", description: "Completed auction results" },
],
};