rfunds-online/app/demo/demo-content.tsx

656 lines
30 KiB
TypeScript

'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<string, string> = {
transport: '\u{1F682}', // train
accommodation: '\u{1F3E8}', // hotel
activity: '\u{26F7}', // skier
food: '\u{1F372}', // pot of food
}
const CATEGORY_LABELS: Record<string, string> = {
transport: 'Transport',
accommodation: 'Accommodation',
activity: 'Activities',
food: 'Food & Drink',
}
const CATEGORY_COLORS: Record<string, string> = {
transport: 'bg-cyan-500',
accommodation: 'bg-violet-500',
activity: 'bg-amber-500',
food: 'bg-rose-500',
}
const CATEGORY_TEXT_COLORS: Record<string, string> = {
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<string, number> = {}
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<string | null>(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<string, number> = {}
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 (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white">
{/* Nav */}
<nav className="border-b border-slate-700/50 backdrop-blur-sm">
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/" className="flex items-center gap-2">
<div className="w-8 h-8 bg-gradient-to-br from-amber-400 to-emerald-500 rounded-lg flex items-center justify-center font-bold text-slate-900 text-sm">
rF
</div>
<span className="font-semibold text-lg">rFunds</span>
</Link>
<span className="text-slate-600">/</span>
<span className="text-sm text-slate-400">Demo</span>
</div>
<div className="flex items-center gap-3">
{/* Connection indicator */}
<span className={`flex items-center gap-1.5 text-xs ${connected ? 'text-emerald-400' : 'text-slate-500'}`}>
<span className={`w-2 h-2 rounded-full ${connected ? 'bg-emerald-400 animate-pulse' : 'bg-slate-600'}`} />
{connected ? 'Live' : 'Connecting...'}
</span>
<button
onClick={handleReset}
disabled={resetting}
className="text-xs px-3 py-1.5 bg-slate-700/60 hover:bg-slate-600/60 rounded-lg text-slate-300 hover:text-white transition-colors disabled:opacity-50"
>
{resetting ? 'Resetting...' : 'Reset Demo'}
</button>
<Link
href="/tbff"
className="text-sm px-4 py-2 bg-emerald-600 hover:bg-emerald-500 rounded-lg transition-colors font-medium"
>
Try Full App
</Link>
</div>
</div>
</nav>
{/* Hero */}
<section className="max-w-6xl mx-auto px-6 pt-12 pb-8">
<div className="text-center max-w-3xl mx-auto">
<h1 className="text-4xl sm:text-5xl font-bold mb-4 bg-gradient-to-r from-amber-300 via-emerald-300 to-cyan-300 bg-clip-text text-transparent">
Alpine Explorer 2026
</h1>
<p className="text-lg text-slate-300 mb-2">Group Expenses</p>
<div className="flex flex-wrap items-center justify-center gap-4 text-sm text-slate-400 mb-6">
<span>{'\u{1F4C5}'} Jul 6-20, 2026</span>
<span>{'\u{1F465}'} {MEMBERS.length} travelers</span>
<span>{'\u{1F3D4}'} Chamonix {'\u2192'} Zermatt {'\u2192'} Dolomites</span>
</div>
{/* Member avatars */}
<div className="flex items-center justify-center gap-2">
{MEMBERS.map((name, i) => {
const colors = ['bg-emerald-500', 'bg-cyan-500', 'bg-violet-500', 'bg-amber-500']
return (
<div
key={name}
className={`w-10 h-10 ${colors[i]} rounded-full flex items-center justify-center text-sm font-bold text-white ring-2 ring-slate-800`}
title={name}
>
{name[0]}
</div>
)
})}
<span className="text-sm text-slate-400 ml-2">{MEMBERS.length} members</span>
</div>
</div>
</section>
{/* Waiting for data state */}
{noData && connected && (
<section className="max-w-6xl mx-auto px-6 pb-8">
<div className="text-center py-8">
<div className="inline-flex items-center gap-2 px-4 py-2 bg-slate-800/50 rounded-xl border border-slate-700/50 text-sm text-slate-400">
<span className="w-4 h-4 border-2 border-slate-500 border-t-emerald-400 rounded-full animate-spin" />
Waiting for demo data from rSpace...
</div>
</div>
</section>
)}
{/* Budget Overview */}
{(budget || expenses.length > 0) && (
<section className="max-w-6xl mx-auto px-6 pb-6">
<div className="bg-slate-800/50 rounded-2xl border border-slate-700/50 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold flex items-center gap-2">
{'\u{1F4CA}'} Trip Budget
</h2>
{budget && (
<span className="text-xs text-emerald-400 flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
live
</span>
)}
</div>
{/* Budget totals */}
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-slate-700/30 rounded-xl p-4 text-center">
<p className="text-2xl font-bold text-white">{formatCurrency(budgetTotal, budget?.currency ?? 'EUR')}</p>
<p className="text-xs text-slate-400 mt-1">Total Budget</p>
</div>
<div className="bg-slate-700/30 rounded-xl p-4 text-center">
<p className="text-2xl font-bold text-emerald-400">{formatCurrency(budgetSpent, budget?.currency ?? 'EUR')}</p>
<p className="text-xs text-slate-400 mt-1">Spent</p>
</div>
<div className="bg-slate-700/30 rounded-xl p-4 text-center">
<p className={`text-2xl font-bold ${budgetRemaining >= 0 ? 'text-cyan-400' : 'text-rose-400'}`}>
{formatCurrency(Math.abs(budgetRemaining), budget?.currency ?? 'EUR')}
</p>
<p className="text-xs text-slate-400 mt-1">{budgetRemaining >= 0 ? 'Remaining' : 'Over Budget'}</p>
</div>
</div>
{/* Progress bar */}
<div className="mb-6">
<div className="flex items-center justify-between text-xs text-slate-400 mb-1.5">
<span>{budgetPct}% used</span>
<span>{formatCurrency(budgetRemaining, budget?.currency ?? 'EUR')} left</span>
</div>
<div className="h-3 bg-slate-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${
budgetPct >= 90 ? 'bg-rose-500' : budgetPct >= 70 ? 'bg-amber-500' : 'bg-emerald-500'
}`}
style={{ width: `${budgetPct}%` }}
/>
</div>
</div>
{/* Category breakdown */}
{categoryBreakdown.length > 0 && (
<div>
<h3 className="text-sm font-semibold text-slate-300 mb-3">Budget by Category</h3>
<div className="grid sm:grid-cols-2 gap-3">
{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 (
<div key={cat.name} className="bg-slate-700/30 rounded-xl p-3">
<div className="flex items-center justify-between mb-2">
<span className="flex items-center gap-2 text-sm text-slate-200">
<span>{CATEGORY_ICONS[catKey] || '\u{1F4B0}'}</span>
{CATEGORY_LABELS[catKey] || cat.name}
</span>
<span className="text-xs text-slate-400">
{formatCurrency(cat.spent, budget?.currency ?? 'EUR')} / {formatCurrency(cat.budget, budget?.currency ?? 'EUR')}
</span>
</div>
<div className="h-2 bg-slate-600 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${
catPct >= 100 ? 'bg-rose-500' : (CATEGORY_COLORS[catKey] || 'bg-emerald-500')
}`}
style={{ width: `${catPct}%` }}
/>
</div>
<p className="text-xs text-slate-500 mt-1">{catPct}% used</p>
</div>
)
})}
</div>
</div>
)}
</div>
</section>
)}
{/* Expenses + Balances Grid */}
{expenses.length > 0 && (
<section className="max-w-6xl mx-auto px-6 pb-6">
<div className="grid md:grid-cols-3 gap-4">
{/* Expense List - spans 2 columns */}
<div className="md:col-span-2 bg-slate-800/50 rounded-2xl border border-slate-700/50 overflow-hidden">
<div className="flex items-center justify-between px-5 py-3 border-b border-slate-700/50">
<div className="flex items-center gap-2">
<span className="text-xl">{'\u{1F4DD}'}</span>
<span className="font-semibold text-sm">Expenses ({expenses.length})</span>
</div>
<span className="text-xs text-slate-400">Click amount to edit</span>
</div>
<div className="divide-y divide-slate-700/30">
{expenses.map((expense) => {
const isEditing = editingExpenseId === expense.id
return (
<div key={expense.id} className="flex items-center gap-4 px-5 py-3 hover:bg-slate-700/20 transition-colors">
{/* Category icon */}
<div className={`w-10 h-10 rounded-xl flex items-center justify-center text-lg ${
CATEGORY_COLORS[expense.category]
? `${CATEGORY_COLORS[expense.category]}/20`
: 'bg-slate-700/40'
}`}>
{CATEGORY_ICONS[expense.category] || '\u{1F4B0}'}
</div>
{/* Description + meta */}
<div className="flex-1 min-w-0">
<p className="text-sm text-slate-200 font-medium truncate">{expense.description}</p>
<div className="flex items-center gap-2 text-xs text-slate-500 mt-0.5">
<span className={CATEGORY_TEXT_COLORS[expense.category] || 'text-slate-400'}>
{CATEGORY_LABELS[expense.category] || expense.category}
</span>
<span>{'\u00B7'}</span>
<span>Paid by {expense.paidBy}</span>
<span>{'\u00B7'}</span>
<span>{expense.split === 'equal' ? `Split ${MEMBERS.length} ways` : 'Custom split'}</span>
<span>{'\u00B7'}</span>
<span>{formatDate(expense.date)}</span>
</div>
</div>
{/* Amount */}
{isEditing ? (
<div className="flex items-center gap-1.5">
<span className="text-sm text-slate-400">{expense.currency === 'CHF' ? 'CHF' : '\u20AC'}</span>
<input
type="number"
value={editAmount}
onChange={(e) => 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
/>
<button
onClick={saveEditExpense}
className="text-emerald-400 hover:text-emerald-300 text-xs px-1.5 py-0.5 bg-emerald-500/10 rounded"
>
{'\u2713'}
</button>
<button
onClick={cancelEdit}
className="text-slate-400 hover:text-slate-300 text-xs px-1.5 py-0.5 bg-slate-700/50 rounded"
>
{'\u2717'}
</button>
</div>
) : (
<button
onClick={() => startEditExpense(expense.id, expense.amount)}
className="text-right font-semibold text-slate-200 hover:text-emerald-400 transition-colors cursor-pointer group"
title="Click to edit amount"
>
<span className="text-sm">{formatCurrency(expense.amount, expense.currency)}</span>
<span className="block text-xs text-slate-500 group-hover:text-emerald-400/60 transition-colors">
{formatCurrency(expense.amount / MEMBERS.length, expense.currency)}/person
</span>
</button>
)}
</div>
)
})}
</div>
{/* Total */}
<div className="flex items-center justify-between px-5 py-3 border-t border-slate-700/50 bg-slate-700/20">
<span className="text-sm font-semibold text-slate-300">Total</span>
<span className="text-lg font-bold text-white">{formatCurrency(totalSpent, expenses[0]?.currency ?? 'EUR')}</span>
</div>
</div>
{/* Balances + Settlements */}
<div className="space-y-4">
{/* Balances card */}
<div className="bg-slate-800/50 rounded-2xl border border-slate-700/50 overflow-hidden">
<div className="px-5 py-3 border-b border-slate-700/50">
<div className="flex items-center gap-2">
<span className="text-xl">{'\u2696\uFE0F'}</span>
<span className="font-semibold text-sm">Balances</span>
</div>
</div>
<div className="p-5 space-y-3">
{balances.map((b) => {
const colors = ['bg-emerald-500', 'bg-cyan-500', 'bg-violet-500', 'bg-amber-500']
const idx = MEMBERS.indexOf(b.name)
return (
<div key={b.name} className="flex items-center gap-3">
<div className={`w-8 h-8 ${colors[idx]} rounded-full flex items-center justify-center text-xs font-bold text-white`}>
{b.name[0]}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-slate-200">{b.name}</p>
<p className="text-xs text-slate-500">
Paid {formatCurrency(b.paid, expenses[0]?.currency ?? 'EUR')}
</p>
</div>
<span className={`text-sm font-semibold ${b.balance >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{b.balance >= 0 ? '+' : ''}{formatCurrency(Math.round(b.balance * 100) / 100, expenses[0]?.currency ?? 'EUR')}
</span>
</div>
)
})}
</div>
</div>
{/* Settlements card */}
{settlements.length > 0 && (
<div className="bg-slate-800/50 rounded-2xl border border-slate-700/50 overflow-hidden">
<div className="px-5 py-3 border-b border-slate-700/50">
<div className="flex items-center gap-2">
<span className="text-xl">{'\u{1F4B8}'}</span>
<span className="font-semibold text-sm">Settle Up</span>
</div>
</div>
<div className="p-5 space-y-3">
{settlements.map((s, i) => (
<div key={i} className="flex items-center gap-2 bg-slate-700/30 rounded-xl p-3">
<span className="text-sm text-rose-400 font-medium">{s.from}</span>
<span className="flex-1 text-center">
<span className="text-xs text-slate-500">{'\u2192'}</span>
<span className="block text-sm font-semibold text-white">
{formatCurrency(s.amount, expenses[0]?.currency ?? 'EUR')}
</span>
</span>
<span className="text-sm text-emerald-400 font-medium">{s.to}</span>
</div>
))}
<p className="text-xs text-slate-500 text-center mt-2">
{settlements.length} payment{settlements.length !== 1 ? 's' : ''} to settle all debts
</p>
</div>
</div>
)}
</div>
</div>
</section>
)}
{/* Expense breakdown by category */}
{expenses.length > 0 && (
<section className="max-w-6xl mx-auto px-6 pb-6">
<div className="bg-slate-800/50 rounded-2xl border border-slate-700/50 p-6">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
{'\u{1F4CA}'} Spending by Category
</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{(['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 (
<div key={cat} className="bg-slate-700/30 rounded-xl p-4 text-center">
<div className="text-2xl mb-2">{CATEGORY_ICONS[cat]}</div>
<p className="text-sm text-slate-300 font-medium">{CATEGORY_LABELS[cat]}</p>
<p className="text-lg font-bold text-white mt-1">
{formatCurrency(catTotal, expenses[0]?.currency ?? 'EUR')}
</p>
<div className="h-1.5 bg-slate-600 rounded-full overflow-hidden mt-2">
<div
className={`h-full rounded-full ${CATEGORY_COLORS[cat]}`}
style={{ width: `${catPct}%` }}
/>
</div>
<p className="text-xs text-slate-500 mt-1">{catPct}% of total</p>
</div>
)
})}
</div>
</div>
</section>
)}
{/* Per-person spending */}
{expenses.length > 0 && (
<section className="max-w-6xl mx-auto px-6 pb-6">
<div className="bg-slate-800/50 rounded-2xl border border-slate-700/50 p-6">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
{'\u{1F464}'} Per Person
</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{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 (
<div key={b.name} className="bg-slate-700/30 rounded-xl p-4 text-center">
<div className={`w-12 h-12 ${colors[i]} rounded-full flex items-center justify-center text-lg font-bold text-white mx-auto mb-2 ring-2 ring-slate-800`}>
{b.name[0]}
</div>
<p className="text-sm text-slate-300 font-medium">{b.name}</p>
<p className="text-lg font-bold text-white mt-1">
{formatCurrency(b.paid, expenses[0]?.currency ?? 'EUR')}
</p>
<p className="text-xs text-slate-500">paid ({paidPct}%)</p>
<p className={`text-sm font-semibold mt-1 ${b.balance >= 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')}
</p>
</div>
)
})}
</div>
</div>
</section>
)}
{/* CTA */}
<section className="max-w-6xl mx-auto px-6 pb-16 text-center">
<div className="bg-slate-800/50 rounded-2xl border border-slate-700/50 p-10">
<h2 className="text-3xl font-bold mb-3">Track Your Group Expenses</h2>
<p className="text-slate-400 mb-6 max-w-lg mx-auto">
rFunds makes it easy to manage shared costs, track budgets, and settle up.
Design custom funding flows with threshold-based mechanisms.
</p>
<div className="flex items-center justify-center gap-4">
<Link
href="/tbff"
className="px-6 py-3 bg-slate-700 hover:bg-slate-600 rounded-xl text-lg font-medium transition-all border border-slate-600"
>
Try the Flow Designer
</Link>
<Link
href="/space"
className="px-8 py-3 bg-emerald-600 hover:bg-emerald-500 rounded-xl text-lg font-medium transition-all shadow-lg shadow-emerald-900/30"
>
Create Your Space
</Link>
</div>
</div>
</section>
{/* Footer */}
<footer className="border-t border-slate-700/50 py-8">
<div className="max-w-6xl mx-auto px-6">
<div className="flex flex-wrap items-center justify-center gap-4 text-sm text-slate-500 mb-4">
<span className="font-medium text-slate-400">r* Ecosystem</span>
<a href="https://rspace.online" className="hover:text-slate-300 transition-colors">{'\u{1F30C}'} rSpace</a>
<a href="https://rmaps.online" className="hover:text-slate-300 transition-colors">{'\u{1F5FA}'} rMaps</a>
<a href="https://rnotes.online" className="hover:text-slate-300 transition-colors">{'\u{1F4DD}'} rNotes</a>
<a href="https://rvote.online" className="hover:text-slate-300 transition-colors">{'\u{1F5F3}'} rVote</a>
<a href="https://rfunds.online" className="hover:text-slate-300 transition-colors font-medium text-slate-300">{'\u{1F4B0}'} rFunds</a>
<a href="https://rtrips.online" className="hover:text-slate-300 transition-colors">{'\u2708\uFE0F'} rTrips</a>
<a href="https://rcart.online" className="hover:text-slate-300 transition-colors">{'\u{1F6D2}'} rCart</a>
<a href="https://rwallet.online" className="hover:text-slate-300 transition-colors">{'\u{1F4BC}'} rWallet</a>
<a href="https://rfiles.online" className="hover:text-slate-300 transition-colors">{'\u{1F4C1}'} rFiles</a>
<a href="https://rinbox.online" className="hover:text-slate-300 transition-colors">{'\u2709\uFE0F'} rInbox</a>
<a href="https://rnetwork.online" className="hover:text-slate-300 transition-colors">{'\u{1F310}'} rNetwork</a>
</div>
<p className="text-center text-xs text-slate-600">
Part of the r* ecosystem collaborative tools for communities.
</p>
</div>
</footer>
</div>
)
}