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

View File

@ -1,132 +1,313 @@
import Link from 'next/link' import Link from 'next/link'
import { Header } from '@/components/Header'
import { EcosystemFooter } from '@/components/EcosystemFooter' import { EcosystemFooter } from '@/components/EcosystemFooter'
export default function LandingPage() { export default function LandingPage() {
return ( return (
<div className="min-h-screen bg-gray-950 text-gray-100"> <div className="min-h-screen bg-[#0b1120] text-[#e2e8f0]">
{/* Hero */} <Header current="cal" />
<section className="px-6 py-20 text-center">
<div className="mx-auto max-w-3xl"> {/* ══ Hero ══════════════════════════════════════ */}
<h1 className="text-4xl sm:text-5xl font-bold leading-tight mb-6"> <section className="text-center pt-28 pb-20 px-6 max-w-[820px] mx-auto">
Group Calendars,{' '} <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">
<span className="bg-gradient-to-r from-blue-400 to-blue-600 bg-clip-text text-transparent"> Relational Calendar
Simplified
</span> </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> </h1>
<p className="text-lg text-gray-400 max-w-xl mx-auto mb-10"> <p className="text-xl text-slate-300 mb-6 leading-relaxed">
One calendar your whole group can see. No more back-and-forth just shared context for when and where to meet. 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> </p>
<div className="flex gap-4 justify-center flex-wrap"> <div className="flex gap-4 justify-center flex-wrap">
<Link <Link
href="/calendar" href="https://demo.rcal.online"
className="px-6 py-3 bg-blue-600 hover:bg-blue-500 rounded-lg font-semibold transition-colors text-white" 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 Try the Demo
</Link> </Link>
<a <a
href="#features" href="#features"
className="px-6 py-3 border border-gray-700 hover:border-gray-500 rounded-lg font-medium transition-colors" 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 Learn More
</a> </a>
</div> </div>
</div>
</section> </section>
{/* Features */} {/* ══ Principles (4-card grid) ══════════════════ */}
<section id="features" className="px-6 py-16 border-t border-gray-800"> <section className="max-w-[1100px] mx-auto px-6 mb-24">
<div className="mx-auto max-w-5xl"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
<h2 className="text-2xl font-bold text-center mb-12"> <div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-8 text-center hover:border-white/[0.12] transition-colors">
A calendar that thinks in space and time <div className="w-12 h-12 mx-auto mb-4 rounded-xl bg-blue-400/[0.12] flex items-center justify-center text-2xl">
</h2> 🤝
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> </div>
<div className="bg-gray-900 border border-gray-800 rounded-xl p-6"> <h3 className="text-[0.95rem] font-semibold text-slate-100 mb-2">Shared by Default</h3>
<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"> <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> </div>
<h3 className="text-lg font-semibold mb-2">Where + When, Together</h3> <h3 className="text-[0.95rem] font-semibold text-slate-100 mb-2">Spatiotemporal</h3>
<p className="text-sm text-gray-400 leading-relaxed"> <p className="text-sm text-slate-500 leading-relaxed">
See events on a calendar and a map side by side. Plan meetups knowing where everyone is, not just when. Events have a where, not just a when. See your schedule on a map and a timeline simultaneously.
</p> </p>
</div> </div>
<div className="bg-gray-900 border border-gray-800 rounded-xl p-6"> <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-10 h-10 rounded-lg bg-blue-600/10 border border-blue-600/20 flex items-center justify-center text-xl mb-4"> <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-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> </div>
<h3 className="text-lg font-semibold mb-2">Moon & Natural Cycles</h3> <h3 className="text-[0.95rem] font-semibold text-slate-100 mb-2">Natural Cycles</h3>
<p className="text-sm text-gray-400 leading-relaxed"> <p className="text-sm text-slate-500 leading-relaxed">
Built-in lunar phase overlay with eclipse detection. Plan around full moons, new moons, and solstices. 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> </p>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
{/* How It Works */} {/* ══ Temporal Zoom Levels ══════════════════════ */}
<section className="px-6 py-16 border-t border-gray-800"> <section className="py-24 px-6">
<div className="mx-auto max-w-3xl"> <div className="max-w-[1100px] mx-auto">
<h2 className="text-2xl font-bold text-center mb-12">How It Works</h2> <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">
<div className="space-y-8"> Temporal Navigation
<div className="flex gap-4"> </span>
<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"> <h2 className="text-3xl sm:text-4xl font-bold text-slate-100 mb-3 tracking-tight leading-[1.2]">
1 Ten levels of time
</div> </h2>
<div> <p className="text-lg text-slate-400 leading-relaxed max-w-[640px] mb-12">
<h3 className="font-semibold mb-1">Add your events</h3> 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 className="text-sm text-gray-400">
Create events with a time and a place. Or import from an existing calendar source.
</p> </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 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> </div>
</div> </div>
</section> </section>
{/* Ecosystem */} {/* ══ Calendar Views ════════════════════════════ */}
<section className="px-6 py-16 border-t border-gray-800"> <section className="py-24 px-6 bg-white/[0.015] border-y border-white/[0.05]">
<div className="mx-auto max-w-3xl"> <div className="max-w-[1100px] mx-auto">
<div className="bg-gray-900 border border-gray-800 rounded-xl p-8 text-center"> <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">
<h2 className="text-xl font-bold mb-3">Works with the r* ecosystem</h2> Four Views
<p className="text-gray-400 mb-6 max-w-md mx-auto"> </span>
rCal embeds into rTrips, rMaps, rNetwork, and more. Any r-tool can display a calendar view through the context API. <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> </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 <Link
href="/calendar" href="https://demo.rcal.online"
className="inline-block px-6 py-3 bg-blue-600 hover:bg-blue-500 rounded-lg font-semibold transition-colors text-white" 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"
> >
Open rCal Open the Demo
</Link> </Link>
<a
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"
>
Explore rStack
</a>
</div> </div>
</div> </div>
</section> </section>

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: '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: '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: '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 // Planning
{ id: 'cal', name: 'rCal', badge: 'rC', color: 'bg-sky-300', emoji: '📅', description: 'Collaborative scheduling & events', domain: 'rcal.online' }, { 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' }, { 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: '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: '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: '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' }, // Sharing
// Social & Media { id: 'photos', name: 'rPhotos', badge: 'rPh', color: 'bg-pink-200', emoji: '📸', description: 'Community photo commons', domain: 'rphotos.online' },
{ 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' },
{ id: 'network', name: 'rNetwork', badge: 'rNe', color: 'bg-blue-300', emoji: '🕸️', description: 'Community network & social graph', domain: 'rnetwork.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: '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' }, { id: 'data', name: 'rData', badge: 'rD', color: 'bg-purple-300', emoji: '📊', description: 'Analytics & insights dashboard', domain: 'rdata.online' },
// Work & Productivity // Work & Productivity
{ id: 'work', name: 'rWork', badge: 'rWo', color: 'bg-slate-300', emoji: '📋', description: 'Project & task management', domain: 'rwork.online' }, { 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', space: 'Creating',
notes: 'Creating', notes: 'Creating',
pubs: 'Creating', pubs: 'Creating',
tube: 'Creating',
swag: 'Creating',
cal: 'Planning', cal: 'Planning',
trips: 'Planning', trips: 'Planning',
maps: 'Planning', maps: 'Planning',
@ -66,13 +69,11 @@ const MODULE_CATEGORIES: Record<string, string> = {
wallet: 'Funding & Commerce', wallet: 'Funding & Commerce',
cart: 'Funding & Commerce', cart: 'Funding & Commerce',
auctions: 'Funding & Commerce', auctions: 'Funding & Commerce',
swag: 'Funding & Commerce', photos: 'Sharing',
photos: 'Social & Media', network: 'Sharing',
tube: 'Social & Media', files: 'Sharing',
network: 'Social & Media', socials: 'Sharing',
socials: 'Social & Media', data: 'Observing',
files: 'Social & Media',
data: 'Social & Media',
work: 'Work & Productivity', work: 'Work & Productivity',
ids: 'Identity & Infrastructure', ids: 'Identity & Infrastructure',
stack: 'Identity & Infrastructure', stack: 'Identity & Infrastructure',
@ -84,7 +85,8 @@ const CATEGORY_ORDER = [
'Communicating', 'Communicating',
'Deciding', 'Deciding',
'Funding & Commerce', 'Funding & Commerce',
'Social & Media', 'Sharing',
'Observing',
'Work & Productivity', 'Work & Productivity',
'Identity & Infrastructure', 'Identity & Infrastructure',
]; ];

View File

@ -1,30 +1,39 @@
'use client'; 'use client';
const FOOTER_LINKS = [ const FOOTER_LINKS = [
// Creating
{ name: 'rSpace', href: 'https://rspace.online' }, { name: 'rSpace', href: 'https://rspace.online' },
{ name: 'rNotes', href: 'https://rnotes.online' }, { name: 'rNotes', href: 'https://rnotes.online' },
{ name: 'rPubs', href: 'https://rpubs.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: 'rCal', href: 'https://rcal.online' },
{ name: 'rTrips', href: 'https://rtrips.online' }, { name: 'rTrips', href: 'https://rtrips.online' },
{ name: 'rMaps', href: 'https://rmaps.online' }, { name: 'rMaps', href: 'https://rmaps.online' },
// Communicating
{ name: 'rChats', href: 'https://rchats.online' }, { name: 'rChats', href: 'https://rchats.online' },
{ name: 'rInbox', href: 'https://rinbox.online' }, { name: 'rInbox', href: 'https://rinbox.online' },
{ name: 'rMail', href: 'https://rmail.online' }, { name: 'rMail', href: 'https://rmail.online' },
{ name: 'rForum', href: 'https://rforum.online' }, { name: 'rForum', href: 'https://rforum.online' },
// Deciding
{ name: 'rChoices', href: 'https://rchoices.online' }, { name: 'rChoices', href: 'https://rchoices.online' },
{ name: 'rVote', href: 'https://rvote.online' }, { name: 'rVote', href: 'https://rvote.online' },
// Funding & Commerce
{ name: 'rFunds', href: 'https://rfunds.online' }, { name: 'rFunds', href: 'https://rfunds.online' },
{ name: 'rWallet', href: 'https://rwallet.online' }, { name: 'rWallet', href: 'https://rwallet.online' },
{ name: 'rCart', href: 'https://rcart.online' }, { name: 'rCart', href: 'https://rcart.online' },
{ name: 'rAuctions', href: 'https://rauctions.online' }, { name: 'rAuctions', href: 'https://rauctions.online' },
{ name: 'rSwag', href: 'https://rswag.online' }, // Sharing
{ name: 'rPhotos', href: 'https://rphotos.online' }, { name: 'rPhotos', href: 'https://rphotos.online' },
{ name: 'rTube', href: 'https://rtube.online' },
{ name: 'rNetwork', href: 'https://rnetwork.online' }, { name: 'rNetwork', href: 'https://rnetwork.online' },
{ name: 'rSocials', href: 'https://rsocials.online' },
{ name: 'rFiles', href: 'https://rfiles.online' }, { name: 'rFiles', href: 'https://rfiles.online' },
{ name: 'rSocials', href: 'https://rsocials.online' },
// Observing
{ name: 'rData', href: 'https://rdata.online' }, { name: 'rData', href: 'https://rdata.online' },
// Work & Productivity
{ name: 'rWork', href: 'https://rwork.online' }, { name: 'rWork', href: 'https://rwork.online' },
// Identity & Infrastructure
{ name: 'rIDs', href: 'https://ridentity.online' }, { name: 'rIDs', href: 'https://ridentity.online' },
{ name: 'rStack', href: 'https://rstack.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) { export function Header({ current = 'notes', breadcrumbs, actions, maxWidth = 'max-w-6xl' }: HeaderProps) {
return ( 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`}> <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 */} {/* Left: App switcher + Space switcher + Breadcrumbs */}
<div className="flex items-center gap-1 min-w-0"> <div className="flex items-center gap-1 min-w-0">

View File

@ -47,7 +47,7 @@ export function UserMenu() {
href="https://auth.ridentity.online" 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" 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> </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"> <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()} {(user.username || 'U')[0].toUpperCase()}
</div> </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> <span className="text-[0.7em] text-slate-500 hidden sm:inline">&#9662;</span>
</button> </button>

View File

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