345 lines
12 KiB
TypeScript
345 lines
12 KiB
TypeScript
/**
|
|
* rCal demo page — server-rendered HTML body.
|
|
*
|
|
* Static July 2026 calendar grid with Alpine Explorer trip events,
|
|
* tab switching (Temporal/Spatial/Lunar/Context), zoom panel,
|
|
* and feature cards. Entirely local state, no WebSocket.
|
|
*/
|
|
|
|
/* ─── Event Data ──────────────────────────────────────────── */
|
|
|
|
interface CalEvent {
|
|
day: number;
|
|
emoji: string;
|
|
label: string;
|
|
color: string;
|
|
bg: string;
|
|
}
|
|
|
|
const TRIP_EVENTS: CalEvent[] = [
|
|
{ day: 6, emoji: "\u2708\uFE0F", label: "Arrive Chamonix", color: "#2dd4bf", bg: "rgba(20,184,166,0.15)" },
|
|
{ day: 7, emoji: "\u{1F97E}", label: "Lac Blanc Hike", color: "#34d399", bg: "rgba(16,185,129,0.15)" },
|
|
{ day: 8, emoji: "\u{1F9D7}", label: "Aiguille du Midi", color: "#fbbf24", bg: "rgba(245,158,11,0.15)" },
|
|
{ day: 9, emoji: "\u{1F97E}", label: "Mer de Glace", color: "#34d399", bg: "rgba(16,185,129,0.15)" },
|
|
{ day: 10, emoji: "\u{1F682}", label: "Train to Zermatt", color: "#22d3ee", bg: "rgba(6,182,212,0.15)" },
|
|
{ day: 11, emoji: "\u{1F97E}", label: "Five Lakes Walk", color: "#34d399", bg: "rgba(16,185,129,0.15)" },
|
|
{ day: 12, emoji: "\u26F7", label: "Glacier Paradise", color: "#fbbf24", bg: "rgba(245,158,11,0.15)" },
|
|
{ day: 13, emoji: "\u{1F3DB}", label: "Alpine Museum", color: "#a78bfa", bg: "rgba(139,92,246,0.15)" },
|
|
{ day: 14, emoji: "\u{1F68C}", label: "Bus to Dolomites", color: "#22d3ee", bg: "rgba(6,182,212,0.15)" },
|
|
{ day: 15, emoji: "\u{1F97E}", label: "Tre Cime Circuit", color: "#34d399", bg: "rgba(16,185,129,0.15)" },
|
|
{ day: 16, emoji: "\u{1FA82}", label: "Paragliding", color: "#fbbf24", bg: "rgba(245,158,11,0.15)" },
|
|
{ day: 17, emoji: "\u{1F6F6}", label: "Lago di Braies", color: "#fbbf24", bg: "rgba(245,158,11,0.15)" },
|
|
{ day: 18, emoji: "\u{1F97E}", label: "Seceda Ridge", color: "#34d399", bg: "rgba(16,185,129,0.15)" },
|
|
{ day: 19, emoji: "\u{1F4F8}", label: "Rest & Photos", color: "#94a3b8", bg: "rgba(100,116,139,0.15)" },
|
|
{ day: 20, emoji: "\u2708\uFE0F", label: "Depart", color: "#2dd4bf", bg: "rgba(20,184,166,0.15)" },
|
|
];
|
|
|
|
const TABS = ["Temporal", "Spatial", "Lunar", "Context"];
|
|
|
|
const ZOOM_LEVELS = [
|
|
"Era", "Century", "Decade", "Year", "Quarter",
|
|
"Month", "Week", "Day", "Hour", "Minute",
|
|
];
|
|
|
|
const FEATURES = [
|
|
{
|
|
icon: "\u{1F50D}",
|
|
title: "Temporal Zoom",
|
|
desc: "Navigate seamlessly from geological eras down to individual minutes. The calendar adapts its grid density and label fidelity at every level.",
|
|
},
|
|
{
|
|
icon: "\u{1F30D}",
|
|
title: "Spatial Context",
|
|
desc: "Events are location-aware. Zoom the map and the calendar filters to show only events within the visible region.",
|
|
},
|
|
{
|
|
icon: "\u{1F319}",
|
|
title: "Lunar Cycles",
|
|
desc: "Overlay moon phases, tidal patterns, and seasonal markers. Useful for agriculture, ceremony, and natural rhythm tracking.",
|
|
},
|
|
{
|
|
icon: "\u{1F4C5}",
|
|
title: "Multi-Calendar",
|
|
desc: "Layer Gregorian, Islamic, Hebrew, Chinese, and custom community calendars. Cross-reference events across time systems.",
|
|
},
|
|
];
|
|
|
|
const LEGEND = [
|
|
{ color: "#2dd4bf", label: "Travel" },
|
|
{ color: "#34d399", label: "Hike" },
|
|
{ color: "#fbbf24", label: "Adventure" },
|
|
{ color: "#22d3ee", label: "Transit" },
|
|
{ color: "#a78bfa", label: "Culture" },
|
|
{ color: "#94a3b8", label: "Rest" },
|
|
];
|
|
|
|
/* ─── Helpers ─────────────────────────────────────────────── */
|
|
|
|
function eventForDay(day: number): CalEvent | undefined {
|
|
return TRIP_EVENTS.find((e) => e.day === day);
|
|
}
|
|
|
|
function isTripDay(day: number): boolean {
|
|
return day >= 6 && day <= 20;
|
|
}
|
|
|
|
/* ─── Render ──────────────────────────────────────────────── */
|
|
|
|
export function renderDemo(): string {
|
|
// July 2026: starts on Wednesday (offset 2 for Mon-based grid), 31 days
|
|
const firstDayOffset = 2; // Mon=0, Tue=1, Wed=2
|
|
const totalDays = 31;
|
|
const dayNames = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
|
|
// Build calendar cells
|
|
const calendarCells: string[] = [];
|
|
|
|
// Empty offset cells
|
|
for (let i = 0; i < firstDayOffset; i++) {
|
|
calendarCells.push(`<div class="rcal-cell rcal-cell--empty"></div>`);
|
|
}
|
|
|
|
// Day cells
|
|
for (let d = 1; d <= totalDays; d++) {
|
|
const ev = eventForDay(d);
|
|
const trip = isTripDay(d);
|
|
const todayClass = d === 15 ? " rcal-cell--today" : "";
|
|
const tripClass = trip ? " rcal-cell--trip" : "";
|
|
|
|
let pill = "";
|
|
if (ev) {
|
|
pill = `<div class="rcal-pill" style="background:${ev.bg};color:${ev.color};border:1px solid ${ev.color}22;">
|
|
<span class="rcal-pill__emoji">${ev.emoji}</span>
|
|
<span class="rcal-pill__label">${ev.label}</span>
|
|
</div>`;
|
|
}
|
|
|
|
calendarCells.push(`<div class="rcal-cell${tripClass}${todayClass}">
|
|
<span class="rcal-cell__num${trip ? " rcal-cell__num--trip" : ""}">${d}</span>
|
|
${pill}
|
|
</div>`);
|
|
}
|
|
|
|
return `
|
|
<div class="rd-root" style="--rd-accent-from:#6366f1; --rd-accent-to:#a78bfa;">
|
|
|
|
<!-- ── Hero ── -->
|
|
<section class="rd-hero">
|
|
<div style="display:inline-block;padding:0.375rem 1rem;background:rgba(99,102,241,0.1);border:1px solid rgba(99,102,241,0.2);border-radius:9999px;font-size:0.875rem;color:#a5b4fc;font-weight:500;margin-bottom:1.5rem;">
|
|
Multi-Dimensional Calendar
|
|
</div>
|
|
<h1>rCal Demo</h1>
|
|
<p class="rd-subtitle">Multi-dimensional calendar with temporal zoom</p>
|
|
<div class="rd-meta">
|
|
<span>\u{1F50D} Temporal Zoom</span>
|
|
<span style="color:#475569">|</span>
|
|
<span>\u{1F30D} Spatial Context</span>
|
|
<span style="color:#475569">|</span>
|
|
<span>\u{1F319} Lunar Cycles</span>
|
|
<span style="color:#475569">|</span>
|
|
<span>\u{1F4C5} Multi-Calendar</span>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── Calendar Section ── -->
|
|
<section class="rd-section rd-section--narrow">
|
|
|
|
<!-- Header bar -->
|
|
<div class="rd-card" style="margin-bottom:0;">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid rgba(51,65,85,0.3);flex-wrap:wrap;gap:0.75rem;">
|
|
<h2 style="font-size:1.25rem;font-weight:700;color:#f1f5f9;margin:0;display:flex;align-items:center;gap:0.5rem;">
|
|
\u{1F4C5} July 2026
|
|
</h2>
|
|
<div style="display:flex;gap:0.25rem;" id="rcal-tabs">
|
|
${TABS.map(
|
|
(tab, i) => `<button data-cal-tab="${tab.toLowerCase()}" style="
|
|
padding:0.375rem 0.875rem;
|
|
border-radius:0.5rem;
|
|
font-size:0.8rem;
|
|
font-weight:500;
|
|
border:none;
|
|
cursor:pointer;
|
|
transition:all 0.15s;
|
|
${i === 0 ? "background:rgba(99,102,241,0.15);color:#818cf8;" : "background:transparent;color:#94a3b8;"}
|
|
">${tab}</button>`,
|
|
).join("\n ")}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Day header row -->
|
|
<div class="rcal-grid rcal-grid--header">
|
|
${dayNames.map((d) => `<div class="rcal-day-header">${d}</div>`).join("\n ")}
|
|
</div>
|
|
|
|
<!-- Calendar grid -->
|
|
<div class="rcal-grid">
|
|
${calendarCells.join("\n ")}
|
|
</div>
|
|
|
|
<!-- Legend -->
|
|
<div style="display:flex;align-items:center;gap:1rem;padding:0.75rem 1.25rem;border-top:1px solid rgba(51,65,85,0.3);flex-wrap:wrap;">
|
|
<span style="font-size:0.75rem;color:#64748b;font-weight:500;">Legend:</span>
|
|
${LEGEND.map(
|
|
(l) => `<span style="display:flex;align-items:center;gap:0.375rem;font-size:0.75rem;color:#94a3b8;">
|
|
<span style="width:0.5rem;height:0.5rem;border-radius:9999px;background:${l.color};display:inline-block;"></span>
|
|
${l.label}
|
|
</span>`,
|
|
).join("\n ")}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── Zoom Panel ── -->
|
|
<section class="rd-section rd-section--narrow">
|
|
<div class="rd-card" style="padding:1.5rem;">
|
|
<h2 style="font-size:1.125rem;font-weight:600;color:#f1f5f9;margin:0 0 1rem;display:flex;align-items:center;gap:0.5rem;">
|
|
\u{1F50D} Temporal Zoom
|
|
</h2>
|
|
<p style="font-size:0.875rem;color:#94a3b8;margin:0 0 1rem;">
|
|
Navigate across temporal granularities. The calendar grid adapts at each zoom level.
|
|
</p>
|
|
<div style="display:flex;flex-wrap:wrap;gap:0.5rem;">
|
|
${ZOOM_LEVELS.map(
|
|
(level) => {
|
|
const isActive = level === "Month";
|
|
return `<div style="
|
|
padding:0.5rem 1rem;
|
|
border-radius:0.5rem;
|
|
font-size:0.8rem;
|
|
font-weight:500;
|
|
border:1px solid ${isActive ? "rgba(99,102,241,0.4)" : "rgba(51,65,85,0.4)"};
|
|
background:${isActive ? "rgba(99,102,241,0.15)" : "rgba(30,41,59,0.5)"};
|
|
color:${isActive ? "#818cf8" : "#64748b"};
|
|
${isActive ? "box-shadow:0 0 12px rgba(99,102,241,0.2);" : ""}
|
|
">${level}${isActive ? " \u25C0" : ""}</div>`;
|
|
},
|
|
).join("\n ")}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── Features ── -->
|
|
<section class="rd-section">
|
|
<div class="rd-grid rd-grid--2">
|
|
${FEATURES.map(
|
|
(f) => `
|
|
<div class="rd-card" style="padding:1.5rem;">
|
|
<div style="font-size:1.75rem;margin-bottom:0.75rem;">${f.icon}</div>
|
|
<h3 style="font-size:1rem;font-weight:600;color:#e2e8f0;margin:0 0 0.5rem;">${f.title}</h3>
|
|
<p style="font-size:0.875rem;color:#94a3b8;margin:0;line-height:1.5;">${f.desc}</p>
|
|
</div>`,
|
|
).join("")}
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── CTA ── -->
|
|
<section class="rd-section rd-section--narrow">
|
|
<div class="rd-cta">
|
|
<h2>Coordinate in Time & Space</h2>
|
|
<p>
|
|
rCal layers temporal zoom, spatial context, and lunar cycles into a single calendar.
|
|
Plan events that respect natural rhythms and local conditions.
|
|
</p>
|
|
<a href="/create-space" style="background:linear-gradient(135deg,#6366f1,#a78bfa);box-shadow:0 8px 24px rgba(99,102,241,0.25);">
|
|
Create Your Space
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|
|
|
|
<style>
|
|
/* ── rCal demo grid ── */
|
|
.rcal-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(7, 1fr);
|
|
gap: 1px;
|
|
background: rgba(51,65,85,0.2);
|
|
padding: 0 1px 1px;
|
|
}
|
|
.rcal-grid--header {
|
|
gap: 0;
|
|
padding: 0;
|
|
background: transparent;
|
|
border-bottom: 1px solid rgba(51,65,85,0.3);
|
|
}
|
|
.rcal-day-header {
|
|
padding: 0.5rem 0;
|
|
text-align: center;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: #64748b;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
.rcal-cell {
|
|
min-height: 3.5rem;
|
|
padding: 0.375rem;
|
|
background: rgba(15,23,42,0.6);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
transition: background 0.15s;
|
|
}
|
|
.rcal-cell:hover {
|
|
background: rgba(30,41,59,0.8);
|
|
}
|
|
.rcal-cell--empty {
|
|
background: rgba(15,23,42,0.3);
|
|
}
|
|
.rcal-cell--trip {
|
|
background: rgba(99,102,241,0.04);
|
|
}
|
|
.rcal-cell--today {
|
|
outline: 2px solid rgba(99,102,241,0.5);
|
|
outline-offset: -2px;
|
|
background: rgba(99,102,241,0.08);
|
|
}
|
|
.rcal-cell__num {
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
color: #64748b;
|
|
line-height: 1;
|
|
}
|
|
.rcal-cell__num--trip {
|
|
color: #a5b4fc;
|
|
font-weight: 600;
|
|
}
|
|
.rcal-pill {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
padding: 0.125rem 0.375rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.625rem;
|
|
line-height: 1.2;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
}
|
|
.rcal-pill__emoji {
|
|
flex-shrink: 0;
|
|
font-size: 0.7rem;
|
|
}
|
|
.rcal-pill__label {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Responsive: stack pill text on small screens */
|
|
@media (max-width: 640px) {
|
|
.rcal-cell {
|
|
min-height: 2.75rem;
|
|
padding: 0.25rem;
|
|
}
|
|
.rcal-pill__label {
|
|
display: none;
|
|
}
|
|
.rcal-pill {
|
|
justify-content: center;
|
|
padding: 0.125rem;
|
|
}
|
|
}
|
|
</style>`;
|
|
}
|