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) => (