code fixes and adding modal

This commit is contained in:
2025-09-12 08:21:52 +01:00
parent fae17b88a4
commit d03f73f751
24 changed files with 524 additions and 303 deletions

View File

@@ -5,13 +5,13 @@ import type {
} from "../../types/types";
import { toast } from "sonner";
const CameraSettingFields = () => {
const CameraSettingFields = ({ initialData, updateCameraConfig }) => {
const initialValues: CameraSettingValues = {
friendlyName: "",
friendlyName: initialData?.propLEDDriverControlURI?.value,
cameraAddress: "",
userName: "",
password: "",
setupCamera: 1,
id: initialData?.id,
};
const validateValues = (values: CameraSettingValues) => {
@@ -29,7 +29,7 @@ const CameraSettingFields = () => {
const handleSubmit = (values: CameraSettingValues) => {
// post values to endpoint
console.log(values);
updateCameraConfig(values);
toast("Settings Saved");
};

View File

@@ -1,15 +1,28 @@
import { useFetchCameraConfig } from "../../hooks/useCameraConfig";
import Card from "../UI/Card";
import CardHeader from "../UI/CardHeader";
import CameraSettingFields from "./CameraSettingFields";
import { faWrench } from "@fortawesome/free-solid-svg-icons";
const CameraSettings = ({ title }: { title: string }) => {
const CameraSettings = ({ title, side }: { title: string; side: string }) => {
const { data, isError, isPending, updateCameraConfig } =
useFetchCameraConfig(side);
return (
<Card>
<div className="relative flex flex-col space-y-3 h-full">
<CardHeader title={title} icon={faWrench} />
<CameraSettingFields />
</div>
{isError && <>Cannot Fetch camera config</>}
{isPending ? (
<>Loading</>
) : (
<div className="relative flex flex-col space-y-3 h-full">
<CardHeader title={title} icon={faWrench} />
<CameraSettingFields
initialData={data}
updateCameraConfig={updateCameraConfig}
/>
</div>
)}
</Card>
);
};

View File

@@ -1,74 +1,88 @@
export async function handleSystemSave(deviceName: string, sntpServer: string, sntpInterval: number, timeZone: string) {
const payload = { // Build JSON
id: "GLOBAL--Device",
fields: [
{ property: "propDeviceName", value: deviceName },
{ property: "propSNTPServer", value: sntpServer },
{ property: "propSNTPIntervalMinutes", value: Number(sntpInterval) },
{ property: "propLocalTimeZone", value: timeZone }
]
};
export async function handleSystemSave(
deviceName: string,
sntpServer: string,
sntpInterval: number,
timeZone: string
) {
const payload = {
// Build JSON
id: "GLOBAL--Device",
fields: [
{ property: "propDeviceName", value: deviceName },
{ property: "propSNTPServer", value: sntpServer },
{ property: "propSNTPIntervalMinutes", value: Number(sntpInterval) },
{ property: "propLocalTimeZone", value: timeZone },
],
};
try {
const response = await fetch("http://192.168.75.11/api/update-config", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify(payload)
});
try {
const response = await fetch("http://192.168.75.11/api/update-config", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`HTTP ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
}
alert("System Settings Saved Successfully!");
} catch (err) {
console.error(err);
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(
`HTTP ${response.status} ${response.statusText}${
text ? ` - ${text}` : ""
}`
);
}
alert("System Settings Saved Successfully!");
} catch (err) {
console.error(err);
}
}
export async function handleSystemRecall() {
const url = "http://192.168.75.11/api/fetch-config?id=GLOBAL--Device";
const url = "http://192.168.75.11/api/fetch-config?id=GLOBAL--Device";
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 7000);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 7000);
try {
const response = await fetch(url, {
method: "GET",
headers: { "Accept": "application/json" },
signal: controller.signal
});
try {
const response = await fetch(url, {
method: "GET",
headers: { Accept: "application/json" },
signal: controller.signal,
});
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`HTTP ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
}
const data = await response.json();
const deviceName = data?.propDeviceName?.value ?? null;
const sntpServer = data?.propSNTPServer?.value ?? null;
const timeZone = data?.propLocalTimeZone?.value ?? null;
let sntpIntervalRaw = data?.propSNTPIntervalMinutes?.value;
let sntpInterval =
typeof sntpIntervalRaw === "number"
? sntpIntervalRaw
: Number.parseInt(String(sntpIntervalRaw).trim(), 10);
if (!Number.isFinite(sntpInterval)) {
sntpInterval = 60;
}
return { deviceName, sntpServer, sntpInterval, timeZone };
} catch (err) {
console.error(err);
return null;
} finally {
clearTimeout(timeoutId);
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(
`HTTP ${response.status} ${response.statusText}${
text ? ` - ${text}` : ""
}`
);
}
}
const data = await response.json();
const deviceName = data?.propDeviceName?.value ?? null;
const sntpServer = data?.propSNTPServer?.value ?? null;
const timeZone = data?.propLocalTimeZone?.value ?? null;
const sntpIntervalRaw = data?.propSNTPIntervalMinutes?.value;
let sntpInterval =
typeof sntpIntervalRaw === "number"
? sntpIntervalRaw
: Number.parseInt(String(sntpIntervalRaw).trim(), 10);
if (!Number.isFinite(sntpInterval)) {
sntpInterval = 60;
}
return { deviceName, sntpServer, sntpInterval, timeZone };
} catch (err) {
console.error(err);
return null;
} finally {
clearTimeout(timeoutId);
}
}

View File

@@ -0,0 +1,32 @@
import NumberPlate from "../PlateStack/NumberPlate";
import ModalComponent from "../UI/ModalComponent";
type SightingModalProps = {
isSightingModalOpen: boolean;
handleClose: () => void;
};
const SightingModal = ({
isSightingModalOpen,
handleClose,
sighting,
}: SightingModalProps) => {
const motionAway = (sighting?.motion ?? "").toUpperCase() === "AWAY";
return (
<ModalComponent isModalOpen={isSightingModalOpen} close={handleClose}>
<button onClick={handleClose}>close</button>
<div>
<h2>{sighting?.vrm}</h2>
<NumberPlate vrm={sighting?.vrm} motion={motionAway} />
<img
src={sighting?.plateUrlInfrared}
height={48}
alt="infrared patch"
className={"opacity-60"}
/>
</div>
</ModalComponent>
);
};
export default SightingModal;

View File

@@ -5,15 +5,15 @@ import { useOverviewOverlay } from "../../hooks/useOverviewOverlay";
import { useSightingFeedContext } from "../../context/SightingFeedContext";
import { useHiDPICanvas } from "../../hooks/useHiDPICanvas";
import NavigationArrow from "../UI/NavigationArrow";
import { useSwipeable } from "react-swipeable";
import { useNavigate } from "react-router";
// import { useSwipeable } from "react-swipeable";
// import { useNavigate } from "react-router";
const SightingOverview = () => {
const navigate = useNavigate();
const handlers = useSwipeable({
onSwipedRight: () => navigate("/front-camera-settings"),
trackMouse: true,
});
// const navigate = useNavigate();
// const handlers = useSwipeable({
// onSwipedRight: () => navigate("/front-camera-settings"),
// trackMouse: true,
// });
const [overlayMode, setOverlayMode] = useState<0 | 1 | 2>(0);
const imgRef = useRef<HTMLImageElement | null>(null);
@@ -23,51 +23,51 @@ const SightingOverview = () => {
setOverlayMode((m) => ((m + 1) % 3) as 0 | 1 | 2);
}, []);
const { effectiveSelected, side, mostRecent, noSighting, isPending } =
useSightingFeedContext();
const { effectiveSelected, side, mostRecent } = useSightingFeedContext();
useOverviewOverlay(mostRecent, overlayMode, imgRef, canvasRef);
const { sync } = useHiDPICanvas(imgRef, canvasRef);
if (noSighting || isPending) return <p>loading</p>;
// if (noSighting || isPending) return <p>loading</p>;
return (
<div className="mt-2 grid gap-3">
<div className="inline-block w-[90%] mx-auto" {...handlers}>
<div className="flex flex-col">
<div className="grid gap-3">
<NavigationArrow side={side} />
<div className="relative aspect-[1280/800]">
<img
ref={imgRef}
onLoad={() => {
sync();
setOverlayMode((m) => m);
}}
src={mostRecent?.overviewUrl || BLANK_IMG}
alt="overview"
className="absolute inset-0 w-full h-full object-contain cursor-pointer z-10"
onClick={onOverviewClick}
style={{
display: mostRecent?.overviewUrl ? "block" : "none",
}}
/>
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full object-contain z-20 pointer-events-none"
/>
<div className="inline-block w-full mx-auto">
<div className="relative aspect-[1280/800]">
<img
ref={imgRef}
onLoad={() => {
sync();
setOverlayMode((m) => m);
}}
src={mostRecent?.overviewUrl || BLANK_IMG}
alt="overview"
className="absolute inset-0 w-full h-full object-contain cursor-pointer z-10 "
onClick={onOverviewClick}
style={{
display: mostRecent?.overviewUrl ? "block" : "none",
}}
/>
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full object-contain z-20 pointer-events-none "
/>
</div>
</div>
<div className="text-xs opacity-80">
Overlay:{" "}
{overlayMode === 0
? "Off"
: overlayMode === 1
? "Plate box"
: "Track + box"}{" "}
(click image to toggle)
</div>
</div>
<SightingWidgetDetails effectiveSelected={effectiveSelected} />
<div className="text-xs opacity-80">
Overlay:{" "}
{overlayMode === 0
? "Off"
: overlayMode === 1
? "Plate box"
: "Track + box"}{" "}
(click image to toggle)
</div>
</div>
);
};

View File

@@ -1,11 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import type { SightingWidgetType } from "../../types/types";
import type { SightingType, SightingWidgetType } from "../../types/types";
import { BLANK_IMG, capitalize, formatAge } from "../../utils/utils";
import NumberPlate from "../PlateStack/NumberPlate";
import Card from "../UI/Card";
import CardHeader from "../UI/CardHeader";
import clsx from "clsx";
import { useSightingFeedContext } from "../../context/SightingFeedContext";
import SightingModal from "../SightingModal/SightingModal";
function useNow(tickMs = 1000) {
const [, setNow] = useState(() => Date.now());
@@ -29,97 +30,112 @@ export default function SightingHistoryWidget({
className,
}: SightingHistoryWidgetProps) {
useNow(1000);
const { sightings, selectedRef, setSelectedRef } = useSightingFeedContext();
const {
sightings,
setSelectedSighting,
setSightingModalOpen,
isSightingModalOpen,
selectedSighting,
} = useSightingFeedContext();
const onRowClick = useCallback(
(ref: number) => {
setSelectedRef(ref);
(sighting: SightingType) => {
if (!sighting) return;
setSightingModalOpen(!isSightingModalOpen);
setSelectedSighting(sighting);
},
[setSelectedRef]
[isSightingModalOpen, setSelectedSighting, setSightingModalOpen]
);
const rows = useMemo(
() => sightings?.filter(Boolean) as SightingWidgetType[],
[sightings]
);
const handleClose = () => {
setSightingModalOpen(false);
};
return (
<Card className={clsx("overflow-y-auto h-100", className)}>
<CardHeader title="Front Camera Sightings" />
<div className="flex flex-col gap-3 ">
{/* Rows */}
<div className="flex flex-col">
{rows?.map((obj, idx) => {
const isNPEDHit = obj?.metadata?.npedJSON?.status_code === 201;
const isSelected = obj?.ref === selectedRef;
const motionAway = (obj?.motion ?? "").toUpperCase() === "AWAY";
const primaryIsColour = obj?.srcCam === 1;
const secondaryMissing = (obj?.vrmSecondary ?? "") === "";
return (
<div
key={idx}
className={`border border-neutral-700 rounded-md mb-2 p-2 cursor-pointer ${
isSelected ? "ring-2 ring-blue-400" : ""
}`}
onClick={() => onRowClick(obj.ref)}
>
{/* Info bar */}
<div className="flex items-center gap-3 text-xs bg-neutral-900 px-2 py-1 rounded">
<div className="min-w-14">
CH: {obj ? obj.charHeight : "—"}
</div>
<div className="min-w-14">
Seen: {obj ? obj.seenCount : "—"}
</div>
<div className="min-w-20">
{obj ? capitalize(obj.motion) : "—"}
</div>
<div className="min-w-14 opacity-80">
{obj ? formatAge(obj.timeStampMillis) : "—"}
</div>
</div>
{/* Patch row */}
<>
<Card className={clsx("overflow-y-auto h-100", className)}>
<CardHeader title="Front Camera Sightings" />
<div className="flex flex-col gap-3 ">
{/* Rows */}
<div className="flex flex-col">
{rows?.map((obj, idx) => {
const isNPEDHit = obj?.metadata?.npedJSON?.status_code === 201;
const motionAway = (obj?.motion ?? "").toUpperCase() === "AWAY";
const primaryIsColour = obj?.srcCam === 1;
const secondaryMissing = (obj?.vrmSecondary ?? "") === "";
console.log(obj);
return (
<div
className={`flex items-center gap-3 mt-2
key={idx}
className={`border border-neutral-700 rounded-md mb-2 p-2 cursor-pointer`}
onClick={() => onRowClick(obj)}
>
{/* Info bar */}
<div className="flex items-center gap-3 text-xs bg-neutral-900 px-2 py-1 rounded">
<div className="min-w-14">
CH: {obj ? obj.charHeight : "—"}
</div>
<div className="min-w-14">
Seen: {obj ? obj.seenCount : "—"}
</div>
<div className="min-w-20">
{obj ? capitalize(obj.motion) : "—"}
</div>
<div className="min-w-14 opacity-80">
{obj ? formatAge(obj.timeStampMillis) : "—"}
</div>
</div>
{/* Patch row */}
<div
className={`flex items-center gap-3 mt-2
${isNPEDHit ? "border border-red-600" : ""}
`}
>
<div
className={`border p-1 ${
primaryIsColour ? "" : "ring-2 ring-lime-400"
} ${!obj ? "opacity-30" : ""}`}
>
<img
src={obj?.plateUrlInfrared || BLANK_IMG}
height={48}
alt="infrared patch"
className={!primaryIsColour ? "" : "opacity-60"}
/>
<div
className={`border p-1 ${
primaryIsColour ? "" : "ring-2 ring-lime-400"
} ${!obj ? "opacity-30" : ""}`}
>
<img
src={obj?.plateUrlInfrared || BLANK_IMG}
height={48}
alt="infrared patch"
className={!primaryIsColour ? "" : "opacity-60"}
/>
</div>
<div
className={`border p-1 ${
primaryIsColour ? "ring-2 ring-lime-400" : ""
} ${
secondaryMissing && primaryIsColour
? "opacity-30 grayscale"
: ""
}`}
>
<img
src={obj?.plateUrlColour || BLANK_IMG}
height={48}
alt="colour patch"
className={primaryIsColour ? "" : "opacity-60"}
/>
</div>
<NumberPlate motion={motionAway} vrm={obj?.vrm} />
</div>
<div
className={`border p-1 ${
primaryIsColour ? "ring-2 ring-lime-400" : ""
} ${
secondaryMissing && primaryIsColour
? "opacity-30 grayscale"
: ""
}`}
>
<img
src={obj?.plateUrlColour || BLANK_IMG}
height={48}
alt="colour patch"
className={primaryIsColour ? "" : "opacity-60"}
/>
</div>
<NumberPlate motion={motionAway} vrm={obj?.vrm} />
</div>
</div>
);
})}
);
})}
</div>
</div>
</div>
</Card>
</Card>
<SightingModal
isSightingModalOpen={isSightingModalOpen}
handleClose={handleClose}
sighting={selectedSighting}
/>
</>
);
}

View File

@@ -1,4 +1,5 @@
import type { SightingWidgetType } from "../../types/types";
import { useState } from "react";
type SightingWidgetDetailsProps = {
effectiveSelected: SightingWidgetType | null;
@@ -7,72 +8,88 @@ type SightingWidgetDetailsProps = {
const SightingWidgetDetails = ({
effectiveSelected,
}: SightingWidgetDetailsProps) => {
const [advancedDetailsEnabled, setAdvancedDetailsEnabled] = useState(false);
const handleDetailsClick = () =>
setAdvancedDetailsEnabled(!advancedDetailsEnabled);
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm">
<div>
VRM:{" "}
<span className="opacity-90">{effectiveSelected?.vrm ?? ""}</span>
</div>
<div>
Timestamp:{" "}
<span className="opacity-90">{effectiveSelected?.timeStamp ?? "—"}</span>
</div>
<div>
Make:{" "}
<span className="opacity-90">{effectiveSelected?.make ?? "—"}</span>
</div>
<div>
Model:{" "}
<span className="opacity-90">{effectiveSelected?.model ?? "—"}</span>
</div>
<div>
Country:{" "}
<span className="opacity-90">{effectiveSelected?.countryCode ?? "—"}</span>
</div>
<div>
Seen:{" "}
<span className="opacity-90">
{effectiveSelected?.seenCount ?? "—"}
</span>
</div>
<div>
Colour:{" "}
<span className="opacity-90">{effectiveSelected?.color ?? "—"}</span>
</div>
<div>
Category:{" "}
<span className="opacity-90">{effectiveSelected?.category ?? "—"}</span>
</div>
<div>
Char Ht:{" "}
<span className="opacity-90">
{effectiveSelected?.charHeight ?? "—"}
</span>
</div>
<div>
Plate Size:{" "}
<span className="opacity-90">
{effectiveSelected?.plateSize ?? "—"}
</span>
</div>
<div>
Overview Size:{" "}
<span className="opacity-90">
{effectiveSelected?.overviewSize ?? "—"}
</span>
</div>
{effectiveSelected?.detailsUrl ? (
<div className="col-span-half">
<a
href={effectiveSelected.detailsUrl}
target="_blank"
className="underline text-blue-300"
>
Sighting Details
</a>
<>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm">
<div>
VRM:{" "}
<span className="opacity-90">{effectiveSelected?.vrm ?? "—"}</span>
</div>
) : null}
</div>
<div>
Make:{" "}
<span className="opacity-90">{effectiveSelected?.make ?? "—"}</span>
</div>
<div>
Model:{" "}
<span className="opacity-90">{effectiveSelected?.model ?? "—"}</span>
</div>
<div>
Colour:{" "}
<span className="opacity-90">{effectiveSelected?.color ?? "—"}</span>
</div>
<div className="col-span-4">
Timestamp:{" "}
<span className="opacity-90">
{effectiveSelected?.timeStamp ?? "—"}
</span>
</div>
{advancedDetailsEnabled && (
<>
{" "}
<div>
Country:{" "}
<span className="opacity-90">
{effectiveSelected?.countryCode ?? "—"}
</span>
</div>
<div>
Seen:{" "}
<span className="opacity-90">
{effectiveSelected?.seenCount ?? "—"}
</span>
</div>
<div>
Category:{" "}
<span className="opacity-90">
{effectiveSelected?.category ?? "—"}
</span>
</div>
<div>
Char Ht:{" "}
<span className="opacity-90">
{effectiveSelected?.charHeight ?? "—"}
</span>
</div>
<div>
Plate Size:{" "}
<span className="opacity-90">
{effectiveSelected?.plateSize ?? "—"}
</span>
</div>
<div>
Overview Size:{" "}
<span className="opacity-90">
{effectiveSelected?.overviewSize ?? "—"}
</span>
</div>
</>
)}
</div>
<div className="col-span-half">
<p
onClick={handleDetailsClick}
className="underline text-blue-300 hover:cursor-pointer"
>
Sighting Details
</p>
</div>
</>
);
};

View File

@@ -5,10 +5,17 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGear, faListCheck } from "@fortawesome/free-solid-svg-icons";
import type { VersionFieldType } from "../../types/types";
async function fetchVersions(signal?: AbortSignal): Promise<VersionFieldType> {
const res = await fetch("http://192.168.75.11/api/versions", { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
async function fetchVersions(
signal?: AbortSignal
): Promise<VersionFieldType | undefined> {
try {
const res = await fetch("http://192.168.75.11/api/versions", { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
} catch (error) {
console.log(error);
return undefined;
}
}
const pad = (n: number) => String(n).padStart(2, "0");
@@ -33,10 +40,14 @@ export default function Header() {
const ac = new AbortController();
fetchVersions(ac.signal)
.then((data) => {
const serverMs = normalizeToMs(data.timeStamp);
if (!data) throw new Error("No data");
const serverMs = normalizeToMs(data?.timeStamp);
setOffsetMs(serverMs - Date.now());
})
return () => ac.abort();
.catch((err) => {
console.log(err);
});
return () => ac.abort("failed");
}, []);
React.useEffect(() => {
@@ -69,13 +80,14 @@ export default function Header() {
<h2>Local: {localStr}</h2>
<h2>UTC: {utcStr}</h2>
</div>
<Link to={"/session-settings"}>
<FontAwesomeIcon className="text-white" icon={faListCheck} />
</Link>
<Link to={"/system-settings"}>
<FontAwesomeIcon className="text-white" icon={faGear} />
</Link>
<div className="flex flex-row space-x-2">
<Link to={"/session-settings"}>
<FontAwesomeIcon className="text-white" icon={faListCheck} />
</Link>
<Link to={"/system-settings"}>
<FontAwesomeIcon className="text-white" icon={faGear} />
</Link>
</div>
</div>
</div>
);

View File

@@ -0,0 +1,26 @@
import type React from "react";
import Modal from "react-modal";
type ModalComponentProps = {
isModalOpen: boolean;
children: React.ReactNode;
};
const ModalComponent = ({
isModalOpen,
children,
close,
}: ModalComponentProps) => {
return (
<Modal
isOpen={isModalOpen}
onRequestClose={close}
className="bg-[#1e2a38] p-6 rounded-lg shadow-lg max-w-[65%] mx-auto mt-20 w-full h-[75%] z-100"
overlayClassName="fixed inset-0 bg-[#1e2a38]/70 flex justify-center items-start z-100"
>
{children}
</Modal>
);
};
export default ModalComponent;

View File

@@ -29,13 +29,13 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
{side === "CameraFront" ? (
<FontAwesomeIcon
icon={faArrowRight}
className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce"
className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)}
/>
) : (
<FontAwesomeIcon
icon={faArrowLeft}
className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce"
className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)}
/>
)}
@@ -47,13 +47,13 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
{side === "Front" ? (
<FontAwesomeIcon
icon={faArrowLeft}
className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce"
className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)}
/>
) : (
<FontAwesomeIcon
icon={faArrowRight}
className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce"
className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)}
/>
)}