rspace-online/modules/revents/mod.ts

165 lines
5.1 KiB
TypeScript

/**
* rEvents module — event aggregation & parsing.
*
* Aggregates events from Luma, Meetup, iCal feeds, and unstructured text.
* The functional app runs as a standalone Next.js container (revents-online:3000)
* and is embedded via externalApp iframe in rSpace.
*/
import { Hono } from "hono";
import { renderShell, renderExternalAppShell, escapeHtml } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
const REVENTS_URL = process.env.REVENTS_URL || "http://revents-online:3000";
const REVENTS_PUBLIC_URL = process.env.REVENTS_PUBLIC_URL || "https://revents.online";
const routes = new Hono();
// ── Proxy API calls to revents-online ──
routes.get("/api/events", async (c) => {
const qs = new URL(c.req.url).search;
try {
const res = await fetch(`${REVENTS_URL}/api/events${qs}`);
const data = await res.json();
return c.json(data);
} catch {
return c.json({ count: 0, results: [] });
}
});
routes.get("/api/sources", async (c) => {
try {
const res = await fetch(`${REVENTS_URL}/api/sources`);
const data = await res.json();
return c.json(data);
} catch {
return c.json([]);
}
});
routes.post("/api/parse", async (c) => {
try {
const body = await c.req.json();
const res = await fetch(`${REVENTS_URL}/api/parse`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json();
return c.json(data);
} catch (e: any) {
return c.json({ error: e.message }, 500);
}
});
// ── Explore page (embedded) ──
routes.get("/explore", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderExternalAppShell({
title: `Explore Events — rEvents | rSpace`,
moduleId: "revents",
spaceSlug: space,
modules: getModuleInfoList(),
appUrl: `${REVENTS_PUBLIC_URL}/explore`,
appName: "rEvents",
theme: "dark",
}));
});
// ── Sources page (embedded) ──
routes.get("/sources", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderExternalAppShell({
title: `Event Sources — rEvents | rSpace`,
moduleId: "revents",
spaceSlug: space,
modules: getModuleInfoList(),
appUrl: `${REVENTS_PUBLIC_URL}/sources`,
appName: "rEvents",
theme: "dark",
}));
});
// ── Hub page ──
routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
const base = `/${escapeHtml(space)}/revents`;
return c.html(renderShell({
title: `rEvents — ${space} | rSpace`,
moduleId: "revents",
spaceSlug: space,
modules: getModuleInfoList(),
styles: `<style>
.rs-hub{max-width:720px;margin:2rem auto;padding:0 1.5rem}
.rs-hub h1{font-size:1.8rem;margin-bottom:.5rem;color:var(--rs-text-primary)}
.rs-hub>p{color:var(--rs-text-secondary);margin-bottom:2rem}
.rs-nav{display:flex;flex-direction:column;gap:1rem}
.rs-nav a{display:flex;align-items:center;gap:1rem;padding:1.25rem 1.5rem;border-radius:12px;background:var(--rs-bg-surface);border:1px solid var(--rs-border);text-decoration:none;color:inherit;transition:border-color .15s,background .15s}
.rs-nav a:hover{border-color:var(--rs-accent);background:var(--rs-bg-hover)}
.rs-nav .nav-icon{font-size:2rem;flex-shrink:0}
.rs-nav .nav-body h3{margin:0 0 .25rem;font-size:1.1rem;color:var(--rs-text-primary)}
.rs-nav .nav-body p{margin:0;font-size:.85rem;color:var(--rs-text-secondary)}
@media(max-width:600px){.rs-hub{margin:1rem auto;padding:0 .75rem}.rs-nav a{padding:1rem;gap:.75rem}.rs-nav .nav-icon{font-size:1.5rem}}
</style>`,
body: `<div class="rs-hub">
<h1>rEvents</h1>
<p>Aggregate events from Luma, Meetup, iCal, and more</p>
<nav class="rs-nav">
<a href="${base}/explore">
<span class="nav-icon">🔍</span>
<div class="nav-body">
<h3>Explore Events</h3>
<p>Browse and search upcoming events from all connected sources</p>
</div>
</a>
<a href="${base}/sources">
<span class="nav-icon">🔗</span>
<div class="nav-body">
<h3>Event Sources</h3>
<p>Connect Luma, Meetup, iCal feeds and manage sync</p>
</div>
</a>
<a href="${REVENTS_PUBLIC_URL}" target="_blank" rel="noopener">
<span class="nav-icon">🎪</span>
<div class="nav-body">
<h3>Open rEvents</h3>
<p>Full standalone app at revents.online</p>
</div>
</a>
</nav>
</div>`,
}));
});
export const eventsModule: RSpaceModule = {
id: "revents",
name: "rEvents",
icon: "🎪",
description: "Event aggregation & discovery from Luma, Meetup, iCal and more",
scoping: { defaultScope: "space", userConfigurable: true },
routes,
standaloneDomain: "revents.online",
landingPage: renderLanding,
externalApp: { url: REVENTS_PUBLIC_URL, name: "rEvents" },
outputPaths: [
{ path: "explore", name: "Explore", icon: "🔍", description: "Browse upcoming events" },
{ path: "sources", name: "Sources", icon: "🔗", description: "Connected event sources" },
],
feeds: [
{
id: "events",
name: "Events",
kind: "data",
description: "Aggregated events from external platforms and manual input",
filterable: true,
},
],
acceptsFeeds: ["data"],
};