diff --git a/e2e/fixtures.cy.ts b/e2e/fixtures.cy.ts
index fd75ea3..08561fa 100644
--- a/e2e/fixtures.cy.ts
+++ b/e2e/fixtures.cy.ts
@@ -1,9 +1,7 @@
beforeEach(() => {
- cy.visit('/fixtures');
+ cy.visit('/');
cy.wait('@fixtures');
cy.get('.nav-link').contains('Fixtures').click();
-
- cy.get('@fixtures').should('have.property', 'state', 'Complete');
});
describe('fixtures', () => {
@@ -32,8 +30,8 @@ describe('fixtures', () => {
expect(loc.pathname).to.eq('/fixtures/1049002');
});
- cy.wait(['@goals', '@goals']).then((interceptions) => {
- const reqQuery = JSON.parse(interceptions[1].request.query.json as string);
+ cy.wait(['@goals', '@goals', '@goals']).then((interceptions) => {
+ const reqQuery = JSON.parse(interceptions[2].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 49863b8..acc5e76 100644
--- a/e2e/goals.cy.ts
+++ b/e2e/goals.cy.ts
@@ -13,6 +13,8 @@ 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 e853761..1284422 100644
--- a/e2e/settings.cy.ts
+++ b/e2e/settings.cy.ts
@@ -1,5 +1,5 @@
beforeEach(() => {
- cy.visit('/settings');
+ cy.visit('/');
cy.get('.nav-link').contains('Settings').click();
});
diff --git a/e2e/tabs.cy.ts b/e2e/tabs.cy.ts
index 195d8ed..b3591fb 100644
--- a/e2e/tabs.cy.ts
+++ b/e2e/tabs.cy.ts
@@ -3,25 +3,16 @@ 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.exist');
- cy.get('[aria-labelledby="settings-tab"]').should('not.exist');
+ cy.get('[aria-labelledby="fixtures-tab"]').should('not.be.visible');
+ cy.get('[aria-labelledby="settings-tab"]').should('not.be.visible');
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 5270620..9f84bf8 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -1,8 +1,23 @@
-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 bdcadf7..4132959 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,58 +1,35 @@
-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([
{
- 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: ,
- },
- ],
+ path: '/',
+ element: ,
+ errorElement: ,
+ },
+ {
+ path: '/goals',
+ 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 7171a97..2db9ce2 100644
--- a/src/components/FixtureRow.tsx
+++ b/src/components/FixtureRow.tsx
@@ -36,8 +36,6 @@ function FixtureRow({fixture}: FixtureRowProps) {
src={fixture.teams.home.logo}
alt="Home team logo"
style={{maxWidth: '20px'}}
- width={20}
- height={20}
>
{fixture.teams.home.name}
@@ -47,8 +45,6 @@ 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 0349340..98946ca 100644
--- a/src/components/FixturesList.tsx
+++ b/src/components/FixturesList.tsx
@@ -26,7 +26,7 @@ function FixturesList({fixtures, leagues}: FixturesListProps) {
{filteredLeagues.map((league) => (
-
+
{league.name}
<>
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 80895c6..2a0a2a0 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -1,84 +1,26 @@
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';
-import {useTheme} from '../hooks/useTheme';
-const DARK = 'dark';
-
-function getLogo(theme: string) {
- return theme === DARK ? logoWhite : logoBlack;
+interface HeaderProps {
+ selectedTheme: string;
+ onClick?: () => void;
}
-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));
+const DARK = 'dark';
+
+function Header({selectedTheme, onClick}: HeaderProps) {
+ const [logo, setLogo] = useState(logoBlack);
useEffect(() => {
- const logoToDisplay = getLogo(theme);
+ const logoToDisplay = selectedTheme === DARK ? logoWhite : logoBlack;
setLogo(logoToDisplay);
- }, [theme]);
+ }, [selectedTheme]);
return (
-
-
-
-
-
-
-
- -
- {
- 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 a95aca6..1196d2a 100644
--- a/src/components/ThemeSelect.tsx
+++ b/src/components/ThemeSelect.tsx
@@ -1,12 +1,13 @@
-import {Theme} from '../hooks/useTheme';
+import {getPreferredTheme} from '../lib/utils';
import Select from './Select';
interface ThemeSelectProps {
- theme: Theme;
onChange: (value: string) => void;
}
-function ThemeSelect({theme, onChange}: ThemeSelectProps) {
+function ThemeSelect({onChange}: ThemeSelectProps) {
+ const preferredTheme = getPreferredTheme();
+
return (
diff --git a/src/hooks/useTheme.tsx b/src/hooks/useTheme.tsx
deleted file mode 100644
index 854d765..0000000
--- a/src/hooks/useTheme.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import {createContext, useContext, useState} from 'react';
-import {getPreferredTheme, setDocumentTheme} from '../lib/utils';
-
-export type Theme = 'dark' | 'light';
-
-interface ThemeContext {
- theme: Theme;
- setTheme: (theme: Theme) => void;
-}
-
-const currentTheme = getPreferredTheme();
-
-const ThemeContext = createContext
({theme: currentTheme, setTheme: () => null});
-
-export const ThemeProvider = ({children}: {children: React.ReactNode}) => {
- const [theme, setTheme] = useState(currentTheme);
-
- function setGlobalTheme(theme: Theme) {
- setTheme(theme);
- setDocumentTheme(theme);
- }
-
- return (
-
- {children}
-
- );
-};
-
-export const useTheme = () => {
- return useContext(ThemeContext);
-};
diff --git a/src/index.css b/src/index.css
index 8d35494..98caf6b 100644
--- a/src/index.css
+++ b/src/index.css
@@ -33,16 +33,3 @@ body {
width: 100%;
max-width: 800px;
}
-
-.fade-in {
- animation: fadeIn 0.5s;
-}
-
-@keyframes fadeIn {
- 0% {
- opacity: 0;
- }
- 100% {
- opacity: 1;
- }
-}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 1fcc048..100b317 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,16 +1,13 @@
-import {Theme} from '../hooks/useTheme';
-
-export function getPreferredTheme(): Theme {
+export function getPreferredTheme() {
let storedTheme = localStorage.getItem('top90-theme');
if (storedTheme) {
- return storedTheme as Theme;
+ return storedTheme;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
-export function setDocumentTheme(theme: Theme) {
- localStorage.setItem('top90-theme', theme);
+export function setTheme(theme: string) {
document.documentElement.setAttribute('data-bs-theme', theme);
}
diff --git a/src/mocks/matchMedia.ts b/src/mocks/matchMedia.ts
deleted file mode 100644
index 49086ab..0000000
--- a/src/mocks/matchMedia.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-// 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(),
- removeListener: vi.fn(),
- addEventListener: vi.fn(),
- removeEventListener: vi.fn(),
- dispatchEvent: vi.fn(),
- })),
-});
diff --git a/src/pages/Fixture.tsx b/src/pages/Fixture.tsx
index b24e2cd..2dfe6c8 100644
--- a/src/pages/Fixture.tsx
+++ b/src/pages/Fixture.tsx
@@ -1,16 +1,23 @@
import {useEffect, useState} from 'react';
-import {NavLink, useParams} from 'react-router-dom';
+import {useNavigate, useParams} from 'react-router-dom';
import FixtureRow from '../components/FixtureRow';
+import Header from '../components/Header';
import Video from '../components/Video';
import {getFixture, GetFixtureResponse} from '../lib/api/fixtures';
import {getGoals, GetGoalsResponse} from '../lib/api/goals';
+import {getPreferredTheme} from '../lib/utils';
function Fixture() {
const {fixtureId} = useParams();
+ const navigate = useNavigate();
const [getFixtureResponse, setGetFixtureResponse] = useState();
const [getGoalsResponse, setGetGoalsResponse] = useState();
+ function navigateHome() {
+ navigate('/');
+ }
+
useEffect(() => {
if (fixtureId) {
getFixture(fixtureId).then((data) => {
@@ -27,29 +34,35 @@ function Fixture() {
}
return (
- <>
-
-
-
-
+
+
+
+
+
- {getGoalsResponse?.goals?.map((goal) => (
-
-
-
- ))}
+
+
+
- {(!getGoalsResponse?.goals || getGoalsResponse.goals.length === 0) && (
-
No goals found for this fixture.
- )}
-
+ {getGoalsResponse?.goals?.map((goal) => (
+
+
+
+ ))}
-
-
- Back to homepage
-
+ {(!getGoalsResponse?.goals || getGoalsResponse.goals.length === 0) && (
+
No goals found for this fixture.
+ )}
+
+
+
+
+
+
- >
+
);
}
diff --git a/src/pages/Fixtures.tsx b/src/pages/Fixtures.tsx
deleted file mode 100644
index 252a4ad..0000000
--- a/src/pages/Fixtures.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import {useEffect, useState} from 'react';
-import FixturesList from '../components/FixturesList';
-import {getFixtures, GetFixturesResponse} from '../lib/api/fixtures';
-import {getLeagues, GetLeaguesResponse} from '../lib/api/leagues';
-
-function Fixtures() {
- const [getFixturesResponse, setGetFixturesResponse] = useState
();
- const [getLeaguesResponse, setGetLeaguesResponse] = useState();
-
- useEffect(() => {
- getFixtures({todayOnly: true}).then((data) => {
- setGetFixturesResponse(data);
- });
- getLeagues().then((data) => {
- setGetLeaguesResponse(data);
- });
- }, []);
-
- return (
-
- );
-}
-
-export default Fixtures;
diff --git a/src/pages/Goal.tsx b/src/pages/Goal.tsx
index 06fc2e2..85a3bc3 100644
--- a/src/pages/Goal.tsx
+++ b/src/pages/Goal.tsx
@@ -4,6 +4,8 @@ import {getGoal, GetGoalResponse} from '../lib/api/goals';
import {useEffect, useState} from 'react';
import {useNavigate, useParams} from 'react-router-dom';
+import Header from '../components/Header';
+import {getPreferredTheme} from '../lib/utils';
function Goal() {
const {goalId} = useParams();
@@ -27,6 +29,7 @@ function Goal() {
+
{getGoalResponse?.goal && }
diff --git a/src/pages/Goals.tsx b/src/pages/Goals.tsx
index 4f91f80..3090f4d 100644
--- a/src/pages/Goals.tsx
+++ b/src/pages/Goals.tsx
@@ -1,14 +1,19 @@
import 'bootstrap/js/dist/tab';
import {useEffect, useState} from 'react';
import ReactPaginate from 'react-paginate';
-import {useOutletContext} from 'react-router-dom';
+import FixturesList from '../components/FixturesList';
+import Header from '../components/Header';
import Input from '../components/Input';
import Select from '../components/Select';
+import ThemeSelect from '../components/ThemeSelect';
import Video from '../components/Video';
import {Pagination} from '../lib/api/core';
+import {getFixtures, GetFixturesResponse} from '../lib/api/fixtures';
import {getGoals, GetGoalsFilter, GetGoalsResponse} from '../lib/api/goals';
+import {getLeagues, GetLeaguesResponse} from '../lib/api/leagues';
import {Player, searchPlayers, SearchPlayersResponse} from '../lib/api/players';
import {getTeams, GetTeamsResponse} from '../lib/api/teams';
+import {getPreferredTheme, setTheme} from '../lib/utils';
const defaultPagination: Pagination = {skip: 0, limit: 5};
@@ -19,29 +24,24 @@ function Goals() {
const [selectedTeamId, setSelectedTeamId] = useState
();
const [selectedPlayer, setSelectedPlayer] = useState();
const [searchInput, setSearchInput] = useState('');
+ const [selectedTheme, setSelectedTheme] = useState(getPreferredTheme());
const [getGoalsResponse, setGetGoalsResponse] = useState();
const [getTeamsResponse, setGetTeamsResponse] = useState();
- const [searchPlayersResponse, setSearchPlayersResponse] = useState();
- const [_, setResetFn] = useOutletContext<[Function, React.Dispatch]>();
+ const [getFixturesResponse, setGetFixturesResponse] = useState();
+ const [getLeaguesResponse, setGetLeaguesResponse] = useState();
+ const [searchPlayesResponse, setSearchPlayersResponse] = useState();
const pageCount = Math.ceil(
(getGoalsResponse ? getGoalsResponse.total : 0) / (pagination.limit || defaultPagination.limit)
);
- function reset() {
- setGetGoalsResponse(undefined);
- getGoals().then((data) => setGetGoalsResponse(data));
-
- setSelectedLeagueId(undefined);
- setSelectedTeamId(undefined);
- setSelectedPlayer(undefined);
- setSearchInput('');
- setCurrentPage(0);
- setPagination(defaultPagination);
- }
-
useEffect(() => {
- setResetFn(() => () => reset());
+ getFixtures({todayOnly: true}).then((data) => {
+ setGetFixturesResponse(data);
+ });
+ getLeagues().then((data) => {
+ setGetLeaguesResponse(data);
+ });
getGoals().then((data) => {
setGetGoalsResponse(data);
});
@@ -138,105 +138,202 @@ function Goals() {
});
}
+ function reset() {
+ setGetGoalsResponse(undefined);
+ getGoals().then((data) => setGetGoalsResponse(data));
+
+ setSelectedLeagueId(undefined);
+ setSelectedTeamId(undefined);
+ setSelectedPlayer(undefined);
+ setSearchInput('');
+ setCurrentPage(0);
+ setPagination(defaultPagination);
+ }
+
+ function changeTheme(value: string) {
+ const selectedTheme = value;
+ localStorage.setItem('top90-theme', selectedTheme);
+ setTheme(selectedTheme);
+ setSelectedTheme(selectedTheme);
+ }
+
return (
-
-
-
-
-
-
-
+
+
+
-
-
-
+
+ -
+
+
+ -
+
+
+ -
+
+
+
-
-
+
+
+
+
+
+
+
+
- {getGoalsResponse?.goals?.map((goal) => (
-
-
-
- ))}
-
-
-
-
-
-
+
+ {getGoalsResponse?.goals?.map((goal) => (
+
+
+
+ ))}
+
+
+
+
+
+
+
+
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
deleted file mode 100644
index 913f2e3..0000000
--- a/src/pages/Settings.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import ThemeSelect from '../components/ThemeSelect';
-import {Theme, useTheme} from '../hooks/useTheme';
-
-function Settings() {
- const {theme, setTheme} = useTheme();
-
- return (
-
-
- setTheme(value as Theme)}>
-
-
- );
-}
-
-export default Settings;