'use client' import Link from 'next/link' import { useCallback, useMemo } from 'react' import { useDemoSync, type DemoShape } from '@/lib/demo-sync' import { AppSwitcher } from '@/components/AppSwitcher' /* ─── Helper: extract shapes by type ──────────────────────────── */ function shapesByType(shapes: Record, type: string): DemoShape[] { return Object.values(shapes).filter((s) => s.type === type) } function shapeByType(shapes: Record, type: string): DemoShape | undefined { return Object.values(shapes).find((s) => s.type === type) } /* ─── Constants ───────────────────────────────────────────────── */ const MEMBER_COLORS = ['bg-teal-500', 'bg-cyan-500', 'bg-blue-500', 'bg-violet-500', 'bg-amber-500', 'bg-rose-500'] const CATEGORY_COLORS: Record = { travel: 'bg-teal-500', hike: 'bg-emerald-500', adventure: 'bg-amber-500', rest: 'bg-slate-500', culture: 'bg-violet-500', FLIGHT: 'bg-teal-500', TRANSPORT: 'bg-cyan-500', ACCOMMODATION: 'bg-teal-500', ACTIVITY: 'bg-emerald-500', MEAL: 'bg-amber-500', FREE_TIME: 'bg-slate-500', OTHER: 'bg-slate-500', } const POLL_OPTION_COLORS = ['bg-amber-500', 'bg-blue-500', 'bg-emerald-500', 'bg-rose-500'] const FALLBACK_RULES = [ 'Majority vote on daily activities', 'Shared expenses split equally', 'Quiet hours after 10pm in huts', 'Everyone carries their own pack', ] /* ─── Loading Skeleton ────────────────────────────────────────── */ function LoadingSkeleton() { return (
) } /* ─── Card Wrapper ──────────────────────────────────────────── */ function CardWrapper({ icon, title, service, href, span = 1, live = false, children, }: { icon: string title: string service: string href: string span?: 1 | 2 live?: boolean children: React.ReactNode }) { return (
{icon} {title} {live && ( live )}
Open in {service} ↗
{children}
) } /* ─── Card: rMaps ───────────────────────────────────────────── */ function RMapsCard({ destinations }: { destinations: DemoShape[]; live: boolean }) { const pins = destinations.length > 0 ? destinations.map((d, i) => ({ name: d.destName as string, country: d.country as string, cx: 160 + i * 245, cy: 180 - i * 20, color: ['#14b8a6', '#06b6d4', '#8b5cf6'][i] || '#94a3b8', stroke: ['#0d9488', '#0891b2', '#7c3aed'][i] || '#64748b', dates: d.arrivalDate && d.departureDate ? `${new Date(d.arrivalDate as string).toLocaleDateString('en', { month: 'short', day: 'numeric' })}–${new Date(d.departureDate as string).getUTCDate()}` : '', })) : [ { name: 'Chamonix', country: 'France', cx: 160, cy: 180, color: '#14b8a6', stroke: '#0d9488', dates: 'Jul 6–10' }, { name: 'Zermatt', country: 'Switzerland', cx: 430, cy: 150, color: '#06b6d4', stroke: '#0891b2', dates: 'Jul 10–14' }, { name: 'Dolomites', country: 'Italy', cx: 650, cy: 140, color: '#8b5cf6', stroke: '#7c3aed', dates: 'Jul 14–20' }, ] const routePath = pins.length >= 3 ? `M${pins[0].cx} ${pins[0].cy} C${pins[0].cx + 90} ${pins[0].cy - 20}, ${pins[1].cx - 80} ${pins[1].cy + 50}, ${pins[1].cx} ${pins[1].cy} C${pins[1].cx + 80} ${pins[1].cy - 50}, ${pins[2].cx - 90} ${pins[2].cy + 20}, ${pins[2].cx} ${pins[2].cy}` : 'M160 180 C250 160, 350 200, 430 150 C510 100, 560 160, 650 140' return ( 0}>
{/* Mountain silhouettes */} {/* Route line */} {/* Destination pins */} {pins.map((p) => ( {p.name} {p.dates} ))} {/* Activity icons */} 🥾 🧗 🚵 🪂 🛶
France Switzerland Italy
) } /* ─── Card: rNotes ──────────────────────────────────────────── */ function RNotesCard({ packingItems, live, onTogglePacked, }: { packingItems: { name: string; packed: boolean; category: string }[] live: boolean onTogglePacked: (index: number) => void }) { return (

Packing Checklist

{packingItems.length === 0 ? ( ) : (
    {packingItems.map((item, idx) => (
  • {item.name}
  • ))}
)}

Trip Rules

    {FALLBACK_RULES.map((rule) => (
  1. {rule}
  2. ))}
) } /* ─── Card: rCal ────────────────────────────────────────────── */ function RCalCard({ calendarEvents, live }: { calendarEvents: Record; live: boolean }) { const daysInJuly = 31 const offset = 2 // July 1, 2026 is a Wednesday (Mon-start grid) const days = Array.from({ length: daysInJuly }, (_, i) => i + 1) const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] // Determine trip range from calendar data const eventDays = Object.keys(calendarEvents).map(Number) const tripStart = eventDays.length > 0 ? Math.min(...eventDays) : 6 const tripEnd = eventDays.length > 0 ? Math.max(...eventDays) : 20 return (

July 2026

{tripEnd - tripStart + 1} days
{dayNames.map((d) => (
{d}
))}
{Array.from({ length: offset }).map((_, i) => (
))} {days.map((day) => { const events = calendarEvents[day] const isTrip = day >= tripStart && day <= tripEnd return (
{day} {events?.map((e) => (
{e.label}
))}
) })}
Travel Hiking Adventure Culture Rest Transit
) } /* ─── Card: rVote ───────────────────────────────────────────── */ interface PollData { question: string options: { label: string; votes: number; color: string }[] totalVotes: number } function RVoteCard({ polls, live }: { polls: PollData[]; live: boolean }) { return ( {polls.length === 0 ? ( ) : (
{polls.map((poll) => (

{poll.question}

{poll.options.map((opt) => { const pct = poll.totalVotes > 0 ? Math.round((opt.votes / poll.totalVotes) * 100) : 0 return (
{opt.label} {opt.votes} vote{opt.votes !== 1 ? 's' : ''} ({pct}%)
) })}

{poll.totalVotes} votes cast

))}
)} ) } /* ─── Card: rFunds ──────────────────────────────────────────── */ function RFundsCard({ expenses, members, live, }: { expenses: { desc: string; who: string; amount: number; split: number }[] members: { name: string; color: string }[] live: boolean }) { const totalSpent = expenses.reduce((s, e) => s + e.amount, 0) const balances: Record = {} members.forEach((m) => (balances[m.name] = 0)) expenses.forEach((e) => { const share = e.amount / e.split balances[e.who] = (balances[e.who] || 0) + e.amount members.slice(0, e.split).forEach((m) => { balances[m.name] = (balances[m.name] || 0) - share }) }) return (

{totalSpent > 0 ? `€${totalSpent.toLocaleString()}` : '...'}

Total group spending

{expenses.length === 0 ? ( ) : ( <>

Recent

{expenses.slice(0, 4).map((e) => (

{e.desc}

{e.who} • split {e.split} ways

€{e.amount}
))}

Balances

{members.map((m) => { const bal = balances[m.name] || 0 return (
{m.name} = 0 ? 'text-emerald-400' : 'text-rose-400'}> {bal >= 0 ? '+' : ''}€{Math.round(bal)}
) })}
)}
) } /* ─── Card: rCart ────────────────────────────────────────────── */ interface CartItemData { item: string target: number funded: number status: 'Purchased' | 'Funding' } function RCartCard({ cartItems, live }: { cartItems: CartItemData[]; live: boolean }) { const totalFunded = cartItems.reduce((s, i) => s + i.funded, 0) const totalTarget = cartItems.reduce((s, i) => s + i.target, 0) return ( {cartItems.length === 0 ? ( ) : (
€{totalFunded} / €{totalTarget} funded {cartItems.filter((i) => i.status === 'Purchased').length}/{cartItems.length} purchased
0 ? Math.round((totalFunded / totalTarget) * 100) : 0}%` }} />
{cartItems.map((item) => { const pct = item.target > 0 ? Math.round((item.funded / item.target) * 100) : 0 return (
{item.item} {item.status === 'Purchased' ? ( ✓ Bought ) : ( €{item.funded}/€{item.target} )}
) })}
)} ) } /* ─── Main Demo Content ─────────────────────────────────────── */ export default function DemoContent() { const { shapes, updateShape, connected, resetDemo } = useDemoSync() const hasShapes = Object.keys(shapes).length > 0 // ── Extract shapes by type ────────────────────────────────── const itinerary = useMemo(() => shapeByType(shapes, 'folk-itinerary'), [shapes]) const destinations = useMemo(() => shapesByType(shapes, 'folk-destination'), [shapes]) const packingList = useMemo(() => shapeByType(shapes, 'folk-packing-list'), [shapes]) const pollShapes = useMemo(() => shapesByType(shapes, 'demo-poll'), [shapes]) const expenseShapes = useMemo(() => shapesByType(shapes, 'demo-expense'), [shapes]) const cartShapes = useMemo(() => shapesByType(shapes, 'demo-cart-item'), [shapes]) const budgetShape = useMemo(() => shapeByType(shapes, 'folk-budget'), [shapes]) // ── Derived data: members from itinerary travelers ───────── const members = useMemo(() => { const travelers = (itinerary?.travelers ?? []) as string[] if (travelers.length === 0) return [] return travelers.map((name, i) => ({ name, color: MEMBER_COLORS[i % MEMBER_COLORS.length], })) }, [itinerary]) // ── Derived data: calendar events from itinerary items ───── const calendarEvents = useMemo(() => { const items = (itinerary?.items ?? []) as { date: string; activity: string; category: string }[] if (items.length === 0) return {} as Record const cal: Record = {} for (const item of items) { if (!item.date) continue // Parse "Jul 6" style dates const match = item.date.match(/(\d+)/) if (!match) continue const day = parseInt(match[1], 10) if (!cal[day]) cal[day] = [] cal[day].push({ label: item.activity, color: CATEGORY_COLORS[item.category] || 'bg-slate-500', }) } return cal }, [itinerary]) // ── Derived data: packing items ──────────────────────────── const packingItems = useMemo(() => { const items = (packingList?.items ?? []) as { name: string; packed: boolean; category: string }[] return items }, [packingList]) // ── Derived data: polls ──────────────────────────────────── const polls = useMemo((): PollData[] => { return pollShapes.map((shape) => { const options = (shape.options ?? []) as { label: string; votes: number }[] const totalVotes = options.reduce((sum, o) => sum + o.votes, 0) return { question: shape.question as string, options: options.map((o, i) => ({ label: o.label, votes: o.votes, color: POLL_OPTION_COLORS[i % POLL_OPTION_COLORS.length], })), totalVotes, } }) }, [pollShapes]) // ── Derived data: expenses ───────────────────────────────── const expenses = useMemo(() => { return expenseShapes.map((shape) => ({ desc: shape.description as string, who: shape.paidBy as string, amount: shape.amount as number, split: members.length || 4, })) }, [expenseShapes, members.length]) // ── Derived data: cart items ─────────────────────────────── const cartItems = useMemo((): CartItemData[] => { return cartShapes.map((shape) => { const price = shape.price as number const funded = shape.funded as number return { item: shape.name as string, target: price, funded, status: (funded >= price ? 'Purchased' : 'Funding') as 'Purchased' | 'Funding', } }) }, [cartShapes]) // ── Derived data: budget info ────────────────────────────── const budgetTotal = (budgetShape?.budgetTotal as number) || 4000 // ── Toggle packing item ──────────────────────────────────── const handleTogglePacked = useCallback( (index: number) => { if (!packingList) return const items = [...(packingList.items as { name: string; packed: boolean; category: string }[])] items[index] = { ...items[index], packed: !items[index].packed } updateShape(packingList.id, { items }) }, [packingList, updateShape] ) // ── Reset demo handler ───────────────────────────────────── const handleReset = useCallback(async () => { try { await resetDemo() } catch (err) { console.error('Failed to reset demo:', err) } }, [resetDemo]) // ── Header info ──────────────────────────────────────────── const tripTitle = (itinerary?.tripTitle as string) || 'Alpine Explorer 2026' const startDate = (itinerary?.startDate as string) || '' const endDate = (itinerary?.endDate as string) || '' const dateRange = startDate && endDate ? `${new Date(startDate).toLocaleDateString('en', { month: 'short', day: 'numeric' })}–${new Date(endDate).toLocaleDateString('en', { month: 'short', day: 'numeric' })}, 2026` : 'Jul 6–20, 2026' return (
{/* Nav */} {/* Trip Header */}

{tripTitle}

{destinations.length > 0 ? destinations.map((d) => d.destName as string).join(' → ') : 'Chamonix → Zermatt → Dolomites'}

📅 {dateRange} 💶 ~€{budgetTotal.toLocaleString()} budget 🏔️ {destinations.length > 0 ? `${new Set(destinations.map((d) => d.country as string)).size} countries` : '3 countries'} {hasShapes && ( Live data )}
{/* Member avatars */}
{(members.length > 0 ? members : [{ name: 'Loading', color: 'bg-slate-600' }]).map((m) => (
{m.name[0]}
))} {members.length > 0 && ( {members.length} explorers )}
{/* rStack Intro */}

Every trip is powered by the rStack — 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.

{/* Canvas Grid */}
{/* Bottom CTA */}

Plan Your Own Group Adventure

The rStack gives your group everything you need — routes, schedules, polls, shared expenses, and gear lists — all connected in one trip canvas.

Start Planning
{/* Footer */}
) }