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/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/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 28aafb79..e5a054fd 100644 --- a/packages/client/src/components/containers/MapContainer.tsx +++ b/packages/client/src/components/containers/MapContainer.tsx @@ -2,8 +2,8 @@ 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 { useAppState } from "@/stores/useAppState"; +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/AppStateEffectsManager.tsx b/packages/client/src/components/global/AppStateEffectsManager.tsx new file mode 100644 index 00000000..b6b2ed5b --- /dev/null +++ b/packages/client/src/components/global/AppStateEffectsManager.tsx @@ -0,0 +1,119 @@ +"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, + 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..7e6d65f8 --- /dev/null +++ b/packages/client/src/components/global/EffectsProvider.tsx @@ -0,0 +1,15 @@ +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 ( + <> + + + + + + ); +} diff --git a/packages/client/src/components/global/LapDataListenerManager.tsx b/packages/client/src/components/global/LapDataListenerManager.tsx new file mode 100644 index 00000000..a71a806a --- /dev/null +++ b/packages/client/src/components/global/LapDataListenerManager.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; + +import { socketIO } from "@/components/global/SocketManager"; +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 { addLapData, clearLapData, fetchLapData } = useLapDataStore(); + + // Fetch initial lap data when manager mounts + useEffect(() => { + fetchLapData(); + }, [fetchLapData]); + + // Handle connection type changes + useEffect(() => { + // Playback mode: no listeners, clear data + if (currentAppState.playbackSwitch) { + clearLapData(); + return; + } + + // Network mode: attach socket listeners + if (currentAppState.connectionType === CONNECTIONTYPES.NETWORK) { + 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 () => { + socketIO.off("lapData", handleLapData); + socketIO.off("lapComplete", handleLapComplete); + }; + } + + // Demo mode: clear existing lap data + else if (currentAppState.connectionType === CONNECTIONTYPES.DEMO) { + clearLapData(); + } + + // 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 new file mode 100644 index 00000000..ec332b93 --- /dev/null +++ b/packages/client/src/components/global/PacketListenerManager.tsx @@ -0,0 +1,66 @@ +import { useEffect, useRef } from "react"; + +import { socketIO } from "@/components/global/SocketManager"; +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 { 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) { + cleanup(); + return; + } + + // Handle different connection types + if (currentAppState.connectionType === CONNECTIONTYPES.NETWORK) { + // Attach network listener + const handlePacket = (packet: ITelemetryData) => { + setCurrentPacket(packet); + }; + + socketIO.on("packet", handlePacket); + + return () => { + socketIO.off("packet", handlePacket); + }; + } else if (currentAppState.connectionType === CONNECTIONTYPES.DEMO) { + // 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 + } + + // Cleanup on unmount or mode change + return cleanup; + }, [ + currentAppState.connectionType, + currentAppState.playbackSwitch, + setCurrentPacket, + ]); + + return null; +} diff --git a/packages/client/src/contexts/SocketContext.tsx b/packages/client/src/components/global/SocketManager.tsx similarity index 68% rename from packages/client/src/contexts/SocketContext.tsx rename to packages/client/src/components/global/SocketManager.tsx index f10137eb..dd705c9b 100644 --- a/packages/client/src/contexts/SocketContext.tsx +++ b/packages/client/src/components/global/SocketManager.tsx @@ -1,15 +1,9 @@ -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 "@/contexts/AppStateContext"; +import { useAppState } from "@/stores/useAppState"; import { notifications } from "@mantine/notifications"; import type { CoordInfoUpdate, @@ -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/HeroMolecules/CarGraphicComponent.tsx b/packages/client/src/components/molecules/HeroMolecules/CarGraphicComponent.tsx index 25a379c8..2e2b291c 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 "@/stores/useAppState"; +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/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 00704b12..2e6bcf2f 100644 --- a/packages/client/src/components/molecules/LogoStatusMolecules/PlaybackDatePicker.tsx +++ b/packages/client/src/components/molecules/LogoStatusMolecules/PlaybackDatePicker.tsx @@ -1,8 +1,8 @@ import axios from "axios"; import React, { useEffect, useState } from "react"; -import { useAppState } from "@/contexts/AppStateContext"; -import { usePlaybackContext } from "@/contexts/PlayBackContext"; +import { useAppState } from "@/stores/useAppState"; +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/SettingsComponent.tsx b/packages/client/src/components/molecules/LogoStatusMolecules/SettingsComponent.tsx index f45b8973..8771a0d1 100644 --- a/packages/client/src/components/molecules/LogoStatusMolecules/SettingsComponent.tsx +++ b/packages/client/src/components/molecules/LogoStatusMolecules/SettingsComponent.tsx @@ -1,11 +1,7 @@ import { useTheme } from "next-themes"; import { useCallback, useState } from "react"; -import { - APPUNITS, - CONNECTIONTYPES, - useAppState, -} from "@/contexts/AppStateContext"; +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"; diff --git a/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx b/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx index 536c6362..ed055307 100644 --- a/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx +++ b/packages/client/src/components/molecules/LogoStatusMolecules/StatusComponent.tsx @@ -5,8 +5,8 @@ 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 { usePacket } from "@/contexts/PacketContext"; +import { CONNECTIONTYPES, useAppState } from "@/stores/useAppState"; +import { usePacketStore } from "@/stores/usePacket"; import { helios } from "@/styles/colors"; import { Switch } from "@mantine/core"; @@ -33,10 +33,10 @@ function PlaybackPickerComponent() { } function StatusComponent() { - const { resolvedTheme } = useTheme(); - const { currentAppState, setCurrentAppState } = useAppState(); - const { currentPacket } = usePacket(); + 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/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..9770dee4 100644 --- a/packages/client/src/components/molecules/MapMolecules/MapText.tsx +++ b/packages/client/src/components/molecules/MapMolecules/MapText.tsx @@ -1,9 +1,10 @@ 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"; 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/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..d382af04 100644 --- a/packages/client/src/components/tabs/AnalysisTab.tsx +++ b/packages/client/src/components/tabs/AnalysisTab.tsx @@ -4,6 +4,7 @@ import React, { useState } from "react"; 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 d28a7953..6997c2b7 100644 --- a/packages/client/src/components/tabs/RaceTab.tsx +++ b/packages/client/src/components/tabs/RaceTab.tsx @@ -6,7 +6,8 @@ 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 { 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"; @@ -37,7 +38,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 +122,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/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/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/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/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/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 600918fe..ec372650 100644 --- a/packages/client/src/pages/[[...slug]].tsx +++ b/packages/client/src/pages/[[...slug]].tsx @@ -4,41 +4,38 @@ 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 { PlaybackContextProvider } from "@/contexts/PlayBackContext"; +import { useAppState } from "@/stores/useAppState"; 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 e587a476..a7520a8a 100644 --- a/packages/client/src/pages/_app.tsx +++ b/packages/client/src/pages/_app.tsx @@ -1,11 +1,8 @@ import { ThemeProvider } from "next-themes"; import type { AppProps } from "next/app"; +import { EffectsProvider } from "@/components/global/EffectsProvider"; import LoadingWrapper from "@/components/global/LoadingWrapper"; -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"; import "@mantine/core/styles.css"; @@ -15,21 +12,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..60fca8b2 --- /dev/null +++ b/packages/client/src/stores/useAppState.ts @@ -0,0 +1,65 @@ +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; + 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; +} + +export const useAppState = create((set) => ({ + currentAppState: { + 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, + }, + + setCurrentAppState: (updater) => + set((state) => ({ + currentAppState: updater(state.currentAppState), + })), +})); diff --git a/packages/client/src/stores/useLapData.ts b/packages/client/src/stores/useLapData.ts new file mode 100644 index 00000000..2c4540e7 --- /dev/null +++ b/packages/client/src/stores/useLapData.ts @@ -0,0 +1,68 @@ +import axios from "axios"; +import { create } from "zustand"; + +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 { + formatLapData: (lapPacket: ILapData) => IFormattedLapData; + lapData: IFormattedLapData[]; + setLapData: (data: IFormattedLapData[]) => void; + addLapData: (data: IFormattedLapData) => void; + clearLapData: () => void; + fetchLapData: () => Promise; +} + +export const useLapDataStore = create((set) => ({ + addLapData: (data) => + set((state) => ({ + lapData: [...state.lapData, data], + })), + + clearLapData: () => set({ lapData: [] }), + + 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: formatLapData, + lapData: [], + setLapData: (data) => set({ lapData: data }), +})); diff --git a/packages/client/src/stores/usePacket.ts b/packages/client/src/stores/usePacket.ts new file mode 100644 index 00000000..90de78aa --- /dev/null +++ b/packages/client/src/stores/usePacket.ts @@ -0,0 +1,14 @@ +import { create } from "zustand"; + +import { generateFakeTelemetryData } from "@shared/helios-types"; +import type { ITelemetryData } from "@shared/helios-types"; + +interface IPacketStore { + currentPacket: ITelemetryData; + setCurrentPacket: (packet: ITelemetryData) => void; +} + +export const usePacketStore = create((set) => ({ + currentPacket: generateFakeTelemetryData(), + setCurrentPacket: (packet) => set({ currentPacket: packet }), +})); 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 }), +}));