diff --git a/lib/module-display.ts b/lib/module-display.ts index b9253fac..4f9e91c4 100644 --- a/lib/module-display.ts +++ b/lib/module-display.ts @@ -38,4 +38,5 @@ export const MODULE_META: Record = { 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: "πŸ›" }, }; diff --git a/modules/rauctions/folk-auctions-hub.ts b/modules/rauctions/folk-auctions-hub.ts new file mode 100644 index 00000000..ef371bad --- /dev/null +++ b/modules/rauctions/folk-auctions-hub.ts @@ -0,0 +1,134 @@ +/** + * β€” 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 = ` +
+
πŸ›
+

No active auctions right now.

+ Create an Auction +
`; + 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 + ? `` + : `
πŸ›
`; + return ` + + ${img} +
+
${this.#esc(a.title)}
+
+ ${bid} + ${endStr ? `Ends ${endStr}` : ""} +
+
+
`; + }).join(""); + } + + #renderError(msg: string) { + const list = this.#shadow.getElementById("auction-list"); + if (!list) return; + list.innerHTML = ` +
+

⚠ ${this.#esc(msg)}

+ Open rAuctions +
`; + } + + #esc(s: string) { + return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + } + + #render() { + this.#shadow.innerHTML = ` + +
+

πŸ› Active Auctions

+ + New Auction +
+
+
Loading auctions…
+
`; + } +} + +customElements.define("folk-auctions-hub", FolkAuctionsHub); diff --git a/modules/rauctions/landing.ts b/modules/rauctions/landing.ts new file mode 100644 index 00000000..464d6aee --- /dev/null +++ b/modules/rauctions/landing.ts @@ -0,0 +1,34 @@ +/** Landing page body HTML for rAuctions */ +export function renderLanding(): string { + return ` +
+
πŸ›
+

rAuctions

+

+ Live community auctions with real-time bidding and USDC settlement. + Create, bid on, and manage auctions for artwork, collectibles, and unique items. +

+
+
+
πŸ”¨
+ Live Bidding +

Real-time bids with countdown timers

+
+
+
πŸ’°
+ USDC Settlement +

On-chain payments via Base

+
+
+
πŸ“Έ
+ Rich Listings +

Photos, descriptions, and reserve prices

+
+
+
πŸ”
+ EncryptID Auth +

Secure identity for bidders & sellers

+
+
+
`; +} diff --git a/modules/rauctions/mod.ts b/modules/rauctions/mod.ts index dd43246f..d689e308 100644 --- a/modules/rauctions/mod.ts +++ b/modules/rauctions/mod.ts @@ -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: `
+ Auctions + Open Full App +
+ `, + scripts: ``, + })); +}); + +// ── 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" }, + ], };