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;