165 lines
5.1 KiB
TypeScript
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"],
|
|
};
|