diff --git a/website/package-lock.json b/website/package-lock.json
index 5cee36bd..d4604466 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -12,16 +12,15 @@
"d3-geo": "^1.11.1",
"d3-scale-chromatic": "^1.3.3",
"date-fns": "^2.6.0",
- "history": "^4.7.2",
- "lodash": "^4.17.4",
+ "lodash": "^4.17.21",
"polished": "^3.4.2",
"prop-types": "^15.6.0",
"rc-slider": "^9.2.4",
- "react": "^16.2.0",
- "react-dom": "^16.2.0",
- "react-helmet": "^6.0.0",
- "react-media": "^1.8.0",
- "react-router-dom": "^5.1.2",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-helmet": "^6.1.0",
+ "react-media": "^1.10.0",
+ "react-router-dom": "^6.24.1",
"react-tweet-embed": "^1.1.0",
"styled-components": "^6.1.11",
"topojson-client": "^3.0.0",
@@ -1141,6 +1140,15 @@
"node": ">= 8"
}
},
+ "node_modules/@remix-run/router": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.17.1.tgz",
+ "integrity": "sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz",
@@ -3523,29 +3531,6 @@
"node": ">= 0.4"
}
},
- "node_modules/history": {
- "version": "4.10.1",
- "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
- "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.1.2",
- "loose-envify": "^1.2.0",
- "resolve-pathname": "^3.0.0",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0",
- "value-equal": "^1.0.1"
- }
- },
- "node_modules/hoist-non-react-statics": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
- "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "react-is": "^16.7.0"
- }
- },
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -3730,12 +3715,6 @@
"node": ">=8"
}
},
- "node_modules/isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
- "license": "MIT"
- },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -4287,15 +4266,6 @@
"license": "MIT",
"peer": true
},
- "node_modules/path-to-regexp": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
- "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
- "license": "MIT",
- "dependencies": {
- "isarray": "0.0.1"
- }
- },
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -4547,32 +4517,28 @@
"license": "MIT"
},
"node_modules/react": {
- "version": "16.14.0",
- "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
- "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"dependencies": {
- "loose-envify": "^1.1.0",
- "object-assign": "^4.1.1",
- "prop-types": "^15.6.2"
+ "loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
- "version": "16.14.0",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
- "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==",
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
- "object-assign": "^4.1.1",
- "prop-types": "^15.6.2",
- "scheduler": "^0.19.1"
+ "scheduler": "^0.23.2"
},
"peerDependencies": {
- "react": "^16.14.0"
+ "react": "^18.3.1"
}
},
"node_modules/react-fast-compare": {
@@ -4628,41 +4594,35 @@
}
},
"node_modules/react-router": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
- "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.24.1.tgz",
+ "integrity": "sha512-PTXFXGK2pyXpHzVo3rR9H7ip4lSPZZc0bHG5CARmj65fTT6qG7sTngmb6lcYu1gf3y/8KxORoy9yn59pGpCnpg==",
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.12.13",
- "history": "^4.9.0",
- "hoist-non-react-statics": "^3.1.0",
- "loose-envify": "^1.3.1",
- "path-to-regexp": "^1.7.0",
- "prop-types": "^15.6.2",
- "react-is": "^16.6.0",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0"
+ "@remix-run/router": "1.17.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
},
"peerDependencies": {
- "react": ">=15"
+ "react": ">=16.8"
}
},
"node_modules/react-router-dom": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
- "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.24.1.tgz",
+ "integrity": "sha512-U19KtXqooqw967Vw0Qcn5cOvrX5Ejo9ORmOtJMzYWtCT4/WOfFLIZGGsVLxcd9UkBO0mSTZtXqhZBsWlHr7+Sg==",
"license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.12.13",
- "history": "^4.9.0",
- "loose-envify": "^1.3.1",
- "prop-types": "^15.6.2",
- "react-router": "5.3.4",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0"
+ "@remix-run/router": "1.17.1",
+ "react-router": "6.24.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
},
"peerDependencies": {
- "react": ">=15"
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
}
},
"node_modules/react-side-effect": {
@@ -4747,12 +4707,6 @@
"node": ">=4"
}
},
- "node_modules/resolve-pathname": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
- "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==",
- "license": "MIT"
- },
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -4854,13 +4808,12 @@
"license": "MIT"
},
"node_modules/scheduler": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
- "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"dependencies": {
- "loose-envify": "^1.1.0",
- "object-assign": "^4.1.1"
+ "loose-envify": "^1.1.0"
}
},
"node_modules/semver": {
@@ -5188,18 +5141,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/tiny-invariant": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
- "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
- "license": "MIT"
- },
- "node_modules/tiny-warning": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
- "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
- "license": "MIT"
- },
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -5350,12 +5291,6 @@
"punycode": "^2.1.0"
}
},
- "node_modules/value-equal": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
- "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==",
- "license": "MIT"
- },
"node_modules/vite": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
diff --git a/website/package.json b/website/package.json
index 4878cf90..5821aca5 100644
--- a/website/package.json
+++ b/website/package.json
@@ -8,16 +8,15 @@
"d3-geo": "^1.11.1",
"d3-scale-chromatic": "^1.3.3",
"date-fns": "^2.6.0",
- "history": "^4.7.2",
- "lodash": "^4.17.4",
+ "lodash": "^4.17.21",
"polished": "^3.4.2",
"prop-types": "^15.6.0",
"rc-slider": "^9.2.4",
- "react": "^16.2.0",
- "react-dom": "^16.2.0",
- "react-helmet": "^6.0.0",
- "react-media": "^1.8.0",
- "react-router-dom": "^5.1.2",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-helmet": "^6.1.0",
+ "react-media": "^1.10.0",
+ "react-router-dom": "^6.24.1",
"react-tweet-embed": "^1.1.0",
"styled-components": "^6.1.11",
"topojson-client": "^3.0.0",
diff --git a/website/src/components/App.tsx b/website/src/components/App.tsx
new file mode 100644
index 00000000..16e83326
--- /dev/null
+++ b/website/src/components/App.tsx
@@ -0,0 +1,45 @@
+import {lazy, Suspense} from 'react';
+import {BrowserRouter, Route, Routes} from 'react-router-dom';
+import {ThemeProvider} from 'styled-components';
+
+import theme from '../resources/theme.json';
+
+const AsyncFootballScheduleScreen = lazy(() =>
+ import('../screens/FootballScheduleScreen/index').then((module) => ({
+ default: module.FootballScheduleScreen,
+ }))
+);
+
+const AsyncExplorablesScreen = lazy(() =>
+ // @ts-expect-error TODO: Remove this after porting explorables to TypeScript.
+ import('../screens/ExplorablesScreen/index').then((module) => ({
+ default: module.ExplorablesScreen,
+ }))
+);
+
+export const App: React.FC = () => {
+ return (
+
+
+
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ );
+};
diff --git a/website/src/components/gameSummary/GameSummary.tsx b/website/src/components/gameSummary/GameSummary.tsx
index 452f5c15..d7fc04d8 100644
--- a/website/src/components/gameSummary/GameSummary.tsx
+++ b/website/src/components/gameSummary/GameSummary.tsx
@@ -1,4 +1,3 @@
-import clone from 'lodash/clone';
import React from 'react';
import {FullSchedule, TeamId} from '../../models';
@@ -12,8 +11,7 @@ export const GameSummary: React.FC<{
readonly selectedSeason: number;
readonly selectedGameIndex: number;
}> = ({selectedSeason, selectedGameIndex}) => {
- const games = schedule[selectedSeason];
- const game = clone(games[selectedGameIndex]);
+ const game = schedule[selectedSeason][selectedGameIndex];
const homeTeamId = game.isHomeGame ? TeamId.ND : game.opponentId;
const awayTeamId = game.isHomeGame ? game.opponentId : TeamId.ND;
diff --git a/website/src/index.tsx b/website/src/index.tsx
index f8572c1a..67fae76e 100644
--- a/website/src/index.tsx
+++ b/website/src/index.tsx
@@ -1,48 +1,15 @@
-import {createBrowserHistory} from 'history';
-import ReactDOM from 'react-dom';
-import {Route, Router, Switch} from 'react-router-dom';
-import {ThemeProvider} from 'styled-components';
+import {createRoot} from 'react-dom/client';
-import theme from './resources/theme.json';
+import {App} from './components/App';
-import './index.css';
import './weather-icons.min.css';
-// Load fonts
+import './index.css';
import 'typeface-bungee';
-import {lazy, Suspense} from 'react';
-
-const history = createBrowserHistory();
-
-export const AsyncFootballScheduleScreen = lazy(() =>
- import('./screens/FootballScheduleScreen/index').then((module) => ({
- default: module.FootballScheduleScreen,
- }))
-);
-
-export const AsyncExplorablesScreen = lazy(() =>
- // @ts-expect-error TODO: Remove this after porting explorables to TypeScript.
- import('./screens/ExplorablesScreen/index').then((module) => ({
- default: module.ExplorablesScreen,
- }))
-);
+const rootDiv = document.getElementById('root');
+if (!rootDiv) {
+ throw new Error('Root element not found');
+}
-ReactDOM.render(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ,
- document.getElementById('root')
-);
+const root = createRoot(rootDiv);
+root.render();
diff --git a/website/src/lib/urls.ts b/website/src/lib/urls.ts
index 76a87d1f..134994ca 100644
--- a/website/src/lib/urls.ts
+++ b/website/src/lib/urls.ts
@@ -10,40 +10,26 @@ const schedule = scheduleJson as FullSchedule;
const DEFAULT_SELECTED_GAME_INDEX = 0;
-export const getYearFromUrl = (url = ''): number => {
- const tokens = url.split('/').filter((val) => val !== '');
-
- let year: number | undefined;
- if (tokens.length > 0 && tokens[0].length === 4) {
- year = Number(tokens[0]);
- }
-
- if (!year || isNaN(year) || !_.has(schedule, year)) {
- return CURRENT_SEASON;
- }
-
- return year;
+export const getSelectedSeasonFromUrlParam = (maybeYearString?: string): number => {
+ if (!maybeYearString || maybeYearString.length !== 4) return CURRENT_SEASON;
+ const year = Number(maybeYearString);
+ return isNaN(year) || !_.has(schedule, year) ? CURRENT_SEASON : year;
};
-export const getSelectedGameIndexFromUrl = (url = ''): number => {
- const year = getYearFromUrl(url);
-
- const tokens = url.split('/').filter((val) => val !== '');
-
- let selectedGameIndex: number | undefined;
- if (tokens.length > 1) {
- selectedGameIndex = Number(tokens[1]);
- }
-
+export const getSelectedGameIndexFromUrlParam = (
+ year: number,
+ maybeWeekString?: string
+): number => {
+ const maybeValidWeek = Number(maybeWeekString);
// Numeric selected game index is provided.
- if (selectedGameIndex && !isNaN(selectedGameIndex)) {
- if (selectedGameIndex <= 0 || selectedGameIndex > schedule[year].length) {
+ if (!isNaN(maybeValidWeek)) {
+ if (maybeValidWeek <= 0 || maybeValidWeek > schedule[year].length) {
// If the selected game index is invalid for this year, use the default selected game index.
return DEFAULT_SELECTED_GAME_INDEX;
} else {
// Otherwise, subtract one from the selected game index in the URL since they are 1-based, not
// 0-based.
- return selectedGameIndex - 1;
+ return maybeValidWeek - 1;
}
}
diff --git a/website/src/screens/ExplorablesScreen/index.jsx b/website/src/screens/ExplorablesScreen/index.jsx
index 484cb08d..acfdbe68 100644
--- a/website/src/screens/ExplorablesScreen/index.jsx
+++ b/website/src/screens/ExplorablesScreen/index.jsx
@@ -1,24 +1,16 @@
import React from 'react';
-import {Route, Switch, useRouteMatch} from 'react-router-dom';
+import {Route, Routes} from 'react-router-dom';
import {Explorables} from '../../components/explorables/Explorables';
import {ExplorablesS1E1} from '../../components/explorables/season1/episode1';
import {ExplorablesS1E2} from '../../components/explorables/season1/episode2';
export const ExplorablesScreen = () => {
- const {path} = useRouteMatch();
-
return (
-
-
-
-
-
-
-
-
-
-
-
+
+ } />
+ } />
+ } />
+
);
};
diff --git a/website/src/screens/FootballScheduleScreen/index.tsx b/website/src/screens/FootballScheduleScreen/index.tsx
index 4023b6fe..ff18191a 100644
--- a/website/src/screens/FootballScheduleScreen/index.tsx
+++ b/website/src/screens/FootballScheduleScreen/index.tsx
@@ -1,13 +1,13 @@
import _ from 'lodash';
import React, {useMemo, useState} from 'react';
import Media from 'react-media';
-import {Route, Switch, useLocation} from 'react-router-dom';
+import {useParams} from 'react-router-dom';
import {Game} from '../../components/Game';
import {GameSummary} from '../../components/gameSummary/GameSummary';
import {NavMenu} from '../../components/NavMenu';
import {LATEST_YEAR} from '../../lib/constants';
-import {getSelectedGameIndexFromUrl, getYearFromUrl} from '../../lib/urls';
+import {getSelectedGameIndexFromUrlParam, getSelectedSeasonFromUrlParam} from '../../lib/urls';
import {FullSchedule} from '../../models';
import scheduleJson from '../../resources/schedule.json';
import {
@@ -24,15 +24,23 @@ import {
const schedule = scheduleJson as FullSchedule;
export const FootballScheduleScreen: React.FC = () => {
- const location = useLocation();
+ const params = useParams<{
+ readonly selectedYear?: string;
+ readonly selectedGameIndex?: string;
+ }>();
const [isNavMenuOpen, setIsNavMenuOpen] = useState(false);
- // Initialize the source and target page titles from the URL.
- const selectedSeason = useMemo(() => getYearFromUrl(location.pathname), [location]);
- const selectedGameIndex = useMemo(
- () => getSelectedGameIndexFromUrl(location.pathname),
- [location]
- );
+ // Get the selected season and game index from the URL.
+ const {selectedSeason, selectedGameIndex} = useMemo(() => {
+ const selectedSeasonInner = getSelectedSeasonFromUrlParam(params.selectedYear);
+ return {
+ selectedSeason: selectedSeasonInner,
+ selectedGameIndex: getSelectedGameIndexFromUrlParam(
+ selectedSeasonInner,
+ params.selectedGameIndex
+ ),
+ };
+ }, [params.selectedYear, params.selectedGameIndex]);
const gamesContent = _.map(schedule[selectedSeason], (game, index) => {
return (
@@ -84,19 +92,16 @@ export const FootballScheduleScreen: React.FC = () => {
- {(matches) =>
- matches ? (
-
-
-
-
-
- {gamesContent}
-
-
+ {(isSmallerScreen) =>
+ isSmallerScreen ? (
+ params.selectedGameIndex ? (
+
+ ) : (
+ {gamesContent}
+ )
) : (
<>
{gamesContent}