Merge pull request #33 from artslidd/develop

Improve planning view
This commit is contained in:
Arthur Belleville 2025-10-29 09:39:47 +01:00 committed by GitHub
commit c3ff447c59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 134 additions and 14 deletions

View file

@ -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";

View file

@ -16,7 +16,6 @@ vi.mock("react-router-dom", async () => {
};
});
vi.mock("../hooks/invite", () => ({
useJoinTablo: () => mockJoinTablo,
}));

View file

@ -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(

View file

@ -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