-
- Group Calendars,{' '}
-
- Simplified
-
-
-
- One calendar your whole group can see. No more back-and-forth — just shared context for when and where to meet.
+
+
+
+ {/* ══ Hero ══════════════════════════════════════ */}
+
+
+ Relational Calendar
+
+
+ Time is shared. Your calendar should be too.
+
+
+ A collaborative calendar for communities, cooperatives, and coordinated groups.
+
+
+ rCal rethinks the calendar as a shared, spatial, and cyclical 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.
+
+
+
+
+ {/* ══ Principles (4-card grid) ══════════════════ */}
+
+
+
+
+ 🤝
+
+
Shared by Default
+
+ One calendar for the whole group. Everyone sees the same context — no more fragmented schedules.
+
+
+
+
+ 🗺
+
+
Spatiotemporal
+
+ Events have a where, not just a when. See your schedule on a map and a timeline simultaneously.
+
+
+
+
+ 🌙
+
+
Natural Cycles
+
+ Lunar phases, eclipses, and solstices built in. Reconnect your planning to the rhythms of the natural world.
+
+
+
+
+ 🔭
+
+
Multi-Scale Zoom
+
+ Ten levels of time — from a 30-second moment to a cosmic era. See today or plan a decade ahead.
+
+
+
+
+
+ {/* ══ Why rCal (alt section) ════════════════════ */}
+
+
+
+ Why rCal?
+
+
+ Calendars were never meant to be personal silos
+
+
+ Mainstream calendars treat time as private property. rCal treats it as a commons — something groups navigate together. Here's what makes it different.
+
+
+
+
📍
+
Where + When, Together
+
+ Every event lives on both a timeline and a map. rCal's split view lets you see where everyone is meeting and when — with nine spatial zoom levels from planet to street address.
+
+
+
+
🔗
+
Coupled Zoom
+
+ 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.
+
+
+
+
📡
+
Multi-Source Sync
+
+ Import from Google, Outlook, Apple, CalDAV, ICS feeds, and Obsidian. Layer multiple sources with per-source color coding and visibility controls.
+
+
+
+
🌑
+
Lunar Overlay
+
+ Eight moon phases rendered on every calendar view with illumination percentages and eclipse detection. Plan gatherings, gardens, and ceremonies around natural cycles.
+
+
+
+
🧩
+
r* Ecosystem Embeds
+
+ rTrips, rMaps, rNetwork, rCart, and rNotes can all embed a calendar view through the context API. One calendar, surfaced everywhere it's needed.
+
+
+
+
🏠
+
Self-Hosted & Sovereign
+
+ Open source and Dockerized. Your events live on your infrastructure — not in a corporate cloud. Full data sovereignty with rIDs authentication.
+
+
+
+
+
+
+ {/* ══ Temporal Zoom Levels ══════════════════════ */}
+
+
+
+ Temporal Navigation
+
+
+ Ten levels of time
+
+
+ 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.
+
+
+
+ {[
+ { 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) => (
+
+
{z.level}
+
+ {z.name}
+ {z.span}
+
+
+ ))}
+
+
+
+
+
+ {/* ══ Calendar Views ════════════════════════════ */}
+
+
+
+ Four Views
+
+
+ One calendar, four perspectives
+
+
+ Switch between views with keyboard shortcuts (1-4) to see your events from the angle that matters most right now.
+
+
+
+
+
+ 📅
+
+
+
Temporal
+ Press 1
+
+
+
+ The classic calendar view — month, week, day, year, and season — enhanced with multi-granularity zoom and event indicators.
+
+
+
+
+
+ 🗺
+
+
+
Spatial
+ Press 2
+
+
+
+ Interactive map powered by Leaflet. Events cluster by location with nine spatial granularity levels from planet to GPS coordinates.
+
+
+
+
+
+ 🌙
+
+
+
Lunar
+ Press 3
+
+
+
+ Moon phase overlay with illumination percentages, eclipse detection, and phase-colored day cells. Plan around the eight phases of the lunar cycle.
+
+
+
+
+
+ 🧩
+
+
+
Context
+ Press 4
+
+
+
+ When embedded inside another r* tool, this view shows calendar data filtered for that tool's entity — a trip, a network, a map layer.
+
+
+
+
+
+
+ {/* ══ Ecosystem Integration ═════════════════════ */}
+
+
+
+ Ecosystem
+
+
+ Part of the r* stack
+
+
+ rCal connects to the full suite of community tools. Any r* app can display or create calendar events through the shared context API.
+
+
+ {[
+ { 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) => (
+
+
+ {app.emoji}
+ {app.name}
+
+
{app.desc}
+
+ ))}
+
+
+
+
+ {/* ══ CTA ═══════════════════════════════════════ */}
+
+
+
+ See time differently
+
+
+ Try the spatiotemporal calendar with lunar overlays, multi-source sync, and community sharing.
+ No account needed for the demo.
- {/* Features */}
-
-
-
- A calendar that thinks in space and time
-
-
-
-
- 🗺
-
-
Where + When, Together
-
- See events on a calendar and a map side by side. Plan meetups knowing where everyone is, not just when.
-
-
-
-
- 🔭
-
-
Zoom From Hours to Eras
-
- Ten levels of time. See today's meetings, zoom out to the whole season, or plan years ahead — all in one view.
-
-
-
-
- 🌙
-
-
Moon & Natural Cycles
-
- Built-in lunar phase overlay with eclipse detection. Plan around full moons, new moons, and solstices.
-
-
-
-
-
-
- {/* How It Works */}
-
-
-
How It Works
-
-
-
- 1
-
-
-
Add your events
-
- Create events with a time and a place. Or import from an existing calendar source.
-
-
-
-
-
- 2
-
-
-
Share with your group
-
- Everyone sees the same calendar. Same context, same view.
-
-
-
-
-
- 3
-
-
-
Find the right zoom level
-
- From a single hour to a decade. The calendar adapts to the scale you need.
-
-
-
-
-
-
-
- {/* Ecosystem */}
-
-
-
-
Works with the r* ecosystem
-
- rCal embeds into rTrips, rMaps, rNetwork, and more. Any r-tool can display a calendar view through the context API.
-
-
- Open rCal
-
-
-
-
-
)
diff --git a/src/components/AppSwitcher.tsx b/src/components/AppSwitcher.tsx
index 65a115d..24f2dcc 100644
--- a/src/components/AppSwitcher.tsx
+++ b/src/components/AppSwitcher.tsx
@@ -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
= {
space: 'Creating',
notes: 'Creating',
pubs: 'Creating',
+ tube: 'Creating',
+ swag: 'Creating',
cal: 'Planning',
trips: 'Planning',
maps: 'Planning',
@@ -66,13 +69,11 @@ const MODULE_CATEGORIES: Record = {
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',
];
diff --git a/src/components/EcosystemFooter.tsx b/src/components/EcosystemFooter.tsx
index ac513c6..4228ed1 100644
--- a/src/components/EcosystemFooter.tsx
+++ b/src/components/EcosystemFooter.tsx
@@ -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' },
];
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 0d4599d..cdade23 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -22,7 +22,7 @@ interface HeaderProps {
export function Header({ current = 'notes', breadcrumbs, actions, maxWidth = 'max-w-6xl' }: HeaderProps) {
return (
-
+
{/* Left: App switcher + Space switcher + Breadcrumbs */}
diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx
index e891244..b6e87f0 100644
--- a/src/components/UserMenu.tsx
+++ b/src/components/UserMenu.tsx
@@ -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
);
}
@@ -63,7 +63,7 @@ export function UserMenu() {
{(user.username || 'U')[0].toUpperCase()}
-
{displayName}
+
🔐 {displayName}
▾
diff --git a/src/middleware.ts b/src/middleware.ts
index bb76e70..d5247da 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -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)
* -
.rcal.online -> rewrite to /s/
*
* Also handles localhost for development.
@@ -17,22 +18,33 @@ export function middleware(request: NextRequest) {
let subdomain: string | null = null;
- // Match production: .rcal.online
+ // Match production: .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: .localhost:port
+ // Development: .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();