diff --git a/e2e/fixtures.cy.ts b/e2e/fixtures.cy.ts index 08561fa..fd75ea3 100644 --- a/e2e/fixtures.cy.ts +++ b/e2e/fixtures.cy.ts @@ -1,7 +1,9 @@ beforeEach(() => { - cy.visit('/'); + cy.visit('/fixtures'); cy.wait('@fixtures'); cy.get('.nav-link').contains('Fixtures').click(); + + cy.get('@fixtures').should('have.property', 'state', 'Complete'); }); describe('fixtures', () => { @@ -30,8 +32,8 @@ describe('fixtures', () => { expect(loc.pathname).to.eq('/fixtures/1049002'); }); - cy.wait(['@goals', '@goals', '@goals']).then((interceptions) => { - const reqQuery = JSON.parse(interceptions[2].request.query.json as string); + cy.wait(['@goals', '@goals']).then((interceptions) => { + const reqQuery = JSON.parse(interceptions[1].request.query.json as string); cy.wrap(reqQuery).its('filter').its('fixtureId').should('equal', 1049002); }); diff --git a/e2e/goals.cy.ts b/e2e/goals.cy.ts index acc5e76..49863b8 100644 --- a/e2e/goals.cy.ts +++ b/e2e/goals.cy.ts @@ -13,8 +13,6 @@ describe('Goals', () => { expect(loc.pathname).to.eq('/goals'); }); - cy.get('@fixtures').should('have.property', 'state', 'Complete'); - cy.get('@leagues').should('have.property', 'state', 'Complete'); cy.get('@goals').should('have.property', 'state', 'Complete'); }); diff --git a/e2e/settings.cy.ts b/e2e/settings.cy.ts index 1284422..e853761 100644 --- a/e2e/settings.cy.ts +++ b/e2e/settings.cy.ts @@ -1,5 +1,5 @@ beforeEach(() => { - cy.visit('/'); + cy.visit('/settings'); cy.get('.nav-link').contains('Settings').click(); }); diff --git a/e2e/tabs.cy.ts b/e2e/tabs.cy.ts index b3591fb..195d8ed 100644 --- a/e2e/tabs.cy.ts +++ b/e2e/tabs.cy.ts @@ -3,16 +3,25 @@ describe('tabs', () => { cy.visit('/'); cy.get('.nav-link.active').contains('Home'); + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/goals'); + }); cy.get('[aria-labelledby="home-tab"]').should('be.visible'); - cy.get('[aria-labelledby="fixtures-tab"]').should('not.be.visible'); - cy.get('[aria-labelledby="settings-tab"]').should('not.be.visible'); + cy.get('[aria-labelledby="fixtures-tab"]').should('not.exist'); + cy.get('[aria-labelledby="settings-tab"]').should('not.exist'); cy.get('.nav-link').contains('Fixtures').click(); cy.get('.nav-link.active').contains('Fixtures'); + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/fixtures'); + }); cy.get('[aria-labelledby="fixtures-tab"]').should('be.visible'); cy.get('.nav-link').contains('Settings').click(); cy.get('.nav-link.active').contains('Settings'); + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/settings'); + }); cy.get('[aria-labelledby="settings-tab"]').should('be.visible'); }); }); diff --git a/src/App.test.tsx b/src/App.test.tsx index 9f84bf8..5270620 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,23 +1,8 @@ +import './mocks/matchMedia.ts'; + import {render} from '@testing-library/react'; import App from './App'; test('renders the app with no errors', () => { - // window.matchMedia is not implemented in JSDOM so need to mock it - // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom - - Object.defineProperty(window, 'matchMedia', { - writable: true, - value: vi.fn().mockImplementation((query) => ({ - matches: false, - media: query, - onchange: null, - addListener: vi.fn(), // deprecated - removeListener: vi.fn(), // deprecated - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - dispatchEvent: vi.fn(), - })), - }); - render(); }); diff --git a/src/App.tsx b/src/App.tsx index 4132959..bdcadf7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,35 +1,58 @@ +import Header from './components/Header'; +import {ThemeProvider} from './hooks/useTheme'; import Error from './pages/Error'; import Fixture from './pages/Fixture'; +import Fixtures from './pages/Fixtures'; import Goal from './pages/Goal'; import Goals from './pages/Goals'; +import Settings from './pages/Settings'; import {createBrowserRouter, Navigate, RouterProvider} from 'react-router-dom'; const router = createBrowserRouter([ { - path: '/', - element: , - errorElement: , - }, - { - path: '/goals', - element: , - errorElement: , - }, - { - path: '/goals/:goalId', - element: , - errorElement: , - }, - { - path: '/fixtures/:fixtureId', - element: , - errorElement: , + element:
, + children: [ + { + path: '/', + element: , + errorElement: , + }, + { + path: '/goals', + element: , + errorElement: , + }, + { + path: '/fixtures', + element: , + errorElement: , + }, + { + path: '/settings', + element: , + errorElement: , + }, + { + path: '/goals/:goalId', + element: , + errorElement: , + }, + { + path: '/fixtures/:fixtureId', + element: , + errorElement: , + }, + ], }, ]); function App() { - return ; + return ( + + + + ); } export default App; diff --git a/src/components/FixtureRow.tsx b/src/components/FixtureRow.tsx index 2db9ce2..7171a97 100644 --- a/src/components/FixtureRow.tsx +++ b/src/components/FixtureRow.tsx @@ -36,6 +36,8 @@ function FixtureRow({fixture}: FixtureRowProps) { src={fixture.teams.home.logo} alt="Home team logo" style={{maxWidth: '20px'}} + width={20} + height={20} >
{fixture.teams.home.name}
@@ -45,6 +47,8 @@ function FixtureRow({fixture}: FixtureRowProps) { src={fixture.teams.away.logo} alt="Away team logo" style={{maxWidth: '20px'}} + width={20} + height={20} >
{fixture.teams.away.name}
diff --git a/src/components/FixturesList.tsx b/src/components/FixturesList.tsx index 98946ca..0349340 100644 --- a/src/components/FixturesList.tsx +++ b/src/components/FixturesList.tsx @@ -26,7 +26,7 @@ function FixturesList({fixtures, leagues}: FixturesListProps) { {filteredLeagues.map((league) => (
- League Logo + League Logo
{league.name}
<> diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 2a0a2a0..80895c6 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,26 +1,84 @@ import {useEffect, useState} from 'react'; +import {NavLink, Outlet, useMatch} from 'react-router-dom'; import logoBlack from '../assets/top90logo-black.avif'; import logoWhite from '../assets/top90logo-white.avif'; - -interface HeaderProps { - selectedTheme: string; - onClick?: () => void; -} +import {useTheme} from '../hooks/useTheme'; const DARK = 'dark'; -function Header({selectedTheme, onClick}: HeaderProps) { - const [logo, setLogo] = useState(logoBlack); +function getLogo(theme: string) { + return theme === DARK ? logoWhite : logoBlack; +} + +function Header() { + // Child components can attach a reset function to this state via the outlet context + // so that the header can reset some state that they control. + const [resetFn, setResetFn] = useState<() => void>(); + const {theme} = useTheme(); + const [logo, setLogo] = useState(getLogo(theme)); useEffect(() => { - const logoToDisplay = selectedTheme === DARK ? logoWhite : logoBlack; + const logoToDisplay = getLogo(theme); setLogo(logoToDisplay); - }, [selectedTheme]); + }, [theme]); return ( -
- logo +
+
+
+ logo +
+ +
    +
  • + { + return `nav-link ${isActive ? 'active' : ''}`; + }} + id="home-tab" + type="button" + aria-controls="home" + aria-selected={Boolean(useMatch('/goals'))} + > + Home + +
  • +
  • + { + return `nav-link ${isActive ? 'active' : ''}`; + }} + id="fixtures-tab" + type="button" + aria-controls="fixtures" + aria-selected={Boolean(useMatch('/fixtures'))} + > + Fixtures + +
  • +
  • + { + return `nav-link ${isActive ? 'active' : ''}`; + }} + id="settings-tab" + type="button" + aria-controls="settings" + aria-selected={Boolean(useMatch('/settings'))} + > + Settings + +
  • +
+ +
+ +
+
); } diff --git a/src/components/ThemeSelect.tsx b/src/components/ThemeSelect.tsx index 1196d2a..08f4e53 100644 --- a/src/components/ThemeSelect.tsx +++ b/src/components/ThemeSelect.tsx @@ -1,13 +1,11 @@ -import {getPreferredTheme} from '../lib/utils'; import Select from './Select'; interface ThemeSelectProps { + theme: Theme; onChange: (value: string) => void; } -function ThemeSelect({onChange}: ThemeSelectProps) { - const preferredTheme = getPreferredTheme(); - +function ThemeSelect({theme, onChange}: ThemeSelectProps) { return ( - - -
- -
- -
-
- -
-
- -
-
+
+
+
+ + + +
- {getGoalsResponse?.goals?.map((goal) => ( -
- -
- ))} +
+ +
+
+ +
+
-
-
+
+
-
-
- -
-
-
-
-
- -
-
-
-
- -
-
+ {getGoalsResponse?.goals?.map((goal) => ( +
+
+ ))} + +
+
+ +
+ {getGoalsResponse?.goals && getGoalsResponse?.goals.length > 0 && ( +
+ +
+ )}
); diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx new file mode 100644 index 0000000..fdd9446 --- /dev/null +++ b/src/pages/Settings.tsx @@ -0,0 +1,16 @@ +import ThemeSelect from '../components/ThemeSelect'; +import {useTheme} from '../hooks/useTheme'; + +function Settings() { + const {theme, setTheme} = useTheme(); + + return ( +
+
+ setTheme(value as Theme)}> +
+
+ ); +} + +export default Settings; diff --git a/src/types.d.ts b/src/types.d.ts index e2937d4..a022b26 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1 +1,3 @@ declare module '*.png'; + +type Theme = 'dark' | 'light';