diff --git a/src/app/page.tsx b/src/app/page.tsx index 4e29107..24905a9 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,15 +1,17 @@ "use client"; 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 type { DashboardGridProps } from "@/components/DashboardGrid"; import type { DashboardGridWidget, DashboardLayoutItem } from "@/lib/dashboard-layout"; import { sortLayoutForPosition } from "@/lib/dashboard-layout"; import CalculatorWidget from "@/components/CalculatorWidget"; +import CalendarWidget from "@/components/CalendarWidget"; import ClockWidget from "@/components/ClockWidget"; -import FavoritesWidget from "@/components/FavoritesWidget"; import DomainCheckWidget from "@/components/DomainCheckWidget"; +import FavoritesWidget from "@/components/FavoritesWidget"; +import NoteWidget from "@/components/NoteWidget"; const DashboardGrid = dynamic(() => import("@/components/DashboardGrid"), { ssr: false, @@ -98,14 +100,6 @@ type CalendarWidgetCalendarConfig = { nextEventsCount: number; }; -type CalendarDay = { - key: string; - date: Date; - inCurrentMonth: boolean; - isToday: boolean; - events: CalendarEvent[]; -}; - type NoteBoardItem = { id: 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 { const source = user.displayName?.trim() || user.email.trim(); @@ -227,37 +187,6 @@ function getUserInitials(user: User): string { return source.slice(0, 2).toUpperCase(); } -function buildCalendarDays(monthDate: Date, eventsByDate: Map): 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 { return Math.max(min, Math.min(max, value)); } @@ -330,8 +259,6 @@ export default function DashboardPage() { const [calendarSources, setCalendarSources] = useState([]); const [calendarSelectionsByWidget, setCalendarSelectionsByWidget] = useState>({}); const [calendarNextEventsCountByWidget, setCalendarNextEventsCountByWidget] = useState>({}); - const [calendarMonth, setCalendarMonth] = useState(() => new Date()); - const [profileMenuOpen, setProfileMenuOpen] = useState(false); const [dashboardError, setDashboardError] = useState(null); const [openWidgetMenuId, setOpenWidgetMenuId] = useState(null); @@ -378,19 +305,6 @@ export default function DashboardPage() { const dashboardSubtitle = settings?.dashboardSubtitle?.trim() || user?.email || ""; const logoUrl = settings?.logoUrl?.trim() || "/logo.svg"; - function groupEventsByDate(events: CalendarEvent[]): Map { - const groupedEvents = new Map(); - - events.forEach((event) => { - const key = dateKey(new Date(event.start)); - const existingEvents = groupedEvents.get(key) ?? []; - - groupedEvents.set(key, [...existingEvents, event]); - }); - - return groupedEvents; - } - useEffect(() => { async function loadCurrentUser() { try { @@ -956,18 +870,6 @@ export default function DashboardPage() { 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) { if (widget.type !== "note" && widget.type !== "noteboard") { return null; @@ -1195,548 +1097,6 @@ export default function DashboardPage() { 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({token.slice(2, -2)}); - } else if (token.startsWith("*") && token.endsWith("*")) { - nodes.push({token.slice(1, -1)}); - } else if (token.startsWith("`") && token.endsWith("`")) { - nodes.push({token.slice(1, -1)}); - } else { - const linkMatch = token.match(/^\[([^\]]+?)\]\(([^)]+?)\)$/); - - if (linkMatch) { - nodes.push( - - {linkMatch[1]} - - ); - } 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

Noch keine Notiz.

; - } - - const lines = content.split("\n"); - const blocks: ReactNode[] = []; - let index = 0; - - while (index < lines.length) { - const line = lines[index]; - - if (!line.trim()) { - blocks.push(
); - index += 1; - continue; - } - - const headingMatch = line.match(/^(#{1,3})\s+(.+)$/); - - if (headingMatch) { - const level = headingMatch[1].length; - const className = `noteMarkdownHeading noteMarkdownHeading${level}`; - - blocks.push( -
- {renderInlineMarkdown(headingMatch[2])} -
- ); - - 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( -
- {tasks.map((task) => ( - - ))} -
- ); - - 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( -
    - {items.map((item) => ( -
  • {renderInlineMarkdown(item.text)}
  • - ))} -
- ); - - 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( -
    - {items.map((item) => ( -
  1. {renderInlineMarkdown(item.text)}
  2. - ))} -
- ); - - continue; - } - - blocks.push( -

- {renderInlineMarkdown(line)} -

- ); - - index += 1; - } - - return
{blocks}
; - } - - function renderNoteMarkdownToolbar(note: NoteBoardItem) { - return ( -
- - - - - - - - - - - - - -
- ); - } - - function renderNoteWidget(widget: Widget) { - const note = getNoteForWidget(widget); - - if (!note) { - return ( -
- {noteError ?

{noteError}

: null} - - -
- ); - } - - const noteIsEditing = editMode || editingNoteWidgetId === widget.id; - - if (!noteIsEditing) { - return renderNoteMarkdownPreview(note); - } - - return ( -
- {noteError ?

{noteError}

: null} - - {renderNoteMarkdownToolbar(note)} - -