Add video feed feature with related components and hooks
- Implemented VideoFeed component to display sightings. - Created useSightingList and useVideoFeed hooks for data fetching and state management. - Added AppProviders for context management. - Updated Dashboard to include VideoFeed. - Introduced types for sightings in utils/types.tsx. - Added Header and Footer components for layout. - Configured React Query for data handling.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,3 +22,5 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
.env
|
||||||
|
|||||||
3
.prettierrc
Normal file
3
.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||||
"@fortawesome/react-fontawesome": "^3.1.1",
|
"@fortawesome/react-fontawesome": "^3.1.1",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"@tanstack/react-router": "^1.141.6",
|
"@tanstack/react-router": "^1.141.6",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
|||||||
1
src/app/config.ts
Normal file
1
src/app/config.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const cambase = import.meta.env.VITE_LOCAL_CAMBASE;
|
||||||
8
src/app/providers/AppProviders.tsx
Normal file
8
src/app/providers/AppProviders.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { type PropsWithChildren } from "react";
|
||||||
|
import { QueryProvider } from "./QueryProvider";
|
||||||
|
|
||||||
|
const AppProviders = ({ children }: PropsWithChildren) => {
|
||||||
|
return <QueryProvider>{children}</QueryProvider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppProviders;
|
||||||
7
src/app/providers/QueryProvider.tsx
Normal file
7
src/app/providers/QueryProvider.tsx
Normal file
@@ -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 <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||||
|
}
|
||||||
10
src/app/queryClient.ts
Normal file
10
src/app/queryClient.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 60_000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
12
src/features/dashboard/Dashboard.tsx
Normal file
12
src/features/dashboard/Dashboard.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import VideoFeed from "./components/videoFeed/VideoFeed";
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Dashboard
|
||||||
|
<VideoFeed />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
18
src/features/dashboard/components/videoFeed/VideoFeed.tsx
Normal file
18
src/features/dashboard/components/videoFeed/VideoFeed.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useSightingList } from "../../hooks/useSightingList";
|
||||||
|
|
||||||
|
const VideoFeed = () => {
|
||||||
|
const { sightingList } = useSightingList();
|
||||||
|
console.log(sightingList);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{sightingList.map((sighting) => (
|
||||||
|
<div key={sighting.ref} className="border p-2 mb-2">
|
||||||
|
<div>Ref: {sighting.ref}</div>
|
||||||
|
<div>vrm: {sighting.vrm}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VideoFeed;
|
||||||
26
src/features/dashboard/hooks/useSightingList.ts
Normal file
26
src/features/dashboard/hooks/useSightingList.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useVideoFeed } from "./useVideoFeed";
|
||||||
|
import type { SightingType } from "../../../utils/types";
|
||||||
|
|
||||||
|
export const useSightingList = () => {
|
||||||
|
const [sightingList, setSightingList] = useState<SightingType[]>([]);
|
||||||
|
const { videoFeedQuery } = useVideoFeed();
|
||||||
|
const latestSighting = videoFeedQuery?.data;
|
||||||
|
const lastProcessedRef = useRef<number>(-1);
|
||||||
|
|
||||||
|
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 };
|
||||||
|
};
|
||||||
32
src/features/dashboard/hooks/useVideoFeed.ts
Normal file
32
src/features/dashboard/hooks/useVideoFeed.ts
Normal file
@@ -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<number>(-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 };
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
|
|||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||||
import { routeTree } from "./routeTree.gen.ts";
|
import { routeTree } from "./routeTree.gen.ts";
|
||||||
|
import AppProviders from "./app/providers/AppProviders.tsx";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
routeTree,
|
routeTree,
|
||||||
@@ -17,6 +18,8 @@ declare module "@tanstack/react-router" {
|
|||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<AppProviders>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</StrictMode>
|
</AppProviders>
|
||||||
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createRootRoute, Outlet } from "@tanstack/react-router";
|
import { createRootRoute, Outlet } from "@tanstack/react-router";
|
||||||
import Header from "../ui/Header";
|
import Header from "../components/ui/Header";
|
||||||
import Footer from "../ui/Footer";
|
import Footer from "../components/ui/Footer";
|
||||||
|
|
||||||
const RootLayout = () => {
|
const RootLayout = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import Dashboard from "../features/dashboard/Dashboard";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
return <div className="">Hello "/"!</div>;
|
return <Dashboard />;
|
||||||
}
|
}
|
||||||
|
|||||||
53
src/utils/types.tsx
Normal file
53
src/utils/types.tsx
Normal file
@@ -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;
|
||||||
|
};
|
||||||
12
yarn.lock
12
yarn.lock
@@ -793,6 +793,18 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tanstack/history/-/history-1.141.0.tgz#6fa5119dd870f1617943903d30e799222a21f6c2"
|
resolved "https://registry.yarnpkg.com/@tanstack/history/-/history-1.141.0.tgz#6fa5119dd870f1617943903d30e799222a21f6c2"
|
||||||
integrity sha512-LS54XNyxyTs5m/pl1lkwlg7uZM3lvsv2FIIV1rsJgnfwVCnI+n4ZGZ2CcjNT13BPu/3hPP+iHmliBSscJxW5FQ==
|
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":
|
"@tanstack/react-router@^1.141.6":
|
||||||
version "1.141.6"
|
version "1.141.6"
|
||||||
resolved "https://registry.yarnpkg.com/@tanstack/react-router/-/react-router-1.141.6.tgz#295a278c377a2d49f385f3f5a4cb53566caf53e2"
|
resolved "https://registry.yarnpkg.com/@tanstack/react-router/-/react-router-1.141.6.tgz#295a278c377a2d49f385f3f5a4cb53566caf53e2"
|
||||||
|
|||||||
Reference in New Issue
Block a user