Files
personal-dashboard/src/components/CalendarWidget.tsx
T

250 lines
7.8 KiB
TypeScript
Raw Normal View History

"use client";
import { useMemo, useState } from "react";
import type { CalendarEvent, CalendarSource } from "@/types/dashboard";
type CalendarWidgetProps = {
widgetId: string;
editMode: boolean;
events: CalendarEvent[];
error: string | null;
sources: CalendarSource[];
selectedSourceIds: string[];
nextEventsCount: number;
onToggleSource: (widgetId: string, sourceId: string) => void;
onChangeEventsCount: (widgetId: string, count: number) => void;
};
type CalendarDay = {
key: string;
date: Date;
inCurrentMonth: boolean;
isToday: boolean;
events: CalendarEvent[];
};
const weekdayLabels = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
function dateKey(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function formatEventDate(value: string): string {
return new Intl.DateTimeFormat("de-DE", {
weekday: "short",
day: "2-digit",
month: "2-digit",
hour: "2-digit",
minute: "2-digit"
}).format(new Date(value));
}
function formatEventTime(value: string): string {
return new Intl.DateTimeFormat("de-DE", {
hour: "2-digit",
minute: "2-digit"
}).format(new Date(value));
}
function formatMonthLabel(value: Date): string {
return new Intl.DateTimeFormat("de-DE", {
month: "long",
year: "numeric"
}).format(value);
}
function buildCalendarDays(monthDate: Date, eventsByDate: Map<string, CalendarEvent[]>): CalendarDay[] {
const year = monthDate.getFullYear();
const month = monthDate.getMonth();
const firstDayOfMonth = new Date(year, month, 1);
const mondayBasedStartOffset = (firstDayOfMonth.getDay() + 6) % 7;
const gridStart = new Date(firstDayOfMonth);
gridStart.setDate(firstDayOfMonth.getDate() - mondayBasedStartOffset);
gridStart.setHours(0, 0, 0, 0);
const today = dateKey(new Date());
const days: CalendarDay[] = [];
for (let index = 0; index < 42; index += 1) {
const date = new Date(gridStart);
date.setDate(gridStart.getDate() + index);
const key = dateKey(date);
days.push({
key,
date,
inCurrentMonth: date.getMonth() === month,
isToday: key === today,
events: eventsByDate.get(key) ?? []
});
}
return days;
}
function groupEventsByDate(events: CalendarEvent[]): Map<string, CalendarEvent[]> {
const grouped = new Map<string, CalendarEvent[]>();
events.forEach((event) => {
const key = dateKey(new Date(event.start));
const existing = grouped.get(key) ?? [];
grouped.set(key, [...existing, event]);
});
return grouped;
}
export default function CalendarWidget({
widgetId,
editMode,
events,
error,
sources,
selectedSourceIds,
nextEventsCount,
onToggleSource,
onChangeEventsCount
}: CalendarWidgetProps) {
const [calendarMonth, setCalendarMonth] = useState(() => new Date());
const eventsByDate = useMemo(() => groupEventsByDate(events), [events]);
const calendarDays = useMemo(() => buildCalendarDays(calendarMonth, eventsByDate), [calendarMonth, eventsByDate]);
const clampedEventsCount = Math.max(0, Math.min(10, nextEventsCount));
const nextEvents = useMemo(
() =>
[...events]
.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())
.slice(0, clampedEventsCount),
[events, clampedEventsCount]
);
return (
<>
{editMode ? (
<details className="calendarSourcePanel widgetNoDrag">
<summary>Kalenderauswahl</summary>
<div className="calendarSourceForm">
{sources.length === 0 ? (
<p className="muted">
Keine Kalenderquellen angelegt. Lege Kalender in den Benutzereinstellungen an.
</p>
) : null}
{sources.map((source) => (
<label className="calendarSourceCheckbox" key={source.id}>
<input
type="checkbox"
checked={selectedSourceIds.includes(source.id)}
onChange={() => onToggleSource(widgetId, source.id)}
/>
<span className="calendarSourceColorDot" style={{ background: source.color }} />
<span>
<strong>{source.name}</strong>
<small>{source.type === "EXCHANGE_EWS" ? "Exchange" : "ICS"}</small>
</span>
</label>
))}
<label className="fieldLabel">
Termine unter dem Kalender
<select
className="select"
value={nextEventsCount}
onChange={(event) => onChangeEventsCount(widgetId, Number(event.target.value))}
>
<option value={0}>Ausblenden</option>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((n) => (
<option key={n} value={n}>{n} {n === 1 ? "Termin" : "Termine"}</option>
))}
</select>
</label>
</div>
</details>
) : null}
<div className="calendarHeader widgetNoDrag">
<button type="button" className="calendarNavButton" onClick={() => setCalendarMonth((c) => new Date(c.getFullYear(), c.getMonth() - 1, 1))}>
Zurück
</button>
<button type="button" className="calendarMonthButton" onClick={() => setCalendarMonth(new Date())}>
{formatMonthLabel(calendarMonth)}
</button>
<button type="button" className="calendarNavButton" onClick={() => setCalendarMonth((c) => new Date(c.getFullYear(), c.getMonth() + 1, 1))}>
Weiter
</button>
</div>
{selectedSourceIds.length === 0 ? <p className="muted calendarStatus">Keine Kalenderquelle ausgewählt.</p> : null}
<div className="calendarWeekdays">
{weekdayLabels.map((label) => (
<div className="calendarWeekday" key={label}>{label}</div>
))}
</div>
<div className="calendarGrid">
{calendarDays.map((day) => (
<div
className={[
"calendarDay",
day.inCurrentMonth ? "" : "calendarDayMuted",
day.isToday ? "calendarDayToday" : "",
day.events.length > 0 ? "calendarDayWithEvents" : ""
]
.filter(Boolean)
.join(" ")}
key={day.key}
>
<span className="calendarDayNumber">{day.date.getDate()}</span>
{day.events.length > 0 ? <span className="calendarEventCount">{day.events.length}</span> : null}
{day.events.length > 0 ? (
<div className="calendarTooltip">
{day.events.slice(0, 5).map((event) => (
<div className="calendarTooltipItem" key={event.id}>
<span className="calendarTooltipTime">{formatEventTime(event.start)}</span>
<span>{event.title}</span>
</div>
))}
{day.events.length > 5 ? <div className="calendarTooltipMore">Weitere Termine vorhanden</div> : null}
</div>
) : null}
</div>
))}
</div>
{error ? <p className="muted calendarStatus">{error}</p> : null}
{clampedEventsCount > 0 ? (
<section className="nextEventsBlock">
<h3>Nächste Termine</h3>
{nextEvents.length === 0 ? <p className="muted">Keine nächsten Termine.</p> : null}
<div className="eventList">
{nextEvents.map((event) => (
<article className="eventItem" key={event.id}>
<div className="eventDate">{formatEventDate(event.start)}</div>
<div className="eventTitle">{event.title}</div>
{event.location ? <div className="eventLocation">{event.location}</div> : null}
</article>
))}
</div>
</section>
) : null}
</>
);
}