Files
dm-pal/src/components/CalendarWidget.tsx
T
itsamejms 5ce2686ced v0.5.0-dm-tools: Initiative tracker, random tables, encounter builder, calendar
- Initiative Tracker: add/remove combatants, roll initiative, HP bars with
  color transitions (green→gold→red), condition badges, round counter,
  next-turn cycling, gold active-state border
- Random Tables: 4 built-in tables (Tavern Names, Weather, NPC Quirks,
  Treasure), dice notation parser, roll history, highlight matching result
- Encounter Builder: AI-generated encounters via LLM, party level/size,
  terrain selector, JSON response parsing, difficulty badge
- Calendar Widget: Faerûn calendar (Hammer–Nightal), event creation per
  day, event indicators, month navigation, year display
- All glass-card wrapped with framer-motion hover/tap animations
- Full production build clean: tsc, vite, cargo, tauri build
2026-06-28 22:40:11 +01:00

140 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
const MONTHS = [
"Hammer", "Alturiak", "Ches", "Tarsakh", "Mirtul", "Kythorn",
"Flamerule", "Eleasis", "Eleint", "Marpenoth", "Uktar", "Nightal",
];
interface CalendarEvent {
day: number;
month: number;
text: string;
color: string;
}
const COLORS = ["var(--color-gold-bright)", "var(--color-info)", "var(--color-success)", "var(--color-danger)"];
export function CalendarWidget() {
const [month, setMonth] = useState(0);
const [year, setYear] = useState(1492);
const [events, setEvents] = useState<CalendarEvent[]>([]);
const [selectedDay, setSelectedDay] = useState<number | null>(null);
const [newEvent, setNewEvent] = useState("");
const today = { day: 15, month: 5 }; // 15th of Kythorn (arbitrary "today")
function addEvent() {
if (!newEvent.trim() || selectedDay === null) return;
setEvents((prev) => [
...prev,
{ day: selectedDay, month, text: newEvent.trim(), color: COLORS[events.length % COLORS.length] },
]);
setNewEvent("");
}
function removeEvent(idx: number) {
setEvents((prev) => prev.filter((_, i) => i !== idx));
}
const dayEvents = events.filter((e) => e.day === selectedDay && e.month === month);
return (
<div className="flex flex-col gap-2 h-full">
{/* Month navigation */}
<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>
{/* 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>
))}
{/* 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;
const hasEvent = events.some((e) => e.day === dayNum && e.month === month);
const isSelected = dayNum === selectedDay;
return (
<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)]"
}`}
>
{dayNum}
{hasEvent && !isToday && (
<span className="block w-1 h-1 rounded-full bg-[var(--color-gold-bright)] mx-auto mt-0" />
)}
</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="text-[10px] font-medium text-[var(--color-text-secondary)] mb-1">
{MONTHS[month]} {selectedDay}
</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: 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>
</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…"
/>
<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>
);
}