commit
c3ff447c59
4 changed files with 134 additions and 14 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ vi.mock("react-router-dom", async () => {
|
|||
};
|
||||
});
|
||||
|
||||
|
||||
vi.mock("../hooks/invite", () => ({
|
||||
useJoinTablo: () => mockJoinTablo,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ vi.mock("../components/AnimatedBackground", () => ({
|
|||
AnimatedBackground: () => <div data-testid="animated-background">Background</div>,
|
||||
}));
|
||||
|
||||
|
||||
describe("LandingPage", () => {
|
||||
it("renders without crashing", () => {
|
||||
const { container } = render(
|
||||
|
|
|
|||
|
|
@ -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<ViewType>("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<ViewType>(initialView);
|
||||
|
||||
const [selectedTabloId, setSelectedTabloId] = useState<string>(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 = () => (
|
||||
<div className="flex-1 bg-card border border-border">
|
||||
|
|
@ -556,7 +679,7 @@ export const PlanningPage = () => {
|
|||
);
|
||||
|
||||
const renderDayView = () => (
|
||||
<div className="flex-1 bg-card border border-border">
|
||||
<div className="flex-1 bg-card border border-border flex flex-col">
|
||||
{/* Day header */}
|
||||
<div className="p-4 border-b border-border text-center">
|
||||
<div className="text-sm text-muted-foreground uppercase">
|
||||
|
|
@ -566,7 +689,7 @@ export const PlanningPage = () => {
|
|||
</div>
|
||||
|
||||
{/* Time slots */}
|
||||
<div className="max-h-[600px] overflow-y-auto">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{timeSlots.map((time) => (
|
||||
<div
|
||||
key={time}
|
||||
|
|
@ -846,7 +969,7 @@ export const PlanningPage = () => {
|
|||
{(["month", "week", "day"] as ViewType[]).map((view) => (
|
||||
<button
|
||||
key={view}
|
||||
onClick={() => setCurrentView(view)}
|
||||
onClick={() => changeView(view)}
|
||||
title={t(`planning:views.${view}Title`)}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors capitalize ${
|
||||
currentView === view
|
||||
|
|
|
|||
Loading…
Reference in a new issue