feat: upgrade 6 module demos to use real interactive components
Replace static HTML mockups and WebSocket skeleton demos with the actual self-contained web components (with built-in demo data) for rcart, rtube, rfunds, rnotes, rtrips, and rvote — matching the rcal demo pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ef1d93d7e9
commit
f7ecf50588
|
|
@ -1,230 +1,112 @@
|
||||||
/**
|
/**
|
||||||
* rCart demo page — static community garden shopping cart.
|
* rCart demo page — server-rendered HTML body.
|
||||||
*
|
*
|
||||||
* Renders a fully server-side demo with 8 cart items, funding progress bars,
|
* Embeds the full <folk-cart-shop space="demo"> component for
|
||||||
* member activity, and summary stats. No WebSocket needed (all static data).
|
* real interactivity (catalog browsing, order tracking, filtering)
|
||||||
|
* plus showcase sections explaining the rCart vision.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ─── Mock Data ─────────────────────────────────────────────── */
|
const FEATURES = [
|
||||||
|
{
|
||||||
const members = [
|
icon: "\u{1F30D}",
|
||||||
{ name: "Alice", color: "#10b981" },
|
title: "Cosmolocal Fulfillment",
|
||||||
{ name: "Bob", color: "#0ea5e9" },
|
desc: "Orders are matched to the nearest capable print shop or makerspace. Design global, manufacture local.",
|
||||||
{ name: "Carol", color: "#f59e0b" },
|
},
|
||||||
{ name: "Dave", color: "#8b5cf6" },
|
{
|
||||||
|
icon: "\u{1F6D2}",
|
||||||
|
title: "Group Shopping",
|
||||||
|
desc: "Communities pool resources and split costs. Transparent funding progress for every item in the cart.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "\u{1F4B0}",
|
||||||
|
title: "Revenue Splits",
|
||||||
|
desc: "Every order automatically splits revenue between provider, creator, and community fund via rFunds flows.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "\u{1F4E6}",
|
||||||
|
title: "Order Tracking",
|
||||||
|
desc: "Follow orders from pending through production to delivery. Real-time status updates across the community.",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
interface CartItem {
|
const INTEGRATIONS = [
|
||||||
name: string;
|
{ icon: "\u{1F30A}", name: "rFunds", desc: "Revenue from orders flows through TBFF budget funnels with enoughness thresholds." },
|
||||||
price: number;
|
{ icon: "\u{1F3A8}", name: "rDesign", desc: "Design artifacts become print-ready catalog entries with one click." },
|
||||||
requestedBy: string;
|
{ icon: "\u{1F465}", name: "rNetwork", desc: "Provider registry matches orders to the closest maker in your network." },
|
||||||
funded: number;
|
{ icon: "\u{1F5FA}", name: "rMaps", desc: "See provider locations and delivery zones on the map." },
|
||||||
status: "Funded" | "In Cart" | "Needs Funding";
|
{ icon: "\u{1F4DD}", name: "rNotes", desc: "Link product specs, sizing guides, and design notes to catalog items." },
|
||||||
}
|
{ icon: "\u2615\uFE0F", name: "rSpace", desc: "Each space has its own shop. Nest catalogs across spaces for cross-community commerce." },
|
||||||
|
|
||||||
const cartItems: CartItem[] = [
|
|
||||||
{ name: "Raised Garden Bed Kit (4x8 ft)", price: 89.99, requestedBy: "Alice", funded: 89.99, status: "Funded" },
|
|
||||||
{ name: "Organic Seed Variety Pack (30 types)", price: 34.5, requestedBy: "Carol", funded: 34.5, status: "Funded" },
|
|
||||||
{ name: "Premium Potting Soil (40 qt, 3-pack)", price: 47.99, requestedBy: "Bob", funded: 32.0, status: "In Cart" },
|
|
||||||
{ name: "Stainless Steel Garden Tool Set", price: 62.0, requestedBy: "Dave", funded: 62.0, status: "Funded" },
|
|
||||||
{ name: "Drip Irrigation Kit (100 ft)", price: 54.95, requestedBy: "Alice", funded: 20.0, status: "Needs Funding" },
|
|
||||||
{ name: "Compost Tumbler (45 gal)", price: 109.0, requestedBy: "Bob", funded: 109.0, status: "Funded" },
|
|
||||||
{ name: "Garden Kneeling Pad & Gloves Set", price: 28.5, requestedBy: "Carol", funded: 12.0, status: "Needs Funding" },
|
|
||||||
{ name: "Solar-Powered Pest Repeller (4-pack)", price: 39.99, requestedBy: "Dave", funded: 39.99, status: "In Cart" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/* ─── Helpers ──────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
function getMemberColor(name: string): string {
|
|
||||||
return members.find((m) => m.name === name)?.color || "#64748b";
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusBadgeClass(status: CartItem["status"]): string {
|
|
||||||
switch (status) {
|
|
||||||
case "Funded":
|
|
||||||
return "rd-badge--emerald";
|
|
||||||
case "In Cart":
|
|
||||||
return "rd-badge--sky";
|
|
||||||
case "Needs Funding":
|
|
||||||
return "rd-badge--amber";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function progressFillClass(pct: number): string {
|
|
||||||
if (pct >= 100) return "rd-progress__fill--emerald";
|
|
||||||
if (pct >= 50) return "rd-progress__fill--sky";
|
|
||||||
return "rd-progress__fill--amber";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Render ─────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
export function renderDemo(): string {
|
export function renderDemo(): string {
|
||||||
const totalCost = cartItems.reduce((sum, item) => sum + item.price, 0);
|
|
||||||
const totalFunded = cartItems.reduce((sum, item) => sum + item.funded, 0);
|
|
||||||
const perPerson = totalCost / members.length;
|
|
||||||
const fundedCount = cartItems.filter((i) => i.status === "Funded").length;
|
|
||||||
const overallPct = Math.round((totalFunded / totalCost) * 100);
|
|
||||||
const uniqueRequesters = new Set(cartItems.map((i) => i.requestedBy)).size;
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="rd-root" style="--rd-accent-from: #10b981; --rd-accent-to: #2dd4bf;">
|
<div class="rd-root" style="--rd-accent-from:#10b981; --rd-accent-to:#2dd4bf;">
|
||||||
|
|
||||||
<!-- ── Hero ── -->
|
<!-- Hero -->
|
||||||
<section class="rd-hero">
|
<section class="rd-hero">
|
||||||
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(16,185,129,0.1);border:1px solid rgba(16,185,129,0.2);border-radius:9999px;font-size:0.875rem;color:#6ee7b7;font-weight:500;margin-bottom:1.5rem;">
|
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(16,185,129,0.1);border:1px solid rgba(16,185,129,0.2);border-radius:9999px;font-size:0.875rem;color:#6ee7b7;font-weight:500;margin-bottom:1.5rem;">
|
||||||
Group Shopping, Together
|
Cosmolocal Print-on-Demand
|
||||||
</div>
|
</div>
|
||||||
<h1>See how rCart works</h1>
|
<h1>rCart Demo</h1>
|
||||||
<p class="rd-subtitle">
|
<p class="rd-subtitle">Group shopping with cosmolocal fulfillment, revenue splits, and transparent funding</p>
|
||||||
A community garden project where neighbors pool resources to buy everything they need together.
|
|
||||||
</p>
|
|
||||||
<div class="rd-meta">
|
<div class="rd-meta">
|
||||||
<span>8 items</span>
|
<span>\u{1F6D2} Catalog & Orders</span>
|
||||||
<span style="color:#475569">|</span>
|
<span style="color:#475569">|</span>
|
||||||
<span>$${totalCost.toFixed(2)} total</span>
|
<span>\u{1F30D} Local Fulfillment</span>
|
||||||
<span style="color:#475569">|</span>
|
<span style="color:#475569">|</span>
|
||||||
<span>${fundedCount}/${cartItems.length} funded</span>
|
<span>\u{1F4B0} Revenue Splits</span>
|
||||||
</div>
|
<span style="color:#475569">|</span>
|
||||||
<div class="rd-avatars">
|
<span>\u{1F4E6} Order Tracking</span>
|
||||||
${members
|
|
||||||
.map(
|
|
||||||
(m) =>
|
|
||||||
`<div class="rd-avatar" style="background:${m.color}" title="${m.name}">${m.name[0]}</div>`,
|
|
||||||
)
|
|
||||||
.join("\n ")}
|
|
||||||
<span class="rd-count">${members.length} members</span>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Overall Funding Progress ── -->
|
<!-- Interactive Shop -->
|
||||||
<section class="rd-section rd-section--narrow">
|
<section class="rd-section rd-section--narrow">
|
||||||
<div class="rd-card" style="padding:1.5rem;margin-bottom:1.5rem;">
|
<div class="rd-card" style="padding:0;overflow:hidden;">
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
|
<folk-cart-shop space="demo"></folk-cart-shop>
|
||||||
<div>
|
|
||||||
<h2 style="font-size:1.125rem;font-weight:600;color:#f1f5f9;margin:0 0 0.25rem;">Community Garden Project</h2>
|
|
||||||
<p class="rd-text-xs rd-text-muted" style="margin:0;">Shared cart for our neighborhood garden setup</p>
|
|
||||||
</div>
|
|
||||||
<div style="text-align:right">
|
|
||||||
<p style="font-size:1.5rem;font-weight:700;color:white;margin:0;">$${totalFunded.toFixed(2)}</p>
|
|
||||||
<p class="rd-text-xs rd-text-muted" style="margin:0;">of $${totalCost.toFixed(2)} funded</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="rd-progress" style="margin-bottom:0.5rem;">
|
|
||||||
<div class="rd-progress__fill" style="width:${overallPct}%"></div>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;">
|
|
||||||
<span class="rd-text-xs rd-text-muted">${overallPct}% funded</span>
|
|
||||||
<span class="rd-text-xs rd-text-muted">$${(totalCost - totalFunded).toFixed(2)} remaining</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── Cart Items ── -->
|
|
||||||
<div class="rd-card" style="margin-bottom:1.5rem;">
|
|
||||||
<div class="rd-card-header">
|
|
||||||
<div class="rd-card-title"><span class="rd-icon">🛒</span> Cart Items</div>
|
|
||||||
<div style="display:flex;align-items:center;gap:0.75rem;font-size:0.75rem;color:#94a3b8;">
|
|
||||||
<span style="display:flex;align-items:center;gap:0.25rem;">
|
|
||||||
<span style="width:0.5rem;height:0.5rem;border-radius:9999px;background:#10b981;display:inline-block;"></span> Funded
|
|
||||||
</span>
|
|
||||||
<span style="display:flex;align-items:center;gap:0.25rem;">
|
|
||||||
<span style="width:0.5rem;height:0.5rem;border-radius:9999px;background:#0ea5e9;display:inline-block;"></span> In Cart
|
|
||||||
</span>
|
|
||||||
<span style="display:flex;align-items:center;gap:0.25rem;">
|
|
||||||
<span style="width:0.5rem;height:0.5rem;border-radius:9999px;background:#f59e0b;display:inline-block;"></span> Needs Funding
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${cartItems
|
|
||||||
.map((item) => {
|
|
||||||
const pct = Math.round((item.funded / item.price) * 100);
|
|
||||||
const memberColor = getMemberColor(item.requestedBy);
|
|
||||||
return `
|
|
||||||
<div style="padding:1rem 1.25rem;${cartItems.indexOf(item) > 0 ? "border-top:1px solid rgba(51,65,85,0.3);" : ""}">
|
|
||||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;margin-bottom:0.5rem;">
|
|
||||||
<div style="min-width:0;flex:1;">
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.25rem;">
|
|
||||||
<span class="rd-text-sm rd-font-medium" style="color:#e2e8f0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${item.name}</span>
|
|
||||||
<span class="rd-badge ${statusBadgeClass(item.status)}">${item.status}</span>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;color:#94a3b8;">
|
|
||||||
<span style="display:flex;align-items:center;gap:0.375rem;">
|
|
||||||
<span style="width:1rem;height:1rem;background:${memberColor};border-radius:9999px;display:inline-flex;align-items:center;justify-content:center;font-size:0.625rem;font-weight:700;color:white;">${item.requestedBy[0]}</span>
|
|
||||||
${item.requestedBy}
|
|
||||||
</span>
|
|
||||||
<span style="color:#475569;">requested this</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="text-align:right;flex-shrink:0;">
|
|
||||||
<p class="rd-text-sm rd-font-semibold" style="color:#e2e8f0;margin:0;">$${item.price.toFixed(2)}</p>
|
|
||||||
${item.status !== "Funded" ? `<p class="rd-text-xs" style="color:#64748b;margin:0;">$${item.funded.toFixed(2)} funded</p>` : ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="rd-progress rd-progress--sm">
|
|
||||||
<div class="rd-progress__fill ${progressFillClass(pct)}" style="width:${pct}%"></div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
})
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── Summary Grid ── -->
|
|
||||||
<div class="rd-grid rd-grid--3" style="margin-bottom:1.5rem;">
|
|
||||||
<div class="rd-stat">
|
|
||||||
<p class="rd-text-xs rd-font-semibold rd-text-muted" style="text-transform:uppercase;letter-spacing:0.05em;margin:0 0 0.5rem;">Total Cost</p>
|
|
||||||
<p class="rd-stat__value">$${totalCost.toFixed(2)}</p>
|
|
||||||
<p class="rd-stat__sub">${cartItems.length} items across ${uniqueRequesters} requesters</p>
|
|
||||||
</div>
|
|
||||||
<div class="rd-stat">
|
|
||||||
<p class="rd-text-xs rd-font-semibold rd-text-muted" style="text-transform:uppercase;letter-spacing:0.05em;margin:0 0 0.5rem;">Amount Funded</p>
|
|
||||||
<p class="rd-stat__value" style="color:#34d399;">$${totalFunded.toFixed(2)}</p>
|
|
||||||
<p class="rd-stat__sub">${fundedCount} of ${cartItems.length} items fully funded</p>
|
|
||||||
</div>
|
|
||||||
<div class="rd-stat">
|
|
||||||
<p class="rd-text-xs rd-font-semibold rd-text-muted" style="text-transform:uppercase;letter-spacing:0.05em;margin:0 0 0.5rem;">Per-Person Split</p>
|
|
||||||
<p class="rd-stat__value" style="color:#2dd4bf;">$${perPerson.toFixed(2)}</p>
|
|
||||||
<p class="rd-stat__sub">split equally among ${members.length} members</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── Member Activity ── -->
|
|
||||||
<div class="rd-card" style="padding:1.5rem;">
|
|
||||||
<h3 class="rd-text-sm rd-font-semibold" style="color:#cbd5e1;margin:0 0 1rem;">Member Activity</h3>
|
|
||||||
<div class="rd-grid rd-grid--2">
|
|
||||||
${members
|
|
||||||
.map((member) => {
|
|
||||||
const requested = cartItems.filter((i) => i.requestedBy === member.name);
|
|
||||||
const requestedTotal = requested.reduce((sum, i) => sum + i.price, 0);
|
|
||||||
return `
|
|
||||||
<div style="display:flex;align-items:center;gap:0.75rem;background:rgba(51,65,85,0.2);border-radius:0.75rem;padding:0.75rem;">
|
|
||||||
<div class="rd-avatar" style="background:${member.color};flex-shrink:0;width:2.5rem;height:2.5rem;font-size:0.875rem;">${member.name[0]}</div>
|
|
||||||
<div style="min-width:0;flex:1;">
|
|
||||||
<p class="rd-text-sm rd-font-medium" style="color:#e2e8f0;margin:0;">${member.name}</p>
|
|
||||||
<p class="rd-text-xs rd-text-muted" style="margin:0;">
|
|
||||||
${requested.length} item${requested.length !== 1 ? "s" : ""} requested
|
|
||||||
<span style="color:#475569;margin:0 0.375rem;">·</span>
|
|
||||||
$${requestedTotal.toFixed(2)} total
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div style="text-align:right;flex-shrink:0;">
|
|
||||||
<p class="rd-text-sm rd-font-medium" style="color:#cbd5e1;margin:0;">$${perPerson.toFixed(2)}</p>
|
|
||||||
<p class="rd-text-xs" style="color:#64748b;margin:0;">share</p>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
})
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── CTA ── -->
|
<!-- Core Concepts -->
|
||||||
|
<section class="rd-section">
|
||||||
|
<div class="rd-grid rd-grid--2">
|
||||||
|
${FEATURES.map(
|
||||||
|
(f) => `
|
||||||
|
<div class="rd-card" style="padding:1.5rem;">
|
||||||
|
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
|
||||||
|
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
|
||||||
|
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
|
||||||
|
</div>`,
|
||||||
|
).join("")}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Ecosystem Integrations -->
|
||||||
|
<section class="rd-section">
|
||||||
|
<h2 style="text-align:center;font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0 0 1.5rem;">
|
||||||
|
r* Ecosystem Integrations
|
||||||
|
</h2>
|
||||||
|
<div class="rd-grid rd-grid--3">
|
||||||
|
${INTEGRATIONS.map(
|
||||||
|
(i) => `
|
||||||
|
<div class="rd-card" style="padding:1.25rem;">
|
||||||
|
<div style="font-size:1.5rem;margin-bottom:0.5rem;">${i.icon}</div>
|
||||||
|
<h3 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0 0 0.375rem;">${i.name}</h3>
|
||||||
|
<p style="font-size:0.8rem;color:#94a3b8;margin:0;line-height:1.4;">${i.desc}</p>
|
||||||
|
</div>`,
|
||||||
|
).join("")}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
<section class="rd-section rd-section--narrow">
|
<section class="rd-section rd-section--narrow">
|
||||||
<div class="rd-cta">
|
<div class="rd-cta">
|
||||||
<h2>Ready to shop together?</h2>
|
<h2>Ready to shop together?</h2>
|
||||||
<p>
|
<p>
|
||||||
Create a shared cart for your group, community, or team. Add items from any store,
|
rCart gives your community a shared catalog with cosmolocal fulfillment,
|
||||||
split costs fairly, and check out together.
|
transparent funding, and automatic revenue splits.
|
||||||
</p>
|
</p>
|
||||||
<a href="/create-space" style="background:linear-gradient(135deg, #10b981, #059669);box-shadow:0 8px 24px rgba(16,185,129,0.25);">
|
<a href="/create-space" style="background:linear-gradient(135deg,#10b981,#059669);box-shadow:0 8px 24px rgba(16,185,129,0.25);">
|
||||||
Create Your First Cart
|
Create Your First Cart
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -451,7 +451,7 @@ routes.get("/", (c) => {
|
||||||
modules: getModuleInfoList(),
|
modules: getModuleInfoList(),
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
body: renderDemo(),
|
body: renderDemo(),
|
||||||
scripts: `<script type="module" src="/modules/rcart/cart-demo.js"></script>`,
|
scripts: `<script type="module" src="/modules/rcart/folk-cart-shop.js"></script>`,
|
||||||
styles: `<link rel="stylesheet" href="/modules/rcart/cart.css">`,
|
styles: `<link rel="stylesheet" href="/modules/rcart/cart.css">`,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,256 +1,116 @@
|
||||||
/**
|
/**
|
||||||
* rFunds demo page — group expense tracking for "Alpine Explorer 2026".
|
* rFunds demo page — server-rendered HTML body.
|
||||||
*
|
*
|
||||||
* Renders server-side HTML skeleton with budget overview, expense list,
|
* Embeds the full <folk-funds-app space="demo" mode="demo"> component
|
||||||
* balances, settlements, category breakdown, and per-person stats.
|
* for real interactivity (flow listing, river visualization, TBFF diagrams)
|
||||||
* The client-side funds-demo.ts hydrates via WebSocket (DemoSync).
|
* plus showcase sections explaining the rFunds vision.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ─── Constants ─────────────────────────────────────────────── */
|
const FEATURES = [
|
||||||
|
{
|
||||||
const MEMBERS = [
|
icon: "\u{1F30A}",
|
||||||
{ name: "Maya", initial: "M", color: "#10b981", bgClass: "rd-bg-emerald" },
|
title: "River Visualization",
|
||||||
{ name: "Liam", initial: "L", color: "#06b6d4", bgClass: "rd-bg-cyan" },
|
desc: "Watch resources flow through animated Sankey rivers. Sources feed into funnels, funnels feed outcomes, and surplus overflows to where it's needed most.",
|
||||||
{ name: "Priya", initial: "P", color: "#8b5cf6", bgClass: "rd-bg-violet" },
|
},
|
||||||
{ name: "Omar", initial: "O", color: "#f59e0b", bgClass: "rd-bg-amber" },
|
{
|
||||||
|
icon: "\u{1F4CA}",
|
||||||
|
title: "TBFF Flows",
|
||||||
|
desc: "Threshold-Based Funding Flows distribute resources based on enoughness. When a funnel is sufficient, surplus flows to the next highest-need area.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "\u{1F4B8}",
|
||||||
|
title: "Treasury Management",
|
||||||
|
desc: "Track deposits, withdrawals, and allocations across all community funnels. Transparent financial governance in real time.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "\u2696\uFE0F",
|
||||||
|
title: "Enoughness Layer",
|
||||||
|
desc: "Each funnel has a sufficiency threshold. Golden glow indicates a funded funnel. Resources keep flowing until everyone has enough.",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const CATEGORIES = [
|
const INTEGRATIONS = [
|
||||||
{ key: "transport", icon: "\u{1F682}", label: "Transport", colorClass: "rd-progress__fill--cyan", badgeClass: "rd-badge--sky", textClass: "rd-cyan" },
|
{ icon: "\u{1F6D2}", name: "rCart", desc: "Order revenue flows through TBFF funnels with automatic creator/provider/community splits." },
|
||||||
{ key: "accommodation", icon: "\u{1F3E8}", label: "Accommodation", colorClass: "rd-progress__fill--violet", badgeClass: "rd-badge--teal", textClass: "rd-violet" },
|
{ icon: "\u{1F5F3}", name: "rVote", desc: "Governance votes determine funding priorities and threshold adjustments." },
|
||||||
{ key: "activity", icon: "\u26F7", label: "Activities", colorClass: "rd-progress__fill--amber", badgeClass: "rd-badge--amber", textClass: "rd-amber" },
|
{ icon: "\u{2708}\uFE0F", name: "rTrips", desc: "Group expenses feed into shared budget flows with per-person tracking." },
|
||||||
{ key: "food", icon: "\u{1F372}", label: "Food & Drink", colorClass: "rd-progress__fill--rose", badgeClass: "rd-badge--rose", textClass: "rd-rose" },
|
{ icon: "\u{1F4DD}", name: "rNotes", desc: "Attach budget rationales, meeting minutes, and audit logs to flows." },
|
||||||
|
{ icon: "\u{1F4C5}", name: "rCal", desc: "Budget reviews, treasury snapshots, and governance votes on the calendar timeline." },
|
||||||
|
{ icon: "\u2615\uFE0F", name: "rSpace", desc: "Each space has its own treasury. Nest flows across spaces for multi-community coordination." },
|
||||||
];
|
];
|
||||||
|
|
||||||
/* ─── Render ─────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
export function renderDemo(): string {
|
export function renderDemo(): string {
|
||||||
return `
|
return `
|
||||||
<div class="rd-root" style="--rd-accent-from: #f59e0b; --rd-accent-to: #10b981;">
|
<div class="rd-root" style="--rd-accent-from:#f59e0b; --rd-accent-to:#10b981;">
|
||||||
|
|
||||||
<!-- ── Hero ── -->
|
<!-- Hero -->
|
||||||
<section class="rd-hero">
|
<section class="rd-hero">
|
||||||
<h1>Alpine Explorer 2026</h1>
|
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.2);border-radius:9999px;font-size:0.875rem;color:#fcd34d;font-weight:500;margin-bottom:1.5rem;">
|
||||||
<p class="rd-subtitle">Group Expenses</p>
|
Threshold-Based Funding Flows
|
||||||
|
</div>
|
||||||
|
<h1>rFunds Demo</h1>
|
||||||
|
<p class="rd-subtitle">Budget flows, river visualization, and treasury management with enoughness thresholds</p>
|
||||||
<div class="rd-meta">
|
<div class="rd-meta">
|
||||||
<span>\u{1F4C5} Jul 6-20, 2026</span>
|
<span>\u{1F30A} River Visualization</span>
|
||||||
<span style="color:#475569">|</span>
|
<span style="color:#475569">|</span>
|
||||||
<span>\u{1F465} ${MEMBERS.length} travelers</span>
|
<span>\u{1F4CA} TBFF Flows</span>
|
||||||
<span style="color:#475569">|</span>
|
<span style="color:#475569">|</span>
|
||||||
<span>\u{1F3D4} Chamonix \u2192 Zermatt \u2192 Dolomites</span>
|
<span>\u{1F4B8} Treasury</span>
|
||||||
</div>
|
<span style="color:#475569">|</span>
|
||||||
<div class="rd-avatars">
|
<span>\u2696\uFE0F Enoughness</span>
|
||||||
${MEMBERS.map(
|
|
||||||
(m) =>
|
|
||||||
`<div class="rd-avatar ${m.bgClass}" title="${m.name}">${m.initial}</div>`,
|
|
||||||
).join("\n ")}
|
|
||||||
<span class="rd-count">${MEMBERS.length} members</span>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Status bar ── -->
|
<!-- Interactive Funds App -->
|
||||||
<div class="rd-section rd-section--narrow">
|
<section class="rd-section rd-section--narrow">
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1.5rem; flex-wrap:wrap; gap:0.75rem">
|
<div class="rd-card" style="padding:0;overflow:hidden;">
|
||||||
<div style="display:flex; align-items:center; gap:0.75rem; flex-wrap:wrap">
|
<folk-funds-app space="demo" mode="demo"></folk-funds-app>
|
||||||
<span id="rd-conn-badge" class="rd-status rd-status--disconnected">Disconnected</span>
|
|
||||||
<span class="rd-badge rd-badge--amber" style="font-size:0.7rem">Live — synced across all r* demos</span>
|
|
||||||
</div>
|
|
||||||
<button id="rd-reset-btn" class="rd-btn rd-btn--ghost" disabled>
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
|
|
||||||
Reset Demo
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── Loading state ── -->
|
|
||||||
<div class="rd-section rd-section--narrow">
|
|
||||||
<div id="rd-loading" class="rd-card" style="border:2px dashed rgba(100,116,139,0.4); display:none">
|
|
||||||
<div class="rd-card-body" style="padding:3rem; text-align:center">
|
|
||||||
<div style="width:2rem; height:2rem; margin:0 auto 0.75rem; border:3px solid rgba(245,158,11,0.2); border-top-color:#f59e0b; border-radius:50%; animation:rd-spin 0.8s linear infinite"></div>
|
|
||||||
<p class="rd-text-muted">Connecting to rSpace...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<style>@keyframes rd-spin { to { transform: rotate(360deg); } }</style>
|
|
||||||
|
|
||||||
<!-- Empty state -->
|
|
||||||
<div id="rd-empty" class="rd-card" style="border:2px dashed rgba(100,116,139,0.4); display:none">
|
|
||||||
<div class="rd-card-body" style="padding:3rem; text-align:center">
|
|
||||||
<div style="font-size:2rem; margin-bottom:0.75rem">\u{1F4B0}</div>
|
|
||||||
<p class="rd-text-muted">No expense data found. Try resetting the demo.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── Budget Overview ── -->
|
|
||||||
<section id="rd-budget-section" class="rd-section rd-section--narrow" style="display:none">
|
|
||||||
<div class="rd-card" style="padding:1.5rem; margin-bottom:1.5rem;">
|
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1rem;">
|
|
||||||
<h2 style="font-size:1.125rem; font-weight:600; color:#f1f5f9; margin:0; display:flex; align-items:center; gap:0.5rem;">
|
|
||||||
\u{1F4CA} Trip Budget
|
|
||||||
</h2>
|
|
||||||
<span class="rd-live">live</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Budget totals: 3-column stat grid -->
|
|
||||||
<div class="rd-grid rd-grid--3" style="margin-bottom:1rem;">
|
|
||||||
<div class="rd-stat">
|
|
||||||
<p class="rd-stat__value" id="rd-budget-total">\u20AC4,000</p>
|
|
||||||
<p class="rd-stat__label">Total Budget</p>
|
|
||||||
</div>
|
|
||||||
<div class="rd-stat">
|
|
||||||
<p class="rd-stat__value rd-emerald" id="rd-budget-spent">\u20AC0</p>
|
|
||||||
<p class="rd-stat__label">Spent</p>
|
|
||||||
</div>
|
|
||||||
<div class="rd-stat">
|
|
||||||
<p class="rd-stat__value rd-cyan" id="rd-budget-remaining">\u20AC4,000</p>
|
|
||||||
<p class="rd-stat__label" id="rd-budget-remaining-label">Remaining</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Progress bar -->
|
|
||||||
<div style="margin-bottom:1rem;">
|
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; font-size:0.75rem; color:#94a3b8; margin-bottom:0.375rem;">
|
|
||||||
<span id="rd-budget-pct-label">0% used</span>
|
|
||||||
<span id="rd-budget-left-label">\u20AC4,000 left</span>
|
|
||||||
</div>
|
|
||||||
<div class="rd-progress">
|
|
||||||
<div class="rd-progress__fill rd-progress__fill--emerald" id="rd-budget-bar" style="width:0%"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Category breakdown -->
|
|
||||||
<div>
|
|
||||||
<h3 style="font-size:0.875rem; font-weight:600; color:#cbd5e1; margin:0 0 0.75rem;">Budget by Category</h3>
|
|
||||||
<div class="rd-grid rd-grid--2" id="rd-category-breakdown">
|
|
||||||
${CATEGORIES.map(
|
|
||||||
(cat) => `
|
|
||||||
<div class="rd-stat" style="padding:0.75rem;" data-category="${cat.key}">
|
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:0.5rem;">
|
|
||||||
<span style="display:flex; align-items:center; gap:0.5rem; font-size:0.875rem; color:#e2e8f0;">
|
|
||||||
${cat.icon} ${cat.label}
|
|
||||||
</span>
|
|
||||||
<span class="rd-text-xs rd-text-muted" data-cat-amounts>\u20AC0 / \u20AC1,000</span>
|
|
||||||
</div>
|
|
||||||
<div class="rd-progress rd-progress--sm">
|
|
||||||
<div class="${cat.colorClass}" style="height:100%; border-radius:9999px; width:0%; transition:width 0.3s" data-cat-bar></div>
|
|
||||||
</div>
|
|
||||||
<p class="rd-text-xs rd-text-dim" style="margin:0.25rem 0 0;" data-cat-pct>0% used</p>
|
|
||||||
</div>`,
|
|
||||||
).join("")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Expenses + Balances grid ── -->
|
<!-- Core Concepts -->
|
||||||
<section id="rd-expenses-section" class="rd-section" style="display:none">
|
<section class="rd-section">
|
||||||
<div style="display:grid; grid-template-columns:1fr; gap:1rem;">
|
<div class="rd-grid rd-grid--2">
|
||||||
|
${FEATURES.map(
|
||||||
<!-- Expense list (left 2/3 on desktop) -->
|
(f) => `
|
||||||
<div class="rd-card" id="rd-expense-card" style="grid-column:1;">
|
<div class="rd-card" style="padding:1.5rem;">
|
||||||
<div class="rd-card-header">
|
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
|
||||||
<div class="rd-card-title"><span class="rd-icon">\u{1F4DD}</span> <span id="rd-expense-count">Expenses (0)</span></div>
|
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
|
||||||
<span class="rd-text-xs rd-text-muted">Click amount to edit</span>
|
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
|
||||||
</div>
|
</div>`,
|
||||||
<div id="rd-expense-list">
|
).join("")}
|
||||||
<!-- Populated by funds-demo.ts -->
|
|
||||||
</div>
|
|
||||||
<!-- Total row -->
|
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; padding:0.75rem 1.25rem; border-top:1px solid rgba(51,65,85,0.5); background:rgba(51,65,85,0.2);">
|
|
||||||
<span style="font-size:0.875rem; font-weight:600; color:#cbd5e1;">Total</span>
|
|
||||||
<span style="font-size:1.125rem; font-weight:700; color:white;" id="rd-expense-total">\u20AC0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Balances (right 1/3 on desktop) -->
|
|
||||||
<div style="display:flex; flex-direction:column; gap:1rem;">
|
|
||||||
<div class="rd-card" id="rd-balances">
|
|
||||||
<div class="rd-card-header">
|
|
||||||
<div class="rd-card-title"><span class="rd-icon">\u2696\uFE0F</span> Balances</div>
|
|
||||||
</div>
|
|
||||||
<div class="rd-card-body" id="rd-balances-body">
|
|
||||||
<!-- Populated by funds-demo.ts -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rd-card" id="rd-settlements">
|
|
||||||
<div class="rd-card-header">
|
|
||||||
<div class="rd-card-title"><span class="rd-icon">\u{1F4B8}</span> Settle Up</div>
|
|
||||||
</div>
|
|
||||||
<div class="rd-card-body" id="rd-settlements-body">
|
|
||||||
<!-- Populated by funds-demo.ts -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Spending by Category ── -->
|
<!-- Ecosystem Integrations -->
|
||||||
<section id="rd-spending-section" class="rd-section" style="display:none">
|
<section class="rd-section">
|
||||||
<div class="rd-card" style="padding:1.5rem;">
|
<h2 style="text-align:center;font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0 0 1.5rem;">
|
||||||
<h2 style="font-size:1.125rem; font-weight:600; margin:0 0 1rem; display:flex; align-items:center; gap:0.5rem;">
|
r* Ecosystem Integrations
|
||||||
\u{1F4CA} Spending by Category
|
</h2>
|
||||||
</h2>
|
<div class="rd-grid rd-grid--3">
|
||||||
<div class="rd-grid rd-grid--4" id="rd-spending-grid">
|
${INTEGRATIONS.map(
|
||||||
${CATEGORIES.map(
|
(i) => `
|
||||||
(cat) => `
|
<div class="rd-card" style="padding:1.25rem;">
|
||||||
<div class="rd-stat" data-spending-cat="${cat.key}">
|
<div style="font-size:1.5rem;margin-bottom:0.5rem;">${i.icon}</div>
|
||||||
<div style="font-size:1.5rem; margin-bottom:0.5rem;">${cat.icon}</div>
|
<h3 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0 0 0.375rem;">${i.name}</h3>
|
||||||
<p style="font-size:0.875rem; color:#cbd5e1; font-weight:500; margin:0;">${cat.label}</p>
|
<p style="font-size:0.8rem;color:#94a3b8;margin:0;line-height:1.4;">${i.desc}</p>
|
||||||
<p style="font-size:1.125rem; font-weight:700; color:white; margin:0.25rem 0;" data-spending-amount>\u20AC0</p>
|
</div>`,
|
||||||
<div class="rd-progress rd-progress--xs" style="margin-bottom:0.25rem;">
|
).join("")}
|
||||||
<div class="${cat.colorClass}" style="height:100%; border-radius:9999px; width:0%; transition:width 0.3s" data-spending-bar></div>
|
|
||||||
</div>
|
|
||||||
<p class="rd-text-xs rd-text-dim" style="margin:0;" data-spending-pct>0% of total</p>
|
|
||||||
</div>`,
|
|
||||||
).join("")}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Per Person ── -->
|
<!-- CTA -->
|
||||||
<section id="rd-person-section" class="rd-section" style="display:none">
|
|
||||||
<div class="rd-card" style="padding:1.5rem;">
|
|
||||||
<h2 style="font-size:1.125rem; font-weight:600; margin:0 0 1rem; display:flex; align-items:center; gap:0.5rem;">
|
|
||||||
\u{1F464} Per Person
|
|
||||||
</h2>
|
|
||||||
<div class="rd-grid rd-grid--4" id="rd-person-grid">
|
|
||||||
${MEMBERS.map(
|
|
||||||
(m) => `
|
|
||||||
<div class="rd-stat" data-person="${m.name}">
|
|
||||||
<div class="rd-avatar ${m.bgClass}" style="width:3rem; height:3rem; font-size:1.125rem; margin:0 auto 0.5rem; box-shadow:0 0 0 2px #1e293b;">${m.initial}</div>
|
|
||||||
<p style="font-size:0.875rem; color:#cbd5e1; font-weight:500; margin:0;">${m.name}</p>
|
|
||||||
<p style="font-size:1.125rem; font-weight:700; color:white; margin:0.25rem 0;" data-person-paid>\u20AC0</p>
|
|
||||||
<p class="rd-text-xs rd-text-dim" style="margin:0;" data-person-pct>paid (0%)</p>
|
|
||||||
<p style="font-size:0.875rem; font-weight:600; margin:0.25rem 0 0;" data-person-balance>\u20AC0</p>
|
|
||||||
</div>`,
|
|
||||||
).join("")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ── CTA ── -->
|
|
||||||
<section class="rd-section rd-section--narrow">
|
<section class="rd-section rd-section--narrow">
|
||||||
<div class="rd-cta">
|
<div class="rd-cta">
|
||||||
<h2>Track Your Group Expenses</h2>
|
<h2>Design Your Funding Flows</h2>
|
||||||
<p>
|
<p>
|
||||||
rFunds makes it easy to manage shared costs, track budgets, and settle up.
|
rFunds lets your community design transparent budget flows with threshold-based
|
||||||
Design custom funding flows with threshold-based mechanisms.
|
mechanisms, enoughness scoring, and animated river visualizations.
|
||||||
</p>
|
</p>
|
||||||
<a href="/create-space" style="background:linear-gradient(135deg, #f59e0b, #10b981); box-shadow:0 8px 24px rgba(245,158,11,0.25);">
|
<a href="/create-space" style="background:linear-gradient(135deg,#f59e0b,#10b981);box-shadow:0 8px 24px rgba(245,158,11,0.25);">
|
||||||
Create Your Space
|
Create Your Space
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>`;
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Responsive grid for expenses + balances on desktop */
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
#rd-expenses-section > div {
|
|
||||||
grid-template-columns: 2fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>`;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,8 +206,7 @@ routes.get("/", (c) => {
|
||||||
modules: getModuleInfoList(),
|
modules: getModuleInfoList(),
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
body: renderDemo(),
|
body: renderDemo(),
|
||||||
demoScripts: `<script type="module" src="/lib/demo-sync.js"></script>
|
scripts: `<script type="module" src="/modules/rfunds/folk-funds-app.js"></script>`,
|
||||||
<script type="module" src="/modules/rfunds/funds-demo.js"></script>`,
|
|
||||||
styles: `<link rel="stylesheet" href="/modules/rfunds/funds.css">`,
|
styles: `<link rel="stylesheet" href="/modules/rfunds/funds.css">`,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,214 +1,104 @@
|
||||||
/**
|
/**
|
||||||
* rNotes demo page — server-rendered HTML body.
|
* rNotes demo page — server-rendered HTML body.
|
||||||
*
|
*
|
||||||
* Returns the static HTML skeleton for the interactive notes demo.
|
* Embeds the full <folk-notes-app space="demo"> component for
|
||||||
* The client-side notes-demo.ts populates note cards, packing list,
|
* real interactivity (notebook browsing, note editing, search, tags)
|
||||||
* sidebar, and notebook header via WebSocket snapshots.
|
* plus showcase sections explaining the rNotes vision.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const FEATURES = [
|
||||||
|
{
|
||||||
|
icon: "\u{1F3A4}",
|
||||||
|
title: "Live Transcription",
|
||||||
|
desc: "Record and transcribe in real time. Stream audio via WebSocket or transcribe offline with Parakeet.js.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "\u270F\uFE0F",
|
||||||
|
title: "Rich Editing",
|
||||||
|
desc: "Headings, lists, code blocks, highlights, images, and file attachments in every note.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "\u{1F4D3}",
|
||||||
|
title: "Notebooks",
|
||||||
|
desc: "Organize notes into notebooks with sections. Nest as deep as you need for any project structure.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "\u{1F3F7}\uFE0F",
|
||||||
|
title: "Flexible Tags",
|
||||||
|
desc: "Cross-cutting tags let you find notes across all notebooks instantly. Filter and search by any combination.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const INTEGRATIONS = [
|
||||||
|
{ icon: "\u{1F4C5}", name: "rCal", desc: "Link notes to calendar events. Meeting agendas, daily journals, and retrospective logs." },
|
||||||
|
{ icon: "\u{1F5FA}", name: "rMaps", desc: "Pin location-aware notes to places on the map. Field notes, venue reviews, site reports." },
|
||||||
|
{ icon: "\u{1F465}", name: "rNetwork", desc: "Collaborate on notes across your network with real-time Automerge sync." },
|
||||||
|
{ icon: "\u{1F3AC}", name: "rTube", desc: "Attach meeting notes, transcripts, and timestamps to video recordings." },
|
||||||
|
{ icon: "\u{1F5F3}", name: "rVote", desc: "Link governance proposals to supporting research notes and discussion threads." },
|
||||||
|
{ icon: "\u2615\uFE0F", name: "rSpace", desc: "Pin any note to the collaborative canvas. Each space has its own knowledge base." },
|
||||||
|
];
|
||||||
|
|
||||||
export function renderDemo(): string {
|
export function renderDemo(): string {
|
||||||
return `
|
return `
|
||||||
<div class="rd-root" style="--rd-accent-from:#f59e0b; --rd-accent-to:#fb923c">
|
<div class="rd-root" style="--rd-accent-from:#f59e0b; --rd-accent-to:#fb923c;">
|
||||||
|
|
||||||
<!-- ── Hero ── -->
|
<!-- Hero -->
|
||||||
<section class="rd-hero">
|
<section class="rd-hero">
|
||||||
<div style="display:inline-flex;align-items:center;gap:0.5rem;padding:0.375rem 1rem;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.2);border-radius:9999px;font-size:0.875rem;color:#fbbf24;font-weight:500;margin-bottom:1.5rem;">
|
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.2);border-radius:9999px;font-size:0.875rem;color:#fbbf24;font-weight:500;margin-bottom:1.5rem;">
|
||||||
<span id="rd-hero-dot" style="width:0.5rem;height:0.5rem;border-radius:9999px;background:#f59e0b;animation:rd-pulse 2s ease-in-out infinite;"></span>
|
Collaborative Knowledge Base
|
||||||
<span id="rd-hero-label">Interactive Demo</span>
|
|
||||||
</div>
|
</div>
|
||||||
<h1>See how rNotes works</h1>
|
<h1>rNotes Demo</h1>
|
||||||
<p class="rd-subtitle">A collaborative knowledge base for your team</p>
|
<p class="rd-subtitle">Notebooks with rich-text notes, voice transcription, and real-time collaboration</p>
|
||||||
<div class="rd-meta">
|
<div class="rd-meta">
|
||||||
<span>Live transcription</span>
|
<span>\u{1F3A4} Transcription</span>
|
||||||
<span>Audio & video</span>
|
<span style="color:#475569">|</span>
|
||||||
<span>Organized notebooks</span>
|
<span>\u270F\uFE0F Rich Editing</span>
|
||||||
<span>Canvas sync</span>
|
<span style="color:#475569">|</span>
|
||||||
<span>Real-time collaboration</span>
|
<span>\u{1F4D3} Notebooks</span>
|
||||||
</div>
|
<span style="color:#475569">|</span>
|
||||||
<div class="rd-avatars" id="rd-avatars">
|
<span>\u{1F3F7}\uFE0F Tags & Search</span>
|
||||||
<div class="rd-avatar" style="background:#14b8a6" title="...">...</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Context bar + Reset ── -->
|
<!-- Interactive Notes App -->
|
||||||
<section class="rd-section rd-section--narrow">
|
<section class="rd-section rd-section--narrow">
|
||||||
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:1rem;margin-bottom:1.5rem;">
|
<div class="rd-card" style="padding:0;overflow:hidden;">
|
||||||
<p style="text-align:center;font-size:0.875rem;color:#94a3b8;max-width:40rem;margin:0;">
|
<folk-notes-app space="demo"></folk-notes-app>
|
||||||
This demo shows a <span style="color:#e2e8f0;font-weight:500">Trip Planning Notebook</span> scenario
|
|
||||||
with notes, a packing list, tags, and canvas sync — all powered by the
|
|
||||||
<span style="color:#e2e8f0;font-weight:500">r* ecosystem</span> with live data from
|
|
||||||
<span style="color:#e2e8f0;font-weight:500">rSpace</span>.
|
|
||||||
</p>
|
|
||||||
<button id="rd-reset-btn" class="rd-btn rd-btn--ghost" disabled>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
|
|
||||||
Reset Demo
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Notebook header card ── -->
|
<!-- Core Concepts -->
|
||||||
<section class="rd-section rd-section--narrow">
|
<section class="rd-section">
|
||||||
<div class="rd-card" id="rd-notebook-header" style="margin-bottom:1.5rem;">
|
<div class="rd-grid rd-grid--2">
|
||||||
<div class="rd-card-header">
|
${FEATURES.map(
|
||||||
<div class="rd-card-title">
|
(f) => `
|
||||||
<span class="rd-icon" style="font-size:1.25rem;">📓</span>
|
<div class="rd-card" style="padding:1.5rem;">
|
||||||
<span id="rd-nb-title">Loading...</span>
|
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
|
||||||
<span id="rd-nb-count" class="rd-text-xs rd-text-muted" style="margin-left:0.5rem;"></span>
|
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
|
||||||
</div>
|
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
|
||||||
<a href="https://rnotes.online" target="_blank" rel="noopener noreferrer" class="rd-card-header rd-open-link">Open in rNotes</a>
|
|
||||||
</div>
|
|
||||||
<div class="rd-card-body" style="padding:0.75rem 1.25rem;">
|
|
||||||
<p id="rd-nb-desc" class="rd-text-sm rd-text-muted" style="margin:0;">Loading notebook data...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── Main layout: sidebar + content ── -->
|
|
||||||
<div style="display:grid;grid-template-columns:1fr;gap:1.5rem;" class="rd-notes-layout">
|
|
||||||
|
|
||||||
<!-- Sidebar -->
|
|
||||||
<div id="rd-sidebar">
|
|
||||||
<div class="rd-card" style="overflow:hidden;">
|
|
||||||
<!-- Sidebar header -->
|
|
||||||
<div style="padding:0.75rem 1rem;border-bottom:1px solid rgba(51,65,85,0.5);display:flex;align-items:center;justify-content:space-between;">
|
|
||||||
<span style="font-size:0.75rem;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:0.05em;">Notebook</span>
|
|
||||||
<span id="rd-sb-note-count" style="font-size:0.75rem;color:#64748b;">0 notes</span>
|
|
||||||
</div>
|
|
||||||
<!-- Active notebook tree -->
|
|
||||||
<div style="padding:0.5rem;">
|
|
||||||
<div style="margin-bottom:0.25rem;">
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0.75rem;border-radius:0.5rem;font-size:0.875rem;background:rgba(245,158,11,0.1);color:#fcd34d;">
|
|
||||||
<span>📓</span>
|
|
||||||
<span id="rd-sb-nb-title" style="font-weight:500;">Loading...</span>
|
|
||||||
</div>
|
|
||||||
<div style="margin-left:1rem;margin-top:0.125rem;">
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:0.375rem 0.75rem;border-radius:0.375rem;font-size:0.75rem;background:rgba(51,65,85,0.4);color:white;">
|
|
||||||
<span>Notes</span>
|
|
||||||
<span id="rd-sb-notes-num" style="color:#475569">0</span>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:0.375rem 0.75rem;border-radius:0.375rem;font-size:0.75rem;color:#64748b;margin-top:0.125rem;cursor:pointer;" onmouseover="this.style.background='rgba(51,65,85,0.2)';this.style.color='#cbd5e1'" onmouseout="this.style.background='';this.style.color='#64748b'">
|
|
||||||
<span>Packing List</span>
|
|
||||||
<span style="color:#475569">1</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Quick info links -->
|
|
||||||
<div style="padding:0.75rem 1rem;border-top:1px solid rgba(51,65,85,0.5);display:flex;flex-direction:column;gap:0.5rem;">
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;color:#64748b;">
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
|
||||||
<span>Search notes...</span>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;color:#64748b;">
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>
|
|
||||||
<span>Browse tags</span>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;color:#64748b;">
|
|
||||||
<svg width="14" height="14" 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>
|
|
||||||
<span>Recent edits</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Notes + Packing list -->
|
|
||||||
<div>
|
|
||||||
<!-- Notes section -->
|
|
||||||
<div id="rd-notes-section">
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;">
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
|
||||||
<h2 style="font-size:0.875rem;font-weight:600;color:#cbd5e1;margin:0;">Notes</h2>
|
|
||||||
<span id="rd-notes-count" class="rd-text-xs rd-text-muted">0 notes</span>
|
|
||||||
</div>
|
|
||||||
<span class="rd-text-xs rd-text-muted">Sort: Recently edited</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading skeleton -->
|
|
||||||
<div id="rd-loading" style="display:flex;flex-direction:column;gap:1rem;">
|
|
||||||
${[1, 2, 3]
|
|
||||||
.map(
|
|
||||||
() => `
|
|
||||||
<div class="rd-card" style="padding:1rem;">
|
|
||||||
<div style="height:1rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:66%;margin-bottom:0.75rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="height:0.75rem;background:rgba(51,65,85,0.3);border-radius:0.25rem;width:100%;margin-bottom:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="height:0.75rem;background:rgba(51,65,85,0.3);border-radius:0.25rem;width:80%;margin-bottom:0.75rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="display:flex;gap:0.5rem;">
|
|
||||||
<div style="height:1.25rem;background:rgba(51,65,85,0.3);border-radius:0.25rem;width:4rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="height:1.25rem;background:rgba(51,65,85,0.3);border-radius:0.25rem;width:5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
</div>
|
|
||||||
</div>`,
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
<style>
|
|
||||||
@keyframes rd-skeleton-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- Note cards container (populated by notes-demo.ts) -->
|
|
||||||
<div id="rd-notes-container" style="display:flex;flex-direction:column;gap:1rem;"></div>
|
|
||||||
|
|
||||||
<!-- Empty state -->
|
|
||||||
<div id="rd-notes-empty" class="rd-card" style="display:none;padding:2rem;text-align:center;">
|
|
||||||
<p class="rd-text-muted rd-text-sm">No notes found. Try resetting the demo.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Packing list section -->
|
|
||||||
<div id="rd-packing-section" style="margin-top:1.5rem;display:none;">
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:1rem;">
|
|
||||||
<h2 style="font-size:0.875rem;font-weight:600;color:#cbd5e1;margin:0;">Packing List</h2>
|
|
||||||
</div>
|
|
||||||
<div id="rd-packing-container"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ── Features showcase ── -->
|
|
||||||
<section class="rd-section" style="margin-top:2rem;">
|
|
||||||
<h2 style="font-size:1.5rem;font-weight:700;color:white;text-align:center;margin-bottom:2rem;">Everything you need to capture knowledge</h2>
|
|
||||||
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:1rem;" class="rd-features-grid">
|
|
||||||
${[
|
|
||||||
{
|
|
||||||
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>`,
|
|
||||||
title: "Live Transcription",
|
|
||||||
desc: "Record and transcribe in real time. Stream audio via WebSocket or transcribe offline with Parakeet.js.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
|
|
||||||
title: "Rich Editing",
|
|
||||||
desc: "Headings, lists, code blocks, highlights, images, and file attachments in every note.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>`,
|
|
||||||
title: "Notebooks",
|
|
||||||
desc: "Organize notes into notebooks with sections. Nest as deep as you need.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>`,
|
|
||||||
title: "Flexible Tags",
|
|
||||||
desc: "Cross-cutting tags let you find notes across all notebooks instantly.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
|
|
||||||
title: "Canvas Sync",
|
|
||||||
desc: "Pin any note to your rSpace canvas for visual collaboration with your team.",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
.map(
|
|
||||||
(f) => `
|
|
||||||
<div class="rd-card" style="padding:1.25rem;">
|
|
||||||
<div style="width:2.5rem;height:2.5rem;background:rgba(245,158,11,0.1);border-radius:0.5rem;display:flex;align-items:center;justify-content:center;margin-bottom:0.75rem;">
|
|
||||||
${f.icon}
|
|
||||||
</div>
|
|
||||||
<h3 style="font-size:0.875rem;font-weight:600;color:white;margin:0 0 0.25rem;">${f.title}</h3>
|
|
||||||
<p style="font-size:0.75rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
|
|
||||||
</div>`,
|
</div>`,
|
||||||
)
|
).join("")}
|
||||||
.join("")}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── CTA ── -->
|
<!-- Ecosystem Integrations -->
|
||||||
|
<section class="rd-section">
|
||||||
|
<h2 style="text-align:center;font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0 0 1.5rem;">
|
||||||
|
r* Ecosystem Integrations
|
||||||
|
</h2>
|
||||||
|
<div class="rd-grid rd-grid--3">
|
||||||
|
${INTEGRATIONS.map(
|
||||||
|
(i) => `
|
||||||
|
<div class="rd-card" style="padding:1.25rem;">
|
||||||
|
<div style="font-size:1.5rem;margin-bottom:0.5rem;">${i.icon}</div>
|
||||||
|
<h3 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0 0 0.375rem;">${i.name}</h3>
|
||||||
|
<p style="font-size:0.8rem;color:#94a3b8;margin:0;line-height:1.4;">${i.desc}</p>
|
||||||
|
</div>`,
|
||||||
|
).join("")}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
<section class="rd-section rd-section--narrow">
|
<section class="rd-section rd-section--narrow">
|
||||||
<div class="rd-cta">
|
<div class="rd-cta">
|
||||||
<h2>Ready to capture everything?</h2>
|
<h2>Ready to capture everything?</h2>
|
||||||
|
|
@ -222,139 +112,5 @@ export function renderDemo(): string {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>`;
|
||||||
|
|
||||||
<style>
|
|
||||||
/* ── Notes-specific layout ── */
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.rd-notes-layout {
|
|
||||||
grid-template-columns: 16rem 1fr !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.rd-features-grid {
|
|
||||||
grid-template-columns: repeat(3, 1fr) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.rd-features-grid {
|
|
||||||
grid-template-columns: repeat(5, 1fr) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Note card styles */
|
|
||||||
.rd-note-card {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.15s;
|
|
||||||
}
|
|
||||||
.rd-note-card:hover {
|
|
||||||
border-color: rgba(100,116,139,0.6);
|
|
||||||
}
|
|
||||||
.rd-note-card--expanded {
|
|
||||||
cursor: default;
|
|
||||||
border-color: rgba(245,158,11,0.3) !important;
|
|
||||||
box-shadow: 0 0 0 1px rgba(245,158,11,0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Synced badge */
|
|
||||||
.rd-synced-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.375rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.25rem 0.625rem;
|
|
||||||
background: rgba(20,184,166,0.1);
|
|
||||||
border: 1px solid rgba(20,184,166,0.2);
|
|
||||||
color: #2dd4bf;
|
|
||||||
border-radius: 9999px;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Markdown rendered content */
|
|
||||||
.rd-md h3 { font-size: 1.125rem; font-weight: 700; color: white; margin: 1rem 0 0.5rem; }
|
|
||||||
.rd-md h4 { font-size: 1rem; font-weight: 600; color: #e2e8f0; margin: 1rem 0 0.5rem; }
|
|
||||||
.rd-md h5 { font-size: 0.875rem; font-weight: 600; color: #cbd5e1; margin: 0.75rem 0 0.25rem; }
|
|
||||||
.rd-md p { font-size: 0.875rem; color: #cbd5e1; margin: 0.25rem 0; line-height: 1.6; }
|
|
||||||
.rd-md strong { color: white; font-weight: 500; }
|
|
||||||
.rd-md em { color: #cbd5e1; font-style: italic; }
|
|
||||||
.rd-md code {
|
|
||||||
color: #fcd34d;
|
|
||||||
background: rgba(30,41,59,1);
|
|
||||||
padding: 0.125rem 0.375rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
.rd-md .rd-md-quote {
|
|
||||||
background: rgba(245,158,11,0.1);
|
|
||||||
border-left: 2px solid #f59e0b;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 0 0.5rem 0.5rem 0;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
.rd-md .rd-md-quote p { color: #fcd34d; }
|
|
||||||
.rd-md ul, .rd-md ol { margin: 0.5rem 0; padding: 0; list-style: none; }
|
|
||||||
.rd-md ul li, .rd-md ol li {
|
|
||||||
display: flex; align-items: flex-start; gap: 0.5rem;
|
|
||||||
font-size: 0.875rem; color: #cbd5e1; padding: 0.125rem 0;
|
|
||||||
}
|
|
||||||
.rd-md ul li::before { content: "\\2022"; color: #f59e0b; margin-top: 0.1rem; flex-shrink: 0; }
|
|
||||||
.rd-md ol li .rd-md-num { color: #f59e0b; font-weight: 500; min-width: 1.2em; text-align: right; flex-shrink: 0; }
|
|
||||||
.rd-md .rd-md-codeblock {
|
|
||||||
background: rgba(2,6,23,1);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
border: 1px solid rgba(51,65,85,0.5);
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
.rd-md .rd-md-codeblock-lang {
|
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: rgba(30,41,59,0.5);
|
|
||||||
border-bottom: 1px solid rgba(51,65,85,0.5);
|
|
||||||
font-size: 0.75rem; color: #94a3b8; font-family: monospace;
|
|
||||||
}
|
|
||||||
.rd-md .rd-md-codeblock pre {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
font-size: 0.75rem; color: #cbd5e1;
|
|
||||||
font-family: monospace;
|
|
||||||
overflow-x: auto;
|
|
||||||
line-height: 1.5;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tag pill */
|
|
||||||
.rd-note-tag {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.125rem 0.5rem;
|
|
||||||
background: rgba(51,65,85,0.5);
|
|
||||||
color: #94a3b8;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
border: 1px solid rgba(51,65,85,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Packing list checkbox */
|
|
||||||
.rd-pack-check {
|
|
||||||
width: 1.25rem; height: 1.25rem; border-radius: 0.25rem; flex-shrink: 0;
|
|
||||||
border: 2px solid #475569;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
cursor: pointer; transition: all 0.15s;
|
|
||||||
}
|
|
||||||
.rd-pack-check--checked {
|
|
||||||
background: #f59e0b; border-color: #f59e0b;
|
|
||||||
}
|
|
||||||
.rd-pack-check:hover { border-color: #64748b; }
|
|
||||||
|
|
||||||
/* Packing item label */
|
|
||||||
.rd-pack-item {
|
|
||||||
display: flex; align-items: center; gap: 0.75rem;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.1s;
|
|
||||||
}
|
|
||||||
.rd-pack-item:hover { background: rgba(51,65,85,0.3); }
|
|
||||||
</style>`;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -371,8 +371,7 @@ routes.get("/", (c) => {
|
||||||
modules: getModuleInfoList(),
|
modules: getModuleInfoList(),
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
body: renderDemo(),
|
body: renderDemo(),
|
||||||
demoScripts: `<script type="module" src="/lib/demo-sync.js"></script>
|
scripts: `<script type="module" src="/modules/rnotes/folk-notes-app.js"></script>`,
|
||||||
<script type="module" src="/modules/rnotes/notes-demo.js"></script>`,
|
|
||||||
styles: `<link rel="stylesheet" href="/modules/rnotes/notes.css">`,
|
styles: `<link rel="stylesheet" href="/modules/rnotes/notes.css">`,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,252 +1,110 @@
|
||||||
/**
|
/**
|
||||||
* rTrips demo page — server-rendered HTML body.
|
* rTrips demo page — server-rendered HTML body.
|
||||||
*
|
*
|
||||||
* "Alpine Explorer 2026" dashboard with 6 cards powered by the rStack:
|
* Embeds the full <folk-trips-planner space="demo"> component for
|
||||||
* Maps (SVG), Notes (packing checklist), Calendar (grid),
|
* real interactivity (trip list, destinations, itinerary, bookings,
|
||||||
* Polls (bars), Funds (expenses), Cart (gear progress).
|
* expenses, packing lists) plus showcase sections explaining the rTrips vision.
|
||||||
*
|
|
||||||
* Client-side trips-demo.ts populates all cards via WebSocket snapshots.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const FEATURES = [
|
||||||
|
{
|
||||||
|
icon: "\u{1F5FA}",
|
||||||
|
title: "Destinations",
|
||||||
|
desc: "Pin destinations on the map with arrival/departure dates, country info, and notes. Reorder your route with drag and drop.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "\u{1F4C5}",
|
||||||
|
title: "Itinerary",
|
||||||
|
desc: "Plan day-by-day activities grouped by date. Categories include hiking, dining, sightseeing, transit, and more.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "\u{1F4B0}",
|
||||||
|
title: "Expense Splitting",
|
||||||
|
desc: "Track group expenses with automatic per-person splits. See who paid what and who owes whom.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "\u{1F392}",
|
||||||
|
title: "Packing Lists",
|
||||||
|
desc: "Collaborative packing checklists organized by category. Check items off as you pack — synced in real time.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const INTEGRATIONS = [
|
||||||
|
{ icon: "\u{1F5FA}", name: "rMaps", desc: "Destinations and routes appear on the interactive map with pins and driving directions." },
|
||||||
|
{ icon: "\u{1F4C5}", name: "rCal", desc: "Trip dates, activities, and bookings sync to the community calendar." },
|
||||||
|
{ icon: "\u{1F30A}", name: "rFunds", desc: "Group expenses feed into shared budget flows with threshold-based splits." },
|
||||||
|
{ icon: "\u{1F5F3}", name: "rVote", desc: "Vote on daily activities, restaurants, and route decisions as a group." },
|
||||||
|
{ icon: "\u{1F4DD}", name: "rNotes", desc: "Attach travel journals, packing tips, and logistics notes to the trip." },
|
||||||
|
{ icon: "\u2615\uFE0F", name: "rSpace", desc: "Each trip lives on its own canvas with maps, notes, polls, and expenses connected." },
|
||||||
|
];
|
||||||
|
|
||||||
export function renderDemo(): string {
|
export function renderDemo(): string {
|
||||||
return `
|
return `
|
||||||
<div class="rd-root" style="--rd-accent-from:#14b8a6; --rd-accent-to:#06b6d4">
|
<div class="rd-root" style="--rd-accent-from:#14b8a6; --rd-accent-to:#06b6d4;">
|
||||||
|
|
||||||
<!-- ── Trip Header ── -->
|
<!-- Hero -->
|
||||||
<section class="rd-hero">
|
<section class="rd-hero">
|
||||||
<div style="display:inline-flex;align-items:center;gap:0.5rem;padding:0.375rem 1rem;background:rgba(20,184,166,0.1);border:1px solid rgba(20,184,166,0.2);border-radius:9999px;font-size:0.875rem;color:#5eead4;font-weight:500;margin-bottom:1.5rem;">
|
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(20,184,166,0.1);border:1px solid rgba(20,184,166,0.2);border-radius:9999px;font-size:0.875rem;color:#5eead4;font-weight:500;margin-bottom:1.5rem;">
|
||||||
<span id="rd-hero-dot" style="width:0.5rem;height:0.5rem;border-radius:9999px;background:#14b8a6;animation:rd-pulse 2s ease-in-out infinite;"></span>
|
Collaborative Trip Planner
|
||||||
<span id="rd-hero-label">Interactive Demo</span>
|
|
||||||
</div>
|
</div>
|
||||||
<h1 id="rd-trip-title" style="background:linear-gradient(135deg,#5eead4,#67e8f9,#93c5fd);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;">Alpine Explorer 2026</h1>
|
<h1>rTrips Demo</h1>
|
||||||
<p class="rd-subtitle" id="rd-trip-route">Chamonix → Zermatt → Dolomites</p>
|
<p class="rd-subtitle">Plan trips together with destinations, itinerary, bookings, expenses, and packing lists</p>
|
||||||
<div class="rd-meta" id="rd-trip-meta">
|
<div class="rd-meta">
|
||||||
<span>📅 Jul 6–20, 2026</span>
|
<span>\u{1F5FA} Destinations</span>
|
||||||
<span>💶 ~€4,000 budget</span>
|
<span style="color:#475569">|</span>
|
||||||
<span>🏔️ 3 countries</span>
|
<span>\u{1F4C5} Itinerary</span>
|
||||||
</div>
|
<span style="color:#475569">|</span>
|
||||||
<div class="rd-avatars" id="rd-avatars">
|
<span>\u{1F4B0} Expenses</span>
|
||||||
<div class="rd-avatar" style="background:#64748b" title="Loading">…</div>
|
<span style="color:#475569">|</span>
|
||||||
|
<span>\u{1F392} Packing</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Context bar ── -->
|
<!-- Interactive Trips Planner -->
|
||||||
<section class="rd-section rd-section--narrow">
|
<section class="rd-section rd-section--narrow">
|
||||||
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:1rem;margin-bottom:1.5rem;">
|
<div class="rd-card" style="padding:0;overflow:hidden;">
|
||||||
<p style="text-align:center;font-size:0.875rem;color:#94a3b8;max-width:40rem;margin:0;">
|
<folk-trips-planner space="demo"></folk-trips-planner>
|
||||||
Every trip is powered by the <span style="color:#e2e8f0;font-weight:500">rStack</span> —
|
|
||||||
a suite of collaborative tools that handle routes, notes, schedules, voting, expenses,
|
|
||||||
and shared purchases. Each card below shows live data with a link to the full tool.
|
|
||||||
</p>
|
|
||||||
<button id="rd-reset-btn" class="rd-btn rd-btn--ghost" disabled>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
|
|
||||||
Reset Demo
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── 6-Card Dashboard Grid ── -->
|
<!-- Core Concepts -->
|
||||||
<section class="rd-section">
|
<section class="rd-section">
|
||||||
<div class="rd-trips-grid">
|
<div class="rd-grid rd-grid--2">
|
||||||
|
${FEATURES.map(
|
||||||
<!-- 1. Route Map (span 2) -->
|
(f) => `
|
||||||
<div class="rd-card rd-trips-card rd-trips-card--span2" id="rd-card-maps">
|
<div class="rd-card" style="padding:1.5rem;">
|
||||||
<div class="rd-trips-card-header">
|
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
|
||||||
<span style="font-size:1.125rem;">🗺️</span>
|
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
|
||||||
<span style="font-weight:600;font-size:0.875rem;">Route Map</span>
|
</div>`,
|
||||||
<span class="rd-trips-live" id="rd-maps-live" style="display:none;">
|
).join("")}
|
||||||
<span style="width:6px;height:6px;border-radius:50%;background:#34d399;animation:rd-pulse 2s ease-in-out infinite;"></span>
|
|
||||||
live
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<a href="https://rmaps.online" target="_blank" rel="noopener noreferrer" class="rd-trips-open-link">Open in rMaps ↗</a>
|
|
||||||
</div>
|
|
||||||
<div class="rd-trips-card-body">
|
|
||||||
<div style="position:relative;width:100%;border-radius:0.75rem;background:rgba(15,23,42,0.6);overflow:hidden;">
|
|
||||||
<svg id="rd-map-svg" viewBox="0 0 800 300" style="width:100%;height:auto;display:block;" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<!-- Mountain silhouettes -->
|
|
||||||
<path d="M0 280 L60 200 L100 240 L160 160 L200 210 L260 130 L320 180 L380 100 L420 150 L480 80 L540 140 L600 110 L660 160 L720 120 L800 180 L800 300 L0 300 Z" fill="rgba(30,41,59,0.8)"/>
|
|
||||||
<path d="M0 280 L80 220 L140 250 L200 190 L280 230 L340 170 L400 200 L460 150 L520 190 L580 160 L640 200 L700 170 L800 220 L800 300 L0 300 Z" fill="rgba(51,65,85,0.6)"/>
|
|
||||||
<path d="M370 100 L380 100 L390 108 L375 105 Z" fill="rgba(255,255,255,0.4)"/>
|
|
||||||
<path d="M470 80 L480 80 L492 90 L476 86 Z" fill="rgba(255,255,255,0.5)"/>
|
|
||||||
<path d="M590 110 L600 110 L612 120 L598 116 Z" fill="rgba(255,255,255,0.4)"/>
|
|
||||||
<!-- Route line (default) -->
|
|
||||||
<path id="rd-route-path" d="M160 180 C250 160, 350 200, 430 150 C510 100, 560 160, 650 140" fill="none" stroke="rgba(94,234,212,0.7)" stroke-width="3" stroke-dasharray="10 6"/>
|
|
||||||
<!-- Default destination pins -->
|
|
||||||
<g id="rd-map-pins">
|
|
||||||
<g><circle cx="160" cy="180" r="8" fill="#14b8a6" stroke="#0d9488" stroke-width="2"/><text x="160" y="210" text-anchor="middle" fill="#94a3b8" font-size="12" font-weight="600">Chamonix</text><text x="160" y="224" text-anchor="middle" fill="#64748b" font-size="10">Jul 6–10</text></g>
|
|
||||||
<g><circle cx="430" cy="150" r="8" fill="#06b6d4" stroke="#0891b2" stroke-width="2"/><text x="430" y="180" text-anchor="middle" fill="#94a3b8" font-size="12" font-weight="600">Zermatt</text><text x="430" y="194" text-anchor="middle" fill="#64748b" font-size="10">Jul 10–14</text></g>
|
|
||||||
<g><circle cx="650" cy="140" r="8" fill="#8b5cf6" stroke="#7c3aed" stroke-width="2"/><text x="650" y="170" text-anchor="middle" fill="#94a3b8" font-size="12" font-weight="600">Dolomites</text><text x="650" y="184" text-anchor="middle" fill="#64748b" font-size="10">Jul 14–20</text></g>
|
|
||||||
</g>
|
|
||||||
<!-- Activity icons -->
|
|
||||||
<text x="280" y="168" font-size="16">🥾</text>
|
|
||||||
<text x="350" y="188" font-size="16">🧗</text>
|
|
||||||
<text x="500" y="128" font-size="16">🚵</text>
|
|
||||||
<text x="560" y="148" font-size="16">🪂</text>
|
|
||||||
<text x="620" y="158" font-size="16">🛶</text>
|
|
||||||
</svg>
|
|
||||||
<div style="position:absolute;bottom:0.75rem;left:0.75rem;display:flex;gap:0.75rem;font-size:0.75rem;color:#94a3b8;">
|
|
||||||
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#14b8a6;"></span> France</span>
|
|
||||||
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#06b6d4;"></span> Switzerland</span>
|
|
||||||
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#8b5cf6;"></span> Italy</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 2. Trip Notes (packing checklist) -->
|
|
||||||
<div class="rd-card rd-trips-card" id="rd-card-notes">
|
|
||||||
<div class="rd-trips-card-header">
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
|
||||||
<span style="font-size:1.125rem;">📝</span>
|
|
||||||
<span style="font-weight:600;font-size:0.875rem;">Trip Notes</span>
|
|
||||||
<span class="rd-trips-live" id="rd-notes-live" style="display:none;">
|
|
||||||
<span style="width:6px;height:6px;border-radius:50%;background:#34d399;animation:rd-pulse 2s ease-in-out infinite;"></span>
|
|
||||||
live
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<a href="https://rnotes.online" target="_blank" rel="noopener noreferrer" class="rd-trips-open-link">Open in rNotes ↗</a>
|
|
||||||
</div>
|
|
||||||
<div class="rd-trips-card-body" id="rd-notes-body">
|
|
||||||
<div>
|
|
||||||
<h4 class="rd-trips-sub-heading">Packing Checklist</h4>
|
|
||||||
<div id="rd-packing-list" class="rd-trips-skeleton">
|
|
||||||
<div style="height:0.875rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:75%;margin-bottom:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="height:0.875rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:50%;margin-bottom:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="height:0.875rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:66%;margin-bottom:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="height:0.875rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:33%;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:1rem;">
|
|
||||||
<h4 class="rd-trips-sub-heading">Trip Rules</h4>
|
|
||||||
<ol style="margin:0;padding:0 0 0 1.25rem;font-size:0.8125rem;color:#cbd5e1;line-height:1.75;">
|
|
||||||
<li>Majority vote on daily activities</li>
|
|
||||||
<li>Shared expenses split equally</li>
|
|
||||||
<li>Quiet hours after 10pm in huts</li>
|
|
||||||
<li>Everyone carries their own pack</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 3. Group Calendar (span 2) -->
|
|
||||||
<div class="rd-card rd-trips-card rd-trips-card--span2" id="rd-card-cal">
|
|
||||||
<div class="rd-trips-card-header">
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
|
||||||
<span style="font-size:1.125rem;">📅</span>
|
|
||||||
<span style="font-weight:600;font-size:0.875rem;">Group Calendar</span>
|
|
||||||
<span class="rd-trips-live" id="rd-cal-live" style="display:none;">
|
|
||||||
<span style="width:6px;height:6px;border-radius:50%;background:#34d399;animation:rd-pulse 2s ease-in-out infinite;"></span>
|
|
||||||
live
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<a href="https://rcal.online" target="_blank" rel="noopener noreferrer" class="rd-trips-open-link">Open in rCal ↗</a>
|
|
||||||
</div>
|
|
||||||
<div class="rd-trips-card-body">
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
|
|
||||||
<h4 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0;">July 2026</h4>
|
|
||||||
<span id="rd-cal-days" style="font-size:0.75rem;color:#94a3b8;">15 days</span>
|
|
||||||
</div>
|
|
||||||
<div id="rd-cal-grid"></div>
|
|
||||||
<div style="display:flex;flex-wrap:wrap;gap:0.75rem;margin-top:0.75rem;font-size:0.75rem;color:#94a3b8;">
|
|
||||||
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#14b8a6;"></span> Travel</span>
|
|
||||||
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#10b981;"></span> Hiking</span>
|
|
||||||
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#f59e0b;"></span> Adventure</span>
|
|
||||||
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#8b5cf6;"></span> Culture</span>
|
|
||||||
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#64748b;"></span> Rest</span>
|
|
||||||
<span style="display:flex;align-items:center;gap:0.25rem;"><span style="width:8px;height:8px;border-radius:50%;background:#06b6d4;"></span> Transit</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 4. Group Polls -->
|
|
||||||
<div class="rd-card rd-trips-card" id="rd-card-polls">
|
|
||||||
<div class="rd-trips-card-header">
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
|
||||||
<span style="font-size:1.125rem;">🗳️</span>
|
|
||||||
<span style="font-weight:600;font-size:0.875rem;">Group Polls</span>
|
|
||||||
<span class="rd-trips-live" id="rd-polls-live" style="display:none;">
|
|
||||||
<span style="width:6px;height:6px;border-radius:50%;background:#34d399;animation:rd-pulse 2s ease-in-out infinite;"></span>
|
|
||||||
live
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<a href="https://rvote.online" target="_blank" rel="noopener noreferrer" class="rd-trips-open-link">Open in rVote ↗</a>
|
|
||||||
</div>
|
|
||||||
<div class="rd-trips-card-body" id="rd-polls-body">
|
|
||||||
<div class="rd-trips-skeleton">
|
|
||||||
<div style="height:0.875rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:75%;margin-bottom:0.75rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="height:0.5rem;background:rgba(51,65,85,0.3);border-radius:0.25rem;width:100%;margin-bottom:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="height:0.5rem;background:rgba(51,65,85,0.3);border-radius:0.25rem;width:66%;margin-bottom:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="height:0.5rem;background:rgba(51,65,85,0.3);border-radius:0.25rem;width:33%;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 5. Group Expenses -->
|
|
||||||
<div class="rd-card rd-trips-card" id="rd-card-funds">
|
|
||||||
<div class="rd-trips-card-header">
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
|
||||||
<span style="font-size:1.125rem;">💰</span>
|
|
||||||
<span style="font-weight:600;font-size:0.875rem;">Group Expenses</span>
|
|
||||||
<span class="rd-trips-live" id="rd-funds-live" style="display:none;">
|
|
||||||
<span style="width:6px;height:6px;border-radius:50%;background:#34d399;animation:rd-pulse 2s ease-in-out infinite;"></span>
|
|
||||||
live
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<a href="https://rfunds.online" target="_blank" rel="noopener noreferrer" class="rd-trips-open-link">Open in rFunds ↗</a>
|
|
||||||
</div>
|
|
||||||
<div class="rd-trips-card-body" id="rd-funds-body">
|
|
||||||
<div style="text-align:center;padding:0.5rem 0;">
|
|
||||||
<p id="rd-funds-total" style="font-size:1.5rem;font-weight:700;color:white;margin:0;">...</p>
|
|
||||||
<p style="font-size:0.75rem;color:#94a3b8;margin:0.25rem 0 0;">Total group spending</p>
|
|
||||||
</div>
|
|
||||||
<div class="rd-trips-skeleton" id="rd-funds-skeleton">
|
|
||||||
<div style="height:0.875rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:75%;margin-bottom:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="height:0.875rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:50%;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
</div>
|
|
||||||
<div id="rd-funds-expenses" style="display:none;"></div>
|
|
||||||
<div id="rd-funds-balances" style="display:none;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 6. Shared Gear (span 2) -->
|
|
||||||
<div class="rd-card rd-trips-card rd-trips-card--span2" id="rd-card-cart">
|
|
||||||
<div class="rd-trips-card-header">
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
|
||||||
<span style="font-size:1.125rem;">🛒</span>
|
|
||||||
<span style="font-weight:600;font-size:0.875rem;">Shared Gear</span>
|
|
||||||
<span class="rd-trips-live" id="rd-cart-live" style="display:none;">
|
|
||||||
<span style="width:6px;height:6px;border-radius:50%;background:#34d399;animation:rd-pulse 2s ease-in-out infinite;"></span>
|
|
||||||
live
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<a href="https://rcart.online" target="_blank" rel="noopener noreferrer" class="rd-trips-open-link">Open in rCart ↗</a>
|
|
||||||
</div>
|
|
||||||
<div class="rd-trips-card-body" id="rd-cart-body">
|
|
||||||
<div class="rd-trips-skeleton" id="rd-cart-skeleton">
|
|
||||||
<div style="height:0.5rem;background:rgba(51,65,85,0.5);border-radius:0.25rem;width:100%;margin-bottom:0.75rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem;">
|
|
||||||
<div style="height:3rem;background:rgba(51,65,85,0.3);border-radius:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="height:3rem;background:rgba(51,65,85,0.3);border-radius:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="height:3rem;background:rgba(51,65,85,0.3);border-radius:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
<div style="height:3rem;background:rgba(51,65,85,0.3);border-radius:0.5rem;animation:rd-skeleton-pulse 1.5s ease-in-out infinite;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="rd-cart-content" style="display:none;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── CTA ── -->
|
<!-- Ecosystem Integrations -->
|
||||||
|
<section class="rd-section">
|
||||||
|
<h2 style="text-align:center;font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0 0 1.5rem;">
|
||||||
|
r* Ecosystem Integrations
|
||||||
|
</h2>
|
||||||
|
<div class="rd-grid rd-grid--3">
|
||||||
|
${INTEGRATIONS.map(
|
||||||
|
(i) => `
|
||||||
|
<div class="rd-card" style="padding:1.25rem;">
|
||||||
|
<div style="font-size:1.5rem;margin-bottom:0.5rem;">${i.icon}</div>
|
||||||
|
<h3 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0 0 0.375rem;">${i.name}</h3>
|
||||||
|
<p style="font-size:0.8rem;color:#94a3b8;margin:0;line-height:1.4;">${i.desc}</p>
|
||||||
|
</div>`,
|
||||||
|
).join("")}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
<section class="rd-section rd-section--narrow">
|
<section class="rd-section rd-section--narrow">
|
||||||
<div class="rd-cta">
|
<div class="rd-cta">
|
||||||
<h2>Plan Your Own Group Adventure</h2>
|
<h2>Plan Your Next Adventure</h2>
|
||||||
<p>
|
<p>
|
||||||
The rStack gives your group everything you need — routes, schedules, polls,
|
rTrips gives your group everything you need — routes, schedules, polls,
|
||||||
shared expenses, and gear lists — all connected in one trip canvas.
|
shared expenses, and packing lists — all connected in one trip canvas.
|
||||||
</p>
|
</p>
|
||||||
<a href="/create-space" style="background:linear-gradient(135deg,#14b8a6,#06b6d4);box-shadow:0 8px 24px rgba(20,184,166,0.25);">
|
<a href="/create-space" style="background:linear-gradient(135deg,#14b8a6,#06b6d4);box-shadow:0 8px 24px rgba(20,184,166,0.25);">
|
||||||
Start Planning
|
Start Planning
|
||||||
|
|
@ -254,161 +112,5 @@ export function renderDemo(): string {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>`;
|
||||||
|
|
||||||
<style>
|
|
||||||
@keyframes rd-skeleton-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
||||||
|
|
||||||
.rd-trips-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.rd-trips-grid {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
.rd-trips-card--span2 {
|
|
||||||
grid-column: span 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rd-trips-card {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.rd-trips-card-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.75rem 1.25rem;
|
|
||||||
border-bottom: 1px solid rgba(51,65,85,0.5);
|
|
||||||
}
|
|
||||||
.rd-trips-card-body {
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
.rd-trips-open-link {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
background: rgba(51,65,85,0.6);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
color: #cbd5e1;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: all 0.15s;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.rd-trips-open-link:hover {
|
|
||||||
background: rgba(71,85,105,0.6);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.rd-trips-live {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #34d399;
|
|
||||||
}
|
|
||||||
.rd-trips-sub-heading {
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #94a3b8;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
}
|
|
||||||
.rd-trips-skeleton {
|
|
||||||
animation: rd-skeleton-pulse 1.5s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Packing item */
|
|
||||||
.rd-trips-pack-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
padding: 0.25rem 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.rd-trips-pack-item:hover { opacity: 0.85; }
|
|
||||||
.rd-trips-pack-check {
|
|
||||||
width: 1rem; height: 1rem; border-radius: 0.25rem; flex-shrink: 0;
|
|
||||||
border: 2px solid #475569;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
.rd-trips-pack-check--checked {
|
|
||||||
background: #14b8a6; border-color: #14b8a6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Calendar grid */
|
|
||||||
.rd-cal-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(7, 1fr);
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
.rd-cal-day-name {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
color: #64748b;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 0.25rem 0;
|
|
||||||
}
|
|
||||||
.rd-cal-cell {
|
|
||||||
min-height: 3.5rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 0.25rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
.rd-cal-cell--trip {
|
|
||||||
background: rgba(51,65,85,0.4);
|
|
||||||
border: 1px solid rgba(71,85,105,0.4);
|
|
||||||
}
|
|
||||||
.rd-cal-cell--empty {
|
|
||||||
background: rgba(30,41,59,0.3);
|
|
||||||
}
|
|
||||||
.rd-cal-cell-num { display: block; margin-bottom: 0.125rem; }
|
|
||||||
.rd-cal-cell-num--trip { color: #e2e8f0; font-weight: 500; }
|
|
||||||
.rd-cal-cell-num--off { color: #475569; }
|
|
||||||
.rd-cal-event {
|
|
||||||
display: block;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
padding: 0 0.25rem;
|
|
||||||
color: white;
|
|
||||||
font-size: 9px;
|
|
||||||
margin-top: 0.125rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Poll bar */
|
|
||||||
.rd-trips-poll-bar-bg {
|
|
||||||
height: 0.5rem;
|
|
||||||
background: rgba(51,65,85,0.7);
|
|
||||||
border-radius: 9999px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.rd-trips-poll-bar {
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 9999px;
|
|
||||||
transition: width 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cart item */
|
|
||||||
.rd-trips-cart-item {
|
|
||||||
background: rgba(51,65,85,0.3);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
.rd-trips-cart-bar-bg {
|
|
||||||
height: 0.375rem;
|
|
||||||
background: rgba(51,65,85,0.6);
|
|
||||||
border-radius: 9999px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.rd-trips-cart-bar {
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 9999px;
|
|
||||||
transition: width 0.3s;
|
|
||||||
}
|
|
||||||
</style>`;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,7 @@ routes.get("/", (c) => {
|
||||||
modules: getModuleInfoList(),
|
modules: getModuleInfoList(),
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
body: renderDemo(),
|
body: renderDemo(),
|
||||||
scripts: `<script type="module" src="/modules/rtrips/trips-demo.js"></script>`,
|
scripts: `<script type="module" src="/modules/rtrips/folk-trips-planner.js"></script>`,
|
||||||
styles: `<link rel="stylesheet" href="/modules/rtrips/trips.css">`,
|
styles: `<link rel="stylesheet" href="/modules/rtrips/trips.css">`,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,301 +1,116 @@
|
||||||
/**
|
/**
|
||||||
* rTube demo page — server-rendered HTML body.
|
* rTube demo page — server-rendered HTML body.
|
||||||
*
|
*
|
||||||
* Video library with sidebar, search, player area, download/copy-link buttons.
|
* Embeds the full <folk-video-player space="demo"> component for
|
||||||
* Client-side tube-demo.ts handles selection, filtering, and playback.
|
* real interactivity (video library, search, playback, live streaming)
|
||||||
|
* plus showcase sections explaining the rTube vision.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ─── Seed Data ─────────────────────────────────────────────── */
|
const FEATURES = [
|
||||||
|
{
|
||||||
interface DemoVideo {
|
icon: "\u{1F3AC}",
|
||||||
name: string;
|
title: "Video Library",
|
||||||
size: number;
|
desc: "Browse, search, and play videos from your community's R2-backed storage. Supports MP4, WebM, MOV, and more.",
|
||||||
}
|
},
|
||||||
|
{
|
||||||
const DEMO_VIDEOS: DemoVideo[] = [
|
icon: "\u{1F4E1}",
|
||||||
{ name: "lac-blanc-test-footage.mp4", size: 245_000_000 },
|
title: "Live Streaming",
|
||||||
{ name: "chamonix-arrival-timelapse.mp4", size: 128_000_000 },
|
desc: "Broadcast live via RTMP from OBS Studio or any streaming software. Viewers watch in real-time with HLS playback.",
|
||||||
{ name: "matterhorn-sunset-4k.mp4", size: 512_000_000 },
|
},
|
||||||
{ name: "paragliding-zermatt.webm", size: 89_000_000 },
|
{
|
||||||
{ name: "glacier-paradise-walk.mp4", size: 340_000_000 },
|
icon: "\u{1F4E4}",
|
||||||
{ name: "tre-cime-circuit-gopro.mp4", size: 420_000_000 },
|
title: "Easy Uploads",
|
||||||
{ name: "dolomites-drone-footage.mkv", size: 780_000_000 },
|
desc: "Authenticated members upload videos directly. Files stream to Cloudflare R2 with automatic format detection.",
|
||||||
{ name: "group-dinner-recap.mp4", size: 67_000_000 },
|
},
|
||||||
|
{
|
||||||
|
icon: "\u{1F517}",
|
||||||
|
title: "Direct Links",
|
||||||
|
desc: "Copy shareable links to any video. HTTP range requests enable efficient streaming and seeking.",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/* ─── Helpers ──────────────────────────────────────────────── */
|
const INTEGRATIONS = [
|
||||||
|
{ icon: "\u{1F4DD}", name: "rNotes", desc: "Attach meeting notes, transcripts, and timestamps to video recordings." },
|
||||||
function formatSize(bytes: number): string {
|
{ icon: "\u{1F4C5}", name: "rCal", desc: "Scheduled recordings and live streams appear on the community calendar." },
|
||||||
if (!bytes) return "";
|
{ icon: "\u{1F465}", name: "rNetwork", desc: "Share videos across your network. Collaborative viewing and commenting." },
|
||||||
const units = ["B", "KB", "MB", "GB"];
|
{ icon: "\u{1F4DA}", name: "rBooks", desc: "Embed video content in publications and educational materials." },
|
||||||
let i = 0;
|
{ icon: "\u{1F5FA}", name: "rMaps", desc: "Geotagged videos appear on the map at their recording location." },
|
||||||
let b = bytes;
|
{ icon: "\u2615\uFE0F", name: "rSpace", desc: "Each space has its own video library. Pin videos to the collaborative canvas." },
|
||||||
while (b >= 1024 && i < units.length - 1) {
|
];
|
||||||
b /= 1024;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
return `${b.toFixed(1)} ${units[i]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIcon(filename: string): string {
|
|
||||||
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
||||||
if (["mp4", "webm", "mov"].includes(ext)) return "\u{1F3AC}";
|
|
||||||
if (["mkv", "avi"].includes(ext)) return "\u26A0\uFE0F";
|
|
||||||
return "\u{1F4C4}";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlayable(filename: string): boolean {
|
|
||||||
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
||||||
return ["mp4", "webm", "mov", "ogg", "m4v"].includes(ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Render ─────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
export function renderDemo(): string {
|
export function renderDemo(): string {
|
||||||
const videoListHTML = DEMO_VIDEOS.map(
|
|
||||||
(v) => `
|
|
||||||
<li class="rd-tube-item${!isPlayable(v.name) ? " rd-tube-item--unplayable" : ""}"
|
|
||||||
data-video="${v.name}" role="button" tabindex="0">
|
|
||||||
<span class="rd-tube-item__icon">${getIcon(v.name)}</span>
|
|
||||||
<span class="rd-tube-item__name" title="${v.name}">${v.name}</span>
|
|
||||||
<span class="rd-tube-item__size">${formatSize(v.size)}</span>
|
|
||||||
</li>`,
|
|
||||||
).join("\n");
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="rd-root" style="--rd-accent-from:#ef4444; --rd-accent-to:#ec4899">
|
<div class="rd-root" style="--rd-accent-from:#ef4444; --rd-accent-to:#ec4899;">
|
||||||
|
|
||||||
<!-- ── Hero ── -->
|
<!-- Hero -->
|
||||||
<section class="rd-hero">
|
<section class="rd-hero">
|
||||||
<h1>Video Library</h1>
|
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.2);border-radius:9999px;font-size:0.875rem;color:#fca5a5;font-weight:500;margin-bottom:1.5rem;">
|
||||||
<p class="rd-subtitle">Browse, preview, and download videos from the Alpine Explorer expedition</p>
|
Community Video Hosting
|
||||||
|
</div>
|
||||||
|
<h1>rTube Demo</h1>
|
||||||
|
<p class="rd-subtitle">Video library, live streaming, and uploads powered by Cloudflare R2</p>
|
||||||
<div class="rd-meta">
|
<div class="rd-meta">
|
||||||
<span>\u{1F3AC} ${DEMO_VIDEOS.length} videos</span>
|
<span>\u{1F3AC} Video Library</span>
|
||||||
<span style="color:#475569">|</span>
|
<span style="color:#475569">|</span>
|
||||||
<span>\u{1F4BE} ${formatSize(DEMO_VIDEOS.reduce((sum, v) => sum + v.size, 0))} total</span>
|
<span>\u{1F4E1} Live Streaming</span>
|
||||||
<span style="color:#475569">|</span>
|
<span style="color:#475569">|</span>
|
||||||
<span>\u{1F3D4} Alpine Explorer 2026</span>
|
<span>\u{1F4E4} Uploads</span>
|
||||||
|
<span style="color:#475569">|</span>
|
||||||
|
<span>\u{1F517} Direct Links</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Two-column layout ── -->
|
<!-- Interactive Video Player -->
|
||||||
<div class="rd-section">
|
<section class="rd-section rd-section--narrow">
|
||||||
<div class="rd-tube-layout">
|
<div class="rd-card" style="padding:0;overflow:hidden;">
|
||||||
|
<folk-video-player space="demo"></folk-video-player>
|
||||||
<!-- Sidebar -->
|
|
||||||
<aside class="rd-tube-sidebar" id="rd-video-sidebar">
|
|
||||||
<input type="text" id="rd-video-search" class="rd-tube-search"
|
|
||||||
placeholder="Search videos..." autocomplete="off" />
|
|
||||||
<h2 class="rd-tube-sidebar__heading">Library</h2>
|
|
||||||
<ul class="rd-tube-list" id="rd-video-list">
|
|
||||||
${videoListHTML}
|
|
||||||
</ul>
|
|
||||||
<p class="rd-tube-empty" id="rd-video-empty" style="display:none">No videos found</p>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- Player area -->
|
|
||||||
<div class="rd-tube-player-wrap" id="rd-video-player-area">
|
|
||||||
<div class="rd-tube-player" id="rd-player-container">
|
|
||||||
<p class="rd-tube-placeholder" id="rd-player-placeholder">Select a video to play</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Info bar (hidden until a video is selected) -->
|
|
||||||
<div class="rd-tube-info" id="rd-video-info" style="display:none">
|
|
||||||
<p class="rd-tube-info__name" id="rd-video-name"></p>
|
|
||||||
<div class="rd-tube-info__actions">
|
|
||||||
<a id="rd-download-btn" href="#" download class="rd-btn rd-btn--ghost rd-btn--sm">
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
||||||
Download
|
|
||||||
</a>
|
|
||||||
<button id="rd-copy-link-btn" class="rd-btn rd-btn--ghost rd-btn--sm">
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
|
||||||
Copy Link
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- ── CTA ── -->
|
<!-- Core Concepts -->
|
||||||
|
<section class="rd-section">
|
||||||
|
<div class="rd-grid rd-grid--2">
|
||||||
|
${FEATURES.map(
|
||||||
|
(f) => `
|
||||||
|
<div class="rd-card" style="padding:1.5rem;">
|
||||||
|
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
|
||||||
|
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
|
||||||
|
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
|
||||||
|
</div>`,
|
||||||
|
).join("")}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Ecosystem Integrations -->
|
||||||
|
<section class="rd-section">
|
||||||
|
<h2 style="text-align:center;font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0 0 1.5rem;">
|
||||||
|
r* Ecosystem Integrations
|
||||||
|
</h2>
|
||||||
|
<div class="rd-grid rd-grid--3">
|
||||||
|
${INTEGRATIONS.map(
|
||||||
|
(i) => `
|
||||||
|
<div class="rd-card" style="padding:1.25rem;">
|
||||||
|
<div style="font-size:1.5rem;margin-bottom:0.5rem;">${i.icon}</div>
|
||||||
|
<h3 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0 0 0.375rem;">${i.name}</h3>
|
||||||
|
<p style="font-size:0.8rem;color:#94a3b8;margin:0;line-height:1.4;">${i.desc}</p>
|
||||||
|
</div>`,
|
||||||
|
).join("")}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
<section class="rd-section rd-section--narrow">
|
<section class="rd-section rd-section--narrow">
|
||||||
<div class="rd-cta">
|
<div class="rd-cta">
|
||||||
<h2>Host Your Video Library</h2>
|
<h2>Host Your Video Library</h2>
|
||||||
<p>
|
<p>
|
||||||
rTube gives your community a private video hosting solution with streaming,
|
rTube gives your community private video hosting with streaming,
|
||||||
uploads, and live broadcasting — all powered by Cloudflare R2.
|
uploads, and live broadcasting — all powered by Cloudflare R2.
|
||||||
</p>
|
</p>
|
||||||
<a href="/create-space" style="background:linear-gradient(135deg,#ef4444,#ec4899); box-shadow:0 8px 24px rgba(239,68,68,0.25);">
|
<a href="/create-space" style="background:linear-gradient(135deg,#ef4444,#ec4899);box-shadow:0 8px 24px rgba(239,68,68,0.25);">
|
||||||
Create Your Space
|
Create Your Space
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>`;
|
||||||
|
|
||||||
<style>
|
|
||||||
/* ── Tube demo layout ── */
|
|
||||||
.rd-tube-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 300px 1fr;
|
|
||||||
gap: 1.5rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.rd-tube-layout {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Sidebar ── */
|
|
||||||
.rd-tube-sidebar {
|
|
||||||
background: rgba(30, 41, 59, 0.5);
|
|
||||||
border: 1px solid rgba(51, 65, 85, 0.5);
|
|
||||||
border-radius: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
max-height: 80vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.rd-tube-search {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.625rem 0.875rem;
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
border: 1px solid #334155;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
color: #f1f5f9;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.15s;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.rd-tube-search::placeholder { color: #64748b; }
|
|
||||||
.rd-tube-search:focus { border-color: #ef4444; }
|
|
||||||
.rd-tube-sidebar__heading {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
color: #64748b;
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Video list ── */
|
|
||||||
.rd-tube-list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.125rem;
|
|
||||||
}
|
|
||||||
.rd-tube-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.625rem;
|
|
||||||
padding: 0.5rem 0.625rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
border-left: 2px solid transparent;
|
|
||||||
}
|
|
||||||
.rd-tube-item:hover { background: rgba(51, 65, 85, 0.5); }
|
|
||||||
.rd-tube-item--active {
|
|
||||||
background: rgba(239, 68, 68, 0.15);
|
|
||||||
border-left-color: #ef4444;
|
|
||||||
}
|
|
||||||
.rd-tube-item--unplayable { opacity: 0.6; }
|
|
||||||
.rd-tube-item__icon { flex-shrink: 0; font-size: 1rem; }
|
|
||||||
.rd-tube-item__name {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
.rd-tube-item__size {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #475569;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.rd-tube-empty {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Player area ── */
|
|
||||||
.rd-tube-player-wrap { min-width: 0; }
|
|
||||||
.rd-tube-player {
|
|
||||||
background: #000;
|
|
||||||
border-radius: 1rem;
|
|
||||||
overflow: hidden;
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.rd-tube-player video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
.rd-tube-placeholder {
|
|
||||||
color: #475569;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.rd-tube-warning {
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem;
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
.rd-tube-warning__icon { font-size: 2.5rem; margin-bottom: 0.75rem; }
|
|
||||||
.rd-tube-warning__ext { font-weight: 700; }
|
|
||||||
.rd-tube-warning__hint {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #64748b;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Info bar ── */
|
|
||||||
.rd-tube-info {
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
background: rgba(30, 41, 59, 0.5);
|
|
||||||
border: 1px solid rgba(51, 65, 85, 0.5);
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
padding: 0.875rem 1rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
.rd-tube-info__name {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #f1f5f9;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.rd-tube-info__actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Button size variant ── */
|
|
||||||
.rd-btn--sm {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
gap: 0.375rem;
|
|
||||||
}
|
|
||||||
</style>`;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ routes.get("/", (c) => {
|
||||||
modules: getModuleInfoList(),
|
modules: getModuleInfoList(),
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
body: renderDemo(),
|
body: renderDemo(),
|
||||||
scripts: `<script type="module" src="/modules/rtube/tube-demo.js"></script>`,
|
scripts: `<script type="module" src="/modules/rtube/folk-video-player.js"></script>`,
|
||||||
styles: `<link rel="stylesheet" href="/modules/rtube/tube.css">`,
|
styles: `<link rel="stylesheet" href="/modules/rtube/tube.css">`,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,116 @@
|
||||||
/**
|
/**
|
||||||
* rVote demo page — server-rendered HTML body.
|
* rVote demo page — server-rendered HTML body.
|
||||||
*
|
*
|
||||||
* Returns the static HTML skeleton for the interactive poll demo.
|
* Embeds the full <folk-vote-dashboard space="demo"> component for
|
||||||
* The client-side vote-demo.ts populates #rd-polls-container via WebSocket.
|
* real interactivity (browse spaces, create proposals, cast conviction
|
||||||
|
* votes, binary final votes) plus showcase sections explaining the rVote vision.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const FEATURES = [
|
||||||
|
{
|
||||||
|
icon: "\u{1F4CA}",
|
||||||
|
title: "Conviction Voting",
|
||||||
|
desc: "Stake credits on proposals you support. Weight costs credits quadratically (weight\u00B2), preventing plutocratic capture while rewarding conviction.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "\u{1F3AF}",
|
||||||
|
title: "Promotion Threshold",
|
||||||
|
desc: "Proposals accumulate conviction score. When they hit the threshold, they auto-promote to a binary YES/NO/ABSTAIN final vote.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "\u23F3",
|
||||||
|
title: "Vote Decay",
|
||||||
|
desc: "Conviction decays after 30 days. This ensures ongoing relevance — stale votes fade, keeping governance dynamic and current.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "\u{1F3DB}\uFE0F",
|
||||||
|
title: "Governance Spaces",
|
||||||
|
desc: "Each community gets its own voting space with configurable thresholds, credit budgets, and voting periods.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const INTEGRATIONS = [
|
||||||
|
{ icon: "\u{1F30A}", name: "rFunds", desc: "Passed proposals trigger funding flows. Vote on budget allocations and threshold adjustments." },
|
||||||
|
{ icon: "\u{1F4DD}", name: "rNotes", desc: "Link supporting research, discussion threads, and rationale documents to proposals." },
|
||||||
|
{ icon: "\u{1F4C5}", name: "rCal", desc: "Voting deadlines, governance meetings, and proposal review periods on the calendar." },
|
||||||
|
{ icon: "\u{1F465}", name: "rNetwork", desc: "Delegate voting power to trusted network members. Liquid democracy across communities." },
|
||||||
|
{ icon: "\u{1F6D2}", name: "rCart", desc: "Vote on merchandise decisions, provider selection, and catalog curation." },
|
||||||
|
{ icon: "\u2615\uFE0F", name: "rSpace", desc: "Each space has its own governance layer. Nest voting across spaces for multi-community decisions." },
|
||||||
|
];
|
||||||
|
|
||||||
export function renderDemo(): string {
|
export function renderDemo(): string {
|
||||||
return `
|
return `
|
||||||
<div class="rd-root" style="--rd-accent-from:#f97316; --rd-accent-to:#fb923c">
|
<div class="rd-root" style="--rd-accent-from:#f97316; --rd-accent-to:#fb923c;">
|
||||||
|
|
||||||
<!-- ── Hero ── -->
|
<!-- Hero -->
|
||||||
<div class="rd-hero">
|
<section class="rd-hero">
|
||||||
|
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(249,115,22,0.1);border:1px solid rgba(249,115,22,0.2);border-radius:9999px;font-size:0.875rem;color:#fdba74;font-weight:500;margin-bottom:1.5rem;">
|
||||||
|
Conviction Voting Engine
|
||||||
|
</div>
|
||||||
<h1>rVote Demo</h1>
|
<h1>rVote Demo</h1>
|
||||||
<p class="rd-subtitle">Interactive polls synced in real-time across all r* demos</p>
|
<p class="rd-subtitle">Credit-weighted conviction voting for collaborative governance decisions</p>
|
||||||
<div class="rd-meta">
|
<div class="rd-meta">
|
||||||
<span>Vote on options with +/- buttons</span>
|
<span>\u{1F4CA} Conviction Voting</span>
|
||||||
<span>Changes sync instantly via WebSocket</span>
|
<span style="color:#475569">|</span>
|
||||||
|
<span>\u{1F3AF} Thresholds</span>
|
||||||
|
<span style="color:#475569">|</span>
|
||||||
|
<span>\u23F3 Vote Decay</span>
|
||||||
|
<span style="color:#475569">|</span>
|
||||||
|
<span>\u{1F3DB}\uFE0F Governance</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- ── Connection bar ── -->
|
<!-- Interactive Vote Dashboard -->
|
||||||
<div class="rd-section rd-section--narrow">
|
<section class="rd-section rd-section--narrow">
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1.5rem; flex-wrap:wrap; gap:0.75rem">
|
<div class="rd-card" style="padding:0;overflow:hidden;">
|
||||||
<div style="display:flex; align-items:center; gap:0.75rem; flex-wrap:wrap">
|
<folk-vote-dashboard space="demo"></folk-vote-dashboard>
|
||||||
<span id="rd-conn-badge" class="rd-status rd-status--disconnected">Disconnected</span>
|
|
||||||
<span class="rd-badge rd-badge--orange" style="font-size:0.7rem">Live — synced across all r* demos</span>
|
|
||||||
</div>
|
|
||||||
<button id="rd-reset-btn" class="rd-btn rd-btn--ghost" disabled>
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
|
|
||||||
Reset Demo
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Loading state (shown until first snapshot) -->
|
<!-- Core Concepts -->
|
||||||
<div id="rd-loading" class="rd-card" style="border:2px dashed rgba(100,116,139,0.4); display:none">
|
<section class="rd-section">
|
||||||
<div class="rd-card-body" style="padding:3rem; text-align:center">
|
<div class="rd-grid rd-grid--2">
|
||||||
<div style="width:2rem; height:2rem; margin:0 auto 0.75rem; border:3px solid rgba(249,115,22,0.2); border-top-color:#f97316; border-radius:50%; animation:rd-spin 0.8s linear infinite"></div>
|
${FEATURES.map(
|
||||||
<p class="rd-text-muted">Connecting to rSpace...</p>
|
(f) => `
|
||||||
</div>
|
<div class="rd-card" style="padding:1.5rem;">
|
||||||
|
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
|
||||||
|
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
|
||||||
|
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
|
||||||
|
</div>`,
|
||||||
|
).join("")}
|
||||||
</div>
|
</div>
|
||||||
<style>@keyframes rd-spin { to { transform: rotate(360deg); } }</style>
|
</section>
|
||||||
|
|
||||||
<!-- Empty state (no polls after connection) -->
|
<!-- Ecosystem Integrations -->
|
||||||
<div id="rd-empty" class="rd-card" style="border:2px dashed rgba(100,116,139,0.4); display:none">
|
<section class="rd-section">
|
||||||
<div class="rd-card-body" style="padding:3rem; text-align:center">
|
<h2 style="text-align:center;font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0 0 1.5rem;">
|
||||||
<div style="font-size:2rem; margin-bottom:0.75rem">🗳</div>
|
r* Ecosystem Integrations
|
||||||
<p class="rd-text-muted">No polls found. Try resetting the demo.</p>
|
</h2>
|
||||||
</div>
|
<div class="rd-grid rd-grid--3">
|
||||||
|
${INTEGRATIONS.map(
|
||||||
|
(i) => `
|
||||||
|
<div class="rd-card" style="padding:1.25rem;">
|
||||||
|
<div style="font-size:1.5rem;margin-bottom:0.5rem;">${i.icon}</div>
|
||||||
|
<h3 style="font-size:0.875rem;font-weight:600;color:#e2e8f0;margin:0 0 0.375rem;">${i.name}</h3>
|
||||||
|
<p style="font-size:0.8rem;color:#94a3b8;margin:0;line-height:1.4;">${i.desc}</p>
|
||||||
|
</div>`,
|
||||||
|
).join("")}
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Poll cards container (populated by vote-demo.ts) -->
|
<!-- CTA -->
|
||||||
<div id="rd-polls-container" style="display:flex; flex-direction:column; gap:1rem"></div>
|
<section class="rd-section rd-section--narrow">
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── CTA ── -->
|
|
||||||
<div class="rd-section rd-section--narrow">
|
|
||||||
<div class="rd-cta">
|
<div class="rd-cta">
|
||||||
<h2>Build with rVote</h2>
|
<h2>Govern Together</h2>
|
||||||
<p>Run conviction-weighted polls and governance decisions in your own community space.</p>
|
<p>
|
||||||
<a href="/create-space" style="background:linear-gradient(135deg,#f97316,#fb923c); box-shadow:0 4px 12px rgba(249,115,22,0.3)">
|
rVote brings conviction-weighted governance to your community.
|
||||||
|
Proposals rise through collective conviction, not majority rule.
|
||||||
|
</p>
|
||||||
|
<a href="/create-space" style="background:linear-gradient(135deg,#f97316,#fb923c);box-shadow:0 8px 24px rgba(249,115,22,0.25);">
|
||||||
Create Your Space
|
Create Your Space
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -337,8 +337,7 @@ routes.get("/", (c) => {
|
||||||
modules: getModuleInfoList(),
|
modules: getModuleInfoList(),
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
body: renderDemo(),
|
body: renderDemo(),
|
||||||
demoScripts: `<script type="module" src="/lib/demo-sync.js"></script>
|
scripts: `<script type="module" src="/modules/rvote/folk-vote-dashboard.js"></script>`,
|
||||||
<script type="module" src="/modules/rvote/vote-demo.js"></script>`,
|
|
||||||
styles: `<link rel="stylesheet" href="/modules/rvote/vote.css">`,
|
styles: `<link rel="stylesheet" href="/modules/rvote/vote.css">`,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue