rspace-online/server/landing.ts

405 lines
15 KiB
TypeScript

/**
* Main landing page for rspace.online/
*
* Server-rendered using the same shell (header, CSS, theme) as all module
* landing pages. Content is ported from the old static Next.js export and
* adapted to use the shared rl-* utility classes.
*/
import type { ModuleInfo } from "../shared/module";
import { escapeHtml, escapeAttr, MODULE_LANDING_CSS, RICH_LANDING_CSS } from "./shell";
export function renderMainLanding(modules: ModuleInfo[]): string {
const moduleListJSON = JSON.stringify(modules);
const demoUrl = "https://demo.rspace.online/rspace";
// Build the ecosystem grid dynamically from registered modules
const ecosystemCards = modules
.map(
(m) => `
<a href="/${escapeAttr(m.id)}" class="rl-card rl-card--center" style="text-decoration:none;color:inherit">
<div class="rl-icon-box">${m.icon}</div>
<h3>${escapeHtml(m.name)}</h3>
<p>${escapeHtml(m.description)}</p>
</a>`,
)
.join("\n");
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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'>🌌</text></svg>">
<title>rSpace — Community Platform</title>
<meta name="description" content="A collaborative, local-first community platform with 22+ interoperable tools. Design global, manufacture local.">
<link rel="stylesheet" href="/shell.css">
<style>${MODULE_LANDING_CSS}</style>
<style>${RICH_LANDING_CSS}</style>
<style>${MAIN_LANDING_CSS}</style>
<script defer src="https://rdata.online/collect.js" data-website-id="6ee7917b-0ed7-44cb-a4c8-91037638526b"></script>
</head>
<body data-theme="dark">
<header class="rstack-header" data-theme="dark">
<div class="rstack-header__left">
<a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/favicon.png" alt="rSpace" class="rstack-header__logo"></a>
<rstack-app-switcher current=""></rstack-app-switcher>
</div>
<div class="rstack-header__center">
<rstack-mi></rstack-mi>
</div>
<div class="rstack-header__right">
<a class="rstack-header__demo-btn" href="${demoUrl}">Try Demo</a>
<rstack-identity></rstack-identity>
</div>
</header>
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">Community Platform</span>
<h1 class="rl-heading main-wordmark">(ou)r<span class="main-wordmark__accent">Space</span></h1>
<p class="rl-subtitle">Remember back when the internet was cool?</p>
<p class="rl-subtext">
A collaborative, local-first platform with ${modules.length}+ interoperable tools.
Own your data, run your community, connect your world &mdash; no landlords required.
</p>
<div class="rl-cta-row">
<a href="${demoUrl}" 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 class="main-badges">
<span class="rl-badge">Local-First</span>
<span class="rl-badge">Self-Hosted</span>
<span class="rl-badge">Open Source</span>
<span class="rl-badge">Offline-Ready</span>
</div>
</div>
<!-- Collaboration Features -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Everything Your Community Needs</h2>
<p class="rl-subtext" style="text-align:center">
Each tool works on its own, and they all work together.
</p>
<div class="rl-grid-3">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128176;</div>
<h3>Community Funds</h3>
<p>Budget rivers, revenue splits, and enoughness thresholds. Transparent finances with quadratic funding built in.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128172;</div>
<h3>Messaging &amp; Forum</h3>
<p>Real-time chat, threaded discussions, and async forums. All synced across devices with CRDT magic.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128193;</div>
<h3>Files &amp; Media</h3>
<p>Upload, organize, and share with memory cards. Public links, folder trees, and metadata that travels with content.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128272;</div>
<h3>Passkey Identity</h3>
<p>One passwordless login for the entire ecosystem. EncryptID uses WebAuthn &mdash; no passwords to leak, no accounts to hack.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128202;</div>
<h3>Dashboards &amp; Data</h3>
<p>Community analytics, voting results, spatial canvases. See everything at a glance and drill into what matters.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128737;</div>
<h3>Privacy by Design</h3>
<p>Your data lives on your device first. End-to-end encrypted sync, per-document keys, zero-knowledge architecture.</p>
</div>
</div>
</div>
</section>
<!-- EncryptID -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<div class="rl-grid-2">
<div>
<span class="rl-tagline">EncryptID</span>
<h2 class="rl-heading">One Passkey for Everything</h2>
<p class="rl-subtext" style="margin-bottom:1.5rem">
Sign in once with your fingerprint or device PIN. Your passkey works
across every rApp &mdash; no passwords, no email verification loops,
no third-party auth providers.
</p>
<ul class="rl-check-list">
<li><strong>WebAuthn passkeys</strong> &mdash; phishing-resistant by default</li>
<li><strong>Guardian recovery</strong> &mdash; 2-of-3 trusted contacts restore access</li>
<li><strong>Device linking</strong> &mdash; QR scan adds your phone or tablet</li>
<li><strong>One RP ID</strong> &mdash; shared across all r*.online domains</li>
</ul>
</div>
<div style="display:flex;align-items:center;justify-content:center">
<div class="rl-card" style="max-width:320px;text-align:center">
<div class="rl-icon-box" style="margin:0 auto 1rem;font-size:2rem">&#128274;</div>
<h3 style="margin-bottom:0.5rem">Passwordless Login</h3>
<p>Touch your fingerprint sensor, tap your security key, or use your device PIN. That&rsquo;s it.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Offline-First -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Offline-First, Always</h2>
<p class="rl-subtext" style="text-align:center">
Your data lives on your device. Changes sync when you&rsquo;re online,
merge automatically when you&rsquo;re not.
</p>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Local Persistence</h3>
<p>Every document is stored in encrypted IndexedDB on your device. Works without internet.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>Auto-Merge CRDT</h3>
<p>Automerge CRDTs resolve conflicts automatically. No &ldquo;someone else is editing&rdquo; lockouts.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Incremental Sync</h3>
<p>Only changed bytes travel over the wire. Reconnect after days offline and catch up in seconds.</p>
</div>
</div>
</div>
</section>
<!-- Philosophy -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="max-width:720px;text-align:center">
<h2 class="rl-heading">The Internet as It Was Always Meant to Be</h2>
<p class="rl-subtext">
No algorithms deciding what you see. No ads. No data harvesting.
Just tools that work for you, run by you, owned by you.
rSpace is infrastructure for communities who refuse to rent their digital commons.
</p>
<div class="rl-cta-row">
<a href="/about" class="rl-cta-secondary">Learn More</a>
</div>
</div>
</section>
<!-- Ecosystem Grid (dynamic) -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">${modules.length} rApps and Growing</h2>
<p class="rl-subtext" style="text-align:center">
Each app is independent and composable. Use one, use all, mix and match.
</p>
<div class="rl-grid-4">
${ecosystemCards}
</div>
</div>
</section>
<!-- Footer -->
<footer class="main-footer">
<div class="rl-container" style="text-align:center">
<p style="color:#64748b;font-size:0.85rem;margin-bottom:0.5rem">
Built with Bun, Hono, Automerge &amp; WebAuthn
</p>
<div style="display:flex;gap:1.5rem;justify-content:center;flex-wrap:wrap">
<a href="/about" style="color:#94a3b8;font-size:0.8rem;text-decoration:none">About</a>
<a href="/create-space" style="color:#94a3b8;font-size:0.8rem;text-decoration:none">Create a Space</a>
<a href="https://auth.rspace.online" style="color:#94a3b8;font-size:0.8rem;text-decoration:none">EncryptID</a>
</div>
</div>
</footer>
<script type="module">
import '/shell.js';
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
// Logged-in users: swap CTA to "Go to My Space"
try {
var raw = localStorage.getItem('encryptid_session');
if (raw) {
var session = JSON.parse(raw);
if (session?.claims?.username) {
var username = session.claims.username.toLowerCase();
var primary = document.getElementById('ml-primary');
if (primary) {
primary.textContent = 'Go to My Space';
primary.href = 'https://' + username + '.rspace.online/rspace';
}
}
}
} catch(e) {}
</script>
</body>
</html>`;
}
// ── Space Dashboard ──
export function renderSpaceDashboard(space: string, modules: ModuleInfo[]): string {
const moduleListJSON = JSON.stringify(modules);
const displayName = space === "demo" ? "Demo Space" : space;
const subtitle = space === "demo"
? "Explore the rSpace ecosystem — click any rApp to try it live with sample data."
: `${modules.length} rApps available in this space.`;
const appCards = modules
.map((m) => {
const href = `/${escapeAttr(space)}/${escapeAttr(m.id)}`;
return `
<a href="${href}" class="sd-card">
<div class="sd-card__icon">${m.icon}</div>
<div class="sd-card__body">
<h3 class="sd-card__name">${escapeHtml(m.name)}</h3>
<p class="sd-card__desc">${escapeHtml(m.description)}</p>
</div>
</a>`;
})
.join("\n");
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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'>🌌</text></svg>">
<title>${escapeHtml(displayName)} | rSpace</title>
<link rel="stylesheet" href="/shell.css">
<style>${MODULE_LANDING_CSS}</style>
<style>${SPACE_DASHBOARD_CSS}</style>
<script defer src="https://rdata.online/collect.js" data-website-id="6ee7917b-0ed7-44cb-a4c8-91037638526b"></script>
</head>
<body data-theme="dark">
<header class="rstack-header" data-theme="dark">
<div class="rstack-header__left">
<a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/favicon.png" alt="rSpace" class="rstack-header__logo"></a>
<rstack-app-switcher current=""></rstack-app-switcher>
<rstack-space-switcher current="${escapeAttr(space)}" name="${escapeAttr(displayName)}"></rstack-space-switcher>
</div>
<div class="rstack-header__center">
<rstack-mi></rstack-mi>
</div>
<div class="rstack-header__right">
<rstack-identity></rstack-identity>
</div>
</header>
<div class="sd-hero">
<h1 class="sd-hero__title">${escapeHtml(displayName)}</h1>
<p class="sd-hero__subtitle">${subtitle}</p>
</div>
<div class="sd-container">
<div class="sd-grid">
${appCards}
</div>
</div>
${space === "demo" ? `
<div class="sd-footer">
<p>Want your own space? <a href="/create-space">Create one</a> — it&rsquo;s free and instant.</p>
</div>` : ""}
<script type="module">
import '/shell.js';
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
</script>
</body>
</html>`;
}
const SPACE_DASHBOARD_CSS = `
.sd-hero {
text-align: center;
padding: 100px 1.5rem 2rem;
}
.sd-hero__title {
font-size: 2.25rem; font-weight: 700; margin: 0 0 0.5rem;
background: linear-gradient(135deg, #14b8a6, #22d3ee);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.sd-hero__subtitle {
font-size: 1.05rem; color: #94a3b8; margin: 0;
max-width: 520px; margin: 0 auto; line-height: 1.5;
}
.sd-container {
max-width: 1100px; margin: 0 auto;
padding: 1rem 1.5rem 3rem;
}
.sd-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 0.75rem;
}
.sd-card {
display: flex; align-items: flex-start; gap: 1rem;
padding: 1rem 1.25rem;
background: rgba(255,255,255,0.025);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 0.75rem;
text-decoration: none; color: inherit;
transition: border-color 0.2s, background 0.2s, transform 0.15s;
cursor: pointer;
}
.sd-card:hover {
border-color: rgba(20,184,166,0.35);
background: rgba(20,184,166,0.04);
transform: translateY(-1px);
}
.sd-card__icon {
font-size: 1.75rem; flex-shrink: 0;
width: 2.5rem; height: 2.5rem;
display: flex; align-items: center; justify-content: center;
}
.sd-card__body { min-width: 0; }
.sd-card__name {
font-size: 0.9rem; font-weight: 600; color: #e2e8f0;
margin: 0 0 0.2rem;
}
.sd-card__desc {
font-size: 0.78rem; color: #64748b; margin: 0;
line-height: 1.45;
}
.sd-footer {
text-align: center; padding: 2rem 1.5rem 3rem;
border-top: 1px solid rgba(255,255,255,0.06);
}
.sd-footer p { color: #64748b; font-size: 0.9rem; margin: 0; }
.sd-footer a { color: #14b8a6; text-decoration: none; font-weight: 600; }
.sd-footer a:hover { text-decoration: underline; }
@media (max-width: 480px) {
.sd-grid { grid-template-columns: 1fr; }
.sd-hero__title { font-size: 1.75rem; }
.sd-hero { padding-top: 80px; }
}
`;
const MAIN_LANDING_CSS = `
/* Main landing page extras (on top of rl-* utilities) */
.main-wordmark {
font-size: 3.5rem;
}
@media (min-width: 640px) { .main-wordmark { font-size: 4.5rem; } }
.main-wordmark__accent {
background: linear-gradient(135deg, #14b8a6, #22d3ee);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.main-badges {
display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap;
margin-top: 1.5rem;
}
.main-footer {
border-top: 1px solid rgba(255,255,255,0.06);
padding: 2.5rem 1.5rem;
}
`;