diff --git a/.gitignore b/.gitignore index a547bf3..50c8dda 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..963354f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "printWidth": 120 +} diff --git a/package.json b/package.json index 76c2c42..f276bbb 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,14 @@ "@fortawesome/free-solid-svg-icons": "^7.1.0", "@fortawesome/react-fontawesome": "^3.1.1", "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-query": "^5.90.12", "@tanstack/react-router": "^1.141.6", + "clsx": "^2.1.1", + "country-flag-icons": "^1.6.4", + "konva": "^10.0.12", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-konva": "^19.2.1", "tailwindcss": "^4.1.18" }, "devDependencies": { diff --git a/src/app/config.ts b/src/app/config.ts new file mode 100644 index 0000000..c3da5a0 --- /dev/null +++ b/src/app/config.ts @@ -0,0 +1 @@ +export const cambase = import.meta.env.VITE_LOCAL_CAMBASE; diff --git a/src/app/providers/AppProviders.tsx b/src/app/providers/AppProviders.tsx new file mode 100644 index 0000000..212fdd3 --- /dev/null +++ b/src/app/providers/AppProviders.tsx @@ -0,0 +1,8 @@ +import { type PropsWithChildren } from "react"; +import { QueryProvider } from "./QueryProvider"; + +const AppProviders = ({ children }: PropsWithChildren) => { + return {children}; +}; + +export default AppProviders; diff --git a/src/app/providers/QueryProvider.tsx b/src/app/providers/QueryProvider.tsx new file mode 100644 index 0000000..33e98f2 --- /dev/null +++ b/src/app/providers/QueryProvider.tsx @@ -0,0 +1,7 @@ +import type { PropsWithChildren } from "react"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { queryClient } from "../queryClient"; + +export function QueryProvider({ children }: PropsWithChildren) { + return {children}; +} diff --git a/src/app/queryClient.ts b/src/app/queryClient.ts new file mode 100644 index 0000000..ee9f895 --- /dev/null +++ b/src/app/queryClient.ts @@ -0,0 +1,10 @@ +import { QueryClient } from "@tanstack/react-query"; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60_000, + refetchOnWindowFocus: false, + }, + }, +}); diff --git a/src/components/CardHeader.tsx b/src/components/CardHeader.tsx new file mode 100644 index 0000000..c8c2aba --- /dev/null +++ b/src/components/CardHeader.tsx @@ -0,0 +1,21 @@ +import clsx from "clsx"; + +type CardHeaderProps = { + title: string; +}; + +const CardHeader = ({ title }: CardHeaderProps) => { + return ( +
+
+

{title}

