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:
parent
cf00a77da2
commit
f46575989f
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Header } from '@/components/Header'
|
||||||
|
|
||||||
|
export default function CalendarLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header current="cal" />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Header } from '@/components/Header'
|
||||||
|
|
||||||
|
export default function ContextLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header current="cal" />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Header } from '@/components/Header'
|
||||||
|
|
||||||
|
export default function DemoLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header current="cal" />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
349
src/app/page.tsx
349
src/app/page.tsx
|
|
@ -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'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'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'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'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>
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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' },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">▾</span>
|
<span className="text-[0.7em] text-slate-500 hidden sm:inline">▾</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue