diff --git a/src/app/context/CameraSettingsContext.ts b/src/app/context/CameraSettingsContext.ts new file mode 100644 index 0000000..4ee294b --- /dev/null +++ b/src/app/context/CameraSettingsContext.ts @@ -0,0 +1,16 @@ +import { createContext, useContext } from "react"; +import type { CameraSettings, CameraSettingsAction } from "../../utils/types"; + +type CameraSettingsContextType = { + state: CameraSettings; + dispatch: (state: CameraSettingsAction) => void; +}; +export const CameraSettingsContext = createContext(null); + +export const useCameraSettingsContext = () => { + const context = useContext(CameraSettingsContext); + if (!context) { + throw new Error("useCameraSettingsContext must be used within a CameraSettingsProvider"); + } + return context; +}; diff --git a/src/app/providers/AppProviders.tsx b/src/app/providers/AppProviders.tsx index 212fdd3..9018fbf 100644 --- a/src/app/providers/AppProviders.tsx +++ b/src/app/providers/AppProviders.tsx @@ -1,8 +1,13 @@ import { type PropsWithChildren } from "react"; import { QueryProvider } from "./QueryProvider"; +import CameraSettingsProvider from "./CameraSettingsProvider"; const AppProviders = ({ children }: PropsWithChildren) => { - return {children}; + return ( + + {children} + + ); }; export default AppProviders; diff --git a/src/app/providers/CameraSettingsProvider.tsx b/src/app/providers/CameraSettingsProvider.tsx new file mode 100644 index 0000000..f33ee72 --- /dev/null +++ b/src/app/providers/CameraSettingsProvider.tsx @@ -0,0 +1,11 @@ +import { CameraSettingsContext } from "../context/CameraSettingsContext"; +import { useReducer, type ReactNode } from "react"; +import { initialState, cameraSettingsReducer } from "../reducers/cameraSettingsReducer"; + +const CameraSettingsProvider = ({ children }: { children: ReactNode }) => { + const [state, dispatch] = useReducer(cameraSettingsReducer, initialState); + + return {children}; +}; + +export default CameraSettingsProvider; diff --git a/src/app/reducers/cameraSettingsReducer.ts b/src/app/reducers/cameraSettingsReducer.ts new file mode 100644 index 0000000..d2573c7 --- /dev/null +++ b/src/app/reducers/cameraSettingsReducer.ts @@ -0,0 +1,17 @@ +import type { CameraSettings, CameraSettingsAction } from "../../utils/types"; + +export const initialState: CameraSettings = { + mode: 0, +}; + +export const cameraSettingsReducer = (state: CameraSettings, action: CameraSettingsAction) => { + switch (action.type) { + case "SET_MODE": + return { + ...state, + mode: action.payload, + }; + default: + return state; + } +}; diff --git a/src/features/dashboard/Dashboard.tsx b/src/features/dashboard/Dashboard.tsx index f244267..2b34df1 100644 --- a/src/features/dashboard/Dashboard.tsx +++ b/src/features/dashboard/Dashboard.tsx @@ -1,3 +1,4 @@ +import PlateRead from "./components/plateRead/PlateRead"; import SightingStack from "./components/sightingStack/SightingStack"; import VideoFeed from "./components/videoFeed/VideoFeed"; import { useSightingList } from "./hooks/useSightingList"; @@ -7,8 +8,11 @@ const Dashboard = () => { const mostRecent = sightingList[0]; return ( -
- +
+
+ + +
); diff --git a/src/features/dashboard/components/platePatch/NumberPlate.tsx b/src/features/dashboard/components/platePatch/NumberPlate.tsx index 2f986cb..0a485bc 100644 --- a/src/features/dashboard/components/platePatch/NumberPlate.tsx +++ b/src/features/dashboard/components/platePatch/NumberPlate.tsx @@ -41,7 +41,7 @@ const NumberPlate = ({ vrm, motion, size }: NumberPlateProps) => { return (
diff --git a/src/features/dashboard/components/plateRead/PlateRead.tsx b/src/features/dashboard/components/plateRead/PlateRead.tsx new file mode 100644 index 0000000..bfd0505 --- /dev/null +++ b/src/features/dashboard/components/plateRead/PlateRead.tsx @@ -0,0 +1,56 @@ +import type { SightingType } from "../../../../utils/types"; +import Card from "../../../../components/ui/Card"; +import CardHeader from "../../../../components/CardHeader"; + +type PlateReadProps = { + sighting: SightingType; +}; + +const PlateRead = ({ sighting }: PlateReadProps) => { + console.log(sighting); + const vrm = sighting?.vrm; + const region = sighting?.laneID; + const timestamp = sighting?.timeStamp; + const seenCount = sighting?.seenCount; + const radarSpeed = sighting?.radarSpeed; + const motion = sighting?.motion; + const countryCode = sighting?.countryCode; + const plateColorUrl = sighting?.plateUrlColour; + + if (!sighting) return
No sighting data available.
; + + return ( + + +
+
VRM:
+
{vrm}
+
Region:
+
{region}
+
Timestamp:
+
{timestamp}
+ +
Seen Count:
+
{seenCount}
+ +
Radar Speed:
+
{radarSpeed} mph
+ +
Motion:
+
{motion}
+ +
Country Code:
+
{countryCode}
+ +
Plate Image:
+
+ + View Image + +
+
+
+ ); +}; + +export default PlateRead; diff --git a/src/features/dashboard/components/sightingStack/SightingItem.tsx b/src/features/dashboard/components/sightingStack/SightingItem.tsx index 4009b44..a216af1 100644 --- a/src/features/dashboard/components/sightingStack/SightingItem.tsx +++ b/src/features/dashboard/components/sightingStack/SightingItem.tsx @@ -1,4 +1,5 @@ import type { SightingType } from "../../../../utils/types"; +import { timeAgo } from "../../../../utils/utils"; import NumberPlate from "../platePatch/NumberPlate"; type SightingItemProps = { @@ -6,15 +7,20 @@ type SightingItemProps = { }; const SightingItem = ({ sighting }: SightingItemProps) => { - console.log(sighting); const motion = sighting.motion.toLowerCase() === "away" ? true : false; + + const timeStamp = timeAgo(sighting.timeStampMillis); + return (
-
Ref: {sighting.ref}
-
vrm: {sighting.vrm}
+
+ {timeStamp} +
+
+ {sighting.vrm} +
-
); diff --git a/src/features/dashboard/components/sightingStack/SightingStack.tsx b/src/features/dashboard/components/sightingStack/SightingStack.tsx index 69917e3..6c8d94d 100644 --- a/src/features/dashboard/components/sightingStack/SightingStack.tsx +++ b/src/features/dashboard/components/sightingStack/SightingStack.tsx @@ -8,11 +8,13 @@ type SightingStackProps = { }; const SightingStack = ({ sightings }: SightingStackProps) => { return ( - - - {sightings.map((sighting) => ( - - ))} + + +
+ {sightings.map((sighting) => ( + + ))} +
); }; diff --git a/src/features/dashboard/components/videoFeed/VideoButton.tsx b/src/features/dashboard/components/videoFeed/VideoButton.tsx new file mode 100644 index 0000000..4b0a4db --- /dev/null +++ b/src/features/dashboard/components/videoFeed/VideoButton.tsx @@ -0,0 +1,42 @@ +import { useState } from "react"; +import { Text, Group, Rect } from "react-konva"; + +type VideoButtonProps = { + x?: number; + y?: number; + onClick?: () => void; + text?: string; +}; + +const VideoButton = ({ x, y, onClick, text }: VideoButtonProps) => { + const [isHovered, setIsHovered] = useState(false); + return ( + { + setIsHovered(true); + const container = e.target.getStage()?.container(); + if (container) container.style.cursor = "pointer"; + }} + onMouseLeave={(e) => { + setIsHovered(false); + const container = e.target.getStage()?.container(); + if (container) container.style.cursor = "default"; + }} + > + + + + ); +}; + +export default VideoButton; diff --git a/src/features/dashboard/components/videoFeed/VideoFeed.tsx b/src/features/dashboard/components/videoFeed/VideoFeed.tsx index 44ec195..28e22b6 100644 --- a/src/features/dashboard/components/videoFeed/VideoFeed.tsx +++ b/src/features/dashboard/components/videoFeed/VideoFeed.tsx @@ -2,6 +2,7 @@ import { Stage, Layer, Image, Rect } from "react-konva"; import type { SightingType } from "../../../../utils/types"; import { useCreateVideoSnapshot } from "../../hooks/useCreateVideoSnapshot"; import { useEffect, useState } from "react"; +import { useCameraSettingsContext } from "../../../../app/context/CameraSettingsContext"; type VideoFeedProps = { mostRecentSighting: SightingType; @@ -9,19 +10,22 @@ type VideoFeedProps = { }; const VideoFeed = ({ mostRecentSighting, isLoading }: VideoFeedProps) => { + const { state: cameraSettings, dispatch } = useCameraSettingsContext(); + + const mode = cameraSettings.mode; const [size, setSize] = useState<{ width: number; height: number }>({ width: 1280, height: 960 }); - const [mode, setMode] = useState(0); + const { image, plateRect, plateTrack } = useCreateVideoSnapshot(mostRecentSighting); const handleModeChange = (newMode: number) => { - if (newMode > 2) setMode(0); - else setMode(newMode); + if (newMode > 2) dispatch({ type: "SET_MODE", payload: 0 }); + else dispatch({ type: "SET_MODE", payload: newMode }); }; useEffect(() => { const updateSize = () => { - const width = window.innerWidth * 0.5; - const height = (width * 3) / 4; + const width = window.innerWidth * 0.48; + const height = (width * 2) / 3; setSize({ width, height }); }; updateSize(); @@ -59,7 +63,7 @@ const VideoFeed = ({ mostRecentSighting, isLoading }: VideoFeedProps) => { height={plateRect?.[3] * size.height} stroke="blue" strokeWidth={4} - zIndex={1} + cornerRadius={5} /> )} @@ -75,7 +79,7 @@ const VideoFeed = ({ mostRecentSighting, isLoading }: VideoFeedProps) => { height={rect[3] * size.height} stroke="red" strokeWidth={2} - zIndex={1} + cornerRadius={5} /> ))} diff --git a/src/features/dashboard/hooks/useCreateVideoSnapshot.ts b/src/features/dashboard/hooks/useCreateVideoSnapshot.ts index f682e12..a7aa9c4 100644 --- a/src/features/dashboard/hooks/useCreateVideoSnapshot.ts +++ b/src/features/dashboard/hooks/useCreateVideoSnapshot.ts @@ -9,8 +9,6 @@ export const useCreateVideoSnapshot = (mostRecentSighting: SightingType) => { image.src = snapshotUrl; - console.log(mostRecentSighting); - const plateRect = mostRecentSighting?.overviewPlateRect; const plateTrack = mostRecentSighting?.plateTrack; diff --git a/src/utils/types.ts b/src/utils/types.ts index c518fa3..b0f29c1 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -51,3 +51,11 @@ export type NpedJSON = { vrm: string; "INSURANCE STATUS": string; }; + +export type CameraSettings = { + mode: number; +}; +export type CameraSettingsAction = { + type: "SET_MODE"; + payload: number; +}; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 0e208a4..2c9a3f3 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -4,3 +4,20 @@ export const formatNumberPlate = (plate: string) => { const formattedPlate = splittedPlate?.join(""); return formattedPlate; }; + +export const timeAgo = (timestampmili: number) => { + const diffMs = Date.now() - new Date(timestampmili).getTime(); + const diffMins = Math.floor(diffMs / 60000); + if (diffMins < 60) { + if (diffMins < 1) { + return "just now"; + } + return `${diffMins === 1 ? "1 minute" : diffMins + " minutes"} ago`; + } else { + const diffHours = Math.floor(diffMins / 60); + if (diffHours < 1) { + return "just now"; + } + return `${diffHours === 1 ? "1 hour" : diffHours + " hours"} ago`; + } +};