Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-02-27 15:37:01 -08:00
commit 18e19ddac8
45 changed files with 3514 additions and 18 deletions

118
modules/books/landing.ts Normal file
View File

@ -0,0 +1,118 @@
/**
* rBooks rich landing page body.
* Returned by landingPage() in the module export;
* the shell wraps it with header, CSS, and analytics.
*/
export function renderLanding(): string {
const demo = "https://demo.rspace.online/rbooks";
return `
<!-- Hero -->
<section class="rl-hero">
<span class="rl-tagline">rBooks</span>
<h1 class="rl-heading">Your Community's Library</h1>
<p class="rl-subtext">
Upload, share, and read PDFs together. A beautiful flipbook reader,
searchable catalog, and community contributions &mdash; all self-hosted.
</p>
<div class="rl-cta-row">
<a href="${demo}" class="rl-cta-primary" id="ml-primary">Start Your Library</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</section>
<!-- How It Works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">How It Works</h2>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Upload PDFs</h3>
<p>Drag-and-drop any PDF. Add title, author, tags, and a license &mdash; it is processed instantly.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>Browse the Library</h3>
<p>Search by title, author, or tag. Featured books rise to the top.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Read with Flipbook</h3>
<p>Open any book in the interactive flipbook reader. Turn pages, zoom in, and bookmark your place.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Features</h2>
<div class="rl-grid-4">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128214;</div>
<h3>Flipbook Reader</h3>
<p>Realistic page-turn animations, zoom, and full-screen mode for a natural reading experience.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#9881;</div>
<h3>PDF Processing</h3>
<p>Automatic page counting, metadata extraction, and optimized delivery to the reader.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128218;</div>
<h3>Space Libraries</h3>
<p>Each community space gets its own curated collection with search and tagging.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128101;</div>
<h3>Community Contributions</h3>
<p>Members upload and share. View counts and download stats track engagement.</p>
</div>
</div>
</div>
</section>
<!-- Ecosystem Integration -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Works With the Ecosystem</h2>
<p class="rl-subtext" style="text-align:center">
rBooks connects to other rSpace modules to extend your library.
</p>
<div class="rl-grid-2">
<div class="rl-integration">
<div class="rl-icon-box">&#128220;</div>
<div>
<h3><a href="/rpubs" style="color:#14b8a6;text-decoration:none">rPubs</a></h3>
<p>Write and publish your own books with the collaborative editor, then list them in rBooks.</p>
</div>
</div>
<div class="rl-integration">
<div class="rl-icon-box">&#128722;</div>
<div>
<h3><a href="/rcart" style="color:#14b8a6;text-decoration:none">rCart</a></h3>
<p>Sell premium PDFs through your community storefront with integrated checkout.</p>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading">Ready to Build Your Library?</h2>
<p class="rl-subtext">
Upload your first PDF and let your community start reading.
</p>
<div class="rl-cta-row">
<a href="${demo}" class="rl-cta-primary">Start Your Library</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back"><a href="/">&larr; Back to rSpace</a></div>`;
}

View File

