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 './globals.css'
|
||||
import { Providers } from '@/providers'
|
||||
import { Header } from '@/components/Header'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
|
||||
const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono' })
|
||||
|
|
@ -25,9 +24,8 @@ export default function RootLayout({
|
|||
<head>
|
||||
<script defer src="https://rdata.online/collect.js" data-website-id="ea665b3c-ac4f-40cd-918e-1f99c5c69fac" />
|
||||
</head>
|
||||
<body className="bg-gray-900 min-h-screen text-gray-100">
|
||||
<body className="bg-[#0b1120] min-h-screen text-gray-100">
|
||||
<Providers>
|
||||
<Header current="cal" />
|
||||
{children}
|
||||
</Providers>
|
||||
</body>
|
||||
|
|
|
|||
413
src/app/page.tsx
413
src/app/page.tsx
|
|
@ -1,136 +1,317 @@
|
|||
import Link from 'next/link'
|
||||
import { Header } from '@/components/Header'
|
||||
import { EcosystemFooter } from '@/components/EcosystemFooter'
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 text-gray-100">
|
||||
{/* Hero */}
|
||||
<section className="px-6 py-20 text-center">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<h1 className="text-4xl sm:text-5xl font-bold leading-tight mb-6">
|
||||
Group Calendars,{' '}
|
||||
<span className="bg-gradient-to-r from-blue-400 to-blue-600 bg-clip-text text-transparent">
|
||||
Simplified
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-lg text-gray-400 max-w-xl mx-auto mb-10">
|
||||
One calendar your whole group can see. No more back-and-forth — just shared context for when and where to meet.
|
||||
<div className="min-h-screen bg-[#0b1120] text-[#e2e8f0]">
|
||||
<Header current="cal" />
|
||||
|
||||
{/* ══ Hero ══════════════════════════════════════ */}
|
||||
<section className="text-center pt-28 pb-20 px-6 max-w-[820px] mx-auto">
|
||||
<span className="inline-block text-xs font-bold tracking-[0.15em] uppercase text-blue-400 bg-blue-400/10 border border-blue-400/20 px-4 py-1.5 rounded-full mb-8">
|
||||
Relational Calendar
|
||||
</span>
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold leading-[1.1] tracking-tight mb-4 bg-gradient-to-r from-blue-400 via-indigo-400 to-violet-400 bg-clip-text text-transparent">
|
||||
Time is shared. Your calendar should be too.
|
||||
</h1>
|
||||
<p className="text-xl text-slate-300 mb-6 leading-relaxed">
|
||||
A collaborative calendar for communities, cooperatives, and coordinated groups.
|
||||
</p>
|
||||
<p className="text-base text-slate-500 leading-relaxed max-w-[640px] mx-auto mb-10">
|
||||
rCal rethinks the calendar as a <span className="text-blue-400 font-semibold">shared, spatial, and cyclical</span> tool.
|
||||
See events across time and place, overlay lunar cycles, zoom from a single hour to a whole decade,
|
||||
and keep everyone on the same page — without the back-and-forth.
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center flex-wrap">
|
||||
<Link
|
||||
href="https://demo.rcal.online"
|
||||
className="inline-flex items-center gap-2 px-7 py-3 rounded-lg font-semibold text-[0.95rem] bg-gradient-to-r from-blue-400 to-indigo-500 text-[#0b1120] hover:-translate-y-0.5 transition-transform"
|
||||
>
|
||||
Try the Demo
|
||||
</Link>
|
||||
<a
|
||||
href="#features"
|
||||
className="inline-flex items-center gap-2 px-7 py-3 rounded-lg font-semibold text-[0.95rem] bg-white/[0.06] border border-white/[0.12] text-slate-300 hover:border-white/25 hover:-translate-y-0.5 transition-all"
|
||||
>
|
||||
Learn More
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ══ Principles (4-card grid) ══════════════════ */}
|
||||
<section className="max-w-[1100px] mx-auto px-6 mb-24">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-8 text-center hover:border-white/[0.12] transition-colors">
|
||||
<div className="w-12 h-12 mx-auto mb-4 rounded-xl bg-blue-400/[0.12] flex items-center justify-center text-2xl">
|
||||
🤝
|
||||
</div>
|
||||
<h3 className="text-[0.95rem] font-semibold text-slate-100 mb-2">Shared by Default</h3>
|
||||
<p className="text-sm text-slate-500 leading-relaxed">
|
||||
One calendar for the whole group. Everyone sees the same context — no more fragmented schedules.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-8 text-center hover:border-white/[0.12] transition-colors">
|
||||
<div className="w-12 h-12 mx-auto mb-4 rounded-xl bg-indigo-400/[0.12] flex items-center justify-center text-2xl">
|
||||
🗺
|
||||
</div>
|
||||
<h3 className="text-[0.95rem] font-semibold text-slate-100 mb-2">Spatiotemporal</h3>
|
||||
<p className="text-sm text-slate-500 leading-relaxed">
|
||||
Events have a where, not just a when. See your schedule on a map and a timeline simultaneously.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-8 text-center hover:border-white/[0.12] transition-colors">
|
||||
<div className="w-12 h-12 mx-auto mb-4 rounded-xl bg-violet-400/[0.12] flex items-center justify-center text-2xl">
|
||||
🌙
|
||||
</div>
|
||||
<h3 className="text-[0.95rem] font-semibold text-slate-100 mb-2">Natural Cycles</h3>
|
||||
<p className="text-sm text-slate-500 leading-relaxed">
|
||||
Lunar phases, eclipses, and solstices built in. Reconnect your planning to the rhythms of the natural world.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-8 text-center hover:border-white/[0.12] transition-colors">
|
||||
<div className="w-12 h-12 mx-auto mb-4 rounded-xl bg-emerald-400/[0.12] flex items-center justify-center text-2xl">
|
||||
🔭
|
||||
</div>
|
||||
<h3 className="text-[0.95rem] font-semibold text-slate-100 mb-2">Multi-Scale Zoom</h3>
|
||||
<p className="text-sm text-slate-500 leading-relaxed">
|
||||
Ten levels of time — from a 30-second moment to a cosmic era. See today or plan a decade ahead.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ══ Why rCal (alt section) ════════════════════ */}
|
||||
<section id="features" className="py-24 px-6 bg-white/[0.015] border-y border-white/[0.05]">
|
||||
<div className="max-w-[1100px] mx-auto">
|
||||
<span className="inline-block text-[0.7rem] font-bold tracking-[0.12em] uppercase text-blue-400 bg-blue-400/10 px-3 py-1 rounded-full mb-4">
|
||||
Why rCal?
|
||||
</span>
|
||||
<h2 className="text-3xl sm:text-4xl font-bold text-slate-100 mb-3 tracking-tight leading-[1.2]">
|
||||
Calendars were never meant to be personal silos
|
||||
</h2>
|
||||
<p className="text-lg text-slate-400 leading-relaxed max-w-[640px] mb-12">
|
||||
Mainstream calendars treat time as private property. rCal treats it as a <span className="text-blue-400 font-semibold">commons</span> — something groups navigate together. Here'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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ══ Temporal Zoom Levels ══════════════════════ */}
|
||||
<section className="py-24 px-6">
|
||||
<div className="max-w-[1100px] mx-auto">
|
||||
<span className="inline-block text-[0.7rem] font-bold tracking-[0.12em] uppercase text-indigo-400 bg-indigo-400/10 px-3 py-1 rounded-full mb-4">
|
||||
Temporal Navigation
|
||||
</span>
|
||||
<h2 className="text-3xl sm:text-4xl font-bold text-slate-100 mb-3 tracking-tight leading-[1.2]">
|
||||
Ten levels of time
|
||||
</h2>
|
||||
<p className="text-lg text-slate-400 leading-relaxed max-w-[640px] mb-12">
|
||||
Most calendars show you a month. rCal lets you zoom from a single moment to a cosmic era — each level revealing a different kind of pattern.
|
||||
</p>
|
||||
<div className="bg-white/[0.02] border border-white/[0.06] rounded-2xl p-7 overflow-x-auto">
|
||||
<div className="flex flex-col gap-3 min-w-[500px]">
|
||||
{[
|
||||
{ level: 0, name: 'Moment', span: '30 seconds', color: 'bg-blue-500', width: 'w-[4%]' },
|
||||
{ level: 1, name: 'Hour', span: '60 minutes', color: 'bg-blue-400', width: 'w-[8%]' },
|
||||
{ level: 2, name: 'Day', span: '24 hours', color: 'bg-blue-400', width: 'w-[14%]' },
|
||||
{ level: 3, name: 'Week', span: '7 days', color: 'bg-indigo-400', width: 'w-[22%]' },
|
||||
{ level: 4, name: 'Month', span: '~30 days', color: 'bg-indigo-400', width: 'w-[32%]' },
|
||||
{ level: 5, name: 'Season', span: '~3 months', color: 'bg-violet-400', width: 'w-[44%]' },
|
||||
{ level: 6, name: 'Year', span: '365 days', color: 'bg-violet-400', width: 'w-[58%]' },
|
||||
{ level: 7, name: 'Decade', span: '10 years', color: 'bg-purple-400', width: 'w-[72%]' },
|
||||
{ level: 8, name: 'Century', span: '100 years', color: 'bg-purple-500', width: 'w-[86%]' },
|
||||
{ level: 9, name: 'Cosmic', span: 'Geological', color: 'bg-purple-600', width: 'w-full' },
|
||||
].map((z) => (
|
||||
<div key={z.level} className="flex items-center gap-4">
|
||||
<span className="text-xs text-slate-500 w-6 text-right font-mono">{z.level}</span>
|
||||
<div className={`${z.width} h-6 ${z.color}/20 rounded-md flex items-center px-3`}>
|
||||
<span className="text-xs font-semibold text-slate-200 whitespace-nowrap">{z.name}</span>
|
||||
<span className="text-[0.65rem] text-slate-500 ml-auto whitespace-nowrap">{z.span}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ══ Calendar Views ════════════════════════════ */}
|
||||
<section className="py-24 px-6 bg-white/[0.015] border-y border-white/[0.05]">
|
||||
<div className="max-w-[1100px] mx-auto">
|
||||
<span className="inline-block text-[0.7rem] font-bold tracking-[0.12em] uppercase text-violet-400 bg-violet-400/10 px-3 py-1 rounded-full mb-4">
|
||||
Four Views
|
||||
</span>
|
||||
<h2 className="text-3xl sm:text-4xl font-bold text-slate-100 mb-3 tracking-tight leading-[1.2]">
|
||||
One calendar, four perspectives
|
||||
</h2>
|
||||
<p className="text-lg text-slate-400 leading-relaxed max-w-[640px] mb-12">
|
||||
Switch between views with keyboard shortcuts (1-4) to see your events from the angle that matters most right now.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-7 hover:border-white/[0.12] transition-colors">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-blue-500/[0.12] flex items-center justify-center text-lg">
|
||||
📅
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-slate-100">Temporal</h3>
|
||||
<span className="text-xs text-slate-500 font-mono">Press 1</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-400 leading-relaxed">
|
||||
The classic calendar view — month, week, day, year, and season — enhanced with multi-granularity zoom and event indicators.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-7 hover:border-white/[0.12] transition-colors">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-500/[0.12] flex items-center justify-center text-lg">
|
||||
🗺
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-slate-100">Spatial</h3>
|
||||
<span className="text-xs text-slate-500 font-mono">Press 2</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-400 leading-relaxed">
|
||||
Interactive map powered by Leaflet. Events cluster by location with nine spatial granularity levels from planet to GPS coordinates.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-7 hover:border-white/[0.12] transition-colors">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-amber-500/[0.12] flex items-center justify-center text-lg">
|
||||
🌙
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-slate-100">Lunar</h3>
|
||||
<span className="text-xs text-slate-500 font-mono">Press 3</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-400 leading-relaxed">
|
||||
Moon phase overlay with illumination percentages, eclipse detection, and phase-colored day cells. Plan around the eight phases of the lunar cycle.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-7 hover:border-white/[0.12] transition-colors">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-cyan-500/[0.12] flex items-center justify-center text-lg">
|
||||
🧩
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-slate-100">Context</h3>
|
||||
<span className="text-xs text-slate-500 font-mono">Press 4</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-400 leading-relaxed">
|
||||
When embedded inside another r* tool, this view shows calendar data filtered for that tool's entity — a trip, a network, a map layer.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ══ Ecosystem Integration ═════════════════════ */}
|
||||
<section className="py-24 px-6">
|
||||
<div className="max-w-[1100px] mx-auto">
|
||||
<span className="inline-block text-[0.7rem] font-bold tracking-[0.12em] uppercase text-emerald-400 bg-emerald-400/10 px-3 py-1 rounded-full mb-4">
|
||||
Ecosystem
|
||||
</span>
|
||||
<h2 className="text-3xl sm:text-4xl font-bold text-slate-100 mb-3 tracking-tight leading-[1.2]">
|
||||
Part of the r* stack
|
||||
</h2>
|
||||
<p className="text-lg text-slate-400 leading-relaxed max-w-[640px] mb-12">
|
||||
rCal connects to the full suite of community tools. Any r* app can display or create calendar events through the shared context API.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{[
|
||||
{ emoji: '🗺', name: 'rTrips', desc: 'Trip itineraries auto-populate with calendar events for departure, accommodation, and activities.' },
|
||||
{ emoji: '📍', name: 'rMaps', desc: 'Location-tagged events appear on shared community maps with time-filtered layers.' },
|
||||
{ emoji: '👥', name: 'rNetwork', desc: 'See when your community members are available and schedule group meetings.' },
|
||||
{ emoji: '🛒', name: 'rCart', desc: 'Product launches, market days, and delivery windows sync to your calendar.' },
|
||||
{ emoji: '📝', name: 'rNotes', desc: 'Meeting notes link back to calendar events. Transcriptions attach to the moment they happened.' },
|
||||
{ emoji: '🏠', name: 'rSpace', desc: 'Each space gets its own calendar. Subdomain routing means each community has a dedicated view.' },
|
||||
].map((app) => (
|
||||
<div key={app.name} className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-6 hover:border-emerald-400/30 hover:-translate-y-0.5 transition-all">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="text-2xl">{app.emoji}</span>
|
||||
<span className="text-base font-bold text-slate-100">{app.name}</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-400 leading-relaxed">{app.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ══ CTA ═══════════════════════════════════════ */}
|
||||
<section className="py-20 px-6 bg-white/[0.015] border-y border-white/[0.05]">
|
||||
<div className="max-w-[640px] mx-auto text-center">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-slate-100 mb-4">
|
||||
See time differently
|
||||
</h2>
|
||||
<p className="text-base text-slate-400 mb-8 leading-relaxed">
|
||||
Try the spatiotemporal calendar with lunar overlays, multi-source sync, and community sharing.
|
||||
No account needed for the demo.
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center flex-wrap">
|
||||
<Link
|
||||
href="/calendar"
|
||||
className="px-6 py-3 bg-blue-600 hover:bg-blue-500 rounded-lg font-semibold transition-colors text-white"
|
||||
href="https://demo.rcal.online"
|
||||
className="inline-flex items-center gap-2 px-7 py-3 rounded-lg font-semibold text-[0.95rem] bg-gradient-to-r from-blue-400 to-indigo-500 text-[#0b1120] hover:-translate-y-0.5 transition-transform"
|
||||
>
|
||||
Try the Demo
|
||||
Open the Demo
|
||||
</Link>
|
||||
<a
|
||||
href="#features"
|
||||
className="px-6 py-3 border border-gray-700 hover:border-gray-500 rounded-lg font-medium transition-colors"
|
||||
href="https://rstack.online"
|
||||
className="inline-flex items-center gap-2 px-7 py-3 rounded-lg font-semibold text-[0.95rem] bg-white/[0.06] border border-white/[0.12] text-slate-300 hover:border-white/25 hover:-translate-y-0.5 transition-all"
|
||||
>
|
||||
Learn More
|
||||
Explore rStack
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features */}
|
||||
<section id="features" className="px-6 py-16 border-t border-gray-800">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<h2 className="text-2xl font-bold text-center mb-12">
|
||||
A calendar that thinks in space and time
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-600/10 border border-blue-600/20 flex items-center justify-center text-xl mb-4">
|
||||
🗺
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">Where + When, Together</h3>
|
||||
<p className="text-sm text-gray-400 leading-relaxed">
|
||||
See events on a calendar and a map side by side. Plan meetups knowing where everyone is, not just when.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-600/10 border border-blue-600/20 flex items-center justify-center text-xl mb-4">
|
||||
🔭
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">Zoom From Hours to Eras</h3>
|
||||
<p className="text-sm text-gray-400 leading-relaxed">
|
||||
Ten levels of time. See today's meetings, zoom out to the whole season, or plan years ahead — all in one view.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-600/10 border border-blue-600/20 flex items-center justify-center text-xl mb-4">
|
||||
🌙
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">Moon & Natural Cycles</h3>
|
||||
<p className="text-sm text-gray-400 leading-relaxed">
|
||||
Built-in lunar phase overlay with eclipse detection. Plan around full moons, new moons, and solstices.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How It Works */}
|
||||
<section className="px-6 py-16 border-t border-gray-800">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<h2 className="text-2xl font-bold text-center mb-12">How It Works</h2>
|
||||
<div className="space-y-8">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-sm font-bold text-white">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold mb-1">Add your events</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
Create events with a time and a place. Or import from an existing calendar source.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-sm font-bold text-white">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold mb-1">Share with your group</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
Everyone sees the same calendar. Same context, same view.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-sm font-bold text-white">
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold mb-1">Find the right zoom level</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
From a single hour to a decade. The calendar adapts to the scale you need.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Ecosystem */}
|
||||
<section className="px-6 py-16 border-t border-gray-800">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-8 text-center">
|
||||
<h2 className="text-xl font-bold mb-3">Works with the r* ecosystem</h2>
|
||||
<p className="text-gray-400 mb-6 max-w-md mx-auto">
|
||||
rCal embeds into rTrips, rMaps, rNetwork, and more. Any r-tool can display a calendar view through the context API.
|
||||
</p>
|
||||
<Link
|
||||
href="/calendar"
|
||||
className="inline-block px-6 py-3 bg-blue-600 hover:bg-blue-500 rounded-lg font-semibold transition-colors text-white"
|
||||
>
|
||||
Open rCal
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<EcosystemFooter current="rCal" />
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ const MODULES: AppModule[] = [
|
|||
{ id: 'space', name: 'rSpace', badge: 'rS', color: 'bg-teal-300', emoji: '🎨', description: 'Real-time collaborative canvas', domain: 'rspace.online' },
|
||||
{ id: 'notes', name: 'rNotes', badge: 'rN', color: 'bg-amber-300', emoji: '📝', description: 'Group note-taking & knowledge capture', domain: 'rnotes.online' },
|
||||
{ id: 'pubs', name: 'rPubs', badge: 'rP', color: 'bg-rose-300', emoji: '📖', description: 'Collaborative publishing platform', domain: 'rpubs.online' },
|
||||
{ id: 'tube', name: 'rTube', badge: 'rTu', color: 'bg-pink-300', emoji: '🎬', description: 'Community video platform', domain: 'rtube.online' },
|
||||
{ id: 'swag', name: 'rSwag', badge: 'rSw', color: 'bg-red-200', emoji: '👕', description: 'Community merch & swag store', domain: 'rswag.online' },
|
||||
// Planning
|
||||
{ id: 'cal', name: 'rCal', badge: 'rC', color: 'bg-sky-300', emoji: '📅', description: 'Collaborative scheduling & events', domain: 'rcal.online' },
|
||||
{ id: 'trips', name: 'rTrips', badge: 'rT', color: 'bg-emerald-300', emoji: '✈️', description: 'Group travel planning in real time', domain: 'rtrips.online' },
|
||||
|
|
@ -34,13 +36,12 @@ const MODULES: AppModule[] = [
|
|||
{ id: 'wallet', name: 'rWallet', badge: 'rW', color: 'bg-yellow-300', emoji: '💰', description: 'Multi-chain crypto wallet', domain: 'rwallet.online' },
|
||||
{ id: 'cart', name: 'rCart', badge: 'rCt', color: 'bg-orange-300', emoji: '🛒', description: 'Group commerce & shared shopping', domain: 'rcart.online' },
|
||||
{ id: 'auctions', name: 'rAuctions', badge: 'rA', color: 'bg-red-300', emoji: '🔨', description: 'Live auction platform', domain: 'rauctions.online' },
|
||||
{ id: 'swag', name: 'rSwag', badge: 'rSw', color: 'bg-red-200', emoji: '👕', description: 'Community merch & swag store', domain: 'rswag.online' },
|
||||
// Social & Media
|
||||
{ id: 'photos', name: 'rPhotos', badge: 'rPh', color: 'bg-pink-200', emoji: '📸', description: 'Shared community photo albums', domain: 'rphotos.online' },
|
||||
{ id: 'tube', name: 'rTube', badge: 'rTu', color: 'bg-pink-300', emoji: '🎬', description: 'Group video platform', domain: 'rtube.online' },
|
||||
// Sharing
|
||||
{ id: 'photos', name: 'rPhotos', badge: 'rPh', color: 'bg-pink-200', emoji: '📸', description: 'Community photo commons', domain: 'rphotos.online' },
|
||||
{ id: 'network', name: 'rNetwork', badge: 'rNe', color: 'bg-blue-300', emoji: '🕸️', description: 'Community network & social graph', domain: 'rnetwork.online' },
|
||||
{ id: 'socials', name: 'rSocials', badge: 'rSo', color: 'bg-sky-200', emoji: '📢', description: 'Social media management', domain: 'rsocials.online' },
|
||||
{ id: 'files', name: 'rFiles', badge: 'rFi', color: 'bg-cyan-300', emoji: '📁', description: 'Collaborative file storage', domain: 'rfiles.online' },
|
||||
{ id: 'socials', name: 'rSocials', badge: 'rSo', color: 'bg-sky-200', emoji: '📢', description: 'Social media management', domain: 'rsocials.online' },
|
||||
// Observing
|
||||
{ id: 'data', name: 'rData', badge: 'rD', color: 'bg-purple-300', emoji: '📊', description: 'Analytics & insights dashboard', domain: 'rdata.online' },
|
||||
// Work & Productivity
|
||||
{ id: 'work', name: 'rWork', badge: 'rWo', color: 'bg-slate-300', emoji: '📋', description: 'Project & task management', domain: 'rwork.online' },
|
||||
|
|
@ -53,6 +54,8 @@ const MODULE_CATEGORIES: Record<string, string> = {
|
|||
space: 'Creating',
|
||||
notes: 'Creating',
|
||||
pubs: 'Creating',
|
||||
tube: 'Creating',
|
||||
swag: 'Creating',
|
||||
cal: 'Planning',
|
||||
trips: 'Planning',
|
||||
maps: 'Planning',
|
||||
|
|
@ -66,13 +69,11 @@ const MODULE_CATEGORIES: Record<string, string> = {
|
|||
wallet: 'Funding & Commerce',
|
||||
cart: 'Funding & Commerce',
|
||||
auctions: 'Funding & Commerce',
|
||||
swag: 'Funding & Commerce',
|
||||
photos: 'Social & Media',
|
||||
tube: 'Social & Media',
|
||||
network: 'Social & Media',
|
||||
socials: 'Social & Media',
|
||||
files: 'Social & Media',
|
||||
data: 'Social & Media',
|
||||
photos: 'Sharing',
|
||||
network: 'Sharing',
|
||||
files: 'Sharing',
|
||||
socials: 'Sharing',
|
||||
data: 'Observing',
|
||||
work: 'Work & Productivity',
|
||||
ids: 'Identity & Infrastructure',
|
||||
stack: 'Identity & Infrastructure',
|
||||
|
|
@ -84,7 +85,8 @@ const CATEGORY_ORDER = [
|
|||
'Communicating',
|
||||
'Deciding',
|
||||
'Funding & Commerce',
|
||||
'Social & Media',
|
||||
'Sharing',
|
||||
'Observing',
|
||||
'Work & Productivity',
|
||||
'Identity & Infrastructure',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ interface HeaderProps {
|
|||
|
||||
export function Header({ current = 'notes', breadcrumbs, actions, maxWidth = 'max-w-6xl' }: HeaderProps) {
|
||||
return (
|
||||
<nav className="border-b border-slate-800 backdrop-blur-sm bg-[#0a0a0a]/90 sticky top-0 z-50">
|
||||
<nav className="border-b border-slate-800 backdrop-blur-sm bg-slate-900/85 sticky top-0 z-50">
|
||||
<div className={`${maxWidth} mx-auto px-4 md:px-6 py-3 flex items-center justify-between gap-2`}>
|
||||
{/* Left: App switcher + Space switcher + Breadcrumbs */}
|
||||
<div className="flex items-center gap-1 min-w-0">
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export function UserMenu() {
|
|||
href="https://auth.ridentity.online"
|
||||
className="px-3 py-1.5 text-sm bg-cyan-500 hover:bg-cyan-400 text-black font-medium rounded-lg transition-colors no-underline"
|
||||
>
|
||||
Sign In
|
||||
🔑 Sign In
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ export function UserMenu() {
|
|||
<div className="w-7 h-7 rounded-full bg-gradient-to-br from-cyan-400 to-violet-500 flex items-center justify-center text-xs font-bold text-white flex-shrink-0">
|
||||
{(user.username || 'U')[0].toUpperCase()}
|
||||
</div>
|
||||
<span className="text-sm text-slate-300 hidden sm:inline">{displayName}</span>
|
||||
<span className="text-sm text-slate-300 hidden sm:inline">🔐 {displayName}</span>
|
||||
<span className="text-[0.7em] text-slate-500 hidden sm:inline">▾</span>
|
||||
</button>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ import { NextResponse } from 'next/server';
|
|||
import type { NextRequest } from 'next/server';
|
||||
|
||||
/**
|
||||
* Middleware to handle subdomain-based space routing.
|
||||
* Middleware to handle subdomain-based routing.
|
||||
*
|
||||
* Routes:
|
||||
* - rcal.online -> home/landing page
|
||||
* - www.rcal.online -> home/landing page
|
||||
* - rcal.online -> landing page (/)
|
||||
* - www.rcal.online -> landing page (/)
|
||||
* - demo.rcal.online -> calendar demo (/demo)
|
||||
* - <space>.rcal.online -> rewrite to /s/<space>
|
||||
*
|
||||
* Also handles localhost for development.
|
||||
|
|
@ -17,22 +18,33 @@ export function middleware(request: NextRequest) {
|
|||
|
||||
let subdomain: string | null = null;
|
||||
|
||||
// Match production: <space>.rcal.online
|
||||
// Match production: <sub>.rcal.online
|
||||
const match = hostname.match(/^([a-z0-9][a-z0-9-]*[a-z0-9]|[a-z0-9])\.\w+\.online/);
|
||||
if (match && match[1] !== 'www') {
|
||||
subdomain = match[1];
|
||||
} else if (hostname.includes('localhost')) {
|
||||
// Development: <space>.localhost:port
|
||||
// Development: <sub>.localhost:port
|
||||
const parts = hostname.split('.localhost')[0].split('.');
|
||||
if (parts.length > 0 && parts[0] !== 'localhost') {
|
||||
subdomain = parts[parts.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a subdomain, rewrite root path to space page
|
||||
if (subdomain && subdomain.length > 0 && url.pathname === '/') {
|
||||
url.pathname = `/s/${subdomain}`;
|
||||
return NextResponse.rewrite(url);
|
||||
if (subdomain && subdomain.length > 0) {
|
||||
// demo.rcal.online → serve the calendar demo
|
||||
if (subdomain === 'demo') {
|
||||
if (url.pathname === '/') {
|
||||
url.pathname = '/demo';
|
||||
return NextResponse.rewrite(url);
|
||||
}
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Other subdomains → space pages
|
||||
if (url.pathname === '/') {
|
||||
url.pathname = `/s/${subdomain}`;
|
||||
return NextResponse.rewrite(url);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
|
|
|
|||
Loading…
Reference in New Issue