Aktueller Stand
This commit is contained in:
@@ -23,7 +23,8 @@ type EventItem = {
|
||||
startAt: string;
|
||||
endAt?: string | null;
|
||||
status: string;
|
||||
category?: { id: string; name: string } | null;
|
||||
publicOverride?: boolean | null;
|
||||
category?: { id: string; name: string; isPublic?: boolean } | null;
|
||||
};
|
||||
|
||||
type ViewItem = {
|
||||
@@ -35,11 +36,12 @@ type ViewItem = {
|
||||
};
|
||||
|
||||
type DateBucket = "past" | "today" | "tomorrow" | "future";
|
||||
type Pane = "calendar" | "list" | "map";
|
||||
|
||||
const MAP_FILTER_STORAGE_KEY = "mapFilters";
|
||||
|
||||
export default function CalendarBoard() {
|
||||
const { data } = useSession();
|
||||
const { data, status } = useSession();
|
||||
const [events, setEvents] = useState<EventItem[]>([]);
|
||||
const [view, setView] = useState<ViewItem | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -60,6 +62,7 @@ export default function CalendarBoard() {
|
||||
const [editError, setEditError] = useState<string | null>(null);
|
||||
const [bulkSelection, setBulkSelection] = useState<Set<string>>(new Set());
|
||||
const [mapFullscreen, setMapFullscreen] = useState(false);
|
||||
const [publicAccessEnabled, setPublicAccessEnabled] = useState<boolean | null>(null);
|
||||
const [isDarkTheme, setIsDarkTheme] = useState(false);
|
||||
const [mapDateFilter, setMapDateFilter] = useState<Set<DateBucket>>(
|
||||
new Set(["past", "today", "tomorrow", "future"])
|
||||
@@ -69,14 +72,12 @@ export default function CalendarBoard() {
|
||||
);
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [prefillStartAt, setPrefillStartAt] = useState<string | null>(null);
|
||||
const [viewOrder, setViewOrder] = useState<
|
||||
Array<"calendar" | "list" | "map">
|
||||
>(["calendar", "map", "list"]);
|
||||
const [collapsed, setCollapsed] = useState<{
|
||||
calendar: boolean;
|
||||
list: boolean;
|
||||
map: boolean;
|
||||
}>({
|
||||
const [viewOrder, setViewOrder] = useState<Array<Pane>>([
|
||||
"list",
|
||||
"map",
|
||||
"calendar"
|
||||
]);
|
||||
const [collapsed, setCollapsed] = useState<Record<Pane, boolean>>({
|
||||
calendar: false,
|
||||
list: false,
|
||||
map: false
|
||||
@@ -85,14 +86,21 @@ export default function CalendarBoard() {
|
||||
start: Date;
|
||||
end: Date;
|
||||
viewType: string;
|
||||
} | null>(null);
|
||||
const [initialView, setInitialView] = useState("dayGridMonth");
|
||||
const [dragSource, setDragSource] = useState<
|
||||
"calendar" | "list" | "map" | null
|
||||
>(null);
|
||||
const [dragOver, setDragOver] = useState<
|
||||
"calendar" | "list" | "map" | null
|
||||
>(null);
|
||||
} | null>(() => {
|
||||
const now = new Date();
|
||||
return {
|
||||
start: new Date(now.getFullYear(), 0, 1),
|
||||
end: new Date(now.getFullYear() + 1, 0, 1),
|
||||
viewType: "dayGridYear"
|
||||
};
|
||||
});
|
||||
const [initialView, setInitialView] = useState("dayGridYear");
|
||||
const [dragSource, setDragSource] = useState<Pane | null>(null);
|
||||
const [dragOver, setDragOver] = useState<Pane | null>(null);
|
||||
const [isPointerDragging, setIsPointerDragging] = useState(false);
|
||||
const pointerDragRef = useRef<{ pointerId: number; source: Pane } | null>(null);
|
||||
const dragOverRef = useRef<Pane | null>(null);
|
||||
const dragHandleRef = useRef<HTMLDivElement | null>(null);
|
||||
const calendarRef = useRef<HTMLDivElement | null>(null);
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const mapRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -101,22 +109,27 @@ export default function CalendarBoard() {
|
||||
const lastActiveElementRef = useRef<HTMLElement | null>(null);
|
||||
const isAdmin =
|
||||
data?.user?.role === "ADMIN" || data?.user?.role === "SUPERADMIN";
|
||||
const canManageView = Boolean(view && data?.user);
|
||||
const showPublicControls = publicAccessEnabled === true;
|
||||
|
||||
const fetchEvents = async () => {
|
||||
const fetchEvents = async (includeView: boolean) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [eventsResponse, viewResponse] = await Promise.all([
|
||||
fetch("/api/events"),
|
||||
fetch("/api/views/default")
|
||||
]);
|
||||
const requests = [fetch("/api/events")];
|
||||
if (includeView) {
|
||||
requests.push(fetch("/api/views/default"));
|
||||
}
|
||||
const [eventsResponse, viewResponse] = await Promise.all(requests);
|
||||
if (!eventsResponse.ok) {
|
||||
throw new Error("Events konnten nicht geladen werden.");
|
||||
}
|
||||
const payload = await eventsResponse.json();
|
||||
setEvents(payload);
|
||||
if (viewResponse.ok) {
|
||||
if (includeView && viewResponse?.ok) {
|
||||
setView(await viewResponse.json());
|
||||
} else {
|
||||
setView(null);
|
||||
}
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
@@ -126,10 +139,9 @@ export default function CalendarBoard() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.user) {
|
||||
fetchEvents();
|
||||
}
|
||||
}, [data?.user]);
|
||||
if (status === "loading") return;
|
||||
fetchEvents(Boolean(data?.user));
|
||||
}, [data?.user, status]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadKey = async () => {
|
||||
@@ -139,6 +151,7 @@ export default function CalendarBoard() {
|
||||
const payload = await response.json();
|
||||
setPlacesKey(payload.apiKey || "");
|
||||
setPlacesProvider(payload.provider === "google" ? "google" : "osm");
|
||||
setPublicAccessEnabled(payload.publicAccessEnabled !== false);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -166,22 +179,11 @@ export default function CalendarBoard() {
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
fetchEvents();
|
||||
fetchEvents(Boolean(data?.user));
|
||||
};
|
||||
window.addEventListener("views-updated", handler);
|
||||
return () => window.removeEventListener("views-updated", handler);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const stored = window.localStorage.getItem("calendarLastView");
|
||||
if (stored) {
|
||||
setInitialView(stored);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
}, [data?.user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document === "undefined") return;
|
||||
@@ -331,7 +333,7 @@ export default function CalendarBoard() {
|
||||
["calendar", "list", "map"].includes(String(item))
|
||||
)
|
||||
)
|
||||
] as Array<"calendar" | "list" | "map">;
|
||||
] as Array<Pane>;
|
||||
if (normalized.includes("calendar") && normalized.includes("list")) {
|
||||
if (!normalized.includes("map")) {
|
||||
normalized.splice(1, 0, "map");
|
||||
@@ -366,7 +368,7 @@ export default function CalendarBoard() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const persistOrder = (nextOrder: Array<"calendar" | "list" | "map">) => {
|
||||
const persistOrder = (nextOrder: Array<Pane>) => {
|
||||
setViewOrder(nextOrder);
|
||||
try {
|
||||
window.localStorage.setItem("calendarViewOrder", JSON.stringify(nextOrder));
|
||||
@@ -375,7 +377,7 @@ export default function CalendarBoard() {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleCollapse = (section: "calendar" | "list" | "map") => {
|
||||
const toggleCollapse = (section: Pane) => {
|
||||
const next = { ...collapsed, [section]: !collapsed[section] };
|
||||
setCollapsed(next);
|
||||
try {
|
||||
@@ -385,10 +387,7 @@ export default function CalendarBoard() {
|
||||
}
|
||||
};
|
||||
|
||||
const swapOrder = (
|
||||
source: "calendar" | "list" | "map",
|
||||
target: "calendar" | "list" | "map"
|
||||
) => {
|
||||
const swapOrder = (source: Pane, target: Pane) => {
|
||||
if (source === target) return;
|
||||
const next = [...viewOrder];
|
||||
const sourceIndex = next.indexOf(source);
|
||||
@@ -401,7 +400,7 @@ export default function CalendarBoard() {
|
||||
|
||||
const onDragStart = (
|
||||
event: React.DragEvent<HTMLDivElement>,
|
||||
section: "calendar" | "list" | "map"
|
||||
section: Pane
|
||||
) => {
|
||||
event.dataTransfer.setData("text/plain", section);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
@@ -418,7 +417,7 @@ export default function CalendarBoard() {
|
||||
}
|
||||
};
|
||||
|
||||
const onDragEnter = (target: "calendar" | "list" | "map") => {
|
||||
const onDragEnter = (target: Pane) => {
|
||||
setDragOver(target);
|
||||
};
|
||||
|
||||
@@ -427,10 +426,7 @@ export default function CalendarBoard() {
|
||||
setDragOver(null);
|
||||
};
|
||||
|
||||
const onDrop = (
|
||||
event: React.DragEvent<HTMLDivElement>,
|
||||
target: "calendar" | "list" | "map"
|
||||
) => {
|
||||
const onDrop = (event: React.DragEvent<HTMLDivElement>, target: Pane) => {
|
||||
event.preventDefault();
|
||||
const source = event.dataTransfer.getData("text/plain") as
|
||||
| "calendar"
|
||||
@@ -443,6 +439,83 @@ export default function CalendarBoard() {
|
||||
setDragOver(null);
|
||||
};
|
||||
|
||||
const getPaneFromPoint = (x: number, y: number) => {
|
||||
if (typeof document === "undefined") return null;
|
||||
const element = document.elementFromPoint(x, y);
|
||||
const card = element?.closest<HTMLElement>("[data-pane]");
|
||||
const pane = card?.dataset.pane as Pane | undefined;
|
||||
if (pane === "calendar" || pane === "list" || pane === "map") {
|
||||
return pane;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const finishPointerDrag = () => {
|
||||
const current = pointerDragRef.current;
|
||||
const handle = dragHandleRef.current;
|
||||
if (current && handle) {
|
||||
try {
|
||||
handle.releasePointerCapture(current.pointerId);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
pointerDragRef.current = null;
|
||||
dragOverRef.current = null;
|
||||
dragHandleRef.current = null;
|
||||
setDragSource(null);
|
||||
setDragOver(null);
|
||||
setIsPointerDragging(false);
|
||||
};
|
||||
|
||||
const onPointerDragStart = (
|
||||
event: React.PointerEvent<HTMLDivElement>,
|
||||
section: Pane
|
||||
) => {
|
||||
if (event.pointerType === "mouse") return;
|
||||
event.preventDefault();
|
||||
pointerDragRef.current = { pointerId: event.pointerId, source: section };
|
||||
dragOverRef.current = section;
|
||||
dragHandleRef.current = event.currentTarget;
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
setDragSource(section);
|
||||
setDragOver(section);
|
||||
setIsPointerDragging(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPointerDragging) return;
|
||||
const handleMove = (event: PointerEvent) => {
|
||||
const current = pointerDragRef.current;
|
||||
if (!current || event.pointerId !== current.pointerId) return;
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
const target = getPaneFromPoint(event.clientX, event.clientY);
|
||||
if (target !== dragOverRef.current) {
|
||||
dragOverRef.current = target;
|
||||
setDragOver(target);
|
||||
}
|
||||
};
|
||||
const handleUp = (event: PointerEvent) => {
|
||||
const current = pointerDragRef.current;
|
||||
if (!current || event.pointerId !== current.pointerId) return;
|
||||
const target = dragOverRef.current;
|
||||
if (target && target !== current.source) {
|
||||
swapOrder(current.source, target);
|
||||
}
|
||||
finishPointerDrag();
|
||||
};
|
||||
window.addEventListener("pointermove", handleMove);
|
||||
window.addEventListener("pointerup", handleUp);
|
||||
window.addEventListener("pointercancel", handleUp);
|
||||
return () => {
|
||||
window.removeEventListener("pointermove", handleMove);
|
||||
window.removeEventListener("pointerup", handleUp);
|
||||
window.removeEventListener("pointercancel", handleUp);
|
||||
};
|
||||
}, [isPointerDragging, swapOrder]);
|
||||
|
||||
const openFormForDate = (date: Date | null) => {
|
||||
if (date) {
|
||||
const prefill = new Date(date);
|
||||
@@ -499,7 +572,7 @@ export default function CalendarBoard() {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ eventId })
|
||||
});
|
||||
await fetchEvents();
|
||||
await fetchEvents(Boolean(data?.user));
|
||||
window.dispatchEvent(new Event("views-updated"));
|
||||
};
|
||||
|
||||
@@ -519,6 +592,14 @@ export default function CalendarBoard() {
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
const parsePublicOverride = (value: FormDataEntryValue | null) => {
|
||||
if (!value) return null;
|
||||
const raw = String(value);
|
||||
if (raw === "public") return true;
|
||||
if (raw === "private") return false;
|
||||
return null;
|
||||
};
|
||||
|
||||
const updateEvent = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setEditStatus(null);
|
||||
@@ -526,7 +607,7 @@ export default function CalendarBoard() {
|
||||
if (!editEvent) return;
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const payload = {
|
||||
const payload: Record<string, unknown> = {
|
||||
title: formData.get("title"),
|
||||
description: formData.get("description"),
|
||||
location: formData.get("location"),
|
||||
@@ -537,6 +618,11 @@ export default function CalendarBoard() {
|
||||
endAt: toIsoString(formData.get("endAt")),
|
||||
categoryId: formData.get("categoryId")
|
||||
};
|
||||
if (showPublicControls) {
|
||||
payload.publicOverride = parsePublicOverride(
|
||||
formData.get("publicOverride")
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/events/${editEvent.id}`, {
|
||||
method: "PATCH",
|
||||
@@ -553,7 +639,7 @@ export default function CalendarBoard() {
|
||||
setEditStatus("Termin aktualisiert.");
|
||||
setIsEditOpen(false);
|
||||
setEditEvent(null);
|
||||
await fetchEvents();
|
||||
await fetchEvents(Boolean(data?.user));
|
||||
};
|
||||
|
||||
const deleteEvent = async (eventId: string) => {
|
||||
@@ -568,7 +654,7 @@ export default function CalendarBoard() {
|
||||
setEditError(null);
|
||||
setIsEditOpen(false);
|
||||
setEditEvent(null);
|
||||
await fetchEvents();
|
||||
await fetchEvents(Boolean(data?.user));
|
||||
};
|
||||
|
||||
const toggleBulkSelection = (eventId: string) => {
|
||||
@@ -603,7 +689,7 @@ export default function CalendarBoard() {
|
||||
)
|
||||
);
|
||||
setBulkSelection(new Set());
|
||||
await fetchEvents();
|
||||
await fetchEvents(Boolean(data?.user));
|
||||
};
|
||||
|
||||
|
||||
@@ -787,52 +873,68 @@ export default function CalendarBoard() {
|
||||
}
|
||||
};
|
||||
|
||||
if (!data?.user) {
|
||||
return (
|
||||
<div className="card-muted text-center">
|
||||
<p className="text-slate-700">
|
||||
Bitte anmelden, um die Vereinskalender zu sehen.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="space-y-4 fade-up">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">Kalender</p>
|
||||
<h2 className="text-2xl font-semibold">Termine im Blick</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{data?.user && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-accent inline-flex items-center gap-2"
|
||||
onClick={() => openFormForDate(null)}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M12 5v14M5 12h14" strokeLinecap="round" />
|
||||
</svg>
|
||||
Termin
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn-accent"
|
||||
onClick={() => openFormForDate(null)}
|
||||
onClick={() => fetchEvents(Boolean(data?.user))}
|
||||
className="btn-ghost p-2"
|
||||
aria-label="Aktualisieren"
|
||||
title="Aktualisieren"
|
||||
>
|
||||
Neuer Termin
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={fetchEvents}
|
||||
className="btn-ghost"
|
||||
>
|
||||
Aktualisieren
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path
|
||||
d="M20 12a8 8 0 1 1-2.34-5.66"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path d="M20 4v6h-6" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<EventForm
|
||||
variant="inline"
|
||||
showTrigger={false}
|
||||
open={formOpen}
|
||||
onOpenChange={(open) => {
|
||||
setFormOpen(open);
|
||||
if (!open) {
|
||||
setPrefillStartAt(null);
|
||||
}
|
||||
}}
|
||||
prefillStartAt={prefillStartAt}
|
||||
/>
|
||||
{data?.user && (
|
||||
<EventForm
|
||||
variant="inline"
|
||||
showTrigger={false}
|
||||
open={formOpen}
|
||||
onOpenChange={(open) => {
|
||||
setFormOpen(open);
|
||||
if (!open) {
|
||||
setPrefillStartAt(null);
|
||||
}
|
||||
}}
|
||||
prefillStartAt={prefillStartAt}
|
||||
/>
|
||||
)}
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
{loading && <p className="text-sm text-slate-500">Lade Termine...</p>}
|
||||
{viewOrder.map((section) => {
|
||||
@@ -841,7 +943,8 @@ export default function CalendarBoard() {
|
||||
<div
|
||||
key="calendar"
|
||||
ref={calendarRef}
|
||||
className={`card drag-card ${
|
||||
data-pane="calendar"
|
||||
className={`card drag-card calendar-pane ${
|
||||
dragSource === "calendar" ? "dragging" : ""
|
||||
} ${
|
||||
dragOver === "calendar" && dragSource && dragSource !== "calendar"
|
||||
@@ -871,6 +974,7 @@ export default function CalendarBoard() {
|
||||
draggable
|
||||
onDragStart={(event) => onDragStart(event, "calendar")}
|
||||
onDragEnd={onDragEnd}
|
||||
onPointerDown={(event) => onPointerDragStart(event, "calendar")}
|
||||
title="Zum Tauschen ziehen"
|
||||
aria-label="Kalender verschieben"
|
||||
>
|
||||
@@ -909,18 +1013,20 @@ export default function CalendarBoard() {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{arg.dayNumberText}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-slate-200 bg-white/80 px-2 py-0.5 text-xs text-slate-600 hover:bg-slate-100"
|
||||
aria-label={`Termin am ${arg.date.toLocaleDateString("de-DE")} anlegen`}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
openFormForDate(arg.date);
|
||||
}}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
{data?.user && (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-slate-200 bg-white/80 px-2 py-0.5 text-xs text-slate-600 hover:bg-slate-100"
|
||||
aria-label={`Termin am ${arg.date.toLocaleDateString("de-DE")} anlegen`}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
openFormForDate(arg.date);
|
||||
}}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
@@ -941,15 +1047,17 @@ export default function CalendarBoard() {
|
||||
{arg.event.extendedProps.category || "Ohne Kategorie"}
|
||||
</div>
|
||||
</div>
|
||||
<ViewToggleButton
|
||||
isSelected={isSelected}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
toggleEvent(arg.event.id);
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
{canManageView && (
|
||||
<ViewToggleButton
|
||||
isSelected={isSelected}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
toggleEvent(arg.event.id);
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
@@ -960,11 +1068,6 @@ export default function CalendarBoard() {
|
||||
end: arg.end,
|
||||
viewType: arg.view.type
|
||||
});
|
||||
try {
|
||||
window.localStorage.setItem("calendarLastView", arg.view.type);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -977,6 +1080,7 @@ export default function CalendarBoard() {
|
||||
<div
|
||||
key="map"
|
||||
ref={mapRef}
|
||||
data-pane="map"
|
||||
className={`card drag-card ${
|
||||
dragSource === "map" ? "dragging" : ""
|
||||
} ${
|
||||
@@ -1014,6 +1118,7 @@ export default function CalendarBoard() {
|
||||
draggable
|
||||
onDragStart={(event) => onDragStart(event, "map")}
|
||||
onDragEnd={onDragEnd}
|
||||
onPointerDown={(event) => onPointerDragStart(event, "map")}
|
||||
title="Zum Tauschen ziehen"
|
||||
aria-label="Karte verschieben"
|
||||
>
|
||||
@@ -1193,6 +1298,7 @@ export default function CalendarBoard() {
|
||||
<div
|
||||
key="list"
|
||||
ref={listRef}
|
||||
data-pane="list"
|
||||
className={`card space-y-4 drag-card ${
|
||||
dragSource === "list" ? "dragging" : ""
|
||||
} ${
|
||||
@@ -1205,13 +1311,13 @@ export default function CalendarBoard() {
|
||||
onDrop={(event) => onDrop(event, "list")}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Liste</h3>
|
||||
<h3 className="text-sm font-semibold text-slate-700">Termine</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-slate-200 px-2 py-1 text-xs text-slate-600"
|
||||
onClick={() => toggleCollapse("list")}
|
||||
aria-label={collapsed.list ? "Liste aufklappen" : "Liste zuklappen"}
|
||||
aria-label={collapsed.list ? "Termine aufklappen" : "Termine zuklappen"}
|
||||
title={collapsed.list ? "Aufklappen" : "Zuklappen"}
|
||||
>
|
||||
{collapsed.list ? <IconChevronDown /> : <IconChevronUp />}
|
||||
@@ -1221,8 +1327,9 @@ export default function CalendarBoard() {
|
||||
draggable
|
||||
onDragStart={(event) => onDragStart(event, "list")}
|
||||
onDragEnd={onDragEnd}
|
||||
onPointerDown={(event) => onPointerDragStart(event, "list")}
|
||||
title="Zum Tauschen ziehen"
|
||||
aria-label="Liste verschieben"
|
||||
aria-label="Termine verschieben"
|
||||
>
|
||||
<IconGrip />
|
||||
</div>
|
||||
@@ -1413,10 +1520,12 @@ export default function CalendarBoard() {
|
||||
</td>
|
||||
<td className="py-3 pr-3">
|
||||
<div className="flex flex-nowrap gap-1">
|
||||
<ViewToggleButton
|
||||
isSelected={selectedEventIds.has(event.id)}
|
||||
onClick={() => toggleEvent(event.id)}
|
||||
/>
|
||||
{canManageView && (
|
||||
<ViewToggleButton
|
||||
isSelected={selectedEventIds.has(event.id)}
|
||||
onClick={() => toggleEvent(event.id)}
|
||||
/>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -1580,7 +1689,7 @@ export default function CalendarBoard() {
|
||||
Beschreibung
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
{detailsEvent.description}
|
||||
{renderLinkedText(detailsEvent.description)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -1594,22 +1703,6 @@ export default function CalendarBoard() {
|
||||
>
|
||||
Google Maps
|
||||
</a>
|
||||
<a
|
||||
className="btn-ghost"
|
||||
href={`https://maps.apple.com/?ll=${detailsEvent.locationLat},${detailsEvent.locationLng}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Apple Karten
|
||||
</a>
|
||||
<a
|
||||
className="btn-ghost"
|
||||
href={`https://www.openstreetmap.org/?mlat=${detailsEvent.locationLat}&mlon=${detailsEvent.locationLng}#map=17/${detailsEvent.locationLat}/${detailsEvent.locationLng}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
OpenStreetMap
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1738,6 +1831,45 @@ export default function CalendarBoard() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{showPublicControls && (
|
||||
<div className="rounded-xl border border-slate-200 bg-slate-50/60 p-3 text-sm text-slate-700 md:col-span-2">
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">
|
||||
Öffentlich
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap gap-3">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="publicOverride"
|
||||
value="inherit"
|
||||
defaultChecked={
|
||||
editEvent.publicOverride !== true &&
|
||||
editEvent.publicOverride !== false
|
||||
}
|
||||
/>
|
||||
Kategorie übernehmen
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="publicOverride"
|
||||
value="public"
|
||||
defaultChecked={editEvent.publicOverride === true}
|
||||
/>
|
||||
Öffentlich
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="publicOverride"
|
||||
value="private"
|
||||
defaultChecked={editEvent.publicOverride === false}
|
||||
/>
|
||||
Nicht öffentlich
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
name="description"
|
||||
@@ -1973,6 +2105,47 @@ function formatLocation(value?: string | null) {
|
||||
return main || value;
|
||||
}
|
||||
|
||||
function renderLinkedText(value: string) {
|
||||
const regex = /\b(https?:\/\/[^\s<]+|www\.[^\s<]+)/gi;
|
||||
const parts: Array<string | JSX.Element> = [];
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(value)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
parts.push(value.slice(lastIndex, match.index));
|
||||
}
|
||||
let rawUrl = match[0];
|
||||
let trailing = "";
|
||||
while (/[),.!?]$/.test(rawUrl)) {
|
||||
trailing = rawUrl.slice(-1) + trailing;
|
||||
rawUrl = rawUrl.slice(0, -1);
|
||||
}
|
||||
const href = rawUrl.startsWith("http") ? rawUrl : `https://${rawUrl}`;
|
||||
parts.push(
|
||||
<a
|
||||
key={`link-${match.index}`}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-emerald-700 underline decoration-emerald-300 underline-offset-2"
|
||||
>
|
||||
{rawUrl}
|
||||
</a>
|
||||
);
|
||||
if (trailing) {
|
||||
parts.push(trailing);
|
||||
}
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
if (lastIndex < value.length) {
|
||||
parts.push(value.slice(lastIndex));
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts : value;
|
||||
}
|
||||
|
||||
function MapAutoBounds({ points }: { points: Array<{ lat: number; lng: number }> }) {
|
||||
const map = useMap();
|
||||
useEffect(() => {
|
||||
@@ -2026,8 +2199,9 @@ function ViewToggleButton({
|
||||
}) {
|
||||
const base =
|
||||
size === "sm"
|
||||
? "event-toggle rounded-full bg-white/80 px-2 py-1 text-[10px] text-slate-700"
|
||||
? "event-toggle rounded-full bg-white/80 px-1.5 py-0.5 text-[9px] text-slate-700"
|
||||
: "rounded-full border border-slate-200 px-3 py-1 text-xs text-slate-700";
|
||||
const iconClass = size === "sm" ? "h-[11px] w-[11px]" : "h-4 w-4";
|
||||
const label = isSelected
|
||||
? "Vom Kalenderfeed entfernen"
|
||||
: "Zum Kalenderfeed hinzufügen";
|
||||
@@ -2039,23 +2213,23 @@ function ViewToggleButton({
|
||||
aria-label={label}
|
||||
title={label}
|
||||
>
|
||||
{isSelected ? <IconBell /> : <IconSleep />}
|
||||
{isSelected ? <IconBell className={iconClass} /> : <IconSleep className={iconClass} />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function IconBell() {
|
||||
function IconBell({ className = "h-4 w-4" }: { className?: string }) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M6 8a6 6 0 1112 0c0 7 2 7 2 7H4s2 0 2-7" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M10 19a2 2 0 004 0" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconSleep() {
|
||||
function IconSleep({ className = "h-4 w-4" }: { className?: string }) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M3 5h6l-6 6h6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M9 9h6l-6 6h6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M15 13h6l-6 6h6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
|
||||
Reference in New Issue
Block a user