From e2e9bc42fded697bc5e80bc56f6958bd5b72f2c1 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 14 Apr 2026 10:43:18 -0400 Subject: [PATCH] =?UTF-8?q?feat(landing):=20full=20redesign=20=E2=80=94=20?= =?UTF-8?q?8-section=20layout=20with=20tabbed=20app=20categories?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hero with animated CSS orbs + grid overlay, stats bar, flow stories with accent borders, 9-category tabbed showcase (all 37 rApps), 3-step how-it-works with dotted connectors, EncryptID, final CTA, categorized footer columns. Fixes hero hiding behind 56px header. New lp-* CSS prefix, reduced-motion support, light/dark mode. Co-Authored-By: Claude Opus 4.6 --- server/landing.ts | 829 ++++++++++++++++++++++++++++++---------------- 1 file changed, 547 insertions(+), 282 deletions(-) diff --git a/server/landing.ts b/server/landing.ts index b52590aa..2e48c82d 100644 --- a/server/landing.ts +++ b/server/landing.ts @@ -9,22 +9,72 @@ import type { ModuleInfo } from "../shared/module"; import { escapeHtml, escapeAttr, MODULE_LANDING_CSS, RICH_LANDING_CSS, versionAssetUrls, getSpaceShellMeta } from "./shell"; +/** Category β†’ module IDs mapping for the tabbed showcase. */ +const CATEGORY_GROUPS: Record = { + plan: { label: "Plan", icon: "πŸ“‹", ids: ["rcal", "rtasks", "rschedule", "rtime"] }, + create: { label: "Create", icon: "✏️", ids: ["rspace", "rdocs", "rnotes", "rdesign", "rsplat", "rpubs", "rsheets", "rbooks"] }, + communicate: { label: "Communicate", icon: "πŸ’¬", ids: ["rchats", "rforum", "rinbox", "rmeets", "rsocials"] }, + govern: { label: "Govern", icon: "βš–οΈ", ids: ["rchoices", "rvote", "rgov", "crowdsurf"] }, + transact: { label: "Transact", icon: "πŸ’°", ids: ["rwallet", "rflows", "rexchange", "rauctions", "rcart", "rswag"] }, + explore: { label: "Explore", icon: "πŸ—ΊοΈ", ids: ["rmaps", "rtrips", "rbnb", "rvnb"] }, + data: { label: "Data", icon: "πŸ“Š", ids: ["rdata", "rfiles", "rphotos", "rtube"] }, + identity: { label: "Identity", icon: "πŸͺͺ", ids: ["rnetwork"] }, + ai: { label: "AI", icon: "πŸ€–", ids: ["ragents"] }, +}; + 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) => ` - -
${m.icon}
-

${escapeHtml(m.name)}

- ${m.standaloneDomain ? `${escapeHtml(m.standaloneDomain)}` : ""} -

${escapeHtml(m.description)}

-
`, - ) - .join("\n"); + // ── Build categorized app panels ── + const allCategorized = new Set(Object.values(CATEGORY_GROUPS).flatMap(g => g.ids)); + const uncategorized = modules.filter(m => !allCategorized.has(m.id)); + + const categoryKeys = Object.keys(CATEGORY_GROUPS); + // Add "other" if there are uncategorized modules + if (uncategorized.length > 0) { + categoryKeys.push("other"); + } + + function renderCard(m: ModuleInfo): string { + return ` + ${m.icon} +
+ ${escapeHtml(m.name)} + ${m.standaloneDomain ? `${escapeHtml(m.standaloneDomain)}` : ""} + ${escapeHtml(m.description)} +
+
`; + } + + const tabButtons = categoryKeys.map((key, i) => { + const g = CATEGORY_GROUPS[key] || { label: "Other", icon: "πŸ“¦" }; + return ``; + }).join("\n "); + + const tabPanels = categoryKeys.map((key, i) => { + const mods = key === "other" + ? uncategorized + : (CATEGORY_GROUPS[key]?.ids ?? []) + .map(id => modules.find(m => m.id === id)) + .filter((m): m is ModuleInfo => m != null); + return `
+
${mods.map(renderCard).join("\n")}
+
`; + }).join("\n "); + + // ── Footer category columns ── + const footerColumns = Object.entries(CATEGORY_GROUPS).map(([, g]) => { + const links = g.ids + .map(id => modules.find(m => m.id === id)) + .filter((m): m is ModuleInfo => m != null) + .map(m => `${escapeHtml(m.name)}`) + .join("\n "); + return ``; + }).join("\n "); return versionAssetUrls(` @@ -34,13 +84,13 @@ export function renderMainLanding(modules: ModuleInfo[]): string { rSpace β€” Own Your Community Infrastructure - + - + @@ -49,7 +99,7 @@ export function renderMainLanding(modules: ModuleInfo[]): string { - + @@ -75,121 +125,134 @@ export function renderMainLanding(modules: ModuleInfo[]): string { - -
-
- The rStack of rApps -

(you)rSpace

-

- One platform. Every tool your community needs. All talking to each other. -

-

- Data flows between apps automatically — no import/export rituals. - Local-first, encrypted, and yours. -

-
- Start (you)rSpace - Explore the rApps + +
+ + + +
+ Local-first community platform +

(you)rSpace

+

+ One platform. ${modules.length} apps. All your community’s tools talking to each other. +

+ +
+ 📡 Offline-first + 🔒 Encrypted + 👥 Multiplayer + 🛠 Open Source +
-
- Local-First - - Multiplayer - - Encrypted - - Open Source +
+ + +
+
+
${modules.length} composable apps
+
1 passkey for everything
+
0 data sold to anyone
- +
-

One rStack. Every Tool Connected.

+

One Platform. Every Tool Connected.

- rApps talk to each other through one shared sync layer. Here’s what that looks like. + rApps share one sync layer. Data flows automatically — no import/export rituals.

-
-
-
- 📅 rCal - - 📋 rTasks - - 💬 rChats +
+
+
+ 📅 rCal + + 📋 rTasks + + 💬 rChats
-

Schedule a meeting, auto-generates a task, notifies your space.

+

Schedule a meeting → auto-generates a task → notifies your space.

-
-
- ☑ rChoices - - 💰 rWallet +
+
+ ☑ rChoices + + 💰 rWallet
-

Vote passes, budget allocation releases automatically.

+

Vote passes → budget allocation releases automatically.

-
-
- 🗺 rMaps - - 📝 rDocs +
+
+ 🗺 rMaps + + 📝 rDocs
-

Pin a community location, shared doc created for that place.

+

Pin a community location → shared doc created for that place.

-
-
- ⏱ rTime - - 📊 rData +
+
+ ⏱ rTime + + 📊 rData
-

Log commitments, analytics update in real-time.

+

Log commitments → analytics update in real-time.

- +
-

${modules.length} rApps and Growing

+

${modules.length} rApps and Growing

Each app is independent and composable. Use one, use all, mix and match.

-
- ${ecosystemCards} +
+
+ ${tabButtons} +
+
+ ${tabPanels} +
- +
-

Offline-First, Always

+

Built Offline-First

Your data lives on your device. Changes sync when you’re online, merge automatically when you’re not.

-
-
-
1
+
+
+
1

Local Persistence

-

Every document is stored in encrypted IndexedDB on your device. Works without internet.

+

Every document stored in encrypted IndexedDB on your device. Works without internet.

-
-
2
+ +
+
2

Auto-Merge CRDT

Automerge CRDTs resolve conflicts automatically. No “someone else is editing” lockouts.

-
-
3
+ +
+
3

Incremental Sync

-

Only changed bytes travel over the wire. Reconnect after days offline and catch up in seconds.

+

Only changed bytes travel the wire. Reconnect after days offline and catch up in seconds.

- +
@@ -212,60 +275,51 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
-
- +
+ -

Touch. Tap. Done.

+

Touch. Tap. Done.

- -
+ +
-

Built for Communities, Not Corporations

-

+

Your community. Your rules. Your data.

+

No algorithms deciding what you see. No ads. No data harvesting. Just tools that work for you, run by you, owned by you.

-

- Your space. Your community. Your rules. -

-
- -