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:
Claude
2026-06-18 13:36:06 +02:00
parent 1ef34445a9
commit a22f1baf4d
3 changed files with 742 additions and 800 deletions
+29 -800
View File
@@ -1,15 +1,17 @@
"use client"; "use client";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import type { CSSProperties, ReactNode } from "react"; import type { CSSProperties } from "react";
import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
import type { DashboardGridProps } from "@/components/DashboardGrid"; import type { DashboardGridProps } from "@/components/DashboardGrid";
import type { DashboardGridWidget, DashboardLayoutItem } from "@/lib/dashboard-layout"; import type { DashboardGridWidget, DashboardLayoutItem } from "@/lib/dashboard-layout";
import { sortLayoutForPosition } from "@/lib/dashboard-layout"; import { sortLayoutForPosition } from "@/lib/dashboard-layout";
import CalculatorWidget from "@/components/CalculatorWidget"; import CalculatorWidget from "@/components/CalculatorWidget";
import CalendarWidget from "@/components/CalendarWidget";
import ClockWidget from "@/components/ClockWidget"; import ClockWidget from "@/components/ClockWidget";
import FavoritesWidget from "@/components/FavoritesWidget";
import DomainCheckWidget from "@/components/DomainCheckWidget"; import DomainCheckWidget from "@/components/DomainCheckWidget";
import FavoritesWidget from "@/components/FavoritesWidget";
import NoteWidget from "@/components/NoteWidget";
const DashboardGrid = dynamic<DashboardGridProps>(() => import("@/components/DashboardGrid"), { const DashboardGrid = dynamic<DashboardGridProps>(() => import("@/components/DashboardGrid"), {
ssr: false, ssr: false,
@@ -98,14 +100,6 @@ type CalendarWidgetCalendarConfig = {
nextEventsCount: number; nextEventsCount: number;
}; };
type CalendarDay = {
key: string;
date: Date;
inCurrentMonth: boolean;
isToday: boolean;
events: CalendarEvent[];
};
type NoteBoardItem = { type NoteBoardItem = {
id: string; id: string;
userId: string; userId: string;
@@ -177,40 +171,6 @@ const widgetCatalog: Array<{
} }
]; ];
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 getUserInitials(user: User): string { function getUserInitials(user: User): string {
const source = user.displayName?.trim() || user.email.trim(); const source = user.displayName?.trim() || user.email.trim();
@@ -227,37 +187,6 @@ function getUserInitials(user: User): string {
return source.slice(0, 2).toUpperCase(); return source.slice(0, 2).toUpperCase();
} }
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 clamp(value: number, min: number, max: number): number { function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value)); return Math.max(min, Math.min(max, value));
} }
@@ -330,8 +259,6 @@ export default function DashboardPage() {
const [calendarSources, setCalendarSources] = useState<CalendarSource[]>([]); const [calendarSources, setCalendarSources] = useState<CalendarSource[]>([]);
const [calendarSelectionsByWidget, setCalendarSelectionsByWidget] = useState<Record<string, string[]>>({}); const [calendarSelectionsByWidget, setCalendarSelectionsByWidget] = useState<Record<string, string[]>>({});
const [calendarNextEventsCountByWidget, setCalendarNextEventsCountByWidget] = useState<Record<string, number>>({}); const [calendarNextEventsCountByWidget, setCalendarNextEventsCountByWidget] = useState<Record<string, number>>({});
const [calendarMonth, setCalendarMonth] = useState(() => new Date());
const [profileMenuOpen, setProfileMenuOpen] = useState(false); const [profileMenuOpen, setProfileMenuOpen] = useState(false);
const [dashboardError, setDashboardError] = useState<string | null>(null); const [dashboardError, setDashboardError] = useState<string | null>(null);
const [openWidgetMenuId, setOpenWidgetMenuId] = useState<string | null>(null); const [openWidgetMenuId, setOpenWidgetMenuId] = useState<string | null>(null);
@@ -378,19 +305,6 @@ export default function DashboardPage() {
const dashboardSubtitle = settings?.dashboardSubtitle?.trim() || user?.email || ""; const dashboardSubtitle = settings?.dashboardSubtitle?.trim() || user?.email || "";
const logoUrl = settings?.logoUrl?.trim() || "/logo.svg"; const logoUrl = settings?.logoUrl?.trim() || "/logo.svg";
function groupEventsByDate(events: CalendarEvent[]): Map<string, CalendarEvent[]> {
const groupedEvents = new Map<string, CalendarEvent[]>();
events.forEach((event) => {
const key = dateKey(new Date(event.start));
const existingEvents = groupedEvents.get(key) ?? [];
groupedEvents.set(key, [...existingEvents, event]);
});
return groupedEvents;
}
useEffect(() => { useEffect(() => {
async function loadCurrentUser() { async function loadCurrentUser() {
try { try {
@@ -956,18 +870,6 @@ export default function DashboardPage() {
setProfileMenuOpen(false); setProfileMenuOpen(false);
} }
function showPreviousMonth() {
setCalendarMonth((current) => new Date(current.getFullYear(), current.getMonth() - 1, 1));
}
function showNextMonth() {
setCalendarMonth((current) => new Date(current.getFullYear(), current.getMonth() + 1, 1));
}
function showCurrentMonth() {
setCalendarMonth(new Date());
}
function renderNoteEditButton(widget: Widget) { function renderNoteEditButton(widget: Widget) {
if (widget.type !== "note" && widget.type !== "noteboard") { if (widget.type !== "note" && widget.type !== "noteboard") {
return null; return null;
@@ -1195,548 +1097,6 @@ export default function DashboardPage() {
return notes.find((note) => note.id === widget.id) ?? null; return notes.find((note) => note.id === widget.id) ?? null;
} }
function getNoteTextarea(noteId: string): HTMLTextAreaElement | null {
if (typeof document === "undefined") {
return null;
}
return document.getElementById(`note-textarea-${noteId}`) as HTMLTextAreaElement | null;
}
function updateNoteContentFromToolbar(
note: NoteBoardItem,
nextContent: string,
nextSelectionStart: number,
nextSelectionEnd = nextSelectionStart
) {
setNotes((current) =>
current.map((currentNote) =>
currentNote.id === note.id
? {
...currentNote,
content: nextContent
}
: currentNote
)
);
void patchNote(note.id, {
content: nextContent
});
window.setTimeout(() => {
const textarea = getNoteTextarea(note.id);
if (!textarea) {
return;
}
textarea.focus();
textarea.setSelectionRange(nextSelectionStart, nextSelectionEnd);
}, 0);
}
function wrapNoteSelection(note: NoteBoardItem, before: string, after: string, placeholder: string) {
const textarea = getNoteTextarea(note.id);
const value = note.content ?? "";
const start = textarea?.selectionStart ?? value.length;
const end = textarea?.selectionEnd ?? value.length;
const selectedText = value.slice(start, end);
const beforeSelection = value.slice(Math.max(0, start - before.length), start);
const afterSelection = value.slice(end, end + after.length);
if (selectedText && beforeSelection === before && afterSelection === after) {
const nextContent = `${value.slice(0, start - before.length)}${selectedText}${value.slice(end + after.length)}`;
const nextStart = start - before.length;
const nextEnd = nextStart + selectedText.length;
updateNoteContentFromToolbar(note, nextContent, nextStart, nextEnd);
return;
}
if (selectedText.startsWith(before) && selectedText.endsWith(after) && selectedText.length > before.length + after.length) {
const unwrappedText = selectedText.slice(before.length, selectedText.length - after.length);
const nextContent = `${value.slice(0, start)}${unwrappedText}${value.slice(end)}`;
updateNoteContentFromToolbar(note, nextContent, start, start + unwrappedText.length);
return;
}
const insertedText = selectedText || placeholder;
const replacement = `${before}${insertedText}${after}`;
const nextContent = `${value.slice(0, start)}${replacement}${value.slice(end)}`;
const selectionStart = start + before.length;
const selectionEnd = selectionStart + insertedText.length;
updateNoteContentFromToolbar(note, nextContent, selectionStart, selectionEnd);
}
function prefixNoteLines(note: NoteBoardItem, prefix: string, placeholder: string) {
const textarea = getNoteTextarea(note.id);
const value = note.content ?? "";
const start = textarea?.selectionStart ?? value.length;
const end = textarea?.selectionEnd ?? value.length;
const lineStart = value.lastIndexOf("\n", Math.max(0, start - 1)) + 1;
const lineEndCandidate = end === start ? end : end;
const nextLineBreak = value.indexOf("\n", lineEndCandidate);
const lineEnd = nextLineBreak === -1 || end === start ? lineEndCandidate : lineEndCandidate;
const selectedBlock = value.slice(lineStart, lineEnd) || placeholder;
const lines = selectedBlock.split("\n");
const isCheckboxMode = prefix === "- [ ] ";
const isOrderedMode = prefix === "1. ";
const isBulletMode = prefix === "- ";
const checkboxPattern = /^(\s*)[-*]\s+\[[ xX]\]\s+/;
const orderedPattern = /^(\s*)\d+\.\s+/;
const bulletPattern = /^(\s*)[-*]\s+(?!\[[ xX]\]\s+)/;
const alreadyFormatted = lines.every((line) => {
if (!line.trim()) {
return true;
}
if (isCheckboxMode) {
return checkboxPattern.test(line);
}
if (isOrderedMode) {
return orderedPattern.test(line);
}
if (isBulletMode) {
return bulletPattern.test(line);
}
return false;
});
const nextLines = lines.map((line, index) => {
if (!line.trim()) {
return line;
}
if (alreadyFormatted) {
if (isCheckboxMode) {
return line.replace(checkboxPattern, "$1");
}
if (isOrderedMode) {
return line.replace(orderedPattern, "$1");
}
if (isBulletMode) {
return line.replace(bulletPattern, "$1");
}
return line;
}
const cleanLine = line
.replace(checkboxPattern, "$1")
.replace(orderedPattern, "$1")
.replace(bulletPattern, "$1");
if (isOrderedMode) {
return cleanLine.replace(/^(\s*)/, `$1${index + 1}. `);
}
return cleanLine.replace(/^(\s*)/, `$1${prefix}`);
});
const prefixedBlock = nextLines.join("\n");
const nextContent = `${value.slice(0, lineStart)}${prefixedBlock}${value.slice(lineEnd)}`;
updateNoteContentFromToolbar(note, nextContent, lineStart, lineStart + prefixedBlock.length);
}
function insertNoteLink(note: NoteBoardItem) {
const textarea = getNoteTextarea(note.id);
const value = note.content ?? "";
const start = textarea?.selectionStart ?? value.length;
const end = textarea?.selectionEnd ?? value.length;
const selectedText = value.slice(start, end) || "Linktext";
const replacement = `[${selectedText}](https://)`;
const nextContent = `${value.slice(0, start)}${replacement}${value.slice(end)}`;
const urlStart = start + selectedText.length + 3;
updateNoteContentFromToolbar(note, nextContent, urlStart, urlStart + 8);
}
function normalizeMarkdownHref(href: string): string {
const cleanHref = href.trim();
if (cleanHref.startsWith("/") || cleanHref.startsWith("#")) {
return cleanHref;
}
try {
const parsedUrl = new URL(cleanHref);
if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:" || parsedUrl.protocol === "mailto:") {
return parsedUrl.toString();
}
} catch {
return "#";
}
return "#";
}
function renderInlineMarkdown(text: string): ReactNode[] {
const nodes: ReactNode[] = [];
const pattern = /(\*\*[^*\n]+?\*\*|\*[^*\n]+?\*|`[^`\n]+?`|\[[^\]\n]+?\]\([^)]+?\))/g;
let lastIndex = 0;
let match: RegExpExecArray | null;
let key = 0;
while ((match = pattern.exec(text)) !== null) {
if (match.index > lastIndex) {
nodes.push(text.slice(lastIndex, match.index));
}
const token = match[0];
if (token.startsWith("**") && token.endsWith("**")) {
nodes.push(<strong key={`strong-${key}`}>{token.slice(2, -2)}</strong>);
} else if (token.startsWith("*") && token.endsWith("*")) {
nodes.push(<em key={`em-${key}`}>{token.slice(1, -1)}</em>);
} else if (token.startsWith("`") && token.endsWith("`")) {
nodes.push(<code key={`code-${key}`}>{token.slice(1, -1)}</code>);
} else {
const linkMatch = token.match(/^\[([^\]]+?)\]\(([^)]+?)\)$/);
if (linkMatch) {
nodes.push(
<a key={`link-${key}`} href={normalizeMarkdownHref(linkMatch[2])} target="_blank" rel="noreferrer">
{linkMatch[1]}
</a>
);
} else {
nodes.push(token);
}
}
key += 1;
lastIndex = pattern.lastIndex;
}
if (lastIndex < text.length) {
nodes.push(text.slice(lastIndex));
}
return nodes;
}
function toggleMarkdownCheckbox(note: NoteBoardItem, lineIndex: number) {
const lines = (note.content ?? "").split("\n");
const line = lines[lineIndex] ?? "";
const match = line.match(/^(\s*[-*]\s+\[)([ xX])(\]\s+.*)$/);
if (!match) {
return;
}
const isChecked = match[2].toLowerCase() === "x";
lines[lineIndex] = `${match[1]}${isChecked ? " " : "x"}${match[3]}`;
const nextContent = lines.join("\n");
setNotes((current) =>
current.map((currentNote) =>
currentNote.id === note.id
? {
...currentNote,
content: nextContent
}
: currentNote
)
);
void patchNote(note.id, {
content: nextContent
});
}
function renderNoteMarkdownPreview(note: NoteBoardItem) {
const content = note.content ?? "";
if (!content.trim()) {
return <p className="muted noteEmptyText">Noch keine Notiz.</p>;
}
const lines = content.split("\n");
const blocks: ReactNode[] = [];
let index = 0;
while (index < lines.length) {
const line = lines[index];
if (!line.trim()) {
blocks.push(<div className="noteMarkdownSpacer" key={`space-${index}`} />);
index += 1;
continue;
}
const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
if (headingMatch) {
const level = headingMatch[1].length;
const className = `noteMarkdownHeading noteMarkdownHeading${level}`;
blocks.push(
<div className={className} key={`heading-${index}`}>
{renderInlineMarkdown(headingMatch[2])}
</div>
);
index += 1;
continue;
}
const taskMatch = line.match(/^(\s*)[-*]\s+\[([ xX])\]\s+(.+)$/);
if (taskMatch) {
const tasks: Array<{ lineIndex: number; checked: boolean; text: string }> = [];
while (index < lines.length) {
const currentMatch = lines[index].match(/^(\s*)[-*]\s+\[([ xX])\]\s+(.+)$/);
if (!currentMatch) {
break;
}
tasks.push({
lineIndex: index,
checked: currentMatch[2].toLowerCase() === "x",
text: currentMatch[3]
});
index += 1;
}
blocks.push(
<div className="noteMarkdownChecklist" key={`tasks-${tasks[0]?.lineIndex ?? index}`}>
{tasks.map((task) => (
<label className="noteMarkdownTask" key={`task-${task.lineIndex}`}>
<input
type="checkbox"
checked={task.checked}
onChange={() => toggleMarkdownCheckbox(note, task.lineIndex)}
/>
<span>{renderInlineMarkdown(task.text)}</span>
</label>
))}
</div>
);
continue;
}
const unorderedMatch = line.match(/^\s*[-*]\s+(?!\[[ xX]\]\s+)(.+)$/);
if (unorderedMatch) {
const items: Array<{ lineIndex: number; text: string }> = [];
while (index < lines.length) {
const currentMatch = lines[index].match(/^\s*[-*]\s+(?!\[[ xX]\]\s+)(.+)$/);
if (!currentMatch) {
break;
}
items.push({
lineIndex: index,
text: currentMatch[1]
});
index += 1;
}
blocks.push(
<ul className="noteMarkdownList" key={`ul-${items[0]?.lineIndex ?? index}`}>
{items.map((item) => (
<li key={`ul-item-${item.lineIndex}`}>{renderInlineMarkdown(item.text)}</li>
))}
</ul>
);
continue;
}
const orderedMatch = line.match(/^\s*\d+\.\s+(.+)$/);
if (orderedMatch) {
const items: Array<{ lineIndex: number; text: string }> = [];
while (index < lines.length) {
const currentMatch = lines[index].match(/^\s*\d+\.\s+(.+)$/);
if (!currentMatch) {
break;
}
items.push({
lineIndex: index,
text: currentMatch[1]
});
index += 1;
}
blocks.push(
<ol className="noteMarkdownList" key={`ol-${items[0]?.lineIndex ?? index}`}>
{items.map((item) => (
<li key={`ol-item-${item.lineIndex}`}>{renderInlineMarkdown(item.text)}</li>
))}
</ol>
);
continue;
}
blocks.push(
<p className="noteMarkdownParagraph" key={`p-${index}`}>
{renderInlineMarkdown(line)}
</p>
);
index += 1;
}
return <div className="noteMarkdownPreview widgetNoDrag">{blocks}</div>;
}
function renderNoteMarkdownToolbar(note: NoteBoardItem) {
return (
<div className="noteMarkdownToolbar widgetNoDrag" aria-label="Markdown-Werkzeuge">
<button
type="button"
className="noteMarkdownButton"
onMouseDown={(event) => event.preventDefault()}
onClick={() => wrapNoteSelection(note, "**", "**", "fett")}
title="Fett"
>
B
</button>
<button
type="button"
className="noteMarkdownButton"
onMouseDown={(event) => event.preventDefault()}
onClick={() => wrapNoteSelection(note, "*", "*", "kursiv")}
title="Kursiv"
>
<em>I</em>
</button>
<button
type="button"
className="noteMarkdownButton noteMarkdownButtonWide"
onMouseDown={(event) => event.preventDefault()}
onClick={() => prefixNoteLines(note, "- ", "Eintrag")}
title="Aufzählung"
>
Liste
</button>
<button
type="button"
className="noteMarkdownButton noteMarkdownButtonWide"
onMouseDown={(event) => event.preventDefault()}
onClick={() => prefixNoteLines(note, "1. ", "Eintrag")}
title="Nummerierte Liste"
>
1.
</button>
<button
type="button"
className="noteMarkdownButton noteMarkdownButtonWide"
onMouseDown={(event) => event.preventDefault()}
onClick={() => prefixNoteLines(note, "- [ ] ", "Aufgabe")}
title="Checkbox"
>
</button>
<button
type="button"
className="noteMarkdownButton"
onMouseDown={(event) => event.preventDefault()}
onClick={() => wrapNoteSelection(note, "`", "`", "Code")}
title="Inline-Code"
>
{"</>"}
</button>
<button
type="button"
className="noteMarkdownButton noteMarkdownButtonWide"
onMouseDown={(event) => event.preventDefault()}
onClick={() => insertNoteLink(note)}
title="Link"
>
Link
</button>
</div>
);
}
function renderNoteWidget(widget: Widget) {
const note = getNoteForWidget(widget);
if (!note) {
return (
<div className="singleNoteWidget widgetNoDrag">
{noteError ? <p className="errorText">{noteError}</p> : null}
<button type="button" className="button buttonSecondary" onClick={() => void createMissingNoteForWidget(widget)}>
Eintrag initialisieren
</button>
</div>
);
}
const noteIsEditing = editMode || editingNoteWidgetId === widget.id;
if (!noteIsEditing) {
return renderNoteMarkdownPreview(note);
}
return (
<div className="singleNoteWidget noteMarkdownEditor widgetNoDrag">
{noteError ? <p className="errorText">{noteError}</p> : null}
{renderNoteMarkdownToolbar(note)}
<textarea
id={`note-textarea-${note.id}`}
className="noteTextarea singleNoteTextarea"
value={note.content}
onChange={(event) => {
const nextContent = event.target.value;
setNotes((current) =>
current.map((currentNote) =>
currentNote.id === note.id
? {
...currentNote,
content: nextContent
}
: currentNote
)
);
}}
onBlur={(event) => void patchNote(note.id, { content: event.target.value })}
placeholder="Notiz schreiben... Markdown wird unterstützt."
spellCheck={true}
/>
</div>
);
}
async function reloadCalendarWidget(widgetId: string) { async function reloadCalendarWidget(widgetId: string) {
const [configResponse, eventsResponse] = await Promise.all([ const [configResponse, eventsResponse] = await Promise.all([
fetch(`/api/calendar/source?widgetId=${encodeURIComponent(widgetId)}`, { fetch(`/api/calendar/source?widgetId=${encodeURIComponent(widgetId)}`, {
@@ -1837,65 +1197,6 @@ export default function DashboardPage() {
void saveCalendarWidgetSources(widgetId, selectedSourceIds, nextEventsCount); void saveCalendarWidgetSources(widgetId, selectedSourceIds, nextEventsCount);
} }
function renderCalendarSourceSettings(widget: Widget) {
if (!editMode) {
return null;
}
const selectedSourceIds = calendarSelectionsByWidget[widget.id] ?? [];
const nextEventsCount = calendarNextEventsCountByWidget[widget.id] ?? 3;
return (
<details className="calendarSourcePanel widgetNoDrag">
<summary>Kalenderauswahl</summary>
<div className="calendarSourceForm">
{calendarSources.length === 0 ? (
<p className="muted">
Keine Kalenderquellen angelegt. Lege Kalender in den Benutzereinstellungen an.
</p>
) : null}
{calendarSources.map((source) => (
<label className="calendarSourceCheckbox" key={source.id}>
<input
type="checkbox"
checked={selectedSourceIds.includes(source.id)}
onChange={() => toggleCalendarSourceForWidget(widget.id, 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) => updateCalendarWidgetNextEventsCount(widget.id, Number(event.target.value))}
>
<option value={0}>Ausblenden</option>
<option value={1}>1 Termin</option>
<option value={2}>2 Termine</option>
<option value={3}>3 Termine</option>
<option value={4}>4 Termine</option>
<option value={5}>5 Termine</option>
<option value={6}>6 Termine</option>
<option value={7}>7 Termine</option>
<option value={8}>8 Termine</option>
<option value={9}>9 Termine</option>
<option value={10}>10 Termine</option>
</select>
</label>
</div>
</details>
);
}
function renderSearchWidget() { function renderSearchWidget() {
return ( return (
<form className="searchWidgetForm widgetNoDrag" onSubmit={handleSearch}> <form className="searchWidgetForm widgetNoDrag" onSubmit={handleSearch}>
@@ -1922,101 +1223,6 @@ export default function DashboardPage() {
); );
} }
function renderCalendarWidget(widget: Widget) {
const widgetEvents = calendarEventsByWidget[widget.id] ?? [];
const widgetError = calendarErrorsByWidget[widget.id] ?? null;
const selectedSourceIds = calendarSelectionsByWidget[widget.id] ?? [];
const widgetEventsByDate = groupEventsByDate(widgetEvents);
const widgetCalendarDays = buildCalendarDays(calendarMonth, widgetEventsByDate);
const nextEventsCount = Math.max(0, Math.min(10, calendarNextEventsCountByWidget[widget.id] ?? 3));
const nextWidgetEvents = [...widgetEvents]
.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())
.slice(0, nextEventsCount);
return (
<>
{renderCalendarSourceSettings(widget)}
<div className="calendarHeader widgetNoDrag">
<button type="button" className="calendarNavButton" onClick={showPreviousMonth}>
Zurück
</button>
<button type="button" className="calendarMonthButton" onClick={showCurrentMonth}>
{formatMonthLabel(calendarMonth)}
</button>
<button type="button" className="calendarNavButton" onClick={showNextMonth}>
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">
{widgetCalendarDays.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>
{widgetError ? <p className="muted calendarStatus">{widgetError}</p> : null}
{nextEventsCount > 0 ? (
<section className="nextEventsBlock">
<h3>Nächste Termine</h3>
{nextWidgetEvents.length === 0 ? <p className="muted">Keine nächsten Termine.</p> : null}
<div className="eventList">
{nextWidgetEvents.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}
</>
);
}
function renderWidgetContent(widget: Widget) { function renderWidgetContent(widget: Widget) {
if (widget.type === "favorites") { if (widget.type === "favorites") {
return ( return (
@@ -2030,7 +1236,18 @@ export default function DashboardPage() {
} }
if (widget.type === "note" || widget.type === "noteboard") { if (widget.type === "note" || widget.type === "noteboard") {
return renderNoteWidget(widget); const note = getNoteForWidget(widget);
return (
<NoteWidget
note={note}
editMode={editMode}
isEditing={editingNoteWidgetId === widget.id}
noteError={noteError}
onSave={(noteId, patch) => void patchNote(noteId, patch)}
onInitialize={() => void createMissingNoteForWidget(widget)}
/>
);
} }
if (widget.type === "search") { if (widget.type === "search") {
@@ -2046,7 +1263,19 @@ export default function DashboardPage() {
} }
if (widget.type === "calendar") { if (widget.type === "calendar") {
return renderCalendarWidget(widget); return (
<CalendarWidget
widgetId={widget.id}
editMode={editMode}
events={calendarEventsByWidget[widget.id] ?? []}
error={calendarErrorsByWidget[widget.id] ?? null}
sources={calendarSources}
selectedSourceIds={calendarSelectionsByWidget[widget.id] ?? []}
nextEventsCount={calendarNextEventsCountByWidget[widget.id] ?? 3}
onToggleSource={(wId, sId) => toggleCalendarSourceForWidget(wId, sId)}
onChangeEventsCount={(wId, count) => updateCalendarWidgetNextEventsCount(wId, count)}
/>
);
} }
if (widget.type === "domain-check") { if (widget.type === "domain-check") {
+264
View File
@@ -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}
</>
);
}
+449
View File
@@ -0,0 +1,449 @@
"use client";
import type { ReactNode } from "react";
import { useState } from "react";
type NoteBoardItem = {
id: string;
userId: string;
type: "note" | string;
title: string;
content: string;
x: number;
y: number;
w: number;
h: number;
createdAt: string;
updatedAt: string;
};
type NoteWidgetProps = {
note: NoteBoardItem | null;
editMode: boolean;
isEditing: boolean;
noteError: string | null;
onSave: (noteId: string, patch: { content: string }) => void;
onInitialize: () => void;
};
function getNoteTextarea(noteId: string): HTMLTextAreaElement | null {
if (typeof document === "undefined") {
return null;
}
return document.getElementById(`note-textarea-${noteId}`) as HTMLTextAreaElement | null;
}
function normalizeMarkdownHref(href: string): string {
const cleanHref = href.trim();
if (cleanHref.startsWith("/") || cleanHref.startsWith("#")) {
return cleanHref;
}
try {
const parsedUrl = new URL(cleanHref);
if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:" || parsedUrl.protocol === "mailto:") {
return parsedUrl.toString();
}
} catch {
return "#";
}
return "#";
}
function renderInlineMarkdown(text: string): ReactNode[] {
const nodes: ReactNode[] = [];
const pattern = /(\*\*[^*\n]+?\*\*|\*[^*\n]+?\*|`[^`\n]+?`|\[[^\]\n]+?\]\([^)]+?\))/g;
let lastIndex = 0;
let match: RegExpExecArray | null;
let key = 0;
while ((match = pattern.exec(text)) !== null) {
if (match.index > lastIndex) {
nodes.push(text.slice(lastIndex, match.index));
}
const token = match[0];
if (token.startsWith("**") && token.endsWith("**")) {
nodes.push(<strong key={`strong-${key}`}>{token.slice(2, -2)}</strong>);
} else if (token.startsWith("*") && token.endsWith("*")) {
nodes.push(<em key={`em-${key}`}>{token.slice(1, -1)}</em>);
} else if (token.startsWith("`") && token.endsWith("`")) {
nodes.push(<code key={`code-${key}`}>{token.slice(1, -1)}</code>);
} else {
const linkMatch = token.match(/^\[([^\]]+?)\]\(([^)]+?)\)$/);
if (linkMatch) {
nodes.push(
<a key={`link-${key}`} href={normalizeMarkdownHref(linkMatch[2])} target="_blank" rel="noreferrer">
{linkMatch[1]}
</a>
);
} else {
nodes.push(token);
}
}
key += 1;
lastIndex = pattern.lastIndex;
}
if (lastIndex < text.length) {
nodes.push(text.slice(lastIndex));
}
return nodes;
}
export default function NoteWidget({ note, editMode, isEditing, noteError, onSave, onInitialize }: NoteWidgetProps) {
const [localContent, setLocalContent] = useState<string | null>(null);
const content = localContent ?? note?.content ?? "";
function updateContentFromToolbar(nextContent: string, nextSelectionStart: number, nextSelectionEnd = nextSelectionStart) {
setLocalContent(nextContent);
onSave(note!.id, { content: nextContent });
window.setTimeout(() => {
const textarea = getNoteTextarea(note!.id);
if (!textarea) {
return;
}
textarea.focus();
textarea.setSelectionRange(nextSelectionStart, nextSelectionEnd);
}, 0);
}
function wrapSelection(before: string, after: string, placeholder: string) {
if (!note) return;
const textarea = getNoteTextarea(note.id);
const value = content;
const start = textarea?.selectionStart ?? value.length;
const end = textarea?.selectionEnd ?? value.length;
const selectedText = value.slice(start, end);
const beforeSelection = value.slice(Math.max(0, start - before.length), start);
const afterSelection = value.slice(end, end + after.length);
if (selectedText && beforeSelection === before && afterSelection === after) {
const nextContent = `${value.slice(0, start - before.length)}${selectedText}${value.slice(end + after.length)}`;
const nextStart = start - before.length;
const nextEnd = nextStart + selectedText.length;
updateContentFromToolbar(nextContent, nextStart, nextEnd);
return;
}
if (selectedText.startsWith(before) && selectedText.endsWith(after) && selectedText.length > before.length + after.length) {
const unwrappedText = selectedText.slice(before.length, selectedText.length - after.length);
const nextContent = `${value.slice(0, start)}${unwrappedText}${value.slice(end)}`;
updateContentFromToolbar(nextContent, start, start + unwrappedText.length);
return;
}
const insertedText = selectedText || placeholder;
const replacement = `${before}${insertedText}${after}`;
const nextContent = `${value.slice(0, start)}${replacement}${value.slice(end)}`;
const selectionStart = start + before.length;
const selectionEnd = selectionStart + insertedText.length;
updateContentFromToolbar(nextContent, selectionStart, selectionEnd);
}
function prefixLines(prefix: string, placeholder: string) {
if (!note) return;
const textarea = getNoteTextarea(note.id);
const value = content;
const start = textarea?.selectionStart ?? value.length;
const end = textarea?.selectionEnd ?? value.length;
const lineStart = value.lastIndexOf("\n", Math.max(0, start - 1)) + 1;
const lineEndCandidate = end === start ? end : end;
const nextLineBreak = value.indexOf("\n", lineEndCandidate);
const lineEnd = nextLineBreak === -1 || end === start ? lineEndCandidate : lineEndCandidate;
const selectedBlock = value.slice(lineStart, lineEnd) || placeholder;
const lines = selectedBlock.split("\n");
const isCheckboxMode = prefix === "- [ ] ";
const isOrderedMode = prefix === "1. ";
const isBulletMode = prefix === "- ";
const checkboxPattern = /^(\s*)[-*]\s+\[[ xX]\]\s+/;
const orderedPattern = /^(\s*)\d+\.\s+/;
const bulletPattern = /^(\s*)[-*]\s+(?!\[[ xX]\]\s+)/;
const alreadyFormatted = lines.every((line) => {
if (!line.trim()) return true;
if (isCheckboxMode) return checkboxPattern.test(line);
if (isOrderedMode) return orderedPattern.test(line);
if (isBulletMode) return bulletPattern.test(line);
return false;
});
const nextLines = lines.map((line, index) => {
if (!line.trim()) return line;
if (alreadyFormatted) {
if (isCheckboxMode) return line.replace(checkboxPattern, "$1");
if (isOrderedMode) return line.replace(orderedPattern, "$1");
if (isBulletMode) return line.replace(bulletPattern, "$1");
return line;
}
const cleanLine = line
.replace(checkboxPattern, "$1")
.replace(orderedPattern, "$1")
.replace(bulletPattern, "$1");
if (isOrderedMode) {
return cleanLine.replace(/^(\s*)/, `$1${index + 1}. `);
}
return cleanLine.replace(/^(\s*)/, `$1${prefix}`);
});
const prefixedBlock = nextLines.join("\n");
const nextContent = `${value.slice(0, lineStart)}${prefixedBlock}${value.slice(lineEnd)}`;
updateContentFromToolbar(nextContent, lineStart, lineStart + prefixedBlock.length);
}
function insertLink() {
if (!note) return;
const textarea = getNoteTextarea(note.id);
const value = content;
const start = textarea?.selectionStart ?? value.length;
const end = textarea?.selectionEnd ?? value.length;
const selectedText = value.slice(start, end) || "Linktext";
const replacement = `[${selectedText}](https://)`;
const nextContent = `${value.slice(0, start)}${replacement}${value.slice(end)}`;
const urlStart = start + selectedText.length + 3;
updateContentFromToolbar(nextContent, urlStart, urlStart + 8);
}
function toggleCheckbox(lineIndex: number) {
if (!note) return;
const lines = content.split("\n");
const line = lines[lineIndex] ?? "";
const match = line.match(/^(\s*[-*]\s+\[)([ xX])(\]\s+.*)$/);
if (!match) return;
const isChecked = match[2].toLowerCase() === "x";
lines[lineIndex] = `${match[1]}${isChecked ? " " : "x"}${match[3]}`;
const nextContent = lines.join("\n");
setLocalContent(nextContent);
onSave(note.id, { content: nextContent });
}
function renderMarkdownPreview() {
if (!content.trim()) {
return <p className="muted noteEmptyText">Noch keine Notiz.</p>;
}
const lines = content.split("\n");
const blocks: ReactNode[] = [];
let index = 0;
while (index < lines.length) {
const line = lines[index];
if (!line.trim()) {
blocks.push(<div className="noteMarkdownSpacer" key={`space-${index}`} />);
index += 1;
continue;
}
const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
if (headingMatch) {
const level = headingMatch[1].length;
const className = `noteMarkdownHeading noteMarkdownHeading${level}`;
blocks.push(
<div className={className} key={`heading-${index}`}>
{renderInlineMarkdown(headingMatch[2])}
</div>
);
index += 1;
continue;
}
const taskMatch = line.match(/^(\s*)[-*]\s+\[([ xX])\]\s+(.+)$/);
if (taskMatch) {
const tasks: Array<{ lineIndex: number; checked: boolean; text: string }> = [];
while (index < lines.length) {
const currentMatch = lines[index].match(/^(\s*)[-*]\s+\[([ xX])\]\s+(.+)$/);
if (!currentMatch) break;
tasks.push({
lineIndex: index,
checked: currentMatch[2].toLowerCase() === "x",
text: currentMatch[3]
});
index += 1;
}
blocks.push(
<div className="noteMarkdownChecklist" key={`tasks-${tasks[0]?.lineIndex ?? index}`}>
{tasks.map((task) => (
<label className="noteMarkdownTask" key={`task-${task.lineIndex}`}>
<input
type="checkbox"
checked={task.checked}
onChange={() => toggleCheckbox(task.lineIndex)}
/>
<span>{renderInlineMarkdown(task.text)}</span>
</label>
))}
</div>
);
continue;
}
const unorderedMatch = line.match(/^\s*[-*]\s+(?!\[[ xX]\]\s+)(.+)$/);
if (unorderedMatch) {
const items: Array<{ lineIndex: number; text: string }> = [];
while (index < lines.length) {
const currentMatch = lines[index].match(/^\s*[-*]\s+(?!\[[ xX]\]\s+)(.+)$/);
if (!currentMatch) break;
items.push({ lineIndex: index, text: currentMatch[1] });
index += 1;
}
blocks.push(
<ul className="noteMarkdownList" key={`ul-${items[0]?.lineIndex ?? index}`}>
{items.map((item) => (
<li key={`ul-item-${item.lineIndex}`}>{renderInlineMarkdown(item.text)}</li>
))}
</ul>
);
continue;
}
const orderedMatch = line.match(/^\s*\d+\.\s+(.+)$/);
if (orderedMatch) {
const items: Array<{ lineIndex: number; text: string }> = [];
while (index < lines.length) {
const currentMatch = lines[index].match(/^\s*\d+\.\s+(.+)$/);
if (!currentMatch) break;
items.push({ lineIndex: index, text: currentMatch[1] });
index += 1;
}
blocks.push(
<ol className="noteMarkdownList" key={`ol-${items[0]?.lineIndex ?? index}`}>
{items.map((item) => (
<li key={`ol-item-${item.lineIndex}`}>{renderInlineMarkdown(item.text)}</li>
))}
</ol>
);
continue;
}
blocks.push(
<p className="noteMarkdownParagraph" key={`p-${index}`}>
{renderInlineMarkdown(line)}
</p>
);
index += 1;
}
return <div className="noteMarkdownPreview widgetNoDrag">{blocks}</div>;
}
function renderToolbar() {
if (!note) return null;
return (
<div className="noteMarkdownToolbar widgetNoDrag" aria-label="Markdown-Werkzeuge">
<button type="button" className="noteMarkdownButton" onMouseDown={(e) => e.preventDefault()} onClick={() => wrapSelection("**", "**", "fett")} title="Fett">
B
</button>
<button type="button" className="noteMarkdownButton" onMouseDown={(e) => e.preventDefault()} onClick={() => wrapSelection("*", "*", "kursiv")} title="Kursiv">
<em>I</em>
</button>
<button type="button" className="noteMarkdownButton noteMarkdownButtonWide" onMouseDown={(e) => e.preventDefault()} onClick={() => prefixLines("- ", "Eintrag")} title="Aufzählung">
&bull; Liste
</button>
<button type="button" className="noteMarkdownButton noteMarkdownButtonWide" onMouseDown={(e) => e.preventDefault()} onClick={() => prefixLines("1. ", "Eintrag")} title="Nummerierte Liste">
1.
</button>
<button type="button" className="noteMarkdownButton noteMarkdownButtonWide" onMouseDown={(e) => e.preventDefault()} onClick={() => prefixLines("- [ ] ", "Aufgabe")} title="Checkbox">
&#9744;
</button>
<button type="button" className="noteMarkdownButton" onMouseDown={(e) => e.preventDefault()} onClick={() => wrapSelection("`", "`", "Code")} title="Inline-Code">
{"</>"}
</button>
<button type="button" className="noteMarkdownButton noteMarkdownButtonWide" onMouseDown={(e) => e.preventDefault()} onClick={() => insertLink()} title="Link">
Link
</button>
</div>
);
}
if (!note) {
return (
<div className="singleNoteWidget widgetNoDrag">
{noteError ? <p className="errorText">{noteError}</p> : null}
<button type="button" className="button buttonSecondary" onClick={onInitialize}>
Eintrag initialisieren
</button>
</div>
);
}
const noteIsEditing = editMode || isEditing;
if (!noteIsEditing) {
return renderMarkdownPreview();
}
return (
<div className="singleNoteWidget noteMarkdownEditor widgetNoDrag">
{noteError ? <p className="errorText">{noteError}</p> : null}
{renderToolbar()}
<textarea
id={`note-textarea-${note.id}`}
className="noteTextarea singleNoteTextarea"
value={content}
onChange={(event) => setLocalContent(event.target.value)}
onBlur={(event) => {
onSave(note.id, { content: event.target.value });
setLocalContent(null);
}}
placeholder="Notiz schreiben... Markdown wird unterstützt."
spellCheck={true}
/>
</div>
);
}