diff --git a/package-lock.json b/package-lock.json index d078af7..2f7fa4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@testing-library/user-event": "^13.5.0", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2", "react-scripts": "5.0.1", "tesseract.js": "^6.0.1", "web-vitals": "^2.1.4" @@ -12944,6 +12945,50 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz", + "integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz", + "integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==", + "dependencies": { + "react-router": "7.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -13781,6 +13826,11 @@ "node": ">= 0.8.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", diff --git a/package.json b/package.json index 67be00c..4ab9a48 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@testing-library/user-event": "^13.5.0", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2", "react-scripts": "5.0.1", "tesseract.js": "^6.0.1", "web-vitals": "^2.1.4" diff --git a/src/App.js b/src/App.js index 11ef573..f324308 100644 --- a/src/App.js +++ b/src/App.js @@ -1,11 +1,14 @@ -import logo from "./logo.svg"; -import "./App.css"; +import { Routes, Route } from "react-router-dom"; import CameraKtp from "./KTPScanner"; +import Dashboard from "./Dashboard"; function App() { return (
- + + } /> + } /> +
); } diff --git a/src/Dashboard.js b/src/Dashboard.js new file mode 100644 index 0000000..92a1a20 --- /dev/null +++ b/src/Dashboard.js @@ -0,0 +1,24 @@ +import React from "react"; +import styles from "./Dashboard.module.css"; +import Header from "./components/Header"; +import Sidebar from "./components/Sidebar"; +import RoleCard from "./components/RoleCard"; +import Chart from "./components/Chart"; + +const Dashboard = () => { + return ( +
+ +
+
+
+ + +
+ +
+
+ ); +}; + +export default Dashboard; diff --git a/src/Dashboard.module.css b/src/Dashboard.module.css new file mode 100644 index 0000000..18c95cf --- /dev/null +++ b/src/Dashboard.module.css @@ -0,0 +1,26 @@ +.dashboard { + display: flex; + width: 100%; + max-width: 1200px; + background: white; + border-radius: 20px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +} + +.mainContent { + flex: 1; + padding: 1rem; +} + +.cards { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +@media (max-width: 768px) { + .dashboard { + flex-direction: column; + } +} diff --git a/src/KTPScanner.js b/src/KTPScanner.js index cda6783..4fe3b49 100644 --- a/src/KTPScanner.js +++ b/src/KTPScanner.js @@ -1,17 +1,26 @@ import React, { useEffect, useRef, useState } from "react"; +import { v4 as uuidv4 } from "uuid"; const STORAGE_KEY = "camera_canvas_gallery"; const CameraCanvas = () => { const videoRef = useRef(null); - const canvasRef = useRef(null); // visible canvas - const hiddenCanvasRef = useRef(null); // hidden canvas for capture + const canvasRef = useRef(null); + const hiddenCanvasRef = useRef(null); const [capturedImage, setCapturedImage] = useState(null); const [galleryImages, setGalleryImages] = useState([]); + const [fileTemp, setFileTemp] = useState(null); const [isFreeze, setIsFreeze] = useState(false); - const freezeFrameRef = useRef(null); // menyimpan freeze frame imageData + const freezeFrameRef = useRef(null); + + const rectRef = useRef({ + x: 0, + y: 0, + width: 0, + height: 0, + radius: 20, + }); - // Fungsi untuk gambar rounded rectangle const drawRoundedRect = (ctx, x, y, width, height, radius) => { ctx.beginPath(); ctx.moveTo(x + radius, y); @@ -29,13 +38,9 @@ const CameraCanvas = () => { ctx.stroke(); }; - // Fungsi untuk mewarnai area luar rectangle dengan hitam semi transparan const fillOutsideRect = (ctx, rect, canvasWidth, canvasHeight) => { ctx.save(); - const { x, y, width, height, radius } = rect; - - // Buat path rounded rectangle ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); @@ -47,39 +52,20 @@ const CameraCanvas = () => { ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); - - // Buat clipping inverse area (area luar rectangle) ctx.rect(0, 0, canvasWidth, canvasHeight); - - // Fill dengan mode 'evenodd' supaya area di luar path rectangle terisi - ctx.fillStyle = "rgba(173, 173, 173, 1)"; // hitam semi transparan + ctx.fillStyle = "rgba(173, 173, 173, 1)"; ctx.fill("evenodd"); - ctx.restore(); }; - // Variabel global untuk posisi rectangle dan ukurannya supaya bisa dipakai di shootImage - const rectRef = useRef({ - x: 0, - y: 0, - width: 0, - height: 0, - radius: 20, - }); - useEffect(() => { - // Load gallery dari localStorage saat pertama kali mount const savedGallery = localStorage.getItem(STORAGE_KEY); - if (savedGallery) { - setGalleryImages(JSON.parse(savedGallery)); - } + if (savedGallery) setGalleryImages(JSON.parse(savedGallery)); const getCameraStream = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ - video: { - facingMode: { ideal: "environment" }, - }, + video: { facingMode: { ideal: "environment" } }, audio: false, }); @@ -88,26 +74,21 @@ const CameraCanvas = () => { videoRef.current.onloadedmetadata = () => { videoRef.current.play(); - const video = videoRef.current; - const canvas = canvasRef.current; // visible canvas - const hiddenCanvas = hiddenCanvasRef.current; // hidden canvas + const canvas = canvasRef.current; + const hiddenCanvas = hiddenCanvasRef.current; const ctx = canvas.getContext("2d"); - // Set ukuran canvas sesuai video asli canvas.width = video.videoWidth; canvas.height = video.videoHeight; - - // Style visible canvas supaya scaled sesuai container dan tidak overflow canvas.style.maxWidth = "100%"; canvas.style.height = "auto"; hiddenCanvas.width = video.videoWidth; hiddenCanvas.height = video.videoHeight; - // Hitung ukuran rectangle KTP const rectWidth = canvas.width * 0.9; - const rectHeight = (53.98 / 85.6) * rectWidth; // aspek rasio KTP + const rectHeight = (53.98 / 85.6) * rectWidth; const rectX = (canvas.width - rectWidth) / 2; const rectY = (canvas.height - rectHeight) / 2; @@ -122,38 +103,26 @@ const CameraCanvas = () => { const drawToCanvas = () => { if (video.readyState === 4) { ctx.clearRect(0, 0, canvas.width, canvas.height); - if (isFreeze && freezeFrameRef.current) { - // Tampilkan freeze frame yang sudah disimpan ctx.putImageData(freezeFrameRef.current, 0, 0); - - drawRoundedRect( - ctx, - rectRef.current.x, - rectRef.current.y, - rectRef.current.width, - rectRef.current.height, - rectRef.current.radius - ); - - // Overlay area luar rectangle dengan hitam + } else { + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + } + drawRoundedRect( + ctx, + rectRef.current.x, + rectRef.current.y, + rectRef.current.width, + rectRef.current.height, + rectRef.current.radius + ); + if (isFreeze) { fillOutsideRect( ctx, rectRef.current, canvas.width, canvas.height ); - } else { - // Render video live + rectangle - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - drawRoundedRect( - ctx, - rectRef.current.x, - rectRef.current.y, - rectRef.current.width, - rectRef.current.height, - rectRef.current.radius - ); } } requestAnimationFrame(drawToCanvas); @@ -170,65 +139,6 @@ const CameraCanvas = () => { getCameraStream(); }, [isFreeze]); - useEffect(() => { - const canvas = canvasRef.current; - const ctx = canvas.getContext("2d"); - const video = videoRef.current; - - // Hitung posisi rectangle sekali - const rectWidth = canvas.width * 0.9; - const rectHeight = (53.98 / 85.6) * rectWidth; - const rectX = (canvas.width - rectWidth) / 2; - const rectY = (canvas.height - rectHeight) / 2; - - rectRef.current = { - x: rectX, - y: rectY, - width: rectWidth, - height: rectHeight, - radius: 20, - }; - - let animationFrameId; - - const drawToCanvas = () => { - if (video.readyState === 4) { - ctx.clearRect(0, 0, canvas.width, canvas.height); - if (isFreeze && freezeFrameRef.current) { - ctx.putImageData(freezeFrameRef.current, 0, 0); - - drawRoundedRect( - ctx, - rectRef.current.x, - rectRef.current.y, - rectRef.current.width, - rectRef.current.height, - rectRef.current.radius - ); - - // Overlay area luar rectangle dengan hitam - fillOutsideRect(ctx, rectRef.current, canvas.width, canvas.height); - } else { - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - drawRoundedRect( - ctx, - rectRef.current.x, - rectRef.current.y, - rectRef.current.width, - rectRef.current.height, - rectRef.current.radius - ); - } - } - animationFrameId = requestAnimationFrame(drawToCanvas); - }; - - drawToCanvas(); - - return () => cancelAnimationFrame(animationFrameId); - }, [isFreeze]); - - // Fungsi untuk capture gambar area rectangle dan simpan ke localStorage + freeze effect const shootImage = () => { const video = videoRef.current; const { x, y, width, height } = rectRef.current; @@ -236,27 +146,21 @@ const CameraCanvas = () => { const hiddenCtx = hiddenCanvas.getContext("2d"); const visibleCtx = canvasRef.current.getContext("2d"); - // Ambil image data canvas visible untuk freeze frame freezeFrameRef.current = visibleCtx.getImageData( 0, 0, canvasRef.current.width, canvasRef.current.height ); - - // Aktifkan freeze frame setIsFreeze(true); - // Tangkap gambar video ke hidden canvas hiddenCtx.drawImage(video, 0, 0, hiddenCanvas.width, hiddenCanvas.height); - // Buat canvas crop const cropCanvas = document.createElement("canvas"); cropCanvas.width = Math.floor(width); cropCanvas.height = Math.floor(height); const cropCtx = cropCanvas.getContext("2d"); - // Crop area rectangle dari hidden canvas cropCtx.drawImage( hiddenCanvas, Math.floor(x), @@ -273,7 +177,88 @@ const CameraCanvas = () => { setCapturedImage(imageDataUrl); }; - // Fungsi hapus gambar dari gallery + const ReadImage = async (capturedImage) => { + const imageId = uuidv4(); + + try { + let res = await fetch( + "https://bot.kediritechnopark.com/webhook/mastersnapper/read", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ imageId, image: capturedImage }), + } + ); + + const { output } = await res.json(); + + // Bersihkan dan parsing JSON dari output + const jsonString = output + .replace(/^```json/, "") + .replace(/```$/, "") + .trim(); + + const data = JSON.parse(jsonString); + + const newImage = { + imageId, + NIK: data.NIK || "", + Nama: data.Nama || "", + TTL: data.TTL || "", + Kelamin: data.Kelamin || "", + Alamat: data.Alamat || "", + RtRw: data["RT/RW"] || "", + KelDesa: data["Kel/Desa"] || "", + Kec: data.Kec || "", + Agama: data.Agama || "", + Hingga: data.Hingga || "", + Pembuatan: data.Pembuatan || "", + Kota: data["Kota Pembuatan"] || "", + }; + + setFileTemp(newImage); + } catch (error) { + console.error("Failed to read image:", error); + } + }; + + const handleSaveTemp = async () => { + try { + await fetch( + "https://bot.kediritechnopark.com/webhook/mastersnapper/save", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ fileTemp }), + } + ); + + const updatedGallery = [fileTemp, ...galleryImages]; + setGalleryImages(updatedGallery); + localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedGallery)); + setFileTemp(null); + } catch (err) { + console.error("Gagal menyimpan ke server:", err); + } + }; + + const handleDeleteTemp = async () => { + try { + await fetch( + "https://bot.kediritechnopark.com/webhook/mastersnapper/delete", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ fileTemp }), + } + ); + + setFileTemp(null); + } catch (err) { + console.error("Gagal menghapus dari server:", err); + } + }; + const removeImage = (index) => { const newGallery = [...galleryImages]; newGallery.splice(index, 1); @@ -281,9 +266,128 @@ const CameraCanvas = () => { localStorage.setItem(STORAGE_KEY, JSON.stringify(newGallery)); }; - // Rasio KTP const aspectRatio = 53.98 / 85.6; + const handleManualUpload = async (e) => { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onloadend = () => { + const imageDataUrl = reader.result; + setCapturedImage(imageDataUrl); + setIsFreeze(true); + + // Create an image object from the uploaded file + const image = new Image(); + image.onload = () => { + // Get the width of the rounded rectangle from rectRef + const rectWidth = rectRef.current.width; + const rectHeight = rectRef.current.height; + + // Create a canvas to draw the uploaded image + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + + // Set the scale factor based on the rectangle width + const scaleFactor = rectWidth / image.width; + + // Calculate the new height based on the aspect ratio + const newHeight = image.height * scaleFactor; + + // Clear the canvas and draw the video or freeze frame + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (isFreeze && freezeFrameRef.current) { + ctx.putImageData(freezeFrameRef.current, 0, 0); + } else { + const video = videoRef.current; + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + } + + // Draw the rounded rectangle + drawRoundedRect( + ctx, + rectRef.current.x, + rectRef.current.y, + rectRef.current.width, + rectRef.current.height, + rectRef.current.radius + ); + + // Draw the image inside the rounded rectangle + ctx.save(); + ctx.beginPath(); + ctx.moveTo( + rectRef.current.x + rectRef.current.radius, + rectRef.current.y + ); + ctx.lineTo( + rectRef.current.x + rectRef.current.width - rectRef.current.radius, + rectRef.current.y + ); + ctx.quadraticCurveTo( + rectRef.current.x + rectRef.current.width, + rectRef.current.y, + rectRef.current.x + rectRef.current.width, + rectRef.current.y + rectRef.current.radius + ); + ctx.lineTo( + rectRef.current.x + rectRef.current.width, + rectRef.current.y + rectRef.current.height - rectRef.current.radius + ); + ctx.quadraticCurveTo( + rectRef.current.x + rectRef.current.width, + rectRef.current.y + rectRef.current.height, + rectRef.current.x + rectRef.current.width - rectRef.current.radius, + rectRef.current.y + rectRef.current.height + ); + ctx.lineTo( + rectRef.current.x + rectRef.current.radius, + rectRef.current.y + rectRef.current.height + ); + ctx.quadraticCurveTo( + rectRef.current.x, + rectRef.current.y + rectRef.current.height, + rectRef.current.x, + rectRef.current.y + rectRef.current.height - rectRef.current.radius + ); + ctx.lineTo( + rectRef.current.x, + rectRef.current.y + rectRef.current.radius + ); + ctx.quadraticCurveTo( + rectRef.current.x, + rectRef.current.y, + rectRef.current.x + rectRef.current.radius, + rectRef.current.y + ); + ctx.closePath(); + ctx.clip(); // Clip the image within the rounded rectangle + + // Draw the uploaded image inside the clipped region + ctx.drawImage( + image, + rectRef.current.x, + rectRef.current.y, + rectWidth, + newHeight // Height is scaled based on the image's aspect ratio + ); + + ctx.restore(); + + // Save the image data into the freeze frame reference + freezeFrameRef.current = ctx.getImageData( + 0, + 0, + canvas.width, + canvas.height + ); + }; + image.src = imageDataUrl; + }; + reader.readAsDataURL(file); + }; + return (
diff --git a/src/RoleCard.js b/src/RoleCard.js new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Chart.js b/src/components/Chart.js new file mode 100644 index 0000000..87a575c --- /dev/null +++ b/src/components/Chart.js @@ -0,0 +1,8 @@ +import React from "react"; +import styles from "./Chart.module.css"; + +const Chart = () => { + return
[Chart Here]
; +}; + +export default Chart; diff --git a/src/components/Chart.module.css b/src/components/Chart.module.css new file mode 100644 index 0000000..96636da --- /dev/null +++ b/src/components/Chart.module.css @@ -0,0 +1,11 @@ +.chart { + margin-top: 2rem; + height: 200px; + background: linear-gradient(to top, #fdd, #fff); + border-radius: 12px; + display: flex; + justify-content: center; + align-items: center; + color: #df3422; + font-weight: 600; +} diff --git a/src/components/Header.js b/src/components/Header.js new file mode 100644 index 0000000..c3ebf36 --- /dev/null +++ b/src/components/Header.js @@ -0,0 +1,13 @@ +// Header.js +import React from "react"; +import styles from "./Header.module.css"; + +const Header = () => { + return ( +
+
Officers & Roles
+
+ ); +}; + +export default Header; diff --git a/src/components/Header.module.css b/src/components/Header.module.css new file mode 100644 index 0000000..36010e7 --- /dev/null +++ b/src/components/Header.module.css @@ -0,0 +1,5 @@ +.header { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 1rem; +} diff --git a/src/components/RoleCard.js b/src/components/RoleCard.js new file mode 100644 index 0000000..9575935 --- /dev/null +++ b/src/components/RoleCard.js @@ -0,0 +1,14 @@ +import React from "react"; +import styles from "./RoleCard.module.css"; + +const RoleCard = ({ title, value, code }) => { + return ( +
+
{title}
+
{value}
+ {code &&
{code}
} +
+ ); +}; + +export default RoleCard; diff --git a/src/components/RoleCard.module.css b/src/components/RoleCard.module.css new file mode 100644 index 0000000..048516d --- /dev/null +++ b/src/components/RoleCard.module.css @@ -0,0 +1,25 @@ +.card { + background: #fff; + border-radius: 16px; + padding: 1rem; + flex: 1; + min-width: 160px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); +} + +.title { + font-weight: 600; + color: #555; +} + +.value { + font-size: 1.5rem; + font-weight: bold; + color: #000; +} + +.code { + font-size: 0.9rem; + color: #df3422; + margin-top: 0.5rem; +} diff --git a/src/components/Sidebar.js b/src/components/Sidebar.js new file mode 100644 index 0000000..d68cf22 --- /dev/null +++ b/src/components/Sidebar.js @@ -0,0 +1,18 @@ +// Sidebar.js +import React from "react"; +import styles from "./Sidebar.module.css"; + +const Sidebar = () => { + return ( +
+
Dashboard
+
+
Officers
+
Roles
+
Key Performances
+
+
+ ); +}; + +export default Sidebar; diff --git a/src/components/Sidebar.module.css b/src/components/Sidebar.module.css new file mode 100644 index 0000000..303756d --- /dev/null +++ b/src/components/Sidebar.module.css @@ -0,0 +1,17 @@ +.sidebar { + width: 200px; + background-color: #df3422; + color: white; + padding: 1rem; +} + +.logo { + font-size: 1.25rem; + font-weight: bold; + margin-bottom: 2rem; +} + +.menuItem { + margin: 1rem 0; + cursor: pointer; +} diff --git a/src/index.js b/src/index.js index d563c0f..43850af 100644 --- a/src/index.js +++ b/src/index.js @@ -1,17 +1,15 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +// index.js +import React from "react"; +import ReactDOM from "react-dom/client"; // ✅ use 'react-dom/client' in React 18+ +import { BrowserRouter } from "react-router-dom"; +import App from "./App"; -const root = ReactDOM.createRoot(document.getElementById('root')); +// ✅ createRoot instead of render +const root = ReactDOM.createRoot(document.getElementById("root")); root.render( - + + + ); - -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals();