'use client' import Link from 'next/link' import { useState, useMemo, useCallback } from 'react' import { useDemoSync, type DemoShape } from '@/lib/demo-sync' /* ─── Types ─────────────────────────────────────────────────── */ interface ExpenseShape extends DemoShape { type: 'demo-expense' description: string amount: number currency: string paidBy: string split: 'equal' | 'custom' category: 'transport' | 'accommodation' | 'activity' | 'food' date: string } interface BudgetShape extends DemoShape { type: 'folk-budget' budgetTitle: string currency: string budgetTotal: number spent: number categories: { name: string; budget: number; spent: number }[] } /* ─── Constants ─────────────────────────────────────────────── */ const MEMBERS = ['Maya', 'Liam', 'Priya', 'Omar'] const CATEGORY_ICONS: Record = { transport: '\u{1F682}', // train accommodation: '\u{1F3E8}', // hotel activity: '\u{26F7}', // skier food: '\u{1F372}', // pot of food } const CATEGORY_LABELS: Record = { transport: 'Transport', accommodation: 'Accommodation', activity: 'Activities', food: 'Food & Drink', } const CATEGORY_COLORS: Record = { transport: 'bg-cyan-500', accommodation: 'bg-violet-500', activity: 'bg-amber-500', food: 'bg-rose-500', } const CATEGORY_TEXT_COLORS: Record = { transport: 'text-cyan-400', accommodation: 'text-violet-400', activity: 'text-amber-400', food: 'text-rose-400', } /* ─── Helpers ───────────────────────────────────────────────── */ function formatCurrency(amount: number, currency: string): string { const symbol = currency === 'CHF' ? 'CHF ' : '\u20AC' if (currency === 'CHF') return `${symbol}${amount.toLocaleString()}` return `${symbol}${amount.toLocaleString()}` } function formatDate(dateStr: string): string { try { const d = new Date(dateStr) return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) } catch { return dateStr } } /* ─── Compute Balances ──────────────────────────────────────── */ function computeBalances(expenses: ExpenseShape[]): { name: string; paid: number; owes: number; balance: number }[] { const totalExpenses = expenses.reduce((sum, e) => sum + e.amount, 0) const perPerson = totalExpenses / MEMBERS.length const paid: Record = {} MEMBERS.forEach((m) => (paid[m] = 0)) expenses.forEach((e) => { paid[e.paidBy] = (paid[e.paidBy] || 0) + e.amount }) return MEMBERS.map((name) => ({ name, paid: paid[name] || 0, owes: perPerson, balance: (paid[name] || 0) - perPerson, })) } function computeSettlements(balances: { name: string; balance: number }[]): { from: string; to: string; amount: number }[] { const debtors = balances.filter((b) => b.balance < -0.01).map((b) => ({ ...b, balance: -b.balance })) const creditors = balances.filter((b) => b.balance > 0.01).map((b) => ({ ...b })) debtors.sort((a, b) => b.balance - a.balance) creditors.sort((a, b) => b.balance - a.balance) const settlements: { from: string; to: string; amount: number }[] = [] let di = 0, ci = 0 while (di < debtors.length && ci < creditors.length) { const amount = Math.min(debtors[di].balance, creditors[ci].balance) if (amount > 0.01) { settlements.push({ from: debtors[di].name, to: creditors[ci].name, amount: Math.round(amount * 100) / 100 }) } debtors[di].balance -= amount creditors[ci].balance -= amount if (debtors[di].balance < 0.01) di++ if (creditors[ci].balance < 0.01) ci++ } return settlements } /* ─── Main Component ────────────────────────────────────────── */ export default function DemoContent() { const { shapes, updateShape, connected, resetDemo } = useDemoSync({ filter: ['demo-expense', 'folk-budget'], }) const [editingExpenseId, setEditingExpenseId] = useState(null) const [editAmount, setEditAmount] = useState('') const [resetting, setResetting] = useState(false) // Extract typed shapes const expenses = useMemo(() => { return Object.values(shapes) .filter((s): s is ExpenseShape => s.type === 'demo-expense') .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) }, [shapes]) const budget = useMemo(() => { return Object.values(shapes).find((s): s is BudgetShape => s.type === 'folk-budget') ?? null }, [shapes]) // Computed values const totalSpent = useMemo(() => expenses.reduce((s, e) => s + e.amount, 0), [expenses]) const balances = useMemo(() => computeBalances(expenses), [expenses]) const settlements = useMemo(() => computeSettlements(balances), [balances]) const budgetTotal = budget?.budgetTotal ?? 4000 const budgetSpent = budget?.spent ?? totalSpent const budgetRemaining = budgetTotal - budgetSpent const budgetPct = budgetTotal > 0 ? Math.min(100, Math.round((budgetSpent / budgetTotal) * 100)) : 0 // Category breakdown from budget shape or computed from expenses const categoryBreakdown = useMemo(() => { if (budget?.categories && budget.categories.length > 0) { return budget.categories } // Fallback: compute from expenses const cats: Record = {} expenses.forEach((e) => { cats[e.category] = (cats[e.category] || 0) + e.amount }) return Object.entries(cats).map(([name, spent]) => ({ name, budget: Math.round(budgetTotal / 4), spent, })) }, [budget, expenses, budgetTotal]) // Expense amount editing const startEditExpense = useCallback((id: string, currentAmount: number) => { setEditingExpenseId(id) setEditAmount(String(currentAmount)) }, []) const saveEditExpense = useCallback(() => { if (!editingExpenseId) return const newAmount = parseFloat(editAmount) if (isNaN(newAmount) || newAmount < 0) { setEditingExpenseId(null) return } updateShape(editingExpenseId, { amount: Math.round(newAmount * 100) / 100 }) setEditingExpenseId(null) setEditAmount('') }, [editingExpenseId, editAmount, updateShape]) const cancelEdit = useCallback(() => { setEditingExpenseId(null) setEditAmount('') }, []) const handleReset = useCallback(async () => { setResetting(true) try { await resetDemo() } catch { // silent } finally { setTimeout(() => setResetting(false), 1000) } }, [resetDemo]) const noData = expenses.length === 0 && !budget return (
{/* Nav */} {/* Hero */}

Alpine Explorer 2026

Group Expenses

{'\u{1F4C5}'} Jul 6-20, 2026 {'\u{1F465}'} {MEMBERS.length} travelers {'\u{1F3D4}'} Chamonix {'\u2192'} Zermatt {'\u2192'} Dolomites
{/* Member avatars */}
{MEMBERS.map((name, i) => { const colors = ['bg-emerald-500', 'bg-cyan-500', 'bg-violet-500', 'bg-amber-500'] return (
{name[0]}
) })} {MEMBERS.length} members
{/* Waiting for data state */} {noData && connected && (
Waiting for demo data from rSpace...
)} {/* Budget Overview */} {(budget || expenses.length > 0) && (

{'\u{1F4CA}'} Trip Budget

{budget && ( live )}
{/* Budget totals */}

{formatCurrency(budgetTotal, budget?.currency ?? 'EUR')}

Total Budget

{formatCurrency(budgetSpent, budget?.currency ?? 'EUR')}

Spent

= 0 ? 'text-cyan-400' : 'text-rose-400'}`}> {formatCurrency(Math.abs(budgetRemaining), budget?.currency ?? 'EUR')}

{budgetRemaining >= 0 ? 'Remaining' : 'Over Budget'}

{/* Progress bar */}
{budgetPct}% used {formatCurrency(budgetRemaining, budget?.currency ?? 'EUR')} left
= 90 ? 'bg-rose-500' : budgetPct >= 70 ? 'bg-amber-500' : 'bg-emerald-500' }`} style={{ width: `${budgetPct}%` }} />
{/* Category breakdown */} {categoryBreakdown.length > 0 && (

Budget by Category

{categoryBreakdown.map((cat) => { const catPct = cat.budget > 0 ? Math.min(100, Math.round((cat.spent / cat.budget) * 100)) : 0 const catKey = cat.name.toLowerCase() return (
{CATEGORY_ICONS[catKey] || '\u{1F4B0}'} {CATEGORY_LABELS[catKey] || cat.name} {formatCurrency(cat.spent, budget?.currency ?? 'EUR')} / {formatCurrency(cat.budget, budget?.currency ?? 'EUR')}
= 100 ? 'bg-rose-500' : (CATEGORY_COLORS[catKey] || 'bg-emerald-500') }`} style={{ width: `${catPct}%` }} />

{catPct}% used

) })}
)}
)} {/* Expenses + Balances Grid */} {expenses.length > 0 && (
{/* Expense List - spans 2 columns */}
{'\u{1F4DD}'} Expenses ({expenses.length})
Click amount to edit
{expenses.map((expense) => { const isEditing = editingExpenseId === expense.id return (
{/* Category icon */}
{CATEGORY_ICONS[expense.category] || '\u{1F4B0}'}
{/* Description + meta */}

{expense.description}

{CATEGORY_LABELS[expense.category] || expense.category} {'\u00B7'} Paid by {expense.paidBy} {'\u00B7'} {expense.split === 'equal' ? `Split ${MEMBERS.length} ways` : 'Custom split'} {'\u00B7'} {formatDate(expense.date)}
{/* Amount */} {isEditing ? (
{expense.currency === 'CHF' ? 'CHF' : '\u20AC'} setEditAmount(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') saveEditExpense() if (e.key === 'Escape') cancelEdit() }} className="w-20 bg-slate-700 border border-emerald-500/50 rounded-lg px-2 py-1 text-sm text-white text-right focus:outline-none focus:border-emerald-400" autoFocus />
) : ( )}
) })}
{/* Total */}
Total {formatCurrency(totalSpent, expenses[0]?.currency ?? 'EUR')}
{/* Balances + Settlements */}
{/* Balances card */}
{'\u2696\uFE0F'} Balances
{balances.map((b) => { const colors = ['bg-emerald-500', 'bg-cyan-500', 'bg-violet-500', 'bg-amber-500'] const idx = MEMBERS.indexOf(b.name) return (
{b.name[0]}

{b.name}

Paid {formatCurrency(b.paid, expenses[0]?.currency ?? 'EUR')}

= 0 ? 'text-emerald-400' : 'text-rose-400'}`}> {b.balance >= 0 ? '+' : ''}{formatCurrency(Math.round(b.balance * 100) / 100, expenses[0]?.currency ?? 'EUR')}
) })}
{/* Settlements card */} {settlements.length > 0 && (
{'\u{1F4B8}'} Settle Up
{settlements.map((s, i) => (
{s.from} {'\u2192'} {formatCurrency(s.amount, expenses[0]?.currency ?? 'EUR')} {s.to}
))}

{settlements.length} payment{settlements.length !== 1 ? 's' : ''} to settle all debts

)}
)} {/* Expense breakdown by category */} {expenses.length > 0 && (

{'\u{1F4CA}'} Spending by Category

{(['transport', 'accommodation', 'activity', 'food'] as const).map((cat) => { const catExpenses = expenses.filter((e) => e.category === cat) const catTotal = catExpenses.reduce((s, e) => s + e.amount, 0) const catPct = totalSpent > 0 ? Math.round((catTotal / totalSpent) * 100) : 0 return (
{CATEGORY_ICONS[cat]}

{CATEGORY_LABELS[cat]}

{formatCurrency(catTotal, expenses[0]?.currency ?? 'EUR')}

{catPct}% of total

) })}
)} {/* Per-person spending */} {expenses.length > 0 && (

{'\u{1F464}'} Per Person

{balances.map((b, i) => { const colors = ['bg-emerald-500', 'bg-cyan-500', 'bg-violet-500', 'bg-amber-500'] const paidPct = totalSpent > 0 ? Math.round((b.paid / totalSpent) * 100) : 0 return (
{b.name[0]}

{b.name}

{formatCurrency(b.paid, expenses[0]?.currency ?? 'EUR')}

paid ({paidPct}%)

= 0 ? 'text-emerald-400' : 'text-rose-400'}`}> {b.balance >= 0 ? 'Gets back ' : 'Owes '} {formatCurrency(Math.abs(Math.round(b.balance * 100) / 100), expenses[0]?.currency ?? 'EUR')}

) })}
)} {/* CTA */}

Track Your Group Expenses

rFunds makes it easy to manage shared costs, track budgets, and settle up. Design custom funding flows with threshold-based mechanisms.

Try the Flow Designer Create Your Space
{/* Footer */}
) }