feat(ui): improve dark mode surfaces and planning event actions

This commit is contained in:
Arthur Belleville 2026-02-24 11:47:27 +01:00
parent 421676c3f0
commit 737cd5f073
No known key found for this signature in database
3 changed files with 1213 additions and 974 deletions

View file

@ -295,7 +295,7 @@ export function GanttChart({
>
<ChevronLeftIcon className="h-4 w-4" />
</Button>
<div className="px-4 py-2 bg-card border border-border rounded-lg min-w-[200px] text-center">
<div className="px-4 py-2 bg-card dark:bg-gray-900 border border-border dark:border-gray-700 rounded-lg min-w-[200px] text-center">
<span className="text-sm font-medium text-foreground">
{formatDateRange(periodStart, periodEnd)}
</span>
@ -334,7 +334,7 @@ export function GanttChart({
{/* Gantt chart */}
<div
className="bg-card rounded-xl border border-border overflow-hidden shadow-sm"
className="bg-card dark:bg-gray-900 rounded-xl border border-border dark:border-gray-700 overflow-hidden shadow-sm"
ref={containerRef}
>
<div className="overflow-x-auto">
@ -441,7 +441,7 @@ export function GanttChart({
{/* Title */}
<h3
className={twMerge(
"font-semibold text-foreground leading-tight line-clamp-1",
"font-semibold text-gray-900 leading-tight line-clamp-1",
isCompact ? "mt-1 text-xs" : "mt-2 text-sm",
)}
>

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,13 @@ import { WebcalModal } from "@ui/components/WebcalModal";
import { downloadICSFile, generateICSFromEvents, toast } from "@xtablo/shared";
import { EventAndTablo } from "@xtablo/shared/types/events.types";
import { Button } from "@xtablo/ui/components/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@xtablo/ui/components/dropdown-menu";
import {
Select,
SelectContent,
@ -11,10 +18,33 @@ import {
SelectValue,
} from "@xtablo/ui/components/select";
import { TypographyH3, TypographyH4 } from "@xtablo/ui/components/typography";
import { ClockIcon, Download, EllipsisVerticalIcon, FolderInputIcon, PlusIcon, RefreshCcw } from "lucide-react";
import {
ClockIcon,
Compass,
Download,
EllipsisVerticalIcon,
Flame,
FolderIcon,
FolderInputIcon,
Gem,
Heart,
Leaf,
PlusIcon,
RefreshCcw,
Sparkles,
Star,
Sun,
Waves,
Zap,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Outlet, useNavigate, useParams, useSearchParams } from "react-router-dom";
import {
Outlet,
useNavigate,
useParams,
useSearchParams,
} from "react-router-dom";
import { useDeleteEvent, useEventsByTablo } from "../hooks/events";
import { useGetAllTabloAccess, useTablosList } from "../hooks/tablos";
import { useIsReadOnlyUser } from "../providers/UserStoreProvider";
@ -22,6 +52,43 @@ import { EventModal } from "../components/EventModal";
type ViewType = "month" | "week" | "day";
function getTabloIcon(color: string | null | undefined) {
switch (color) {
case "bg-blue-500":
return Zap;
case "bg-green-500":
return Leaf;
case "bg-purple-500":
return Gem;
case "bg-red-500":
return Flame;
case "bg-yellow-500":
return Star;
case "bg-indigo-500":
return Compass;
case "bg-pink-500":
return Heart;
case "bg-teal-500":
return Waves;
case "bg-orange-500":
return Sun;
case "bg-cyan-500":
return Sparkles;
default:
return FolderIcon;
}
}
function getTabloIconColor(color: string | null | undefined): string {
switch (color) {
case "bg-yellow-500":
case "bg-cyan-500":
return "text-gray-700";
default:
return "text-white";
}
}
export const PlanningPage = () => {
const { t } = useTranslation(["planning", "common"]);
const { tablo_id } = useParams();
@ -33,10 +100,14 @@ export const PlanningPage = () => {
// Initialize view from URL search params, default to "month"
const viewFromUrl = searchParams.get("view") as ViewType | null;
const initialView: ViewType =
viewFromUrl && ["month", "week", "day"].includes(viewFromUrl) ? viewFromUrl : "month";
viewFromUrl && ["month", "week", "day"].includes(viewFromUrl)
? viewFromUrl
: "month";
const [currentView, setCurrentView] = useState<ViewType>(initialView);
const [selectedTabloId, setSelectedTabloId] = useState<string>(tablo_id || "all");
const [selectedTabloId, setSelectedTabloId] = useState<string>(
tablo_id || "all",
);
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
const [isWebcalModalOpen, setIsWebcalModalOpen] = useState(false);
const isReadOnly = useIsReadOnlyUser();
@ -48,9 +119,8 @@ export const PlanningPage = () => {
const { data: tablos, isLoading: tablosLoading } = useTablosList();
// Fetch events for selected tablo or all tablos
const { data: tabloEvents = [], isLoading: tabloEventsLoading } = useEventsByTablo(
selectedTabloId !== "all" ? selectedTabloId : null
);
const { data: tabloEvents = [], isLoading: tabloEventsLoading } =
useEventsByTablo(selectedTabloId !== "all" ? selectedTabloId : null);
// Fetch all tablo accesses
const { data: tabloAccess } = useGetAllTabloAccess();
@ -61,7 +131,7 @@ export const PlanningPage = () => {
if (
tabloAccess?.find(
(access: { tablo_id: string; is_admin: boolean }) =>
access.tablo_id === event.tablo_id && access.is_admin
access.tablo_id === event.tablo_id && access.is_admin,
)
) {
return true;
@ -84,13 +154,17 @@ export const PlanningPage = () => {
return newParams;
});
},
[setSearchParams]
[setSearchParams],
);
// Sync view with URL on mount and when URL changes
useEffect(() => {
const viewParam = searchParams.get("view") as ViewType | null;
if (viewParam && ["month", "week", "day"].includes(viewParam) && viewParam !== currentView) {
if (
viewParam &&
["month", "week", "day"].includes(viewParam) &&
viewParam !== currentView
) {
setCurrentView(viewParam);
} else if (!viewParam) {
// If no view param in URL, set it to current view
@ -100,7 +174,7 @@ export const PlanningPage = () => {
newParams.set("view", currentView);
return newParams;
},
{ replace: true }
{ replace: true },
);
}
}, [searchParams, currentView, setSearchParams]);
@ -151,7 +225,8 @@ export const PlanningPage = () => {
const calendarName =
selectedTabloId === "all"
? t("planning:allEvents")
: tablos?.find((t) => t.id === selectedTabloId)?.name || t("planning:title");
: tablos?.find((t) => t.id === selectedTabloId)?.name ||
t("planning:title");
const icsContent = generateICSFromEvents(tabloEvents, calendarName);
const filename =
@ -176,6 +251,18 @@ export const PlanningPage = () => {
}
};
const navigateToEditEvent = (event: EventAndTablo) => {
if (!canEditEvent(event) || isReadOnly) return;
const params = new URLSearchParams(searchParams);
params.set("tab", "events");
navigate({
pathname: `/planning/${event.tablo_id}/events/${event.event_id}/edit`,
search: `?${params.toString()}`,
});
};
const monthNames = [
t("planning:months.january"),
t("planning:months.february"),
@ -268,7 +355,12 @@ export const PlanningPage = () => {
// const nowMinute = now.getMinutes();
const nowDay = now.getDate();
fullDate.setHours(Number(time.split(":")[0]), Number(time.split(":")[1]), 0, 0);
fullDate.setHours(
Number(time.split(":")[0]),
Number(time.split(":")[1]),
0,
0,
);
const hour = fullDate.getHours();
// const minute = fullDate.getMinutes();
@ -336,7 +428,8 @@ export const PlanningPage = () => {
const daysInMonth = lastDay.getDate();
// Adjust for Monday as first day of week
const startingDayOfWeek = firstDay.getDay();
const mondayStartingDay = startingDayOfWeek === 0 ? 6 : startingDayOfWeek - 1;
const mondayStartingDay =
startingDayOfWeek === 0 ? 6 : startingDayOfWeek - 1;
const days = [];
for (let i = 0; i < mondayStartingDay; i++) {
@ -373,7 +466,9 @@ export const PlanningPage = () => {
if (currentView === "week") {
const weekDays = getWeekDays();
const weekDateStrings = weekDays.map(formatDate);
return tabloEvents.filter((event) => weekDateStrings.includes(event.start_date));
return tabloEvents.filter((event) =>
weekDateStrings.includes(event.start_date),
);
} else if (currentView === "day") {
const dateString = formatDate(currentDate);
return tabloEvents.filter((event) => event.start_date === dateString);
@ -394,7 +489,7 @@ export const PlanningPage = () => {
...visibleEvents.map((event) => {
const [hour] = event.start_time.split(":").map(Number);
return hour;
})
}),
);
// Return the earlier of 8am or the earliest event hour
@ -418,7 +513,7 @@ export const PlanningPage = () => {
}
const [hour] = event.end_time.split(":").map(Number);
return hour;
})
}),
);
// Return the later of 7pm or the latest event hour
@ -445,7 +540,7 @@ export const PlanningPage = () => {
return Array.from(
{ length: numSlots },
(_, i) => `${(startHour + i).toString().padStart(2, "0")}:00`
(_, i) => `${(startHour + i).toString().padStart(2, "0")}:00`,
);
};
@ -473,7 +568,9 @@ export const PlanningPage = () => {
className={`min-h-[120px] border-b border-border ${
(index + 1) % 7 !== 0 ? "border-r border-border" : ""
} ${day ? "cursor-pointer hover:bg-muted" : "bg-muted"} ${
day && formatDate(day) === formatDate(new Date()) ? "bg-primary/10" : ""
day && formatDate(day) === formatDate(new Date())
? "bg-primary/10"
: ""
}`}
onClick={() => {
if (day) {
@ -491,7 +588,9 @@ export const PlanningPage = () => {
<div className="p-2">
<div
className={`text-sm font-medium mb-1 ${
formatDate(day) === formatDate(new Date()) ? "text-primary" : "text-foreground"
formatDate(day) === formatDate(new Date())
? "text-primary"
: "text-foreground"
}`}
>
{day.getDate()}
@ -518,14 +617,18 @@ export const PlanningPage = () => {
onClick={(e) => {
e.stopPropagation();
if (canEditEvent(event)) {
navigate(`/planning/${event.tablo_id}/events/${event.event_id}/edit`);
navigate(
`/planning/${event.tablo_id}/events/${event.event_id}/edit`,
);
}
}}
>
<div className="truncate">
{formatTime(event.start_time)} {event.title}
{selectedTabloId === "all" && event.tablo_name && (
<span className="opacity-75 ml-1"> {event.tablo_name}</span>
<span className="opacity-75 ml-1">
{event.tablo_name}
</span>
)}
</div>
{canDeleteEvent(event) && (
@ -573,7 +676,9 @@ export const PlanningPage = () => {
</div>
<div
className={`text-lg font-medium mt-1 ${
formatDate(day) === formatDate(new Date()) ? "text-primary" : "text-foreground"
formatDate(day) === formatDate(new Date())
? "text-primary"
: "text-foreground"
}`}
>
{day.getDate()}
@ -615,10 +720,18 @@ export const PlanningPage = () => {
)}
{getEventsForDate(day)
.filter((event) => event.start_time.startsWith(time.split(":")[0]))
.filter((event) =>
event.start_time.startsWith(time.split(":")[0]),
)
.map((event) => {
const eventHeight = calculateEventHeight(event.start_time, event.end_time);
const eventOffset = calculateEventOffset(event.start_time, time);
const eventHeight = calculateEventHeight(
event.start_time,
event.end_time,
);
const eventOffset = calculateEventOffset(
event.start_time,
time,
);
return (
<div
key={event.event_id}
@ -635,7 +748,7 @@ export const PlanningPage = () => {
minHeight: "30px",
}}
title={`${formatTime(event.start_time)} - ${formatTime(
event.end_time
event.end_time,
)} ${event.title}${
selectedTabloId === "all" && event.tablo_name
? ` - ${event.tablo_name}`
@ -644,19 +757,24 @@ export const PlanningPage = () => {
onClick={(e) => {
e.stopPropagation();
if (canEditEvent(event)) {
navigate(`/planning/${event.tablo_id}/events/${event.event_id}/edit`);
navigate(
`/planning/${event.tablo_id}/events/${event.event_id}/edit`,
);
}
}}
>
<div className="text-[10px] font-medium leading-tight">
{event.title}
{selectedTabloId === "all" && event.tablo_name && (
<span className="opacity-75 ml-1"> {event.tablo_name}</span>
<span className="opacity-75 ml-1">
{event.tablo_name}
</span>
)}
</div>
{eventHeight >= 30 && (
<div className="text-[9px] opacity-75 leading-tight">
{formatTime(event.start_time)} - {formatTime(event.end_time)}
{formatTime(event.start_time)} -{" "}
{formatTime(event.end_time)}
</div>
)}
{canDeleteEvent(event) && (
@ -689,7 +807,9 @@ export const PlanningPage = () => {
<div className="text-sm text-muted-foreground uppercase">
{dayNames[currentDate.getDay()]}
</div>
<div className="text-2xl font-medium text-foreground mt-1">{currentDate.getDate()}</div>
<div className="text-2xl font-medium text-foreground mt-1">
{currentDate.getDate()}
</div>
</div>
{/* Time slots */}
@ -722,10 +842,18 @@ export const PlanningPage = () => {
)}
{getEventsForDate(currentDate)
.filter((event) => event.start_time.startsWith(time.split(":")[0]))
.filter((event) =>
event.start_time.startsWith(time.split(":")[0]),
)
.map((event) => {
const eventHeight = calculateEventHeight(event.start_time, event.end_time);
const eventOffset = calculateEventOffset(event.start_time, time);
const eventHeight = calculateEventHeight(
event.start_time,
event.end_time,
);
const eventOffset = calculateEventOffset(
event.start_time,
time,
);
return (
<div
key={event.event_id}
@ -742,7 +870,7 @@ export const PlanningPage = () => {
minHeight: "30px",
}}
title={`${formatTime(event.start_time)} - ${formatTime(
event.end_time
event.end_time,
)} ${event.title}${
selectedTabloId === "all" && event.tablo_name
? ` - ${event.tablo_name}`
@ -751,19 +879,24 @@ export const PlanningPage = () => {
onClick={(e) => {
e.stopPropagation();
if (canEditEvent(event)) {
navigate(`/planning/${event.tablo_id}/events/${event.event_id}/edit`);
navigate(
`/planning/${event.tablo_id}/events/${event.event_id}/edit`,
);
}
}}
>
<div className="text-[10px] font-medium truncate leading-tight">
{event.title}
{selectedTabloId === "all" && event.tablo_name && (
<span className="opacity-75 ml-1"> {event.tablo_name}</span>
<span className="opacity-75 ml-1">
{event.tablo_name}
</span>
)}
</div>
{eventHeight >= 30 && (
<div className="text-[9px] opacity-75 leading-tight">
{formatTime(event.start_time)} - {formatTime(event.end_time)}
{formatTime(event.start_time)} -{" "}
{formatTime(event.end_time)}
</div>
)}
{eventHeight >= 75 && event.description && (
@ -799,11 +932,26 @@ export const PlanningPage = () => {
today.setHours(0, 0, 0, 0);
const filtered = tabloEvents.filter((e) => {
if (showAllEvents) return true;
const eventDate = e.start_date ? new Date(e.start_date + "T00:00:00") : null;
const eventDate = e.start_date
? new Date(e.start_date + "T00:00:00")
: null;
return !eventDate || eventDate >= today;
});
const months = ["JAN", "FÉV", "MAR", "AVR", "MAI", "JUN", "JUL", "AOÛ", "SEP", "OCT", "NOV", "DÉC"];
const months = [
"JAN",
"FÉV",
"MAR",
"AVR",
"MAI",
"JUN",
"JUL",
"AOÛ",
"SEP",
"OCT",
"NOV",
"DÉC",
];
return (
<div className="py-6 px-2">
@ -814,7 +962,9 @@ export const PlanningPage = () => {
</h1>
<button
type="button"
onClick={() => { if (!isReadOnly) setIsCreateEventOpen(true); }}
onClick={() => {
if (!isReadOnly) setIsCreateEventOpen(true);
}}
disabled={isReadOnly}
className="flex items-center gap-2 px-5 py-3 bg-[#804EEC] hover:bg-[#6f3fd4] text-white rounded-xl transition-colors font-medium shadow-sm disabled:opacity-50"
>
@ -852,21 +1002,35 @@ export const PlanningPage = () => {
{/* Event cards grid */}
{tabloEventsLoading ? (
<div className="flex items-center justify-center py-24">
<img src="/icon.jpg" alt="Loading..." className="animate-spin rounded-full h-8 w-8 object-cover" />
<img
src="/icon.jpg"
alt="Loading..."
className="animate-spin rounded-full h-8 w-8 object-cover"
/>
</div>
) : filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center py-24 text-center">
<p className="text-gray-500 dark:text-gray-400 text-lg font-medium">Aucun événement trouvé</p>
<p className="text-gray-500 dark:text-gray-400 text-lg font-medium">
Aucun événement trouvé
</p>
<p className="text-gray-400 dark:text-gray-500 text-sm mt-1">
{showAllEvents ? "Aucun événement trouvé" : "Aucun événement à venir"}
{showAllEvents
? "Aucun événement trouvé"
: "Aucun événement à venir"}
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{filtered.map((event) => {
const date = event.start_date ? new Date(event.start_date + "T00:00:00") : null;
const date = event.start_date
? new Date(event.start_date + "T00:00:00")
: null;
const monthLabel = date ? months[date.getMonth()] : "";
const dayLabel = date ? String(date.getDate()).padStart(2, "0") : "";
const dayLabel = date
? String(date.getDate()).padStart(2, "0")
: "";
const TabloIcon = getTabloIcon(event.tablo_color);
const iconColor = getTabloIconColor(event.tablo_color);
const timeLabel = event.start_time
? `${event.start_time.slice(0, 5)}${event.end_time ? ` ${event.end_time.slice(0, 5)}` : ""}`
: null;
@ -874,16 +1038,39 @@ export const PlanningPage = () => {
return (
<div
key={event.event_id}
className="bg-white dark:bg-gray-800 rounded-2xl p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all hover:border-gray-300 dark:hover:border-gray-600 relative group cursor-pointer"
onClick={() => navigate(`/planning/${event.event_id}?tablo_id=${event.tablo_id}`)}
className="bg-white dark:bg-gray-800 rounded-2xl p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all hover:border-gray-300 dark:hover:border-gray-600 relative group"
>
<button
type="button"
onClick={(e) => { e.stopPropagation(); }}
className="absolute top-6 right-6 p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors opacity-0 group-hover:opacity-100"
>
<EllipsisVerticalIcon className="w-5 h-5" />
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="absolute top-6 right-6 p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors opacity-0 group-hover:opacity-100"
aria-label="Options de l'événement"
>
<EllipsisVerticalIcon className="w-5 h-5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem
onClick={() => navigateToEditEvent(event)}
disabled={!canEditEvent(event) || isReadOnly}
>
Replanifier
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => deleteEvent.mutate(event.event_id)}
disabled={
!canDeleteEvent(event) ||
isReadOnly ||
deleteEvent.isPending
}
className="text-destructive focus:text-destructive"
>
Supprimer
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-start gap-4 mb-4">
{/* Date badge */}
@ -922,11 +1109,13 @@ export const PlanningPage = () => {
{event.tablo_name && (
<div className="flex items-center gap-2.5 text-[#344054] dark:text-gray-300">
<div
className={`w-4 h-4 rounded-[3px] flex-shrink-0 flex items-center justify-center text-[7px] font-bold text-white ${event.tablo_color || "bg-gray-400"}`}
className={`w-5 h-5 rounded-[5px] flex-shrink-0 flex items-center justify-center ${event.tablo_color || "bg-gray-400"}`}
>
{event.tablo_name.charAt(0).toUpperCase()}
<TabloIcon className={`w-3 h-3 ${iconColor}`} />
</div>
<span className="text-sm truncate">{event.tablo_name}</span>
<span className="text-sm truncate">
{event.tablo_name}
</span>
</div>
)}
</div>
@ -947,7 +1136,9 @@ export const PlanningPage = () => {
mode="create"
isOpen={isCreateEventOpen}
onClose={() => setIsCreateEventOpen(false)}
defaultTabloId={selectedTabloId !== "all" ? selectedTabloId : undefined}
defaultTabloId={
selectedTabloId !== "all" ? selectedTabloId : undefined
}
defaultDate={currentDate}
/>
<Outlet />
@ -968,10 +1159,15 @@ export const PlanningPage = () => {
onValueChange={(value) => setSelectedTabloId(value)}
disabled={tablosLoading}
>
<SelectTrigger className="w-full" aria-label={t("planning:selectTablo")}>
<SelectTrigger
className="w-full"
aria-label={t("planning:selectTablo")}
>
<SelectValue
placeholder={
tablosLoading ? t("common:actions.loading") : t("planning:selectTablo")
tablosLoading
? t("common:actions.loading")
: t("planning:selectTablo")
}
/>
</SelectTrigger>
@ -996,15 +1192,17 @@ export const PlanningPage = () => {
"Vous êtes en mode lecture seule. Vous ne pouvez pas créer d'événement.",
type: "error",
},
{ timeout: 5000 }
{ timeout: 5000 },
);
return;
}
if (selectedTabloId === "all") {
navigate(`/planning/create?date=${currentDate.toISOString()}`);
navigate(
`/planning/create?date=${currentDate.toISOString()}`,
);
} else {
navigate(
`/planning/create?tablo_id=${selectedTabloId}&date=${currentDate.toISOString()}`
`/planning/create?tablo_id=${selectedTabloId}&date=${currentDate.toISOString()}`,
);
}
}}
@ -1025,7 +1223,7 @@ export const PlanningPage = () => {
"Vous êtes en mode lecture seule. Vous ne pouvez pas importer de calendrier.",
type: "error",
},
{ timeout: 5000 }
{ timeout: 5000 },
);
return;
}
@ -1058,7 +1256,10 @@ export const PlanningPage = () => {
</div>
<div className="grid grid-cols-7 gap-1 text-xs">
{dayNamesShort.map((day) => (
<div key={day} className="text-center text-muted-foreground p-1">
<div
key={day}
className="text-center text-muted-foreground p-1"
>
{day.slice(0, 1)}
</div>
))}
@ -1094,12 +1295,25 @@ export const PlanningPage = () => {
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<TypographyH3>{t("planning:title")}</TypographyH3>
<Button onClick={goToToday} variant="outline" size="sm" className="border-[#804EEC] text-[#804EEC] hover:bg-[#804EEC]/10">
<Button
onClick={goToToday}
variant="outline"
size="sm"
className="border-[#804EEC] text-[#804EEC] hover:bg-[#804EEC]/10"
>
{t("planning:today")}
</Button>
<div className="flex items-center space-x-2">
<button onClick={() => navigateDate(-1)} className="p-2 hover:bg-muted rounded">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<button
onClick={() => navigateDate(-1)}
className="p-2 hover:bg-muted rounded"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
@ -1108,8 +1322,16 @@ export const PlanningPage = () => {
/>
</svg>
</button>
<button onClick={() => navigateDate(1)} className="p-2 hover:bg-muted rounded">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<button
onClick={() => navigateDate(1)}
className="p-2 hover:bg-muted rounded"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
@ -1164,7 +1386,9 @@ export const PlanningPage = () => {
alt="Loading..."
className="animate-spin rounded-full h-8 w-8 object-cover"
/>
<span className="ml-2 text-muted-foreground">{t("planning:loadingEvents")}</span>
<span className="ml-2 text-muted-foreground">
{t("planning:loadingEvents")}
</span>
</div>
) : (
<>
@ -1179,9 +1403,14 @@ export const PlanningPage = () => {
<Outlet />
{isImportModalOpen && <ImportICSModal onClose={() => setIsImportModalOpen(false)} />}
{isImportModalOpen && (
<ImportICSModal onClose={() => setIsImportModalOpen(false)} />
)}
<WebcalModal open={isWebcalModalOpen} onOpenChange={setIsWebcalModalOpen} />
<WebcalModal
open={isWebcalModalOpen}
onOpenChange={setIsWebcalModalOpen}
/>
</div>
);
};