405 lines
15 KiB
TypeScript
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 — 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">💰</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">💬</div>
|
|
<h3>Messaging & 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">📁</div>
|
|
<h3>Files & 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">🔐</div>
|
|
<h3>Passkey Identity</h3>
|
|
<p>One passwordless login for the entire ecosystem. EncryptID uses WebAuthn — no passwords to leak, no accounts to hack.</p>
|
|
</div>
|
|
<div class="rl-card rl-card--center">
|
|
<div class="rl-icon-box">📊</div>
|
|
<h3>Dashboards & 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">🛡</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 — no passwords, no email verification loops,
|
|
no third-party auth providers.
|
|
</p>
|
|
<ul class="rl-check-list">
|
|
<li><strong>WebAuthn passkeys</strong> — phishing-resistant by default</li>
|
|
<li><strong>Guardian recovery</strong> — 2-of-3 trusted contacts restore access</li>
|
|
<li><strong>Device linking</strong> — QR scan adds your phone or tablet</li>
|
|
<li><strong>One RP ID</strong> — 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">🔒</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’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’re online,
|
|
merge automatically when you’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 “someone else is editing” 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 & 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’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;
|
|
}
|
|
`;
|