rtrips-online/src/app/demo/demo-content.tsx

763 lines
33 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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<string, DemoShape>, type: string): DemoShape[] {
return Object.values(shapes).filter((s) => s.type === type)
}
function shapeByType(shapes: Record<string, DemoShape>, 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<string, string> = {
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 (
<div className="animate-pulse space-y-3">
<div className="h-4 bg-slate-700/50 rounded w-3/4" />
<div className="h-4 bg-slate-700/50 rounded w-1/2" />
<div className="h-4 bg-slate-700/50 rounded w-2/3" />
<div className="h-4 bg-slate-700/50 rounded w-1/3" />
</div>
)
}
/* ─── 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 (
<div
className={`bg-slate-800/50 rounded-2xl border border-slate-700/50 overflow-hidden ${
span === 2 ? 'md:col-span-2' : ''
}`}
>
<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">{icon}</span>
<span className="font-semibold text-sm">{title}</span>
{live && (
<span className="flex items-center gap-1 text-xs text-emerald-400">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
live
</span>
)}
</div>
<a
href={`https://${href}`}
target="_blank"
rel="noopener noreferrer"
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"
>
Open in {service}
</a>
</div>
<div className="p-5">{children}</div>
</div>
)
}
/* ─── 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 610' },
{ name: 'Zermatt', country: 'Switzerland', cx: 430, cy: 150, color: '#06b6d4', stroke: '#0891b2', dates: 'Jul 1014' },
{ name: 'Dolomites', country: 'Italy', cx: 650, cy: 140, color: '#8b5cf6', stroke: '#7c3aed', dates: 'Jul 1420' },
]
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 (
<CardWrapper icon="🗺️" title="Route Map" service="rMaps" href="rmaps.online" span={2} live={destinations.length > 0}>
<div className="relative w-full h-64 rounded-xl bg-slate-900/60 overflow-hidden">
<svg viewBox="0 0 800 300" className="w-full h-full" 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 */}
<path d={routePath} fill="none" stroke="rgba(94,234,212,0.7)" strokeWidth="3" strokeDasharray="10 6" />
{/* Destination pins */}
{pins.map((p) => (
<g key={p.name}>
<circle cx={p.cx} cy={p.cy} r="8" fill={p.color} stroke={p.stroke} strokeWidth="2" />
<text x={p.cx} y={p.cy + 30} textAnchor="middle" fill="#94a3b8" fontSize="12" fontWeight="600">
{p.name}
</text>
<text x={p.cx} y={p.cy + 44} textAnchor="middle" fill="#64748b" fontSize="10">
{p.dates}
</text>
</g>
))}
{/* Activity icons */}
<text x="280" y="168" fontSize="16">🥾</text>
<text x="350" y="188" fontSize="16">🧗</text>
<text x="500" y="128" fontSize="16">🚵</text>
<text x="560" y="148" fontSize="16">🪂</text>
<text x="620" y="158" fontSize="16">🛶</text>
</svg>
<div className="absolute bottom-3 left-3 flex gap-3 text-xs text-slate-400">
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-teal-500" /> France</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-cyan-500" /> Switzerland</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-violet-500" /> Italy</span>
</div>
</div>
</CardWrapper>
)
}
/* ─── Card: rNotes ──────────────────────────────────────────── */
function RNotesCard({
packingItems,
live,
onTogglePacked,
}: {
packingItems: { name: string; packed: boolean; category: string }[]
live: boolean
onTogglePacked: (index: number) => void
}) {
return (
<CardWrapper icon="📝" title="Trip Notes" service="rNotes" href="rnotes.online" live={live}>
<div className="space-y-4">
<div>
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">
Packing Checklist
</h4>
{packingItems.length === 0 ? (
<LoadingSkeleton />
) : (
<ul className="space-y-1.5">
{packingItems.map((item, idx) => (
<li key={`${item.name}-${idx}`} className="flex items-center gap-2 text-sm">
<button
onClick={() => onTogglePacked(idx)}
className={`w-4 h-4 rounded border flex-shrink-0 flex items-center justify-center transition-colors cursor-pointer ${
item.packed
? 'bg-teal-500/20 border-teal-500 text-teal-400'
: 'border-slate-600 hover:border-slate-400'
}`}
>
{item.packed && (
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</button>
<span className={item.packed ? 'text-slate-400 line-through' : 'text-slate-200'}>
{item.name}
</span>
</li>
))}
</ul>
)}
</div>
<div>
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">
Trip Rules
</h4>
<ol className="space-y-1 text-sm text-slate-300 list-decimal list-inside">
{FALLBACK_RULES.map((rule) => (
<li key={rule}>{rule}</li>
))}
</ol>
</div>
</div>
</CardWrapper>
)
}
/* ─── Card: rCal ────────────────────────────────────────────── */
function RCalCard({ calendarEvents, live }: { calendarEvents: Record<number, { label: string; color: string }[]>; 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 (
<CardWrapper icon="📅" title="Group Calendar" service="rCal" href="rtrips.online" span={2} live={live}>
<div>
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-semibold text-slate-200">July 2026</h4>
<span className="text-xs text-slate-400">{tripEnd - tripStart + 1} days</span>
</div>
<div className="grid grid-cols-7 gap-1 mb-1">
{dayNames.map((d) => (
<div key={d} className="text-center text-xs text-slate-500 font-medium py-1">{d}</div>
))}
</div>
<div className="grid grid-cols-7 gap-1">
{Array.from({ length: offset }).map((_, i) => (
<div key={`empty-${i}`} className="h-14" />
))}
{days.map((day) => {
const events = calendarEvents[day]
const isTrip = day >= tripStart && day <= tripEnd
return (
<div
key={day}
className={`h-14 rounded-lg p-1 text-xs ${
isTrip ? 'bg-slate-700/40 border border-slate-600/40' : 'bg-slate-800/30'
}`}
>
<span className={isTrip ? 'text-slate-200 font-medium' : 'text-slate-500'}>{day}</span>
{events?.map((e) => (
<div key={e.label} className={`${e.color} rounded px-1 py-0.5 text-white truncate mt-0.5`} style={{ fontSize: '9px' }}>
{e.label}
</div>
))}
</div>
)
})}
</div>
<div className="flex flex-wrap gap-3 mt-3 text-xs text-slate-400">
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-teal-500" /> Travel</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-emerald-500" /> Hiking</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-amber-500" /> Adventure</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-violet-500" /> Culture</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-slate-500" /> Rest</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-cyan-500" /> Transit</span>
</div>
</div>
</CardWrapper>
)
}
/* ─── Card: rVote ───────────────────────────────────────────── */
interface PollData {
question: string
options: { label: string; votes: number; color: string }[]
totalVotes: number
}
function RVoteCard({ polls, live }: { polls: PollData[]; live: boolean }) {
return (
<CardWrapper icon="🗳️" title="Group Polls" service="rVote" href="rvote.online" live={live}>
{polls.length === 0 ? (
<LoadingSkeleton />
) : (
<div className="space-y-5">
{polls.map((poll) => (
<div key={poll.question}>
<h4 className="text-sm font-medium text-slate-200 mb-2">{poll.question}</h4>
<div className="space-y-2">
{poll.options.map((opt) => {
const pct = poll.totalVotes > 0 ? Math.round((opt.votes / poll.totalVotes) * 100) : 0
return (
<div key={opt.label}>
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-slate-300">{opt.label}</span>
<span className="text-slate-400">
{opt.votes} vote{opt.votes !== 1 ? 's' : ''} ({pct}%)
</span>
</div>
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
<div className={`h-full ${opt.color} rounded-full transition-all`} style={{ width: `${pct}%` }} />
</div>
</div>
)
})}
</div>
<p className="text-xs text-slate-500 mt-1">{poll.totalVotes} votes cast</p>
</div>
))}
</div>
)}
</CardWrapper>
)
}
/* ─── 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<string, number> = {}
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 (
<CardWrapper icon="💰" title="Group Expenses" service="rFunds" href="rfunds.online" live={live}>
<div className="space-y-4">
<div className="text-center py-2">
<p className="text-2xl font-bold text-white">{totalSpent > 0 ? `${totalSpent.toLocaleString()}` : '...'}</p>
<p className="text-xs text-slate-400">Total group spending</p>
</div>
{expenses.length === 0 ? (
<LoadingSkeleton />
) : (
<>
<div>
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Recent</h4>
<div className="space-y-2">
{expenses.slice(0, 4).map((e) => (
<div key={e.desc} className="flex items-center justify-between text-sm">
<div className="min-w-0">
<p className="text-slate-200 truncate">{e.desc}</p>
<p className="text-xs text-slate-500">{e.who} split {e.split} ways</p>
</div>
<span className="text-slate-300 font-medium ml-2 flex-shrink-0">{e.amount}</span>
</div>
))}
</div>
</div>
<div>
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Balances</h4>
<div className="grid grid-cols-2 gap-1.5">
{members.map((m) => {
const bal = balances[m.name] || 0
return (
<div key={m.name} className="flex items-center justify-between text-xs">
<span className="text-slate-300">{m.name}</span>
<span className={bal >= 0 ? 'text-emerald-400' : 'text-rose-400'}>
{bal >= 0 ? '+' : ''}{Math.round(bal)}
</span>
</div>
)
})}
</div>
</div>
</>
)}
</div>
</CardWrapper>
)
}
/* ─── 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 (
<CardWrapper icon="🛒" title="Shared Gear" service="rCart" href="rcart.online" span={2} live={live}>
{cartItems.length === 0 ? (
<LoadingSkeleton />
) : (
<div className="space-y-3">
<div className="flex items-center justify-between text-sm mb-1">
<span className="text-slate-300">{totalFunded} / {totalTarget} funded</span>
<span className="text-xs text-slate-400">
{cartItems.filter((i) => i.status === 'Purchased').length}/{cartItems.length} purchased
</span>
</div>
<div className="h-2 bg-slate-700 rounded-full overflow-hidden mb-4">
<div className="h-full bg-teal-500 rounded-full" style={{ width: `${totalTarget > 0 ? Math.round((totalFunded / totalTarget) * 100) : 0}%` }} />
</div>
<div className="grid sm:grid-cols-2 gap-3">
{cartItems.map((item) => {
const pct = item.target > 0 ? Math.round((item.funded / item.target) * 100) : 0
return (
<div key={item.item} className="bg-slate-700/30 rounded-lg p-3">
<div className="flex items-center justify-between mb-1.5">
<span className="text-sm text-slate-200">{item.item}</span>
{item.status === 'Purchased' ? (
<span className="text-xs px-2 py-0.5 bg-emerald-500/20 text-emerald-400 rounded-full"> Bought</span>
) : (
<span className="text-xs text-slate-400">{item.funded}/{item.target}</span>
)}
</div>
<div className="h-1.5 bg-slate-600 rounded-full overflow-hidden">
<div className={`h-full rounded-full ${pct === 100 ? 'bg-emerald-500' : 'bg-teal-500'}`} style={{ width: `${pct}%` }} />
</div>
</div>
)
})}
</div>
</div>
)}
</CardWrapper>
)
}
/* ─── 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<number, { label: string; color: string }[]>
const cal: Record<number, { label: string; color: string }[]> = {}
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 620, 2026'
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-7xl mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<AppSwitcher current="trips" />
<Link href="/" className="flex items-center gap-2">
<div className="w-8 h-8 bg-gradient-to-br from-teal-400 to-cyan-500 rounded-lg flex items-center justify-center font-bold text-slate-900 text-sm">
rT
</div>
<span className="font-semibold text-lg">rTrips</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 ? 'Connected to rSpace' : 'Connecting...'}
</span>
<button
onClick={handleReset}
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"
title="Reset demo data to initial state"
>
Reset Demo
</button>
<Link
href="/trips/new"
className="text-sm px-4 py-2 bg-teal-600 hover:bg-teal-500 rounded-lg transition-colors font-medium"
>
Plan Your Own Trip
</Link>
</div>
</div>
</nav>
{/* Trip Header */}
<section className="max-w-7xl 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-teal-300 via-cyan-300 to-blue-300 bg-clip-text text-transparent">
{tripTitle}
</h1>
<p className="text-lg text-slate-300 mb-2">
{destinations.length > 0
? destinations.map((d) => d.destName as string).join(' → ')
: 'Chamonix → Zermatt → Dolomites'}
</p>
<div className="flex flex-wrap items-center justify-center gap-4 text-sm text-slate-400 mb-6">
<span>📅 {dateRange}</span>
<span>💶 ~{budgetTotal.toLocaleString()} budget</span>
<span>🏔 {destinations.length > 0 ? `${new Set(destinations.map((d) => d.country as string)).size} countries` : '3 countries'}</span>
{hasShapes && (
<span className="flex items-center gap-1 text-emerald-400">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
Live data
</span>
)}
</div>
{/* Member avatars */}
<div className="flex items-center justify-center gap-2">
{(members.length > 0 ? members : [{ name: 'Loading', color: 'bg-slate-600' }]).map((m) => (
<div
key={m.name}
className={`w-10 h-10 ${m.color} rounded-full flex items-center justify-center text-sm font-bold text-white ring-2 ring-slate-800`}
title={m.name}
>
{m.name[0]}
</div>
))}
{members.length > 0 && (
<span className="text-sm text-slate-400 ml-2">{members.length} explorers</span>
)}
</div>
</div>
</section>
{/* rStack Intro */}
<section className="max-w-7xl mx-auto px-6 pb-6">
<p className="text-center text-sm text-slate-400 max-w-2xl mx-auto">
Every trip is powered by the <span className="text-slate-200 font-medium">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>
</section>
{/* Canvas Grid */}
<section className="max-w-7xl mx-auto px-6 pb-16">
<div className="grid md:grid-cols-3 gap-4">
<RMapsCard destinations={destinations} live={hasShapes} />
<RNotesCard packingItems={packingItems} live={hasShapes} onTogglePacked={handleTogglePacked} />
<RCalCard calendarEvents={calendarEvents} live={hasShapes} />
<RVoteCard polls={polls} live={hasShapes} />
<RFundsCard expenses={expenses} members={members} live={hasShapes} />
<RCartCard cartItems={cartItems} live={hasShapes} />
</div>
</section>
{/* Bottom CTA */}
<section className="max-w-7xl mx-auto px-6 pb-20 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">Plan Your Own Group Adventure</h2>
<p className="text-slate-400 mb-6 max-w-lg mx-auto">
The rStack gives your group everything you need routes, schedules, polls, shared
expenses, and gear lists all connected in one trip canvas.
</p>
<Link
href="/trips/new"
className="inline-block px-8 py-4 bg-teal-600 hover:bg-teal-500 rounded-xl text-lg font-medium transition-all shadow-lg shadow-teal-900/30"
>
Start Planning
</Link>
</div>
</section>
{/* Footer */}
<footer className="border-t border-slate-700/50 py-8">
<div className="max-w-7xl 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">rStack</span>
<a href="https://rtrips.online" className="hover:text-slate-300 transition-colors"> rTrips</a>
<a href="https://rmaps.online" className="hover:text-slate-300 transition-colors">🗺 rMaps</a>
<a href="https://rnotes.online" className="hover:text-slate-300 transition-colors">📝 rNotes</a>
<a href="https://rvote.online" className="hover:text-slate-300 transition-colors">🗳 rVote</a>
<a href="https://rfunds.online" className="hover:text-slate-300 transition-colors">💸 rFunds</a>
<a href="https://rcart.online" className="hover:text-slate-300 transition-colors">🛒 rCart</a>
<a href="https://rspace.online" className="hover:text-slate-300 transition-colors">🌌 rSpace</a>
<a href="https://rfiles.online" className="hover:text-slate-300 transition-colors">📁 rFiles</a>
<a href="https://rwallet.online" className="hover:text-slate-300 transition-colors">💰 rWallet</a>
</div>
<p className="text-center text-xs text-slate-600">
Collaborative trip planning for groups that love adventure.
</p>
</div>
</footer>
</div>
)
}