building out the components, updating the UI in overview and adding a little sqlite viewer
This commit is contained in:
+349
-120
@@ -1,60 +1,116 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { usePersistentState } from "../lib/usePersistentState";
|
||||
|
||||
const MONTHS = [
|
||||
"Hammer", "Alturiak", "Ches", "Tarsakh", "Mirtul", "Kythorn",
|
||||
"Flamerule", "Eleasis", "Eleint", "Marpenoth", "Uktar", "Nightal",
|
||||
// ponytail: the calendar is user-configurable for homebrew campaigns.
|
||||
// `months` carry their own day count so a custom calendar can have variable
|
||||
// month lengths. Default = the Forgotten Realms calendar (12 × 30).
|
||||
interface CalMonth {
|
||||
name: string;
|
||||
days: number;
|
||||
}
|
||||
interface CalConfig {
|
||||
months: CalMonth[];
|
||||
yearLabel: string; // suffix shown after the year, e.g. "DR"
|
||||
}
|
||||
|
||||
const FR_MONTHS: CalMonth[] = [
|
||||
{ name: "Hammer", days: 30 }, { name: "Alturiak", days: 30 }, { name: "Ches", days: 30 },
|
||||
{ name: "Tarsakh", days: 30 }, { name: "Mirtul", days: 30 }, { name: "Kythorn", days: 30 },
|
||||
{ name: "Flamerule", days: 30 }, { name: "Eleasis", days: 30 }, { name: "Eleint", days: 30 },
|
||||
{ name: "Marpenoth", days: 30 }, { name: "Uktar", days: 30 }, { name: "Nightal", days: 30 },
|
||||
];
|
||||
const DEFAULT_CONFIG: CalConfig = { months: FR_MONTHS, yearLabel: "DR" };
|
||||
|
||||
// ponytail: named categories with fixed colors so events are color-coded by
|
||||
// intent, not cycled arbitrarily. A DM picks the category when creating.
|
||||
const CATEGORIES = [
|
||||
{ label: "Festival", color: "#e6cc80" },
|
||||
{ label: "Quest", color: "var(--color-gold-bright)" },
|
||||
{ label: "NPC", color: "var(--color-info)" },
|
||||
{ label: "Political", color: "#a335ee" },
|
||||
{ label: "Combat", color: "var(--color-danger)" },
|
||||
{ label: "Custom", color: "var(--color-success)" },
|
||||
];
|
||||
const catColor = (cat: string) => CATEGORIES.find((c) => c.label === cat)?.color ?? "var(--color-gold-bright)";
|
||||
|
||||
type Recur = "none" | "yearly" | "monthly";
|
||||
|
||||
interface CalendarEvent {
|
||||
day: number;
|
||||
month: number;
|
||||
text: string;
|
||||
color: string;
|
||||
category: string;
|
||||
recur: Recur;
|
||||
}
|
||||
|
||||
const COLORS = ["var(--color-gold-bright)", "var(--color-info)", "var(--color-success)", "var(--color-danger)"];
|
||||
function yearLen(cfg: CalConfig): number {
|
||||
return cfg.months.reduce((s, m) => s + m.days, 0);
|
||||
}
|
||||
function daysInMonth(cfg: CalConfig, m: number): number {
|
||||
return cfg.months[m]?.days ?? 30;
|
||||
}
|
||||
function monthName(cfg: CalConfig, m: number): string {
|
||||
return cfg.months[m]?.name ?? `Month ${m + 1}`;
|
||||
}
|
||||
// absolute day index for a (day,month,year) under a variable-length calendar.
|
||||
function dayIndex(day: number, month: number, year: number, cfg: CalConfig): number {
|
||||
let idx = year * yearLen(cfg);
|
||||
for (let i = 0; i < month; i++) idx += cfg.months[i].days;
|
||||
return idx + (day - 1);
|
||||
}
|
||||
// inverse of dayIndex.
|
||||
function fromIndex(idx: number, cfg: CalConfig): { day: number; month: number; year: number } {
|
||||
const yl = yearLen(cfg);
|
||||
let year = Math.floor(idx / yl);
|
||||
let rem = idx % yl;
|
||||
let month = 0;
|
||||
while (month < cfg.months.length && rem >= cfg.months[month].days) {
|
||||
rem -= cfg.months[month].days;
|
||||
month++;
|
||||
}
|
||||
return { day: rem + 1, month, year };
|
||||
}
|
||||
|
||||
export function CalendarWidget() {
|
||||
const [config, setConfig] = usePersistentState<CalConfig>("calendar.config", DEFAULT_CONFIG);
|
||||
const [month, setMonth] = useState(0);
|
||||
const [year, setYear] = useState(1492);
|
||||
const [events, setEvents] = usePersistentState<CalendarEvent[]>("calendar.events", []);
|
||||
const [selectedDay, setSelectedDay] = useState<number | null>(null);
|
||||
const [newEvent, setNewEvent] = useState("");
|
||||
const [newCategory, setNewCategory] = useState(CATEGORIES[0].label);
|
||||
const [newRecur, setNewRecur] = useState<Recur>("none");
|
||||
const [view, setView] = useState<"month" | "agenda" | "edit">("month");
|
||||
// ponytail: the campaign's current date, persisted so it survives reloads.
|
||||
// Advances with the "Next day" button; "Today" jumps the view to it.
|
||||
const [today, setToday] = usePersistentState<{ day: number; month: number; year: number }>(
|
||||
"calendar.today",
|
||||
{ day: 15, month: 5, year: 1492 },
|
||||
);
|
||||
|
||||
// ponytail: 8 moon phases cycling on a ~30-day lunar cycle. Derived from
|
||||
// a continuous day index so the phase is stable for any date.
|
||||
const MOON_PHASES = ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"];
|
||||
// ponytail: 8 moon phases on a 30-day cycle, derived from the absolute day
|
||||
// index so the phase is stable for any date under any calendar config.
|
||||
function moonPhase(day: number, month: number, year: number): string {
|
||||
const idx = Math.floor(((year * 360 + month * 30 + (day - 1)) % 30) / 30 * 8);
|
||||
return MOON_PHASES[((idx % 8) + 8) % 8];
|
||||
const idx = dayIndex(day, month, year, config);
|
||||
const p = Math.floor(((idx % 30) / 30) * 8);
|
||||
return MOON_PHASES[((p % 8) + 8) % 8];
|
||||
}
|
||||
|
||||
// ponytail: deterministic weather per day (stable for a given date).
|
||||
// A simple PRNG seeded by the date index so the forecast doesn't reshuffle
|
||||
// every render. Coarse by design — a DM can override narratively.
|
||||
const WEATHER = [
|
||||
"☀ Clear", "⛅ Fair", "☁ Overcast", "🌧 Light rain", "⛈ Storm",
|
||||
"❄ Snow", "🌫 Fog", "💨 Windy", "🌀 Unnatural",
|
||||
];
|
||||
function weatherFor(day: number, month: number, year: number): string {
|
||||
const seed = (year * 360 + month * 30 + (day - 1)) % 97;
|
||||
return WEATHER[seed % WEATHER.length];
|
||||
const idx = dayIndex(day, month, year, config);
|
||||
return WEATHER[Math.abs(idx) % WEATHER.length];
|
||||
}
|
||||
|
||||
// Advance the campaign date by one day (30-day months, 12-month year).
|
||||
function nextDay() {
|
||||
setToday((t) => {
|
||||
let { day, month, year } = t;
|
||||
day += 1;
|
||||
if (day > 30) { day = 1; month += 1; }
|
||||
if (month > 11) { month = 0; year += 1; }
|
||||
if (day > daysInMonth(config, month)) { day = 1; month += 1; }
|
||||
if (month >= config.months.length) { month = 0; year += 1; }
|
||||
return { day, month, year };
|
||||
});
|
||||
}
|
||||
@@ -69,7 +125,7 @@ export function CalendarWidget() {
|
||||
if (!newEvent.trim() || selectedDay === null) return;
|
||||
setEvents((prev) => [
|
||||
...prev,
|
||||
{ day: selectedDay, month, text: newEvent.trim(), color: COLORS[events.length % COLORS.length] },
|
||||
{ day: selectedDay, month, text: newEvent.trim(), category: newCategory, recur: newRecur },
|
||||
]);
|
||||
setNewEvent("");
|
||||
}
|
||||
@@ -78,130 +134,303 @@ export function CalendarWidget() {
|
||||
setEvents((prev) => prev.filter((_, i) => i !== idx));
|
||||
}
|
||||
|
||||
const dayEvents = events.filter((e) => e.day === selectedDay && e.month === month);
|
||||
// ponytail: does an event fall on a given (day,month)? Recurring events
|
||||
// match on month (yearly) or every month (monthly). One-off events match
|
||||
// day+month only — we don't store the creation year, so a one-off repeats
|
||||
// visually each year. Acceptable for a DM calendar; store year if it bites.
|
||||
function eventOn(e: CalendarEvent, day: number, m: number): boolean {
|
||||
if (e.recur === "monthly") return e.day === day;
|
||||
if (e.recur === "yearly") return e.day === day && e.month === m;
|
||||
return e.day === day && e.month === m;
|
||||
}
|
||||
|
||||
const dayEvents = useMemo(
|
||||
() => events.filter((e) => e.day === selectedDay && (e.recur === "monthly" || e.month === month)),
|
||||
[events, selectedDay, month],
|
||||
);
|
||||
|
||||
// ponytail: agenda — the next N events from today forward, recurring expanded.
|
||||
const agenda = useMemo(() => {
|
||||
const startIdx = dayIndex(today.day, today.month, today.year, config);
|
||||
const upcoming: { e: CalendarEvent; day: number; month: number; year: number }[] = [];
|
||||
const scan = 2 * yearLen(config); // ~2 years
|
||||
for (let off = 0; off < scan && upcoming.length < 12; off++) {
|
||||
const { day, month: dm, year: dy } = fromIndex(startIdx + off, config);
|
||||
for (const e of events) {
|
||||
if (eventOn(e, day, dm)) upcoming.push({ e, day, month: dm, year: dy });
|
||||
}
|
||||
}
|
||||
return upcoming;
|
||||
}, [events, today, config]);
|
||||
|
||||
const dim = daysInMonth(config, month);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 h-full">
|
||||
{/* Month navigation */}
|
||||
{/* View toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => { if (month === 0) { setMonth(11); setYear(year - 1); } else setMonth(month - 1); }}
|
||||
className="text-[var(--color-text-dim)] hover:text-[var(--color-gold-bright)] cursor-pointer text-sm"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<div className="text-center">
|
||||
<div className="font-heading text-[var(--color-gold-bright)] text-xs font-semibold">
|
||||
{MONTHS[month]}
|
||||
</div>
|
||||
<div className="text-[var(--color-text-dim)] text-[10px]">{year} DR</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { if (month === 11) { setMonth(0); setYear(year + 1); } else setMonth(month + 1); }}
|
||||
className="text-[var(--color-text-dim)] hover:text-[var(--color-gold-bright)] cursor-pointer text-sm"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Campaign date controls */}
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="text-[10px] text-[var(--color-text-secondary)]">
|
||||
Today: {MONTHS[today.month]} {today.day}, {today.year} DR {moonPhase(today.day, today.month, today.year)}
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={jumpToToday}
|
||||
className="rounded bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] text-[var(--color-text-dim)] px-1.5 py-0.5 text-[9px] cursor-pointer hover:text-[var(--color-gold-bright)]"
|
||||
title="Jump the view to today"
|
||||
onClick={() => setView("month")}
|
||||
className={`rounded px-2 py-0.5 text-[10px] cursor-pointer transition-colors ${view === "month" ? "bg-[var(--color-gold-glow)] text-[var(--color-gold-bright)]" : "text-[var(--color-text-dim)] hover:text-[var(--color-text-secondary)]"}`}
|
||||
>
|
||||
Today
|
||||
Month
|
||||
</button>
|
||||
<button
|
||||
onClick={nextDay}
|
||||
className="rounded bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-1.5 py-0.5 text-[9px] font-semibold cursor-pointer hover:bg-[var(--color-gold-muted)]"
|
||||
title="Advance the campaign date by one day"
|
||||
onClick={() => setView("agenda")}
|
||||
className={`rounded px-2 py-0.5 text-[10px] cursor-pointer transition-colors ${view === "agenda" ? "bg-[var(--color-gold-glow)] text-[var(--color-gold-bright)]" : "text-[var(--color-text-dim)] hover:text-[var(--color-text-secondary)]"}`}
|
||||
>
|
||||
Next day →
|
||||
Agenda
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView("edit")}
|
||||
className={`rounded px-2 py-0.5 text-[10px] cursor-pointer transition-colors ${view === "edit" ? "bg-[var(--color-gold-glow)] text-[var(--color-gold-bright)]" : "text-[var(--color-text-dim)] hover:text-[var(--color-text-secondary)]"}`}
|
||||
title="Edit the calendar (months, day counts, year label)"
|
||||
>
|
||||
⚙
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-[10px] text-[var(--color-text-secondary)]">
|
||||
Today: {monthName(config, today.month)} {today.day}, {today.year} {config.yearLabel} {moonPhase(today.day, today.month, today.year)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Day grid */}
|
||||
<div className="grid grid-cols-7 gap-0.5">
|
||||
{["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map((d) => (
|
||||
<div key={d} className="text-[var(--color-text-dim)] text-[9px] text-center font-medium">
|
||||
{d}
|
||||
{view === "edit" ? (
|
||||
<div className="flex-1 overflow-y-auto flex flex-col gap-2">
|
||||
<div className="text-[10px] text-[var(--color-text-dim)]">
|
||||
Edit months, day counts, and the year label. Changes apply immediately.
|
||||
</div>
|
||||
))}
|
||||
{/* Start offset - Faerûn calendar starts on first of month */}
|
||||
{Array.from({ length: 30 }, (_, i) => {
|
||||
const dayNum = i + 1;
|
||||
const isToday = dayNum === today.day && month === today.month && year === today.year;
|
||||
const hasEvent = events.some((e) => e.day === dayNum && e.month === month);
|
||||
const isSelected = dayNum === selectedDay;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-[10px] text-[var(--color-text-secondary)]">Year label</label>
|
||||
<input
|
||||
className="rounded bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-2 py-1 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-[10px] w-20"
|
||||
value={config.yearLabel}
|
||||
onChange={(e) => setConfig({ ...config, yearLabel: e.target.value })}
|
||||
placeholder="DR"
|
||||
/>
|
||||
<span className="text-[10px] text-[var(--color-text-dim)]">→ shown as “{year} {config.yearLabel}”</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{config.months.map((m, i) => (
|
||||
<div key={i} className="flex gap-1 items-center">
|
||||
<span className="text-[9px] text-[var(--color-text-dim)] w-4 font-mono shrink-0">{i + 1}</span>
|
||||
<input
|
||||
className="flex-1 min-w-0 rounded bg-[var(--color-bg-deep)] border border-[var(--color-border-subtle)] px-2 py-1 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-[10px]"
|
||||
value={m.name}
|
||||
onChange={(e) => setConfig({ ...config, months: config.months.map((x, j) => j === i ? { ...x, name: e.target.value } : x) })}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={99}
|
||||
className="w-14 rounded bg-[var(--color-bg-deep)] border border-[var(--color-border-subtle)] px-2 py-1 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-[10px] font-mono"
|
||||
value={m.days}
|
||||
onChange={(e) => setConfig({ ...config, months: config.months.map((x, j) => j === i ? { ...x, days: Math.max(1, parseInt(e.target.value) || 1) } : x) })}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setConfig({ ...config, months: config.months.filter((_, j) => j !== i) })}
|
||||
className="text-[var(--color-text-dim)] hover:text-[var(--color-danger)] text-[10px] cursor-pointer shrink-0"
|
||||
title="Remove month"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
key={dayNum}
|
||||
onClick={() => setSelectedDay(dayNum)}
|
||||
className={`rounded text-[10px] py-1 cursor-pointer transition-colors ${
|
||||
isToday
|
||||
? "bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] font-bold"
|
||||
: isSelected
|
||||
? "bg-[var(--color-bg-card-hover)] text-[var(--color-text-primary)]"
|
||||
: "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-card)]"
|
||||
}`}
|
||||
onClick={() => setConfig({ ...config, months: [...config.months, { name: `Month ${config.months.length + 1}`, days: 30 }] })}
|
||||
className="rounded bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] text-[var(--color-text-dim)] px-2 py-1 text-[10px] hover:text-[var(--color-gold-bright)] hover:border-[var(--color-gold-bright)] cursor-pointer"
|
||||
>
|
||||
{dayNum}
|
||||
{hasEvent && !isToday && (
|
||||
<span className="block w-1 h-1 rounded-full bg-[var(--color-gold-bright)] mx-auto mt-0" />
|
||||
)}
|
||||
<span className="block text-[7px] leading-none opacity-60">{moonPhase(dayNum, month, year)}</span>
|
||||
+ month
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setConfig(DEFAULT_CONFIG); setMonth(0); }}
|
||||
className="rounded bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] text-[var(--color-text-dim)] px-2 py-1 text-[10px] hover:text-[var(--color-danger)] hover:border-[var(--color-danger)] cursor-pointer"
|
||||
title="Reset to the Forgotten Realms calendar"
|
||||
>
|
||||
Reset to Forgotten Realms
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView("month")}
|
||||
className="ml-auto rounded bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-3 py-1 text-[10px] font-semibold cursor-pointer hover:bg-[var(--color-gold-muted)]"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Events for selected day */}
|
||||
{selectedDay !== null && (
|
||||
<div className="flex-1 overflow-y-auto border-t border-[var(--color-border-subtle)] pt-2">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="text-[10px] font-medium text-[var(--color-text-secondary)]">
|
||||
{MONTHS[month]} {selectedDay}
|
||||
</div>
|
||||
<span className="text-[10px] text-[var(--color-gold-bright)]" title="Weather for this day">
|
||||
{weatherFor(selectedDay, month, year)}
|
||||
</span>
|
||||
</div>
|
||||
{dayEvents.length === 0 && (
|
||||
<div className="text-[var(--color-text-dim)] text-[10px] italic">No events</div>
|
||||
</div>
|
||||
) : view === "month" ? (
|
||||
<>
|
||||
{/* Month navigation */}
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => { if (month === 0) { setMonth(config.months.length - 1); setYear(year - 1); } else setMonth(month - 1); }}
|
||||
className="text-[var(--color-text-dim)] hover:text-[var(--color-gold-bright)] cursor-pointer text-sm"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<div className="text-center">
|
||||
<div className="font-heading text-[var(--color-gold-bright)] text-xs font-semibold">
|
||||
{monthName(config, month)}
|
||||
</div>
|
||||
<div className="text-[var(--color-text-dim)] text-[10px]">{year} {config.yearLabel}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { if (month === config.months.length - 1) { setMonth(0); setYear(year + 1); } else setMonth(month + 1); }}
|
||||
className="text-[var(--color-text-dim)] hover:text-[var(--color-gold-bright)] cursor-pointer text-sm"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<button
|
||||
onClick={jumpToToday}
|
||||
className="rounded bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] text-[var(--color-text-dim)] px-1.5 py-0.5 text-[9px] cursor-pointer hover:text-[var(--color-gold-bright)]"
|
||||
title="Jump the view to today"
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
<button
|
||||
onClick={nextDay}
|
||||
className="rounded bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-1.5 py-0.5 text-[9px] font-semibold cursor-pointer hover:bg-[var(--color-gold-muted)]"
|
||||
title="Advance the campaign date by one day"
|
||||
>
|
||||
Next day →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Day grid */}
|
||||
<div className="grid grid-cols-7 gap-0.5">
|
||||
{["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map((d) => (
|
||||
<div key={d} className="text-[var(--color-text-dim)] text-[9px] text-center font-medium">
|
||||
{d}
|
||||
</div>
|
||||
))}
|
||||
{Array.from({ length: dim }, (_, i) => {
|
||||
const dayNum = i + 1;
|
||||
const isToday = dayNum === today.day && month === today.month && year === today.year;
|
||||
const dayEvts = events.filter((e) => e.day === dayNum && (e.recur === "monthly" || e.month === month));
|
||||
const isSelected = dayNum === selectedDay;
|
||||
return (
|
||||
<button
|
||||
key={dayNum}
|
||||
onClick={() => setSelectedDay(dayNum)}
|
||||
className={`rounded text-[10px] py-1 cursor-pointer transition-colors relative ${
|
||||
isToday
|
||||
? "bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] font-bold"
|
||||
: isSelected
|
||||
? "bg-[var(--color-bg-card-hover)] text-[var(--color-text-primary)]"
|
||||
: "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-card)]"
|
||||
}`}
|
||||
>
|
||||
{dayNum}
|
||||
{dayEvts.length > 0 && !isToday && (
|
||||
<span className="flex justify-center gap-0.5 mt-0.5">
|
||||
{dayEvts.slice(0, 3).map((e, j) => (
|
||||
<span key={j} className="w-1 h-1 rounded-full" style={{ backgroundColor: catColor(e.category) }} />
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
<span className="block text-[7px] leading-none opacity-60">{moonPhase(dayNum, month, year)}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Events for selected day */}
|
||||
{selectedDay !== null && (
|
||||
<div className="flex-1 overflow-y-auto border-t border-[var(--color-border-subtle)] pt-2">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="text-[10px] font-medium text-[var(--color-text-secondary)]">
|
||||
{monthName(config, month)} {selectedDay}
|
||||
</div>
|
||||
<span className="text-[10px] text-[var(--color-gold-bright)]" title="Weather for this day">
|
||||
{weatherFor(selectedDay, month, year)}
|
||||
</span>
|
||||
</div>
|
||||
{dayEvents.length === 0 && (
|
||||
<div className="text-[var(--color-text-dim)] text-[10px] italic">No events</div>
|
||||
)}
|
||||
{dayEvents.map((e, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5 py-0.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ backgroundColor: catColor(e.category) }} />
|
||||
<span className="text-[var(--color-text-primary)] text-[10px] flex-1">{e.text}</span>
|
||||
{e.recur !== "none" && (
|
||||
<span className="text-[8px] text-[var(--color-text-dim)]" title={`Repeats ${e.recur}`}>↻{e.recur === "monthly" ? "m" : "y"}</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => removeEvent(events.indexOf(e))}
|
||||
className="text-[var(--color-text-dim)] hover:text-[var(--color-danger)] text-[10px] cursor-pointer"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex flex-col gap-1 mt-1">
|
||||
<input
|
||||
className="rounded bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-2 py-1 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-[10px]"
|
||||
value={newEvent}
|
||||
onChange={(e) => setNewEvent(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && addEvent()}
|
||||
placeholder="Add event…"
|
||||
/>
|
||||
<div className="flex gap-1">
|
||||
<select
|
||||
value={newCategory}
|
||||
onChange={(e) => setNewCategory(e.target.value)}
|
||||
className="flex-1 min-w-0 rounded bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-[9px] cursor-pointer"
|
||||
>
|
||||
{CATEGORIES.map((c) => <option key={c.label} value={c.label}>{c.label}</option>)}
|
||||
</select>
|
||||
<select
|
||||
value={newRecur}
|
||||
onChange={(e) => setNewRecur(e.target.value as Recur)}
|
||||
className="rounded bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-[9px] cursor-pointer"
|
||||
title="Repeat"
|
||||
>
|
||||
<option value="none">Once</option>
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Yearly</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={addEvent}
|
||||
className="rounded bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-2 py-1 text-[10px] font-semibold cursor-pointer hover:bg-[var(--color-gold-muted)] transition-colors"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{dayEvents.map((e, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5 py-0.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ backgroundColor: e.color }} />
|
||||
<span className="text-[var(--color-text-primary)] text-[10px] flex-1">{e.text}</span>
|
||||
<button
|
||||
onClick={() => removeEvent(events.indexOf(e))}
|
||||
className="text-[var(--color-text-dim)] hover:text-[var(--color-danger)] text-[10px] cursor-pointer"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
// Agenda view — next events from today forward.
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="text-[10px] font-medium text-[var(--color-text-secondary)] mb-1">
|
||||
Upcoming events
|
||||
</div>
|
||||
{agenda.length === 0 && (
|
||||
<div className="text-[var(--color-text-dim)] text-[10px] italic">No upcoming events.</div>
|
||||
)}
|
||||
{agenda.map((a, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5 py-1 border-b border-[var(--color-border-subtle)]">
|
||||
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ backgroundColor: catColor(a.e.category) }} />
|
||||
<span className="text-[var(--color-text-secondary)] text-[10px] font-mono shrink-0">
|
||||
{monthName(config, a.month).slice(0, 3)} {a.day}
|
||||
</span>
|
||||
<span className="text-[var(--color-text-primary)] text-[10px] flex-1">{a.e.text}</span>
|
||||
{a.e.recur !== "none" && (
|
||||
<span className="text-[8px] text-[var(--color-text-dim)]">↻{a.e.recur === "monthly" ? "m" : "y"}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-1 mt-1">
|
||||
<input
|
||||
className="flex-1 min-w-0 rounded bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-2 py-1 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-[10px]"
|
||||
value={newEvent}
|
||||
onChange={(e) => setNewEvent(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && addEvent()}
|
||||
placeholder="Add event…"
|
||||
/>
|
||||
<div className="mt-2 flex justify-end">
|
||||
<button
|
||||
onClick={addEvent}
|
||||
className="rounded bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-2 py-1 text-[10px] font-semibold cursor-pointer hover:bg-[var(--color-gold-muted)] transition-colors"
|
||||
onClick={nextDay}
|
||||
className="rounded bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-1.5 py-0.5 text-[9px] font-semibold cursor-pointer hover:bg-[var(--color-gold-muted)]"
|
||||
>
|
||||
+
|
||||
Next day →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user