import React, { useEffect, useRef, useState } from 'react' import {API, Auth, graphqlOperation} from 'aws-amplify' import {Heading, Grid, Card, useTheme, Text, View, useAuthenticator, Button} from '@aws-amplify/ui-react'; import ChartRace from 'react-chart-race'; import WebRTCViewer from "../web-components/webrtc-viewer" import * as customStatements from '../graphql/custom-statements' import { CORE_OVERVIEW_ID, RACE_STATES, RACE_STATE_ICONS, RACER_ICON, LAPTIME_TYPE_STRING, LEADERBOARD_INDEX_ICONS } from '../utils/constants' import {useNavigate} from "react-router-dom"; const KVSCHANNEL_REGION = process.env.REACT_KVS_CHANNEL_REGION const KVSCHANNEL_ARN = process.env.REACT_KVS_CHANNEL_ARN const NR_OF_LATEST_LAPS_DISPLAYED = 5 const LEADERBOARD_NR_OF_PEOPLE = 3 export default function RaceOverview() { const [subscriptions, setSubscriptions] = useState([]) const [claimSubscriptions, setClaimSubscriptions] = useState([]) const [config, setConfig] = useState() const [race, setRace] = useState() const [players, setPlayers] = useState([]) const [playerLaps, setPlayerLaps] = useState({}) const [fastestLap, setFastestLap] = useState() const [latestLaps, setLatestLaps] = useState([]) const [allTimeTop3, setAllTimeTop3] = useState([]) const [loading, setLoading] = useState(false) const latestLapsRef = useRef(latestLaps) const allLaps = useRef([]) const fastestLapRef = useRef(fastestLap) const subscriptionsRef = useRef(subscriptions) const claimSubscriptionRef = useRef(claimSubscriptions) const raceRef = useRef(race) const { route, signOut } = useAuthenticator((context) => [ context.route, context.signOut, ]); const navigate = useNavigate(); useEffect(() => { init() return () => { unsubscribeSubscriptions() } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const unsubscribeSubscriptions = () => { const subs = subscriptionsRef.current for (const sub of subs) { console.log("UNSUBSCRIBING: ", sub) sub.unsubscribe() } } const init = async () => { setLoading(true) await fetchCreds(); await setRaceDataFromOverview() setLoading(false) } const fetchCreds = async () => { const { accessKeyId, secretAccessKey, sessionToken } = await Auth.currentUserCredentials() const config = { credentials: { accessKeyId: accessKeyId, secretAccessKey: secretAccessKey, sessionToken: sessionToken }, channelARN: KVSCHANNEL_ARN, region: KVSCHANNEL_REGION, debug: true }; setConfig(config) } const getRaceIdFromOverview = async () => { const overview = await getOverview() var raceId = null if (overview && overview.currentRace !== null) { raceId = overview?.currentRace?.id createSubscriptions(raceId) } return raceId } const fetchInitialLapTimes = async () => { const lapTimes = await API.graphql(graphqlOperation(customStatements.customLapTimesByRaceId, { raceId: raceRef.current.id, limit: 1000 })) const allTimeFastest = await API.graphql(graphqlOperation(customStatements.customLapTimesByTime, { type: LAPTIME_TYPE_STRING, limit: LEADERBOARD_NR_OF_PEOPLE, sortDirection: "ASC" })) const lapTimeItems = lapTimes.data.lapTimesByRaceId.items const latestLaps = lapTimeItems.sort(function (a, b) { return new Date(b.createdAt) - new Date(a.createdAt); }).slice(0, NR_OF_LATEST_LAPS_DISPLAYED); if (lapTimeItems.length > 0) { const fastestLap = lapTimeItems.reduce(function (prev, current) { return (prev.timeInMilliSec < current.timeInMilliSec) ? prev : current }) fastestLapRef.current = fastestLap setFastestLap(fastestLap) } latestLapsRef.current = latestLaps allLaps.current = lapTimeItems setLatestLaps(latestLaps) setAllTimeTop3(allTimeFastest.data.lapTimesByTime.items) lapsCompletedPerPlayer(lapTimeItems) } const setRaceDataFromOverview = async () => { const raceId = await getRaceIdFromOverview() await fetchCurrentRaceAndLapTimes(raceId) } const createClaimSubscriptions = async (raceId) => { unsubscribeClaimsSubscriptions() const updateClaimSubscription = await API.graphql(graphqlOperation(customStatements.customOnUpdatePlayerClaim)).subscribe({ next: ({ _, value }) => { console.log("Updated Claim: ", value); fetchCurrentRaceAndLapTimes(raceId) }, error: error => console.warn(error) }); const createClaimSubscription = await API.graphql(graphqlOperation(customStatements.customOnCreatePlayerClaim)).subscribe({ next: ({ _, value }) => { console.log("New Claim: ", value); fetchCurrentRaceAndLapTimes(raceId) }, error: error => console.warn(error) }); const subs = [ updateClaimSubscription, createClaimSubscription, ] setClaimSubscriptions(subs) claimSubscriptionRef.current = subs } const unsubscribeClaimsSubscriptions = async () => { const subs = claimSubscriptionRef.current for (const sub of subs) { console.log("UNSUBSCRIBING: ", sub) sub.unsubscribe() } } const createSubscriptions = async (raceId) => { // Clean up any pre-existing subscriptions unsubscribeSubscriptions() const updateRaceSubscription = await API.graphql({ query: customStatements.customOnUpdateRaceById, variables: { id: raceId } }).subscribe({ next: ({ _, value }) => { fetchCurrentRaceAndLapTimes(raceId) }, error: error => console.warn(error) }); const updateOverviewSubscription = await API.graphql({ query: customStatements.customOnUpdateOverview }).subscribe({ next: ({ _, value }) => { console.log("Updated Overview: ", value); setRaceDataFromOverview() }, error: error => console.warn(error) }); const updateOnIncomingLaptime = (lapTime) => { var currentLaps = latestLapsRef.current currentLaps.unshift(lapTime) if (currentLaps.length > NR_OF_LATEST_LAPS_DISPLAYED) { currentLaps.pop() } const newAllLaps = [...allLaps.current, lapTime] lapsCompletedPerPlayer(newAllLaps) allLaps.current = newAllLaps latestLapsRef.current = currentLaps if (fastestLapRef.current && lapTime.timeInMilliSec < fastestLapRef.current.timeInMilliSec) { fastestLapRef.current = lapTime setFastestLap(lapTime) } setLatestLaps([...currentLaps]) } const lapTimeSubscription = await API.graphql({ query: customStatements.customOnCreateLapTime }).subscribe({ next: ({ _, value }) => { console.log("New LapTime: ", value); updateOnIncomingLaptime(value.data.onCreateLapTime) //fetchLapTimes() }, error: error => console.warn(error) }); const subs = [ updateRaceSubscription, updateOverviewSubscription, lapTimeSubscription ] setSubscriptions(subs) subscriptionsRef.current = subs } const lapsCompletedPerPlayer = (lapTimes) => { const currRaceId = raceRef.current.id const currRacePlayers = raceRef.current.players.items const playerLapsObject = {} for (const player of currRacePlayers) { const lapsOfPlayer = lapTimes.filter(item => item.playerId === player.id && currRaceId === item.raceId) playerLapsObject[player.id] = lapsOfPlayer.length } setPlayerLaps(playerLapsObject) } const getOverview = async () => { var overview = null; try { const overviewData = await API.graphql(graphqlOperation(customStatements.customGetOverview, { id: CORE_OVERVIEW_ID })) overview = overviewData.data.getOverview || null } catch (error) { console.error("No overview found, likely that you need to initialise") console.log(error) } return overview } const fetchCurrentRaceAndLapTimes = async (raceId) => { try { const raceData = await API.graphql(graphqlOperation(customStatements.customGetRace, { id: raceId })) const currentRace = raceData.data.getRace if (currentRace.currentRaceState === RACE_STATES.LOBBY && claimSubscriptionRef.current.length === 0) { await createClaimSubscriptions(currentRace.id) } if (currentRace.currentRaceState !== RACE_STATES.LOBBY && claimSubscriptionRef.current.length > 0) { unsubscribeClaimsSubscriptions() } const activePlayers = currentRace.players.items.filter(item => item.claims.items[0]?.username !== undefined) setRace(currentRace) setPlayers(activePlayers) raceRef.current = currentRace await fetchInitialLapTimes() } catch (err) { console.error(err) } } const { tokens } = useTheme(); const getChartData = (players, playerLaps) => { var objects = [] for (const player of players) { objects.push({ id: player.id, title: `${player.claims.items[0]?.username}`, value: playerLaps[player.id] || 0, color: `${player.car.color}` }) } return objects } const chartData = getChartData(players, playerLaps) const getMaxLaps = () => { var max = 0 for (const playerLap of Object.values(playerLaps)) { if (playerLap > max) { max = playerLap } } return max } if (race === null || loading ) { return ( <> Awaiting race information ... ) } else if ( route !== 'authenticated' ) { return ( <> You must be authenticated to view this page. ) }else { return ( Lap: {getMaxLaps()} / {race?.nrOfLaps} { race && <> Race status: {RACE_STATE_ICONS[race?.currentRaceState]} Current race id: {race?.id} } Leaderboard (# of laps): { chartData.length > 0 && } 📹 Live stream: {/* */} {config && } Latest laps: { latestLaps.map(item => { return ( {`${item.player.claims.items[0]?.username || "unnamed"}`} {`${item.timeInMilliSec}ms`} ) }) } 🏆 All-time laptimes top 3: 🏆 { allTimeTop3.map((item, index) => { return `${LEADERBOARD_INDEX_ICONS[index]} ${item.player.claims.items[0]?.username || "unnamed"} (${item.timeInMilliSec}ms) ` }) } ✨ Current race - fastest lap: ✨ {fastestLap && {`${RACER_ICON} ${fastestLap.player.claims.items[0]?.username || "unnamed"} - ${fastestLap.timeInMilliSec}ms`} } ) } }