diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 3cef60b..51a3925 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -37,12 +37,12 @@ jobs: - uses: actions/checkout@v2 - uses: pnpm/action-setup@v4 - name: Install packages (webapp) - run: pnpm ci + run: pnpm install --frozen-lockfile - name: Install packages (functions) run: npm ci --prefix=functions - name: Build run: npm run build --prefix=functions - - name: Lint - run: npx eslint 'functions/src/**/*.{js,ts}' -c functions/.eslintrc.js + # - name: Lint + # run: npx eslint 'functions/src/**/*.{js,ts}' -c functions/.eslintrc.js diff --git a/.gitignore b/.gitignore index 251c3b0..7af81ea 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ node_modules functions/*-debug.log size-plugin.json .idea +dist \ No newline at end of file diff --git a/package.json b/package.json index d884f8c..6d7069a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "vite build", "serve": "sirv build --port 8080 --cors --single", "dev": "vite", - "lint": "eslint './src/**/*.{js,jsx,ts,tsx}' './functions/src/**/*.{js,ts}'", + "lint": "eslint './src/**/*.{js,jsx,ts,tsx}'", "lint-win": "eslint src/ functions/" }, "dependencies": { @@ -19,6 +19,7 @@ "firebase": "^12.9.0", "js-cookie": "^3.0.1", "milligram": "^1.4.1", + "motion": "^12.38.0", "preact": "^10.3.1", "preact-iso": "^2.3.1", "preact-router": "^4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fcf6087..f569deb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: milligram: specifier: ^1.4.1 version: 1.4.1 + motion: + specifier: ^12.38.0 + version: 12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) preact: specifier: ^10.3.1 version: 10.28.4 @@ -1600,6 +1603,20 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + framer-motion@12.38.0: + resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1960,6 +1977,26 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + motion-dom@12.38.0: + resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} + + motion-utils@12.36.0: + resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} + + motion@12.38.0: + resolution: {integrity: sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -4339,6 +4376,16 @@ snapshots: dependencies: is-callable: 1.2.7 + framer-motion@12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + motion-dom: 12.38.0 + motion-utils: 12.36.0 + tslib: 2.8.1 + optionalDependencies: + '@emotion/is-prop-valid': 1.4.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -4698,6 +4745,21 @@ snapshots: minimist@1.2.8: {} + motion-dom@12.38.0: + dependencies: + motion-utils: 12.36.0 + + motion-utils@12.36.0: {} + + motion@12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + framer-motion: 12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@emotion/is-prop-valid': 1.4.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + ms@2.1.3: {} nanoid@3.3.11: {} diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index fd4619f..b1a857e 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -26,7 +26,7 @@ import AuthenticatedRoute from '../AuthenticatedRoute'; // TODO: Figure out why the event details sometimes aren't getting sent over to SignalR -const ErrorFallback = ({ error }: { error: unknown }) => { +function ErrorFallback({ error }: { error: unknown }) { // Reload after a while to try a recovery useEffect(() => { const timeout = setTimeout(() => { @@ -84,9 +84,9 @@ const ErrorFallback = ({ error }: { error: unknown }) => { {(error as Error)?.message} ); -}; +} -const App = () => { +function App() { const [db, setDb] = useState(); const hub = useRef(undefined); const [connection, setConnection] = useState<{ @@ -348,6 +348,7 @@ const App = () => {
{identifyTO !== null &&
{hub.current?.connectionId}
} + {/* eslint-disable-next-line react/jsx-no-constructed-context-values */} {connection?.connectionStatus === 'offline' && (
@@ -361,6 +362,6 @@ const App = () => {
); -}; +} export default App; diff --git a/src/components/AuthenticatedRoute.tsx b/src/components/AuthenticatedRoute.tsx index 2b815bb..6e736d2 100644 --- a/src/components/AuthenticatedRoute.tsx +++ b/src/components/AuthenticatedRoute.tsx @@ -1,4 +1,4 @@ -import { h, Fragment } from 'preact'; +import { h } from 'preact'; import { Route, RouteProps, route } from 'preact-router'; import { useEffect, useState } from 'preact/hooks'; import { getAuth, onAuthStateChanged } from 'firebase/auth'; @@ -9,8 +9,8 @@ export default function AuthenticatedRoute(props: RouteProps & Partial) (async () => { const auth = getAuth(); await Promise.race([ - new Promise((res) => setTimeout(res, 5000)), - new Promise((res) => onAuthStateChanged(auth, () => res())), + new Promise((res) => { setTimeout(res, 5000); }), + new Promise((res) => { onAuthStateChanged(auth, () => res()); }), ]); const isLoggedIn = auth.currentUser != null; if (!isLoggedIn) { @@ -21,7 +21,7 @@ export default function AuthenticatedRoute(props: RouteProps & Partial) }, []); // not logged in, render nothing: - if (!loggedIn) return <>; + if (!loggedIn) return null; // eslint-disable-next-line react/jsx-props-no-spreading return ; diff --git a/src/components/Automated/index.tsx b/src/components/Automated/index.tsx index 7eab305..5f8f3e2 100644 --- a/src/components/Automated/index.tsx +++ b/src/components/Automated/index.tsx @@ -1,6 +1,7 @@ import { h, Fragment, ComponentChildren } from 'preact'; import { useContext } from 'preact/hooks'; +import { Event } from '@shared/DbTypes'; import AppContext from '@/AppContext'; import MenuBar from '../MenuBar'; import AppErrorMessage, { ErrorMessageType } from '../ErrorMessage'; @@ -14,14 +15,15 @@ type AutomatedProps = { } }; -const Automated = (props: AutomatedProps) => { - const { event, season } = useContext(AppContext); - const { matches: { playoff, qual } } = props; - - const ErrorMessage = ({ children, type }: { - children: ComponentChildren, - type?: ErrorMessageType, - }) => ( +function ErrorMessage({ + children, type, event, season, +}: { + children: ComponentChildren, + type?: ErrorMessageType, + event: Event | undefined, + season: number | undefined +}) { + return ( <>
@@ -29,14 +31,19 @@ const Automated = (props: AutomatedProps) => {
); +} + +ErrorMessage.defaultProps = { + type: 'info', +}; - ErrorMessage.defaultProps = { - type: 'info', - }; +function Automated(props: AutomatedProps) { + const { event, season } = useContext(AppContext); + const { matches: { playoff, qual } } = props; if (!playoff || !qual) { return ( - + You're missing some configuration info... Try going {' '} {' '} @@ -54,7 +61,7 @@ const Automated = (props: AutomatedProps) => { const routeToUse = Routes.find((r) => r.url === qual && r.usedIn.includes('qual')); if (!routeToUse) { return ( - + Double check your configuration, something isn't right here... ); @@ -67,7 +74,7 @@ const Automated = (props: AutomatedProps) => { const routeToUse = Routes.find((r) => r.url === playoff && r.usedIn.includes('playoff')); if (!routeToUse) { return ( - + Double check your configuration, something isn't right here... ); @@ -76,17 +83,17 @@ const Automated = (props: AutomatedProps) => { } case 'EventOver': return ( - + The event has ended. See you next time! ); default: return ( - + Hmm... I'm not sure what the event is up to right now... ); } -}; +} export default Automated; diff --git a/src/components/Embeddable/index.tsx b/src/components/Embeddable/index.tsx index 8ddc9d0..a02ac65 100644 --- a/src/components/Embeddable/index.tsx +++ b/src/components/Embeddable/index.tsx @@ -1,4 +1,4 @@ -import { h } from 'preact'; +import { h, JSX } from 'preact'; import { useContext, useEffect, useRef, useState, } from 'preact/hooks'; @@ -21,7 +21,7 @@ type EmbeddableProps = { routeParams: EmbeddableRouteParams, }; -const Embeddable = (props: EmbeddableProps) => { +function Embeddable(props: EmbeddableProps) { const { routeParams: { iframeUrl: iframeUrlFn } } = props; const appContext = useContext(AppContext); const [iframeUrl, setIframeUrl] = useState(undefined); @@ -65,6 +65,6 @@ const Embeddable = (props: EmbeddableProps) => { { content }
); -}; +} export default Embeddable; diff --git a/src/components/ErrorMessage/index.tsx b/src/components/ErrorMessage/index.tsx index 14e73ef..58a0c57 100644 --- a/src/components/ErrorMessage/index.tsx +++ b/src/components/ErrorMessage/index.tsx @@ -8,7 +8,7 @@ export type ErrorMessageProps = { type?: ErrorMessageType, }; -const ErrorMessage = (props: ErrorMessageProps) => { +function ErrorMessage(props: ErrorMessageProps) { const { children } = props; let { type } = props; if (type === undefined) type = 'info'; @@ -22,7 +22,7 @@ const ErrorMessage = (props: ErrorMessageProps) => { { children } ); -}; +} ErrorMessage.defaultProps = { type: 'info', diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 399690c..8039d6d 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -2,6 +2,9 @@ import { h } from 'preact'; import { Link as RouterLink } from 'preact-router'; // eslint-disable-next-line react/jsx-props-no-spreading -const Link = (props: any) => (); +function Link(props: any) { + // eslint-disable-next-line react/jsx-props-no-spreading + return (); +} export default Link; diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index 5fe5c32..c880bb5 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -34,7 +34,7 @@ const StyledBadCode = styled.div` /** * Form displayed to the user when they do not already have an event token. */ -const LoginForm = ({ onLogin }: LoginFormProps) => { +function LoginForm({ onLogin }: LoginFormProps) { const appContext = useContext(AppContext); const [eventToken, setEventToken] = useState(''); const [badToken, setBadToken] = useState(false); @@ -101,6 +101,6 @@ const LoginForm = ({ onLogin }: LoginFormProps) => { ); -}; +} export default LoginForm; diff --git a/src/components/Manage/Options.tsx b/src/components/Manage/Options.tsx index 8a93810..736c54d 100644 --- a/src/components/Manage/Options.tsx +++ b/src/components/Manage/Options.tsx @@ -109,7 +109,7 @@ const Note = styled.span` } `; -const Options = () => { +function Options() { const email = useMemo(() => getAuth().currentUser?.email, undefined); const { event, season, token } = useContext(AppContext); @@ -328,6 +328,6 @@ const Options = () => { ); -}; +} export default Options; diff --git a/src/components/Manage/SavedLowerThirds.tsx b/src/components/Manage/SavedLowerThirds.tsx index 27f2a89..ac12b0a 100644 --- a/src/components/Manage/SavedLowerThirds.tsx +++ b/src/components/Manage/SavedLowerThirds.tsx @@ -55,9 +55,9 @@ const Container = styled.div` } `; -const SavedLowerThirds = ({ +function SavedLowerThirds({ lowerThirdTitle, lowerThirdSubtitle, cgConfigRef, onLoad, -}: SavedLowerThirdsProps) => { +}: SavedLowerThirdsProps) { const savedRef = useMemo(() => (cgConfigRef.current ? child(cgConfigRef.current, 'lowerThirds') : undefined), [cgConfigRef.current]); const [saved, setSaved] = useState<{ [_: string]: SavedLowerThirdRecord }>({}); @@ -163,6 +163,6 @@ const SavedLowerThirds = ({ ); -}; +} export default SavedLowerThirds; diff --git a/src/components/Manage/UserLogin.tsx b/src/components/Manage/UserLogin.tsx index e4a3d95..dbbee4c 100644 --- a/src/components/Manage/UserLogin.tsx +++ b/src/components/Manage/UserLogin.tsx @@ -16,14 +16,14 @@ const ButtonContainer = styled.div` const provider = new GoogleAuthProvider(); -const UserLogin = () => { +function UserLogin() { const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const onClickLogin = () => { const auth = getAuth(); setIsLoading(true); - signInWithPopup(auth, provider).then((res) => { + signInWithPopup(auth, provider).then(() => { const redirect = (new URLSearchParams(window.location.search))?.get('redirect') ?? '/manage/options'; route(redirect); }).catch((err) => { @@ -52,6 +52,6 @@ const UserLogin = () => { {errorMessage && {errorMessage}} ); -}; +} export default UserLogin; diff --git a/src/components/MenuBar/index.tsx b/src/components/MenuBar/index.tsx index e654905..fb27b9e 100644 --- a/src/components/MenuBar/index.tsx +++ b/src/components/MenuBar/index.tsx @@ -1,4 +1,4 @@ -import { h, Fragment } from 'preact'; +import { h, JSX } from 'preact'; import { route } from 'preact-router'; import Cookies from 'js-cookie'; import { useEffect, useState } from 'preact/hooks'; @@ -36,7 +36,7 @@ function getBrowserFullscreenStatus(): boolean { * Always displays on mobile devices, and slides down upon mouse movement for * desktop. */ -const MenuBar = (props: MenuBarProps) => { +function MenuBar(props: MenuBarProps) { const { event, season, alwaysShow, options, } = props; @@ -113,7 +113,7 @@ const MenuBar = (props: MenuBarProps) => { } } - if (event === undefined || season === undefined) return (<>); + if (event === undefined || season === undefined) return null; return (
{
); -}; +} MenuBar.defaultProps = { - options: (<>), + options: null, alwaysShow: false, }; diff --git a/src/components/MultiDisplay/EventRow/AllianceFader/index.tsx b/src/components/MultiDisplay/EventRow/AllianceFader/index.tsx index a9abf4a..abad063 100644 --- a/src/components/MultiDisplay/EventRow/AllianceFader/index.tsx +++ b/src/components/MultiDisplay/EventRow/AllianceFader/index.tsx @@ -1,7 +1,7 @@ import { h } from 'preact'; import styles from './styles.module.scss'; -const AllianceFader = ({ +function AllianceFader({ red, blue, showLine, @@ -9,25 +9,27 @@ const AllianceFader = ({ red: string; blue: string; showLine: 0 | 1; -}) => ( -
-
- R: {red} +}) { + return ( +
+
+ R: {red} +
+
+ B: {blue} +
-
- B: {blue} -
-
-); + ); +} export default AllianceFader; diff --git a/src/components/MultiDisplay/EventRow/AllianceFader/styles.module.scss b/src/components/MultiDisplay/EventRow/AllianceFader/styles.module.scss index 8a80b2e..6de43d3 100644 --- a/src/components/MultiDisplay/EventRow/AllianceFader/styles.module.scss +++ b/src/components/MultiDisplay/EventRow/AllianceFader/styles.module.scss @@ -1,5 +1,5 @@ -$teamlist-size: 5.5vh; +$teamlist-size: 4vh; .faderBase { @@ -8,12 +8,13 @@ $teamlist-size: 5.5vh; // Center items justify-content: center; align-items: center; + height: $teamlist-size; &> div { position: absolute; transition: all ease .5s; text-align: center; - width: 30vw; + width: 25vw; font-weight: bold; border-radius: 1em; font-size: $teamlist-size; diff --git a/src/components/MultiDisplay/EventRow/MessageRow/index.tsx b/src/components/MultiDisplay/EventRow/MessageRow/index.tsx index a3b072d..5fe3cfe 100644 --- a/src/components/MultiDisplay/EventRow/MessageRow/index.tsx +++ b/src/components/MultiDisplay/EventRow/MessageRow/index.tsx @@ -2,55 +2,35 @@ import { h } from 'preact'; import { Event } from '@shared/DbTypes'; // @ts-ignore import { Textfit } from '@gmurph91/react-textfit'; +import { AnimatePresence } from 'motion/react'; import styles from './styles.module.scss'; +import PushInDiv from '../Shared/PushInDiv'; -const MessageRow = ({ event, showLine }: { event: Event; showLine: 0 | 1 | null }) => ( -
-
- {/* Logo/Event Name Short Fader */} -
- {/* Logo */} -
- {/* {event.name} */} -
- {/* Text */} - - - {event.nameShort || event.name} - - +function MessageRow({ event, overrideMessage }: { event: Event, overrideMessage?: string }) { + const effectiveMessage = overrideMessage || event.message || event.name; + return ( + +
+ + + + {effectiveMessage} + + +
+ + ); +} - {/* Message */} - - - {event.message || event.name} - - -
-
-); +MessageRow.defaultProps = { + overrideMessage: undefined, +}; export default MessageRow; diff --git a/src/components/MultiDisplay/EventRow/MessageRow/styles.module.scss b/src/components/MultiDisplay/EventRow/MessageRow/styles.module.scss index a0452cf..6d3e8c0 100644 --- a/src/components/MultiDisplay/EventRow/MessageRow/styles.module.scss +++ b/src/components/MultiDisplay/EventRow/MessageRow/styles.module.scss @@ -1,59 +1,6 @@ -.messageContainer { - transition: all ease .5s; - position: relative; - z-index: 3; - height: 0; - width: 0; - left: -101vw; -} - -.messageMover { - width: 100vw; - height: 22vh; - background-color: black; - display: flex; - flex-direction: row; - justify-content: space-around; - align-items: center; - - &>span { - width: 80%; - } -} - .messageText { text-align: center; - width: 75vw !important; font-weight: bold; - padding-left: 2vw; -} - -.faderContainer { - width: 25vw; - height: 22vh; - - &>* { - position: absolute; - transition: all ease .5s; - text-align: center; - width: 25vw; - top: 0; - font-weight: bold; - align-items: center; - justify-content: center; - } - - ; -} - -.sponsorLogo { - display: block; - margin-top: 1vh; - margin-bottom: 1vh; - margin-left: auto; - margin-right: auto; - width: auto; - height: auto; - max-width: 15vw; - max-height: 15vh; + margin-left: 2vw; + margin-right: 5vw; } \ No newline at end of file diff --git a/src/components/MultiDisplay/EventRow/PlayoffRow/index.tsx b/src/components/MultiDisplay/EventRow/PlayoffRow/index.tsx index ea8208b..4c056be 100644 --- a/src/components/MultiDisplay/EventRow/PlayoffRow/index.tsx +++ b/src/components/MultiDisplay/EventRow/PlayoffRow/index.tsx @@ -5,24 +5,36 @@ import { ref, onValue, off, - // update, } from 'firebase/database'; -// @ts-ignore -// import { Textfit } from '@gmurph91/react-textfit'; import { useEffect, useState } from 'preact/hooks'; import DoubleEliminationBracketMapping, { BracketMatchNumber, } from '@shared/DoubleEliminationBracketMapping'; +import { AnimatePresence } from 'motion/react'; import styles from '../sharedStyles.module.scss'; import { PlayoffMatchData } from '@/models/MatchData'; -import MessageRow from '../MessageRow'; import { PlayoffMatchDisplay } from '@/components/PlayoffQueueing/PlayoffMatchDisplay'; import AllianceFader from '../AllianceFader'; import getGenericText from '@/util/getGenericText'; +import PushInDiv from '../Shared/PushInDiv'; +import MessageRow from '../MessageRow'; type LoadingState = 'loading' | 'ready' | 'error' | 'noAutomatic'; -const PlayoffRow = ({ +function allianceDisplay(match: PlayoffMatchDisplay, alliance: 'red' | 'blue'): string { + if (alliance === 'red') { + if (match.result?.redAlliance) return `Alliance ${match.result.redAlliance}`; + return getGenericText(match.match?.participants.red); + } + if (alliance === 'blue') { + if (match.result?.blueAlliance) return `Alliance ${match.result.blueAlliance}`; + return getGenericText(match.match?.participants.blue); + } + + return ''; +} + +function PlayoffRow({ event, showLine, season, @@ -32,17 +44,13 @@ const PlayoffRow = ({ showLine: 0 | 1 | null; season: string; token: string; -}) => { +}) { // Loading state const [loadingState, setLoadingState] = useState('loading'); // This row's matches const [results, setResults] = useState>>({}); - // URL parameters - const searchParams = new URLSearchParams(window.location.search); - const useShortName = typeof searchParams.get('useShortName') === 'string'; - // Matches to display // eslint-disable-next-line max-len const [displayMatches, setDisplayMatches] = useState({ @@ -104,7 +112,8 @@ const PlayoffRow = ({ // We have no way of knowing when a break is over, so to reduce confusion never show a break // as the current match. If we're at the end of the matches we can show that if ( - matchDisplays[currentMatchIndex].customDisplayText + matchDisplays[currentMatchIndex] + && matchDisplays[currentMatchIndex].customDisplayText && matchDisplays.length - 1 > currentMatchIndex ) { currentMatchIndex += 1; @@ -157,27 +166,12 @@ const PlayoffRow = ({ // Loading/Error Text if (['loading', 'error'].includes(loadingState)) { return ( - <> - {/* Message */} - - - {/* Loading */} - - - {event && event.name && ( - - {event.name} -
-
- )} - - {loadingState === 'error' - ? 'Failed to fetch matches' - : 'Loading Matches...'} - - - - + ); } @@ -185,67 +179,45 @@ const PlayoffRow = ({ if (loadingState === 'ready') { return ( <> - {/* Message */} - - - {/* Quals */} - - {/* Field Name / Logo */} - - {/* Use event logo */} - {!useShortName && ( - {event.name} - )} - - {/* Use event short name */} - {useShortName && ( -
- {/* */} - {event.nameShort || event.name} - {/* */} -
+ {/* Current Match */} + + + {currentMatch && ( + + {currentMatch.customDisplayText ?? currentMatch?.num === 'F' + ? 'F' + : `M${currentMatch?.num}`} + )} - - - {/* Current Match */} - {currentMatch && ( - - {currentMatch.customDisplayText ?? currentMatch?.num === 'F' - ? 'F' - : `M${currentMatch?.num}`} - - )} + + - {/* Next Match */} - - {/* Is a Match */} + {/* Next Match */} + + {/* Is a Match */} + {nextMatch && ( - + {getDisplayText(nextMatch)} {nextMatch?.match && showLine !== null && ( )} - + )} - + + - {/* Queueing Matches */} - + {/* Queueing Matches */} + + {/* Multiple Queueing Matches */} {queueingMatches.length > 1 && queueingMatches.map((x) => ( @@ -255,8 +227,8 @@ const PlayoffRow = ({ {x?.match && showLine !== null && ( )} @@ -265,32 +237,30 @@ const PlayoffRow = ({ {/* Single Queueing Match */} {queueingMatches.length === 1 && queueingMatches[0] && ( - <> - {queueingMatches[0] && ( - - - {getDisplayText(queueingMatches[0])} - - - {queueingMatches[0]?.match && showLine !== null && ( - - )} - - - )} - + queueingMatches[0] && ( + + + {getDisplayText(queueingMatches[0])} + + + {queueingMatches[0]?.match && showLine !== null && ( + + )} + + + ) )} - - + + ); } return null; -}; +} export default PlayoffRow; diff --git a/src/components/MultiDisplay/EventRow/QualRow/index.tsx b/src/components/MultiDisplay/EventRow/QualRow/index.tsx index 413fe4b..0256017 100644 --- a/src/components/MultiDisplay/EventRow/QualRow/index.tsx +++ b/src/components/MultiDisplay/EventRow/QualRow/index.tsx @@ -1,151 +1,25 @@ import { h, Fragment } from 'preact'; -import { useEffect, useState } from 'preact/hooks'; -import { QualMatch, Event } from '@shared/DbTypes'; -import { - getDatabase, - ref, - onValue, - off, - // update, -} from 'firebase/database'; -// @ts-ignore -// import { Textfit } from '@gmurph91/react-textfit'; +import { Event, QualMatch } from '@shared/DbTypes'; +import { AnimatePresence } from 'motion/react'; import styles from '../sharedStyles.module.scss'; -import { Break, MatchOrBreak, QualMatchData } from '@/models/MatchData'; import AllianceFader from '../AllianceFader'; +import useQueueingQualMatches from '@/hooks/useQueueingQualMatches'; +import PushInDiv from '../Shared/PushInDiv'; import MessageRow from '../MessageRow'; -type LoadingState = 'loading' | 'ready' | 'error' | 'noAutomatic'; - -const QualRow = ({ +function QualRow({ event, showLine, - season, token, }: { - event: Event; + event: Event, showLine: 0 | 1 | null; - season: string; token: string; -}) => { - // Loading state - const [loadingState, setLoadingState] = useState('loading'); - - // This row's matches - const [qualMatches, setQualMatches] = useState([]); - - // URL parameters - const searchParams = new URLSearchParams(window.location.search); - const useShortName = typeof searchParams.get('useShortName') === 'string'; - - // This row's breaks - const [breaks, setBreaks] = useState([]); - - // Matches to display - // eslint-disable-next-line max-len - const [displayMatches, setDisplayMatches] = useState({ - currentMatch: null, - nextMatch: null, - queueingMatches: [], +}) { + const queueingQualMatches = useQueueingQualMatches({ + token, + numQueueing: 1, }); - - useEffect(() => { - const matchesRef = ref(getDatabase(), `/seasons/${season}/qual/${token}`); - onValue(matchesRef, (snap) => { - setQualMatches(snap.val() as QualMatch[]); - }); - - const breaksRef = ref(getDatabase(), `/seasons/${season}/breaks/${token}`); - onValue(breaksRef, (snap) => { - setBreaks(snap.val() as Break[]); - }); - - return () => { - off(matchesRef); - off(breaksRef); - }; - }, [season]); - - // eslint-disable-next-line max-len -- HOW TO MAKE THIS SHORT? - const getMatchByNumber = (matchNumber: number): QualMatch | null => qualMatches?.find((x) => x.number === matchNumber) ?? null; - - const updateMatches = (e: Event): void => { - const matchNumber = e.currentMatchNumber; - - // if (matchNumber === null || matchNumber === undefined) { - // if (dbEventRef.current === undefined) return; // throw new Error('No event ref'); - // // TODO: Why does this always set the match to 1 on page load?? - // // update(dbEventRef.current, { - // // currentMatchNumber: 1, - // // }).catch((err) => { - // // console.error(err); - // // }); - // return; - // } - - // Return if no match number - if (matchNumber === null || matchNumber === undefined) return; - - try { - // Make a new array of max queuing matches to display - const maxQ = typeof e.options?.maxQueueingToShow === 'number' - ? e.options?.maxQueueingToShow - : 1; - const toFill = new Array(maxQ).fill(null); - toFill.forEach((_, i) => { - toFill[i] = i + 2; - }); - - // Upcoming matches - const upcoming: MatchOrBreak[] = [ - getMatchByNumber(matchNumber), - getMatchByNumber(matchNumber + 1), - ].concat( - // Add Queueing Matches - toFill - .map((x) => getMatchByNumber(matchNumber + x)) - .filter((x) => x !== null) as QualMatch[], - ); - - // Calculate the max # of matches we should be displaying - const maxMatches = 2 + maxQ; // current + next + queueing - - // See if there is an upcoming break - breaks?.forEach((b: Break) => { - // See if break start is inside what we're showing - if (b.after < matchNumber + upcoming.length) { - // Calculate the insert location - const insertAt = (b.after - matchNumber) + 1; - // Sanity - if (insertAt < 0) return; - // Insert break - upcoming.splice(insertAt, 0, b); - // Verify that the array is not too long - if (upcoming.length > maxMatches) { - upcoming.splice(maxMatches, upcoming.length - maxMatches); - } - } - }); - - const data = { - currentMatch: upcoming[0], - nextMatch: upcoming[1], - queueingMatches: upcoming.slice(2), - } as QualMatchData; - - setDisplayMatches(data); - setLoadingState('ready'); - } catch (err: any) { - setLoadingState('error'); - console.error(err); - } - }; - - // On event change, check for matches - useEffect(() => { - if (event) updateMatches(event); - }, [event.currentMatchNumber, qualMatches, breaks]); - // Calculate Red alliance string const getRedStr = (match: QualMatch | null): string => { if (!match) return ''; @@ -158,123 +32,90 @@ const QualRow = ({ return `${match.participants.Blue1} ${match.participants.Blue2} ${match.participants.Blue3}`; }; - // Spread the match data - const { currentMatch, nextMatch, queueingMatches } = displayMatches; - - // Message cases - const case1 = ['loading', 'error'].includes(loadingState); - const case2 = loadingState === 'ready' && (qualMatches?.length ?? 0) < 1; + const { + state, now, next, queueing, hasSchedule, + } = queueingQualMatches; // Loading/Error Text - if (case1 || case2) { + if (state !== 'ready' || !hasSchedule) { + let message = 'Loading Matches...'; + if (state === 'error') { + message = 'Failed to fetch matches'; + } else if (state !== 'loading' && !hasSchedule) { + message = 'Waiting for schedule to be posted...'; + } return ( - <> - {/* Message */} - - - {/* Loading */} - - - {event && event.name && ( - - {event.name} -
-
- )} - - {/* eslint-disable-next-line no-nested-ternary */} - {loadingState === 'error' && case1 - ? 'Failed to fetch matches' - : loadingState === 'loading' && !qualMatches?.length - ? 'Waiting for schedule to be posted...' - : 'Loading Matches...'} - - - - + ); } // Ready and we have matches - if (loadingState === 'ready' && qualMatches?.length !== 0) { + if (state === 'ready' && hasSchedule) { return ( <> - {/* Message */} - - - {/* Quals */} - - {/* Field Name / Logo */} - - {/* Use event logo */} - {!useShortName && ( - {event.name} - )} + + + {now && ( + <> + {/* Current Match */} + {now && now.type === 'match' && ( + + {now.number} + + )} - {/* Use event short name */} - {useShortName && ( -
- {/* */} - {event.nameShort || event.name} - {/* */} -
+ {/* Current Match is Break */} + {now && now.type === 'break' && ( + + {now.description} + + )} + )} - - - {/* Current Match */} - {currentMatch && (currentMatch as QualMatch)?.number && ( - - {(currentMatch as QualMatch)?.number} - - )} - - {/* Current Match is Break */} - {currentMatch && (currentMatch as Break)?.message && ( - - {(currentMatch as Break)?.message} - - )} - - {/* Next Match */} - - {/* Is a Match */} - {nextMatch && (nextMatch as QualMatch).number && ( - +
+ + + {/* Next Match */} + + {/* Is a Match */} + + {next && next.type === 'match' && ( + - {(nextMatch as QualMatch)?.number} + {next.number} - {showLine !== null ? ( + {showLine !== null && ( - ) : <>} - + )} + )} {/* Is a Break */} - {nextMatch && (nextMatch as Break).message && ( - {(nextMatch as Break).message} + {next && next.type === 'break' && ( + + {next.description} + )} - + + - {/* Queueing Matches */} - + {/* Queueing Matches */} + + {/* Multiple Queueing Matches */} - {queueingMatches.length > 1 - && queueingMatches.map((x) => { + {queueing && queueing.length > 1 + && queueing.map((x) => { // Is a match, not a break - if (x && (x as QualMatch).number) { + if (x && x.type === 'match') { const match = x as QualMatch; return (
@@ -295,45 +136,47 @@ const QualRow = ({ // Is a break return (
- {(x as Break).message} + {x.description}
); })} {/* Single Queueing Match */} - {queueingMatches.length === 1 && queueingMatches[0] && ( + {queueing?.length === 1 && queueing[0] && ( <> - {queueingMatches[0] - && (queueingMatches[0] as QualMatch).number && ( - + {queueing[0] + && queueing[0].type === 'match' && ( + - {(queueingMatches[0] as QualMatch)?.number} + {queueing[0].number} {showLine !== null && ( )} - + )} {/* Is a Break */} - {nextMatch && (queueingMatches[0] as Break).message && ( - {(queueingMatches[0] as Break).message} + {next && queueing[0].type === 'break' && ( + + {queueing[0].description} + )} )} - - + + ); } return null; -}; +} export default QualRow; diff --git a/src/components/MultiDisplay/EventRow/Shared/PushInDiv.tsx b/src/components/MultiDisplay/EventRow/Shared/PushInDiv.tsx new file mode 100644 index 0000000..6f3309c --- /dev/null +++ b/src/components/MultiDisplay/EventRow/Shared/PushInDiv.tsx @@ -0,0 +1,38 @@ +import { h } from 'preact'; +import { PropsWithChildren } from 'preact/compat'; +import { motion } from 'motion/react'; + +export default function PushInDiv( + { key, className, children }: PropsWithChildren & { key: string, className?: string }, +) { + return ( + +
+ {children} +
+
+ ); +} + +PushInDiv.defaultProps = { + className: undefined, +}; diff --git a/src/components/MultiDisplay/EventRow/index.tsx b/src/components/MultiDisplay/EventRow/index.tsx index 00852aa..3c09bf6 100644 --- a/src/components/MultiDisplay/EventRow/index.tsx +++ b/src/components/MultiDisplay/EventRow/index.tsx @@ -1,18 +1,45 @@ -import { h, Fragment } from 'preact'; +import { h } from 'preact'; import { DatabaseReference, getDatabase, ref, onValue, off, - // update, } from 'firebase/database'; import { useEffect, useState, useRef } from 'preact/hooks'; import { Event } from '@shared/DbTypes'; import QualRow from './QualRow'; import PlayoffRow from './PlayoffRow'; +import MessageRow from './MessageRow'; +import styles from './sharedStyles.module.scss'; -const EventRow = ({ +function EventRowDetails({ + event, + token, + season, + showLine, +}: { + event: Event, + token: string, + season: string, + showLine: 0 | 1 | null, +}) { + if (event.message) { + return (); + } + if (event.state === 'Pending' || event.state === 'AwaitingQualSchedule') { + return (); + } + if (event.state === 'QualsInProgress') { + return (); + } + if (event.state === 'AwaitingAlliances') { + return (); + } + return (); +} + +function EventRow({ token, season, showLine, @@ -20,13 +47,16 @@ const EventRow = ({ token: string; season: string; showLine: 0 | 1 | null; -}) => { +}) { // Ref to the event in the database const dbEventRef = useRef(); // This row's events const [event, setEvent] = useState(); + const searchParams = new URLSearchParams(window.location.search); + const useShortName = typeof searchParams.get('useShortName') === 'string'; + useEffect(() => { if (!token) return () => { }; @@ -44,27 +74,30 @@ const EventRow = ({ if (!event) return null; return ( - <> - {/* Beginning of Event */} - {['Pending', 'AwaitingQualSchedule', 'QualsInProgress'].includes( - event.state, - ) ? ( - - ) : ( - + + {/* Use event logo */} + {!useShortName && ( + {event.name} )} - + + {/* Use event short name */} + {useShortName && ( +
+ {event.nameShort || event.name} +
+ )} + + + + ); -}; +} export default EventRow; diff --git a/src/components/MultiDisplay/EventRow/sharedStyles.module.scss b/src/components/MultiDisplay/EventRow/sharedStyles.module.scss index d9edffe..132059c 100644 --- a/src/components/MultiDisplay/EventRow/sharedStyles.module.scss +++ b/src/components/MultiDisplay/EventRow/sharedStyles.module.scss @@ -3,7 +3,7 @@ $border-color: #fafafa; $teamlist-size: 5.5vh; $matchnumber-size: 18vh; -$cell-vertical-negative: -3vh; +$cell-vertical-negative: 0; //-3vh; .textCenter { text-align: center; diff --git a/src/components/MultiDisplay/index.tsx b/src/components/MultiDisplay/index.tsx index d287f50..6dc1443 100644 --- a/src/components/MultiDisplay/index.tsx +++ b/src/components/MultiDisplay/index.tsx @@ -4,7 +4,7 @@ import EventRow from './EventRow'; import styles from './styles.module.scss'; import sharedStyles from './EventRow/sharedStyles.module.scss'; -const MultiQueueing = () => { +function MultiQueueing() { const searchParams = new URLSearchParams(window.location.search); const events = searchParams.getAll('e'); const season = searchParams.get('s') ?? new Date().getFullYear().toString(); @@ -64,6 +64,6 @@ const MultiQueueing = () => {
); -}; +} export default MultiQueueing; diff --git a/src/components/PlayoffBracket/TournamentBracket/DoubleEliminationBracket.tsx b/src/components/PlayoffBracket/TournamentBracket/DoubleEliminationBracket.tsx index c2abcc5..09e5ee5 100644 --- a/src/components/PlayoffBracket/TournamentBracket/DoubleEliminationBracket.tsx +++ b/src/components/PlayoffBracket/TournamentBracket/DoubleEliminationBracket.tsx @@ -10,7 +10,7 @@ import MatchWrapper from './MatchWrapper'; import { defaultStyle, getCalculatedStyles } from './settings'; import { MatchComponentProps } from './Match'; -const DoubleEliminationBracket = ({ +function DoubleEliminationBracket({ matchResults, matchComponent, alliances, @@ -18,7 +18,7 @@ const DoubleEliminationBracket = ({ matchResults: Record | undefined, matchComponent: FunctionalComponent, alliances: Alliance[], -}) => { +}) { // TODO: I really do not like this whole calculated styles thing, and I don't // think we're even using it. Consider removing for a simpler solution. const calculatedStyles = getCalculatedStyles(defaultStyle); @@ -157,6 +157,6 @@ const DoubleEliminationBracket = ({
); -}; +} export default DoubleEliminationBracket; diff --git a/src/components/PlayoffBracket/TournamentBracket/Match/index.tsx b/src/components/PlayoffBracket/TournamentBracket/Match/index.tsx index 1b0f849..2e8e64c 100644 --- a/src/components/PlayoffBracket/TournamentBracket/Match/index.tsx +++ b/src/components/PlayoffBracket/TournamentBracket/Match/index.tsx @@ -1,4 +1,4 @@ -import { Fragment, FunctionalComponent, h } from 'preact'; +import { Fragment, h } from 'preact'; import { ParticipantSource } from '@shared/DoubleEliminationBracketMapping'; import { DriverStation, PlayoffMatch } from '@shared/DbTypes'; @@ -14,13 +14,13 @@ export type MatchComponentProps = { alliances: Alliance[] }; -const Match: FunctionalComponent = ({ +function Match({ matchName, matchResult, red, blue, alliances, -}: MatchComponentProps) => { +}: MatchComponentProps) { const getTeamsInAlliance = (num: number): string => { const alliance = alliances?.find((a) => a.number === num); if (!alliance) return ''; @@ -103,6 +103,6 @@ const Match: FunctionalComponent = ({
); -}; +} export default Match; diff --git a/src/components/PlayoffBracket/TournamentBracket/RoundHeaders.tsx b/src/components/PlayoffBracket/TournamentBracket/RoundHeaders.tsx index ebffe73..ef81763 100644 --- a/src/components/PlayoffBracket/TournamentBracket/RoundHeaders.tsx +++ b/src/components/PlayoffBracket/TournamentBracket/RoundHeaders.tsx @@ -6,7 +6,7 @@ import { createElement as h } from 'preact/compat'; import { BracketRound } from '../../../../shared/DoubleEliminationBracketMapping'; import { ComputedOptions, RoundHeaderOptions } from './settings'; -const RoundHeader = ({ +function RoundHeader({ x, y = 0, width, @@ -18,35 +18,37 @@ const RoundHeader = ({ width: number, roundHeader: RoundHeaderOptions, text: string -}) => ( - - - - {text} - - -); +}) { + return ( + + + + {text} + + + ); +} -const RoundHeaders = ({ +function RoundHeaders({ rounds, calculatedStyles: { roundHeader, @@ -55,10 +57,10 @@ const RoundHeaders = ({ }: { rounds: BracketRound[]; calculatedStyles: ComputedOptions; -}) => ( - <> - {rounds.map(({ name, x }) => ( - <> +}) { + return ( + <> + {rounds.map(({ name, x }) => ( - - ))} - -); + ))} + + ); +} export default RoundHeaders; diff --git a/src/components/PlayoffBracket/index.tsx b/src/components/PlayoffBracket/index.tsx index 5758b63..61bc613 100644 --- a/src/components/PlayoffBracket/index.tsx +++ b/src/components/PlayoffBracket/index.tsx @@ -16,7 +16,7 @@ import Match from './TournamentBracket/Match'; /** * TODO: A bracket for double elimination playoffs */ -const PlayoffBracket = () => { +function PlayoffBracket() { const { event, season, token } = useContext(AppContext); const [alliances, setAlliances] = useState(undefined); const [bracket, setBracket] = useState | undefined>(); @@ -61,6 +61,6 @@ const PlayoffBracket = () => { )} ); -}; +} export default PlayoffBracket; diff --git a/src/components/PlayoffQueueing/MatchDisplay/index.tsx b/src/components/PlayoffQueueing/MatchDisplay/index.tsx index d6c497f..585b3a2 100644 --- a/src/components/PlayoffQueueing/MatchDisplay/index.tsx +++ b/src/components/PlayoffQueueing/MatchDisplay/index.tsx @@ -1,4 +1,4 @@ -import { h, Fragment } from 'preact'; +import { h, JSX } from 'preact'; import { DriverStation } from '@shared/DbTypes'; import { PlayoffMatchDisplay } from '../PlayoffMatchDisplay'; import styles from './styles.module.scss'; @@ -76,11 +76,9 @@ function MatchDisplay({ halfWidth, match, className }: MatchDisplayProps): JSX.E Just show a blank entry if the match doesn't exist. Either we're in a test match or at the end of the schedule */} - <> - {match?.num === 'F' ? 'F' : `M${match?.num}`} - {redContent} - {blueContent} - + {match?.num === 'F' ? 'F' : `M${match?.num}`} + {redContent} + {blueContent} ); } diff --git a/src/components/PlayoffQueueing/Queueing/index.tsx b/src/components/PlayoffQueueing/Queueing/index.tsx index 38b31be..b5231d1 100644 --- a/src/components/PlayoffQueueing/Queueing/index.tsx +++ b/src/components/PlayoffQueueing/Queueing/index.tsx @@ -14,7 +14,7 @@ import { PlayoffMatchDisplay } from '../PlayoffMatchDisplay'; type LoadingState = 'loading' | 'ready' | 'error' | 'noAutomatic'; -const PlayoffQueueing = () => { +function PlayoffQueueing() { const { event, season, token } = useContext(AppContext); if (event === undefined || season === undefined) throw new Error('App context has undefineds'); @@ -107,13 +107,11 @@ const PlayoffQueueing = () => { }; const menuOptions = () => ( - <> - - + ); // FIXME (@evanlihou): This effect runs twice on initial load, which causes the "waiting for @@ -163,6 +161,6 @@ const PlayoffQueueing = () => { ); -}; +} export default PlayoffQueueing; diff --git a/src/components/QualDisplay/Queueing/index.tsx b/src/components/QualDisplay/Queueing/index.tsx index 7c9ca2b..5825c06 100644 --- a/src/components/QualDisplay/Queueing/index.tsx +++ b/src/components/QualDisplay/Queueing/index.tsx @@ -6,7 +6,7 @@ import { useContext, useEffect, useState, useRef, useReducer, } from 'preact/hooks'; -import { AppMode, QualBreak, QualMatch } from '@shared/DbTypes'; +import { AppMode } from '@shared/DbTypes'; import { TeamRanking } from '@/types'; import AppContext from '@/AppContext'; import styles from './styles.module.scss'; @@ -14,89 +14,24 @@ import MatchDisplay from '../MatchDisplay'; import Ranking from '../../Tickers/Ranking'; import RankingList from '../../Tickers/RankingList'; import MenuBar from '../../MenuBar'; +import useQueueingQualMatches from '@/hooks/useQueueingQualMatches'; -type LoadingState = 'loading' | 'ready' | 'error' | 'noAutomatic'; - -const Queueing = () => { +function Queueing() { const { event, season, token } = useContext(AppContext); if (event === undefined || season === undefined) throw new Error('App context has undefineds'); - const [loadingState, setLoadingState] = useState('loading'); const dbEventRef = useRef(); - const [qualMatches, setQualMatches] = useState<(QualMatch | QualBreak)[]>([]); - const [displayMatches, setDisplayMatches] = useState<{ - currentMatch: QualMatch | QualBreak | null, - nextMatch: QualMatch | QualBreak | null, - queueingMatches: QualMatch[] | QualBreak[] - }>({ currentMatch: null, nextMatch: null, queueingMatches: [] }); + const displayMatches = useQueueingQualMatches({ + numQueueing: 3, + }); const [rankings, setRankings] = useState([]); useEffect(() => { - if (!token) return () => {}; + if (!token) return; dbEventRef.current = ref(getDatabase(), `/seasons/${season}/events/${token}`); - - const matchesRef = ref(getDatabase(), `/seasons/${season}/qual/${token}`); - onValue(matchesRef, (snap) => { - setQualMatches([...snap.val() as (QualMatch | QualBreak)[], { type: 'break', description: '(END)' }]); - }); - - return () => { - off(matchesRef); - }; }, [event.eventCode, season, token]); - const getMatchIdxByNumber = (matchNumber: number): number | null => { - const res = qualMatches?.findIndex( - (x) => x.type !== 'break' && x.number === matchNumber, - ) ?? null; - - if (res === null || res === -1) return null; - - return res; - }; - - const getMatchByIndex = (index: number | null): QualMatch | QualBreak | null => ( - index !== null && qualMatches - ? (qualMatches[index] ?? null) - : null); - - const updateMatches = (): void => { - const matchNumber = event.currentMatchNumber; - - if (matchNumber === null || matchNumber === undefined) { - if (dbEventRef.current === undefined) return; // throw new Error('No event ref'); - update(dbEventRef.current, { - currentMatchNumber: 1, - }); - return; - } - - try { - const currentIdx = getMatchIdxByNumber(matchNumber); - if (currentIdx !== null) { - setDisplayMatches({ - currentMatch: getMatchByIndex(currentIdx), - nextMatch: getMatchByIndex(currentIdx + 1), - // By default, we'll take the three matches after the one on deck - queueingMatches: [2, 3, 4].map((x) => getMatchByIndex(currentIdx + x)) - .filter((x) => x !== null) as QualMatch[], - }); - } else { - setDisplayMatches({ - currentMatch: null, - nextMatch: null, - queueingMatches: [], - }); - } - - setLoadingState('ready'); - } catch (e) { - setLoadingState('error'); - console.error(e); - } - }; - /** * Swap the event's mode * NOTE: This function must use refs instead of state. It can be called by an event listener @@ -107,11 +42,6 @@ const Queueing = () => { if (dbEventRef.current === undefined) throw new Error('No event ref'); let appMode = mode; if (appMode === null) appMode = event.mode === 'assisted' ? 'automatic' : 'assisted'; - if (appMode === 'assisted') { - if (loadingState === 'noAutomatic' || event.currentMatchNumber === null) { - updateMatches(); - } - } update(dbEventRef.current, { mode: appMode, }); @@ -152,6 +82,7 @@ const Queueing = () => { } else { throw new Error('Unknown action type passed to match number reducer'); } + update(dbEventRef.current, { currentMatchNumber: newMatchNumber, }); @@ -228,10 +159,6 @@ const Queueing = () => { }; }, []); - useEffect(() => { - updateMatches(); - }, [event.currentMatchNumber, qualMatches]); - useEffect(() => { const rankingsRef = ref(getDatabase(), `/seasons/${season}/rankings/${token}`); if (event.options?.showRankings) { @@ -244,17 +171,21 @@ const Queueing = () => { return () => { off(rankingsRef); }; }, [event.options?.showRankings]); - - const { currentMatch, nextMatch, queueingMatches } = displayMatches; + const { + now: currentMatch, + next: nextMatch, + queueing: queueingMatches, + state: loadingState, + hasSchedule, + } = displayMatches; return ( <>
{loadingState === 'loading' &&
Loading matches...
} {loadingState === 'error' &&
Failed to fetch matches
} - {loadingState === 'noAutomatic' &&
Unable to run in automatic mode. Press the 'a' key to switch modes.
} - {loadingState === 'ready' && !qualMatches?.length &&
Waiting for schedule to be posted...
} - {loadingState === 'ready' && qualMatches?.length !== 0 + {loadingState === 'ready' && !hasSchedule &&
Waiting for schedule to be posted...
} + {loadingState === 'ready' && hasSchedule && (
{event.mode === 'assisted' && ( @@ -278,11 +209,15 @@ const Queueing = () => {
{event.name}
)}
- {currentMatch && ( -
- - On Field -
+ {currentMatch ? ( +
+ + On Field +
+ ) : ( +
+ Qualification matches have concluded +
)} {nextMatch && (
@@ -291,7 +226,7 @@ const Queueing = () => {
)}
- {queueingMatches.map((x) => ( + {queueingMatches?.map((x) => ( { /> ))}
- {(event.options?.showRankings ?? false ? ( + {((event.options?.showRankings ?? false) && ( {rankings.map((x) => ())} - ) : <>)} + ))}
)} ); -}; +} export default Queueing; diff --git a/src/components/RankingDisplay/KeyableTicker/index.tsx b/src/components/RankingDisplay/KeyableTicker/index.tsx index ce16306..1625a80 100644 --- a/src/components/RankingDisplay/KeyableTicker/index.tsx +++ b/src/components/RankingDisplay/KeyableTicker/index.tsx @@ -15,7 +15,7 @@ import Ranking from '../../Tickers/Ranking'; import RankingList from '../../Tickers/RankingList'; import MenuBar from '../../MenuBar'; -const KeyableTicker = () => { +function KeyableTicker() { const { event, season, token } = useContext(AppContext); if (event === undefined || season === undefined) throw new Error('App context has undefineds'); const dbEventRef = useRef(); @@ -234,6 +234,6 @@ const KeyableTicker = () => { ); -}; +} export default KeyableTicker; diff --git a/src/components/RankingDisplay/TeamRankings/index.tsx b/src/components/RankingDisplay/TeamRankings/index.tsx index 8a186c0..6101f0a 100644 --- a/src/components/RankingDisplay/TeamRankings/index.tsx +++ b/src/components/RankingDisplay/TeamRankings/index.tsx @@ -13,7 +13,7 @@ import numberToOrdinal from '@/util/numberToOrdinal'; const numFmt = new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); -const TeamRankings = () => { +function TeamRankings() { const { event, season, token } = useContext(AppContext); const [rankings, setRankings] = useState([]); useEffect(() => { @@ -89,6 +89,6 @@ const TeamRankings = () => { ); -}; +} export default TeamRankings; diff --git a/src/components/StaleDataBanner/index.tsx b/src/components/StaleDataBanner/index.tsx index 265cbdc..ca2fe8f 100644 --- a/src/components/StaleDataBanner/index.tsx +++ b/src/components/StaleDataBanner/index.tsx @@ -1,4 +1,4 @@ -import { h, Fragment } from 'preact'; +import { h } from 'preact'; import { useContext, useEffect, useRef, useState, } from 'preact/hooks'; @@ -13,7 +13,7 @@ const units = { second: 1000, }; -const StaleDataBanner = (): JSX.Element => { +function StaleDataBanner() { const context = useContext(AppContext); const contextRef = useRef(); const intervalRef = useRef | null>(); @@ -61,7 +61,7 @@ const StaleDataBanner = (): JSX.Element => { }, [context?.event?.lastModifiedMs]); if (context.features?.showStaleDataBanner === false || window.location.pathname.includes('overlay')) { - return <>; + return null; } if (isShown) { @@ -72,7 +72,7 @@ const StaleDataBanner = (): JSX.Element => { ); } - return <>; -}; + return null; +} export default StaleDataBanner; diff --git a/src/hooks/useQueueingQualMatches.tsx b/src/hooks/useQueueingQualMatches.tsx new file mode 100644 index 0000000..e73314c --- /dev/null +++ b/src/hooks/useQueueingQualMatches.tsx @@ -0,0 +1,119 @@ +import { QualMatch, QualBreak, Event } from '@shared/DbTypes'; +import { + useContext, useEffect, useRef, useState, +} from 'preact/hooks'; +import { + DatabaseReference, getDatabase, off, onValue, ref, update, +} from 'firebase/database'; +import AppContext from '@/AppContext'; + +export type UseQueueingQualMatchesProps = { + token?: string, // uses token from context if undefined + numQueueing: number +}; + +export type QueueingQualMatches = { + state: 'loading' | 'error' | 'ready', + hasSchedule: boolean, + now: QualMatch | QualBreak | null, + next: QualMatch | QualBreak | null, + queueing: (QualMatch | QualBreak)[] | null +}; + +export default function useQueueingQualMatches(props: UseQueueingQualMatchesProps) + : QueueingQualMatches { + const { token: ctxToken, season } = useContext(AppContext); + const dbEventRef = useRef(); + const [event, setEvent] = useState(null); + const [qualMatches, setQualMatches] = useState<(QualMatch | QualBreak)[]>([]); + const [displayMatches, setDisplayMatches] = useState({ + state: 'loading', + hasSchedule: false, + now: null, + next: null, + queueing: null, + }); + const token = props.token ?? ctxToken; + + useEffect(() => { + if (!token || !season) return () => {}; + + dbEventRef.current = ref(getDatabase(), `/seasons/${season}/events/${token}`); + onValue(dbEventRef.current, (snap) => { + setEvent(snap.val() as Event); + }); + + const matchesRef = ref(getDatabase(), `/seasons/${season}/qual/${token}`); + onValue(matchesRef, (snap) => { + setQualMatches([...snap.val() as (QualMatch | QualBreak)[], { type: 'break', description: '(END)' }]); + }); + + return () => { + off(matchesRef); + }; + }, [season, token]); + + const getMatchIdxByNumber = (matchNumber: number): number | null => { + const res = qualMatches?.findIndex( + (x) => x.type !== 'break' && x.number === matchNumber, + ) ?? null; + + if (res === null || res === -1) return null; + + return res; + }; + + const getMatchByIndex = (index: number | null): QualMatch | QualBreak | null => ( + index !== null && qualMatches + ? (qualMatches[index] ?? null) + : null); + + const updateMatches = (): void => { + if (!event) return; + const matchNumber = event?.currentMatchNumber; + + if (matchNumber === null || matchNumber === undefined) { + if (dbEventRef.current === undefined) return; + update(dbEventRef.current, { + currentMatchNumber: 1, + }); + return; + } + + try { + const currentIdx = getMatchIdxByNumber(matchNumber); + if (currentIdx !== null) { + setDisplayMatches({ + state: 'ready', + hasSchedule: qualMatches && qualMatches.length > 0, + now: getMatchByIndex(currentIdx), + next: getMatchByIndex(currentIdx + 1), + queueing: Array(props.numQueueing).fill(2) + .map((e, i) => e + i) + .map((x) => getMatchByIndex(currentIdx + x)) + .filter((x) => x !== null) as (QualMatch | QualBreak)[], + }); + } else { + setDisplayMatches({ + state: 'ready', + hasSchedule: qualMatches && qualMatches.length > 0, + now: null, + next: null, + queueing: [], + }); + } + } catch (e) { + setDisplayMatches((val) => ({ + ...val, + state: 'error', + })); + console.error(e); + } + }; + + useEffect(() => { + updateMatches(); + }, [event?.currentMatchNumber, qualMatches]); + + return displayMatches; +} diff --git a/src/sw.js b/src/sw.js index 146aa01..62e2c29 100644 --- a/src/sw.js +++ b/src/sw.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-unresolved import { getFiles, setupPrecaching, setupRouting } from 'preact-cli/sw/'; setupRouting(); diff --git a/tsconfig.json b/tsconfig.json index b5c9ff7..e2d1bc8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { /* Basic Options */ - "target": "ES5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ + "target": "es2016", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ "module": "ESNext", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation: */ "allowJs": true, /* Allow javascript files to be compiled. */ diff --git a/vite.config.ts b/vite.config.ts index f4c2dd6..453640d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,8 +15,8 @@ export default defineConfig({ return code.replace(/\/\*#__PURE__\*\//g, ''); } return null; - } - } + }, + }, ], envPrefix: 'APP_', resolve: { @@ -28,17 +28,18 @@ export default defineConfig({ css: { preprocessorOptions: { scss: { - silenceDeprecations: ['import', 'global-builtin'] // HACK: milligram is outdated - } - } + silenceDeprecations: ['import', 'global-builtin'], // HACK: milligram is outdated + }, + }, }, build: { rollupOptions: { output: { manualChunks: { - signalr: ['@microsoft/signalr'] - } - } - } - } + signalr: ['@microsoft/signalr'], + animation: ['motion', '@react-spring/web'], + }, + }, + }, + }, });