@ -13,6 +13,7 @@ import { sql } from "../../shared/db/pool";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
import {
verifyEncryptIDToken,
extractToken,
@ -301,6 +302,7 @@ export const booksModule: RSpaceModule = {
description: "Community PDF library with flipbook reader",
routes,
standaloneDomain: "rbooks.online",
landingPage: renderLanding,
feeds: [
{
id: "reading-list",

266
modules/cal/landing.ts Normal file
View File

@ -0,0 +1,266 @@
/**
* rCal landing page relational calendar.
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">rCal</span>
<h1 class="rl-heading">Time is shared.<br>Your calendar should be too.</h1>
<p class="rl-subtext">
A spatiotemporal calendar that couples where and when, supports natural cycles,
and zooms from 30-second moments to geological epochs.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rcal" class="rl-cta-primary" id="ml-primary">Try the Demo</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
<!-- Principles -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Design principles</h2>
<div class="rl-grid-4" style="margin-top:2rem">
<div class="rl-card rl-card--center">
<div class="rl-icon-box"><span style="font-size:1.25rem">&#129309;</span></div>
<h3>Shared by Default</h3>
<p>Calendars belong to spaces, not individuals. Everyone sees the same schedule.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box"><span style="font-size:1.25rem">&#128506;</span></div>
<h3>Spatiotemporal</h3>
<p>Events have places, not just times. Where and when are coupled at every zoom level.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box"><span style="font-size:1.25rem">&#127769;</span></div>
<h3>Natural Cycles</h3>
<p>Lunar phases, solstices, and seasonal rhythms overlaid on the Gregorian grid.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box"><span style="font-size:1.25rem">&#128301;</span></div>
<h3>Multi-Scale Zoom</h3>
<p>Ten zoom levels from 30-second moments to cosmic time. One unified timeline.</p>
</div>
</div>
</div>
</section>
<!-- Why rCal -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Why rCal</h2>
<div class="rl-grid-3" style="margin-top:2rem">
<div class="rl-card">
<h3>Where + When</h3>
<p>Every event carries location context. Zoom into a city and see only what happens there.</p>
</div>
<div class="rl-card">
<h3>Coupled Zoom</h3>
<p>Temporal and spatial zoom are linked. Zoom out in time and the map zooms out too.</p>
</div>
<div class="rl-card">
<h3>Multi-Source Sync</h3>
<p>Pull events from ICS feeds, CalDAV servers, and other r* modules into one view.</p>
</div>
<div class="rl-card">
<h3>Lunar Overlay</h3>
<p>Moon phases computed and displayed alongside events. Plan by natural cycles.</p>
</div>
<div class="rl-card">
<h3>r* Ecosystem Embeds</h3>
<p>rVote deadlines, rWork sprints, rFunds milestones &mdash; all surface as calendar events.</p>
</div>
<div class="rl-card">
<h3>Self-Hosted</h3>
<p>Your data lives on your infrastructure. No vendor lock-in, no surveillance calendars.</p>
</div>
</div>
</div>
</section>
<!-- Temporal Zoom -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Temporal zoom</h2>
<p class="rl-subtext" style="text-align:center">Ten levels of time &mdash; from the blink of an eye to deep time.</p>
<div class="rl-card" style="max-width:700px;margin:2rem auto 0">
<div class="rl-zoom-bar">
<div class="rl-zoom-bar__row">
<span class="rl-zoom-bar__label">0</span>
<div class="rl-zoom-bar__bar" style="width:10%;background:rgba(99,102,241,0.3)">
<span class="rl-zoom-bar__name">Moment</span>
</div>
<span class="rl-zoom-bar__span">30 s</span>
</div>
<div class="rl-zoom-bar__row">
<span class="rl-zoom-bar__label">1</span>
<div class="rl-zoom-bar__bar" style="width:15%;background:rgba(99,102,241,0.28)">
<span class="rl-zoom-bar__name">Minute</span>
</div>
<span class="rl-zoom-bar__span">1&ndash;10 min</span>
</div>
<div class="rl-zoom-bar__row">
<span class="rl-zoom-bar__label">2</span>
<div class="rl-zoom-bar__bar" style="width:22%;background:rgba(99,102,241,0.25)">
<span class="rl-zoom-bar__name">Hour</span>
</div>
<span class="rl-zoom-bar__span">1&ndash;6 hrs</span>
</div>
<div class="rl-zoom-bar__row">
<span class="rl-zoom-bar__label">3</span>
<div class="rl-zoom-bar__bar" style="width:30%;background:rgba(99,102,241,0.22)">
<span class="rl-zoom-bar__name">Day</span>
</div>
<span class="rl-zoom-bar__span">24 hrs</span>
</div>
<div class="rl-zoom-bar__row">
<span class="rl-zoom-bar__label">4</span>
<div class="rl-zoom-bar__bar" style="width:40%;background:rgba(99,102,241,0.20)">
<span class="rl-zoom-bar__name">Week</span>
</div>
<span class="rl-zoom-bar__span">7 days</span>
</div>
<div class="rl-zoom-bar__row">
<span class="rl-zoom-bar__label">5</span>
<div class="rl-zoom-bar__bar" style="width:50%;background:rgba(99,102,241,0.18)">
<span class="rl-zoom-bar__name">Lunar</span>
</div>
<span class="rl-zoom-bar__span">29.5 days</span>
</div>
<div class="rl-zoom-bar__row">
<span class="rl-zoom-bar__label">6</span>
<div class="rl-zoom-bar__bar" style="width:60%;background:rgba(99,102,241,0.16)">
<span class="rl-zoom-bar__name">Season</span>
</div>
<span class="rl-zoom-bar__span">~90 days</span>
</div>
<div class="rl-zoom-bar__row">
<span class="rl-zoom-bar__label">7</span>
<div class="rl-zoom-bar__bar" style="width:70%;background:rgba(99,102,241,0.14)">
<span class="rl-zoom-bar__name">Year</span>
</div>
<span class="rl-zoom-bar__span">365 days</span>
</div>
<div class="rl-zoom-bar__row">
<span class="rl-zoom-bar__label">8</span>
<div class="rl-zoom-bar__bar" style="width:85%;background:rgba(99,102,241,0.12)">
<span class="rl-zoom-bar__name">Epoch</span>
</div>
<span class="rl-zoom-bar__span">decades&ndash;centuries</span>
</div>
<div class="rl-zoom-bar__row">
<span class="rl-zoom-bar__label">9</span>
<div class="rl-zoom-bar__bar" style="width:100%;background:rgba(99,102,241,0.10)">
<span class="rl-zoom-bar__name">Cosmic</span>
</div>
<span class="rl-zoom-bar__span">geological</span>
</div>
</div>
</div>
</div>
</section>
<!-- Calendar Views -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Four views</h2>
<div class="rl-grid-4" style="margin-top:2rem">
<div class="rl-card rl-card--center">
<div class="rl-icon-box"><span style="font-size:1.25rem">&#128197;</span></div>
<h3>Temporal</h3>
<p>Classic calendar grid &mdash; day, week, month. The view you know.</p>
<p style="font-size:0.7rem;color:#64748b;margin-top:0.5rem"><kbd style="background:rgba(255,255,255,0.1);padding:0.1rem 0.3rem;border-radius:3px;font-size:0.65rem">T</kbd> to switch</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box"><span style="font-size:1.25rem">&#128506;</span></div>
<h3>Spatial</h3>
<p>Map view with events pinned to locations. Zoom couples time and space.</p>
<p style="font-size:0.7rem;color:#64748b;margin-top:0.5rem"><kbd style="background:rgba(255,255,255,0.1);padding:0.1rem 0.3rem;border-radius:3px;font-size:0.65rem">S</kbd> to switch</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box"><span style="font-size:1.25rem">&#127769;</span></div>
<h3>Lunar</h3>
<p>Moon-phase overlay with illumination percentages. Plan with natural rhythms.</p>
<p style="font-size:0.7rem;color:#64748b;margin-top:0.5rem"><kbd style="background:rgba(255,255,255,0.1);padding:0.1rem 0.3rem;border-radius:3px;font-size:0.65rem">L</kbd> to switch</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box"><span style="font-size:1.25rem">&#129513;</span></div>
<h3>Context</h3>
<p>Events grouped by r* source module. See your rWork sprints next to rVote deadlines.</p>
<p style="font-size:0.7rem;color:#64748b;margin-top:0.5rem"><kbd style="background:rgba(255,255,255,0.1);padding:0.1rem 0.3rem;border-radius:3px;font-size:0.65rem">C</kbd> to switch</p>
</div>
</div>
</div>
</section>
<!-- Ecosystem Integration -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Ecosystem integration</h2>
<p class="rl-subtext" style="text-align:center">rCal pulls events from across the r* ecosystem automatically.</p>
<div class="rl-grid-3" style="margin-top:2rem">
<div class="rl-integration">
<div class="rl-icon-box" style="flex-shrink:0"><span style="font-size:1.25rem">&#9992;&#65039;</span></div>
<div>
<h3><a href="/rtrips" style="color:#e2e8f0;text-decoration:none">rTrips</a></h3>
<p>Travel itineraries appear as multi-day calendar events with location pins.</p>
</div>
</div>
<div class="rl-integration">
<div class="rl-icon-box" style="flex-shrink:0"><span style="font-size:1.25rem">&#128506;</span></div>
<div>
<h3><a href="/rmaps" style="color:#e2e8f0;text-decoration:none">rMaps</a></h3>
<p>Location-tagged events render on the spatial view map layer.</p>
</div>
</div>
<div class="rl-integration">
<div class="rl-icon-box" style="flex-shrink:0"><span style="font-size:1.25rem">&#128101;</span></div>
<div>
<h3><a href="/rnetwork" style="color:#e2e8f0;text-decoration:none">rNetwork</a></h3>
<p>Meeting events auto-link to participant profiles and relationship context.</p>
</div>
</div>
<div class="rl-integration">
<div class="rl-icon-box" style="flex-shrink:0"><span style="font-size:1.25rem">&#128722;</span></div>
<div>
<h3><a href="/rcart" style="color:#e2e8f0;text-decoration:none">rCart</a></h3>
<p>Group-buy deadlines and fulfillment ETAs show up as calendar milestones.</p>
</div>
</div>
<div class="rl-integration">
<div class="rl-icon-box" style="flex-shrink:0"><span style="font-size:1.25rem">&#128221;</span></div>
<div>
<h3><a href="/rnotes" style="color:#e2e8f0;text-decoration:none">rNotes</a></h3>
<p>Meeting notes link back to the calendar event that spawned them.</p>
</div>
</div>
<div class="rl-integration">
<div class="rl-icon-box" style="flex-shrink:0"><span style="font-size:1.25rem">&#127760;</span></div>
<div>
<h3><a href="/" style="color:#e2e8f0;text-decoration:none">rSpace</a></h3>
<p>Space-level milestones and deadlines aggregate across all modules into one timeline.</p>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading">See time differently</h2>
<p class="rl-subtext">
A calendar that understands place, cycles, and community. Try the demo or create your own space.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rcal" class="rl-cta-primary" id="ml-primary">Open rCal</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back">
<a href="/">&larr; Back to rSpace</a>
</div>`;
}

View File

@ -13,6 +13,7 @@ import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
const routes = new Hono();
@ -394,6 +395,7 @@ export const calModule: RSpaceModule = {
description: "Temporal coordination calendar with lunar, solar, and seasonal systems",
routes,
standaloneDomain: "rcal.online",
landingPage: renderLanding,
feeds: [
{
id: "events",

127
modules/cart/landing.ts Normal file
View File

@ -0,0 +1,127 @@
/**
* rCart landing page group shopping, together.
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">rCart</span>
<h1 class="rl-heading">Group Shopping, Together</h1>
<p class="rl-subtext">
A shared shopping cart for communities. Pool orders, unlock bulk pricing,
and fulfill locally through the cosmolocal provider network.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rcart" class="rl-cta-primary" id="ml-primary">Browse the Shop</a>
<a href="/rpubs" class="rl-cta-secondary">Create with rPubs</a>
</div>
</div>
<!-- How it works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">How it works</h2>
<div class="rl-grid-3" style="margin-top:2rem">
<div class="rl-step">
<span class="rl-step__num">1</span>
<h3>Create a Space</h3>
<p>Your space is your shared shopping context. Members see the same catalog and cart.</p>
</div>
<div class="rl-step">
<span class="rl-step__num">2</span>
<h3>Add Products</h3>
<p>List print-ready artifacts from rPubs, or browse what others have published.</p>
</div>
<div class="rl-step">
<span class="rl-step__num">3</span>
<h3>Pay Together</h3>
<p>Pool orders to hit bulk pricing tiers. Pay with crypto or card. Revenue splits automatically.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Features</h2>
<div class="rl-grid-4" style="margin-top:2rem">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 002 1.61h9.72a2 2 0 002-1.61L23 6H6"/></svg>
</div>
<h3>Universal Cart</h3>
<p>One cart shared across your space. Members add items, pool orders, and hit bulk thresholds together.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></svg>
</div>
<h3>Crypto + Card</h3>
<p>Pay with credit card or cryptocurrency. x402 protocol support for instant crypto payments.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>
</div>
<h3>Real-time Updates</h3>
<p>Order status updates in real time &mdash; from placement through production to delivery.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<h3>Passkey Login</h3>
<p>Passwordless authentication with EncryptID passkeys. One tap to sign in across all r* apps.</p>
</div>
</div>
</div>
</section>
<!-- Fulfillment -->
<section class="rl-section">
<div class="rl-container">
<div class="rl-grid-2">
<div>
<h2 class="rl-heading">Cosmolocal fulfillment</h2>
<p class="rl-subtext" style="margin-bottom:1.5rem">
Every order is matched to the nearest capable provider. Design is global, manufacturing is local.
</p>
<ul class="rl-check-list">
<li><strong>Provider matching</strong> &mdash; automatic routing by capability, location, and cost</li>
<li><strong>Revenue splits</strong> &mdash; creator, community, and provider shares via rFunds</li>
<li><strong>Order tracking</strong> &mdash; real-time status from accepted to delivered</li>
<li><strong>Volume pricing</strong> &mdash; automatic tier detection from pooled orders</li>
</ul>
</div>
<div class="rl-card rl-card--center" style="display:flex;align-items:center;justify-content:center">
<div>
<div class="rl-icon-box" style="margin:0 auto 1rem">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>
</div>
<h3>Nearest Provider Wins</h3>
<p>Less shipping, lower emissions,<br>faster delivery, local jobs.</p>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading">Ready to shop together?</h2>
<p class="rl-subtext">
Browse the catalog, pool orders with your community, and support local providers.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rcart" class="rl-cta-primary" id="ml-primary">Open rCart</a>
<a href="/rpubs" class="rl-cta-secondary">Create with rPubs</a>
</div>
</div>
</section>
<div class="rl-back">
<a href="/">&larr; Back to rSpace</a>
</div>`;
}

View File

@ -15,6 +15,7 @@ import { getModuleInfoList } from "../../shared/module";
import { depositOrderRevenue } from "./flow";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
const routes = new Hono();
@ -460,6 +461,7 @@ export const cartModule: RSpaceModule = {
description: "Cosmolocal print-on-demand shop",
routes,
standaloneDomain: "rcart.online",
landingPage: renderLanding,
feeds: [
{
id: "orders",

120
modules/choices/landing.ts Normal file
View File

@ -0,0 +1,120 @@
/**
* rChoices rich landing page body.
* Returned by landingPage() in the module export;
* the shell wraps it with header, CSS, and analytics.
*/
export function renderLanding(): string {
const demo = "https://demo.rspace.online/rchoices";
return `
<!-- Hero -->
<section class="rl-hero">
<span class="rl-tagline">rChoices</span>
<h1 class="rl-heading">Decide Together, Fairly</h1>
<p class="rl-subtext">
Quadratic voting, ranked choice, and multi-criteria scoring &mdash; all as
interactive shapes on your canvas. Drop a choice, let members vote,
watch results emerge in real time.
</p>
<div class="rl-cta-row">
<a href="${demo}" class="rl-cta-primary" id="ml-primary">Make Better Decisions</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</section>
<!-- How It Works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">How It Works</h2>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Create a Choice Shape</h3>
<p>Pick a voting method, name it, and add options. The shape appears on the canvas.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>Members Vote</h3>
<p>Space members interact with the shape to cast votes, rank preferences, or score criteria.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Results Emerge</h3>
<p>Live tallies update as votes arrive. View charts, rankings, and spider diagrams in real time.</p>
</div>
</div>
</div>
</section>
<!-- Voting Methods -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Voting Methods</h2>
<p class="rl-subtext" style="text-align:center">
Three powerful mechanisms, each designed for different decision contexts.
</p>
<div class="rl-grid-3">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#9878;</div>
<h3>Quadratic Voting</h3>
<p>Express intensity of preference. The cost of additional votes on one option grows exponentially, balancing passion with fairness.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128203;</div>
<h3>Ranked Choice</h3>
<p>Order your preferences from first to last. Instant-runoff tabulation finds the option with the broadest support.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128376;</div>
<h3>Multi-Criteria Scoring</h3>
<p>Score options across weighted attributes. Spider diagrams reveal trade-offs at a glance.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Features</h2>
<div class="rl-grid-4">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#127912;</div>
<h3>Canvas Integration</h3>
<p>Choice shapes live on the canvas alongside notes, images, and other shapes &mdash; full spatial context.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#9889;</div>
<h3>Real-time Results</h3>
<p>Tallies, rankings, and spider charts update live as members vote via Automerge CRDT sync.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128295;</div>
<h3>Configurable Parameters</h3>
<p>Set vote budgets, deadlines, anonymity, and quorum thresholds per choice shape.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#9745;</div>
<h3>rVote Integration</h3>
<p>Connect to <a href="/rvote" style="color:#14b8a6">rVote</a> for formal governance proposals backed by on-canvas choices.</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading">Better Decisions Start Here</h2>
<p class="rl-subtext">
Drop a choice shape on your canvas and let your community weigh in.
</p>
<div class="rl-cta-row">
<a href="${demo}" class="rl-cta-primary">Make Better Decisions</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back"><a href="/">&larr; Back to rSpace</a></div>`;
}

View File

@ -11,6 +11,7 @@
import { Hono } from "hono";
import { renderShell } from "../../server/shell";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
import { getModuleInfoList } from "../../shared/module";
import { getDocumentData } from "../../server/community-store";
@ -67,6 +68,7 @@ export const choicesModule: RSpaceModule = {
description: "Polls, rankings, and multi-criteria scoring",
routes,
standaloneDomain: "rchoices.online",
landingPage: renderLanding,
feeds: [
{
id: "poll-results",

120
modules/data/landing.ts Normal file
View File

@ -0,0 +1,120 @@
/**
* rData rich landing page body.
* Returned by landingPage() in the module export;
* the shell wraps it with header, CSS, and analytics.
*/
export function renderLanding(): string {
const demo = "https://demo.rspace.online/rdata";
return `
<!-- Hero -->
<section class="rl-hero">
<span class="rl-tagline">rData</span>
<h1 class="rl-heading">Privacy-First Analytics</h1>
<p class="rl-subtext">
See how your community engages without tracking individuals.
No cookies, no consent banners, no third-party data &mdash; just
clean, real-time metrics from your own server.
</p>
<div class="rl-cta-row">
<a href="${demo}" class="rl-cta-primary" id="ml-primary">View Analytics</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</section>
<!-- How It Works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">How It Works</h2>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Auto-Instrumented</h3>
<p>Every rSpace module is automatically tracked &mdash; no code changes, no tag managers.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>View Dashboard</h3>
<p>Open rData to see page views, active visitors, referrers, and top content in real time.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Share Insights</h3>
<p>Export stats or embed dashboard widgets in your canvas to keep the community informed.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Features</h2>
<div class="rl-grid-4">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128274;</div>
<h3>No Cookies Required</h3>
<p>Umami uses a privacy-friendly fingerprinting method. Zero cookies, zero local storage.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#9989;</div>
<h3>GDPR Compliant</h3>
<p>No personal data collected. No consent banner needed. Compliant out of the box.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#9889;</div>
<h3>Real-Time Dashboard</h3>
<p>Active visitors, page views, bounce rate, and session duration update live.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#129470;</div>
<h3>Lightweight Script</h3>
<p>Under 2 KB tracker script. No impact on page load performance.</p>
</div>
</div>
</div>
</section>
<!-- Why Self-Hosted -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Why Self-Hosted Analytics?</h2>
<p class="rl-subtext" style="text-align:center">
Compare rData with third-party analytics platforms.
</p>
<div class="rl-grid-3">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128683;</div>
<h3>No Third-Party Data</h3>
<p>Your visitor metrics never leave your server. No ad networks, no data brokers, no surprises.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128683;</div>
<h3>No Cookie Banners</h3>
<p>Because no cookies are set, you skip the annoying consent popups entirely.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#129471;</div>
<h3>Sub-2 KB Script</h3>
<p>Google Analytics loads 45 KB+. The Umami tracker is under 2 KB &mdash; 20x lighter.</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading">Analytics Without the Guilt</h2>
<p class="rl-subtext">
Respect your visitors. Understand your community. Keep your data.
</p>
<div class="rl-cta-row">
<a href="${demo}" class="rl-cta-primary">View Analytics</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back"><a href="/">&larr; Back to rSpace</a></div>`;
}

View File

@ -9,6 +9,7 @@ import { Hono } from "hono";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
const routes = new Hono();
@ -139,6 +140,7 @@ export const dataModule: RSpaceModule = {
description: "Privacy-first analytics for the r* ecosystem",
routes,
standaloneDomain: "rdata.online",
landingPage: renderLanding,
feeds: [
{
id: "analytics",

94
modules/files/landing.ts Normal file
View File

@ -0,0 +1,94 @@
/**
* Files module landing page rich content for rspace.online/rfiles
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">rFiles</span>
<h1 class="rl-heading">Share Files, Your Way</h1>
<p class="rl-subtext">
Upload, organize, and share with public links and memory cards.
Built for communities who want control over their shared files.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rfiles" class="rl-cta-primary" id="ml-primary">Start Sharing</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
<!-- How it works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">How It Works</h2>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Upload Files</h3>
<p>Drag and drop any file type. Images, documents, archives -- everything is welcome.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>Organize in Folders</h3>
<p>Group files by project, topic, or team. Tag and search across your whole library.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Share with Links</h3>
<p>Generate public share links with optional expiration, download limits, and password protection.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">Features</h2>
<p class="rl-subtext" style="text-align:center;">
Everything you need for community file management, without the surveillance.
</p>
<div class="rl-grid-4">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128279;</div>
<h3>Public Share Links</h3>
<p>Create expiring, password-protected, download-limited share links for any file.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128196;</div>
<h3>Memory Cards</h3>
<p>Attach structured metadata cards to files -- notes, context, and tags that travel with the content.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128193;</div>
<h3>Folder Organization</h3>
<p>Nest files in folders, tag freely, and search by name, type, or metadata across your library.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#127760;</div>
<h3>Multi-Space Storage</h3>
<p>Each community space gets its own file library. Share across spaces or keep things private.</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section">
<div class="rl-container" style="text-align:center;">
<h2 class="rl-heading">Ready to share?</h2>
<p class="rl-subtext">
Try the demo or create your own space to start uploading.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rfiles" class="rl-cta-primary">Start Sharing</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back">
<a href="/">&larr; Back to rSpace</a>
</div>`;
}

View File

@ -13,6 +13,7 @@ import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
const routes = new Hono();
@ -384,6 +385,7 @@ export const filesModule: RSpaceModule = {
icon: "\uD83D\uDCC1",
description: "File sharing, share links, and memory cards",
routes,
landingPage: renderLanding,
standaloneDomain: "rfiles.online",
feeds: [
{

186
modules/forum/landing.ts Normal file
View File

@ -0,0 +1,186 @@
/**
* Forum module landing page static HTML, no React.
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline" style="color:#818cf8;background:rgba(129,140,248,0.1);border-color:rgba(129,140,248,0.2)">rForum</span>
<h1 class="rl-heading" style="background:linear-gradient(135deg,#a5b4fc,#c4b5fd,#d8b4fe);-webkit-background-clip:text;background-clip:text">
Deploy Your Own Discourse Forum in Minutes
</h1>
<p class="rl-subtext">
Automated cloud provisioning for self-hosted Discourse.
No DevOps required. Choose your region, configure your settings, and go live.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rforum" class="rl-cta-primary" id="ml-primary"
style="background:linear-gradient(135deg,#4f46e5,#6d28d9)">Get Started</a>
<a href="#pricing" class="rl-cta-secondary">View Pricing</a>
</div>
</div>
<!-- How it Works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;background:linear-gradient(135deg,#a5b4fc,#c4b5fd);-webkit-background-clip:text;background-clip:text">How It Works</h2>
<div class="rl-grid-3" style="margin-top:2rem">
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(99,102,241,0.12);color:#818cf8">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h3>1. Configure</h3>
<p>
Choose your subdomain, server region, and instance size.
Add SMTP credentials for email delivery. Set your admin email.
</p>
</div>
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(139,92,246,0.12);color:#a78bfa">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M12 5l7 7-7 7" />
</svg>
</div>
<h3>2. Provision</h3>
<p>
We create a cloud server on Hetzner, install Discourse,
configure SSL via Let's Encrypt, and set up DNS. Takes about 10-15 minutes.
</p>
</div>
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(168,85,247,0.12);color:#c084fc">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<h3>3. Go Live</h3>
<p>
Your forum is ready. Log in as admin, customize your community,
invite members, and start conversations. Full SSH access included.
</p>
</div>
</div>
</div>
</section>
<!-- Pricing -->
<section class="rl-section rl-section--alt" id="pricing">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;background:linear-gradient(135deg,#a5b4fc,#c4b5fd);-webkit-background-clip:text;background-clip:text">
Transparent Pricing
</h2>
<p class="rl-subtext" style="text-align:center">
Pay only for the cloud server. Hetzner pricing passed through directly &mdash; no markup.
</p>
<div class="rl-grid-3" style="margin-top:2rem">
<!-- Starter -->
<div class="rl-card">
<h3>Starter</h3>
<p style="font-size:0.82rem;color:#94a3b8;margin-bottom:1rem">Small communities</p>
<div style="font-size:2rem;font-weight:700;color:#e2e8f0;margin-bottom:0.25rem">
&euro;3.79<span style="font-size:0.875rem;font-weight:400;color:#94a3b8">/mo</span>
</div>
<p style="font-size:0.75rem;color:#64748b;margin-bottom:1.25rem">Hetzner CX22</p>
<ul class="rl-check-list">
<li>2 vCPU cores</li>
<li>4 GB RAM</li>
<li>40 GB NVMe SSD</li>
<li>Up to ~500 users</li>
</ul>
</div>
<!-- Standard -->
<div class="rl-card" style="border-color:rgba(99,102,241,0.5)">
<span class="rl-badge" style="background:#6366f1;margin-bottom:0.5rem">RECOMMENDED</span>
<h3>Standard</h3>
<p style="font-size:0.82rem;color:#94a3b8;margin-bottom:1rem">Growing communities</p>
<div style="font-size:2rem;font-weight:700;color:#e2e8f0;margin-bottom:0.25rem">
&euro;6.80<span style="font-size:0.875rem;font-weight:400;color:#94a3b8">/mo</span>
</div>
<p style="font-size:0.75rem;color:#64748b;margin-bottom:1.25rem">Hetzner CX32</p>
<ul class="rl-check-list">
<li>4 vCPU cores</li>
<li>8 GB RAM</li>
<li>80 GB NVMe SSD</li>
<li>Up to ~2,000 users</li>
</ul>
</div>
<!-- Performance -->
<div class="rl-card">
<h3>Performance</h3>
<p style="font-size:0.82rem;color:#94a3b8;margin-bottom:1rem">Large communities</p>
<div style="font-size:2rem;font-weight:700;color:#e2e8f0;margin-bottom:0.25rem">
&euro;13.80<span style="font-size:0.875rem;font-weight:400;color:#94a3b8">/mo</span>
</div>
<p style="font-size:0.75rem;color:#64748b;margin-bottom:1.25rem">Hetzner CX42</p>
<ul class="rl-check-list">
<li>8 vCPU cores</li>
<li>16 GB RAM</li>
<li>160 GB NVMe SSD</li>
<li>Up to ~10,000 users</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;background:linear-gradient(135deg,#a5b4fc,#c4b5fd);-webkit-background-clip:text;background-clip:text">
What You Get
</h2>
<div class="rl-grid-3" style="margin-top:2rem">
<div class="rl-card">
<h3>Automated SSL</h3>
<p>Let's Encrypt certificates provisioned automatically during setup.</p>
</div>
<div class="rl-card">
<h3>Multiple Regions</h3>
<p>Deploy in Germany, Finland, or the US East/West Coast.</p>
</div>
<div class="rl-card">
<h3>Full SSH Access</h3>
<p>Your server, your rules. SSH in anytime for custom configuration.</p>
</div>
<div class="rl-card">
<h3>One-Click Updates</h3>
<p>Discourse's built-in admin panel handles version upgrades.</p>
</div>
<div class="rl-card">
<h3>DNS Management</h3>
<p>Automatic DNS setup for *.rforum.online subdomains.</p>
</div>
<div class="rl-card">
<h3>Real-Time Logs</h3>
<p>Watch your forum provision step-by-step in the dashboard.</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading" style="background:linear-gradient(135deg,#a5b4fc,#c4b5fd);-webkit-background-clip:text;background-clip:text">
Ready to launch your community?
</h2>
<p class="rl-subtext">
Deploy a production Discourse forum in under 15 minutes. No DevOps experience needed.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rforum" class="rl-cta-primary"
style="background:linear-gradient(135deg,#4f46e5,#6d28d9)">Launch Your Forum</a>
</div>
</div>
</section>
<div class="rl-back"><a href="/">&larr; Back to rSpace</a></div>
`;
}

View File

@ -12,6 +12,7 @@ import { getModuleInfoList } from "../../shared/module";
import { provisionInstance, destroyInstance } from "./lib/provisioner";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
const routes = new Hono();
@ -175,6 +176,7 @@ export const forumModule: RSpaceModule = {
icon: "\uD83D\uDCAC",
description: "Deploy and manage Discourse forums",
routes,
landingPage: renderLanding,
standaloneDomain: "rforum.online",
feeds: [
{

117
modules/funds/landing.ts Normal file
View File

@ -0,0 +1,117 @@
/**
* Funds module landing page rich content for rspace.online/rfunds
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">rFunds</span>
<h1 class="rl-heading">Visualize Your Community's Money</h1>
<p class="rl-subtext">
Budget flows, river visualization, and conviction funding.
Watch resources move through your community in real time.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rfunds" class="rl-cta-primary" id="ml-primary">See Your Funds Flow</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
<!-- How it works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">How It Works</h2>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Define Budget Streams</h3>
<p>Create named flows between funding pools. Set rates, thresholds, and allocation rules.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>Watch Money Flow</h3>
<p>The river visualization animates budget movement in real time -- deposits, withdrawals, and funnels.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Govern Allocation</h3>
<p>Community members signal preferences through conviction funding. Resources flow where attention goes.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">Features</h2>
<p class="rl-subtext" style="text-align:center;">
Transparent treasury management for communities of any size.
</p>
<div class="rl-grid-4">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#127754;</div>
<h3>River Visualization</h3>
<p>Animated flow diagram showing how funds move between pools, funnels, and outcomes in real time.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128161;</div>
<h3>Conviction Funding</h3>
<p>Continuous signaling lets community members express preferences that compound over time.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128168;</div>
<h3>Budget Streams</h3>
<p>Named flows between funding pools with configurable rates, thresholds, and activation rules.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128202;</div>
<h3>Treasury Dashboard</h3>
<p>Full overview of balances, inflows, outflows, and historical transaction data for your community.</p>
</div>
</div>
</div>
</section>
<!-- Integration -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">Ecosystem Integration</h2>
<div class="rl-grid-2" style="max-width:700px;margin:0 auto;">
<div class="rl-integration">
<div class="rl-icon-box">&#128176;</div>
<div>
<h3>rWallet</h3>
<p>Connects to <a href="/rwallet" style="color:#14b8a6;">rWallet</a> for on-chain balances and wallet-based treasury tracking.</p>
</div>
</div>
<div class="rl-integration">
<div class="rl-icon-box">&#128499;</div>
<div>
<h3>rVote</h3>
<p>Pairs with <a href="/rvote" style="color:#14b8a6;">rVote</a> for governance decisions that direct fund allocation.</p>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center;">
<h2 class="rl-heading">See your community's funds in motion</h2>
<p class="rl-subtext">
Try the demo or create a space to set up your own budget flows.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rfunds" class="rl-cta-primary">See Your Funds Flow</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back">
<a href="/">&larr; Back to rSpace</a>
</div>`;
}

View File

@ -12,6 +12,7 @@ import { renderShell } from "../../server/shell";
import type { RSpaceModule } from "../../shared/module";
import { getModuleInfoList } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
const FLOW_SERVICE_URL = process.env.FLOW_SERVICE_URL || "http://payment-flow:3010";
@ -245,6 +246,7 @@ export const fundsModule: RSpaceModule = {
icon: "\uD83C\uDF0A",
description: "Budget flows, river visualization, and treasury management",
routes,
landingPage: renderLanding,
standaloneDomain: "rfunds.online",
feeds: [
{

153
modules/inbox/landing.ts Normal file
View File

@ -0,0 +1,153 @@
/**
* Inbox module landing page static HTML, no React.
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline" style="color:#06b6d4;background:rgba(6,182,212,0.1);border-color:rgba(6,182,212,0.2)">rInbox</span>
<h1 class="rl-heading" style="background:linear-gradient(135deg,#67e8f9,#93c5fd,#c4b5fd);-webkit-background-clip:text;background-clip:text">
Collaborative Multi-Sig Inbox
</h1>
<p class="rl-subtext">
A shared email client where teams read, discuss, and approve messages
together &mdash; with cryptographic multi-signature workflows before anything gets sent.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rinbox" class="rl-cta-primary" id="ml-primary"
style="background:linear-gradient(135deg,#0891b2,#0e7490)">Open Inbox</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
<!-- How it Works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;background:linear-gradient(135deg,#67e8f9,#93c5fd);-webkit-background-clip:text;background-clip:text">How It Works</h2>
<div class="rl-grid-3" style="margin-top:2rem">
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(6,182,212,0.12);color:#06b6d4">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h3>1. Connect Mailbox</h3>
<p>
Your team shares one real email address. Every member sees every thread
in real-time &mdash; no forwarding, no BCC chains, no lost context.
</p>
</div>
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(59,130,246,0.12);color:#3b82f6">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z" />
</svg>
</div>
<h3>2. Triage Together</h3>
<p>
Comment on any thread privately before replying. Tag members,
discuss strategy, and reach consensus &mdash; all alongside the original message.
</p>
</div>
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(139,92,246,0.12);color:#8b5cf6">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h3>3. Approve with Multi-Sig</h3>
<p>
Outbound emails require M-of-N cryptographic signatures before sending.
Every approval is auditable &mdash; no rogue replies.
</p>
</div>
</div>
</div>
</section>
<!-- Collaboration Features -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;background:linear-gradient(135deg,#67e8f9,#93c5fd);-webkit-background-clip:text;background-clip:text">
Collaboration Features
</h2>
<p class="rl-subtext" style="text-align:center">Built for collective intelligence, not individual productivity.</p>
<div class="rl-grid-2" style="margin-top:2rem">
<div class="rl-card">
<h3 style="color:#06b6d4;margin-bottom:0.75rem">Real-Time Collaboration</h3>
<ul class="rl-check-list">
<li><strong>Threaded Comments</strong> &mdash; discuss any email privately before replying</li>
<li><strong>Draft Reviews</strong> &mdash; collaborative draft editing with inline suggestions</li>
<li><strong>Forwarding Rules</strong> &mdash; route incoming messages by sender, subject, or tag</li>
<li><strong>Inline Signatures</strong> &mdash; per-mailbox signature templates with merge fields</li>
<li><strong>Read Receipts</strong> &mdash; see who has read each thread and when</li>
</ul>
</div>
<div class="rl-card">
<h3 style="color:#8b5cf6;margin-bottom:0.75rem">Approval Workflows</h3>
<ul class="rl-check-list">
<li><strong>Multi-sig sending</strong> &mdash; outbound emails require M-of-N approval</li>
<li><strong>Configurable thresholds</strong> &mdash; set approval requirements per mailbox</li>
<li><strong>Audit trail</strong> &mdash; every approval, rejection, and edit is logged</li>
<li><strong>Role-based access</strong> &mdash; Admin, Reviewer, Approver, Viewer, Bot</li>
<li><strong>Deadline alerts</strong> &mdash; notifications when approvals are waiting</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Team Roles -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;background:linear-gradient(135deg,#67e8f9,#93c5fd);-webkit-background-clip:text;background-clip:text">
Team Roles
</h2>
<p class="rl-subtext" style="text-align:center">Fine-grained access control for every team member.</p>
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:0.75rem;margin-top:2rem">
<div class="rl-card rl-card--center" style="padding:1.25rem">
<p style="font-size:0.875rem;font-weight:600;color:#06b6d4">Admin</p>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.35rem">Full control, manage members</p>
</div>
<div class="rl-card rl-card--center" style="padding:1.25rem">
<p style="font-size:0.875rem;font-weight:600;color:#3b82f6">Reviewer</p>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.35rem">Read, comment, suggest edits</p>
</div>
<div class="rl-card rl-card--center" style="padding:1.25rem">
<p style="font-size:0.875rem;font-weight:600;color:#8b5cf6">Approver</p>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.35rem">Sign outbound emails</p>
</div>
<div class="rl-card rl-card--center" style="padding:1.25rem">
<p style="font-size:0.875rem;font-weight:600;color:#94a3b8">Viewer</p>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.35rem">Read-only access</p>
</div>
<div class="rl-card rl-card--center" style="padding:1.25rem">
<p style="font-size:0.875rem;font-weight:600;color:#94a3b8">Bot</p>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.35rem">Automated integrations</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading" style="background:linear-gradient(135deg,#67e8f9,#93c5fd);-webkit-background-clip:text;background-clip:text">
Start collaborating on email
</h2>
<p class="rl-subtext">
Sign in with your passkey to join a shared inbox.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rinbox" class="rl-cta-primary"
style="background:linear-gradient(135deg,#0891b2,#0e7490)">Open Inbox</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back"><a href="/">&larr; Back to rSpace</a></div>
`;
}

View File

@ -13,6 +13,7 @@ import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
const routes = new Hono();
@ -599,6 +600,7 @@ export const inboxModule: RSpaceModule = {
icon: "\u{1F4E8}",
description: "Collaborative email with multisig approval",
routes,
landingPage: renderLanding,
standaloneDomain: "rinbox.online",
feeds: [
{

320
modules/maps/landing.ts Normal file
View File

@ -0,0 +1,320 @@
/**
* Maps module landing page static HTML, no React.
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline" style="color:#10b981;background:rgba(16,185,129,0.1);border-color:rgba(16,185,129,0.2)">rMaps</span>
<h1 class="rl-heading" style="background:linear-gradient(135deg,#6ee7b7,#2dd4bf,#67e8f9);-webkit-background-clip:text;background-clip:text">
Real-Time Collaborative Maps
</h1>
<p class="rl-subtext">
Share live locations, navigate indoor and outdoor spaces, coordinate meetups
&mdash; all from the browser. No app install. No tracking. No data collection.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rmaps" class="rl-cta-primary" id="ml-primary"
style="background:linear-gradient(135deg,#059669,#0d9488)">Try the Demo</a>
<a href="/create-space" class="rl-cta-secondary">Create a Room</a>
</div>
<p style="font-size:0.82rem;color:#64748b;margin-top:1rem">No sign-up required to join. Works on any device.</p>
</div>
<!-- Core Features -->
<section class="rl-section">
<div class="rl-container">
<span class="rl-tagline" style="color:#10b981;background:rgba(16,185,129,0.1);border-color:rgba(16,185,129,0.2);display:block;text-align:center;margin:0 auto 1rem">Core Features</span>
<h2 class="rl-heading" style="text-align:center;background:linear-gradient(135deg,#6ee7b7,#2dd4bf);-webkit-background-clip:text;background-clip:text">
Everything you need to find your friends
</h2>
<div class="rl-grid-4" style="margin-top:2rem">
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(16,185,129,0.12);color:#10b981">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
</svg>
</div>
<h3>Live GPS Sharing</h3>
<p>
Real-time location updates via WebSocket. See everyone on the map
as they move, with stale detection and high-accuracy fallback.
</p>
</div>
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(6,182,212,0.12);color:#06b6d4">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 6.75V15m6-6v8.25m.503 3.498l4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 00-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0z" />
</svg>
</div>
<h3>Indoor + Outdoor Nav</h3>
<p>
Turn-by-turn routing via OSRM outdoors, seamless switch to c3nav
for indoor venues. Multi-floor, level-aware navigation.
</p>
</div>
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(99,102,241,0.12);color:#6366f1">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 3v1.5M3 21v-6m0 0l2.77-.693a9 9 0 016.208.682l.108.054a9 9 0 006.086.71l3.114-.732a48.524 48.524 0 01-.005-10.499l-3.11.732a9 9 0 01-6.085-.711l-.108-.054a9 9 0 00-6.208-.682L3 4.5M3 15V4.5" />
</svg>
</div>
<h3>Meeting Points</h3>
<p>
Drop waypoints for meetups, events, and points of interest.
Search by address, share coordinates, or pin from your location.
</p>
</div>
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(244,63,94,0.12);color:#f43f5e">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
</svg>
</div>
<h3>Privacy First</h3>
<p>
Ghost mode, precision levels, one-toggle location sharing.
Zero tracking, zero data collection. You control who sees you.
</p>
</div>
</div>
</div>
</section>
<!-- What Makes rMaps Different -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<span class="rl-tagline" style="color:#2dd4bf;background:rgba(45,212,191,0.1);border-color:rgba(45,212,191,0.2);display:block;text-align:center;margin:0 auto 1rem">Beyond Google Maps</span>
<h2 class="rl-heading" style="text-align:center;background:linear-gradient(135deg,#6ee7b7,#2dd4bf);-webkit-background-clip:text;background-clip:text">
What makes rMaps different
</h2>
<div class="rl-grid-2" style="margin-top:2rem">
<div class="rl-integration">
<div class="rl-icon-box" style="background:rgba(16,185,129,0.12);color:#10b981;font-size:1.25rem;flex-shrink:0">
&#x1F3D5;&#xFE0F;
</div>
<div>
<h3>CCC Event Integration</h3>
<p>
Native c3nav integration for 39C3, Camp, and other CCC events. Indoor maps with
multi-floor routing, venue bounds detection, and automatic map switching
when you walk inside.
</p>
</div>
</div>
<div class="rl-integration">
<div class="rl-icon-box" style="background:rgba(6,182,212,0.12);color:#06b6d4;font-size:1.25rem;flex-shrink:0">
&#x1F4E1;
</div>
<div>
<h3>Location Pinging</h3>
<p>
Request a friend's location with one tap. Push notifications via
Web Push API with vibration alerts. Works even when the app is
backgrounded via service worker.
</p>
</div>
</div>
<div class="rl-integration">
<div class="rl-icon-box" style="background:rgba(99,102,241,0.12);color:#6366f1;font-size:1.25rem;flex-shrink:0">
&#x1F4E6;
</div>
<div>
<h3>Google Maps Import</h3>
<p>
Import your saved places from Google Takeout ZIP exports. GeoJSON
parsing, auto-emoji mapping by place type, and preview before
importing.
</p>
</div>
</div>
<div class="rl-integration">
<div class="rl-icon-box" style="background:rgba(245,158,11,0.12);color:#f59e0b;font-size:1.25rem;flex-shrink:0">
&#x1F4F1;
</div>
<div>
<h3>PWA &amp; Offline Mode</h3>
<p>
Install as a native app. Three-tier service worker caching keeps
maps accessible offline with up to 500 cached tiles. Background
sync handles location updates.
</p>
</div>
</div>
<div class="rl-integration">
<div class="rl-icon-box" style="background:rgba(139,92,246,0.12);color:#8b5cf6;font-size:1.25rem;flex-shrink:0">
&#x1F517;
</div>
<div>
<h3>Instant Room Sharing</h3>
<p>
Generate a QR code or shareable link for any room. Friends scan or tap
to join instantly &mdash; no account needed, no app download.
Native share dialog on mobile.
</p>
</div>
</div>
<div class="rl-integration">
<div class="rl-icon-box" style="background:rgba(244,63,94,0.12);color:#f43f5e;font-size:1.25rem;flex-shrink:0">
&#x1F504;
</div>
<div>
<h3>Conflict-Free Sync</h3>
<p>
Automerge CRDT architecture ensures everyone sees the same map state,
even through disconnections. WebSocket real-time sync with automatic
reconnection and state recovery.
</p>
</div>
</div>
</div>
</div>
</section>
<!-- How it Works -->
<section class="rl-section">
<div class="rl-container">
<span class="rl-tagline" style="color:#06b6d4;background:rgba(6,182,212,0.1);border-color:rgba(6,182,212,0.2);display:block;text-align:center;margin:0 auto 1rem">How It Works</span>
<h2 class="rl-heading" style="text-align:center;background:linear-gradient(135deg,#6ee7b7,#2dd4bf);-webkit-background-clip:text;background-clip:text">
Three steps to find your crew
</h2>
<div class="rl-grid-3" style="margin-top:2rem">
<div class="rl-step">
<div class="rl-step__num" style="background:rgba(16,185,129,0.15);color:#10b981">1</div>
<h3>Create a Map Room</h3>
<p>
Sign in and name your room. Get a shareable link or a custom slug.
</p>
</div>
<div class="rl-step">
<div class="rl-step__num" style="background:rgba(6,182,212,0.15);color:#06b6d4">2</div>
<h3>Share with Friends</h3>
<p>
Send the link or scan the QR code. Friends join from any browser
&mdash; no app download, no account creation needed.
</p>
</div>
<div class="rl-step">
<div class="rl-step__num" style="background:rgba(99,102,241,0.15);color:#6366f1">3</div>
<h3>Navigate Together</h3>
<p>
See everyone in real time. Drop meeting points, get turn-by-turn
directions, and ping friends when you need to regroup.
</p>
</div>
</div>
</div>
</section>
<!-- Use Cases -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<span class="rl-tagline" style="color:#6366f1;background:rgba(99,102,241,0.1);border-color:rgba(99,102,241,0.2);display:block;text-align:center;margin:0 auto 1rem">Built For</span>
<h2 class="rl-heading" style="text-align:center;background:linear-gradient(135deg,#6ee7b7,#2dd4bf);-webkit-background-clip:text;background-clip:text">
Maps for every gathering
</h2>
<div class="rl-grid-3" style="margin-top:2rem">
<div class="rl-card rl-card--center">
<div style="font-size:2rem;margin-bottom:0.75rem">&#x1F3D5;&#xFE0F;</div>
<h3>Festivals &amp; Camps</h3>
<p>
Navigate massive venues with indoor maps. Find stages, food courts,
and your crew across multi-day events like CCC Camp.
</p>
</div>
<div class="rl-card rl-card--center">
<div style="font-size:2rem;margin-bottom:0.75rem">&#x1F3D9;&#xFE0F;</div>
<h3>City Exploration</h3>
<p>
Exploring a new city with friends? Share locations, drop pins at
restaurants and landmarks, import your Google Maps saved places.
</p>
</div>
<div class="rl-card rl-card--center">
<div style="font-size:2rem;margin-bottom:0.75rem">&#x1F91D;</div>
<h3>Group Coordination</h3>
<p>
Conferences, retreats, team offsites. Set meeting points, ping
stragglers, and get walking directions to the next session.
</p>
</div>
</div>
<p style="text-align:center;font-size:0.82rem;color:#64748b;margin-top:1.5rem">
First built for <a href="https://events.ccc.de/" target="_blank" rel="noopener noreferrer" style="color:rgba(16,185,129,0.7)">CCC events</a> &mdash; now for any gathering.
</p>
</div>
</section>
<!-- Technical Highlights -->
<section class="rl-section">
<div class="rl-container">
<span class="rl-tagline" style="color:#94a3b8;background:rgba(100,116,139,0.1);border-color:rgba(100,116,139,0.2);display:block;text-align:center;margin:0 auto 1rem">Under the Hood</span>
<h2 class="rl-heading" style="text-align:center;background:linear-gradient(135deg,#6ee7b7,#2dd4bf);-webkit-background-clip:text;background-clip:text">
Built on open standards
</h2>
<div class="rl-grid-4" style="margin-top:2rem">
<div class="rl-card rl-card--center" style="padding:1rem">
<p style="font-size:0.875rem;font-weight:600;color:#e2e8f0">MapLibre GL</p>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.25rem">Open-source maps</p>
</div>
<div class="rl-card rl-card--center" style="padding:1rem">
<p style="font-size:0.875rem;font-weight:600;color:#e2e8f0">OSRM</p>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.25rem">Outdoor routing</p>
</div>
<div class="rl-card rl-card--center" style="padding:1rem">
<p style="font-size:0.875rem;font-weight:600;color:#e2e8f0">c3nav</p>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.25rem">Indoor navigation</p>
</div>
<div class="rl-card rl-card--center" style="padding:1rem">
<p style="font-size:0.875rem;font-weight:600;color:#e2e8f0">Automerge</p>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.25rem">CRDT sync</p>
</div>
<div class="rl-card rl-card--center" style="padding:1rem">
<p style="font-size:0.875rem;font-weight:600;color:#e2e8f0">Web Push</p>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.25rem">Notifications</p>
</div>
<div class="rl-card rl-card--center" style="padding:1rem">
<p style="font-size:0.875rem;font-weight:600;color:#e2e8f0">Service Worker</p>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.25rem">Offline &amp; PWA</p>
</div>
<div class="rl-card rl-card--center" style="padding:1rem">
<p style="font-size:0.875rem;font-weight:600;color:#e2e8f0">WebSocket</p>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.25rem">Real-time sync</p>
</div>
<div class="rl-card rl-card--center" style="padding:1rem">
<p style="font-size:0.875rem;font-weight:600;color:#e2e8f0">EncryptID</p>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.25rem">Identity &amp; auth</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading" style="background:linear-gradient(135deg,#6ee7b7,#2dd4bf);-webkit-background-clip:text;background-clip:text">
Ready to find your crew?
</h2>
<p class="rl-subtext">Create a map room and share the link. That's it.</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rmaps" class="rl-cta-primary"
style="background:linear-gradient(135deg,#059669,#0d9488)">Try the Demo</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back"><a href="/">&larr; Back to rSpace</a></div>
`;
}

View File

@ -10,6 +10,7 @@ import { Hono } from "hono";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
const routes = new Hono();
@ -167,6 +168,7 @@ export const mapsModule: RSpaceModule = {
icon: "\u{1F5FA}",
description: "Real-time collaborative location sharing and indoor/outdoor maps",
routes,
landingPage: renderLanding,
standaloneDomain: "rmaps.online",
feeds: [
{

183
modules/notes/landing.ts Normal file
View File

@ -0,0 +1,183 @@
/**
* Notes module landing page static HTML, no React.
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline" style="color:#f59e0b;background:rgba(245,158,11,0.1);border-color:rgba(245,158,11,0.2)">rNotes</span>
<h1 class="rl-heading" style="background:linear-gradient(135deg,#f59e0b,#f97316);-webkit-background-clip:text;background-clip:text">
Capture Everything, Find Anything, and Share your Insights
</h1>
<p class="rl-subtext">
Notes, clips, voice recordings, and live transcription &mdash; all in one place.
Speak and watch your words appear in real time, or drop in audio and video files to transcribe offline.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rnotes" class="rl-cta-primary" id="ml-primary"
style="background:linear-gradient(135deg,#f59e0b,#d97706)">Open Notebook</a>
<a href="https://demo.rspace.online/rnotes" class="rl-cta-secondary"
style="border-color:rgba(34,197,94,0.3);color:#4ade80">Unlock Article</a>
<a href="https://demo.rspace.online/rnotes" class="rl-cta-secondary"
style="border-color:rgba(139,92,246,0.3);color:#a78bfa">Transcribe</a>
</div>
</div>
<!-- How it works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;background:linear-gradient(135deg,#f59e0b,#f97316);-webkit-background-clip:text;background-clip:text">How It Works</h2>
<div class="rl-grid-4">
<div class="rl-step">
<div class="rl-step__num" style="background:rgba(245,158,11,0.1);color:#f59e0b">1</div>
<h3>Live Transcribe</h3>
<p>Speak and watch your words appear in real time. WebSocket streaming with live timestamps.</p>
</div>
<div class="rl-step">
<div class="rl-step__num" style="background:rgba(245,158,11,0.1);color:#f59e0b">2</div>
<h3>Audio &amp; Video</h3>
<p>Drop in audio or video files and get a full transcript. Powered by NVIDIA Parakeet &mdash; runs entirely in your browser.</p>
</div>
<div class="rl-step">
<div class="rl-step__num" style="background:rgba(245,158,11,0.1);color:#f59e0b">3</div>
<h3>Notebooks &amp; Tags</h3>
<p>Organize transcripts into notebooks alongside notes, clips, code, and files. Tag freely, search everything.</p>
</div>
<div class="rl-step">
<div class="rl-step__num" style="background:rgba(245,158,11,0.1);color:#f59e0b">4</div>
<h3>Private &amp; Offline</h3>
<p>Parakeet.js runs entirely in the browser. Your audio never leaves your device &mdash; full offline support.</p>
</div>
</div>
</div>
</section>
<!-- Memory Cards -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;background:linear-gradient(135deg,#f59e0b,#f97316);-webkit-background-clip:text;background-clip:text">Memory Cards</h2>
<p class="rl-subtext" style="text-align:center">
Every note is a Memory Card &mdash; a typed, structured unit of knowledge with hierarchy,
properties, and attachments. Designed for round-trip interoperability with Logseq.
</p>
<div class="rl-grid-3">
<!-- 7 Card Types -->
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(245,158,11,0.12);color:#f59e0b">
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
</svg>
</div>
<h3>7 Card Types</h3>
<div style="display:flex;flex-wrap:wrap;gap:0.35rem;margin-bottom:0.75rem">
<span class="rl-badge" style="background:rgba(245,158,11,0.2);color:#f59e0b">note</span>
<span class="rl-badge" style="background:rgba(59,130,246,0.2);color:#3b82f6">link</span>
<span class="rl-badge" style="background:rgba(34,197,94,0.2);color:#22c55e">task</span>
<span class="rl-badge" style="background:rgba(234,179,8,0.2);color:#eab308">idea</span>
<span class="rl-badge" style="background:rgba(168,85,247,0.2);color:#a855f7">person</span>
<span class="rl-badge" style="background:rgba(236,72,153,0.2);color:#ec4899">reference</span>
<span class="rl-badge" style="background:rgba(100,116,139,0.2);color:#94a3b8">file</span>
</div>
<p>Each card type has distinct styling and behavior. Typed notes surface in filtered views and canvas visualizations.</p>
</div>
<!-- Hierarchy & Properties -->
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(245,158,11,0.12);color:#f59e0b">
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg>
</div>
<h3>Hierarchy &amp; Properties</h3>
<p>
Nest cards under parents to build knowledge trees. Add structured
<code style="font-size:0.8rem;color:rgba(245,158,11,0.8)">key:: value</code>
properties &mdash; compatible with Logseq's property syntax.
</p>
<div style="margin-top:0.5rem;font-family:monospace;font-size:0.75rem;color:#64748b;line-height:1.7">
<div><span style="color:#94a3b8">type::</span> idea</div>
<div><span style="color:#94a3b8">status::</span> doing</div>
<div><span style="color:#94a3b8">tags::</span> #research, #web3</div>
</div>
</div>
<!-- Logseq Import/Export -->
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(245,158,11,0.12);color:#f59e0b">
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
</div>
<h3>Logseq Import &amp; Export</h3>
<p>
Export your notebooks as Logseq-compatible ZIP archives. Import a Logseq graph and keep your pages,
properties, tags, and hierarchy intact.
</p>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.5rem">
Round-trip fidelity: card types, tags, attachments, and parent-child structure all survive the journey.
</p>
</div>
<!-- Dual Format Storage -->
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(245,158,11,0.12);color:#f59e0b">
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
</div>
<h3>Dual Format Storage</h3>
<p>
Every card stores rich TipTap JSON for editing and portable Markdown for search, export, and interoperability.
Write once, read anywhere.
</p>
</div>
<!-- Structured Attachments -->
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(245,158,11,0.12);color:#f59e0b">
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
</svg>
</div>
<h3>Structured Attachments</h3>
<p>
Attach images, PDFs, audio, and files to any card with roles (primary, preview, supporting) and captions.
Thumbnails render inline.
</p>
</div>
<!-- FUN Model -->
<div class="rl-card">
<div class="rl-icon-box" style="background:rgba(245,158,11,0.12);color:#f59e0b">
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
</div>
<h3>FUN, Not CRUD</h3>
<p>
<span style="color:#f59e0b;font-weight:600">F</span>orget,
<span style="color:#f59e0b;font-weight:600">U</span>pdate,
<span style="color:#f59e0b;font-weight:600">N</span>ew &mdash;
nothing is permanently destroyed. Forgotten cards are archived and can be remembered at any time.
</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading" style="background:linear-gradient(135deg,#f59e0b,#f97316);-webkit-background-clip:text;background-clip:text">Start capturing</h2>
<p class="rl-subtext">Notes, voice, clips, and code &mdash; all in one notebook.</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rnotes" class="rl-cta-primary"
style="background:linear-gradient(135deg,#f59e0b,#d97706)">Open Notebook</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back"><a href="/">&larr; Back to rSpace</a></div>
`;
}

View File

@ -13,6 +13,7 @@ import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
const routes = new Hono();
@ -379,6 +380,7 @@ export const notesModule: RSpaceModule = {
icon: "\u{1F4DD}",
description: "Notebooks with rich-text notes, voice transcription, and collaboration",
routes,
landingPage: renderLanding,
standaloneDomain: "rnotes.online",
feeds: [
{

94
modules/photos/landing.ts Normal file
View File

@ -0,0 +1,94 @@
/**
* Photos module landing page rich content for rspace.online/rphotos
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">rPhotos</span>
<h1 class="rl-heading">Community Photo Commons</h1>
<p class="rl-subtext">
Share memories with your community, powered by Immich.
Self-hosted, AI-organized, and privacy-first.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rphotos" class="rl-cta-primary" id="ml-primary">Browse Gallery</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
<!-- How it works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">How It Works</h2>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Upload Photos</h3>
<p>Drop photos from any device. Bulk upload entire albums or individual shots.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>Auto-Organize</h3>
<p>Immich's AI detects faces and objects, sorting your photos into smart groups automatically.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Share Albums</h3>
<p>Create shared albums for events, projects, or teams. Everyone in the space can contribute.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">Features</h2>
<p class="rl-subtext" style="text-align:center;">
A full-featured photo platform that respects your community's data.
</p>
<div class="rl-grid-4">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#129302;</div>
<h3>Immich-Powered</h3>
<p>AI-driven face and object detection for automatic organization, search, and discovery.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128247;</div>
<h3>Shared Albums</h3>
<p>Collaborative photo albums for events, gatherings, and ongoing projects. Everyone can add.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128197;</div>
<h3>Timeline View</h3>
<p>Browse photos chronologically. Scroll through your community's visual history day by day.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128274;</div>
<h3>Privacy-First</h3>
<p>Self-hosted storage means your photos never leave your infrastructure. No third-party scanning.</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section">
<div class="rl-container" style="text-align:center;">
<h2 class="rl-heading">Your photos, your community</h2>
<p class="rl-subtext">
Try the demo gallery or create a space to start your own photo commons.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rphotos" class="rl-cta-primary">Browse Gallery</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back">
<a href="/">&larr; Back to rSpace</a>
</div>`;
}

View File

@ -10,6 +10,7 @@ import { Hono } from "hono";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
const routes = new Hono();
@ -126,6 +127,7 @@ export const photosModule: RSpaceModule = {
icon: "📸",
description: "Community photo commons",
routes,
landingPage: renderLanding,
standaloneDomain: "rphotos.online",
feeds: [
{

221
modules/pubs/landing.ts Normal file
View File

@ -0,0 +1,221 @@
/**
* rPubs landing page community pocket press.
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">rPubs</span>
<h1 class="rl-heading">Write it. Press it. Share it.</h1>
<p class="rl-subtext">
Drop in a markdown document, pick a pocket format, and get a print-ready PDF in seconds.
Group up with other authors to unlock bulk pricing through collaborative print runs.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rpubs" class="rl-cta-primary" id="ml-primary">Try the Press</a>
<a href="/rcart" class="rl-cta-secondary">Browse the Shop</a>
</div>
</div>
<!-- How it works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">How it works</h2>
<div class="rl-grid-3" style="margin-top:2rem">
<div class="rl-step">
<span class="rl-step__num">1</span>
<h3>Write / paste</h3>
<p>Drop in markdown or rich text. Headings, images, footnotes &mdash; it all just works.</p>
</div>
<div class="rl-step">
<span class="rl-step__num">2</span>
<h3>Press it</h3>
<p>Pick a pocket format. rPubs typesets your document with Typst and generates a print-ready PDF.</p>
</div>
<div class="rl-step">
<span class="rl-step__num">3</span>
<h3>Print locally</h3>
<p>Print at home, at a local shop, or list it on rCart for cosmolocal fulfillment.</p>
</div>
</div>
</div>
</section>
<!-- Pocket Formats -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Four pocket formats</h2>
<p class="rl-subtext" style="text-align:center">From palm-sized zines to digest readers. All print-ready at 300 dpi with bleeds.</p>
<div class="rl-grid-4" style="margin-top:2rem">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>
</div>
<h3>A7 Pocket</h3>
<p>74 &times; 105 mm &mdash; fits in a shirt pocket. Perfect for poems, manifestos, tiny zines.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>
</div>
<h3>Quarter Letter</h3>
<p>4.25 &times; 5.5&quot; &mdash; half a half-sheet. Great for chapbooks and field guides.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>
</div>
<h3>A6 Booklet</h3>
<p>105 &times; 148 mm &mdash; postcard size. The workhorse format for essays and readers.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>
</div>
<h3>Digest</h3>
<p>5.5 &times; 8.5&quot; &mdash; half-letter. Standard trade paperback for longer works.</p>
</div>
</div>
</div>
</section>
<!-- Group Buys -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Group buys &mdash; better together</h2>
<p class="rl-subtext" style="text-align:center">Pool orders across titles. The more copies in a run, the cheaper each one gets.</p>
<div class="rl-grid-3" style="margin-top:2rem">
<div class="rl-card rl-card--center">
<h3>25+ copies</h3>
<p>Saddle-stitch binding</p>
<p style="font-size:1.5rem;font-weight:700;color:#14b8a6;margin:0.5rem 0">$8</p>
<p style="font-size:0.75rem;color:#64748b">per copy</p>
</div>
<div class="rl-card rl-card--center">
<h3>50+ copies</h3>
<p>Perfect-bind</p>
<p style="font-size:1.5rem;font-weight:700;color:#14b8a6;margin:0.5rem 0">$6</p>
<span class="rl-badge">Save 25%</span>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.5rem">per copy</p>
</div>
<div class="rl-card rl-card--center">
<h3>100+ copies</h3>
<p>Trade edition</p>
<p style="font-size:1.5rem;font-weight:700;color:#14b8a6;margin:0.5rem 0">$4.50</p>
<span class="rl-badge">Save 44%</span>
<p style="font-size:0.75rem;color:#64748b;margin-top:0.5rem">per copy</p>
</div>
</div>
</div>
</section>
<!-- How collaborative print runs work -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">How collaborative print runs work</h2>
<div class="rl-grid-4" style="margin-top:2rem">
<div class="rl-step">
<span class="rl-step__num">1</span>
<h3>Authors list titles</h3>
<p>Publish your print-ready artifact to the rCart catalog.</p>
</div>
<div class="rl-step">
<span class="rl-step__num">2</span>
<h3>Readers pre-order</h3>
<p>Buyers add copies to a shared cart. Orders accumulate toward tier thresholds.</p>
</div>
<div class="rl-step">
<span class="rl-step__num">3</span>
<h3>Threshold met</h3>
<p>When the combined order hits a tier, the print run triggers automatically.</p>
</div>
<div class="rl-step">
<span class="rl-step__num">4</span>
<h3>Local fulfillment</h3>
<p>The nearest cosmolocal provider prints and ships. Revenue splits to creator, community, and provider.</p>
</div>
</div>
</div>
</section>
<!-- Cross-title batching -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Cross-title batching</h2>
<p class="rl-subtext" style="text-align:center">
Orders from <em>different</em> titles count toward the same tier. A community reading list
can hit trade-edition pricing even if no single title has 100 orders.
</p>
<div class="rl-card" style="max-width:640px;margin:2rem auto 0">
<h3 style="margin-bottom:1rem">Example batch</h3>
<div style="display:flex;flex-direction:column;gap:0.5rem">
<div style="display:flex;justify-content:space-between;font-size:0.875rem">
<span style="color:#e2e8f0">The Commons</span><span style="color:#14b8a6;font-weight:600">35 copies</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:0.875rem">
<span style="color:#e2e8f0">Mycelial Networks</span><span style="color:#14b8a6;font-weight:600">40 copies</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:0.875rem">
<span style="color:#e2e8f0">Cosmolocal Reader</span><span style="color:#14b8a6;font-weight:600">30 copies</span>
</div>
</div>
<div class="rl-divider"><span>combined</span></div>
<div style="text-align:center">
<span style="font-size:1.5rem;font-weight:700;color:#14b8a6">105 copies</span>
<span style="display:block;font-size:0.8rem;color:#64748b;margin-top:0.25rem">= Trade edition @ $4.50/copy</span>
</div>
</div>
</div>
</section>
<!-- rCart integration -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<div class="rl-grid-2">
<div>
<h2 class="rl-heading">rCart integration</h2>
<p class="rl-subtext" style="margin-bottom:1.5rem">
Group purchasing is built right into the shop. Every rPubs artifact can be listed,
batched, and fulfilled through rCart.
</p>
<ul class="rl-check-list">
<li><strong>One-click listing</strong> &mdash; publish to the rCart catalog straight from the press</li>
<li><strong>Group carts</strong> &mdash; space members pool orders automatically</li>
<li><strong>Revenue splits</strong> &mdash; creator, community, and provider shares via <a href="/rfunds" style="color:#14b8a6">rFunds</a></li>
<li><strong>Cosmolocal fulfillment</strong> &mdash; nearest provider prints and ships</li>
<li><strong>Order tracking</strong> &mdash; real-time status from press to doorstep</li>
</ul>
</div>
<div class="rl-card rl-card--center" style="display:flex;align-items:center;justify-content:center">
<div>
<div class="rl-icon-box" style="margin:0 auto 1rem">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 002 1.61h9.72a2 2 0 002-1.61L23 6H6"/></svg>
</div>
<h3>Shop + Press</h3>
<p>rPubs creates the artifact.<br>rCart sells and fulfills it.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Cosmolocal -->
<section class="rl-section">
<div class="rl-container" style="text-align:center">
<span class="rl-tagline">rPubs &times; Cosmolocal</span>
<h2 class="rl-heading">Design global, manufacture local</h2>
<p class="rl-subtext">
Every print run is routed to the nearest capable provider. Reduce shipping emissions,
support local economies, and still benefit from shared design and pooled demand.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rpubs" class="rl-cta-primary" id="ml-primary">Start pressing</a>
<a href="/rcart" class="rl-cta-secondary">Visit the Shop</a>
</div>
</div>
</section>
<div class="rl-back">
<a href="/">&larr; Back to rSpace</a>
</div>`;
}

View File

@ -16,6 +16,7 @@ import type { BookFormat } from "./formats";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR || "/tmp/rpubs-artifacts";
@ -342,6 +343,7 @@ export const pubsModule: RSpaceModule = {
description: "Drop in a document, get a pocket book",
routes,
standaloneDomain: "rpubs.online",
landingPage: renderLanding,
feeds: [
{
id: "publications",

165
modules/rsocials/landing.ts Normal file
View File

@ -0,0 +1,165 @@
/**
* rSocials rich landing page body.
* Returned by landingPage() in the module export;
* the shell wraps it with header, CSS, and analytics.
*/
export function renderLanding(): string {
const demo = "https://demo.rspace.online/rsocials";
return `
<!-- Hero -->
<section class="rl-hero">
<span class="rl-tagline">rSocials</span>
<h1 class="rl-heading">Social Media Under Your Control</h1>
<p class="rl-subtext">
Schedule, publish, and analyze across every major platform &mdash; all
from your self-hosted rSpace instance. No per-seat pricing, no
third-party data mining.
</p>
<div class="rl-cta-row">
<a href="${demo}" class="rl-cta-primary" id="ml-primary">Try Demo</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</section>
<!-- How It Works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">How It Works</h2>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Connect Platforms</h3>
<p>Link your X/Twitter, LinkedIn, Instagram, TikTok, and more in one place.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>Schedule Content</h3>
<p>Draft posts, attach media, pick dates, and queue them up across every channel.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Publish &amp; Analyze</h3>
<p>Posts go live on schedule. Track engagement, reach, and growth from a single dashboard.</p>
</div>
</div>
</div>
</section>
<!-- Supported Platforms -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Supported Platforms</h2>
<p class="rl-subtext" style="text-align:center">
Publish everywhere your audience lives &mdash; 16 platforms and counting.
</p>
<div class="rl-grid-4">
<div class="rl-card rl-card--center"><h3>X / Twitter</h3></div>
<div class="rl-card rl-card--center"><h3>LinkedIn</h3></div>
<div class="rl-card rl-card--center"><h3>Facebook</h3></div>
<div class="rl-card rl-card--center"><h3>Instagram</h3></div>
<div class="rl-card rl-card--center"><h3>TikTok</h3></div>
<div class="rl-card rl-card--center"><h3>YouTube</h3></div>
<div class="rl-card rl-card--center"><h3>Pinterest</h3></div>
<div class="rl-card rl-card--center"><h3>Reddit</h3></div>
<div class="rl-card rl-card--center"><h3>Threads</h3></div>
<div class="rl-card rl-card--center"><h3>BlueSky</h3></div>
<div class="rl-card rl-card--center"><h3>Mastodon</h3></div>
<div class="rl-card rl-card--center"><h3>Dribbble</h3></div>
<div class="rl-card rl-card--center"><h3>Slack</h3></div>
<div class="rl-card rl-card--center"><h3>Discord</h3></div>
<div class="rl-card rl-card--center"><h3>Telegram</h3></div>
<div class="rl-card rl-card--center"><h3>RSS</h3></div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Features</h2>
<div class="rl-grid-4">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128197;</div>
<h3>Calendar View</h3>
<p>Visual month/week/day planner so you never double-post or miss a slot.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#9997;</div>
<h3>AI Copywriting</h3>
<p>Generate caption variations, hashtags, and thread hooks with a single click.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#127912;</div>
<h3>Post Designer</h3>
<p>Resize images, add text overlays, and preview how posts look on each platform.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128101;</div>
<h3>Team Collaboration</h3>
<p>Invite editors &amp; approvers. Review queues keep quality high.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128736;</div>
<h3>Per-Platform Optimization</h3>
<p>Tailor copy, image crops, and hashtags for each network automatically.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128246;</div>
<h3>RSS Auto-Post</h3>
<p>Watch any RSS feed and auto-publish new items to selected channels.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128200;</div>
<h3>Analytics Dashboard</h3>
<p>Unified metrics across all platforms: impressions, clicks, engagement rate.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#127760;</div>
<h3>rSpace Ecosystem</h3>
<p>Connect to <a href="/rpubs" style="color:#14b8a6">rPubs</a>, <a href="/rforum" style="color:#14b8a6">rForum</a>, and more for seamless cross-posting.</p>
</div>
</div>
</div>
</section>
<!-- Why Self-Hosted -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Why Self-Hosted?</h2>
<div class="rl-grid-3">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128274;</div>
<h3>Data Sovereignty</h3>
<p>API tokens and analytics stay on your server. No third-party vendor sees your audience data.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128176;</div>
<h3>No Per-Seat Pricing</h3>
<p>Add unlimited team members. Pay only for hosting, not per-user SaaS fees.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128268;</div>
<h3>Open Source &amp; Extensible</h3>
<p>Fork it, theme it, add custom integrations. Postiz under the hood, rSpace on top.</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading">Ready to Own Your Social Presence?</h2>
<p class="rl-subtext">
Deploy rSocials in your rSpace and start scheduling in minutes.
</p>
<div class="rl-cta-row">
<a href="${demo}" class="rl-cta-primary">Try Demo</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back"><a href="/">&larr; Back to rSpace</a></div>`;
}

View File

@ -9,6 +9,7 @@ import { Hono } from "hono";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
const routes = new Hono();
@ -187,6 +188,7 @@ export const socialsModule: RSpaceModule = {
description: "Federated social feed aggregator for communities",
routes,
standaloneDomain: "rsocials.online",
landingPage: renderLanding,
feeds: [
{
id: "social-feed",

94
modules/splat/landing.ts Normal file
View File

@ -0,0 +1,94 @@
/**
* Splat module landing page rich content for rspace.online/rsplat
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">rSplat</span>
<h1 class="rl-heading">3D Gaussian Splats on the Infinite Canvas</h1>
<p class="rl-subtext">
View, share, and gate 3D scenes captured with Gaussian splatting.
Three.js-powered viewing with optional x402 micropayment gating.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rsplat" class="rl-cta-primary" id="ml-primary">Explore Splats</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
<!-- How it works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">How It Works</h2>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Upload .ply/.splat Files</h3>
<p>Upload Gaussian splat files captured from real-world scenes or generated from photos and video.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>View in Browser</h3>
<p>Explore 3D scenes directly in the browser with orbit controls, zoom, and pan. No app install needed.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Gate with x402 Micropayments</h3>
<p>Optionally gate uploads with HTTP 402 micropayments. Monetize premium 3D content natively.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">Features</h2>
<p class="rl-subtext" style="text-align:center;">
A complete pipeline from capture to community gallery.
</p>
<div class="rl-grid-4">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#127912;</div>
<h3>Three.js Viewer</h3>
<p>Hardware-accelerated WebGL rendering with orbit controls, smooth camera, and full-screen mode.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128444;</div>
<h3>Canvas Integration</h3>
<p>Embed splats as spatial elements on the rSpace infinite canvas alongside other module content.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128176;</div>
<h3>x402 Gating</h3>
<p>Monetize 3D content with HTTP 402 micropayments. Viewers pay per-view or per-download automatically.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#127748;</div>
<h3>Community Gallery</h3>
<p>Browse, search, and discover 3D scenes shared by your community. Sorted by recent, popular, or tagged.</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section">
<div class="rl-container" style="text-align:center;">
<h2 class="rl-heading">Step into 3D</h2>
<p class="rl-subtext">
Explore the demo gallery or upload your own Gaussian splats.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rsplat" class="rl-cta-primary">Explore Splats</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back">
<a href="/">&larr; Back to rSpace</a>
</div>`;
}

View File

@ -13,6 +13,7 @@ import { sql } from "../../shared/db/pool";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
import {
verifyEncryptIDToken,
extractToken,
@ -539,6 +540,7 @@ export const splatModule: RSpaceModule = {
icon: "🔮",
description: "3D Gaussian splat viewer",
routes,
landingPage: renderLanding,
standaloneDomain: "rsplat.online",
hidden: true,

123
modules/swag/landing.ts Normal file
View File

@ -0,0 +1,123 @@
/**
* Swag module landing page rich content for rspace.online/rswag
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">rSwag</span>
<h1 class="rl-heading">Design Community Merch</h1>
<p class="rl-subtext">
Stickers, posters, and tees from your browser.
Upload a design, pick a product, and get print-ready files instantly.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rswag" class="rl-cta-primary" id="ml-primary">Start Designing</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
<!-- How it works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">How It Works</h2>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Choose a Product</h3>
<p>Pick from stickers, posters, t-shirts, or patches. Each product has print-ready specs built in.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>Upload Your Design</h3>
<p>Drop in your artwork. Sharp resizes, crops, and optimizes for the selected product automatically.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Preview &amp; Order</h3>
<p>See a live mockup, download print-ready files, or send directly to rCart for community ordering.</p>
</div>
</div>
</div>
</section>
<!-- Products -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">Products</h2>
<p class="rl-subtext" style="text-align:center;">
Four product types, each with professional print specs.
</p>
<div class="rl-grid-4">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#129534;</div>
<h3>Stickers</h3>
<p>Vinyl die-cut stickers. Weatherproof, UV-resistant, and ready for laptops, bottles, and walls.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128444;</div>
<h3>Posters</h3>
<p>A3 and A2 prints at 300 DPI. Gallery-quality paper with bleed margins included.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128085;</div>
<h3>T-Shirts</h3>
<p>Direct-to-garment (DTG) printing. Full-color designs on comfortable, pre-shrunk cotton.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#129525;</div>
<h3>Patches</h3>
<p>Embroidered patches with iron-on backing. Perfect for jackets, bags, and hats.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">Features</h2>
<div class="rl-grid-4">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128065;</div>
<h3>Live Preview</h3>
<p>See your design on product mockups before committing. Adjust and iterate in real time.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128424;</div>
<h3>Print-Ready Export</h3>
<p>Download files with correct DPI, bleed margins, and color profiles for professional printing.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#9889;</div>
<h3>Sharp Processing</h3>
<p>Server-side image processing with Sharp. Fast resizing, format conversion, and optimization.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128722;</div>
<h3>rCart Integration</h3>
<p>Send finished designs directly to <a href="/rcart" style="color:#14b8a6;">rCart</a> for community ordering and fulfillment.</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center;">
<h2 class="rl-heading">Make something your community can wear</h2>
<p class="rl-subtext">
Try the designer or create a space to start your merch line.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rswag" class="rl-cta-primary">Start Designing</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back">
<a href="/">&larr; Back to rSpace</a>
</div>`;
}

View File

@ -14,6 +14,7 @@ import { processImage } from "./process-image";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
const routes = new Hono();
@ -246,6 +247,7 @@ export const swagModule: RSpaceModule = {
icon: "\u{1F3A8}",
description: "Design print-ready swag: stickers, posters, tees",
routes,
landingPage: renderLanding,
standaloneDomain: "rswag.online",
feeds: [
{

212
modules/trips/landing.ts Normal file
View File

@ -0,0 +1,212 @@
/**
* Trips module landing page static HTML, no React.
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">rTrips</span>
<h1 class="rl-heading">Plan Your Trip, Naturally</h1>
<p class="rl-subtext">
Describe your dream trip in plain language. We'll structure it into
itineraries, budgets, and bookings &mdash; then give you a collaborative
canvas to plan together in real-time.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rtrips" class="rl-cta-primary" id="ml-primary">Try Demo</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
<!-- How it Works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">How It Works</h2>
<p class="rl-subtext" style="text-align:center">Three steps from dream to departure.</p>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Describe It</h3>
<p>
Tell us about your trip in natural language. &ldquo;Fly from Toronto to Bali
for 2 weeks in March, budget $3000.&rdquo; We parse it into structured data
you can refine.
</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>We Structure It</h3>
<p>
AI extracts destinations, dates, budgets, and bookings into organized views.
Edit itineraries, track expenses, manage packing lists &mdash; all structured
and searchable.
</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Collaborate on Canvas</h3>
<p>
Open the collaborative canvas to plan visually with your travel partners.
Drag destinations, connect itineraries, and brainstorm together in real-time
or async.
</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Everything You Need to Travel Together</h2>
<p class="rl-subtext" style="text-align:center">
rTrips brings every piece of trip planning into one place &mdash; so your group
spends less time coordinating and more time exploring.
</p>
<div class="rl-grid-3">
<div class="rl-card">
<div class="rl-icon-box">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />
</svg>
</div>
<h3>Collaborative Itineraries</h3>
<p>
Build day-by-day plans together in real-time. Everyone can add destinations,
suggest activities, and rearrange the schedule &mdash; changes sync instantly.
</p>
</div>
<div class="rl-card">
<div class="rl-icon-box">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 00-2.455 2.456z" />
</svg>
</div>
<h3>Smart Suggestions</h3>
<p>
AI-powered recommendations for destinations, restaurants, and activities based
on your group's interests, budget, and travel dates.
</p>
</div>
<div class="rl-card">
<div class="rl-icon-box">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z" />
</svg>
</div>
<h3>Budget Tracking</h3>
<p>
Split costs, track expenses across the group, and keep a running total so
everyone knows exactly where the money goes. No more messy spreadsheets.
</p>
</div>
<div class="rl-card">
<div class="rl-icon-box">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 6.75V15m6-6v8.25m.503 3.498l4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 00-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0z" />
</svg>
</div>
<h3>Map Integration</h3>
<p>
Visualize your entire trip on <a href="/rmaps" style="color:#14b8a6">rMaps</a>. See routes between destinations,
nearby points of interest, and real-time location sharing during travel days.
</p>
</div>
<div class="rl-card">
<div class="rl-icon-box">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15a2.25 2.25 0 012.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
</svg>
</div>
<h3>Packing Lists</h3>
<p>
Shared checklists so nothing gets forgotten. Assign items to people, mark
off as you pack, and see at a glance what the group still needs.
</p>
</div>
<div class="rl-card">
<div class="rl-icon-box">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
</div>
<h3>Offline Access</h3>
<p>
Download your full itinerary, maps, and booking confirmations for travel
without connectivity. Everything you need, even without a signal.
</p>
</div>
</div>
</div>
</section>
<!-- Built for Groups -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Built for Groups</h2>
<p class="rl-subtext" style="text-align:center">
Solo trip planners are everywhere. rTrips is purpose-built for the messy, beautiful
reality of traveling with other people.
</p>
<div class="rl-grid-3">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
</svg>
</div>
<h3>Friends &amp; Family</h3>
<p>
Family reunions, friend getaways, multi-generational trips. Everyone contributes
ideas, votes on restaurants, and stays in sync without endless group chats.
</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z" />
</svg>
</div>
<h3>Teams &amp; Offsites</h3>
<p>
Company retreats, conference travel, team offsites. Coordinate logistics,
share flight details, and manage group bookings from a single shared workspace.
</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12.75 3.03v.568c0 .334.148.65.405.864l1.068.89c.442.369.535 1.01.216 1.49l-.51.766a2.25 2.25 0 01-1.161.886l-.143.048a1.107 1.107 0 00-.57 1.664c.369.555.169 1.307-.427 1.605L9 13.125l.423 1.059a.956.956 0 01-1.652.928l-.679-.906a1.125 1.125 0 00-1.906.172L4.5 15.75l-.612.153M12.75 3.031a9 9 0 00-8.862 12.872M12.75 3.031a9 9 0 016.69 14.036m0 0l-.177-.529A2.25 2.25 0 0017.128 15H16.5l-.324-.324a1.453 1.453 0 00-2.328.377l-.036.073a1.586 1.586 0 01-.982.816l-.99.282c-.55.157-.894.702-.8 1.267l.073.438a2.25 2.25 0 01-1.228 2.446L10.5 21l-.652-.174M12.75 3.031l.176.53" />
</svg>
</div>
<h3>Retreats &amp; Events</h3>
<p>
Yoga retreats, wedding trips, festival crews. Organize large groups with
sub-itineraries, optional activities, and shared costs that stay transparent.
</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading">Ready to plan your next adventure?</h2>
<p class="rl-subtext">Just describe where you want to go. We'll handle the rest.</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rtrips" class="rl-cta-primary">Plan a Trip</a>
</div>
</div>
</section>
<div class="rl-back"><a href="/">&larr; Back to rSpace</a></div>
`;
}

View File

@ -13,6 +13,7 @@ import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
const OSRM_URL = process.env.OSRM_URL || "http://osrm-backend:5000";
@ -271,6 +272,7 @@ export const tripsModule: RSpaceModule = {
icon: "\u{2708}\u{FE0F}",
description: "Collaborative trip planner with itinerary, bookings, and expense splitting",
routes,
landingPage: renderLanding,
standaloneDomain: "rtrips.online",
feeds: [
{

110
modules/tube/landing.ts Normal file
View File

@ -0,0 +1,110 @@
/**
* Tube module landing page rich content for rspace.online/rtube
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">rTube</span>
<h1 class="rl-heading">Community Video Platform</h1>
<p class="rl-subtext">
Host, stream, and share video without big tech.
Your community's video library with HLS streaming and RTMP ingest.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rtube" class="rl-cta-primary" id="ml-primary">Start Streaming</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
<!-- How it works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">How It Works</h2>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Upload Videos</h3>
<p>Drop video files in any major format. MP4, WebM, MKV, MOV -- they all work.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>Stream Live</h3>
<p>Point OBS or any RTMP-compatible software at your space's ingest URL and go live instantly.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Share with Your Community</h3>
<p>Videos are organized by space. Share links, embed on your canvas, or browse the community library.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">Features</h2>
<p class="rl-subtext" style="text-align:center;">
Everything you need to run your own community video platform.
</p>
<div class="rl-grid-4">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128250;</div>
<h3>HLS Streaming</h3>
<p>Adaptive bitrate streaming ensures smooth playback on any connection speed or device.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128225;</div>
<h3>RTMP Ingest</h3>
<p>Go live with OBS, Streamlabs, or any RTMP-compatible streaming software. One URL, instant broadcast.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#9729;</div>
<h3>R2 Storage</h3>
<p>Videos stored on Cloudflare R2 for fast, globally distributed delivery with no egress fees.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#127909;</div>
<h3>Community Channels</h3>
<p>Each space gets its own video channel. Organize content by community, project, or topic.</p>
</div>
</div>
</div>
</section>
<!-- Technical -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center;">Technical Details</h2>
<div class="rl-card" style="max-width:600px;margin:0 auto;">
<ul class="rl-check-list">
<li><strong>H.264 / H.265</strong> hardware-accelerated codec support</li>
<li><strong>WebM</strong> (VP8/VP9) for open-format video</li>
<li><strong>Adaptive bitrate HLS</strong> for smooth playback on any connection</li>
<li><strong>HTTP Range requests</strong> for efficient seeking and partial downloads</li>
<li><strong>RTMP ingest</strong> compatible with OBS, Streamlabs, and ffmpeg</li>
</ul>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center;">
<h2 class="rl-heading">Your community's own video platform</h2>
<p class="rl-subtext">
Try the demo or create a space to start hosting and streaming.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rtube" class="rl-cta-primary">Start Streaming</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back">
<a href="/">&larr; Back to rSpace</a>
</div>`;
}

View File

@ -10,6 +10,7 @@ import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
import { S3Client, ListObjectsV2Command, GetObjectCommand, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
const routes = new Hono();
@ -209,6 +210,7 @@ export const tubeModule: RSpaceModule = {
icon: "\u{1F3AC}",
description: "Community video hosting & live streaming",
routes,
landingPage: renderLanding,
standaloneDomain: "rtube.online",
feeds: [
{

184
modules/vote/landing.ts Normal file
View File

@ -0,0 +1,184 @@
/**
* rVote landing page democratic backlog prioritization.
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">rVote</span>
<h1 class="rl-heading">Democratic Backlog<br>Prioritization</h1>
<p class="rl-subtext">
Quadratic conviction voting with time-decay. Surface the ideas your community actually cares about &mdash;
not just the loudest voices.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rvote" class="rl-cta-primary" id="ml-primary">Try the Demo</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
<!-- ELI5 -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Three ideas, one system</h2>
<div class="rl-grid-3" style="margin-top:2rem">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<span style="font-size:1.25rem;font-weight:700">x&sup2;</span>
</div>
<h3>Quadratic Cost</h3>
<p>Each additional vote on the same proposal costs quadratically more credits. One vote = 1 credit, two = 4, three = 9. Prevents plutocratic capture.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>
</div>
<h3>Reddit-style Ranking</h3>
<p>Proposals accumulate conviction score from community votes. The most supported ideas float to the top.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
</div>
<h3>Vote Decay</h3>
<p>Votes lose weight over 30&ndash;60 days. Stale support fades automatically &mdash; no need to manually close polls.</p>
</div>
</div>
</div>
</section>
<!-- What is QPR -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">What is Quadratic Priority Ranking?</h2>
<div class="rl-grid-2" style="margin-top:2rem">
<div class="rl-card">
<h3 style="color:#f87171">The Problem</h3>
<p>Traditional voting (1 person = 1 vote) lets small, passionate groups dominate.
Token-weighted voting lets whales decide everything. Neither reflects genuine community preference.</p>
</div>
<div class="rl-card">
<h3 style="color:#14b8a6">The Solution</h3>
<p>Quadratic voting makes strong preferences expensive. You <em>can</em> signal that you care a lot &mdash;
but it costs quadratically more. This balances intensity of preference with breadth of support.</p>
</div>
</div>
</div>
</section>
<!-- Vote Cost Calculator -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Vote cost calculator</h2>
<p class="rl-subtext" style="text-align:center">The cost of conviction grows quadratically.</p>
<div class="rl-card" style="max-width:500px;margin:2rem auto 0">
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:0.5rem;text-align:center">
<div>
<div style="font-size:1.5rem;font-weight:700;color:#14b8a6">1</div>
<div style="font-size:0.7rem;color:#64748b">vote</div>
<div style="font-size:0.85rem;font-weight:600;color:#e2e8f0;margin-top:0.25rem">1 credit</div>
</div>
<div>
<div style="font-size:1.5rem;font-weight:700;color:#14b8a6">2</div>
<div style="font-size:0.7rem;color:#64748b">votes</div>
<div style="font-size:0.85rem;font-weight:600;color:#e2e8f0;margin-top:0.25rem">4 credits</div>
</div>
<div>
<div style="font-size:1.5rem;font-weight:700;color:#14b8a6">3</div>
<div style="font-size:0.7rem;color:#64748b">votes</div>
<div style="font-size:0.85rem;font-weight:600;color:#e2e8f0;margin-top:0.25rem">9 credits</div>
</div>
<div>
<div style="font-size:1.5rem;font-weight:700;color:#14b8a6">4</div>
<div style="font-size:0.7rem;color:#64748b">votes</div>
<div style="font-size:0.85rem;font-weight:600;color:#e2e8f0;margin-top:0.25rem">16 credits</div>
</div>
<div>
<div style="font-size:1.5rem;font-weight:700;color:#14b8a6">5</div>
<div style="font-size:0.7rem;color:#64748b">votes</div>
<div style="font-size:0.85rem;font-weight:600;color:#e2e8f0;margin-top:0.25rem">25 credits</div>
</div>
</div>
</div>
</div>
</section>
<!-- How it works: 3 stages -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">The lifecycle of a proposal</h2>
<div class="rl-grid-3" style="margin-top:2rem">
<div class="rl-step">
<span class="rl-step__num">1</span>
<h3>Ranking (QPR)</h3>
<p>Community members spend credits to upvote proposals. Conviction score accumulates with quadratic cost.</p>
</div>
<div class="rl-step">
<span class="rl-step__num">2</span>
<h3>Score reaches +100</h3>
<p>When a proposal crosses the promotion threshold (default 100), it automatically enters the final vote.</p>
</div>
<div class="rl-step">
<span class="rl-step__num">3</span>
<h3>Pass / Fail Vote</h3>
<p>A time-limited binary vote (Yes / No / Abstain) decides the outcome. Simple majority wins.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Built for real governance</h2>
<div class="rl-grid-4" style="margin-top:2rem">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
</div>
<h3>Earn Credits Daily</h3>
<p>Every verified member receives a daily credit allowance. No pay-to-play.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
</div>
<h3>Vote Decay</h3>
<p>Votes decay linearly from day 30 to day 60. Stale support fades, keeping rankings fresh.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<h3>Sybil Resistant</h3>
<p>Passkey authentication via EncryptID. One person, one identity, one credit stream.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 014-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 01-4 4H3"/></svg>
</div>
<h3>Auto Promotion</h3>
<p>Proposals that hit the threshold automatically move to a final pass/fail vote. No admin bottleneck.</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading">Ready to prioritize democratically?</h2>
<p class="rl-subtext">
Create a governance space, submit proposals, and let your community decide what matters most.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rvote" class="rl-cta-primary" id="ml-primary">Open rVote</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back">
<a href="/">&larr; Back to rSpace</a>
</div>`;
}

View File

@ -13,6 +13,7 @@ import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
const routes = new Hono();
@ -346,6 +347,7 @@ export const voteModule: RSpaceModule = {
description: "Conviction voting engine for collaborative governance",
routes,
standaloneDomain: "rvote.online",
landingPage: renderLanding,
feeds: [
{
id: "proposals",

119
modules/wallet/landing.ts Normal file
View File

@ -0,0 +1,119 @@
/**
* rWallet rich landing page body.
* Returned by landingPage() in the module export;
* the shell wraps it with header, CSS, and analytics.
*/
export function renderLanding(): string {
const demo = "https://demo.rspace.online/rwallet";
return `
<!-- Hero -->
<section class="rl-hero">
<span class="rl-tagline">rWallet</span>
<h1 class="rl-heading">Community Treasury, Transparent</h1>
<p class="rl-subtext">
Visualize your Safe multisig across every chain &mdash; balances,
transactions, and governance &mdash; all in one client-side dashboard.
No backend database, no custody risk.
</p>
<div class="rl-cta-row">
<a href="${demo}" class="rl-cta-primary" id="ml-primary">View Your Treasury</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</section>
<!-- How It Works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">How It Works</h2>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Connect Your Safe</h3>
<p>Paste any Safe address. rWallet auto-detects which chains it lives on.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>View Across Chains</h3>
<p>See token balances, USD valuations, and transaction history for every chain in one view.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Govern Together</h3>
<p>Review pending multisig transactions and coordinate approvals with your team.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Features</h2>
<div class="rl-grid-4">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#9939;</div>
<h3>Multi-Chain Support</h3>
<p>Ethereum, Polygon, Base, Gnosis, Arbitrum, Optimism, Celo, Avalanche, BSC, and zkSync.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128203;</div>
<h3>Transaction History</h3>
<p>Browse incoming and outgoing transfers with human-readable labels and USD values.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128272;</div>
<h3>Multisig Governance</h3>
<p>See signer thresholds, pending confirmations, and execution status at a glance.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128176;</div>
<h3>Token Balances</h3>
<p>ERC-20 and native token balances aggregated across all detected chains.</p>
</div>
</div>
</div>
</section>
<!-- Ecosystem Integration -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Ecosystem Integration</h2>
<p class="rl-subtext" style="text-align:center">
rWallet connects to other rSpace modules for a complete treasury workflow.
</p>
<div class="rl-grid-2">
<div class="rl-integration">
<div class="rl-icon-box">&#128200;</div>
<div>
<h3><a href="/rfunds" style="color:#14b8a6;text-decoration:none">rFunds</a></h3>
<p>Overlay budget categories on your wallet data to see where funds are allocated and how they flow.</p>
</div>
</div>
<div class="rl-integration">
<div class="rl-icon-box">&#9745;</div>
<div>
<h3><a href="/rvote" style="color:#14b8a6;text-decoration:none">rVote</a></h3>
<p>Link governance proposals directly to treasury transactions for full accountability.</p>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading">Ready to Open Your Treasury?</h2>
<p class="rl-subtext">
Paste a Safe address and explore your multichain holdings in seconds.
</p>
<div class="rl-cta-row">
<a href="${demo}" class="rl-cta-primary">View Your Treasury</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back"><a href="/">&larr; Back to rSpace</a></div>`;
}

View File

@ -9,6 +9,7 @@ import { Hono } from "hono";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
const routes = new Hono();
@ -113,6 +114,7 @@ export const walletModule: RSpaceModule = {
description: "Multichain Safe wallet visualization and treasury management",
routes,
standaloneDomain: "rwallet.online",
landingPage: renderLanding,
feeds: [
{
id: "balances",

127
modules/work/landing.ts Normal file
View File

@ -0,0 +1,127 @@
/**
* rWork landing page collective task management.
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">rWork</span>
<h1 class="rl-heading">Get things done,<br>together</h1>
<p class="rl-subtext">
Kanban boards, team spaces, and real-time collaboration &mdash; built for groups
that share work, not just assign it.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rwork" class="rl-cta-primary" id="ml-primary">Try the Demo</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
<!-- How it works -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">How it works</h2>
<div class="rl-grid-3" style="margin-top:2rem">
<div class="rl-step">
<span class="rl-step__num">1</span>
<h3>Create a Space</h3>
<p>Every workspace lives inside an rSpace &mdash; shared by default with your team.</p>
</div>
<div class="rl-step">
<span class="rl-step__num">2</span>
<h3>Add Tasks</h3>
<p>Create tasks with titles, descriptions, labels, and priorities. Drag them across columns.</p>
</div>
<div class="rl-step">
<span class="rl-step__num">3</span>
<h3>Collaborate</h3>
<p>Assign, comment, and track progress in real time. Everyone sees the same board.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Features</h2>
<p class="rl-subtext" style="text-align:center">Everything you need for collaborative task management, nothing you don't.</p>
<div class="rl-grid-4" style="margin-top:2rem">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
</div>
<h3>Kanban Boards</h3>
<p>Drag-and-drop columns with customizable statuses. See everything at a glance.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg>
</div>
<h3>Team Spaces</h3>
<p>Each workspace is scoped to a space. Members share boards, tasks, and activity.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>
</div>
<h3>Real-time Sync</h3>
<p>Changes propagate instantly. No refresh needed &mdash; everyone stays in sync.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
</div>
<h3>Markdown Native</h3>
<p>Task descriptions support full markdown. Write rich content without leaving the board.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
</div>
<h3>Custom Pipelines</h3>
<p>Define your own status columns &mdash; TODO, In Progress, Review, Done, or whatever fits.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
</div>
<h3>Notifications</h3>
<p>Get notified when tasks are assigned, moved, or commented on.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>
</div>
<h3>Acceptance Criteria</h3>
<p>Define clear done conditions. Check them off as you go.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>
</div>
<h3>rSpace Ecosystem</h3>
<p>Tasks link to rVote proposals, rCal events, rFunds budgets, and more.</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading">Ready to collaborate?</h2>
<p class="rl-subtext">
Create a workspace, invite your team, and start shipping together.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rwork" class="rl-cta-primary" id="ml-primary">Open rWork</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back">
<a href="/">&larr; Back to rSpace</a>
</div>`;
}

View File

@ -13,6 +13,7 @@ import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
const routes = new Hono();
@ -236,6 +237,7 @@ export const workModule: RSpaceModule = {
description: "Kanban workspace boards for collaborative task management",
routes,
standaloneDomain: "rwork.online",
landingPage: renderLanding,
feeds: [
{
id: "task-activity",

View File

@ -1081,14 +1081,23 @@ const server = Bun.serve<WSData>({
const mod = allModules.find((m) => m.id === firstSegment);
if (mod) {
if (pathSegments.length === 1) {
// Try proxying the rich standalone landing page
// 1. Check for inline rich landing page
if (mod.landingPage) {
const html = renderModuleLanding({
module: mod,
modules: getModuleInfoList(),
bodyHTML: mod.landingPage(),
});
return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" } });
}
// 2. Try proxying the rich standalone landing page
const proxyHtml = await fetchLandingPage(mod, getModuleInfoList());
if (proxyHtml) {
return new Response(proxyHtml, {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}
// Fallback to generic landing page
// 3. Fallback to generic landing page
const html = renderModuleLanding({
module: mod,
modules: getModuleInfoList(),

View File

@ -423,13 +423,36 @@ export interface ModuleLandingOptions {
modules: ModuleInfo[];
/** Theme */
theme?: "dark" | "light";
/** Rich body HTML from a module's landing.ts (replaces generic hero) */
bodyHTML?: string;
}
export function renderModuleLanding(opts: ModuleLandingOptions): string {
const { module: mod, modules, theme = "dark" } = opts;
const { module: mod, modules, theme = "dark", bodyHTML } = opts;
const moduleListJSON = JSON.stringify(modules);
const demoUrl = `https://demo.rspace.online/${mod.id}`;
const cssBlock = bodyHTML
? `<style>${MODULE_LANDING_CSS}</style>\n <style>${RICH_LANDING_CSS}</style>`
: `<style>${MODULE_LANDING_CSS}</style>`;
const bodyContent = bodyHTML
? bodyHTML
: `<div class="ml-hero">
<div class="ml-container">
<span class="ml-icon">${mod.icon}</span>
<h1 class="ml-name">${escapeHtml(mod.name)}</h1>
<p class="ml-desc">${escapeHtml(mod.description)}</p>
<div class="ml-ctas">
<a href="${demoUrl}" class="ml-cta-primary" id="ml-primary">Try Demo</a>
<a href="/create-space" class="ml-cta-secondary">Create a Space</a>
</div>
</div>
</div>
<div class="ml-back">
<a href="/"> Back to rSpace</a>
</div>`;
return `<!DOCTYPE html>
<html lang="en">
<head>
@ -438,7 +461,7 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>${mod.icon}</text></svg>">
<title>${escapeHtml(mod.name)} rSpace</title>
<link rel="stylesheet" href="/shell.css">
<style>${MODULE_LANDING_CSS}</style>
${cssBlock}
<script defer src="https://rdata.online/collect.js" data-website-id="6ee7917b-0ed7-44cb-a4c8-91037638526b"></script>
</head>
<body data-theme="${theme}">
@ -452,20 +475,7 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
<rstack-identity></rstack-identity>
</div>
</header>
<div class="ml-hero">
<div class="ml-container">
<span class="ml-icon">${mod.icon}</span>
<h1 class="ml-name">${escapeHtml(mod.name)}</h1>
<p class="ml-desc">${escapeHtml(mod.description)}</p>
<div class="ml-ctas">
<a href="${demoUrl}" class="ml-cta-primary" id="ml-primary">Try Demo</a>
<a href="/create-space" class="ml-cta-secondary">Create a Space</a>
</div>
</div>
</div>
<div class="ml-back">
<a href="/"> Back to rSpace</a>
</div>
${bodyContent}
<script type="module">
import '/shell.js';
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
@ -529,6 +539,184 @@ body {
@media (max-width: 600px) { .ml-name { font-size: 2rem; } .ml-icon { font-size: 3rem; } }
`;
const RICH_LANDING_CSS = `
/* ── Rich Landing Page Utilities ── */
.rl-section {
border-top: 1px solid rgba(255,255,255,0.06);
padding: 4rem 1.5rem;
}
.rl-section--alt { background: rgba(255,255,255,0.015); }
.rl-container { max-width: 1100px; margin: 0 auto; }
.rl-hero {
text-align: center; padding: 5rem 1.5rem 3rem;
max-width: 820px; margin: 0 auto;
}
.rl-tagline {
display: inline-block; font-size: 0.7rem; font-weight: 700;
letter-spacing: 0.12em; text-transform: uppercase;
color: #14b8a6; background: rgba(20,184,166,0.1);
border: 1px solid rgba(20,184,166,0.2);
padding: 0.35rem 1rem; border-radius: 9999px; margin-bottom: 1.5rem;
}
.rl-heading {
font-size: 2rem; font-weight: 700; line-height: 1.15;
margin-bottom: 0.75rem; letter-spacing: -0.01em;
background: linear-gradient(135deg, #14b8a6, #22d3ee);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.rl-hero .rl-heading { font-size: 2.5rem; }
@media (min-width: 640px) { .rl-hero .rl-heading { font-size: 3rem; } }
.rl-subtext {
font-size: 1.05rem; color: #94a3b8; line-height: 1.65;
max-width: 640px; margin: 0 auto 2rem;
}
.rl-hero .rl-subtext { font-size: 1.15rem; }
/* Grids */
.rl-grid-2 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
.rl-grid-3 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
.rl-grid-4 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
@media (min-width: 640px) {
.rl-grid-2 { grid-template-columns: repeat(2, 1fr); }
.rl-grid-3 { grid-template-columns: repeat(3, 1fr); }
.rl-grid-4 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
.rl-grid-4 { grid-template-columns: repeat(4, 1fr); }
}
/* Card */
.rl-card {
background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06);
border-radius: 1rem; padding: 1.75rem;
transition: border-color 0.2s;
}
.rl-card:hover { border-color: rgba(20,184,166,0.3); }
.rl-card h3 { font-size: 0.95rem; font-weight: 600; color: #e2e8f0; margin-bottom: 0.5rem; }
.rl-card p { font-size: 0.875rem; color: #94a3b8; line-height: 1.6; }
.rl-card--center { text-align: center; }
/* Step circles */
.rl-step {
display: flex; flex-direction: column; align-items: center; text-align: center;
}
.rl-step__num {
width: 2.5rem; height: 2.5rem; border-radius: 9999px;
background: rgba(20,184,166,0.1); color: #14b8a6;
display: flex; align-items: center; justify-content: center;
font-size: 0.8rem; font-weight: 700; margin-bottom: 0.75rem;
}
.rl-step h3 { font-size: 0.95rem; font-weight: 600; color: #e2e8f0; margin-bottom: 0.25rem; }
.rl-step p { font-size: 0.82rem; color: #94a3b8; line-height: 1.55; }
/* CTA row */
.rl-cta-row {
display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap;
margin-top: 2rem;
}
.rl-cta-primary {
display: inline-block; padding: 0.8rem 2rem; border-radius: 0.5rem;
background: linear-gradient(135deg, #14b8a6, #0d9488);
color: white; font-size: 0.95rem; font-weight: 600;
text-decoration: none; transition: transform 0.2s, box-shadow 0.2s;
}
.rl-cta-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(20,184,166,0.3); }
.rl-cta-secondary {
display: inline-block; padding: 0.8rem 2rem; border-radius: 0.5rem;
background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.15);
color: #94a3b8; font-size: 0.95rem; font-weight: 600;
text-decoration: none; transition: transform 0.2s, border-color 0.2s, color 0.2s;
}
.rl-cta-secondary:hover { transform: translateY(-2px); border-color: rgba(255,255,255,0.35); color: white; }
/* Check list */
.rl-check-list { list-style: none; padding: 0; margin: 0; }
.rl-check-list li {
display: flex; align-items: flex-start; gap: 0.5rem;
font-size: 0.875rem; color: #94a3b8; line-height: 1.55;
padding: 0.35rem 0;
}
.rl-check-list li::before {
content: "✓"; color: #14b8a6; font-weight: 700; flex-shrink: 0; margin-top: 0.05em;
}
.rl-check-list li strong { color: #e2e8f0; font-weight: 600; }
/* Badge */
.rl-badge {
display: inline-block; font-size: 0.65rem; font-weight: 700;
color: white; background: #14b8a6;
padding: 0.15rem 0.5rem; border-radius: 9999px;
}
/* Divider */
.rl-divider {
display: flex; align-items: center; gap: 0.75rem; margin: 1.5rem 0;
}
.rl-divider::before, .rl-divider::after {
content: ""; flex: 1; height: 1px; background: rgba(255,255,255,0.06);
}
.rl-divider span { font-size: 0.75rem; color: #64748b; white-space: nowrap; }
/* Icon box */
.rl-icon-box {
width: 3rem; height: 3rem; border-radius: 0.75rem;
background: rgba(20,184,166,0.12); color: #14b8a6;
display: flex; align-items: center; justify-content: center;
font-size: 1.5rem; margin-bottom: 1rem;
}
.rl-card--center .rl-icon-box { margin: 0 auto 1rem; }
/* Integration (2-col with icon) */
.rl-integration {
display: flex; align-items: flex-start; gap: 1rem;
background: rgba(20,184,166,0.04); border: 1px solid rgba(20,184,166,0.15);
border-radius: 1rem; padding: 1.5rem;
}
.rl-integration h3 { font-size: 0.95rem; font-weight: 600; color: #e2e8f0; margin-bottom: 0.35rem; }
.rl-integration p { font-size: 0.85rem; color: #94a3b8; line-height: 1.55; }
/* Back link */
.rl-back { padding: 2rem 0 3rem; text-align: center; }
.rl-back a { font-size: 0.85rem; color: #64748b; text-decoration: none; transition: color 0.2s; }
.rl-back a:hover { color: #e2e8f0; }
/* Progress bar */
.rl-progress { height: 0.5rem; border-radius: 9999px; background: rgba(255,255,255,0.06); overflow: hidden; }
.rl-progress__fill { height: 100%; border-radius: 9999px; background: #14b8a6; }
/* Tier row */
.rl-tier {
display: flex; gap: 0.5rem; margin: 1rem 0;
}
.rl-tier__item {
flex: 1; text-align: center; border-radius: 0.5rem;
border: 1px solid rgba(255,255,255,0.06); padding: 0.5rem; font-size: 0.75rem;
}
.rl-tier__item--active {
border-color: rgba(20,184,166,0.4); background: rgba(20,184,166,0.05); color: #14b8a6;
}
.rl-tier__item--active strong { color: #14b8a6; }
/* Temporal zoom bar */
.rl-zoom-bar { display: flex; flex-direction: column; gap: 0.5rem; }
.rl-zoom-bar__row { display: flex; align-items: center; gap: 0.75rem; }
.rl-zoom-bar__label { font-size: 0.7rem; color: #64748b; width: 1.2rem; text-align: right; font-family: monospace; }
.rl-zoom-bar__bar {
height: 1.5rem; border-radius: 0.375rem; background: rgba(99,102,241,0.15);
display: flex; align-items: center; padding: 0 0.75rem;
}
.rl-zoom-bar__name { font-size: 0.75rem; font-weight: 600; color: #e2e8f0; white-space: nowrap; }
.rl-zoom-bar__span { font-size: 0.6rem; color: #64748b; margin-left: auto; white-space: nowrap; }
/* Responsive helpers */
@media (max-width: 600px) {
.rl-hero { padding: 3rem 1rem 2rem; }
.rl-hero .rl-heading { font-size: 2rem; }
.rl-section { padding: 2.5rem 1rem; }
}
`;
export function escapeHtml(s: string): string {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

View File

@ -33,6 +33,8 @@ export interface RSpaceModule {
onSpaceDelete?: (spaceSlug: string) => Promise<void>;
/** If true, module is hidden from app switcher (still has routes) */
hidden?: boolean;
/** Optional: render rich landing page body HTML */
landingPage?: () => string;
}
/** Registry of all loaded modules */
@ -60,6 +62,7 @@ export interface ModuleInfo {
feeds?: FeedDefinition[];
acceptsFeeds?: FlowKind[];
hidden?: boolean;
hasLandingPage?: boolean;
}
export function getModuleInfoList(): ModuleInfo[] {
@ -73,5 +76,6 @@ export function getModuleInfoList(): ModuleInfo[] {
...(m.standaloneDomain ? { standaloneDomain: m.standaloneDomain } : {}),
...(m.feeds ? { feeds: m.feeds } : {}),
...(m.acceptsFeeds ? { acceptsFeeds: m.acceptsFeeds } : {}),
...(m.landingPage ? { hasLandingPage: true } : {}),
}));
}