diff --git a/__tests__/components/schedule/ScheduleContainer.test.tsx b/__tests__/components/schedule/ScheduleContainer.test.tsx new file mode 100644 index 00000000..7337ce5c --- /dev/null +++ b/__tests__/components/schedule/ScheduleContainer.test.tsx @@ -0,0 +1,62 @@ +import { render, screen } from "@testing-library/react"; +import ScheduleContainer from "../../../components/schedule/ScheduleContainer"; +import { DailySchedule } from "../../../hooks/useSchedule"; +import { useMediaQuery } from "../../../hooks/useMediaQuery"; + +// Mock the hook +jest.mock("../../../hooks/useMediaQuery"); + +// Mock child components to verify rendering +jest.mock("../../../components/schedule/ScheduleGrid", () => { + const MockScheduleGrid = () =>
Grid
; + MockScheduleGrid.displayName = "MockScheduleGrid"; + return MockScheduleGrid; +}); + +jest.mock("../../../components/schedule/ScheduleMobile", () => { + const MockScheduleMobile = () =>
Mobile
; + MockScheduleMobile.displayName = "MockScheduleMobile"; + return MockScheduleMobile; +}); + +// Mock context +jest.mock("../../../context/ScheduleContext", () => ({ + useScheduleContext: () => ({ + savedSessionIds: [], + isSaved: () => false, + toggleSession: jest.fn(), + }), +})); + +const mockSchedule: DailySchedule[] = []; + +describe("ScheduleContainer Optimization", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("renders both components initially (SSR/Hydration)", () => { + (useMediaQuery as jest.Mock).mockReturnValue(null); + render(); + + // Both should be present to support hydration matching and CSS hiding + expect(screen.getByTestId("schedule-grid")).toBeInTheDocument(); + expect(screen.getByTestId("schedule-mobile")).toBeInTheDocument(); + }); + + it("renders only ScheduleGrid on Desktop", () => { + (useMediaQuery as jest.Mock).mockReturnValue(false); + render(); + + expect(screen.getByTestId("schedule-grid")).toBeInTheDocument(); + expect(screen.queryByTestId("schedule-mobile")).not.toBeInTheDocument(); + }); + + it("renders only ScheduleMobile on Mobile", () => { + (useMediaQuery as jest.Mock).mockReturnValue(true); + render(); + + expect(screen.queryByTestId("schedule-grid")).not.toBeInTheDocument(); + expect(screen.getByTestId("schedule-mobile")).toBeInTheDocument(); + }); +}); diff --git a/components/schedule/ScheduleContainer.tsx b/components/schedule/ScheduleContainer.tsx index 3fa22ef2..9c3379fb 100644 --- a/components/schedule/ScheduleContainer.tsx +++ b/components/schedule/ScheduleContainer.tsx @@ -6,6 +6,7 @@ import ScheduleGrid from "./ScheduleGrid"; import ScheduleMobile from "./ScheduleMobile"; import { useScheduleContext } from "@/context/ScheduleContext"; import styles from "./schedule.module.scss"; +import { useMediaQuery } from "@/hooks/useMediaQuery"; interface ScheduleContainerProps { initialSchedule: DailySchedule[]; @@ -15,6 +16,8 @@ interface ScheduleContainerProps { export default function ScheduleContainer({ initialSchedule, year }: ScheduleContainerProps) { const { savedSessionIds } = useScheduleContext(); const [showSavedOnly, setShowSavedOnly] = useState(false); + const isMobile = useMediaQuery("(max-width: 991px)"); + const filteredSchedule = useMemo(() => { if (!showSavedOnly) { return initialSchedule; @@ -46,12 +49,24 @@ export default function ScheduleContainer({ initialSchedule, year }: ScheduleCon -
- -
-
- -
+ {/* + Optimization: Only render the visible component to reduce DOM nodes. + Render both (hidden via CSS) only during SSR/initial hydration (isMobile === null). + */} + + {/* Desktop View */} + {(isMobile === null || isMobile === false) && ( +
+ +
+ )} + + {/* Mobile View */} + {(isMobile === null || isMobile === true) && ( +
+ +
+ )} ); } diff --git a/cypress/screenshots/home-editions.cy.ts/Home Pages (2023-2026) -- should load the homepage for 2023 with correct venue and dates (failed).png b/cypress/screenshots/home-editions.cy.ts/Home Pages (2023-2026) -- should load the homepage for 2023 with correct venue and dates (failed).png new file mode 100644 index 00000000..0a3e64c2 Binary files /dev/null and b/cypress/screenshots/home-editions.cy.ts/Home Pages (2023-2026) -- should load the homepage for 2023 with correct venue and dates (failed).png differ diff --git a/cypress/screenshots/home-editions.cy.ts/Home Pages (2023-2026) -- should load the homepage for 2024 with correct venue and dates (failed).png b/cypress/screenshots/home-editions.cy.ts/Home Pages (2023-2026) -- should load the homepage for 2024 with correct venue and dates (failed).png new file mode 100644 index 00000000..732340f9 Binary files /dev/null and b/cypress/screenshots/home-editions.cy.ts/Home Pages (2023-2026) -- should load the homepage for 2024 with correct venue and dates (failed).png differ diff --git a/cypress/screenshots/home-editions.cy.ts/Home Pages (2023-2026) -- should load the homepage for 2025 with correct venue and dates (failed).png b/cypress/screenshots/home-editions.cy.ts/Home Pages (2023-2026) -- should load the homepage for 2025 with correct venue and dates (failed).png new file mode 100644 index 00000000..26e721ed Binary files /dev/null and b/cypress/screenshots/home-editions.cy.ts/Home Pages (2023-2026) -- should load the homepage for 2025 with correct venue and dates (failed).png differ diff --git a/cypress/screenshots/home-editions.cy.ts/Home Pages (2023-2026) -- should load the homepage for 2026 with correct venue and dates (failed).png b/cypress/screenshots/home-editions.cy.ts/Home Pages (2023-2026) -- should load the homepage for 2026 with correct venue and dates (failed).png new file mode 100644 index 00000000..d9e799e7 Binary files /dev/null and b/cypress/screenshots/home-editions.cy.ts/Home Pages (2023-2026) -- should load the homepage for 2026 with correct venue and dates (failed).png differ diff --git a/hooks/useMediaQuery.ts b/hooks/useMediaQuery.ts new file mode 100644 index 00000000..a967f50b --- /dev/null +++ b/hooks/useMediaQuery.ts @@ -0,0 +1,37 @@ +"use client"; + +import { useState, useEffect } from "react"; + +/** + * Custom hook to detect if a media query matches. + * Returns null during SSR/hydration to avoid mismatches, + * and then updates to true/false on the client. + */ +export function useMediaQuery(query: string): boolean | null { + const [matches, setMatches] = useState(null); + + useEffect(() => { + const media = window.matchMedia(query); + if (media.matches !== matches) { + setMatches(media.matches); + } + const listener = (e: MediaQueryListEvent) => setMatches(e.matches); + + // Support for older browsers and modern ones + if (media.addEventListener) { + media.addEventListener("change", listener); + } else { + media.addListener(listener); + } + return () => { + if (media.removeEventListener) { + media.removeEventListener("change", listener); + } else { + media.removeListener(listener); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [query]); + + return matches; +} diff --git a/stylelint.config.mjs b/stylelint.config.mjs index ce94e4ff..ff7408cf 100644 --- a/stylelint.config.mjs +++ b/stylelint.config.mjs @@ -1,8 +1,5 @@ const config = { - extends: [ - "stylelint-config-standard", - "stylelint-config-standard-scss" - ], + extends: ["stylelint-config-standard", "stylelint-config-standard-scss"], rules: { "no-descending-specificity": null, "selector-class-pattern": null, @@ -14,8 +11,8 @@ const config = { "keyframes-name-pattern": null, "declaration-block-no-shorthand-property-overrides": null, "block-no-empty": null, - "number-max-precision": null - } + "number-max-precision": null, + }, }; export default config;