updated NPED code

This commit is contained in:
2025-08-29 10:07:59 +01:00
parent 5ededd8e05
commit d2b8827987
16 changed files with 285 additions and 150 deletions

View File

@@ -4,17 +4,20 @@ import { Route, Routes } from "react-router";
import FrontCamera from "./pages/FrontCamera";
import RearCamera from "./pages/RearCamera";
import SystemSettings from "./pages/SystemSettings";
import { NPEDUserProvider } from "./context/providers/NPEDUserContextProvider";
function App() {
return (
<Routes>
<Route path="/" element={<Container />}>
<Route index element={<Dashboard />} />
<Route path="front-camera-settings" element={<FrontCamera />} />
<Route path="rear-camera-settings" element={<RearCamera />} />
<Route path="system-settings" element={<SystemSettings />} />
</Route>
</Routes>
<NPEDUserProvider>
<Routes>
<Route path="/" element={<Container />}>
<Route index element={<Dashboard />} />
<Route path="front-camera-settings" element={<FrontCamera />} />
<Route path="rear-camera-settings" element={<RearCamera />} />
<Route path="system-settings" element={<SystemSettings />} />
</Route>
</Routes>
</NPEDUserProvider>
);
}

View File

@@ -1,8 +1,11 @@
import Card from "../../UI/Card";
import CardHeader from "../../UI/CardHeader";
import NPEDFields from "./NPEDFields";
import { useNPEDContext } from "../../../context/NPEDUserContext";
const NPEDCard = () => {
const { user } = useNPEDContext();
console.log(user);
return (
<Card>
<CardHeader title={"NPED Config"} />

View File

@@ -1,62 +1,97 @@
import { Form, Formik, Field } from "formik";
import FormGroup from "../components/FormGroup";
import type { NPEDFieldType } from "../../../types/types";
import { useNPEDAuth } from "../../../hooks/useNPEDAuh";
import type { NPEDErrorValues, NPEDFieldType } from "../../../types/types";
import { useNPEDAuth } from "../../../hooks/useNPEDAuth";
import { toast } from "sonner";
const NPEDFields = () => {
const { signIn, isError } = useNPEDAuth();
const { signIn } = useNPEDAuth();
const initialValues = {
username: "",
password: "",
clientId: "",
frontId: "NPEDFront",
rearId: "NPEDRear",
};
const handleSubmit = (values: NPEDFieldType) => {
console.log(isError);
signIn(values);
const valuesToSend = {
...values,
};
signIn(valuesToSend);
toast("Signed in successfully");
};
const validateValues = (values: NPEDFieldType) => {
const errors: NPEDErrorValues = {};
if (!values.username) errors.username = "Required";
if (!values.password) errors.password = "Required";
if (!values.clientId) errors.clientId = "Required";
return errors;
};
return (
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
<Form className="flex flex-col space-y-2">
<FormGroup>
<label htmlFor="username">Username</label>
<Field
name="username"
type="text"
id="username"
placeholder="NPED username"
className="p-1.5 border border-gray-400 rounded-lg"
/>
</FormGroup>
<FormGroup>
<label htmlFor="password">Password</label>
<Field
name="password"
type="password"
id="password"
placeholder="NPED Password"
className="p-1.5 border border-gray-400 rounded-lg"
/>
</FormGroup>
<FormGroup>
<label htmlFor="clientId">Client ID</label>
<Field
name="clientId"
type="text"
id="clientId"
placeholder="NPED client ID"
className="p-1.5 border border-gray-400 rounded-lg"
/>
</FormGroup>
<button
type="submit"
className="w-1/4 text-white bg-green-700 hover:bg-green-800 font-small rounded-lg text-sm px-2 py-2.5"
>
Login
</button>
</Form>
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validate={validateValues}
>
{({ errors, touched }) => (
<Form className="flex flex-col space-y-5">
<FormGroup>
<label htmlFor="username">Username</label>
{touched.username && errors.username && (
<small className="absolute right-0 -top-5 text-red-500">
{errors.username}
</small>
)}
<Field
name="username"
type="text"
id="username"
placeholder="NPED username"
className="p-1.5 border border-gray-400 rounded-lg"
/>
</FormGroup>
<FormGroup>
<label htmlFor="password">Password</label>
{touched.password && errors.password && (
<small className="absolute right-0 -top-5 text-red-500">
{errors.password}
</small>
)}
<Field
name="password"
type="password"
id="password"
placeholder="NPED Password"
className="p-1.5 border border-gray-400 rounded-lg"
/>
</FormGroup>
<FormGroup>
<label htmlFor="clientId">Client ID</label>
{touched.clientId && errors.clientId && (
<small className="absolute right-0 -top-5 text-red-500">
{errors.clientId}
</small>
)}
<Field
name="clientId"
type="text"
id="clientId"
placeholder="NPED client ID"
className="p-1.5 border border-gray-400 rounded-lg"
/>
</FormGroup>
<button
type="submit"
className="w-1/4 text-white bg-green-700 hover:bg-green-800 font-small rounded-lg text-sm px-2 py-2.5"
>
Login
</button>
</Form>
)}
</Formik>
);
};

View File

@@ -6,7 +6,7 @@ type FormGroupProps = {
const FormGroup = ({ children }: FormGroupProps) => {
return (
<div className="flex flex-col md:flex-row items-center justify-between">
<div className="flex flex-col md:flex-row items-center justify-between relative">
{children}
</div>
);

View File

@@ -1,14 +1,19 @@
import { Link } from "react-router";
import Logo from "/MAV.svg";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSliders } from "@fortawesome/free-solid-svg-icons";
const Header = () => {
return (
<div className="relative bg-[#253445] border-b border-gray-500 items-center mx-auto px-2 sm:px-6 lg:px-8 p-4">
<div className="relative bg-[#253445] border-b border-gray-500 items-center mx-auto px-2 sm:px-6 lg:px-8 p-4 flex flex-row justify-between">
<div className="w-30">
<Link to={"/"}>
<img src={Logo} alt="Logo" width={100} height={100} />
</Link>
</div>
<Link to={"/system-settings"}>
<FontAwesomeIcon icon={faSliders} />
</Link>
</div>
);
};

View File

@@ -0,0 +1,17 @@
import { createContext, useContext, type SetStateAction } from "react";
import type { NPEDUser } from "../types/types";
type UserContextValue = {
user: NPEDUser | null;
setUser: React.Dispatch<SetStateAction<NPEDUser | null>>;
};
export const NPEDUserContext = createContext<UserContextValue | undefined>(
undefined
);
export const useNPEDContext = () => {
const ctx = useContext(NPEDUserContext);
if (!ctx)
throw new Error("useNPEDContext must be used within <NPEDUserProvider>");
return ctx;
};

View File

@@ -1,6 +1,5 @@
import { createContext, useContext, type ReactNode } from "react";
import { createContext, useContext } from "react";
import type { SightingWidgetType } from "../types/types";
import { useSightingFeed } from "../hooks/useSightingFeed";
type SightingFeedContextType = {
sightings: (SightingWidgetType | null | undefined)[];
@@ -13,50 +12,10 @@ type SightingFeedContextType = {
noSighting: boolean;
};
type SightingFeedProviderProps = {
url: string;
children: ReactNode;
side: string;
};
export const SightingFeedContext = createContext<
SightingFeedContextType | undefined
>(undefined);
const SightingFeedContext = createContext<SightingFeedContextType | undefined>(
undefined
);
export const SightingFeedProvider = ({
children,
url,
side,
}: SightingFeedProviderProps) => {
const {
sightings,
selectedRef,
setSelectedRef,
effectiveSelected,
mostRecent,
isPending,
noSighting,
} = useSightingFeed(url);
return (
<SightingFeedContext.Provider
value={{
sightings,
selectedRef,
setSelectedRef,
effectiveSelected,
mostRecent,
side,
isPending,
noSighting,
}}
>
{children}
</SightingFeedContext.Provider>
);
};
// eslint-disable-next-line react-refresh/only-export-components
export const useSightingFeedContext = () => {
const ctx = useContext(SightingFeedContext);
if (!ctx)

View File

@@ -0,0 +1,17 @@
import { useState, type ReactNode } from "react";
import type { NPEDUser } from "../../types/types";
import { NPEDUserContext } from "../NPEDUserContext";
type NPEDUserProviderType = {
children: ReactNode;
};
export const NPEDUserProvider = ({ children }: NPEDUserProviderType) => {
const [user, setUser] = useState<NPEDUser | null>(null);
return (
<NPEDUserContext.Provider value={{ user, setUser }}>
{children}
</NPEDUserContext.Provider>
);
};

View File

@@ -0,0 +1,42 @@
import type { ReactNode } from "react";
import { useSightingFeed } from "../../hooks/useSightingFeed";
import { SightingFeedContext } from "../SightingFeedContext";
type SightingFeedProviderProps = {
url: string;
children: ReactNode;
side: string;
};
export const SightingFeedProvider = ({
children,
url,
side,
}: SightingFeedProviderProps) => {
const {
sightings,
selectedRef,
setSelectedRef,
effectiveSelected,
mostRecent,
isPending,
noSighting,
} = useSightingFeed(url);
return (
<SightingFeedContext.Provider
value={{
sightings,
selectedRef,
setSelectedRef,
effectiveSelected,
mostRecent,
side,
isPending,
noSighting,
}}
>
{children}
</SightingFeedContext.Provider>
);
};

View File

@@ -1,49 +0,0 @@
import { useMutation } from "@tanstack/react-query";
import type { NPEDFieldType } from "../types/types";
const url = "https://jsonplaceholder.typicode.com/posts";
async function signIn(loginDetails: NPEDFieldType) {
console.log(loginDetails);
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(loginDetails),
});
if (!response.ok) throw new Error("cannot reach NPED endpoint");
return response.json();
}
async function signOut() {
const response = await fetch(url, { method: "POST" });
if (!response.ok) throw new Error("cannot reach NPED sign out endpoint");
return response.json();
}
function setUserContext(user) {
console.log(user);
}
export const useNPEDAuth = () => {
const signInMutation = useMutation({
mutationKey: ["NPEDSignin"],
mutationFn: signIn,
onSuccess: (data) => setUserContext(data),
});
const signOutMutation = useMutation({
mutationKey: ["auth", "NPEDSignOut"],
mutationFn: signOut,
onSuccess: () => setUserContext(null),
});
return {
signIn: signInMutation.mutate,
signInAsync: signInMutation.mutateAsync,
isPending: signInMutation.isPending,
isError: signInMutation.isError,
error: signInMutation.error,
data: signInMutation.data,
signOut: signOutMutation.mutate,
};
};

85
src/hooks/useNPEDAuth.ts Normal file
View File

@@ -0,0 +1,85 @@
import { useMutation } from "@tanstack/react-query";
import type { NPEDFieldType } from "../types/types";
import { useNPEDContext } from "../context/NPEDUserContext";
const base_url = import.meta.env.VITE_OUTSIDE_BASEURL;
async function signIn(loginDetails: NPEDFieldType) {
const { frontId, rearId, username, password, clientId } = loginDetails;
const NPEDLoginURLFront = `${base_url}/update-config?id=${frontId}`;
const NPEDLoginURLRear = `${base_url}/update-config?id=${rearId}`;
const frontCameraPayload = {
id: frontId,
fields: [
{ property: "propEnabled", value: true },
{ property: "propUsername", value: username },
{ property: "propPassword", value: password },
{ property: "propClientID", value: clientId },
],
};
const rearCameraPayload = {
id: rearId,
fields: [
{ property: "propEnabled", value: true },
{ property: "propUsername", value: username },
{ property: "propPassword", value: password },
{ property: "propClientID", value: clientId },
],
};
const frontCameraResponse = await fetch(NPEDLoginURLFront, {
method: "POST",
body: JSON.stringify(frontCameraPayload),
});
const rearCameraResponse = await fetch(NPEDLoginURLRear, {
method: "POST",
body: JSON.stringify(rearCameraPayload),
});
if (!frontCameraResponse.ok) throw new Error("cannot reach NPED endpoint");
if (!rearCameraResponse.ok) throw new Error("cannot reach NPED endpoint");
console.log(frontCameraResponse);
console.log(rearCameraResponse);
return {
frontResponse: frontCameraResponse.json(),
rearResponse: rearCameraResponse.json(),
};
}
async function signOut() {
const response = await fetch(url, { method: "POST" });
if (!response.ok) throw new Error("cannot reach NPED sign out endpoint");
return response.json();
}
export const useNPEDAuth = () => {
const { setUser } = useNPEDContext();
const signInMutation = useMutation({
mutationKey: ["NPEDSignin"],
mutationFn: signIn,
onSuccess: async (data) => setUser(await data.frontResponse),
});
const signOutMutation = useMutation({
mutationKey: ["auth", "NPEDSignOut"],
mutationFn: signOut,
onSuccess: () => setUser(null),
});
return {
signIn: signInMutation.mutate,
signInAsync: signInMutation.mutateAsync,
isPending: signInMutation.isPending,
isError: signInMutation.isError,
error: signInMutation.error,
data: signInMutation.data,
signOut: signOutMutation.mutate,
};
};

View File

@@ -2,7 +2,7 @@ import FrontCameraOverviewCard from "../components/FrontCameraOverview/FrontCame
import RearCameraOverviewCard from "../components/RearCameraOverview/RearCameraOverviewCard";
import SightingHistoryWidget from "../components/SightingsWidget/SightingWidget";
import { SightingFeedProvider } from "../context/SightingFeedContext";
import { SightingFeedProvider } from "../context/providers/SightingFeedProvider";
const Dashboard = () => {
return (

View File

@@ -3,6 +3,7 @@ import "react-tabs/style/react-tabs.css";
import NPEDCard from "../components/SettingForms/NPED/NPEDCard";
import SettingForms from "../components/SettingForms/SettingForms/SettingForms";
import NPEDHotlistCard from "../components/SettingForms/NPED/NPEDHotlistCard";
import { Toaster } from "sonner";
const SystemSettings = () => {
return (
@@ -24,6 +25,7 @@ const SystemSettings = () => {
</div>
</TabPanel>
</Tabs>
<Toaster />
</div>
);
};

View File

@@ -59,9 +59,11 @@ export type InitialValuesForm = {
};
export type NPEDFieldType = {
frontId: string;
username: string;
password: string;
clientId: string;
rearId: string;
};
export type HotlistUploadType = {
@@ -97,3 +99,15 @@ export type SightingWidgetType = {
overviewPlateRect?: [number, number, number, number]; // [x,y,w,h] normalized 0..1
plateTrack?: [number, number, number, number][]; // list of rects normalized 0..1
};
export type NPEDUser = {
username: string;
clientId: string;
password?: string;
};
export type NPEDErrorValues = {
username?: string | undefined;
password?: string | undefined;
clientId?: string | undefined;
};