From f3d350e64ecf706d0bf06887e140e2b93bb7532e Mon Sep 17 00:00:00 2001 From: Meik Date: Sat, 17 Jan 2026 20:51:25 +0100 Subject: [PATCH] Aktueller Stand --- components/CalendarBoard.tsx | 304 ++++++++++++++++++++++++++--------- 1 file changed, 231 insertions(+), 73 deletions(-) diff --git a/components/CalendarBoard.tsx b/components/CalendarBoard.tsx index 7719c74..d918848 100644 --- a/components/CalendarBoard.tsx +++ b/components/CalendarBoard.tsx @@ -39,6 +39,15 @@ type DateBucket = "past" | "today" | "tomorrow" | "future"; type Pane = "calendar" | "list" | "map"; const MAP_FILTER_STORAGE_KEY = "mapFilters"; +const CALENDAR_VIEW_STORAGE_KEY = "calendarViewType"; +const LIST_FILTER_STORAGE_KEY = "listQuickFilters"; +const DEFAULT_CALENDAR_VIEW = "dayGridMonth"; +const ALLOWED_CALENDAR_VIEWS = [ + "dayGridMonth", + "timeGridWeek", + "dayGridYear", + "listWeek" +]; export default function CalendarBoard() { const { data, status } = useSession(); @@ -74,27 +83,30 @@ export default function CalendarBoard() { const [prefillStartAt, setPrefillStartAt] = useState(null); const [viewOrder, setViewOrder] = useState>([ "list", - "map", - "calendar" + "calendar", + "map" ]); const [collapsed, setCollapsed] = useState>({ calendar: false, list: false, map: false }); - const [activeRange, setActiveRange] = useState<{ - start: Date; - end: Date; - viewType: string; - } | null>(() => { - const now = new Date(); - return { - start: new Date(now.getFullYear(), 0, 1), - end: new Date(now.getFullYear() + 1, 0, 1), - viewType: "dayGridYear" - }; + const [listQuickRange, setListQuickRange] = useState< + "next7" | "next30" | "nextYear" | null + >("next30"); + const [hidePastInList, setHidePastInList] = useState(true); + const [initialView, setInitialView] = useState(() => { + if (typeof window === "undefined") return DEFAULT_CALENDAR_VIEW; + try { + const stored = window.localStorage.getItem(CALENDAR_VIEW_STORAGE_KEY); + if (stored && ALLOWED_CALENDAR_VIEWS.includes(stored)) { + return stored; + } + } catch { + // ignore + } + return DEFAULT_CALENDAR_VIEW; }); - const [initialView, setInitialView] = useState("dayGridYear"); const [dragSource, setDragSource] = useState(null); const [dragOver, setDragOver] = useState(null); const [isPointerDragging, setIsPointerDragging] = useState(false); @@ -310,6 +322,39 @@ export default function CalendarBoard() { } }, []); + useEffect(() => { + try { + const stored = window.localStorage.getItem(LIST_FILTER_STORAGE_KEY); + if (!stored) return; + const parsed = JSON.parse(stored); + if ( + parsed && + typeof parsed === "object" && + (parsed.quickRange === "next7" || + parsed.quickRange === "next30" || + parsed.quickRange === "nextYear" || + parsed.quickRange === null) && + typeof parsed.hidePast === "boolean" + ) { + setListQuickRange(parsed.quickRange); + setHidePastInList(parsed.hidePast); + } + } catch { + // ignore + } + }, []); + + useEffect(() => { + try { + window.localStorage.setItem( + LIST_FILTER_STORAGE_KEY, + JSON.stringify({ quickRange: listQuickRange, hidePast: hidePastInList }) + ); + } catch { + // ignore + } + }, [listQuickRange, hidePastInList]); + useEffect(() => { try { window.localStorage.setItem( @@ -527,25 +572,14 @@ export default function CalendarBoard() { setFormOpen(true); }; - const applyTruncatedTitles = (root: HTMLElement) => { + const applyEventTooltip = (root: HTMLElement, title: string) => { const elements = Array.from( root.querySelectorAll(".event-shell .truncate") ); elements.forEach((element) => { - const text = element.textContent?.trim(); - if (!text) { - element.removeAttribute("title"); - return; - } - const isTruncated = - element.scrollWidth > element.clientWidth || - element.scrollHeight > element.clientHeight; - if (isTruncated) { - element.setAttribute("title", text); - } else { - element.removeAttribute("title"); - } + element.removeAttribute("title"); }); + root.setAttribute("title", title); }; const calendarEvents = useMemo( @@ -756,6 +790,41 @@ export default function CalendarBoard() { return sorted; }, [events, query, categoryFilter, sortKey, sortDir]); + const startOfDay = (value: Date) => + new Date(value.getFullYear(), value.getMonth(), value.getDate()); + + const addDays = (value: Date, amount: number) => { + const next = new Date(value); + next.setDate(next.getDate() + amount); + return next; + }; + + const listRange = useMemo(() => { + if (!listQuickRange && !hidePastInList) return null; + const today = startOfDay(new Date()); + const start = hidePastInList + ? today + : listQuickRange + ? addDays(today, -1) + : today; + let end: Date | null = null; + if (listQuickRange === "next7") { + end = addDays(today, 7); + } else if (listQuickRange === "next30") { + end = addDays(today, 30); + } else if (listQuickRange === "nextYear") { + end = addDays(today, 365); + } + return { start, end }; + }, [listQuickRange, hidePastInList]); + + const filterButtonClass = (active: boolean) => + `rounded-full border px-3 py-1 text-xs font-semibold transition ${ + active + ? "border-slate-900 bg-slate-900 text-white" + : "border-slate-200 text-slate-700 hover:bg-slate-100" + }`; + const categoryOptions = useMemo(() => { const list = events .map((event) => event.category?.name || "Ohne Kategorie") @@ -861,18 +930,38 @@ export default function CalendarBoard() { }; const displayedEvents = useMemo(() => { - if (!activeRange) { + if (!listRange) { return filteredEvents; } return filteredEvents.filter((event) => { const start = new Date(event.startAt); - return start >= activeRange.start && start < activeRange.end; + if (Number.isNaN(start.getTime())) return false; + if (start < listRange.start) return false; + if (listRange.end && start >= listRange.end) return false; + return true; }); - }, [filteredEvents, activeRange]); + }, [filteredEvents, listRange]); + + const tableWidths = isAdmin + ? { + checkbox: "w-[5%]", + date: "w-[18%]", + title: "w-[26%]", + category: "w-[16%]", + location: "w-[23%]", + actions: "w-[12%]" + } + : { + date: "w-[20%]", + title: "w-[30%]", + category: "w-[18%]", + location: "w-[24%]", + actions: "w-[8%]" + }; useEffect(() => { setBulkSelection((prev) => (prev.size > 0 ? new Set() : prev)); - }, [query, categoryFilter, activeRange]); + }, [query, categoryFilter, listRange]); const toggleSort = (nextKey: string) => { let nextDir: "asc" | "desc" = "asc"; @@ -1012,7 +1101,7 @@ export default function CalendarBoard() { headerToolbar={{ left: "prev,next today", center: "title", - right: "dayGridMonth,timeGridWeek,dayGridYear,listWeek" + right: "timeGridWeek,dayGridMonth,dayGridYear,listWeek" }} views={{ dayGridMonth: { buttonText: "Monat" }, @@ -1084,15 +1173,34 @@ export default function CalendarBoard() { }} eventDidMount={(info) => { if (!info.el) return; - requestAnimationFrame(() => applyTruncatedTitles(info.el)); + const startLabel = info.event.allDay + ? "Ganztägig" + : info.event.start + ? info.event.start.toLocaleTimeString("de-DE", { + hour: "2-digit", + minute: "2-digit" + }) + : ""; + const tooltip = startLabel + ? `${info.event.title} - ${startLabel}` + : info.event.title; + requestAnimationFrame(() => + applyEventTooltip(info.el, tooltip) + ); }} height="auto" datesSet={(arg) => { - setActiveRange({ - start: arg.start, - end: arg.end, - viewType: arg.view.type - }); + if (ALLOWED_CALENDAR_VIEWS.includes(arg.view.type)) { + setInitialView(arg.view.type); + try { + window.localStorage.setItem( + CALENDAR_VIEW_STORAGE_KEY, + arg.view.type + ); + } catch { + // ignore + } + } }} /> )} @@ -1363,12 +1471,56 @@ export default function CalendarBoard() { {!collapsed.list && ( <>
- setQuery(event.target.value)} - placeholder="Suchen..." - className="min-w-[220px] rounded-xl border border-slate-300 px-3 py-2 text-sm" - /> +
+ setQuery(event.target.value)} + placeholder="Suchen..." + className="min-w-[220px] rounded-xl border border-slate-300 px-3 py-2 text-sm" + /> +
+ + + + +
+
{isAdmin && (