diff --git a/src/api/base.ts b/src/api/base.ts index 15cc9e783..d2c45fbb1 100644 --- a/src/api/base.ts +++ b/src/api/base.ts @@ -21,6 +21,10 @@ interface ErrorData { error: string } +export class NetworkError extends Error { + name = 'NetworkError' +} + export const request = ( method: HTTPMethod, endpoint: string, @@ -33,12 +37,17 @@ export const request = ( onCancel(() => controller.abort()) const jwt = await getWorkingJWT() - const resp = await fetch(apiUrl + endpoint + qs(params), { - method, - body: JSON.stringify(body), - headers: jwt ? { Authorization: `Bearer ${jwt.raw}` } : {}, - signal, - }) + let resp: Response + try { + resp = await fetch(apiUrl + endpoint + qs(params), { + method, + body: JSON.stringify(body), + headers: jwt ? { Authorization: `Bearer ${jwt.raw}` } : {}, + signal, + }) + } catch (error_) { + throw new NetworkError(error_.message) + } const text = await resp.text() diff --git a/src/components/analysis-table.tsx b/src/components/analysis-table.tsx index 388b545b2..2057f936b 100644 --- a/src/components/analysis-table.tsx +++ b/src/components/analysis-table.tsx @@ -25,6 +25,7 @@ import { usePromise } from '@/utils/use-promise' import { getEventTeams } from '@/api/event-team-info/get-event-teams' import { eventTeamUrl } from '@/utils/urls/event-team' import { EventTeamInfo } from '@/api/event-team-info' +import { isData } from '@/utils/is-data' interface Props { eventKey: string @@ -224,7 +225,10 @@ const AnalysisTable = ({ const rankColumn: Column = { title: 'Rank', key: 'Rank', - getCell: (row) => rankingInfo?.find((r) => r.team === 'frc' + row.team), + getCell: (row) => + isData(rankingInfo) + ? rankingInfo.find((r) => r.team === 'frc' + row.team) + : undefined, getCellValue: (cell) => cell?.rank ?? Infinity, renderCell: (cell) => ( diff --git a/src/components/chart.tsx b/src/components/chart.tsx index aec5d3311..83a7c9a3c 100644 --- a/src/components/chart.tsx +++ b/src/components/chart.tsx @@ -24,6 +24,7 @@ import { cleanFieldName } from '@/utils/clean-field-name' import { getFieldKey } from '@/utils/get-field-key' import { getReports } from '@/api/report/get-reports' import { GetReport } from '@/api/report' +import { isData } from '@/utils/is-data' const commentsDisplayStyle = css` grid-column: 1 / -1; @@ -106,7 +107,7 @@ export const ChartCard = ({ return matchesAutoFieldName || matchesTeleopFieldName })?.name - const matchesWithSelectedStat = (matchesStats || []) + const matchesWithSelectedStat = (isData(matchesStats) ? matchesStats : []) .map(({ matchKey, stats }) => { const matchingStat = stats.find((f) => f.name === fullFieldName) if (matchingStat) return { matchKey, matchingStat } @@ -190,7 +191,11 @@ export const ChartCard = ({ {selectedMatchKey && ( r.matchKey === selectedMatchKey)} + reports={ + isData(allReports) + ? allReports.filter((r) => r.matchKey === selectedMatchKey) + : [] + } /> )} diff --git a/src/components/comment-card.tsx b/src/components/comment-card.tsx index 26ef25a3c..3e0872835 100644 --- a/src/components/comment-card.tsx +++ b/src/components/comment-card.tsx @@ -6,6 +6,7 @@ import Icon from './icon' import { mdiMessageReplyText } from '@mdi/js' import { formatUserName } from '@/utils/format-user-name' import { getFastestUser } from '@/cache/users/get-fastest' +import { isData } from '@/utils/is-data' const commentCardStyle = css` display: grid; @@ -52,7 +53,11 @@ export const CommentCard = ({ class={commentCardStyle} > - {showReporter && {formatUserName(reporter, reporterId)}} + {showReporter && ( + + {formatUserName(isData(reporter) ? reporter : undefined, reporterId)} + + )}

{report.comment}

) diff --git a/src/components/dialog.tsx b/src/components/dialog.tsx index b4497f451..2fdf33982 100644 --- a/src/components/dialog.tsx +++ b/src/components/dialog.tsx @@ -100,7 +100,13 @@ export const DialogDisplayer = () => { )} diff --git a/src/components/report-viewer.tsx b/src/components/report-viewer.tsx index 5a8e9dcd3..84f9b497e 100644 --- a/src/components/report-viewer.tsx +++ b/src/components/report-viewer.tsx @@ -12,6 +12,7 @@ import { CommentCard } from './comment-card' import { css } from 'linaria' import { cleanFieldName } from '@/utils/clean-field-name' import { ProfileLink } from './profile-link' +import { isData } from '@/utils/is-data' interface Props { report: Report @@ -66,10 +67,10 @@ export const ReportViewer = ({ report, onEditClick }: Props) => { const reporterId = report.reporterId const eventInfo = useEventInfo(report.eventKey) const matchInfo = useMatchInfo(report.eventKey, report.matchKey) - const schema = useSchema(eventInfo?.schemaId) - const displayableFields = schema?.schema.filter( - (field) => field.reportReference !== undefined, - ) + const schema = useSchema(isData(eventInfo) ? eventInfo.schemaId : undefined) + const displayableFields = isData(schema) + ? schema.schema.filter((field) => field.reportReference !== undefined) + : undefined const autoFields = displayableFields?.filter( (field) => field.period === 'auto', ) @@ -99,8 +100,8 @@ export const ReportViewer = ({ report, onEditClick }: Props) => {
{matchInfo && ( diff --git a/src/routes/event-analysis.tsx b/src/routes/event-analysis.tsx index 94ba5454e..db3a0898b 100644 --- a/src/routes/event-analysis.tsx +++ b/src/routes/event-analysis.tsx @@ -13,6 +13,7 @@ import { tablePageTableStyle, } from '@/utils/table-page-style' import Card from '@/components/card' +import { isData } from '@/utils/is-data' interface Props { eventKey: string @@ -26,7 +27,7 @@ const EventAnalysis: FunctionComponent = ({ eventKey }) => { const eventStats = usePromise(() => getEventStats(eventKey), [eventKey]) const eventInfo = useEventInfo(eventKey) - const schema = useSchema(eventInfo?.schemaId) + const schema = useSchema(isData(eventInfo) ? eventInfo.schemaId : undefined) return ( = ({ eventKey }) => { class={tablePageStyle} wrapperClass={tablePageWrapperStyle} > - {eventStats && schema ? ( + {isData(eventStats) && schema ? ( eventStats.length === 0 ? ( 'No Event Data' ) : ( @@ -43,7 +44,7 @@ const EventAnalysis: FunctionComponent = ({ eventKey }) => { ( {team} diff --git a/src/routes/event-match.tsx b/src/routes/event-match.tsx index 1da8ca0b3..f3fbb0918 100644 --- a/src/routes/event-match.tsx +++ b/src/routes/event-match.tsx @@ -20,6 +20,7 @@ import { BooleanDisplay } from '@/components/boolean-display' import { matchHasTeam } from '@/utils/match-has-team' import { VideoCard } from '@/components/video-card' import { cleanYoutubeUrl } from '@/utils/clean-youtube-url' +import { isData } from '@/utils/is-data' import { MatchReports } from '@/components/match-reports' import { getReports } from '@/api/report/get-reports' import Icon from '@/components/icon' @@ -126,12 +127,15 @@ const EventMatch = ({ eventKey, matchKey }: Props) => { const m = formatMatchKey(matchKey) const event = useEventInfo(eventKey) const match = useMatchInfo(eventKey, matchKey) + const matchRedAlliance = isData(match) ? match.redAlliance : undefined const reports = usePromise(() => { if (isOnline) { return getReports({ event: eventKey, match: matchKey }) } }, [eventKey, matchKey, isOnline]) - const schema = useSchema(isOnline ? event?.schemaId : undefined) + const schema = useSchema( + isOnline && isData(event) ? event.schemaId : undefined, + ) const eventTeamsStats = usePromise(() => { if (isOnline) { return getEventStats(eventKey) @@ -142,8 +146,9 @@ const EventMatch = ({ eventKey, matchKey }: Props) => { showEventResults, ) - const matchHasBeenPlayed = - match?.blueScore !== undefined && match.redScore !== undefined + const matchHasBeenPlayed = isData(match) + ? match.blueScore !== undefined && match.redScore !== undefined + : undefined // When the match loads (or changes), useEffect(() => { @@ -151,7 +156,7 @@ const EventMatch = ({ eventKey, matchKey }: Props) => { }, [matchHasBeenPlayed]) const teamsStats = usePromise(() => { - if (match && isOnline) { + if (isData(match) && isOnline) { return Promise.all( [...match.redAlliance, ...match.blueAlliance].map((t) => getMatchTeamStats(eventKey, match.key, t).then(processTeamStats), @@ -173,7 +178,8 @@ const EventMatch = ({ eventKey, matchKey }: Props) => { class={clsx( matchStyle, match && loadedMatchStyle, - match?.videos && + isData(match) && + match.videos && match.videos.length > 0 && isOnline && matchWithVideoStyle, @@ -211,10 +217,21 @@ const EventMatch = ({ eventKey, matchKey }: Props) => {
)} - - {reports && reports.length > 0 ? ( + + {isData(reports) && reports.length > 0 ? ( @@ -226,8 +243,12 @@ const EventMatch = ({ eventKey, matchKey }: Props) => { )} {matchHasBeenPlayed /* final score if the match is over */ && ( -
{match.redScore}
-
{match.blueScore}
+
+ {isData(match) && match.redScore} +
+
+ {isData(match) && match.blueScore} +
)} @@ -265,17 +286,25 @@ const EventMatch = ({ eventKey, matchKey }: Props) => { eventKey={eventKey} teams={ selectedDisplay === showEventResults - ? eventTeamsStats?.filter((t) => - matchHasTeam('frc' + t.team)(match), - ) - : teamsStats + ? isData(eventTeamsStats) + ? eventTeamsStats.filter((t) => + matchHasTeam('frc' + t.team)( + isData(match) + ? match + : { key: '', redAlliance: [], blueAlliance: [] }, + ), + ) + : undefined + : isData(teamsStats) + ? teamsStats + : undefined } - schema={schema} + schema={isData(schema) ? schema : { id: -1, schema: [] }} renderTeam={(team, link) => (
{ )} {/* shows videos if the match has them and online */} - {isOnline && match.videos && match.videos.length > 0 && ( - - )} + {isOnline && + isData(match) && + match.videos && + match.videos.length > 0 && } ) : ( diff --git a/src/routes/event-team-comments.tsx b/src/routes/event-team-comments.tsx index b9d4341d2..8b1ece34b 100644 --- a/src/routes/event-team-comments.tsx +++ b/src/routes/event-team-comments.tsx @@ -10,6 +10,7 @@ import { formatMatchKeyShort } from '@/utils/format-match-key-short' import { compareMatchKeys } from '@/utils/compare-matches' import { GetReport } from '@/api/report' import { getReports } from '@/api/report/get-reports' +import { isData } from '@/utils/is-data' interface Props { eventKey: string @@ -33,16 +34,15 @@ const EventTeamComments = ({ eventKey, teamNum }: Props) => { team, eventKey, ]) - const commentsByMatch = reports?.reduce<{ [matchKey: string]: GetReport[] }>( - (acc, report) => { - if (report.comment) { - // eslint-disable-next-line caleb/@typescript-eslint/no-unnecessary-condition - ;(acc[report.matchKey] || (acc[report.matchKey] = [])).push(report) - } - return acc - }, - {}, - ) + const commentsByMatch = isData(reports) + ? reports.reduce<{ [matchKey: string]: GetReport[] }>((acc, report) => { + if (report.comment) { + // eslint-disable-next-line caleb/@typescript-eslint/no-unnecessary-condition + ;(acc[report.matchKey] || (acc[report.matchKey] = [])).push(report) + } + return acc + }, {}) + : undefined return ( { class={eventTeamMatchesStyle} > {matches ? ( - + ) : ( )} diff --git a/src/routes/event-team.tsx b/src/routes/event-team.tsx index 510af1d07..818a4371e 100644 --- a/src/routes/event-team.tsx +++ b/src/routes/event-team.tsx @@ -27,6 +27,7 @@ import { useCurrentTime } from '@/utils/use-current-time' import { saveTeam, useSavedTeams, removeTeam } from '@/api/save-teams' import IconButton from '@/components/icon-button' import { EventTeamInfo } from '@/api/event-team-info' +import { isData } from '@/utils/is-data' const sectionStyle = css` font-weight: normal; @@ -162,10 +163,11 @@ const EventTeam = ({ eventKey, teamNum }: Props) => { () => getEventTeamInfo(eventKey, 'frc' + teamNum).catch(() => undefined), [eventKey, teamNum], ) - const schema = useSchema(eventInfo?.schemaId) - const teamMatches = useEventMatches(eventKey, 'frc' + teamNum)?.sort( - compareMatches, - ) + const schema = useSchema(isData(eventInfo) ? eventInfo.schemaId : undefined) + let teamMatches = useEventMatches(eventKey, 'frc' + teamNum) + teamMatches = isData(teamMatches) + ? teamMatches.sort(compareMatches) + : undefined const nextMatch = teamMatches && nextIncompleteMatch(teamMatches) @@ -203,7 +205,7 @@ const EventTeam = ({ eventKey, teamNum }: Props) => { )} @@ -79,7 +80,7 @@ const Event = ({ eventKey }: Props) => { link /> )} - {matches ? ( + {isData(matches) ? ( matches.length > 0 ? ( ) : ( diff --git a/src/routes/home.tsx b/src/routes/home.tsx index 194f3a4e7..280f35ea6 100644 --- a/src/routes/home.tsx +++ b/src/routes/home.tsx @@ -13,6 +13,7 @@ import { UnstyledList } from '@/components/unstyled-list' import { useYears } from '@/utils/use-years' import IconButton from '@/components/icon-button' import { mdiCrosshairsGps } from '@mdi/js' +import { isData } from '@/utils/is-data' const homeStyle = css` display: grid; @@ -46,7 +47,8 @@ const Home = () => { const [location, prompt] = useGeoLocation() const [query, setQuery] = useState('') const lowerCaseQuery = query.toLowerCase() - const years = useYears().sort().reverse() + let years = useYears() + years = isData(years) ? years.sort().reverse() : [] const [yearVal, setYear] = useQueryState('year', years[0]) const year = Number(yearVal) const events = useEvents(year) @@ -65,7 +67,7 @@ const Home = () => { )} - {events ? ( + {isData(events) ? ( <> {events diff --git a/src/routes/leaderboard.tsx b/src/routes/leaderboard.tsx index a9744839c..6e527ec08 100644 --- a/src/routes/leaderboard.tsx +++ b/src/routes/leaderboard.tsx @@ -10,6 +10,7 @@ import { useYears } from '@/utils/use-years' import { Dropdown } from '@/components/dropdown' import { getFastestUser } from '@/cache/users/get-fastest' import { UserInfo } from '@/api/user' +import { isData } from '@/utils/is-data' const leaderboardCardTitleStyle = css` font-weight: 500; @@ -58,10 +59,16 @@ const LeaderboardList = () => { return (
- - {leaderboard?.map((user) => ( - - )) || } + + {isData(leaderboard) ? ( + leaderboard.map((user) => ) + ) : ( + + )}
) } diff --git a/src/routes/signup.tsx b/src/routes/signup.tsx index 66f49899b..5a8a051f5 100644 --- a/src/routes/signup.tsx +++ b/src/routes/signup.tsx @@ -19,6 +19,7 @@ import { ErrorBoundary, useErrorEmitter } from '@/components/error-boundary' import { authenticate } from '@/api/authenticate' import { route } from '@/router' import { Realm } from '@/api/realm' +import { isData } from '@/utils/is-data' const signUpStyle = css` padding: 1.5rem; @@ -91,10 +92,12 @@ const SignUpForm = () => { maxLength={maxPasswordLength} /> - value={realms.find((r) => r.id === realmId)} + value={ + isData(realms) ? realms.find((r) => r.id === realmId) : undefined + } emptyLabel="Select a realm" class={dropdownClass} - options={realms} + options={isData(realms) ? realms : []} required onChange={(v) => setRealmId(v.id)} getKey={(v) => v.id} diff --git a/src/routes/user-reports.tsx b/src/routes/user-reports.tsx index c2558a250..8ff5cfda7 100644 --- a/src/routes/user-reports.tsx +++ b/src/routes/user-reports.tsx @@ -9,6 +9,7 @@ import { GetReport } from '@/api/report' import { useEventInfo } from '@/cache/event-info/use' import { formatTeamNumber } from '@/utils/format-team-number' import { getFastestUser } from '@/cache/users/get-fastest' +import { isData } from '@/utils/is-data' interface Props { userId: string @@ -36,12 +37,15 @@ const UserReports = ({ userId }: Props) => { return ( - {reports ? ( + {isData(reports) ? ( reports.map((report) => { return }) diff --git a/src/routes/user.tsx b/src/routes/user.tsx index 86e05bbf7..8bcc23089 100644 --- a/src/routes/user.tsx +++ b/src/routes/user.tsx @@ -34,6 +34,7 @@ import clsx from 'clsx' import { getReports } from '@/api/report/get-reports' import { noop } from '@/utils/empty-promise' import { getFastestUser } from '@/cache/users/get-fastest' +import { isData } from '@/utils/is-data' const RoleInfo = ({ save, @@ -336,7 +337,7 @@ const UserProfileCard = ({ )} - {reports && ( + {isData(reports) && (