diff --git a/apps/main/src/components/TabloEventsSection.test.tsx b/apps/main/src/components/TabloEventsSection.test.tsx
index c9aa98e..d466882 100644
--- a/apps/main/src/components/TabloEventsSection.test.tsx
+++ b/apps/main/src/components/TabloEventsSection.test.tsx
@@ -1,4 +1,3 @@
-import { fireEvent, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../utils/testHelpers";
import { TabloEventsSection } from "./TabloEventsSection";
diff --git a/apps/main/src/pages/join.test.tsx b/apps/main/src/pages/join.test.tsx
index ee04bdd..2f5a65c 100644
--- a/apps/main/src/pages/join.test.tsx
+++ b/apps/main/src/pages/join.test.tsx
@@ -16,7 +16,6 @@ vi.mock("react-router-dom", async () => {
};
});
-
vi.mock("../hooks/invite", () => ({
useJoinTablo: () => mockJoinTablo,
}));
diff --git a/apps/main/src/pages/landing.test.tsx b/apps/main/src/pages/landing.test.tsx
index 0853c1b..dd75362 100644
--- a/apps/main/src/pages/landing.test.tsx
+++ b/apps/main/src/pages/landing.test.tsx
@@ -13,7 +13,6 @@ vi.mock("../components/AnimatedBackground", () => ({
AnimatedBackground: () =>
Background
,
}));
-
describe("LandingPage", () => {
it("renders without crashing", () => {
const { container } = render(
diff --git a/apps/main/src/pages/planning.tsx b/apps/main/src/pages/planning.tsx
index 275bd5f..f5d5cfc 100644
--- a/apps/main/src/pages/planning.tsx
+++ b/apps/main/src/pages/planning.tsx
@@ -12,9 +12,9 @@ import {
} from "@xtablo/ui/components/select";
import { TypographyH3 } from "@xtablo/ui/components/typography";
import { Download, FolderInputIcon, PlusIcon, RefreshCcw } from "lucide-react";
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
-import { Outlet, useNavigate, useParams } 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";
@@ -25,9 +25,16 @@ export const PlanningPage = () => {
const { t } = useTranslation(["planning", "common"]);
const { tablo_id } = useParams();
const navigate = useNavigate();
+ const [searchParams, setSearchParams] = useSearchParams();
const [currentDate, setCurrentDate] = useState(new Date());
// const [selectedDate, setSelectedDate] = useState(new Date());
- const [currentView, setCurrentView] = useState("month");
+
+ // 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";
+ const [currentView, setCurrentView] = useState(initialView);
+
const [selectedTabloId, setSelectedTabloId] = useState(tablo_id || "all");
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
const [isWebcalModalOpen, setIsWebcalModalOpen] = useState(false);
@@ -63,6 +70,37 @@ export const PlanningPage = () => {
return canDeleteEvent(event);
};
+ // Function to change view and update URL
+ const changeView = useCallback(
+ (newView: ViewType) => {
+ setCurrentView(newView);
+ setSearchParams((params) => {
+ const newParams = new URLSearchParams(params);
+ newParams.set("view", newView);
+ return newParams;
+ });
+ },
+ [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) {
+ setCurrentView(viewParam);
+ } else if (!viewParam) {
+ // If no view param in URL, set it to current view
+ setSearchParams(
+ (params) => {
+ const newParams = new URLSearchParams(params);
+ newParams.set("view", currentView);
+ return newParams;
+ },
+ { replace: true }
+ );
+ }
+ }, [searchParams, currentView, setSearchParams]);
+
// Keyboard shortcuts for view switching
useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => {
@@ -80,26 +118,26 @@ export const PlanningPage = () => {
case "m":
case "1":
event.preventDefault();
- setCurrentView("month");
+ changeView("month");
break;
case "w":
case "s":
case "2":
event.preventDefault();
- setCurrentView("week");
+ changeView("week");
break;
case "d":
case "j":
case "3":
event.preventDefault();
- setCurrentView("day");
+ changeView("day");
break;
}
};
window.addEventListener("keydown", handleKeyPress);
return () => window.removeEventListener("keydown", handleKeyPress);
- }, []);
+ }, [changeView]);
const handleExportICS = () => {
if (!tabloEvents || tabloEvents.length === 0) {
@@ -322,7 +360,92 @@ export const PlanningPage = () => {
return weekDays;
};
- const timeSlots = Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, "0")}:00`);
+ // Get visible events for the current view
+ const getVisibleEvents = () => {
+ if (currentView === "month") {
+ return [];
+ }
+
+ if (currentView === "week") {
+ const weekDays = getWeekDays();
+ const weekDateStrings = weekDays.map(formatDate);
+ 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);
+ }
+
+ return [];
+ };
+
+ // Get the earliest event hour from visible events in the current view
+ const getEarliestEventHour = () => {
+ const visibleEvents = getVisibleEvents();
+
+ if (visibleEvents.length === 0) {
+ return 8; // Default to 8am if no events
+ }
+
+ const earliestHour = Math.min(
+ ...visibleEvents.map((event) => {
+ const [hour] = event.start_time.split(":").map(Number);
+ return hour;
+ })
+ );
+
+ // Return the earlier of 8am or the earliest event hour
+ return Math.min(8, earliestHour);
+ };
+
+ // Get the latest event hour from visible events in the current view
+ const getLatestEventHour = () => {
+ const visibleEvents = getVisibleEvents();
+
+ if (visibleEvents.length === 0) {
+ return 19; // Default to 7pm if no events
+ }
+
+ const latestHour = Math.max(
+ ...visibleEvents.map((event) => {
+ if (!event.end_time) {
+ // If no end time, use start time + 1 hour
+ const [hour] = event.start_time.split(":").map(Number);
+ return hour + 1;
+ }
+ const [hour] = event.end_time.split(":").map(Number);
+ return hour;
+ })
+ );
+
+ // Return the later of 7pm or the latest event hour
+ return Math.max(19, latestHour);
+ };
+
+ // Generate time slots based on view type
+ const getTimeSlots = () => {
+ if (currentView === "month") {
+ // Month view doesn't use time slots
+ return [];
+ }
+
+ const earliestEventHour = getEarliestEventHour();
+ const latestEventHour = getLatestEventHour();
+
+ // Add buffer: start 1 hour before earliest event (but not before 6am)
+ const startHour = Math.max(6, earliestEventHour - 1);
+
+ // Add buffer: end 1 hour after latest event (but not after midnight)
+ const endHour = Math.min(24, latestEventHour + 1);
+
+ const numSlots = endHour - startHour;
+
+ return Array.from(
+ { length: numSlots },
+ (_, i) => `${(startHour + i).toString().padStart(2, "0")}:00`
+ );
+ };
+
+ const timeSlots = getTimeSlots();
const renderMonthView = () => (
@@ -556,7 +679,7 @@ export const PlanningPage = () => {
);
const renderDayView = () => (
-
+
{/* Day header */}
@@ -566,7 +689,7 @@ export const PlanningPage = () => {
{/* Time slots */}
-
+
{timeSlots.map((time) => (
{
{(["month", "week", "day"] as ViewType[]).map((view) => (