Compare commits

...

1 Commits

Author SHA1 Message Date
Jeff Emmett 96a00a6f36 feat(revents): add events module scaffold 2026-03-16 00:31:42 +01:00
2 changed files with 253 additions and 0 deletions

View File

@ -0,0 +1,89 @@
/**
* rEvents landing page event aggregation & discovery.
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline" style="color:#a78bfa;background:rgba(167,139,250,0.1);border-color:rgba(167,139,250,0.2)">
Event Aggregator
</span>
<h1 class="rl-heading" style="background:linear-gradient(to right,#a78bfa,#ec4899);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">
All your events. One place.
</h1>
<p class="rl-subtitle">
Aggregate events from Luma, Meetup, iCal feeds, and more into a unified stream for your community.
</p>
<p class="rl-subtext">
rEvents is the <span style="color:#a78bfa;font-weight:600">event ingestion layer</span> for the rStack.
Connect external event platforms, parse unstructured text into structured events,
and keep your community's calendar in sync &mdash; automatically.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/revents" class="rl-cta-primary" id="ml-primary"
style="background:linear-gradient(to right,#a78bfa,#ec4899);color:#0b1120">
Try the Demo
</a>
<a href="#features" class="rl-cta-secondary">Learn More</a>
</div>
</div>
<!-- Features (3-card grid) -->
<section class="rl-section" style="border-top:none" id="features">
<div class="rl-container">
<div class="rl-grid-3">
<div class="rl-card rl-card--center" style="padding:2rem">
<div class="rl-icon-box" style="background:rgba(167,139,250,0.12);font-size:1.5rem">
<span style="font-size:1.5rem">&#128279;</span>
</div>
<h3>Aggregate</h3>
<p>Pull events from Luma, Meetup, iCal feeds, and RSS into one unified stream. Sync on demand or on a schedule.</p>
</div>
<div class="rl-card rl-card--center" style="padding:2rem">
<div class="rl-icon-box" style="background:rgba(236,72,153,0.12);font-size:1.5rem">
<span style="font-size:1.5rem">&#10024;</span>
</div>
<h3>Parse</h3>
<p>Paste unstructured text &mdash; emails, messages, flyers &mdash; and extract structured event data with dates, locations, and more.</p>
</div>
<div class="rl-card rl-card--center" style="padding:2rem">
<div class="rl-icon-box" style="background:rgba(96,165,250,0.12);font-size:1.5rem">
<span style="font-size:1.5rem">&#128197;</span>
</div>
<h3>Connect</h3>
<p>Push events to rCal, surface them in rInbox, coordinate across the entire rStack ecosystem seamlessly.</p>
</div>
</div>
</div>
</section>
<!-- Supported Sources -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-section-title" style="text-align:center;margin-bottom:2rem">Supported Sources</h2>
<div class="rl-grid-4">
<div class="rl-card rl-card--center" style="padding:1.5rem">
<div style="font-size:2rem;margin-bottom:0.5rem">&#127876;</div>
<h3 style="font-size:1rem">Luma</h3>
<p style="font-size:0.8rem">lu.ma calendar events</p>
</div>
<div class="rl-card rl-card--center" style="padding:1.5rem">
<div style="font-size:2rem;margin-bottom:0.5rem">&#127941;</div>
<h3 style="font-size:1rem">Meetup</h3>
<p style="font-size:0.8rem">Meetup.com groups</p>
</div>
<div class="rl-card rl-card--center" style="padding:1.5rem">
<div style="font-size:2rem;margin-bottom:0.5rem">&#128197;</div>
<h3 style="font-size:1rem">iCal / ICS</h3>
<p style="font-size:0.8rem">Any .ics feed URL</p>
</div>
<div class="rl-card rl-card--center" style="padding:1.5rem">
<div style="font-size:2rem;margin-bottom:0.5rem">&#9997;</div>
<h3 style="font-size:1rem">Manual / Text</h3>
<p style="font-size:0.8rem">Paste &amp; parse anything</p>
</div>
</div>
</div>
</section>
`;
}

164
modules/revents/mod.ts Normal file
View File

@ -0,0 +1,164 @@
/**
* 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"],
};