feat: redesign landing page and add demo.rcal.online subdomain

Replace the minimal landing page with a full rStack-style marketing page
featuring hero with gradient text, 4-card principles grid, feature pillars,
temporal zoom visualization, four-view showcase, ecosystem integration
cards, and CTA section.

Move the calendar app to /demo route served at demo.rcal.online via
middleware subdomain routing. Add per-route layouts for Header placement
so the landing page controls its own chrome.

Also includes AppSwitcher/EcosystemFooter category reorganization
(rTube/rSwag to Creating, Social & Media split into Sharing + Observing).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-25 14:13:23 -08:00
parent cf00a77da2
commit f46575989f
11 changed files with 561 additions and 147 deletions

View File

@ -0,0 +1,14 @@
import { Header } from '@/components/Header'
export default function CalendarLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<>
<Header current="cal" />
{children}
</>
)
}

View File

@ -0,0 +1,14 @@
import { Header } from '@/components/Header'
export default function ContextLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<>
<Header current="cal" />
{children}
</>
)
}

14
src/app/demo/layout.tsx Normal file
View File

@ -0,0 +1,14 @@
import { Header } from '@/components/Header'
export default function DemoLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<>
<Header current="cal" />
{children}
</>
)
}

170
src/app/demo/page.tsx Normal file
View File

