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}