Aktueller Stand
This commit is contained in:
@@ -39,6 +39,15 @@ type DateBucket = "past" | "today" | "tomorrow" | "future";
|
|||||||
type Pane = "calendar" | "list" | "map";
|
type Pane = "calendar" | "list" | "map";
|
||||||
|
|
||||||
const MAP_FILTER_STORAGE_KEY = "mapFilters";
|
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() {
|
export default function CalendarBoard() {
|
||||||
const { data, status } = useSession();
|
const { data, status } = useSession();
|
||||||
@@ -74,27 +83,30 @@ export default function CalendarBoard() {
|
|||||||
const [prefillStartAt, setPrefillStartAt] = useState<string | null>(null);
|
const [prefillStartAt, setPrefillStartAt] = useState<string | null>(null);
|
||||||
const [viewOrder, setViewOrder] = useState<Array<Pane>>([
|
const [viewOrder, setViewOrder] = useState<Array<Pane>>([
|
||||||
"list",
|
"list",
|
||||||
"map",
|
"calendar",
|
||||||
"calendar"
|
"map"
|
||||||
]);
|
]);
|
||||||
const [collapsed, setCollapsed] = useState<Record<Pane, boolean>>({
|
const [collapsed, setCollapsed] = useState<Record<Pane, boolean>>({
|
||||||
calendar: false,
|
calendar: false,
|
||||||
list: false,
|
list: false,
|
||||||
map: false
|
map: false
|
||||||
});
|
});
|
||||||
const [activeRange, setActiveRange] = useState<{
|
const [listQuickRange, setListQuickRange] = useState<
|
||||||
start: Date;
|
"next7" | "next30" | "nextYear" | null
|
||||||
end: Date;
|
>("next30");
|
||||||
viewType: string;
|
const [hidePastInList, setHidePastInList] = useState(true);
|
||||||
} | null>(() => {
|
const [initialView, setInitialView] = useState(() => {
|
||||||
const now = new Date();
|
if (typeof window === "undefined") return DEFAULT_CALENDAR_VIEW;
|
||||||
return {
|
try {
|
||||||
start: new Date(now.getFullYear(), 0, 1),
|
const stored = window.localStorage.getItem(CALENDAR_VIEW_STORAGE_KEY);
|
||||||
end: new Date(now.getFullYear() + 1, 0, 1),
|
if (stored && ALLOWED_CALENDAR_VIEWS.includes(stored)) {
|
||||||
viewType: "dayGridYear"
|
return stored;
|
||||||
};
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return DEFAULT_CALENDAR_VIEW;
|
||||||
});
|
});
|
||||||
const [initialView, setInitialView] = useState("dayGridYear");
|
|
||||||
const [dragSource, setDragSource] = useState<Pane | null>(null);
|
const [dragSource, setDragSource] = useState<Pane | null>(null);
|
||||||
const [dragOver, setDragOver] = useState<Pane | null>(null);
|
const [dragOver, setDragOver] = useState<Pane | null>(null);
|
||||||
const [isPointerDragging, setIsPointerDragging] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
@@ -527,25 +572,14 @@ export default function CalendarBoard() {
|
|||||||
setFormOpen(true);
|
setFormOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyTruncatedTitles = (root: HTMLElement) => {
|
const applyEventTooltip = (root: HTMLElement, title: string) => {
|
||||||
const elements = Array.from(
|
const elements = Array.from(
|
||||||
root.querySelectorAll<HTMLElement>(".event-shell .truncate")
|
root.querySelectorAll<HTMLElement>(".event-shell .truncate")
|
||||||
);
|
);
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
const text = element.textContent?.trim();
|
|
||||||
if (!text) {
|
|
||||||
element.removeAttribute("title");
|
element.removeAttribute("title");
|
||||||
return;
|
|
||||||
}
|
|
||||||
const isTruncated =
|
|
||||||
element.scrollWidth > element.clientWidth ||
|
|
||||||
element.scrollHeight > element.clientHeight;
|
|
||||||
if (isTruncated) {
|
|
||||||
element.setAttribute("title", text);
|
|
||||||
} else {
|
|
||||||
element.removeAttribute("title");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
root.setAttribute("title", title);
|
||||||
};
|
};
|
||||||
|
|
||||||
const calendarEvents = useMemo(
|
const calendarEvents = useMemo(
|
||||||
@@ -756,6 +790,41 @@ export default function CalendarBoard() {
|
|||||||
return sorted;
|
return sorted;
|
||||||
}, [events, query, categoryFilter, sortKey, sortDir]);
|
}, [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 categoryOptions = useMemo(() => {
|
||||||
const list = events
|
const list = events
|
||||||
.map((event) => event.category?.name || "Ohne Kategorie")
|
.map((event) => event.category?.name || "Ohne Kategorie")
|
||||||
@@ -861,18 +930,38 @@ export default function CalendarBoard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const displayedEvents = useMemo(() => {
|
const displayedEvents = useMemo(() => {
|
||||||
if (!activeRange) {
|
if (!listRange) {
|
||||||
return filteredEvents;
|
return filteredEvents;
|
||||||
}
|
}
|
||||||
return filteredEvents.filter((event) => {
|
return filteredEvents.filter((event) => {
|
||||||
const start = new Date(event.startAt);
|
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(() => {
|
useEffect(() => {
|
||||||
setBulkSelection((prev) => (prev.size > 0 ? new Set() : prev));
|
setBulkSelection((prev) => (prev.size > 0 ? new Set() : prev));
|
||||||
}, [query, categoryFilter, activeRange]);
|
}, [query, categoryFilter, listRange]);
|
||||||
|
|
||||||
const toggleSort = (nextKey: string) => {
|
const toggleSort = (nextKey: string) => {
|
||||||
let nextDir: "asc" | "desc" = "asc";
|
let nextDir: "asc" | "desc" = "asc";
|
||||||
@@ -1012,7 +1101,7 @@ export default function CalendarBoard() {
|
|||||||
headerToolbar={{
|
headerToolbar={{
|
||||||
left: "prev,next today",
|
left: "prev,next today",
|
||||||
center: "title",
|
center: "title",
|
||||||
right: "dayGridMonth,timeGridWeek,dayGridYear,listWeek"
|
right: "timeGridWeek,dayGridMonth,dayGridYear,listWeek"
|
||||||
}}
|
}}
|
||||||
views={{
|
views={{
|
||||||
dayGridMonth: { buttonText: "Monat" },
|
dayGridMonth: { buttonText: "Monat" },
|
||||||
@@ -1084,15 +1173,34 @@ export default function CalendarBoard() {
|
|||||||
}}
|
}}
|
||||||
eventDidMount={(info) => {
|
eventDidMount={(info) => {
|
||||||
if (!info.el) return;
|
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"
|
height="auto"
|
||||||
datesSet={(arg) => {
|
datesSet={(arg) => {
|
||||||
setActiveRange({
|
if (ALLOWED_CALENDAR_VIEWS.includes(arg.view.type)) {
|
||||||
start: arg.start,
|
setInitialView(arg.view.type);
|
||||||
end: arg.end,
|
try {
|
||||||
viewType: arg.view.type
|
window.localStorage.setItem(
|
||||||
});
|
CALENDAR_VIEW_STORAGE_KEY,
|
||||||
|
arg.view.type
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -1363,12 +1471,56 @@ export default function CalendarBoard() {
|
|||||||
{!collapsed.list && (
|
{!collapsed.list && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<input
|
<input
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
placeholder="Suchen..."
|
placeholder="Suchen..."
|
||||||
className="min-w-[220px] rounded-xl border border-slate-300 px-3 py-2 text-sm"
|
className="min-w-[220px] rounded-xl border border-slate-300 px-3 py-2 text-sm"
|
||||||
/>
|
/>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={filterButtonClass(listQuickRange === "next7")}
|
||||||
|
onClick={() =>
|
||||||
|
setListQuickRange((current) =>
|
||||||
|
current === "next7" ? null : "next7"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
In 7 Tagen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={filterButtonClass(listQuickRange === "next30")}
|
||||||
|
onClick={() =>
|
||||||
|
setListQuickRange((current) =>
|
||||||
|
current === "next30" ? null : "next30"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
In 30 Tagen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={filterButtonClass(listQuickRange === "nextYear")}
|
||||||
|
onClick={() =>
|
||||||
|
setListQuickRange((current) =>
|
||||||
|
current === "nextYear" ? null : "nextYear"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Nächstes Jahr
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={filterButtonClass(hidePastInList)}
|
||||||
|
onClick={() => setHidePastInList((current) => !current)}
|
||||||
|
>
|
||||||
|
Vergangene ausblenden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -1414,11 +1566,11 @@ export default function CalendarBoard() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="list-table min-w-full text-left text-sm">
|
<table className="list-table w-full table-fixed text-left text-sm">
|
||||||
<thead className="text-xs uppercase tracking-[0.2em] text-slate-500">
|
<thead className="text-xs uppercase tracking-[0.2em] text-slate-500">
|
||||||
<tr>
|
<tr>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<th className="pb-2 w-[44px] pl-2">
|
<th className={`pb-2 pl-2 ${tableWidths.checkbox}`}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
aria-label="Alle sichtbaren Termine auswählen"
|
aria-label="Alle sichtbaren Termine auswählen"
|
||||||
@@ -1434,7 +1586,7 @@ export default function CalendarBoard() {
|
|||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
<th className="pb-2">
|
<th className={`pb-2 whitespace-nowrap ${tableWidths.date}`}>
|
||||||
<SortButton
|
<SortButton
|
||||||
label="Datum"
|
label="Datum"
|
||||||
active={sortKey === "startAt"}
|
active={sortKey === "startAt"}
|
||||||
@@ -1442,7 +1594,7 @@ export default function CalendarBoard() {
|
|||||||
onClick={() => toggleSort("startAt")}
|
onClick={() => toggleSort("startAt")}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="pb-2">
|
<th className={`pb-2 ${tableWidths.title}`}>
|
||||||
<SortButton
|
<SortButton
|
||||||
label="Titel"
|
label="Titel"
|
||||||
active={sortKey === "title"}
|
active={sortKey === "title"}
|
||||||
@@ -1450,8 +1602,8 @@ export default function CalendarBoard() {
|
|||||||
onClick={() => toggleSort("title")}
|
onClick={() => toggleSort("title")}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="pb-2">
|
<th className={`pb-2 ${tableWidths.category}`}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<SortButton
|
<SortButton
|
||||||
label="Kategorie"
|
label="Kategorie"
|
||||||
active={sortKey === "category"}
|
active={sortKey === "category"}
|
||||||
@@ -1459,7 +1611,7 @@ export default function CalendarBoard() {
|
|||||||
onClick={() => toggleSort("category")}
|
onClick={() => toggleSort("category")}
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-xs text-slate-600"
|
className="max-w-full rounded-full border border-slate-200 bg-white px-2 py-0.5 text-xs text-slate-600"
|
||||||
value={categoryFilter}
|
value={categoryFilter}
|
||||||
onChange={(event) => setCategoryFilter(event.target.value)}
|
onChange={(event) => setCategoryFilter(event.target.value)}
|
||||||
aria-label="Kategorie filtern"
|
aria-label="Kategorie filtern"
|
||||||
@@ -1473,7 +1625,7 @@ export default function CalendarBoard() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th className="pb-2">
|
<th className={`pb-2 ${tableWidths.location}`}>
|
||||||
<SortButton
|
<SortButton
|
||||||
label="Ort"
|
label="Ort"
|
||||||
active={sortKey === "location"}
|
active={sortKey === "location"}
|
||||||
@@ -1481,7 +1633,9 @@ export default function CalendarBoard() {
|
|||||||
onClick={() => toggleSort("location")}
|
onClick={() => toggleSort("location")}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="pb-2 w-[96px]">Aktionen</th>
|
<th className={`pb-2 whitespace-nowrap ${tableWidths.actions}`}>
|
||||||
|
Aktionen
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -1495,6 +1649,10 @@ export default function CalendarBoard() {
|
|||||||
displayedEvents.map((event) => {
|
displayedEvents.map((event) => {
|
||||||
const categoryName = event.category?.name || "Ohne Kategorie";
|
const categoryName = event.category?.name || "Ohne Kategorie";
|
||||||
const locationLabel = formatLocation(event.location);
|
const locationLabel = formatLocation(event.location);
|
||||||
|
const dateLabel = new Date(event.startAt).toLocaleString("de-DE", {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short"
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={event.id}
|
key={event.id}
|
||||||
@@ -1510,7 +1668,7 @@ export default function CalendarBoard() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<td className="py-3 pr-3 pl-2">
|
<td className="py-2 pr-3 pl-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
aria-label={`${event.title} auswählen`}
|
aria-label={`${event.title} auswählen`}
|
||||||
@@ -1519,29 +1677,28 @@ export default function CalendarBoard() {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td className="py-3 pr-3 pl-2 whitespace-nowrap">
|
<td className="py-2 pr-3 pl-2">
|
||||||
{new Date(event.startAt).toLocaleString("de-DE", {
|
<span className="block w-full truncate whitespace-nowrap" title={dateLabel}>
|
||||||
dateStyle: "medium",
|
{dateLabel}
|
||||||
timeStyle: "short"
|
</span>
|
||||||
})}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-3 font-medium">
|
<td className="py-2 pr-3 font-medium">
|
||||||
<span
|
<span
|
||||||
className="block max-w-[200px] truncate sm:max-w-[280px] lg:max-w-[420px] xl:max-w-[520px] 2xl:max-w-[640px]"
|
className="block w-full truncate"
|
||||||
title={event.title}
|
title={event.title}
|
||||||
>
|
>
|
||||||
{event.title}
|
{event.title}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-3">
|
<td className="py-2 pr-3">
|
||||||
<span
|
<span
|
||||||
className="block max-w-[160px] truncate sm:max-w-[220px] lg:max-w-[280px] xl:max-w-[320px]"
|
className="block w-full truncate"
|
||||||
title={categoryName}
|
title={categoryName}
|
||||||
>
|
>
|
||||||
{categoryName}
|
{categoryName}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-3">
|
<td className="py-2 pr-3">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span
|
<span
|
||||||
className="min-w-0 flex-1 truncate"
|
className="min-w-0 flex-1 truncate"
|
||||||
@@ -1563,12 +1720,13 @@ export default function CalendarBoard() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-3">
|
<td className="py-2 pr-3 whitespace-nowrap">
|
||||||
<div className="flex flex-nowrap gap-1">
|
<div className="flex flex-nowrap justify-end gap-1">
|
||||||
{canManageView && (
|
{canManageView && (
|
||||||
<ViewToggleButton
|
<ViewToggleButton
|
||||||
isSelected={selectedEventIds.has(event.id)}
|
isSelected={selectedEventIds.has(event.id)}
|
||||||
onClick={() => toggleEvent(event.id)}
|
onClick={() => toggleEvent(event.id)}
|
||||||
|
size="sm"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
|
|||||||
Reference in New Issue
Block a user