@ -0,0 +1,170 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Calendar as CalendarIcon, MapPin, Clock, ZoomIn, ZoomOut, Link2, Unlink2 } from 'lucide-react'
import { TemporalZoomController } from '@/components/calendar'
import { CalendarHeader } from '@/components/calendar/CalendarHeader'
import { CalendarSidebar } from '@/components/calendar/CalendarSidebar'
import { TabLayout } from '@/components/ui/TabLayout'
import { TemporalTab } from '@/components/tabs/TemporalTab'
import { SpatialTab } from '@/components/tabs/SpatialTab'
import { LunarTab } from '@/components/tabs/LunarTab'
import { ContextTab } from '@/components/tabs/ContextTab'
import { useCalendarStore, useEffectiveSpatialGranularity } from '@/lib/store'
import { TemporalGranularity, TEMPORAL_GRANULARITY_LABELS, GRANULARITY_LABELS } from '@/lib/types'
import type { TabView } from '@/lib/types'
export default function DemoPage() {
const [sidebarOpen, setSidebarOpen] = useState(true)
const [zoomPanelOpen, setZoomPanelOpen] = useState(false)
const {
temporalGranularity,
activeTab,
setActiveTab,
zoomCoupled,
toggleZoomCoupled,
zoomIn,
zoomOut,
} = useCalendarStore()
const effectiveSpatial = useEffectiveSpatialGranularity()
// Keyboard shortcuts
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
if (e.ctrlKey || e.metaKey || e.altKey) return
switch (e.key) {
case 'l':
case 'L':
e.preventDefault()
toggleZoomCoupled()
break
// Tab switching: 1-4
case '1':
e.preventDefault()
setActiveTab('temporal')
break
case '2':
e.preventDefault()
setActiveTab('spatial')
break
case '3':
e.preventDefault()
setActiveTab('lunar')
break
case '4':
e.preventDefault()
setActiveTab('context')
break
}
},
[toggleZoomCoupled, setActiveTab]
)
useEffect(() => {
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [handleKeyDown])
return (
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
{/* Sidebar */}
{sidebarOpen && (
<CalendarSidebar onClose={() => setSidebarOpen(false)} />
)}
{/* Main content */}
<div className="flex-1 flex flex-col overflow-hidden">
<CalendarHeader
onToggleSidebar={() => setSidebarOpen(!sidebarOpen)}
sidebarOpen={sidebarOpen}
/>
{/* Main area with optional zoom panel */}
<div className="flex-1 flex overflow-hidden">
{/* Tab layout */}
<div className="flex-1 overflow-hidden">
<TabLayout>
{{
temporal: <TemporalTab />,
spatial: <SpatialTab />,
lunar: <LunarTab />,
context: <ContextTab />,
}}
</TabLayout>
</div>
{/* Zoom control panel (collapsible) */}
{zoomPanelOpen && (
<aside className="w-80 border-l border-gray-200 dark:border-gray-700 p-4 overflow-auto bg-white dark:bg-gray-800">
<TemporalZoomController showSpatial={true} />
</aside>
)}
</div>
{/* Footer with calendar info and quick zoom controls */}
<footer className="border-t border-gray-200 dark:border-gray-700 px-4 py-2 bg-white dark:bg-gray-800">
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
<div className="flex items-center gap-4">
<span className="flex items-center gap-1">
<CalendarIcon className="w-4 h-4" />
Gregorian
</span>
<span className="flex items-center gap-1">
<Clock className="w-4 h-4" />
{TEMPORAL_GRANULARITY_LABELS[temporalGranularity]}
</span>
<span className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
{GRANULARITY_LABELS[effectiveSpatial]}
</span>
{activeTab === 'spatial' && (
<button
onClick={toggleZoomCoupled}
className={`flex items-center gap-1 px-2 py-0.5 rounded text-xs transition-colors ${
zoomCoupled
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30'
: 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
title={zoomCoupled ? 'Unlink spatial from temporal (L)' : 'Link spatial to temporal (L)'}
>
{zoomCoupled ? <Link2 className="w-3 h-3" /> : <Unlink2 className="w-3 h-3" />}
{zoomCoupled ? 'Coupled' : 'Independent'}
</button>
)}
</div>
{/* Quick controls */}
<div className="flex items-center gap-2">
<button
onClick={zoomIn}
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
title="Zoom in (+)"
>
<ZoomIn className="w-4 h-4" />
</button>
<button
onClick={zoomOut}
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
title="Zoom out (-)"
>
<ZoomOut className="w-4 h-4" />
</button>
<button
onClick={() => setZoomPanelOpen(!zoomPanelOpen)}
className={`px-2 py-1 text-xs rounded transition-colors ${
zoomPanelOpen
? 'bg-blue-500 text-white'
: 'hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
>
{zoomPanelOpen ? 'Hide' : 'Zoom Panel'}
</button>
</div>
</div>
</footer>
</div>
</div>
)
}

View File

@ -2,7 +2,6 @@ import type { Metadata } from 'next'
import { Inter, JetBrains_Mono } from 'next/font/google'
import './globals.css'
import { Providers } from '@/providers'
import { Header } from '@/components/Header'
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono' })
@ -25,9 +24,8 @@ export default function RootLayout({
<head>
<script defer src="https://rdata.online/collect.js" data-website-id="ea665b3c-ac4f-40cd-918e-1f99c5c69fac" />
</head>
<body className="bg-gray-900 min-h-screen text-gray-100">
<body className="bg-[#0b1120] min-h-screen text-gray-100">
<Providers>
<Header current="cal" />
{children}
</Providers>
</body>

View File

@ -1,136 +1,317 @@
import Link from 'next/link'
import { Header } from '@/components/Header'
import { EcosystemFooter } from '@/components/EcosystemFooter'
export default function LandingPage() {
return (
<div className="min-h-screen bg-gray-950 text-gray-100">
{/* Hero */}
<section className="px-6 py-20 text-center">
<div className="mx-auto max-w-3xl">
<h1 className="text-4xl sm:text-5xl font-bold leading-tight mb-6">
Group Calendars,{' '}
<span className="bg-gradient-to-r from-blue-400 to-blue-600 bg-clip-text text-transparent">
Simplified
</span>
</h1>
<p className="text-lg text-gray-400 max-w-xl mx-auto mb-10">
One calendar your whole group can see. No more back-and-forth just shared context for when and where to meet.
<div className="min-h-screen bg-[#0b1120] text-[#e2e8f0]">
<Header current="cal" />
{/* ══ Hero ══════════════════════════════════════ */}
<section className="text-center pt-28 pb-20 px-6 max-w-[820px] mx-auto">
<span className="inline-block text-xs font-bold tracking-[0.15em] uppercase text-blue-400 bg-blue-400/10 border border-blue-400/20 px-4 py-1.5 rounded-full mb-8">
Relational Calendar
</span>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold leading-[1.1] tracking-tight mb-4 bg-gradient-to-r from-blue-400 via-indigo-400 to-violet-400 bg-clip-text text-transparent">
Time is shared. Your calendar should be too.
</h1>
<p className="text-xl text-slate-300 mb-6 leading-relaxed">
A collaborative calendar for communities, cooperatives, and coordinated groups.
</p>
<p className="text-base text-slate-500 leading-relaxed max-w-[640px] mx-auto mb-10">
rCal rethinks the calendar as a <span className="text-blue-400 font-semibold">shared, spatial, and cyclical</span> tool.
See events across time and place, overlay lunar cycles, zoom from a single hour to a whole decade,
and keep everyone on the same page without the back-and-forth.
</p>
<div className="flex gap-4 justify-center flex-wrap">
<Link
href="https://demo.rcal.online"
className="inline-flex items-center gap-2 px-7 py-3 rounded-lg font-semibold text-[0.95rem] bg-gradient-to-r from-blue-400 to-indigo-500 text-[#0b1120] hover:-translate-y-0.5 transition-transform"
>
Try the Demo
</Link>
<a
href="#features"
className="inline-flex items-center gap-2 px-7 py-3 rounded-lg font-semibold text-[0.95rem] bg-white/[0.06] border border-white/[0.12] text-slate-300 hover:border-white/25 hover:-translate-y-0.5 transition-all"
>
Learn More
</a>
</div>
</section>
{/* ══ Principles (4-card grid) ══════════════════ */}
<section className="max-w-[1100px] mx-auto px-6 mb-24">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-8 text-center hover:border-white/[0.12] transition-colors">
<div className="w-12 h-12 mx-auto mb-4 rounded-xl bg-blue-400/[0.12] flex items-center justify-center text-2xl">
🤝
</div>
<h3 className="text-[0.95rem] font-semibold text-slate-100 mb-2">Shared by Default</h3>
<p className="text-sm text-slate-500 leading-relaxed">
One calendar for the whole group. Everyone sees the same context no more fragmented schedules.
</p>
</div>
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-8 text-center hover:border-white/[0.12] transition-colors">
<div className="w-12 h-12 mx-auto mb-4 rounded-xl bg-indigo-400/[0.12] flex items-center justify-center text-2xl">
🗺
</div>
<h3 className="text-[0.95rem] font-semibold text-slate-100 mb-2">Spatiotemporal</h3>
<p className="text-sm text-slate-500 leading-relaxed">
Events have a where, not just a when. See your schedule on a map and a timeline simultaneously.
</p>
</div>
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-8 text-center hover:border-white/[0.12] transition-colors">
<div className="w-12 h-12 mx-auto mb-4 rounded-xl bg-violet-400/[0.12] flex items-center justify-center text-2xl">
🌙
</div>
<h3 className="text-[0.95rem] font-semibold text-slate-100 mb-2">Natural Cycles</h3>
<p className="text-sm text-slate-500 leading-relaxed">
Lunar phases, eclipses, and solstices built in. Reconnect your planning to the rhythms of the natural world.
</p>
</div>
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-8 text-center hover:border-white/[0.12] transition-colors">
<div className="w-12 h-12 mx-auto mb-4 rounded-xl bg-emerald-400/[0.12] flex items-center justify-center text-2xl">
🔭
</div>
<h3 className="text-[0.95rem] font-semibold text-slate-100 mb-2">Multi-Scale Zoom</h3>
<p className="text-sm text-slate-500 leading-relaxed">
Ten levels of time from a 30-second moment to a cosmic era. See today or plan a decade ahead.
</p>
</div>
</div>
</section>
{/* ══ Why rCal (alt section) ════════════════════ */}
<section id="features" className="py-24 px-6 bg-white/[0.015] border-y border-white/[0.05]">
<div className="max-w-[1100px] mx-auto">
<span className="inline-block text-[0.7rem] font-bold tracking-[0.12em] uppercase text-blue-400 bg-blue-400/10 px-3 py-1 rounded-full mb-4">
Why rCal?
</span>
<h2 className="text-3xl sm:text-4xl font-bold text-slate-100 mb-3 tracking-tight leading-[1.2]">
Calendars were never meant to be personal silos
</h2>
<p className="text-lg text-slate-400 leading-relaxed max-w-[640px] mb-12">
Mainstream calendars treat time as private property. rCal treats it as a <span className="text-blue-400 font-semibold">commons</span> something groups navigate together. Here&apos;s what makes it different.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="bg-white/[0.03] border border-blue-400/[0.12] rounded-2xl p-7 hover:border-blue-400/25 transition-colors">
<div className="text-3xl mb-4">📍</div>
<h3 className="text-base font-semibold text-slate-100 mb-2">Where + When, Together</h3>
<p className="text-[0.88rem] text-slate-400 leading-relaxed">
Every event lives on both a timeline and a map. rCal&apos;s split view lets you see where everyone is meeting and when with nine spatial zoom levels from planet to street address.
</p>
</div>
<div className="bg-white/[0.03] border border-blue-400/[0.12] rounded-2xl p-7 hover:border-blue-400/25 transition-colors">
<div className="text-3xl mb-4">🔗</div>
<h3 className="text-base font-semibold text-slate-100 mb-2">Coupled Zoom</h3>
<p className="text-[0.88rem] text-slate-400 leading-relaxed">
Lock temporal and spatial zoom together: zoom out in time and the map zooms out to match. Planning a week? See the city. Planning a decade? See the continent.
</p>
</div>
<div className="bg-white/[0.03] border border-blue-400/[0.12] rounded-2xl p-7 hover:border-blue-400/25 transition-colors">
<div className="text-3xl mb-4">📡</div>
<h3 className="text-base font-semibold text-slate-100 mb-2">Multi-Source Sync</h3>
<p className="text-[0.88rem] text-slate-400 leading-relaxed">
Import from Google, Outlook, Apple, CalDAV, ICS feeds, and Obsidian. Layer multiple sources with per-source color coding and visibility controls.
</p>
</div>
<div className="bg-white/[0.03] border border-blue-400/[0.12] rounded-2xl p-7 hover:border-blue-400/25 transition-colors">
<div className="text-3xl mb-4">🌑</div>
<h3 className="text-base font-semibold text-slate-100 mb-2">Lunar Overlay</h3>
<p className="text-[0.88rem] text-slate-400 leading-relaxed">
Eight moon phases rendered on every calendar view with illumination percentages and eclipse detection. Plan gatherings, gardens, and ceremonies around natural cycles.
</p>
</div>
<div className="bg-white/[0.03] border border-blue-400/[0.12] rounded-2xl p-7 hover:border-blue-400/25 transition-colors">
<div className="text-3xl mb-4">🧩</div>
<h3 className="text-base font-semibold text-slate-100 mb-2">r* Ecosystem Embeds</h3>
<p className="text-[0.88rem] text-slate-400 leading-relaxed">
rTrips, rMaps, rNetwork, rCart, and rNotes can all embed a calendar view through the context API. One calendar, surfaced everywhere it&apos;s needed.
</p>
</div>
<div className="bg-white/[0.03] border border-blue-400/[0.12] rounded-2xl p-7 hover:border-blue-400/25 transition-colors">
<div className="text-3xl mb-4">🏠</div>
<h3 className="text-base font-semibold text-slate-100 mb-2">Self-Hosted & Sovereign</h3>
<p className="text-[0.88rem] text-slate-400 leading-relaxed">
Open source and Dockerized. Your events live on your infrastructure not in a corporate cloud. Full data sovereignty with rIDs authentication.
</p>
</div>
</div>
</div>
</section>
{/* ══ Temporal Zoom Levels ══════════════════════ */}
<section className="py-24 px-6">
<div className="max-w-[1100px] mx-auto">
<span className="inline-block text-[0.7rem] font-bold tracking-[0.12em] uppercase text-indigo-400 bg-indigo-400/10 px-3 py-1 rounded-full mb-4">
Temporal Navigation
</span>
<h2 className="text-3xl sm:text-4xl font-bold text-slate-100 mb-3 tracking-tight leading-[1.2]">
Ten levels of time
</h2>
<p className="text-lg text-slate-400 leading-relaxed max-w-[640px] mb-12">
Most calendars show you a month. rCal lets you zoom from a single moment to a cosmic era each level revealing a different kind of pattern.
</p>
<div className="bg-white/[0.02] border border-white/[0.06] rounded-2xl p-7 overflow-x-auto">
<div className="flex flex-col gap-3 min-w-[500px]">
{[
{ level: 0, name: 'Moment', span: '30 seconds', color: 'bg-blue-500', width: 'w-[4%]' },
{ level: 1, name: 'Hour', span: '60 minutes', color: 'bg-blue-400', width: 'w-[8%]' },
{ level: 2, name: 'Day', span: '24 hours', color: 'bg-blue-400', width: 'w-[14%]' },
{ level: 3, name: 'Week', span: '7 days', color: 'bg-indigo-400', width: 'w-[22%]' },
{ level: 4, name: 'Month', span: '~30 days', color: 'bg-indigo-400', width: 'w-[32%]' },
{ level: 5, name: 'Season', span: '~3 months', color: 'bg-violet-400', width: 'w-[44%]' },
{ level: 6, name: 'Year', span: '365 days', color: 'bg-violet-400', width: 'w-[58%]' },
{ level: 7, name: 'Decade', span: '10 years', color: 'bg-purple-400', width: 'w-[72%]' },
{ level: 8, name: 'Century', span: '100 years', color: 'bg-purple-500', width: 'w-[86%]' },
{ level: 9, name: 'Cosmic', span: 'Geological', color: 'bg-purple-600', width: 'w-full' },
].map((z) => (
<div key={z.level} className="flex items-center gap-4">
<span className="text-xs text-slate-500 w-6 text-right font-mono">{z.level}</span>
<div className={`${z.width} h-6 ${z.color}/20 rounded-md flex items-center px-3`}>
<span className="text-xs font-semibold text-slate-200 whitespace-nowrap">{z.name}</span>
<span className="text-[0.65rem] text-slate-500 ml-auto whitespace-nowrap">{z.span}</span>
</div>
</div>
))}
</div>
</div>
</div>
</section>
{/* ══ Calendar Views ════════════════════════════ */}
<section className="py-24 px-6 bg-white/[0.015] border-y border-white/[0.05]">
<div className="max-w-[1100px] mx-auto">
<span className="inline-block text-[0.7rem] font-bold tracking-[0.12em] uppercase text-violet-400 bg-violet-400/10 px-3 py-1 rounded-full mb-4">
Four Views
</span>
<h2 className="text-3xl sm:text-4xl font-bold text-slate-100 mb-3 tracking-tight leading-[1.2]">
One calendar, four perspectives
</h2>
<p className="text-lg text-slate-400 leading-relaxed max-w-[640px] mb-12">
Switch between views with keyboard shortcuts (1-4) to see your events from the angle that matters most right now.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-7 hover:border-white/[0.12] transition-colors">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-blue-500/[0.12] flex items-center justify-center text-lg">
📅
</div>
<div>
<h3 className="text-base font-semibold text-slate-100">Temporal</h3>
<span className="text-xs text-slate-500 font-mono">Press 1</span>
</div>
</div>
<p className="text-sm text-slate-400 leading-relaxed">
The classic calendar view month, week, day, year, and season enhanced with multi-granularity zoom and event indicators.
</p>
</div>
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-7 hover:border-white/[0.12] transition-colors">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-emerald-500/[0.12] flex items-center justify-center text-lg">
🗺
</div>
<div>
<h3 className="text-base font-semibold text-slate-100">Spatial</h3>
<span className="text-xs text-slate-500 font-mono">Press 2</span>
</div>
</div>
<p className="text-sm text-slate-400 leading-relaxed">
Interactive map powered by Leaflet. Events cluster by location with nine spatial granularity levels from planet to GPS coordinates.
</p>
</div>
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-7 hover:border-white/[0.12] transition-colors">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-amber-500/[0.12] flex items-center justify-center text-lg">
🌙
</div>
<div>
<h3 className="text-base font-semibold text-slate-100">Lunar</h3>
<span className="text-xs text-slate-500 font-mono">Press 3</span>
</div>
</div>
<p className="text-sm text-slate-400 leading-relaxed">
Moon phase overlay with illumination percentages, eclipse detection, and phase-colored day cells. Plan around the eight phases of the lunar cycle.
</p>
</div>
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-7 hover:border-white/[0.12] transition-colors">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-cyan-500/[0.12] flex items-center justify-center text-lg">
🧩
</div>
<div>
<h3 className="text-base font-semibold text-slate-100">Context</h3>
<span className="text-xs text-slate-500 font-mono">Press 4</span>
</div>
</div>
<p className="text-sm text-slate-400 leading-relaxed">
When embedded inside another r* tool, this view shows calendar data filtered for that tool&apos;s entity a trip, a network, a map layer.
</p>
</div>
</div>
</div>
</section>
{/* ══ Ecosystem Integration ═════════════════════ */}
<section className="py-24 px-6">
<div className="max-w-[1100px] mx-auto">
<span className="inline-block text-[0.7rem] font-bold tracking-[0.12em] uppercase text-emerald-400 bg-emerald-400/10 px-3 py-1 rounded-full mb-4">
Ecosystem
</span>
<h2 className="text-3xl sm:text-4xl font-bold text-slate-100 mb-3 tracking-tight leading-[1.2]">
Part of the r* stack
</h2>
<p className="text-lg text-slate-400 leading-relaxed max-w-[640px] mb-12">
rCal connects to the full suite of community tools. Any r* app can display or create calendar events through the shared context API.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
{[
{ emoji: '🗺', name: 'rTrips', desc: 'Trip itineraries auto-populate with calendar events for departure, accommodation, and activities.' },
{ emoji: '📍', name: 'rMaps', desc: 'Location-tagged events appear on shared community maps with time-filtered layers.' },
{ emoji: '👥', name: 'rNetwork', desc: 'See when your community members are available and schedule group meetings.' },
{ emoji: '🛒', name: 'rCart', desc: 'Product launches, market days, and delivery windows sync to your calendar.' },
{ emoji: '📝', name: 'rNotes', desc: 'Meeting notes link back to calendar events. Transcriptions attach to the moment they happened.' },
{ emoji: '🏠', name: 'rSpace', desc: 'Each space gets its own calendar. Subdomain routing means each community has a dedicated view.' },
].map((app) => (
<div key={app.name} className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-6 hover:border-emerald-400/30 hover:-translate-y-0.5 transition-all">
<div className="flex items-center gap-3 mb-3">
<span className="text-2xl">{app.emoji}</span>
<span className="text-base font-bold text-slate-100">{app.name}</span>
</div>
<p className="text-sm text-slate-400 leading-relaxed">{app.desc}</p>
</div>
))}
</div>
</div>
</section>
{/* ══ CTA ═══════════════════════════════════════ */}
<section className="py-20 px-6 bg-white/[0.015] border-y border-white/[0.05]">
<div className="max-w-[640px] mx-auto text-center">
<h2 className="text-2xl sm:text-3xl font-bold text-slate-100 mb-4">
See time differently
</h2>
<p className="text-base text-slate-400 mb-8 leading-relaxed">
Try the spatiotemporal calendar with lunar overlays, multi-source sync, and community sharing.
No account needed for the demo.
</p>
<div className="flex gap-4 justify-center flex-wrap">
<Link
href="/calendar"
className="px-6 py-3 bg-blue-600 hover:bg-blue-500 rounded-lg font-semibold transition-colors text-white"
href="https://demo.rcal.online"
className="inline-flex items-center gap-2 px-7 py-3 rounded-lg font-semibold text-[0.95rem] bg-gradient-to-r from-blue-400 to-indigo-500 text-[#0b1120] hover:-translate-y-0.5 transition-transform"
>
Try the Demo
Open the Demo
</Link>
<a
href="#features"
className="px-6 py-3 border border-gray-700 hover:border-gray-500 rounded-lg font-medium transition-colors"
href="https://rstack.online"
className="inline-flex items-center gap-2 px-7 py-3 rounded-lg font-semibold text-[0.95rem] bg-white/[0.06] border border-white/[0.12] text-slate-300 hover:border-white/25 hover:-translate-y-0.5 transition-all"
>
Learn More
Explore rStack
</a>
</div>
</div>
</section>
{/* Features */}
<section id="features" className="px-6 py-16 border-t border-gray-800">
<div className="mx-auto max-w-5xl">
<h2 className="text-2xl font-bold text-center mb-12">
A calendar that thinks in space and time
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-gray-900 border border-gray-800 rounded-xl p-6">
<div className="w-10 h-10 rounded-lg bg-blue-600/10 border border-blue-600/20 flex items-center justify-center text-xl mb-4">
🗺
</div>
<h3 className="text-lg font-semibold mb-2">Where + When, Together</h3>
<p className="text-sm text-gray-400 leading-relaxed">
See events on a calendar and a map side by side. Plan meetups knowing where everyone is, not just when.
</p>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-xl p-6">
<div className="w-10 h-10 rounded-lg bg-blue-600/10 border border-blue-600/20 flex items-center justify-center text-xl mb-4">
🔭
</div>
<h3 className="text-lg font-semibold mb-2">Zoom From Hours to Eras</h3>
<p className="text-sm text-gray-400 leading-relaxed">
Ten levels of time. See today's meetings, zoom out to the whole season, or plan years ahead all in one view.
</p>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-xl p-6">
<div className="w-10 h-10 rounded-lg bg-blue-600/10 border border-blue-600/20 flex items-center justify-center text-xl mb-4">
🌙
</div>
<h3 className="text-lg font-semibold mb-2">Moon & Natural Cycles</h3>
<p className="text-sm text-gray-400 leading-relaxed">
Built-in lunar phase overlay with eclipse detection. Plan around full moons, new moons, and solstices.
</p>
</div>
</div>
</div>
</section>
{/* How It Works */}
<section className="px-6 py-16 border-t border-gray-800">
<div className="mx-auto max-w-3xl">
<h2 className="text-2xl font-bold text-center mb-12">How It Works</h2>
<div className="space-y-8">
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-sm font-bold text-white">
1
</div>
<div>
<h3 className="font-semibold mb-1">Add your events</h3>
<p className="text-sm text-gray-400">
Create events with a time and a place. Or import from an existing calendar source.
</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-sm font-bold text-white">
2
</div>
<div>
<h3 className="font-semibold mb-1">Share with your group</h3>
<p className="text-sm text-gray-400">
Everyone sees the same calendar. Same context, same view.
</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-sm font-bold text-white">
3
</div>
<div>
<h3 className="font-semibold mb-1">Find the right zoom level</h3>
<p className="text-sm text-gray-400">
From a single hour to a decade. The calendar adapts to the scale you need.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Ecosystem */}
<section className="px-6 py-16 border-t border-gray-800">
<div className="mx-auto max-w-3xl">
<div className="bg-gray-900 border border-gray-800 rounded-xl p-8 text-center">
<h2 className="text-xl font-bold mb-3">Works with the r* ecosystem</h2>
<p className="text-gray-400 mb-6 max-w-md mx-auto">
rCal embeds into rTrips, rMaps, rNetwork, and more. Any r-tool can display a calendar view through the context API.
</p>
<Link
href="/calendar"
className="inline-block px-6 py-3 bg-blue-600 hover:bg-blue-500 rounded-lg font-semibold transition-colors text-white"
>
Open rCal
</Link>
</div>
</div>
</section>
<EcosystemFooter current="rCal" />
</div>
)

View File

@ -17,6 +17,8 @@ const MODULES: AppModule[] = [
{ id: 'space', name: 'rSpace', badge: 'rS', color: 'bg-teal-300', emoji: '🎨', description: 'Real-time collaborative canvas', domain: 'rspace.online' },
{ id: 'notes', name: 'rNotes', badge: 'rN', color: 'bg-amber-300', emoji: '📝', description: 'Group note-taking & knowledge capture', domain: 'rnotes.online' },
{ id: 'pubs', name: 'rPubs', badge: 'rP', color: 'bg-rose-300', emoji: '📖', description: 'Collaborative publishing platform', domain: 'rpubs.online' },
{ id: 'tube', name: 'rTube', badge: 'rTu', color: 'bg-pink-300', emoji: '🎬', description: 'Community video platform', domain: 'rtube.online' },
{ id: 'swag', name: 'rSwag', badge: 'rSw', color: 'bg-red-200', emoji: '👕', description: 'Community merch & swag store', domain: 'rswag.online' },
// Planning
{ id: 'cal', name: 'rCal', badge: 'rC', color: 'bg-sky-300', emoji: '📅', description: 'Collaborative scheduling & events', domain: 'rcal.online' },
{ id: 'trips', name: 'rTrips', badge: 'rT', color: 'bg-emerald-300', emoji: '✈️', description: 'Group travel planning in real time', domain: 'rtrips.online' },
@ -34,13 +36,12 @@ const MODULES: AppModule[] = [
{ id: 'wallet', name: 'rWallet', badge: 'rW', color: 'bg-yellow-300', emoji: '💰', description: 'Multi-chain crypto wallet', domain: 'rwallet.online' },
{ id: 'cart', name: 'rCart', badge: 'rCt', color: 'bg-orange-300', emoji: '🛒', description: 'Group commerce & shared shopping', domain: 'rcart.online' },
{ id: 'auctions', name: 'rAuctions', badge: 'rA', color: 'bg-red-300', emoji: '🔨', description: 'Live auction platform', domain: 'rauctions.online' },
{ id: 'swag', name: 'rSwag', badge: 'rSw', color: 'bg-red-200', emoji: '👕', description: 'Community merch & swag store', domain: 'rswag.online' },
// Social & Media
{ id: 'photos', name: 'rPhotos', badge: 'rPh', color: 'bg-pink-200', emoji: '📸', description: 'Shared community photo albums', domain: 'rphotos.online' },
{ id: 'tube', name: 'rTube', badge: 'rTu', color: 'bg-pink-300', emoji: '🎬', description: 'Group video platform', domain: 'rtube.online' },
// Sharing
{ id: 'photos', name: 'rPhotos', badge: 'rPh', color: 'bg-pink-200', emoji: '📸', description: 'Community photo commons', domain: 'rphotos.online' },
{ id: 'network', name: 'rNetwork', badge: 'rNe', color: 'bg-blue-300', emoji: '🕸️', description: 'Community network & social graph', domain: 'rnetwork.online' },
{ id: 'socials', name: 'rSocials', badge: 'rSo', color: 'bg-sky-200', emoji: '📢', description: 'Social media management', domain: 'rsocials.online' },
{ id: 'files', name: 'rFiles', badge: 'rFi', color: 'bg-cyan-300', emoji: '📁', description: 'Collaborative file storage', domain: 'rfiles.online' },
{ id: 'socials', name: 'rSocials', badge: 'rSo', color: 'bg-sky-200', emoji: '📢', description: 'Social media management', domain: 'rsocials.online' },
// Observing
{ id: 'data', name: 'rData', badge: 'rD', color: 'bg-purple-300', emoji: '📊', description: 'Analytics & insights dashboard', domain: 'rdata.online' },
// Work & Productivity
{ id: 'work', name: 'rWork', badge: 'rWo', color: 'bg-slate-300', emoji: '📋', description: 'Project & task management', domain: 'rwork.online' },
@ -53,6 +54,8 @@ const MODULE_CATEGORIES: Record<string, string> = {
space: 'Creating',
notes: 'Creating',
pubs: 'Creating',
tube: 'Creating',
swag: 'Creating',
cal: 'Planning',
trips: 'Planning',
maps: 'Planning',
@ -66,13 +69,11 @@ const MODULE_CATEGORIES: Record<string, string> = {
wallet: 'Funding & Commerce',
cart: 'Funding & Commerce',
auctions: 'Funding & Commerce',
swag: 'Funding & Commerce',
photos: 'Social & Media',
tube: 'Social & Media',
network: 'Social & Media',
socials: 'Social & Media',
files: 'Social & Media',
data: 'Social & Media',
photos: 'Sharing',
network: 'Sharing',
files: 'Sharing',
socials: 'Sharing',
data: 'Observing',
work: 'Work & Productivity',
ids: 'Identity & Infrastructure',
stack: 'Identity & Infrastructure',
@ -84,7 +85,8 @@ const CATEGORY_ORDER = [
'Communicating',
'Deciding',
'Funding & Commerce',
'Social & Media',
'Sharing',
'Observing',
'Work & Productivity',
'Identity & Infrastructure',
];

View File

@ -1,30 +1,39 @@
'use client';
const FOOTER_LINKS = [
// Creating
{ name: 'rSpace', href: 'https://rspace.online' },
{ name: 'rNotes', href: 'https://rnotes.online' },
{ name: 'rPubs', href: 'https://rpubs.online' },
{ name: 'rTube', href: 'https://rtube.online' },
{ name: 'rSwag', href: 'https://rswag.online' },
// Planning
{ name: 'rCal', href: 'https://rcal.online' },
{ name: 'rTrips', href: 'https://rtrips.online' },
{ name: 'rMaps', href: 'https://rmaps.online' },
// Communicating
{ name: 'rChats', href: 'https://rchats.online' },
{ name: 'rInbox', href: 'https://rinbox.online' },
{ name: 'rMail', href: 'https://rmail.online' },
{ name: 'rForum', href: 'https://rforum.online' },
// Deciding
{ name: 'rChoices', href: 'https://rchoices.online' },
{ name: 'rVote', href: 'https://rvote.online' },
// Funding & Commerce
{ name: 'rFunds', href: 'https://rfunds.online' },
{ name: 'rWallet', href: 'https://rwallet.online' },
{ name: 'rCart', href: 'https://rcart.online' },
{ name: 'rAuctions', href: 'https://rauctions.online' },
{ name: 'rSwag', href: 'https://rswag.online' },
// Sharing
{ name: 'rPhotos', href: 'https://rphotos.online' },
{ name: 'rTube', href: 'https://rtube.online' },
{ name: 'rNetwork', href: 'https://rnetwork.online' },
{ name: 'rSocials', href: 'https://rsocials.online' },
{ name: 'rFiles', href: 'https://rfiles.online' },
{ name: 'rSocials', href: 'https://rsocials.online' },
// Observing
{ name: 'rData', href: 'https://rdata.online' },
// Work & Productivity
{ name: 'rWork', href: 'https://rwork.online' },
// Identity & Infrastructure
{ name: 'rIDs', href: 'https://ridentity.online' },
{ name: 'rStack', href: 'https://rstack.online' },
];

View File

@ -22,7 +22,7 @@ interface HeaderProps {
export function Header({ current = 'notes', breadcrumbs, actions, maxWidth = 'max-w-6xl' }: HeaderProps) {
return (
<nav className="border-b border-slate-800 backdrop-blur-sm bg-[#0a0a0a]/90 sticky top-0 z-50">
<nav className="border-b border-slate-800 backdrop-blur-sm bg-slate-900/85 sticky top-0 z-50">
<div className={`${maxWidth} mx-auto px-4 md:px-6 py-3 flex items-center justify-between gap-2`}>
{/* Left: App switcher + Space switcher + Breadcrumbs */}
<div className="flex items-center gap-1 min-w-0">

View File

@ -47,7 +47,7 @@ export function UserMenu() {
href="https://auth.ridentity.online"
className="px-3 py-1.5 text-sm bg-cyan-500 hover:bg-cyan-400 text-black font-medium rounded-lg transition-colors no-underline"
>
Sign In
🔑 Sign In
</a>
);
}
@ -63,7 +63,7 @@ export function UserMenu() {
<div className="w-7 h-7 rounded-full bg-gradient-to-br from-cyan-400 to-violet-500 flex items-center justify-center text-xs font-bold text-white flex-shrink-0">
{(user.username || 'U')[0].toUpperCase()}
</div>
<span className="text-sm text-slate-300 hidden sm:inline">{displayName}</span>
<span className="text-sm text-slate-300 hidden sm:inline">🔐 {displayName}</span>
<span className="text-[0.7em] text-slate-500 hidden sm:inline">&#9662;</span>
</button>

View File

@ -2,11 +2,12 @@ import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* Middleware to handle subdomain-based space routing.
* Middleware to handle subdomain-based routing.
*
* Routes:
* - rcal.online -> home/landing page
* - www.rcal.online -> home/landing page
* - rcal.online -> landing page (/)
* - www.rcal.online -> landing page (/)
* - demo.rcal.online -> calendar demo (/demo)
* - <space>.rcal.online -> rewrite to /s/<space>
*
* Also handles localhost for development.
@ -17,22 +18,33 @@ export function middleware(request: NextRequest) {
let subdomain: string | null = null;
// Match production: <space>.rcal.online
// Match production: <sub>.rcal.online
const match = hostname.match(/^([a-z0-9][a-z0-9-]*[a-z0-9]|[a-z0-9])\.\w+\.online/);
if (match && match[1] !== 'www') {
subdomain = match[1];
} else if (hostname.includes('localhost')) {
// Development: <space>.localhost:port
// Development: <sub>.localhost:port
const parts = hostname.split('.localhost')[0].split('.');
if (parts.length > 0 && parts[0] !== 'localhost') {
subdomain = parts[parts.length - 1];
}
}
// If we have a subdomain, rewrite root path to space page
if (subdomain && subdomain.length > 0 && url.pathname === '/') {
url.pathname = `/s/${subdomain}`;
return NextResponse.rewrite(url);
if (subdomain && subdomain.length > 0) {
// demo.rcal.online → serve the calendar demo
if (subdomain === 'demo') {
if (url.pathname === '/') {
url.pathname = '/demo';
return NextResponse.rewrite(url);
}
return NextResponse.next();
}
// Other subdomains → space pages
if (url.pathname === '/') {
url.pathname = `/s/${subdomain}`;
return NextResponse.rewrite(url);
}
}
return NextResponse.next();