rspace-online/modules/rcart/demo.ts

235 lines
11 KiB
TypeScript

/**
* rCart demo page — static community garden shopping cart.
*
* Renders a fully server-side demo with 8 cart items, funding progress bars,
* member activity, and summary stats. No WebSocket needed (all static data).
*/
/* ─── Mock Data ─────────────────────────────────────────────── */
const members = [
{ name: "Alice", color: "#10b981" },
{ name: "Bob", color: "#0ea5e9" },
{ name: "Carol", color: "#f59e0b" },
{ name: "Dave", color: "#8b5cf6" },
];
interface CartItem {
name: string;
price: number;
requestedBy: string;
funded: number;
status: "Funded" | "In Cart" | "Needs Funding";
}
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 {
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 `
<div class="rd-root" style="--rd-accent-from: #10b981; --rd-accent-to: #2dd4bf;">
<!-- ── 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;">
Group Shopping, Together
</div>
<h1>See how rCart works</h1>
<p class="rd-subtitle">
A community garden project where neighbors pool resources to buy everything they need together.
</p>
<div class="rd-meta">
<span>8 items</span>
<span style="color:#475569">|</span>
<span>$${totalCost.toFixed(2)} total</span>
<span style="color:#475569">|</span>
<span>${fundedCount}/${cartItems.length} funded</span>
</div>
<div class="rd-avatars">
${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>
</section>
<!-- ── Overall Funding Progress ── -->
<section class="rd-section rd-section--narrow">
<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:0.75rem;">
<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">&#128722;</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;">&middot;</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>
</section>
<!-- ── CTA ── -->
<section class="rd-section rd-section--narrow">
<div class="rd-cta">
<h2>Ready to shop together?</h2>
<p>
Create a shared cart for your group, community, or team. Add items from any store,
split costs fairly, and check out together.
</p>
<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
</a>
</div>
</section>
</div>`;
}