From f3c55c18f76243be3d4a20ac270127aa26843462 Mon Sep 17 00:00:00 2001 From: alexwhelan12 Date: Sat, 27 Sep 2025 13:50:50 -0600 Subject: [PATCH 1/4] Switched packet and lap contexts to zustand stores --- .../src/components/atoms/BatteryIcon.tsx | 4 +- .../client/src/components/atoms/SpeedAtom.tsx | 4 +- .../src/components/atoms/ThrottleIcon.tsx | 4 +- .../components/containers/MapContainer.tsx | 4 +- .../global/LapDataListenerManager.tsx | 42 +++++++ .../global/PacketListenerManager.tsx | 42 +++++++ .../HeroMolecules/CarGraphicComponent.tsx | 6 +- .../HeroMolecules/GearParkBrakeComponent.tsx | 4 +- .../PlaybackDatePicker.tsx | 7 +- .../LogoStatusMolecules/StatusComponent.tsx | 8 +- .../client/src/components/tabs/RaceTab.tsx | 8 +- .../client/src/contexts/LapDataContext.tsx | 114 ------------------ .../client/src/contexts/PacketContext.tsx | 72 ----------- packages/client/src/hooks/PIS/PIS.battery.tsx | 4 +- packages/client/src/hooks/PIS/PIS.faults.tsx | 4 +- packages/client/src/hooks/PIS/PIS.mbms.tsx | 4 +- packages/client/src/hooks/PIS/PIS.motor.tsx | 4 +- packages/client/src/hooks/PIS/PIS.mppt.tsx | 4 +- packages/client/src/pages/[[...slug]].tsx | 7 +- packages/client/src/pages/_app.tsx | 16 ++- packages/client/src/stores/useLapData.ts | 85 +++++++++++++ packages/client/src/stores/usePacket.ts | 42 +++++++ 22 files changed, 256 insertions(+), 233 deletions(-) create mode 100644 packages/client/src/components/global/LapDataListenerManager.tsx create mode 100644 packages/client/src/components/global/PacketListenerManager.tsx delete mode 100644 packages/client/src/contexts/LapDataContext.tsx delete mode 100644 packages/client/src/contexts/PacketContext.tsx create mode 100644 packages/client/src/stores/useLapData.ts create mode 100644 packages/client/src/stores/usePacket.ts diff --git a/packages/client/src/components/atoms/BatteryIcon.tsx b/packages/client/src/components/atoms/BatteryIcon.tsx index 73ef2a27..4e14636e 100644 --- a/packages/client/src/components/atoms/BatteryIcon.tsx +++ b/packages/client/src/components/atoms/BatteryIcon.tsx @@ -1,7 +1,7 @@ -import { usePacket } from "@/contexts/PacketContext"; +import { usePacketStore } from "@/stores/usePacket"; function BatteryIconComponent() { - const { currentPacket } = usePacket(); + const { currentPacket } = usePacketStore(); const batteryLevel = currentPacket.Battery.PackStateOfCharge; return ( diff --git a/packages/client/src/components/atoms/SpeedAtom.tsx b/packages/client/src/components/atoms/SpeedAtom.tsx index b9be92be..f034482e 100644 --- a/packages/client/src/components/atoms/SpeedAtom.tsx +++ b/packages/client/src/components/atoms/SpeedAtom.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { usePacket } from "@/contexts/PacketContext"; import useUnitsHandler from "@/hooks/PIS/useUnitsHandler"; import { UnitType } from "@/objects/PIS/PIS.interface"; +import { usePacketStore } from "@/stores/usePacket"; import { calculateVehicleVelocity } from "@shared/helios-types"; function SpeedAtom() { - const { currentPacket } = usePacket(); + const { currentPacket } = usePacketStore(); const speedValue = React.useMemo( () => diff --git a/packages/client/src/components/atoms/ThrottleIcon.tsx b/packages/client/src/components/atoms/ThrottleIcon.tsx index 0bd5b5b2..8d62e55a 100644 --- a/packages/client/src/components/atoms/ThrottleIcon.tsx +++ b/packages/client/src/components/atoms/ThrottleIcon.tsx @@ -1,7 +1,7 @@ -import { usePacket } from "@/contexts/PacketContext"; +import { usePacketStore } from "@/stores/usePacket"; function ThrottleIcon() { - const { currentPacket } = usePacket(); + const { currentPacket } = usePacketStore(); const gasPos = currentPacket.B3.Acceleration; const regenPos = currentPacket.B3.RegenBraking; diff --git a/packages/client/src/components/containers/MapContainer.tsx b/packages/client/src/components/containers/MapContainer.tsx index 28aafb79..ad4818a8 100644 --- a/packages/client/src/components/containers/MapContainer.tsx +++ b/packages/client/src/components/containers/MapContainer.tsx @@ -3,7 +3,7 @@ import { type JSX, useEffect, useState } from "react"; import Map from "@/components/molecules/MapMolecules/Map"; import MapText from "@/components/molecules/MapMolecules/MapText"; import { useAppState } from "@/contexts/AppStateContext"; -import { usePacket } from "@/contexts/PacketContext"; +import { usePacketStore } from "@/stores/usePacket"; import { Coords } from "@shared/helios-types"; import { GEO_DATA } from "../molecules/MapMolecules/MapSetup"; @@ -18,7 +18,7 @@ const startingLocation: Coords = { }; function MapContainer(): JSX.Element { const { currentAppState } = useAppState(); - const { currentPacket } = usePacket(); + const { currentPacket } = usePacketStore(); const isDemo = currentAppState.connectionType === "DEMO"; const [carLocation, setCarLocation] = useState( diff --git a/packages/client/src/components/global/LapDataListenerManager.tsx b/packages/client/src/components/global/LapDataListenerManager.tsx new file mode 100644 index 00000000..6fad53ec --- /dev/null +++ b/packages/client/src/components/global/LapDataListenerManager.tsx @@ -0,0 +1,42 @@ +import { useEffect } from "react"; + +import { CONNECTIONTYPES, useAppState } from "@/contexts/AppStateContext"; +import { useLapDataStore } from "@/stores/useLapData"; + +export function LapListenerManager(): React.ReactElement | null { + const { currentAppState } = useAppState(); + const { + attachNetworkListener, + clearLapData, + detachNetworkListener, + fetchLapData, + } = useLapDataStore(); + + useEffect(() => { + // Always fetch initial lap data when manager mounts + fetchLapData(); + + if (currentAppState.playbackSwitch) { + // playback mode: no listeners, just clear data if needed + clearLapData(); + detachNetworkListener(); + return; + } + + if (currentAppState.connectionType === CONNECTIONTYPES.NETWORK) { + attachNetworkListener(); + return () => { + detachNetworkListener(); + }; + } else if (currentAppState.connectionType === CONNECTIONTYPES.DEMO) { + clearLapData(); + } else if (currentAppState.connectionType === CONNECTIONTYPES.RADIO) { + } + + return () => { + detachNetworkListener(); + }; + }, [currentAppState.connectionType, currentAppState.playbackSwitch]); + + return null; +} diff --git a/packages/client/src/components/global/PacketListenerManager.tsx b/packages/client/src/components/global/PacketListenerManager.tsx new file mode 100644 index 00000000..f4d81d19 --- /dev/null +++ b/packages/client/src/components/global/PacketListenerManager.tsx @@ -0,0 +1,42 @@ +import { useEffect } from "react"; + +import { useAppState } from "@/contexts/AppStateContext"; +import { CONNECTIONTYPES } from "@/contexts/AppStateContext"; +import { usePacketStore } from "@/stores/usePacket"; + +export function PacketListenerManager(): React.ReactElement | null { + const { currentAppState } = useAppState(); + const { + attachNetworkListener, + detachNetworkListener, + startDemoMode, + stopDemoMode, + } = usePacketStore(); + + useEffect(() => { + if (currentAppState.playbackSwitch) { + // Nothing to attach + stopDemoMode(); + detachNetworkListener(); + return; + } + + if (currentAppState.connectionType === CONNECTIONTYPES.NETWORK) { + attachNetworkListener(); + return () => detachNetworkListener(); + } else if (currentAppState.connectionType === CONNECTIONTYPES.DEMO) { + startDemoMode(); + return () => stopDemoMode(); + } else if (currentAppState.connectionType === CONNECTIONTYPES.RADIO) { + // TODO: handle radio + } + + // Clean up on unmount + return () => { + stopDemoMode(); + detachNetworkListener(); + }; + }, [currentAppState.connectionType, currentAppState.playbackSwitch]); + + return null; +} diff --git a/packages/client/src/components/molecules/HeroMolecules/CarGraphicComponent.tsx b/packages/client/src/components/molecules/HeroMolecules/CarGraphicComponent.tsx index 25a379c8..5fdba85a 100644 --- a/packages/client/src/components/molecules/HeroMolecules/CarGraphicComponent.tsx +++ b/packages/client/src/components/molecules/HeroMolecules/CarGraphicComponent.tsx @@ -10,7 +10,8 @@ import type { IndicationLocations, } from "@/components/molecules/HeroMolecules/HeroTypes"; import { ISeverity } from "@/components/molecules/HeroMolecules/HeroTypes"; -import { usePacket } from "@/contexts/PacketContext"; +import { useAppState } from "@/contexts/AppStateContext"; +import { usePacketStore } from "@/stores/usePacket"; import { ContactShadows, OrbitControls } from "@react-three/drei"; import { Canvas } from "@react-three/fiber"; import { calculateVehicleVelocity } from "@shared/helios-types"; @@ -25,7 +26,8 @@ const targetIntensity = 0.8; const duration = 500; const CarGraphicComponent = () => { - const { currentPacket } = usePacket(); + const { currentPacket } = usePacketStore(); + const { currentAppState } = useAppState(); const [isClear, changeClear] = useState(false); const [indications, setIndications] = useState({ battery: ISeverity.CLEAR, diff --git a/packages/client/src/components/molecules/HeroMolecules/GearParkBrakeComponent.tsx b/packages/client/src/components/molecules/HeroMolecules/GearParkBrakeComponent.tsx index 32b5bd35..937b64d9 100644 --- a/packages/client/src/components/molecules/HeroMolecules/GearParkBrakeComponent.tsx +++ b/packages/client/src/components/molecules/HeroMolecules/GearParkBrakeComponent.tsx @@ -1,8 +1,8 @@ import ParkingBrakeIcon from "@/components/atoms/ParkingBreakIcon"; -import { usePacket } from "@/contexts/PacketContext"; +import { usePacketStore } from "@/stores/usePacket"; function GearParkBrakeComponent() { - const { currentPacket } = usePacket(); + const { currentPacket } = usePacketStore(); const selectedCSS = "font-bold text-helios"; const reverse = currentPacket.B3.ReverseDigital; const forward = !currentPacket.B3.ReverseDigital; diff --git a/packages/client/src/components/molecules/LogoStatusMolecules/PlaybackDatePicker.tsx b/packages/client/src/components/molecules/LogoStatusMolecules/PlaybackDatePicker.tsx index 00704b12..7082f73e 100644 --- a/packages/client/src/components/molecules/LogoStatusMolecules/PlaybackDatePicker.tsx +++ b/packages/client/src/components/molecules/LogoStatusMolecules/PlaybackDatePicker.tsx @@ -2,7 +2,7 @@ import axios from "axios"; import React, { useEffect, useState } from "react"; import { useAppState } from "@/contexts/AppStateContext"; -import { usePlaybackContext } from "@/contexts/PlayBackContext"; +import { usePlaybackStore } from "@/stores/usePlayback"; import { notifications } from "@mantine/notifications"; import CalendarMonthIcon from "@mui/icons-material/CalendarMonth"; import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined"; @@ -108,7 +108,10 @@ function PlaybackDatePicker() { const [confirmedPlaybackDateTime, setConfirmedPlaybackDateTime] = useState(playbackDateTime); - const { playbackData, setPlaybackData } = usePlaybackContext(); + const { playbackData, setPlaybackData } = usePlaybackStore((state) => ({ + playbackData: state.playbackData, + setPlaybackData: state.setPlaybackData, + })); const fetchPlaybackData = async () => { if ( diff --git a/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx b/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx index 536c6362..5daf3e1f 100644 --- a/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx +++ b/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx @@ -6,8 +6,7 @@ import CarIcon from "@/components/atoms/CarIcon"; import LatencyDotsIcon from "@/components/atoms/LatencyDotsIcon"; import UserComputerIcon from "@/components/atoms/UserComputerIcon"; import { CONNECTIONTYPES, useAppState } from "@/contexts/AppStateContext"; -import { usePacket } from "@/contexts/PacketContext"; -import { helios } from "@/styles/colors"; +import { usePacketStore } from "@/stores/usePacket"; import { Switch } from "@mantine/core"; function PlaybackPickerComponent() { @@ -33,9 +32,8 @@ function PlaybackPickerComponent() { } function StatusComponent() { - const { resolvedTheme } = useTheme(); - const { currentAppState, setCurrentAppState } = useAppState(); - const { currentPacket } = usePacket(); + const { currentAppState } = useAppState(); + const { currentPacket } = usePacketStore(); const userConnection = currentAppState.socketConnected; // TODO: change carConnection from socketIO.connected to carConnection.connected const carConnection = currentAppState.socketConnected; diff --git a/packages/client/src/components/tabs/RaceTab.tsx b/packages/client/src/components/tabs/RaceTab.tsx index d28a7953..f9cdc4ef 100644 --- a/packages/client/src/components/tabs/RaceTab.tsx +++ b/packages/client/src/components/tabs/RaceTab.tsx @@ -2,10 +2,7 @@ import axios from "axios"; import { useTheme } from "next-themes"; import React, { useCallback, useEffect, useState } from "react"; -import { columns } from "@/components/config/lapTableConfig"; -import ColumnFilters from "@/components/molecules/RaceTabMolecules/ColumnFilters"; -import DriverFilter from "@/components/molecules/RaceTabMolecules/DriverFilter"; -import RaceTabTable from "@/components/molecules/RaceTabMolecules/RaceTabTable"; +import { useAppState } from "@/contexts/AppStateContext"; import { useLapData } from "@/contexts/LapDataContext"; import { notifications } from "@mantine/notifications"; import { SelectChangeEvent } from "@mui/material/Select"; @@ -37,7 +34,7 @@ function RaceTab() { const [Rfid, setDriverRFID] = useState(""); const [driverData, setDriverData] = useState([]); const [copy, setCopy] = useState(0); - const { formatLapData, lapData } = useLapData(); + const { fetchLapData, formatLapData, lapData } = useLapDataStore(); const [filteredLaps, setFilteredLaps] = useState(lapData); const [sorting, setSorting] = useState([ @@ -121,6 +118,7 @@ function RaceTab() { // fetching driver names when component mounts useEffect(() => { + fetchLapData(); fetchDriverNames() .then((response) => { const driverData = response.data.map((driver: IDriverData) => ({ diff --git a/packages/client/src/contexts/LapDataContext.tsx b/packages/client/src/contexts/LapDataContext.tsx deleted file mode 100644 index 20e4692c..00000000 --- a/packages/client/src/contexts/LapDataContext.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import axios from "axios"; -import { - JSX, - type PropsWithChildren, - createContext, - useCallback, - useContext, - useEffect, - useState, -} from "react"; - -import { CONNECTIONTYPES, useAppState } from "@/contexts/AppStateContext"; -import { socketIO } from "@/contexts/SocketContext"; -import { notifications } from "@mantine/notifications"; -import { IFormattedLapData, ILapData, prodURL } from "@shared/helios-types"; - -export const formatLapData = (lapPacket: ILapData): IFormattedLapData => ({ - Rfid: lapPacket.Rfid, - data: { - ampHours: parseFloat(lapPacket.data.ampHours.toFixed(2)), - averagePackCurrent: parseFloat( - lapPacket.data.averagePackCurrent.toFixed(2), - ), - averageSpeed: parseFloat(lapPacket.data.averageSpeed.toFixed(2)), - batterySecondsRemaining: parseFloat( - lapPacket.data.batterySecondsRemaining.toFixed(2), - ), - distance: parseFloat(lapPacket.data.distance.toFixed(2)), - energyConsumed: parseFloat(lapPacket.data.energyConsumed.toFixed(2)), - lapTime: parseFloat(lapPacket.data.lapTime.toFixed(2)), - netPowerOut: parseFloat(lapPacket.data.netPowerOut.toFixed(2)), - timeStamp: new Date(lapPacket.data.timeStamp).toLocaleString("en-US"), - totalPowerIn: parseFloat(lapPacket.data.totalPowerIn.toFixed(2)), - totalPowerOut: parseFloat(lapPacket.data.totalPowerOut.toFixed(2)), - }, - timestamp: lapPacket.timestamp, -}); - -interface ILapDataContextReturn { - lapData: IFormattedLapData[]; - formatLapData: (lapPacket: ILapData) => IFormattedLapData; -} - -const lapDataContext = createContext({ - formatLapData, - lapData: [], -}); - -export function LapDataContextProvider({ - children, -}: PropsWithChildren): JSX.Element { - const { currentAppState } = useAppState(); - const [lapData, setLapData] = useState([]); - - const onLapData = useCallback((lapPacket: ILapData) => { - const formattedData = formatLapData(lapPacket); - setLapData((prev) => [...prev, formattedData]); - }, []); - - const onLapComplete = useCallback(() => { - notifications.show({ - color: "green", - message: "A lap has been completed!", - title: "Lap Completion", - }); - }, []); - - const fetchLapData = useCallback(async () => { - try { - const response = await axios.get(`${prodURL}/laps`); - - if (!Array.isArray(response)) { - throw new Error("Invalid API response format"); - } - - setLapData(response.data.data.map(formatLapData)); - } catch (error) { - return { error: "Error fetching lap data" }; - } - }, []); - - useEffect(() => { - fetchLapData(); - }, [fetchLapData]); - - useEffect(() => { - if ( - currentAppState.connectionType === CONNECTIONTYPES.NETWORK && - !currentAppState.playbackSwitch - ) { - socketIO.on("lapData", onLapData); - socketIO.on("lapComplete", onLapComplete); - return () => { - socketIO.off("lapData", onLapData); - socketIO.off("lapComplete", onLapComplete); - }; - } - }, [ - currentAppState.connectionType, - currentAppState.playbackSwitch, - onLapData, - onLapComplete, - ]); - - return ( - - {children} - - ); -} - -export function useLapData(): ILapDataContextReturn { - return useContext(lapDataContext); -} diff --git a/packages/client/src/contexts/PacketContext.tsx b/packages/client/src/contexts/PacketContext.tsx deleted file mode 100644 index 5ef55a30..00000000 --- a/packages/client/src/contexts/PacketContext.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { - type JSX, - type PropsWithChildren, - createContext, - useContext, - useEffect, - useState, -} from "react"; - -import { CONNECTIONTYPES, useAppState } from "@/contexts/AppStateContext"; -import { socketIO } from "@/contexts/SocketContext"; -import { generateFakeTelemetryData } from "@shared/helios-types"; -import type { ITelemetryData } from "@shared/helios-types"; - -interface IPackContextReturn { - currentPacket: ITelemetryData; - setCurrentPacket: (packet: ITelemetryData) => void; -} - -const packetContext = createContext( - {} as IPackContextReturn, -); - -export function PacketContextProvider({ - children, -}: PropsWithChildren): JSX.Element { - const { currentAppState } = useAppState(); - - const [currentPacket, setCurrentPacket] = useState( - generateFakeTelemetryData() as ITelemetryData, - ); - - // Generate random data for local dev mode - function onPacket(packet: ITelemetryData) { - setCurrentPacket(packet); - } - useEffect(() => { - if (currentAppState.playbackSwitch) { - setCurrentPacket(generateFakeTelemetryData()); - return; - } - - if (currentAppState.connectionType === CONNECTIONTYPES.NETWORK) { - socketIO.on("packet", onPacket); - return () => { - socketIO.off("packet", onPacket); - }; - } else if (currentAppState.connectionType === CONNECTIONTYPES.DEMO) { - const interval = setInterval(() => { - setCurrentPacket(generateFakeTelemetryData()); - }, 500); - return () => clearInterval(interval); - } else if (currentAppState.connectionType === CONNECTIONTYPES.RADIO) { - // Radio connection - } - }, [currentAppState.connectionType, currentAppState.playbackSwitch]); - - return ( - - {children} - - ); -} - -export function usePacket(): IPackContextReturn { - return useContext(packetContext); -} diff --git a/packages/client/src/hooks/PIS/PIS.battery.tsx b/packages/client/src/hooks/PIS/PIS.battery.tsx index 12d07177..aff0a9d0 100644 --- a/packages/client/src/hooks/PIS/PIS.battery.tsx +++ b/packages/client/src/hooks/PIS/PIS.battery.tsx @@ -2,13 +2,13 @@ import { FaultLocations, ISeverity, } from "@/components/molecules/HeroMolecules/HeroTypes"; -import { usePacket } from "@/contexts/PacketContext"; import type I_PIS from "@/objects/PIS/PIS.interface"; import { type I_PISField } from "@/objects/PIS/PIS.interface"; import { UnitType } from "@/objects/PIS/PIS.interface"; +import { usePacketStore } from "@/stores/usePacket"; const Battery = (): I_PIS => { - const { currentPacket } = usePacket(); + const { currentPacket } = usePacketStore(); const { Battery } = currentPacket; // Battery will now have be split into the faults and the warning and here we will simply show what everything means const data = { diff --git a/packages/client/src/hooks/PIS/PIS.faults.tsx b/packages/client/src/hooks/PIS/PIS.faults.tsx index c77cebe1..751c9d14 100644 --- a/packages/client/src/hooks/PIS/PIS.faults.tsx +++ b/packages/client/src/hooks/PIS/PIS.faults.tsx @@ -2,11 +2,11 @@ import { FaultLocations, ISeverity, } from "@/components/molecules/HeroMolecules/HeroTypes"; -import { usePacket } from "@/contexts/PacketContext"; import type I_PIS from "@/objects/PIS/PIS.interface"; +import { usePacketStore } from "@/stores/usePacket"; const Faults = (): I_PIS => { - const { currentPacket } = usePacket(); + const { currentPacket } = usePacketStore(); const { BatteryFaults, Contactor, MBMS, MotorDetails0, MotorDetails1 } = currentPacket; const data = { diff --git a/packages/client/src/hooks/PIS/PIS.mbms.tsx b/packages/client/src/hooks/PIS/PIS.mbms.tsx index 3c72dd15..0f22e9a8 100644 --- a/packages/client/src/hooks/PIS/PIS.mbms.tsx +++ b/packages/client/src/hooks/PIS/PIS.mbms.tsx @@ -1,10 +1,10 @@ -import { usePacket } from "@/contexts/PacketContext"; import type I_PIS from "@/objects/PIS/PIS.interface"; import { type I_PISField } from "@/objects/PIS/PIS.interface"; import { UnitType } from "@/objects/PIS/PIS.interface"; +import { usePacketStore } from "@/stores/usePacket"; const Mbms = (): I_PIS => { - const { currentPacket } = usePacket(); + const { currentPacket } = usePacketStore(); const { Battery, Contactor, MBMS } = currentPacket; const data = { Contactor: [ diff --git a/packages/client/src/hooks/PIS/PIS.motor.tsx b/packages/client/src/hooks/PIS/PIS.motor.tsx index 9dc2dfa1..a9c4995c 100644 --- a/packages/client/src/hooks/PIS/PIS.motor.tsx +++ b/packages/client/src/hooks/PIS/PIS.motor.tsx @@ -1,11 +1,11 @@ /* eslint-disable sort-keys */ -import { usePacket } from "@/contexts/PacketContext"; import type I_PIS from "@/objects/PIS/PIS.interface"; import { type I_PISField } from "@/objects/PIS/PIS.interface"; import { UnitType } from "@/objects/PIS/PIS.interface"; +import { usePacketStore } from "@/stores/usePacket"; const Motor = (): I_PIS => { - const { currentPacket } = usePacket(); + const { currentPacket } = usePacketStore(); const { KeyMotor, MotorDetails0, MotorDetails1 } = currentPacket; return { KeyMotorDetails: [ diff --git a/packages/client/src/hooks/PIS/PIS.mppt.tsx b/packages/client/src/hooks/PIS/PIS.mppt.tsx index 010129b0..b6b4f0cc 100644 --- a/packages/client/src/hooks/PIS/PIS.mppt.tsx +++ b/packages/client/src/hooks/PIS/PIS.mppt.tsx @@ -1,9 +1,9 @@ -import { usePacket } from "@/contexts/PacketContext"; import type I_PIS from "@/objects/PIS/PIS.interface"; import { type I_PISField, UnitType } from "@/objects/PIS/PIS.interface"; +import { usePacketStore } from "@/stores/usePacket"; const MPPT = (): I_PIS => { - const { currentPacket } = usePacket(); + const { currentPacket } = usePacketStore(); const { MPPT } = currentPacket; const data = { Unit0: { diff --git a/packages/client/src/pages/[[...slug]].tsx b/packages/client/src/pages/[[...slug]].tsx index 600918fe..3d0b64f7 100644 --- a/packages/client/src/pages/[[...slug]].tsx +++ b/packages/client/src/pages/[[...slug]].tsx @@ -5,14 +5,13 @@ import MapContainer from "@/components/containers/MapContainer"; import TabsContainer from "@/components/containers/TabsContainer"; import PlaybackSlider from "@/components/molecules/PlaybackMolecules/PlaybackSlider"; import { useAppState } from "@/contexts/AppStateContext"; -import { PlaybackContextProvider } from "@/contexts/PlayBackContext"; export default function Home() { const { currentAppState } = useAppState(); return ( -
- +
+
@@ -38,7 +37,7 @@ export default function Home() { )}
- +
); } diff --git a/packages/client/src/pages/_app.tsx b/packages/client/src/pages/_app.tsx index e587a476..42b7c209 100644 --- a/packages/client/src/pages/_app.tsx +++ b/packages/client/src/pages/_app.tsx @@ -1,10 +1,10 @@ import { ThemeProvider } from "next-themes"; import type { AppProps } from "next/app"; +import { LapListenerManager } from "@/components/global/LapDataListenerManager"; import LoadingWrapper from "@/components/global/LoadingWrapper"; +import { PacketListenerManager } from "@/components/global/PacketListenerManager"; import { AppStateContextProvider } from "@/contexts/AppStateContext"; -import { LapDataContextProvider } from "@/contexts/LapDataContext"; -import { PacketContextProvider } from "@/contexts/PacketContext"; import { SocketContextProvider } from "@/contexts/SocketContext"; import "@/styles/globals.css"; import { MantineProvider } from "@mantine/core"; @@ -20,13 +20,11 @@ export default function App({ Component, pageProps }: AppProps) { - - - - - - - + + + + + diff --git a/packages/client/src/stores/useLapData.ts b/packages/client/src/stores/useLapData.ts new file mode 100644 index 00000000..c9427e40 --- /dev/null +++ b/packages/client/src/stores/useLapData.ts @@ -0,0 +1,85 @@ +import axios from "axios"; +import { create } from "zustand"; + +import { socketIO } from "@/contexts/SocketContext"; +import { notifications } from "@mantine/notifications"; +import { IFormattedLapData, ILapData, prodURL } from "@shared/helios-types"; + +export const formatLapData = (lapPacket: ILapData): IFormattedLapData => ({ + Rfid: lapPacket.Rfid, + data: { + ampHours: parseFloat(lapPacket.data.ampHours.toFixed(2)), + averagePackCurrent: parseFloat( + lapPacket.data.averagePackCurrent.toFixed(2), + ), + averageSpeed: parseFloat(lapPacket.data.averageSpeed.toFixed(2)), + batterySecondsRemaining: parseFloat( + lapPacket.data.batterySecondsRemaining.toFixed(2), + ), + distance: parseFloat(lapPacket.data.distance.toFixed(2)), + energyConsumed: parseFloat(lapPacket.data.energyConsumed.toFixed(2)), + lapTime: parseFloat(lapPacket.data.lapTime.toFixed(2)), + netPowerOut: parseFloat(lapPacket.data.netPowerOut.toFixed(2)), + timeStamp: new Date(lapPacket.data.timeStamp).toLocaleString("en-US"), + totalPowerIn: parseFloat(lapPacket.data.totalPowerIn.toFixed(2)), + totalPowerOut: parseFloat(lapPacket.data.totalPowerOut.toFixed(2)), + }, + timestamp: lapPacket.timestamp, +}); + +interface LapDataState { + attachNetworkListener: () => void; + detachNetworkListener: () => void; + lapData: IFormattedLapData[]; + formatLapData: (lapPacket: ILapData) => IFormattedLapData; + onLapData: (lapPacket: ILapData) => void; + onLapComplete: () => void; + fetchLapData: () => Promise; + clearLapData: () => void; +} + +export const useLapDataStore = create((set, get) => ({ + attachNetworkListener: () => { + socketIO.on("lapData", get().onLapData); + socketIO.on("lapComplete", get().onLapComplete); + }, + clearLapData: () => set({ lapData: [] }), + detachNetworkListener: () => { + socketIO.off("lapData", get().onLapData); + socketIO.off("lapComplete", get().onLapComplete); + }, + fetchLapData: async () => { + try { + const response = await axios.get(`${prodURL}/laps`); + + if (!Array.isArray(response.data?.data)) { + throw new Error("Invalid API response format"); + } + + set({ + lapData: response.data.data.map(formatLapData), + }); + } catch (error) { + notifications.show({ + color: "red", + message: "Failed to fetch lap data from the server.", + title: "Error", + }); + } + }, + formatLapData, + lapData: [], + onLapComplete: () => { + notifications.show({ + color: "green", + message: "A lap has been completed!", + title: "Lap Completion", + }); + }, + onLapData: (lapPacket: ILapData) => { + const formattedData = formatLapData(lapPacket); + set((state) => ({ + lapData: [...state.lapData, formattedData], + })); + }, +})); diff --git a/packages/client/src/stores/usePacket.ts b/packages/client/src/stores/usePacket.ts new file mode 100644 index 00000000..27e25470 --- /dev/null +++ b/packages/client/src/stores/usePacket.ts @@ -0,0 +1,42 @@ +import { create } from "zustand"; + +import { socketIO } from "@/contexts/SocketContext"; +import { generateFakeTelemetryData } from "@shared/helios-types"; +import type { ITelemetryData } from "@shared/helios-types"; + +interface IPacketStore { + currentPacket: ITelemetryData; + setCurrentPacket: (packet: ITelemetryData) => void; + startDemoMode: () => void; + stopDemoMode: () => void; + attachNetworkListener: () => void; + detachNetworkListener: () => void; +} + +let demoInterval: NodeJS.Timeout | null = null; + +const onPacket = (packet: ITelemetryData) => { + usePacketStore.getState().setCurrentPacket(packet); +}; + +export const usePacketStore = create((set) => ({ + attachNetworkListener: () => { + socketIO.on("packet", onPacket); + }, + currentPacket: generateFakeTelemetryData(), + detachNetworkListener: () => { + socketIO.off("packet", onPacket); + }, + setCurrentPacket: (packet) => set({ currentPacket: packet }), + startDemoMode: () => { + demoInterval = setInterval(() => { + set({ currentPacket: generateFakeTelemetryData() }); + }, 500); + }, + stopDemoMode: () => { + if (demoInterval) { + clearInterval(demoInterval); + demoInterval = null; + } + }, +})); From db00fa6e2f6a4aa216cc854838cfd638c016fecc Mon Sep 17 00:00:00 2001 From: alexwhelan12 Date: Wed, 1 Oct 2025 20:12:19 -0600 Subject: [PATCH 2/4] Converted contexts to zustand stores --- .../containers/BottomInformationContainer.tsx | 2 +- .../src/components/containers/MLContainer.tsx | 1 + .../components/containers/MapContainer.tsx | 2 +- .../global/AppStateEffectsManager.tsx | 120 +++++++++ .../src/components/global/EffectsProvider.tsx | 13 + .../global/LapDataListenerManager.tsx | 63 +++-- .../client/src/components/global/Loading.tsx | 2 +- .../src/components/global/LoadingWrapper.tsx | 160 ++++++------ .../global/PacketListenerManager.tsx | 68 ++++-- .../HeroMolecules/CarGraphicComponent.tsx | 2 +- .../DataPickerMolecules/DatePickerColumn.tsx | 2 +- .../PlaybackDatePicker.tsx | 2 +- .../LogoStatusMolecules/SettingsComponent.tsx | 7 +- .../LogoStatusMolecules/StatusComponent.tsx | 2 +- .../components/molecules/MapMolecules/Map.tsx | 1 + .../molecules/MapMolecules/MapControls.tsx | 1 + .../molecules/MapMolecules/MapText.tsx | 2 +- .../PlaybackMolecules/PlaybackSlider.tsx | 10 +- .../src/components/tabs/AnalysisTab.tsx | 2 +- .../client/src/components/tabs/RaceTab.tsx | 4 +- .../transformers/PISTransformer.tsx | 2 +- .../client/src/contexts/AppStateContext.tsx | 231 ------------------ .../client/src/contexts/PlayBackContext.tsx | 39 --- .../client/src/contexts/SocketContext.tsx | 2 +- .../client/src/hooks/PIS/useUnitsHandler.tsx | 2 +- packages/client/src/pages/[[...slug]].tsx | 2 +- packages/client/src/pages/_app.tsx | 30 +-- packages/client/src/stores/useAppState.ts | 76 ++++++ packages/client/src/stores/useLapData.ts | 43 +--- packages/client/src/stores/usePacket.ts | 28 --- packages/client/src/stores/usePlayback.ts | 13 + 31 files changed, 443 insertions(+), 491 deletions(-) create mode 100644 packages/client/src/components/global/AppStateEffectsManager.tsx create mode 100644 packages/client/src/components/global/EffectsProvider.tsx delete mode 100644 packages/client/src/contexts/AppStateContext.tsx delete mode 100644 packages/client/src/contexts/PlayBackContext.tsx create mode 100644 packages/client/src/stores/useAppState.ts create mode 100644 packages/client/src/stores/usePlayback.ts diff --git a/packages/client/src/components/containers/BottomInformationContainer.tsx b/packages/client/src/components/containers/BottomInformationContainer.tsx index 62fef3c8..8778b39a 100644 --- a/packages/client/src/components/containers/BottomInformationContainer.tsx +++ b/packages/client/src/components/containers/BottomInformationContainer.tsx @@ -1,9 +1,9 @@ import { useCallback } from "react"; -import { useAppState } from "@/contexts/AppStateContext"; import usePIS from "@/hooks/PIS/usePIS"; import { useFavouriteLookupTable } from "@/hooks/favouriteLookupTable"; import type I_PIS from "@/objects/PIS/PIS.interface"; +import { useAppState } from "@/stores/useAppState"; function BottomInformationContainer() { const { currentAppState, setCurrentAppState } = useAppState(); diff --git a/packages/client/src/components/containers/MLContainer.tsx b/packages/client/src/components/containers/MLContainer.tsx index f7df786c..df580d46 100644 --- a/packages/client/src/components/containers/MLContainer.tsx +++ b/packages/client/src/components/containers/MLContainer.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from "react"; import type { PlotParams } from "react-plotly.js"; import useWindowDimensions from "@/hooks/PIS/useWindowDimensions"; +import { useAppState } from "@/stores/useAppState"; import Plotly from "./Plotly"; diff --git a/packages/client/src/components/containers/MapContainer.tsx b/packages/client/src/components/containers/MapContainer.tsx index ad4818a8..e5a054fd 100644 --- a/packages/client/src/components/containers/MapContainer.tsx +++ b/packages/client/src/components/containers/MapContainer.tsx @@ -2,7 +2,7 @@ import { type JSX, useEffect, useState } from "react"; import Map from "@/components/molecules/MapMolecules/Map"; import MapText from "@/components/molecules/MapMolecules/MapText"; -import { useAppState } from "@/contexts/AppStateContext"; +import { useAppState } from "@/stores/useAppState"; import { usePacketStore } from "@/stores/usePacket"; import { Coords } from "@shared/helios-types"; diff --git a/packages/client/src/components/global/AppStateEffectsManager.tsx b/packages/client/src/components/global/AppStateEffectsManager.tsx new file mode 100644 index 00000000..61feaa7e --- /dev/null +++ b/packages/client/src/components/global/AppStateEffectsManager.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { useCallback, useEffect } from "react"; + +import { CONNECTIONTYPES, useAppState } from "@/stores/useAppState"; + +export default function AppStateEffects() { + const { currentAppState, setCurrentAppState } = useAppState(); + + useEffect(() => { + if (currentAppState.connectionType === CONNECTIONTYPES.DEMO) { + if (currentAppState.socketConnected) { + setCurrentAppState((prev) => ({ + ...prev, + connectionType: CONNECTIONTYPES.NETWORK, + loading: false, + })); + } + if (currentAppState.radioConnected) { + setCurrentAppState((prev) => ({ + ...prev, + connectionType: CONNECTIONTYPES.RADIO, + loading: false, + })); + } + } + if (currentAppState.connectionType === CONNECTIONTYPES.NETWORK) { + setCurrentAppState((prev) => ({ + ...prev, + loading: !currentAppState.socketConnected, + })); + } + if (currentAppState.connectionType === CONNECTIONTYPES.RADIO) { + setCurrentAppState((prev) => ({ + ...prev, + loading: !currentAppState.radioConnected, + })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + currentAppState.socketConnected, + currentAppState.radioConnected, + setCurrentAppState, + ]); + + useEffect(() => { + setTimeout(() => { + setCurrentAppState((prev) => ({ + ...prev, + loading: false, + })); + }, 5000); + }, [setCurrentAppState]); + + const fetchSettingsFromLocalStorage = useCallback(() => { + const savedSettings = localStorage.getItem("settings"); + const favourites = localStorage.getItem("favourites"); + + if (savedSettings) { + const parsedSettings = JSON.parse(savedSettings); + + const parsedFavourites = favourites + ? (JSON.parse(favourites) as string[]) + : [ + "Motor Temp", + "Battery Cell Voltage", + "Vehicle Velocity", + "Pack Voltage", + "Pack Current", + "Battery Average Voltage", + ]; + + const hasPlaybackDateTime = !!parsedSettings.playbackDateTime; + + const parsedPlaybackDateTime = hasPlaybackDateTime + ? { + date: parsedSettings.playbackDateTime!.date + ? new Date(parsedSettings.playbackDateTime!.date) + : null, + endTime: parsedSettings.playbackDateTime!.endTime + ? new Date(parsedSettings.playbackDateTime!.endTime) + : null, + startTime: parsedSettings.playbackDateTime!.startTime + ? new Date(parsedSettings.playbackDateTime!.startTime) + : null, + } + : { + date: null, + endTime: null, + startTime: null, + }; + + setCurrentAppState((prev) => ({ + ...prev, + appUnits: parsedSettings.appUnits ?? prev.appUnits, + connectionType: parsedSettings.connectionType ?? prev.connectionType, + darkMode: parsedSettings.darkMode ?? prev.darkMode, + favourites: parsedFavourites, + lapCoords: parsedSettings.lapCoords ?? prev.lapCoords, + playbackDateTime: parsedPlaybackDateTime, + })); + } + }, [setCurrentAppState]); + + const saveSettingsToLocalStorage = useCallback(() => { + localStorage.setItem("settings", JSON.stringify(currentAppState)); + }, [currentAppState]); + + useEffect(() => { + fetchSettingsFromLocalStorage(); + }, [fetchSettingsFromLocalStorage]); + + useEffect(() => { + if (!currentAppState.loading) { + saveSettingsToLocalStorage(); + } + }, [currentAppState.loading, saveSettingsToLocalStorage]); + + return null; // This component only handles side effects +} diff --git a/packages/client/src/components/global/EffectsProvider.tsx b/packages/client/src/components/global/EffectsProvider.tsx new file mode 100644 index 00000000..ab54a3f0 --- /dev/null +++ b/packages/client/src/components/global/EffectsProvider.tsx @@ -0,0 +1,13 @@ +import AppStateEffectsManager from "@/components/global/AppStateEffectsManager"; +import { LapListenerManager } from "@/components/global/LapDataListenerManager"; +import { PacketListenerManager } from "@/components/global/PacketListenerManager"; + +export function EffectsProvider() { + return ( + <> + + + + + ); +} diff --git a/packages/client/src/components/global/LapDataListenerManager.tsx b/packages/client/src/components/global/LapDataListenerManager.tsx index 6fad53ec..fb8c953c 100644 --- a/packages/client/src/components/global/LapDataListenerManager.tsx +++ b/packages/client/src/components/global/LapDataListenerManager.tsx @@ -1,42 +1,67 @@ import { useEffect } from "react"; -import { CONNECTIONTYPES, useAppState } from "@/contexts/AppStateContext"; -import { useLapDataStore } from "@/stores/useLapData"; +import { socketIO } from "@/contexts/SocketContext"; +import { CONNECTIONTYPES, useAppState } from "@/stores/useAppState"; +import { formatLapData, useLapDataStore } from "@/stores/useLapData"; +import { notifications } from "@mantine/notifications"; +import type { ILapData } from "@shared/helios-types"; export function LapListenerManager(): React.ReactElement | null { const { currentAppState } = useAppState(); - const { - attachNetworkListener, - clearLapData, - detachNetworkListener, - fetchLapData, - } = useLapDataStore(); + const { addLapData, clearLapData, fetchLapData } = useLapDataStore(); + // Fetch initial lap data when manager mounts useEffect(() => { - // Always fetch initial lap data when manager mounts fetchLapData(); + }, [fetchLapData]); + // Handle connection type changes + useEffect(() => { + // Playback mode: no listeners, clear data if (currentAppState.playbackSwitch) { - // playback mode: no listeners, just clear data if needed clearLapData(); - detachNetworkListener(); return; } + // Network mode: attach socket listeners if (currentAppState.connectionType === CONNECTIONTYPES.NETWORK) { - attachNetworkListener(); + const handleLapData = (lapPacket: ILapData) => { + const formattedData = formatLapData(lapPacket); + addLapData(formattedData); + }; + + const handleLapComplete = () => { + notifications.show({ + color: "green", + message: "A lap has been completed!", + title: "Lap Completion", + }); + }; + + socketIO.on("lapData", handleLapData); + socketIO.on("lapComplete", handleLapComplete); + return () => { - detachNetworkListener(); + socketIO.off("lapData", handleLapData); + socketIO.off("lapComplete", handleLapComplete); }; - } else if (currentAppState.connectionType === CONNECTIONTYPES.DEMO) { + } + + // Demo mode: clear existing lap data + else if (currentAppState.connectionType === CONNECTIONTYPES.DEMO) { clearLapData(); - } else if (currentAppState.connectionType === CONNECTIONTYPES.RADIO) { } - return () => { - detachNetworkListener(); - }; - }, [currentAppState.connectionType, currentAppState.playbackSwitch]); + // Radio mode: TODO + else if (currentAppState.connectionType === CONNECTIONTYPES.RADIO) { + // TODO: handle radio + } + }, [ + currentAppState.connectionType, + currentAppState.playbackSwitch, + addLapData, + clearLapData, + ]); return null; } diff --git a/packages/client/src/components/global/Loading.tsx b/packages/client/src/components/global/Loading.tsx index 98709e5a..7cfb9e79 100644 --- a/packages/client/src/components/global/Loading.tsx +++ b/packages/client/src/components/global/Loading.tsx @@ -3,7 +3,7 @@ import { memo, useMemo } from "react"; import { twMerge } from "tailwind-merge"; import { LOADINGSTAGES } from "@/components/global/LoadingWrapper"; -import { useAppState } from "@/contexts/AppStateContext"; +import { useAppState } from "@/stores/useAppState"; const Loading = (props: { currentLoadingState: LOADINGSTAGES }) => { const { currentAppState } = useAppState(); diff --git a/packages/client/src/components/global/LoadingWrapper.tsx b/packages/client/src/components/global/LoadingWrapper.tsx index 778dbfce..cd80df9d 100644 --- a/packages/client/src/components/global/LoadingWrapper.tsx +++ b/packages/client/src/components/global/LoadingWrapper.tsx @@ -1,80 +1,80 @@ -import { type PropsWithChildren, memo, useEffect, useState } from "react"; - -import Loading from "@/components/global/Loading"; -import { useAppState } from "@/contexts/AppStateContext"; - -export enum LOADINGSTAGES { - DRIVE_IN = 1, - PENDING = 2, - READY = 3, -} - -const LoadingWrapper = ({ children }: PropsWithChildren) => { - const { currentAppState, setCurrentAppState } = useAppState(); - const [currentLoadingState, setCurrentLoadingStage] = useState( - LOADINGSTAGES.DRIVE_IN, - ); - - useEffect(() => { - let driveInTimeout: NodeJS.Timeout; - let pendingTimeout: NodeJS.Timeout; - // let readyTimeout: NodeJS.Timeout; - let confirmTimeout: NodeJS.Timeout; - - // If loading is true again after loading previously finished, restart loading by setting to drive in - if ( - currentAppState.loading && - currentLoadingState === LOADINGSTAGES.READY - ) { - setCurrentLoadingStage(LOADINGSTAGES.DRIVE_IN); - setCurrentAppState((prev) => ({ - ...prev, - displayLoading: true, - })); - } - - // Switch to drive off after app state reports loading is complete and minimum animation time is fulfilled - if (currentLoadingState === LOADINGSTAGES.DRIVE_IN) { - driveInTimeout = setTimeout(() => { - setCurrentLoadingStage(LOADINGSTAGES.PENDING); - }, 1000); - } - - // Once site is ready, delay loader for 3 additional seconds and then transition to driving off screen - if (!currentAppState.loading) { - pendingTimeout = setTimeout(() => { - setCurrentLoadingStage(LOADINGSTAGES.READY); - }, 3000); - } - - // Confirm with App State that car driving off screen animation is fulfilled before hiding loader - if (currentLoadingState === LOADINGSTAGES.READY) { - confirmTimeout = setTimeout(() => { - setCurrentAppState((prev) => ({ - ...prev, - displayLoading: false, - })); - }, 800); - } - - return () => { - clearTimeout(driveInTimeout); - // clearTimeout(readyTimeout); - clearTimeout(pendingTimeout); - clearTimeout(confirmTimeout); - }; - // TODO: Adding these makes the timeouts clear before they run since the app state is constantly rerendered @brian-ngyn - }, [currentAppState.loading, currentLoadingState, setCurrentAppState]); - - return ( -
- {currentAppState.displayLoading ? ( - - ) : ( - <>{children} - )}{" "} -
- ); -}; - -export default memo(LoadingWrapper); +import { type PropsWithChildren, memo, useEffect, useState } from "react"; + +import Loading from "@/components/global/Loading"; +import { useAppState } from "@/stores/useAppState"; + +export enum LOADINGSTAGES { + DRIVE_IN = 1, + PENDING = 2, + READY = 3, +} + +const LoadingWrapper = ({ children }: PropsWithChildren) => { + const { currentAppState, setCurrentAppState } = useAppState(); + const [currentLoadingState, setCurrentLoadingStage] = useState( + LOADINGSTAGES.DRIVE_IN, + ); + + useEffect(() => { + let driveInTimeout: NodeJS.Timeout; + let pendingTimeout: NodeJS.Timeout; + // let readyTimeout: NodeJS.Timeout; + let confirmTimeout: NodeJS.Timeout; + + // If loading is true again after loading previously finished, restart loading by setting to drive in + if ( + currentAppState.loading && + currentLoadingState === LOADINGSTAGES.READY + ) { + setCurrentLoadingStage(LOADINGSTAGES.DRIVE_IN); + setCurrentAppState((prev) => ({ + ...prev, + displayLoading: true, + })); + } + + // Switch to drive off after app state reports loading is complete and minimum animation time is fulfilled + if (currentLoadingState === LOADINGSTAGES.DRIVE_IN) { + driveInTimeout = setTimeout(() => { + setCurrentLoadingStage(LOADINGSTAGES.PENDING); + }, 1000); + } + + // Once site is ready, delay loader for 3 additional seconds and then transition to driving off screen + if (!currentAppState.loading) { + pendingTimeout = setTimeout(() => { + setCurrentLoadingStage(LOADINGSTAGES.READY); + }, 3000); + } + + // Confirm with App State that car driving off screen animation is fulfilled before hiding loader + if (currentLoadingState === LOADINGSTAGES.READY) { + confirmTimeout = setTimeout(() => { + setCurrentAppState((prev) => ({ + ...prev, + displayLoading: false, + })); + }, 800); + } + + return () => { + clearTimeout(driveInTimeout); + // clearTimeout(readyTimeout); + clearTimeout(pendingTimeout); + clearTimeout(confirmTimeout); + }; + // TODO: Adding these makes the timeouts clear before they run since the app state is constantly rerendered @brian-ngyn + }, [currentAppState.loading, currentLoadingState, setCurrentAppState]); + + return ( +
+ {currentAppState.displayLoading ? ( + + ) : ( + <>{children} + )}{" "} +
+ ); +}; + +export default memo(LoadingWrapper); diff --git a/packages/client/src/components/global/PacketListenerManager.tsx b/packages/client/src/components/global/PacketListenerManager.tsx index f4d81d19..b45e3e56 100644 --- a/packages/client/src/components/global/PacketListenerManager.tsx +++ b/packages/client/src/components/global/PacketListenerManager.tsx @@ -1,42 +1,66 @@ -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; -import { useAppState } from "@/contexts/AppStateContext"; -import { CONNECTIONTYPES } from "@/contexts/AppStateContext"; +import { socketIO } from "@/contexts/SocketContext"; +import { CONNECTIONTYPES, useAppState } from "@/stores/useAppState"; import { usePacketStore } from "@/stores/usePacket"; +import { generateFakeTelemetryData } from "@shared/helios-types"; +import type { ITelemetryData } from "@shared/helios-types"; export function PacketListenerManager(): React.ReactElement | null { const { currentAppState } = useAppState(); - const { - attachNetworkListener, - detachNetworkListener, - startDemoMode, - stopDemoMode, - } = usePacketStore(); + const { setCurrentPacket } = usePacketStore(); + const demoIntervalRef = useRef(null); useEffect(() => { + // Clean up any existing listeners/intervals + const cleanup = () => { + if (demoIntervalRef.current) { + clearInterval(demoIntervalRef.current); + demoIntervalRef.current = null; + } + }; + + // If playback mode is on, don't attach anything if (currentAppState.playbackSwitch) { - // Nothing to attach - stopDemoMode(); - detachNetworkListener(); + cleanup(); return; } + // Handle different connection types if (currentAppState.connectionType === CONNECTIONTYPES.NETWORK) { - attachNetworkListener(); - return () => detachNetworkListener(); + // Attach network listener + const handlePacket = (packet: ITelemetryData) => { + setCurrentPacket(packet); + }; + + socketIO.on("packet", handlePacket); + + return () => { + socketIO.off("packet", handlePacket); + }; } else if (currentAppState.connectionType === CONNECTIONTYPES.DEMO) { - startDemoMode(); - return () => stopDemoMode(); + // Start demo mode + demoIntervalRef.current = setInterval(() => { + setCurrentPacket(generateFakeTelemetryData()); + }, 500); + + return () => { + if (demoIntervalRef.current) { + clearInterval(demoIntervalRef.current); + demoIntervalRef.current = null; + } + }; } else if (currentAppState.connectionType === CONNECTIONTYPES.RADIO) { // TODO: handle radio } - // Clean up on unmount - return () => { - stopDemoMode(); - detachNetworkListener(); - }; - }, [currentAppState.connectionType, currentAppState.playbackSwitch]); + // Cleanup on unmount or mode change + return cleanup; + }, [ + currentAppState.connectionType, + currentAppState.playbackSwitch, + setCurrentPacket, + ]); return null; } diff --git a/packages/client/src/components/molecules/HeroMolecules/CarGraphicComponent.tsx b/packages/client/src/components/molecules/HeroMolecules/CarGraphicComponent.tsx index 5fdba85a..2e2b291c 100644 --- a/packages/client/src/components/molecules/HeroMolecules/CarGraphicComponent.tsx +++ b/packages/client/src/components/molecules/HeroMolecules/CarGraphicComponent.tsx @@ -10,7 +10,7 @@ import type { IndicationLocations, } from "@/components/molecules/HeroMolecules/HeroTypes"; import { ISeverity } from "@/components/molecules/HeroMolecules/HeroTypes"; -import { useAppState } from "@/contexts/AppStateContext"; +import { useAppState } from "@/stores/useAppState"; import { usePacketStore } from "@/stores/usePacket"; import { ContactShadows, OrbitControls } from "@react-three/drei"; import { Canvas } from "@react-three/fiber"; diff --git a/packages/client/src/components/molecules/LogoStatusMolecules/DataPickerMolecules/DatePickerColumn.tsx b/packages/client/src/components/molecules/LogoStatusMolecules/DataPickerMolecules/DatePickerColumn.tsx index 77cf7907..e0b997a5 100644 --- a/packages/client/src/components/molecules/LogoStatusMolecules/DataPickerMolecules/DatePickerColumn.tsx +++ b/packages/client/src/components/molecules/LogoStatusMolecules/DataPickerMolecules/DatePickerColumn.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { useAppState } from "@/contexts/AppStateContext"; +import { useAppState } from "@/stores/useAppState"; import { DatePicker, TimeInput } from "@mantine/dates"; import { notifications } from "@mantine/notifications"; import { Button } from "@mui/material"; diff --git a/packages/client/src/components/molecules/LogoStatusMolecules/PlaybackDatePicker.tsx b/packages/client/src/components/molecules/LogoStatusMolecules/PlaybackDatePicker.tsx index 7082f73e..2e6bcf2f 100644 --- a/packages/client/src/components/molecules/LogoStatusMolecules/PlaybackDatePicker.tsx +++ b/packages/client/src/components/molecules/LogoStatusMolecules/PlaybackDatePicker.tsx @@ -1,7 +1,7 @@ import axios from "axios"; import React, { useEffect, useState } from "react"; -import { useAppState } from "@/contexts/AppStateContext"; +import { useAppState } from "@/stores/useAppState"; import { usePlaybackStore } from "@/stores/usePlayback"; import { notifications } from "@mantine/notifications"; import CalendarMonthIcon from "@mui/icons-material/CalendarMonth"; diff --git a/packages/client/src/components/molecules/LogoStatusMolecules/SettingsComponent.tsx b/packages/client/src/components/molecules/LogoStatusMolecules/SettingsComponent.tsx index f45b8973..77e0e435 100644 --- a/packages/client/src/components/molecules/LogoStatusMolecules/SettingsComponent.tsx +++ b/packages/client/src/components/molecules/LogoStatusMolecules/SettingsComponent.tsx @@ -1,12 +1,7 @@ import { useTheme } from "next-themes"; import { useCallback, useState } from "react"; -import { - APPUNITS, - CONNECTIONTYPES, - useAppState, -} from "@/contexts/AppStateContext"; -import { helios, heliosCompliment, sand } from "@/styles/colors"; +import { APPUNITS, CONNECTIONTYPES, useAppState } from "@/stores/useAppState"; import { TimeInput } from "@mantine/dates"; import SettingsIcon from "@mui/icons-material/Settings"; import Modal from "@mui/material/Modal"; diff --git a/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx b/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx index 5daf3e1f..e2f90acc 100644 --- a/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx +++ b/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx @@ -5,7 +5,7 @@ import AWSIcon from "@/components/atoms/AWSIcon"; import CarIcon from "@/components/atoms/CarIcon"; import LatencyDotsIcon from "@/components/atoms/LatencyDotsIcon"; import UserComputerIcon from "@/components/atoms/UserComputerIcon"; -import { CONNECTIONTYPES, useAppState } from "@/contexts/AppStateContext"; +import { CONNECTIONTYPES, useAppState } from "@/stores/useAppState"; import { usePacketStore } from "@/stores/usePacket"; import { Switch } from "@mantine/core"; diff --git a/packages/client/src/components/molecules/MapMolecules/Map.tsx b/packages/client/src/components/molecules/MapMolecules/Map.tsx index b4002f6d..2efae396 100644 --- a/packages/client/src/components/molecules/MapMolecules/Map.tsx +++ b/packages/client/src/components/molecules/MapMolecules/Map.tsx @@ -18,6 +18,7 @@ import ReactMapGL, { } from "react-map-gl"; import HeliosModel from "@/assets/HeliosBirdseye.png"; +import { useAppState } from "@/stores/useAppState"; import SportsScoreIcon from "@mui/icons-material/SportsScore"; import { type Coords, diff --git a/packages/client/src/components/molecules/MapMolecules/MapControls.tsx b/packages/client/src/components/molecules/MapMolecules/MapControls.tsx index 4720bc4c..a506ccde 100644 --- a/packages/client/src/components/molecules/MapMolecules/MapControls.tsx +++ b/packages/client/src/components/molecules/MapMolecules/MapControls.tsx @@ -5,6 +5,7 @@ import { FaMagnifyingGlass } from "react-icons/fa6"; import { twMerge } from "tailwind-merge"; import { TrackList } from "@/components/molecules/MapMolecules/Map"; +import { useAppState } from "@/stores/useAppState"; import { Coords } from "@shared/helios-types/src/types"; type MapStates = { diff --git a/packages/client/src/components/molecules/MapMolecules/MapText.tsx b/packages/client/src/components/molecules/MapMolecules/MapText.tsx index d749a1e5..7456dab8 100644 --- a/packages/client/src/components/molecules/MapMolecules/MapText.tsx +++ b/packages/client/src/components/molecules/MapMolecules/MapText.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from "react"; import { socketIO } from "@/contexts/SocketContext"; import useUnitsHandler from "@/hooks/PIS/useUnitsHandler"; import { UnitType } from "@/objects/PIS/PIS.interface"; -import { mediumGray } from "@/styles/colors"; +import { useAppState } from "@/stores/useAppState"; import { Button, Popover } from "@mantine/core"; import { type IRaceInfo } from "@shared/helios-types"; diff --git a/packages/client/src/components/molecules/PlaybackMolecules/PlaybackSlider.tsx b/packages/client/src/components/molecules/PlaybackMolecules/PlaybackSlider.tsx index 4ca8e553..4a3f83b2 100644 --- a/packages/client/src/components/molecules/PlaybackMolecules/PlaybackSlider.tsx +++ b/packages/client/src/components/molecules/PlaybackMolecules/PlaybackSlider.tsx @@ -4,8 +4,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import PauseIcon from "@/components/atoms/PauseIcon"; import PlayIcon from "@/components/atoms/PlayIcon"; -import { usePacket } from "@/contexts/PacketContext"; -import { usePlaybackContext } from "@/contexts/PlayBackContext"; +import { usePacketStore } from "@/stores/usePacket"; +import { usePlaybackStore } from "@/stores/usePlayback"; import Tooltip from "@mui/material/Tooltip"; export default function PlaybackSlider() { @@ -18,8 +18,10 @@ export default function PlaybackSlider() { const [tooltipPosition, setTooltipPosition] = useState({ left: 0, top: 0 }); const hoverAnchorRef = useRef(null); - const { setCurrentPacket } = usePacket(); - const { playbackData } = usePlaybackContext(); + const { setCurrentPacket } = usePacketStore(); + const { playbackData } = usePlaybackStore((state) => ({ + playbackData: state.playbackData, + })); const { hasData, sortedData } = useMemo(() => { const sortedData = diff --git a/packages/client/src/components/tabs/AnalysisTab.tsx b/packages/client/src/components/tabs/AnalysisTab.tsx index be40a2ea..543772c7 100644 --- a/packages/client/src/components/tabs/AnalysisTab.tsx +++ b/packages/client/src/components/tabs/AnalysisTab.tsx @@ -4,7 +4,7 @@ import React, { useState } from "react"; import { twMerge } from "tailwind-merge"; import { tabs } from "@/objects/TabRoutes"; -import { helios, lightGray, mediumGray } from "@/styles/colors"; +import { useAppState } from "@/stores/useAppState"; import { ThemeProvider } from "@emotion/react"; import { Tab, Tabs, createTheme } from "@mui/material"; diff --git a/packages/client/src/components/tabs/RaceTab.tsx b/packages/client/src/components/tabs/RaceTab.tsx index f9cdc4ef..4d2c55e2 100644 --- a/packages/client/src/components/tabs/RaceTab.tsx +++ b/packages/client/src/components/tabs/RaceTab.tsx @@ -2,8 +2,8 @@ import axios from "axios"; import { useTheme } from "next-themes"; import React, { useCallback, useEffect, useState } from "react"; -import { useAppState } from "@/contexts/AppStateContext"; -import { useLapData } from "@/contexts/LapDataContext"; +import { useAppState } from "@/stores/useAppState"; +import { useLapDataStore } from "@/stores/useLapData"; import { notifications } from "@mantine/notifications"; import { SelectChangeEvent } from "@mui/material/Select"; import { type IFormattedLapData, prodURL } from "@shared/helios-types"; diff --git a/packages/client/src/components/transformers/PISTransformer.tsx b/packages/client/src/components/transformers/PISTransformer.tsx index 447fa323..f86081e6 100644 --- a/packages/client/src/components/transformers/PISTransformer.tsx +++ b/packages/client/src/components/transformers/PISTransformer.tsx @@ -1,6 +1,5 @@ import { type JSX, useCallback } from "react"; -import { useAppState } from "@/contexts/AppStateContext"; import useUnitsHandler from "@/hooks/PIS/useUnitsHandler"; import useFullscreen from "@/hooks/useFullscreen"; import { @@ -8,6 +7,7 @@ import { type I_PISFieldData, } from "@/objects/PIS/PIS.interface"; import type I_PIS from "@/objects/PIS/PIS.interface"; +import { useAppState } from "@/stores/useAppState"; type RangeCheckedFieldDataProps = { fieldData: I_PISFieldData; diff --git a/packages/client/src/contexts/AppStateContext.tsx b/packages/client/src/contexts/AppStateContext.tsx deleted file mode 100644 index f8f73c85..00000000 --- a/packages/client/src/contexts/AppStateContext.tsx +++ /dev/null @@ -1,231 +0,0 @@ -// This file controls app settings. -import { - type Dispatch, - type ReactNode, - type SetStateAction, - createContext, - useCallback, - useContext, - useEffect, - useState, -} from "react"; - -import type { IPlaybackDateTime } from "@/components/molecules/LogoStatusMolecules/PlaybackDatePicker"; -import { type Coords, FINISH_LINE_LOCATION } from "@shared/helios-types"; - -interface Props { - children: ReactNode | ReactNode[]; -} - -export enum CONNECTIONTYPES { - DEMO = "DEMO", - NETWORK = "NETWORK", - RADIO = "RADIO", -} - -export enum APPUNITS { - METRIC = "metric", - IMPERIAL = "imperial", -} - -interface IAppState { - displayLoading: boolean; - loading: boolean; - error: boolean; - appUnits: APPUNITS; - favourites: string[]; - connectionType: CONNECTIONTYPES; - socketConnected: boolean; - mqttConnected: boolean; - radioConnected: boolean; - userLatency: number; - carLatency: number; - lapCoords: Coords; - playbackSwitch: boolean; - playbackDateTime: IPlaybackDateTime; -} -interface IAppStateReturn { - currentAppState: IAppState; - setCurrentAppState: Dispatch>; -} - -const appStateContext = createContext({} as IAppStateReturn); - -/** - * Just another context provider that manages a lot of the app's state - * for the future, you could technically use redux and it might be better to do that instead - * thats for future recruits though - * - * you can read some documentation on context providers here: - * https://www.telerik.com/blogs/react-basics-how-when-use-react-context - * - * and then you can read the documentation for this specific context provider here: - * in docs/CLIENT.md#appstatecontextprovider - * - */ -export function AppStateContextProvider({ children }: Props) { - const [currentAppState, setCurrentAppState] = useState({ - appUnits: APPUNITS.METRIC, - carLatency: 0, - connectionType: CONNECTIONTYPES.DEMO, - displayLoading: true, - error: false, - favourites: [], - lapCoords: FINISH_LINE_LOCATION, - loading: true, - mqttConnected: false, - playbackDateTime: { - date: null, - endTime: null, - startTime: null, - }, - playbackSwitch: false, - radioConnected: false, - socketConnected: false, - userLatency: 0, - }); - - // Connection State Manager - useEffect(() => { - if (currentAppState.connectionType === CONNECTIONTYPES.DEMO) { - if (currentAppState.socketConnected) { - setCurrentAppState((prev) => ({ - ...prev, - connectionType: CONNECTIONTYPES.NETWORK, - loading: false, - })); - } - if (currentAppState.radioConnected) { - setCurrentAppState((prev) => ({ - ...prev, - connectionType: CONNECTIONTYPES.RADIO, - loading: false, - })); - } - } - if (currentAppState.connectionType === CONNECTIONTYPES.NETWORK) { - setCurrentAppState((prev) => ({ - ...prev, - loading: !currentAppState.socketConnected, - })); - } - if (currentAppState.connectionType === CONNECTIONTYPES.RADIO) { - setCurrentAppState((prev) => ({ - ...prev, - loading: !currentAppState.radioConnected, - })); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentAppState.socketConnected, currentAppState.radioConnected]); - - useEffect(() => { - setTimeout(() => { - setCurrentAppState((prev) => ({ - ...prev, - loading: false, - })); - }, 5000); - }, []); - - const fetchSettingsFromLocalStorage = useCallback(() => { - const savedSettings = localStorage.getItem("settings"); - const favourites = localStorage.getItem("favourites"); - - if (savedSettings) { - const parsedSettings = JSON.parse(savedSettings) as Partial; - - const parsedFavourites = favourites - ? (JSON.parse(favourites) as string[]) - : [ - "Motor Temp", - "Battery Cell Voltage", - "Vehicle Velocity", - "Pack Voltage", - "Pack Current", - "Battery Average Voltage", - ]; - - const hasPlaybackDateTime = !!parsedSettings.playbackDateTime; - - const parsedPlaybackDateTime = hasPlaybackDateTime - ? { - date: parsedSettings.playbackDateTime!.date - ? new Date(parsedSettings.playbackDateTime!.date) - : null, - endTime: parsedSettings.playbackDateTime!.endTime - ? new Date(parsedSettings.playbackDateTime!.endTime) - : null, - startTime: parsedSettings.playbackDateTime!.startTime - ? new Date(parsedSettings.playbackDateTime!.startTime) - : null, - } - : { - date: null, - endTime: null, - startTime: null, - }; - - setCurrentAppState((prev) => ({ - ...prev, - appUnits: parsedSettings.appUnits ?? prev.appUnits, - connectionType: parsedSettings.connectionType ?? prev.connectionType, - favourites: parsedFavourites, - lapCoords: parsedSettings.lapCoords ?? prev.lapCoords, - playbackDateTime: parsedPlaybackDateTime, - })); - } else if (favourites === null && savedSettings) { - const parsedSettings: IAppState = JSON.parse(savedSettings) as IAppState; - setCurrentAppState((prev) => ({ - ...prev, - appUnits: parsedSettings.appUnits, - connectionType: parsedSettings.connectionType, - favourites: [ - "Motor Temp", - "Battery Cell Voltage", - "Vehicle Velocity", - "Pack Voltage", - "Pack Current", - "Battery Average Voltage", - ], - lapCoords: parsedSettings.lapCoords, - })); - } - }, []); - - const saveSettingsToLocalStorage = useCallback(() => { - localStorage.setItem("settings", JSON.stringify(currentAppState)); - }, [currentAppState]); - - useEffect(() => { - fetchSettingsFromLocalStorage(); - }, [fetchSettingsFromLocalStorage]); - - useEffect(() => { - if (!currentAppState.loading) { - saveSettingsToLocalStorage(); - } - }, [currentAppState.loading, saveSettingsToLocalStorage]); - - // useEffect(() => { - // if (!currentAppState.loading) { - // setTimeout(() => { - // setAnimateLoading(false); - // }, 1000); - // } - // }, [currentAppState.loading]); - - return ( - - {children} - - ); -} - -export function useAppState(): IAppStateReturn { - return useContext(appStateContext); -} diff --git a/packages/client/src/contexts/PlayBackContext.tsx b/packages/client/src/contexts/PlayBackContext.tsx deleted file mode 100644 index 50e26afe..00000000 --- a/packages/client/src/contexts/PlayBackContext.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { - type JSX, - type PropsWithChildren, - createContext, - useContext, - useState, -} from "react"; - -import { ITelemetryData } from "@shared/helios-types"; - -interface IPlaybackContextReturn { - playbackData: ITelemetryData[]; - setPlaybackData: (data: ITelemetryData[]) => void; -} - -const playbackContext = createContext( - {} as IPlaybackContextReturn, -); - -export function PlaybackContextProvider({ - children, -}: PropsWithChildren): JSX.Element { - const [playbackData, setPlaybackData] = useState([]); - - return ( - - {children} - - ); -} - -export function usePlaybackContext(): IPlaybackContextReturn { - return useContext(playbackContext); -} diff --git a/packages/client/src/contexts/SocketContext.tsx b/packages/client/src/contexts/SocketContext.tsx index f10137eb..744a6f14 100644 --- a/packages/client/src/contexts/SocketContext.tsx +++ b/packages/client/src/contexts/SocketContext.tsx @@ -9,7 +9,7 @@ import { } from "react"; import { type Socket, io } from "socket.io-client"; -import { useAppState } from "@/contexts/AppStateContext"; +import { useAppState } from "@/stores/useAppState"; import { notifications } from "@mantine/notifications"; import type { CoordInfoUpdate, diff --git a/packages/client/src/hooks/PIS/useUnitsHandler.tsx b/packages/client/src/hooks/PIS/useUnitsHandler.tsx index 119f9060..620780b0 100644 --- a/packages/client/src/hooks/PIS/useUnitsHandler.tsx +++ b/packages/client/src/hooks/PIS/useUnitsHandler.tsx @@ -1,5 +1,5 @@ -import { APPUNITS, useAppState } from "@/contexts/AppStateContext"; import { UnitType } from "@/objects/PIS/PIS.interface"; +import { APPUNITS, useAppState } from "@/stores/useAppState"; const MI_TO_KM = 0.621371; diff --git a/packages/client/src/pages/[[...slug]].tsx b/packages/client/src/pages/[[...slug]].tsx index 3d0b64f7..4fe8c2dc 100644 --- a/packages/client/src/pages/[[...slug]].tsx +++ b/packages/client/src/pages/[[...slug]].tsx @@ -4,7 +4,7 @@ import LogoStatusContainer from "@/components/containers/LogoStatusContainer"; import MapContainer from "@/components/containers/MapContainer"; import TabsContainer from "@/components/containers/TabsContainer"; import PlaybackSlider from "@/components/molecules/PlaybackMolecules/PlaybackSlider"; -import { useAppState } from "@/contexts/AppStateContext"; +import { useAppState } from "@/stores/useAppState"; export default function Home() { const { currentAppState } = useAppState(); diff --git a/packages/client/src/pages/_app.tsx b/packages/client/src/pages/_app.tsx index 42b7c209..f6c07035 100644 --- a/packages/client/src/pages/_app.tsx +++ b/packages/client/src/pages/_app.tsx @@ -1,10 +1,8 @@ import { ThemeProvider } from "next-themes"; import type { AppProps } from "next/app"; -import { LapListenerManager } from "@/components/global/LapDataListenerManager"; +import { EffectsProvider } from "@/components/global/EffectsProvider"; import LoadingWrapper from "@/components/global/LoadingWrapper"; -import { PacketListenerManager } from "@/components/global/PacketListenerManager"; -import { AppStateContextProvider } from "@/contexts/AppStateContext"; import { SocketContextProvider } from "@/contexts/SocketContext"; import "@/styles/globals.css"; import { MantineProvider } from "@mantine/core"; @@ -15,19 +13,17 @@ import "@mantine/notifications/styles.css"; export default function App({ Component, pageProps }: AppProps) { return ( - - - - - - - - - - - - - - + <> + {/* Initialize side-effect logic for Zustand store state files */} + + + + + + + + + + ); } diff --git a/packages/client/src/stores/useAppState.ts b/packages/client/src/stores/useAppState.ts new file mode 100644 index 00000000..67da6ed3 --- /dev/null +++ b/packages/client/src/stores/useAppState.ts @@ -0,0 +1,76 @@ +import { create } from "zustand"; + +import type { IPlaybackDateTime } from "@/components/molecules/LogoStatusMolecules/PlaybackDatePicker"; +import { type Coords, FINISH_LINE_LOCATION } from "@shared/helios-types"; + +export enum CONNECTIONTYPES { + DEMO = "DEMO", + NETWORK = "NETWORK", + RADIO = "RADIO", +} + +export enum APPUNITS { + METRIC = "metric", + IMPERIAL = "imperial", +} + +interface IAppState { + displayLoading: boolean; + loading: boolean; + error: boolean; + darkMode: boolean; + appUnits: APPUNITS; + favourites: string[]; + connectionType: CONNECTIONTYPES; + socketConnected: boolean; + mqttConnected: boolean; + radioConnected: boolean; + userLatency: number; + carLatency: number; + lapCoords: Coords; + playbackSwitch: boolean; + playbackDateTime: IPlaybackDateTime; +} + +interface AppStateStore { + currentAppState: IAppState; + setCurrentAppState: (updater: (prev: IAppState) => IAppState) => void; + toggleDarkMode: () => void; +} + +export const useAppState = create((set) => ({ + currentAppState: { + appUnits: APPUNITS.METRIC, + carLatency: 0, + connectionType: CONNECTIONTYPES.DEMO, + darkMode: false, + displayLoading: true, + error: false, + favourites: [], + lapCoords: FINISH_LINE_LOCATION, + loading: true, + mqttConnected: false, + playbackDateTime: { + date: null, + endTime: null, + startTime: null, + }, + playbackSwitch: false, + radioConnected: false, + socketConnected: false, + userLatency: 0, + }, + + setCurrentAppState: (updater) => + set((state) => ({ + currentAppState: updater(state.currentAppState), + })), + + toggleDarkMode: () => + set((state) => ({ + currentAppState: { + ...state.currentAppState, + darkMode: !state.currentAppState.darkMode, + }, + })), +})); diff --git a/packages/client/src/stores/useLapData.ts b/packages/client/src/stores/useLapData.ts index c9427e40..2c4540e7 100644 --- a/packages/client/src/stores/useLapData.ts +++ b/packages/client/src/stores/useLapData.ts @@ -1,7 +1,6 @@ import axios from "axios"; import { create } from "zustand"; -import { socketIO } from "@/contexts/SocketContext"; import { notifications } from "@mantine/notifications"; import { IFormattedLapData, ILapData, prodURL } from "@shared/helios-types"; @@ -28,26 +27,22 @@ export const formatLapData = (lapPacket: ILapData): IFormattedLapData => ({ }); interface LapDataState { - attachNetworkListener: () => void; - detachNetworkListener: () => void; - lapData: IFormattedLapData[]; formatLapData: (lapPacket: ILapData) => IFormattedLapData; - onLapData: (lapPacket: ILapData) => void; - onLapComplete: () => void; - fetchLapData: () => Promise; + lapData: IFormattedLapData[]; + setLapData: (data: IFormattedLapData[]) => void; + addLapData: (data: IFormattedLapData) => void; clearLapData: () => void; + fetchLapData: () => Promise; } -export const useLapDataStore = create((set, get) => ({ - attachNetworkListener: () => { - socketIO.on("lapData", get().onLapData); - socketIO.on("lapComplete", get().onLapComplete); - }, +export const useLapDataStore = create((set) => ({ + addLapData: (data) => + set((state) => ({ + lapData: [...state.lapData, data], + })), + clearLapData: () => set({ lapData: [] }), - detachNetworkListener: () => { - socketIO.off("lapData", get().onLapData); - socketIO.off("lapComplete", get().onLapComplete); - }, + fetchLapData: async () => { try { const response = await axios.get(`${prodURL}/laps`); @@ -67,19 +62,7 @@ export const useLapDataStore = create((set, get) => ({ }); } }, - formatLapData, + formatLapData: formatLapData, lapData: [], - onLapComplete: () => { - notifications.show({ - color: "green", - message: "A lap has been completed!", - title: "Lap Completion", - }); - }, - onLapData: (lapPacket: ILapData) => { - const formattedData = formatLapData(lapPacket); - set((state) => ({ - lapData: [...state.lapData, formattedData], - })); - }, + setLapData: (data) => set({ lapData: data }), })); diff --git a/packages/client/src/stores/usePacket.ts b/packages/client/src/stores/usePacket.ts index 27e25470..90de78aa 100644 --- a/packages/client/src/stores/usePacket.ts +++ b/packages/client/src/stores/usePacket.ts @@ -1,42 +1,14 @@ import { create } from "zustand"; -import { socketIO } from "@/contexts/SocketContext"; import { generateFakeTelemetryData } from "@shared/helios-types"; import type { ITelemetryData } from "@shared/helios-types"; interface IPacketStore { currentPacket: ITelemetryData; setCurrentPacket: (packet: ITelemetryData) => void; - startDemoMode: () => void; - stopDemoMode: () => void; - attachNetworkListener: () => void; - detachNetworkListener: () => void; } -let demoInterval: NodeJS.Timeout | null = null; - -const onPacket = (packet: ITelemetryData) => { - usePacketStore.getState().setCurrentPacket(packet); -}; - export const usePacketStore = create((set) => ({ - attachNetworkListener: () => { - socketIO.on("packet", onPacket); - }, currentPacket: generateFakeTelemetryData(), - detachNetworkListener: () => { - socketIO.off("packet", onPacket); - }, setCurrentPacket: (packet) => set({ currentPacket: packet }), - startDemoMode: () => { - demoInterval = setInterval(() => { - set({ currentPacket: generateFakeTelemetryData() }); - }, 500); - }, - stopDemoMode: () => { - if (demoInterval) { - clearInterval(demoInterval); - demoInterval = null; - } - }, })); diff --git a/packages/client/src/stores/usePlayback.ts b/packages/client/src/stores/usePlayback.ts new file mode 100644 index 00000000..39118d56 --- /dev/null +++ b/packages/client/src/stores/usePlayback.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +import { ITelemetryData } from "@shared/helios-types"; + +interface IPlaybackStore { + playbackData: ITelemetryData[]; + setPlaybackData: (data: ITelemetryData[]) => void; +} + +export const usePlaybackStore = create((set) => ({ + playbackData: [], + setPlaybackData: (data) => set({ playbackData: data }), +})); From 356184459852afa43bf74fa950e8cb45c3cca884 Mon Sep 17 00:00:00 2001 From: alexwhelan12 Date: Wed, 8 Oct 2025 19:05:04 -0600 Subject: [PATCH 3/4] fix build errors --- .../molecules/LogoStatusMolecules/SettingsComponent.tsx | 1 + .../molecules/LogoStatusMolecules/StatusComponent.tsx | 2 ++ .../client/src/components/molecules/MapMolecules/MapText.tsx | 1 + packages/client/src/components/tabs/AnalysisTab.tsx | 1 + packages/client/src/components/tabs/RaceTab.tsx | 4 ++++ 5 files changed, 9 insertions(+) diff --git a/packages/client/src/components/molecules/LogoStatusMolecules/SettingsComponent.tsx b/packages/client/src/components/molecules/LogoStatusMolecules/SettingsComponent.tsx index 77e0e435..8771a0d1 100644 --- a/packages/client/src/components/molecules/LogoStatusMolecules/SettingsComponent.tsx +++ b/packages/client/src/components/molecules/LogoStatusMolecules/SettingsComponent.tsx @@ -2,6 +2,7 @@ import { useTheme } from "next-themes"; import { useCallback, useState } from "react"; import { APPUNITS, CONNECTIONTYPES, useAppState } from "@/stores/useAppState"; +import { helios, heliosCompliment, sand } from "@/styles/colors"; import { TimeInput } from "@mantine/dates"; import SettingsIcon from "@mui/icons-material/Settings"; import Modal from "@mui/material/Modal"; diff --git a/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx b/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx index e2f90acc..ed055307 100644 --- a/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx +++ b/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx @@ -7,6 +7,7 @@ import LatencyDotsIcon from "@/components/atoms/LatencyDotsIcon"; import UserComputerIcon from "@/components/atoms/UserComputerIcon"; import { CONNECTIONTYPES, useAppState } from "@/stores/useAppState"; import { usePacketStore } from "@/stores/usePacket"; +import { helios } from "@/styles/colors"; import { Switch } from "@mantine/core"; function PlaybackPickerComponent() { @@ -35,6 +36,7 @@ function StatusComponent() { const { currentAppState } = useAppState(); const { currentPacket } = usePacketStore(); const userConnection = currentAppState.socketConnected; + const { resolvedTheme } = useTheme(); // TODO: change carConnection from socketIO.connected to carConnection.connected const carConnection = currentAppState.socketConnected; const colorTheme = resolvedTheme === "dark" ? "white" : "black"; diff --git a/packages/client/src/components/molecules/MapMolecules/MapText.tsx b/packages/client/src/components/molecules/MapMolecules/MapText.tsx index 7456dab8..ed6836b9 100644 --- a/packages/client/src/components/molecules/MapMolecules/MapText.tsx +++ b/packages/client/src/components/molecules/MapMolecules/MapText.tsx @@ -5,6 +5,7 @@ import { socketIO } from "@/contexts/SocketContext"; import useUnitsHandler from "@/hooks/PIS/useUnitsHandler"; import { UnitType } from "@/objects/PIS/PIS.interface"; import { useAppState } from "@/stores/useAppState"; +import { mediumGray } from "@/styles/colors"; import { Button, Popover } from "@mantine/core"; import { type IRaceInfo } from "@shared/helios-types"; diff --git a/packages/client/src/components/tabs/AnalysisTab.tsx b/packages/client/src/components/tabs/AnalysisTab.tsx index 543772c7..d382af04 100644 --- a/packages/client/src/components/tabs/AnalysisTab.tsx +++ b/packages/client/src/components/tabs/AnalysisTab.tsx @@ -5,6 +5,7 @@ import { twMerge } from "tailwind-merge"; import { tabs } from "@/objects/TabRoutes"; import { useAppState } from "@/stores/useAppState"; +import { helios, lightGray, mediumGray } from "@/styles/colors"; import { ThemeProvider } from "@emotion/react"; import { Tab, Tabs, createTheme } from "@mui/material"; diff --git a/packages/client/src/components/tabs/RaceTab.tsx b/packages/client/src/components/tabs/RaceTab.tsx index 4d2c55e2..6997c2b7 100644 --- a/packages/client/src/components/tabs/RaceTab.tsx +++ b/packages/client/src/components/tabs/RaceTab.tsx @@ -2,6 +2,10 @@ import axios from "axios"; import { useTheme } from "next-themes"; import React, { useCallback, useEffect, useState } from "react"; +import { columns } from "@/components/config/lapTableConfig"; +import ColumnFilters from "@/components/molecules/RaceTabMolecules/ColumnFilters"; +import DriverFilter from "@/components/molecules/RaceTabMolecules/DriverFilter"; +import RaceTabTable from "@/components/molecules/RaceTabMolecules/RaceTabTable"; import { useAppState } from "@/stores/useAppState"; import { useLapDataStore } from "@/stores/useLapData"; import { notifications } from "@mantine/notifications"; From fd13bdd314d270213ffdf6acb9e001260da91492 Mon Sep 17 00:00:00 2001 From: alexwhelan12 Date: Sat, 18 Oct 2025 15:54:25 -0600 Subject: [PATCH 4/4] Fixed theme issue and removed SocketContext --- docs/CLIENT.md | 1 - .../global/AppStateEffectsManager.tsx | 1 - .../src/components/global/EffectsProvider.tsx | 2 + .../global/LapDataListenerManager.tsx | 2 +- .../global/PacketListenerManager.tsx | 2 +- .../global/SocketManager.tsx} | 73 ++++++------------- .../molecules/MapMolecules/MapText.tsx | 2 +- packages/client/src/pages/[[...slug]].tsx | 44 ++++++----- packages/client/src/pages/_app.tsx | 15 ++-- packages/client/src/stores/useAppState.ts | 11 --- 10 files changed, 56 insertions(+), 97 deletions(-) rename packages/client/src/{contexts/SocketContext.tsx => components/global/SocketManager.tsx} (69%) diff --git a/docs/CLIENT.md b/docs/CLIENT.md index 328f7c90..6965dc55 100644 --- a/docs/CLIENT.md +++ b/docs/CLIENT.md @@ -104,7 +104,6 @@ const [currentAppState, setCurrentAppState] = useState({ appUnits: APPUNITS.METRIC, carLatency: 0, connectionType: CONNECTIONTYPES.DEMO, - darkMode: false, displayLoading: true, error: false, favourites: [], diff --git a/packages/client/src/components/global/AppStateEffectsManager.tsx b/packages/client/src/components/global/AppStateEffectsManager.tsx index 61feaa7e..b6b2ed5b 100644 --- a/packages/client/src/components/global/AppStateEffectsManager.tsx +++ b/packages/client/src/components/global/AppStateEffectsManager.tsx @@ -94,7 +94,6 @@ export default function AppStateEffects() { ...prev, appUnits: parsedSettings.appUnits ?? prev.appUnits, connectionType: parsedSettings.connectionType ?? prev.connectionType, - darkMode: parsedSettings.darkMode ?? prev.darkMode, favourites: parsedFavourites, lapCoords: parsedSettings.lapCoords ?? prev.lapCoords, playbackDateTime: parsedPlaybackDateTime, diff --git a/packages/client/src/components/global/EffectsProvider.tsx b/packages/client/src/components/global/EffectsProvider.tsx index ab54a3f0..7e6d65f8 100644 --- a/packages/client/src/components/global/EffectsProvider.tsx +++ b/packages/client/src/components/global/EffectsProvider.tsx @@ -1,6 +1,7 @@ import AppStateEffectsManager from "@/components/global/AppStateEffectsManager"; import { LapListenerManager } from "@/components/global/LapDataListenerManager"; import { PacketListenerManager } from "@/components/global/PacketListenerManager"; +import SocketManager from "@/components/global/SocketManager"; export function EffectsProvider() { return ( @@ -8,6 +9,7 @@ export function EffectsProvider() { + ); } diff --git a/packages/client/src/components/global/LapDataListenerManager.tsx b/packages/client/src/components/global/LapDataListenerManager.tsx index fb8c953c..a71a806a 100644 --- a/packages/client/src/components/global/LapDataListenerManager.tsx +++ b/packages/client/src/components/global/LapDataListenerManager.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; -import { socketIO } from "@/contexts/SocketContext"; +import { socketIO } from "@/components/global/SocketManager"; import { CONNECTIONTYPES, useAppState } from "@/stores/useAppState"; import { formatLapData, useLapDataStore } from "@/stores/useLapData"; import { notifications } from "@mantine/notifications"; diff --git a/packages/client/src/components/global/PacketListenerManager.tsx b/packages/client/src/components/global/PacketListenerManager.tsx index b45e3e56..ec332b93 100644 --- a/packages/client/src/components/global/PacketListenerManager.tsx +++ b/packages/client/src/components/global/PacketListenerManager.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from "react"; -import { socketIO } from "@/contexts/SocketContext"; +import { socketIO } from "@/components/global/SocketManager"; import { CONNECTIONTYPES, useAppState } from "@/stores/useAppState"; import { usePacketStore } from "@/stores/usePacket"; import { generateFakeTelemetryData } from "@shared/helios-types"; diff --git a/packages/client/src/contexts/SocketContext.tsx b/packages/client/src/components/global/SocketManager.tsx similarity index 69% rename from packages/client/src/contexts/SocketContext.tsx rename to packages/client/src/components/global/SocketManager.tsx index 744a6f14..dd705c9b 100644 --- a/packages/client/src/contexts/SocketContext.tsx +++ b/packages/client/src/components/global/SocketManager.tsx @@ -1,12 +1,6 @@ -import { - type JSX, - type PropsWithChildren, - createContext, - useCallback, - useContext, - useEffect, - useRef, -} from "react"; +"use client"; + +import { useCallback, useEffect, useRef } from "react"; import { type Socket, io } from "socket.io-client"; import { useAppState } from "@/stores/useAppState"; @@ -37,16 +31,13 @@ interface ServerToClientEvents { carConnect: (data: { message: string }) => void; } -// Defaults to using client fakerJS, change Data to Network in site settings to connect to server +// The socket instance export const socketIO: Socket = io( socketURL, { autoConnect: false }, ); -const socketContext = createContext({}); -export function SocketContextProvider({ - children, -}: PropsWithChildren): JSX.Element { +export default function SocketManager() { const { setCurrentAppState } = useAppState(); const start = useRef(null); @@ -59,29 +50,22 @@ export function SocketContextProvider({ const onLapCoords = useCallback( (coords: CoordUpdateResponse) => { - if ("error" in coords) { - return; - } + if ("error" in coords) return; setCurrentAppState((prev) => ({ ...prev, lapCoords: coords as Coords })); }, [setCurrentAppState], ); + const onCarConnect = useCallback(() => { - setCurrentAppState((prev) => ({ - ...prev, - mqttConnected: true, - })); + setCurrentAppState((prev) => ({ ...prev, mqttConnected: true })); notifications.show({ - color: "", message: "Car has reconnected!", title: "Connection Restored", }); }, [setCurrentAppState]); + const onCarDisconnect = useCallback(() => { - setCurrentAppState((prev) => ({ - ...prev, - mqttConnected: false, - })); + setCurrentAppState((prev) => ({ ...prev, mqttConnected: false })); notifications.show({ color: "red", message: "Car has disconnected!", @@ -90,24 +74,32 @@ export function SocketContextProvider({ }, [setCurrentAppState]); useEffect(() => { - // Connect to the socket + // Connect the socket socketIO.connect(); - // Ping the server every second to measure user latency + // Ping interval const id = setInterval(() => { start.current = Date.now(); - socketIO.emit("ping", () => { const duration = Date.now() - (start.current as number); setCurrentAppState((prev) => ({ ...prev, userLatency: duration })); }); }, 10000); - // Register event listeners + // Register listeners socketIO.on("carDisconnect", onCarDisconnect); socketIO.on("carConnect", onCarConnect); socketIO.on("carLatency", onCarLatency); socketIO.on("lapCoords", onLapCoords); + + socketIO.on("connect", () => { + setCurrentAppState((prev) => ({ ...prev, socketConnected: true })); + }); + + socketIO.on("disconnect", () => { + setCurrentAppState((prev) => ({ ...prev, socketConnected: false })); + }); + return () => { socketIO.disconnect(); clearInterval(id); @@ -124,24 +116,5 @@ export function SocketContextProvider({ setCurrentAppState, ]); - // Socket connection status listeners - socketIO.on("connect", () => { - setCurrentAppState((prev) => ({ - ...prev, - socketConnected: true, - })); - }); - - socketIO.on("disconnect", () => { - setCurrentAppState((prev) => ({ - ...prev, - socketConnected: false, - })); - }); - - return {children}; -} - -export function useSocket() { - return useContext(socketContext); + return null; // This component does not render anything } diff --git a/packages/client/src/components/molecules/MapMolecules/MapText.tsx b/packages/client/src/components/molecules/MapMolecules/MapText.tsx index ed6836b9..9770dee4 100644 --- a/packages/client/src/components/molecules/MapMolecules/MapText.tsx +++ b/packages/client/src/components/molecules/MapMolecules/MapText.tsx @@ -1,7 +1,7 @@ import { useTheme } from "next-themes"; import { useCallback, useEffect, useState } from "react"; -import { socketIO } from "@/contexts/SocketContext"; +import { socketIO } from "@/components/global/SocketManager"; import useUnitsHandler from "@/hooks/PIS/useUnitsHandler"; import { UnitType } from "@/objects/PIS/PIS.interface"; import { useAppState } from "@/stores/useAppState"; diff --git a/packages/client/src/pages/[[...slug]].tsx b/packages/client/src/pages/[[...slug]].tsx index 4fe8c2dc..ec372650 100644 --- a/packages/client/src/pages/[[...slug]].tsx +++ b/packages/client/src/pages/[[...slug]].tsx @@ -10,33 +10,31 @@ export default function Home() { const { currentAppState } = useAppState(); return ( -
-
-
-
-
- -
-
- -
+
+
+
+
+
-
-
- -
-
- -
+
+
-
- {currentAppState.playbackSwitch ? ( - - ) : ( - - )} +
+
+
+ +
+
+
+
+ {currentAppState.playbackSwitch ? ( + + ) : ( + + )} +
); diff --git a/packages/client/src/pages/_app.tsx b/packages/client/src/pages/_app.tsx index f6c07035..a7520a8a 100644 --- a/packages/client/src/pages/_app.tsx +++ b/packages/client/src/pages/_app.tsx @@ -3,7 +3,6 @@ import type { AppProps } from "next/app"; import { EffectsProvider } from "@/components/global/EffectsProvider"; import LoadingWrapper from "@/components/global/LoadingWrapper"; -import { SocketContextProvider } from "@/contexts/SocketContext"; import "@/styles/globals.css"; import { MantineProvider } from "@mantine/core"; import "@mantine/core/styles.css"; @@ -14,16 +13,16 @@ import "@mantine/notifications/styles.css"; export default function App({ Component, pageProps }: AppProps) { return ( <> - {/* Initialize side-effect logic for Zustand store state files */} - - - - + + + + {/* Initialize side-effect logic for Zustand store state files */} + - - + + ); } diff --git a/packages/client/src/stores/useAppState.ts b/packages/client/src/stores/useAppState.ts index 67da6ed3..60fca8b2 100644 --- a/packages/client/src/stores/useAppState.ts +++ b/packages/client/src/stores/useAppState.ts @@ -18,7 +18,6 @@ interface IAppState { displayLoading: boolean; loading: boolean; error: boolean; - darkMode: boolean; appUnits: APPUNITS; favourites: string[]; connectionType: CONNECTIONTYPES; @@ -35,7 +34,6 @@ interface IAppState { interface AppStateStore { currentAppState: IAppState; setCurrentAppState: (updater: (prev: IAppState) => IAppState) => void; - toggleDarkMode: () => void; } export const useAppState = create((set) => ({ @@ -43,7 +41,6 @@ export const useAppState = create((set) => ({ appUnits: APPUNITS.METRIC, carLatency: 0, connectionType: CONNECTIONTYPES.DEMO, - darkMode: false, displayLoading: true, error: false, favourites: [], @@ -65,12 +62,4 @@ export const useAppState = create((set) => ({ set((state) => ({ currentAppState: updater(state.currentAppState), })), - - toggleDarkMode: () => - set((state) => ({ - currentAppState: { - ...state.currentAppState, - darkMode: !state.currentAppState.darkMode, - }, - })), }));