Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: refactor routing #97

Merged
merged 13 commits into from
Dec 20, 2023
8 changes: 5 additions & 3 deletions e2e/fixtures.cy.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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);
});

Expand Down
2 changes: 0 additions & 2 deletions e2e/goals.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand Down
2 changes: 1 addition & 1 deletion e2e/settings.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
beforeEach(() => {
cy.visit('/');
cy.visit('/settings');

cy.get('.nav-link').contains('Settings').click();
});
Expand Down
13 changes: 11 additions & 2 deletions e2e/tabs.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
19 changes: 2 additions & 17 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<App />);
});
61 changes: 42 additions & 19 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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: <Navigate to="/goals" />,
errorElement: <Error />,
},
{
path: '/goals',
element: <Goals />,
errorElement: <Error />,
},
{
path: '/goals/:goalId',
element: <Goal />,
errorElement: <Error />,
},
{
path: '/fixtures/:fixtureId',
element: <Fixture />,
errorElement: <Error />,
element: <Header />,
children: [
{
path: '/',
element: <Navigate to="/goals" />,
errorElement: <Error />,
},
{
path: '/goals',
element: <Goals />,
errorElement: <Error />,
},
{
path: '/fixtures',
element: <Fixtures />,
errorElement: <Error />,
},
{
path: '/settings',
element: <Settings />,
errorElement: <Error />,
},
{
path: '/goals/:goalId',
element: <Goal />,
errorElement: <Error />,
},
{
path: '/fixtures/:fixtureId',
element: <Fixture />,
errorElement: <Error />,
},
],
},
]);

function App() {
return <RouterProvider router={router} />;
return (
<ThemeProvider>
<RouterProvider router={router} />
</ThemeProvider>
);
}

export default App;
4 changes: 4 additions & 0 deletions src/components/FixtureRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ function FixtureRow({fixture}: FixtureRowProps) {
src={fixture.teams.home.logo}
alt="Home team logo"
style={{maxWidth: '20px'}}
width={20}
height={20}
></img>
<div>{fixture.teams.home.name}</div>
</div>
Expand All @@ -45,6 +47,8 @@ function FixtureRow({fixture}: FixtureRowProps) {
src={fixture.teams.away.logo}
alt="Away team logo"
style={{maxWidth: '20px'}}
width={20}
height={20}
></img>
<div>{fixture.teams.away.name}</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/FixturesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function FixturesList({fixtures, leagues}: FixturesListProps) {
{filteredLeagues.map((league) => (
<div key={league.id} className="w-100" data-cy="fixture-group">
<div className="d-flex mb-3 mt-4 align-items-center">
<img src={league.logo} alt="League Logo" className="me-2" height={25}></img>
<img src={league.logo} alt="League Logo" className="me-2" height={25} width={25}></img>
<div>{league.name}</div>
</div>
<>
Expand Down
80 changes: 69 additions & 11 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="d-flex justify-content-center">
<img height={250} src={logo} onClick={onClick} alt="logo" role="button" />
<div className="container d-flex justify-content-center">
<div className="top90-app-container">
<div className="d-flex justify-content-center">
<img height={250} src={logo} onClick={resetFn} alt="logo" role="button" />
</div>

<ul className="nav nav-tabs" role="tablist">
<li className="nav-item">
<NavLink
to="/goals"
className={({isActive}) => {
return `nav-link ${isActive ? 'active' : ''}`;
}}
id="home-tab"
type="button"
aria-controls="home"
aria-selected={Boolean(useMatch('/goals'))}
>
Home
</NavLink>
</li>
<li className="nav-item">
<NavLink
to="/fixtures"
className={({isActive}) => {
return `nav-link ${isActive ? 'active' : ''}`;
}}
id="fixtures-tab"
type="button"
aria-controls="fixtures"
aria-selected={Boolean(useMatch('/fixtures'))}
>
Fixtures
</NavLink>
</li>
<li className="nav-item">
<NavLink
to="/settings"
className={({isActive}) => {
return `nav-link ${isActive ? 'active' : ''}`;
}}
id="settings-tab"
type="button"
aria-controls="settings"
aria-selected={Boolean(useMatch('/settings'))}
>
Settings
</NavLink>
</li>
</ul>

<div className="tab-content">
<Outlet context={[resetFn, setResetFn]} />
</div>
</div>
</div>
);
}
Expand Down
8 changes: 3 additions & 5 deletions src/components/ThemeSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
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 (
<Select
label="Theme"
options={[
{key: 'dark', value: 'dark', displayName: 'Dark'},
{key: 'light', value: 'light', displayName: 'Light'},
]}
value={preferredTheme}
value={theme}
onChange={onChange}
showAllOption={false}
/>
Expand Down
30 changes: 30 additions & 0 deletions src/hooks/useTheme.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {createContext, useContext, useState} from 'react';
import {getPreferredTheme, setDocumentTheme} from '../lib/utils';

interface ThemeContext {
theme: Theme;
setTheme: (theme: Theme) => void;
}

const currentTheme = getPreferredTheme();

const ThemeContext = createContext<ThemeContext>({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 (
<ThemeContext.Provider value={{theme, setTheme: setGlobalTheme}}>
{children}
</ThemeContext.Provider>
);
};

export const useTheme = () => {
return useContext(ThemeContext);
};
7 changes: 4 additions & 3 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
export function getPreferredTheme() {
export function getPreferredTheme(): Theme {
let storedTheme = localStorage.getItem('top90-theme');
if (storedTheme) {
return storedTheme;
return storedTheme as Theme;
}

return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}

export function setTheme(theme: string) {
export function setDocumentTheme(theme: Theme) {
localStorage.setItem('top90-theme', theme);
document.documentElement.setAttribute('data-bs-theme', theme);
}

Expand Down
16 changes: 16 additions & 0 deletions src/mocks/matchMedia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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(),
})),
});
Loading
Loading