From 70e71acf3bf3312f50ce8d5877906f41126ecf1e Mon Sep 17 00:00:00 2001 From: Rama Priyanto Date: Wed, 8 Apr 2026 16:49:00 +0700 Subject: [PATCH] fix:history, loading --- app/dashboard/etle/page.tsx | 143 ++++++++++++++------------- components/ui/drawer.tsx | 3 +- components/ui/spinner.tsx | 12 +++ components/zustand/plate-history.tsx | 80 +++++++++++++++ package-lock.json | 34 ++++++- package.json | 3 +- 6 files changed, 199 insertions(+), 76 deletions(-) create mode 100644 components/ui/spinner.tsx create mode 100644 components/zustand/plate-history.tsx diff --git a/app/dashboard/etle/page.tsx b/app/dashboard/etle/page.tsx index 9db9aa5..aade8c7 100644 --- a/app/dashboard/etle/page.tsx +++ b/app/dashboard/etle/page.tsx @@ -2,48 +2,47 @@ import { ArrowLeftIcon, ChevronLeft } from "lucide-react" import Link from "next/link" -import { useRouter } from "next/navigation" -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerDescription, - DrawerFooter, - DrawerHeader, - DrawerTitle, - DrawerTrigger, -} from "@/components/ui/drawer" +import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer" import { Button } from "@/components/ui/button" import Image from "next/image" import { useEffect, useState } from "react" import { socket } from "@/components/socket" +import { usePlateStore } from "@/components/zustand/plate-history" +import { Spinner } from "@/components/ui/spinner" -interface historyType { - img: string - plate: string -} const stream_dummy = [ - "/app/static/uploads/ds1.mp4", - "/app/static/uploads/ds2.mp4", - "/app/static/uploads/ds3.mp4", - "/app/static/uploads/ds4.mp4", + { id: "asssd", url: "/app/static/uploads/ds1.mp4" }, + { id: "asaggssd", url: "/app/static/uploads/ds2.mp4" }, + { id: "asss112d", url: "/app/static/uploads/ds3.mp4" }, + { id: "asgsasssd", url: "/app/static/uploads/ds4.mp4" }, ] export default function Etle() { - const router = useRouter() const [sessionId, setSessionId] = useState(null) const [imageSrc, setImageSrc] = useState("") const [plate, setPlate] = useState("") const [confidence, setConfidence] = useState(null) - const [history, setHistory] = useState([]) + const activeStream = usePlateStore((s) => s.activeStreamId) + const setActiveStream = usePlateStore((s) => s.setActiveStream) + const history = usePlateStore((s) => s.history) + const addPlate = usePlateStore((s) => s.addPlate) + const [loading, setLoading] = useState(true) + + const currentStream = usePlateStore.getState().activeStreamId const startDetection = (index: number) => { - const stream = stream_dummy[index] + setLoading(true) + if (history[activeStream]) setPlate("") + setConfidence(null) + const stream = stream_dummy[index].url socket.emit("stop_detection") socket.emit("start_detection", { stream_url: stream, }) + setTimeout(() => { + setLoading(false) + }, 2000) } useEffect(() => { @@ -53,15 +52,19 @@ export default function Etle() { socket.on("connected", (data) => { setSessionId(data.session_id) + const stream = stream_dummy.find((a) => a.id == currentStream) + setActiveStream(currentStream) socket.emit("start_detection", { - stream_url: "/app/static/uploads/ds1.mp4", + stream_url: stream?.url ?? stream_dummy[0].url, }) + setTimeout(() => { + setLoading(false) + }, 2000) }) socket.on("video_frame", (data) => { const thumb = `data:image/jpeg;base64,${data.frame}` setImageSrc(thumb) - // setHistory((prev) => [thumb, ...prev].slice(0, 20)) }) socket.on("plate_detected", (data) => { @@ -69,24 +72,24 @@ export default function Etle() { setConfidence(data.confidence ?? null) const thumb = `data:image/jpeg;base64,${data.frame_thumb}` - setHistory((prev) => { - // cek apakah plate sudah pernah ada - const exists = prev.some((item) => item.plate === data.plate_text) - if (exists) return prev // skip kalau duplicate + if (!currentStream) return - return [{ img: thumb, plate: data.plate_text }, ...prev] + addPlate(currentStream, { + imgThumb: thumb, + plate: data.plate_text, + confidence: data.confidence, }) }) - // socket.on("detection_stopped", (data) => { - // console.log("Total:", data.total) - // }) - socket.on("error", (data) => { console.error(data.message) }) + socket.on("detection_stopped", (data) => { + console.log("Selesai. Total:", data.total) + }) + return () => { socket.emit("stop_detection") socket.removeAllListeners() @@ -94,11 +97,16 @@ export default function Etle() { } }, []) - // const startDetection = () => { - // socket.emit("start_detection", { - // stream_url: "/app/static/uploads/ds1", - // }) - // } + const [hasMounted, setHasMounted] = useState(false) + + useEffect(() => { + setHasMounted(true) + }, []) + + if (!hasMounted) { + return null + } + return (
@@ -109,20 +117,33 @@ export default function Etle() {

TRAFFIC VIOLATIONS

- {/* */}
+

now {currentStream}

{stream_dummy.map((stream, index) => ( - ))}
- {imageSrc && ( - Realtime + {loading ? ( +
+ +
+ ) : ( + imageSrc && ( + Realtime + ) )} - - -
-
-
- Total Image: 10 -
{" "} -
-
-
- */}
) } diff --git a/components/ui/drawer.tsx b/components/ui/drawer.tsx index ea930ee..ff7ffbf 100644 --- a/components/ui/drawer.tsx +++ b/components/ui/drawer.tsx @@ -55,13 +55,14 @@ function DrawerContent({ -
+
{children} diff --git a/components/ui/spinner.tsx b/components/ui/spinner.tsx new file mode 100644 index 0000000..4dfd697 --- /dev/null +++ b/components/ui/spinner.tsx @@ -0,0 +1,12 @@ +import { LoaderIcon } from "lucide-react" +import { cn } from "@/lib/utils" +export function Spinner({ className, ...props }: React.ComponentProps<"svg">) { + return ( + + ) +} diff --git a/components/zustand/plate-history.tsx b/components/zustand/plate-history.tsx new file mode 100644 index 0000000..f235bb9 --- /dev/null +++ b/components/zustand/plate-history.tsx @@ -0,0 +1,80 @@ +import { create } from "zustand" +import { persist } from "zustand/middleware" + +type PlateItem = { + imgThumb: string + plate: string + confidence?: number | null +} + +type PlateState = { + activeStreamId: string | "" + history: Record + + setActiveStream: (id: string) => void + addPlate: (streamId: string, newData: PlateItem) => void + clearHistory: (streamId: string) => void +} + +export const usePlateStore = create()( + persist( + (set, get) => ({ + // active stream + activeStreamId: "", + + setActiveStream: (id: string) => set({ activeStreamId: id }), + + // history per stream + history: {}, + + addPlate: (streamId: string, newData: PlateItem) => { + const state = get() + const currentHistory = state.history[streamId] || [] + + const index = currentHistory.findIndex( + (item) => item.plate === newData.plate + ) + + let updatedHistory + + // kalau belum ada → tambah + if (index === -1) { + updatedHistory = [newData, ...currentHistory] + } else { + const existing = currentHistory[index] + + // kalau confidence lebih tinggi → replace + if ((newData.confidence ?? 0) > (existing.confidence ?? 0)) { + updatedHistory = [ + newData, + ...currentHistory.filter((_, i) => i !== index), + ] + } else { + updatedHistory = currentHistory + } + } + + updatedHistory = updatedHistory + + set({ + history: { + ...state.history, + [streamId]: updatedHistory, + }, + }) + }, + + // optional: clear history per stream + clearHistory: (streamId) => { + const state = get() + const newHistory = { ...state.history } + delete newHistory[streamId] + + set({ history: newHistory }) + }, + }), + { + name: "plate-storage", // key localStorage + } + ) +) diff --git a/package-lock.json b/package-lock.json index ac8002a..d2ee638 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,9 +38,9 @@ "tailwindcss": "^4.2.1", "tw-animate-css": "^1.4.0", "typescript": "^5.9.3", - "vaul": "^1.1.2" - }, - "devDependencies": {} + "vaul": "^1.1.2", + "zustand": "^5.0.12" + } }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", @@ -10639,6 +10639,34 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 2975759..0f2cff3 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "tailwindcss": "^4.2.1", "tw-animate-css": "^1.4.0", "typescript": "^5.9.3", - "vaul": "^1.1.2" + "vaul": "^1.1.2", + "zustand": "^5.0.12" } }