+
+
+ ); +}; + +export default CardHeader; diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx new file mode 100644 index 0000000..2b814a9 --- /dev/null +++ b/src/components/ui/Card.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import clsx from "clsx"; + +type CardProps = { + children: React.ReactNode; + className?: string; +}; + +const Card = ({ children, className }: CardProps) => { + return ( +
+ {children} +
+ ); +}; + +export default Card; diff --git a/src/ui/Footer.tsx b/src/components/ui/Footer.tsx similarity index 100% rename from src/ui/Footer.tsx rename to src/components/ui/Footer.tsx diff --git a/src/ui/Header.tsx b/src/components/ui/Header.tsx similarity index 100% rename from src/ui/Header.tsx rename to src/components/ui/Header.tsx diff --git a/src/features/dashboard/Dashboard.tsx b/src/features/dashboard/Dashboard.tsx new file mode 100644 index 0000000..f244267 --- /dev/null +++ b/src/features/dashboard/Dashboard.tsx @@ -0,0 +1,17 @@ +import SightingStack from "./components/sightingStack/SightingStack"; +import VideoFeed from "./components/videoFeed/VideoFeed"; +import { useSightingList } from "./hooks/useSightingList"; + +const Dashboard = () => { + const { sightingList, isLoading } = useSightingList(); + const mostRecent = sightingList[0]; + + return ( +
+ + +
+ ); +}; + +export default Dashboard; diff --git a/src/features/dashboard/components/platePatch/NumberPlate.tsx b/src/features/dashboard/components/platePatch/NumberPlate.tsx new file mode 100644 index 0000000..2f986cb --- /dev/null +++ b/src/features/dashboard/components/platePatch/NumberPlate.tsx @@ -0,0 +1,57 @@ +import { GB } from "country-flag-icons/react/3x2"; +import { formatNumberPlate } from "../../../../utils/utils"; + +type NumberPlateProps = { + vrm?: string | undefined; + motion?: boolean; + size?: "xs" | "sm" | "md" | "lg"; +}; + +const NumberPlate = ({ vrm, motion, size }: NumberPlateProps) => { + let options = { + plateWidth: "w-[14rem]", + textSize: "text-2xl", + borderWidth: "border-6", + }; + + switch (size) { + case "xs": + options = { + plateWidth: "w-[8rem]", + textSize: "text-md", + borderWidth: "border-4", + }; + break; + case "sm": + options = { + plateWidth: "w-[10rem]", + textSize: "text-lg", + borderWidth: "border-4", + }; + break; + case "lg": + options = { + plateWidth: "w-[16rem]", + textSize: "text-3xl", + borderWidth: "border-6", + }; + break; + } + + return ( +
+
+
+ +
+

{vrm && formatNumberPlate(vrm)}

+
+
+ ); +}; + +export default NumberPlate; diff --git a/src/features/dashboard/components/sightingStack/SightingItem.tsx b/src/features/dashboard/components/sightingStack/SightingItem.tsx new file mode 100644 index 0000000..4009b44 --- /dev/null +++ b/src/features/dashboard/components/sightingStack/SightingItem.tsx @@ -0,0 +1,23 @@ +import type { SightingType } from "../../../../utils/types"; +import NumberPlate from "../platePatch/NumberPlate"; + +type SightingItemProps = { + sighting: SightingType; +}; + +const SightingItem = ({ sighting }: SightingItemProps) => { + console.log(sighting); + const motion = sighting.motion.toLowerCase() === "away" ? true : false; + return ( +
+
+
Ref: {sighting.ref}
+
vrm: {sighting.vrm}
+
+ + +
+ ); +}; + +export default SightingItem; diff --git a/src/features/dashboard/components/sightingStack/SightingStack.tsx b/src/features/dashboard/components/sightingStack/SightingStack.tsx new file mode 100644 index 0000000..69917e3 --- /dev/null +++ b/src/features/dashboard/components/sightingStack/SightingStack.tsx @@ -0,0 +1,20 @@ +import CardHeader from "../../../../components/CardHeader"; +import Card from "../../../../components/ui/Card"; +import type { SightingType } from "../../../../utils/types"; +import SightingItem from "./SightingItem"; + +type SightingStackProps = { + sightings: SightingType[]; +}; +const SightingStack = ({ sightings }: SightingStackProps) => { + return ( + + + {sightings.map((sighting) => ( + + ))} + + ); +}; + +export default SightingStack; diff --git a/src/features/dashboard/components/videoFeed/VideoFeed.tsx b/src/features/dashboard/components/videoFeed/VideoFeed.tsx new file mode 100644 index 0000000..44ec195 --- /dev/null +++ b/src/features/dashboard/components/videoFeed/VideoFeed.tsx @@ -0,0 +1,88 @@ +import { Stage, Layer, Image, Rect } from "react-konva"; +import type { SightingType } from "../../../../utils/types"; +import { useCreateVideoSnapshot } from "../../hooks/useCreateVideoSnapshot"; +import { useEffect, useState } from "react"; + +type VideoFeedProps = { + mostRecentSighting: SightingType; + isLoading: boolean; +}; + +const VideoFeed = ({ mostRecentSighting, isLoading }: VideoFeedProps) => { + 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); + }; + + useEffect(() => { + const updateSize = () => { + const width = window.innerWidth * 0.5; + const height = (width * 3) / 4; + setSize({ width, height }); + }; + updateSize(); + window.addEventListener("resize", updateSize); + return () => window.removeEventListener("resize", updateSize); + }, []); + + if (isLoading) return <>Loading...; + + return ( +
+ handleModeChange(mode + 1)}> + + { + const container = e.target.getStage()?.container(); + if (container) container.style.cursor = "pointer"; + }} + onMouseLeave={(e) => { + const container = e.target.getStage()?.container(); + if (container) container.style.cursor = "default"; + }} + cornerRadius={10} + /> + + {plateRect && mode === 1 && ( + + + + )} + + {plateTrack && mode === 2 && ( + + {plateTrack.map((rect, index) => ( + + ))} + + )} + +
+ ); +}; + +export default VideoFeed; diff --git a/src/features/dashboard/hooks/useCreateVideoSnapshot.ts b/src/features/dashboard/hooks/useCreateVideoSnapshot.ts new file mode 100644 index 0000000..f682e12 --- /dev/null +++ b/src/features/dashboard/hooks/useCreateVideoSnapshot.ts @@ -0,0 +1,19 @@ +import { useRef } from "react"; +import type { SightingType } from "../../../utils/types"; + +export const useCreateVideoSnapshot = (mostRecentSighting: SightingType) => { + const latestBitMapRef = useRef(null); + const snapshotUrl = mostRecentSighting?.overviewUrl; + + const image = new Image(); + + image.src = snapshotUrl; + + console.log(mostRecentSighting); + + const plateRect = mostRecentSighting?.overviewPlateRect; + + const plateTrack = mostRecentSighting?.plateTrack; + + return { snapshotUrl, latestBitMapRef, image, plateRect, plateTrack }; +}; diff --git a/src/features/dashboard/hooks/useSightingList.ts b/src/features/dashboard/hooks/useSightingList.ts new file mode 100644 index 0000000..7c5900b --- /dev/null +++ b/src/features/dashboard/hooks/useSightingList.ts @@ -0,0 +1,27 @@ +import { useEffect, useRef, useState } from "react"; +import { useVideoFeed } from "./useVideoFeed"; +import type { SightingType } from "../../../utils/types"; + +export const useSightingList = () => { + const [sightingList, setSightingList] = useState([]); + const { videoFeedQuery } = useVideoFeed(); + const latestSighting = videoFeedQuery?.data; + const lastProcessedRef = useRef(-1); + const isLoading = videoFeedQuery?.isPending; + + useEffect(() => { + if (!latestSighting || latestSighting.ref === undefined || latestSighting.ref === -1) return; + + if (latestSighting.ref !== lastProcessedRef.current) { + lastProcessedRef.current = latestSighting.ref; + + // eslint-disable-next-line react-hooks/set-state-in-effect + setSightingList((prevList) => { + if (prevList[0]?.ref === latestSighting.ref) return prevList; + const dedupPrev = prevList.filter((s) => s.ref !== latestSighting.ref); + return [latestSighting, ...dedupPrev].slice(0, 10); + }); + } + }, [latestSighting, latestSighting?.ref]); + return { sightingList, isLoading }; +}; diff --git a/src/features/dashboard/hooks/useVideoFeed.ts b/src/features/dashboard/hooks/useVideoFeed.ts new file mode 100644 index 0000000..9ec65eb --- /dev/null +++ b/src/features/dashboard/hooks/useVideoFeed.ts @@ -0,0 +1,32 @@ +import { useQuery } from "@tanstack/react-query"; +import { cambase } from "../../../app/config"; +import { useEffect, useRef } from "react"; + +const fetchVideoFeed = async (refId: number) => { + const response = await fetch(`${cambase}/mergedHistory/sightingSummary?mostRecentRef=${refId}`); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); +}; + +export const useVideoFeed = () => { + const currentRefId = useRef(-1); + + const videoFeedQuery = useQuery({ + queryKey: ["videoFeed"], + queryFn: () => fetchVideoFeed(currentRefId.current), + refetchInterval: 1000, + refetchOnWindowFocus: false, + retry: false, + staleTime: 0, + }); + + useEffect(() => { + if (videoFeedQuery.data?.ref !== -1) { + currentRefId.current = videoFeedQuery?.data?.ref; + } + }, [videoFeedQuery.data]); + + return { videoFeedQuery }; +}; diff --git a/src/main.tsx b/src/main.tsx index 8aa86cf..8673bda 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client"; import "./index.css"; import { RouterProvider, createRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen.ts"; +import AppProviders from "./app/providers/AppProviders.tsx"; const router = createRouter({ routeTree, @@ -17,6 +18,8 @@ declare module "@tanstack/react-router" { createRoot(document.getElementById("root")!).render( - - + + + + , ); diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 0115855..e8e601b 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,6 +1,6 @@ import { createRootRoute, Outlet } from "@tanstack/react-router"; -import Header from "../ui/Header"; -import Footer from "../ui/Footer"; +import Header from "../components/ui/Header"; +import Footer from "../components/ui/Footer"; const RootLayout = () => { return ( diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 88639e6..eba55f3 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,9 +1,10 @@ import { createFileRoute } from "@tanstack/react-router"; +import Dashboard from "../features/dashboard/Dashboard"; export const Route = createFileRoute("/")({ component: RouteComponent, }); function RouteComponent() { - return
Hello "/"!
; + return ; } diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..c518fa3 --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,53 @@ +export type SightingType = { + ref: number; + SaFID: string; + overviewUrl: string; + plateUrlInfrared: string; + plateUrlColour: string; + vrm: string; + vrmSecondary: string; + countryCode: string; + timeStamp: string; + detailsUrl: string; + overviewPlateRect?: [number, number, number, number]; + plateTrack?: [number, number, number, number][]; + make: string; + model: string; + color: string; + category: string; + charHeight: string; + seenCount: string; + timeStampMillis: number; + motion: string; + debug: string; + srcCam: number; + locationName: string; + laneID: string; + plateSize: string; + overviewSize: string; + radarSpeed: string; + trackSpeed: string; + metadata?: Metadata; +}; + +export type Metadata = { + npedJSON: NpedJSON; + "hotlist-matches": HotlistMatches; + hotlistMatches: HotlistMatches; +}; + +export type HotlistMatches = { + Hotlist0: boolean; + Hotlist1: boolean; + Hotlist2: boolean; +}; + +export type NpedJSON = { + status_code: number; + reason_phrase: string; + "NPED CATEGORY": "A" | "B" | "C" | "D"; + "MOT STATUS": boolean; + "TAX STATUS": boolean; + vrm: string; + "INSURANCE STATUS": string; +}; diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..0e208a4 --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,6 @@ +export const formatNumberPlate = (plate: string) => { + const splittedPlate = plate?.split(""); + splittedPlate?.splice(4, 0, " "); + const formattedPlate = splittedPlate?.join(""); + return formattedPlate; +}; diff --git a/yarn.lock b/yarn.lock index 3b0bf49..d14dca0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -793,6 +793,18 @@ resolved "https://registry.yarnpkg.com/@tanstack/history/-/history-1.141.0.tgz#6fa5119dd870f1617943903d30e799222a21f6c2" integrity sha512-LS54XNyxyTs5m/pl1lkwlg7uZM3lvsv2FIIV1rsJgnfwVCnI+n4ZGZ2CcjNT13BPu/3hPP+iHmliBSscJxW5FQ== +"@tanstack/query-core@5.90.12": + version "5.90.12" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.90.12.tgz#e1f5f47e72ef7d0fc794325936921c700352515e" + integrity sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg== + +"@tanstack/react-query@^5.90.12": + version "5.90.12" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.90.12.tgz#49536842eff6487a9e645a453fea2642d8f4f209" + integrity sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg== + dependencies: + "@tanstack/query-core" "5.90.12" + "@tanstack/react-router@^1.141.6": version "1.141.6" resolved "https://registry.yarnpkg.com/@tanstack/react-router/-/react-router-1.141.6.tgz#295a278c377a2d49f385f3f5a4cb53566caf53e2" @@ -953,6 +965,16 @@ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c" integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== +"@types/react-reconciler@^0.28.9": + version "0.28.9" + resolved "https://registry.yarnpkg.com/@types/react-reconciler/-/react-reconciler-0.28.9.tgz#d24b4864c384e770c83275b3fe73fba00269c83b" + integrity sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg== + +"@types/react-reconciler@^0.32.3": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@types/react-reconciler/-/react-reconciler-0.32.3.tgz#eb4b346f367f29f07628032934d30a4f3f9eaba7" + integrity sha512-cMi5ZrLG7UtbL7LTK6hq9w/EZIRk4Mf1Z5qHoI+qBh7/WkYkFXQ7gOto2yfUvPzF5ERMAhaXS5eTQ2SAnHjLzA== + "@types/react@^19.2.5": version "19.2.7" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.7.tgz#84e62c0f23e8e4e5ac2cadcea1ffeacccae7f62f" @@ -1211,6 +1233,11 @@ chokidar@^3.6.0: optionalDependencies: fsevents "~2.3.2" +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -1238,6 +1265,11 @@ cookie-es@^2.0.0: resolved "https://registry.yarnpkg.com/cookie-es/-/cookie-es-2.0.0.tgz#ca6163d7ef8686ea6bbdd551f1de575569c1ed69" integrity sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg== +country-flag-icons@^1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/country-flag-icons/-/country-flag-icons-1.6.4.tgz#ddf9d07465678ab0a5b04b0d2f60717c2f46cd6f" + integrity sha512-Z3Zi419FI889tlElMsVhCIS5eRkiLDWixr576J5DPiTe5RGxpbRi+enMpHdYVp5iK5WFjr8P/RgyIFAGhFsiFg== + cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" @@ -1616,6 +1648,13 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +its-fine@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/its-fine/-/its-fine-2.0.0.tgz#a90b18a3ee4c211a1fb6faac2abcc2b682ce1f21" + integrity sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng== + dependencies: + "@types/react-reconciler" "^0.28.9" + jiti@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92" @@ -1665,6 +1704,11 @@ keyv@^4.5.4: dependencies: json-buffer "3.0.1" +konva@^10.0.12: + version "10.0.12" + resolved "https://registry.yarnpkg.com/konva/-/konva-10.0.12.tgz#4d0c8d94b007c67b73630ea6369bc272f86d7e29" + integrity sha512-DHmkeG5FbW6tLCkbMQTi1ihWycfzljrn0V7umUUuewxx7aoINcI71ksgBX9fTPNXhlsK4/JoMgKwI/iCde+BRw== + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -1906,6 +1950,23 @@ react-dom@^19.2.0: dependencies: scheduler "^0.27.0" +react-konva@^19.2.1: + version "19.2.1" + resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-19.2.1.tgz#a8009c884d29ad5806adcc99ca83804e13f5ef76" + integrity sha512-sqZWCzQGpdMrU5aeunR0oxUY8UeCPbU8gnAYxMtAn6BT4coeSpiATKOctsoxRu6F56TAcF+s0c6Lul9ansNqQA== + dependencies: + "@types/react-reconciler" "^0.32.3" + its-fine "^2.0.0" + react-reconciler "0.33.0" + scheduler "0.27.0" + +react-reconciler@0.33.0: + version "0.33.0" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.33.0.tgz#9dd20208d45baa5b0b4701781f858236657f15e1" + integrity sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA== + dependencies: + scheduler "^0.27.0" + react-refresh@^0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" @@ -1975,7 +2036,7 @@ rollup@^4.43.0: "@rollup/rollup-win32-x64-msvc" "4.53.5" fsevents "~2.3.2" -scheduler@^0.27.0: +scheduler@0.27.0, scheduler@^0.27.0: version "0.27.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==