Extract NoteWidget and CalendarWidget from monolithic page.tsx
Split page.tsx from ~2370 to ~1600 lines by extracting: - NoteWidget: Markdown editor/preview, toolbar, checkbox toggle, all formatting helpers (bold, italic, lists, links, code) - CalendarWidget: Month grid, event tooltips, source selection, next-events list, month navigation Both components are self-contained with their own state where appropriate (calendar month navigation, local content edits) while receiving data and callbacks from the parent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
type CalendarEvent = {
|
||||
id: string;
|
||||
title: string;
|
||||
start: string;
|
||||
end: string | null;
|
||||
location: string | null;
|
||||
};
|
||||
|
||||
type CalendarSource = {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
type: string;
|
||||
passwordConfigured: boolean;
|
||||
};
|
||||
|
||||
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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user