This commit is contained in:
zadit biasa aja
2025-06-28 14:51:16 +00:00
parent 3006d1332c
commit 05e92fe410
4 changed files with 364 additions and 81 deletions

267
src/FormComponent.js Normal file
View File

@@ -0,0 +1,267 @@
import React, { useEffect, useState } from "react";
const fieldLabels = {
nik: "NIK",
fullName: "Nama Lengkap",
birthPlace: "Tempat Lahir",
birthDate: "Tanggal Lahir",
gender: "Jenis Kelamin",
address: "Alamat",
neighborhoodCode: "RT/RW",
village: "Kelurahan/Desa",
subDistrict: "Kecamatan",
religion: "Agama",
maritalStatus: "Status Perkawinan",
occupation: "Pekerjaan",
nationality: "Kewarganegaraan",
validUntil: "Berlaku Hingga",
issuedCity: "Kota Terbit",
issuedDate: "Tanggal Terbit",
};
function Modal({ isOpen, onClose, loading, fileTemp, onSave, onDelete }) {
const [formData, setFormData] = useState({});
const [step, setStep] = useState(0);
// ❗Field yang disembunyikan, bisa diisi sesuai kebutuhan
const disabledFields = [];
useEffect(() => {
if (fileTemp) {
setFormData(Array.isArray(fileTemp) ? fileTemp[0] : fileTemp);
setStep(0);
} else {
setFormData({});
}
}, [fileTemp]);
if (!isOpen) return null;
const handleChange = (key, newValue, isDate = false) => {
setFormData((prev) => ({
...prev,
[key]: isDate ? { ...prev[key], value: newValue } : newValue,
}));
};
const formatDate = (value) => {
if (!value) return "";
const d = new Date(value);
return isNaN(d) ? "" : d.toISOString().split("T")[0];
};
const renderInput = (key, value) => {
if (value && typeof value === "object" && value.type === "dateTime") {
return (
<input
type="date"
value={formatDate(value.value)}
onChange={(e) => handleChange(key, e.target.value, true)}
style={styles.input}
/>
);
}
if (key === "address") {
return (
<textarea
rows={2}
value={value || ""}
onChange={(e) => handleChange(key, e.target.value)}
style={{ ...styles.input, resize: "vertical" }}
/>
);
}
return (
<input
type="text"
value={value != null ? value : ""}
onChange={(e) => handleChange(key, e.target.value)}
style={styles.input}
/>
);
};
// Langkah-langkah form (per halaman)
const rawSteps = [
["nik", "fullName", "birthPlace", "birthDate"],
["gender", "address", "neighborhoodCode", "village", "subDistrict"],
["religion", "maritalStatus", "occupation"],
["nationality", "validUntil", "issuedCity", "issuedDate"],
];
// Filter field yang disable/hide
const steps = rawSteps.map((fields) =>
fields.filter((key) => !disabledFields.includes(key))
);
// Filter langkah kosong
const visibleSteps = steps.filter((step) => step.length > 0);
return (
<div style={styles.overlay} onClick={onClose}>
<div style={styles.modal} onClick={(e) => e.stopPropagation()}>
{loading ? (
<div style={styles.spinnerContainer}>
<div style={styles.spinner} />
<style>{spinnerStyle}</style>
</div>
) : (
Object.keys(formData).length > 0 && (
<>
<h4>
Verifikasi Data (Langkah {step + 1} dari {visibleSteps.length})
</h4>
<table style={styles.table}>
<tbody>
{visibleSteps[step].map((key) => (
<tr key={key} style={styles.tableRow}>
<td style={styles.tableLabel}>
{fieldLabels[key] || key}
</td>
<td style={styles.tableInput}>
{renderInput(key, formData[key])}
</td>
</tr>
))}
</tbody>
</table>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginTop: 10,
}}
>
<button
disabled={step === 0}
onClick={() => setStep((s) => s - 1)}
style={{
...styles.saveButton,
opacity: step === 0 ? 0.5 : 1,
}}
>
&lt; Sebelumnya
</button>
<button
disabled={step === visibleSteps.length - 1}
onClick={() => setStep((s) => s + 1)}
style={{
...styles.saveButton,
opacity: step === visibleSteps.length - 1 ? 0.5 : 1,
}}
>
Selanjutnya &gt;
</button>
</div>
<div style={styles.actions}>
<button
onClick={() => onSave(formData)}
style={styles.saveButton}
>
Simpan ke Galeri
</button>
<button onClick={onDelete} style={styles.deleteButton}>
Hapus
</button>
</div>
</>
)
)}
</div>
</div>
);
}
// Styles dan spinner animation
const styles = {
overlay: {
position: "fixed",
inset: 0,
backgroundColor: "rgba(0,0,0,0.5)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: 1000,
},
modal: {
backgroundColor: "white",
borderRadius: 8,
padding: 20,
minWidth: 350,
maxWidth: "90vw",
maxHeight: "80vh",
overflowY: "auto",
boxShadow: "0 2px 10px rgba(0,0,0,0.3)",
},
spinnerContainer: {
textAlign: "center",
padding: 40,
},
spinner: {
border: "4px solid #f3f3f3",
borderTop: "4px solid #3498db",
borderRadius: "50%",
width: 40,
height: 40,
animation: "spin 1s linear infinite",
margin: "0 auto",
},
table: {
width: "100%",
borderCollapse: "collapse",
},
tableRow: {
borderBottom: "1px solid #eee",
},
tableLabel: {
padding: "8px 10px",
fontWeight: "bold",
width: "30%",
verticalAlign: "top",
textTransform: "capitalize",
},
tableInput: {
padding: "8px 10px",
},
input: {
padding: 6,
borderRadius: 4,
border: "1px solid #ccc",
width: "100%",
},
actions: {
marginTop: 20,
textAlign: "right",
},
saveButton: {
marginRight: 10,
backgroundColor: "green",
color: "white",
border: "none",
padding: "8px 14px",
borderRadius: 4,
cursor: "pointer",
},
deleteButton: {
backgroundColor: "red",
color: "white",
border: "none",
padding: "8px 14px",
borderRadius: 4,
cursor: "pointer",
},
};
const spinnerStyle = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
export default Modal;

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { v4 as uuidv4 } from "uuid"; import Modal from "./Modal";
import FormComponent from "./FormComponent";
const STORAGE_KEY = "camera_canvas_gallery"; const STORAGE_KEY = "camera_canvas_gallery";
@@ -13,6 +14,9 @@ const CameraCanvas = () => {
const [isFreeze, setIsFreeze] = useState(false); const [isFreeze, setIsFreeze] = useState(false);
const freezeFrameRef = useRef(null); const freezeFrameRef = useRef(null);
const [modalOpen, setModalOpen] = useState(false);
const [loading, setLoading] = useState(false);
const rectRef = useRef({ const rectRef = useRef({
x: 0, x: 0,
y: 0, y: 0,
@@ -178,58 +182,38 @@ const CameraCanvas = () => {
}; };
const ReadImage = async (capturedImage) => { const ReadImage = async (capturedImage) => {
const imageId = uuidv4();
try { try {
setLoading(true);
setModalOpen(true);
let res = await fetch( let res = await fetch(
"https://bot.kediritechnopark.com/webhook/mastersnapper/read", "https://bot.kediritechnopark.com/webhook/mastersnapper/read",
{ {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ imageId, image: capturedImage }), body: JSON.stringify({ image: capturedImage }),
} }
); );
const { output } = await res.json(); setLoading(false);
// Bersihkan dan parsing JSON dari output const data = await res.json();
const jsonString = output console.log(data);
.replace(/^```json/, "")
.replace(/```$/, "")
.trim();
const data = JSON.parse(jsonString); setFileTemp(data);
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) { } catch (error) {
console.error("Failed to read image:", error); console.error("Failed to read image:", error);
} }
}; };
const handleSaveTemp = async () => { const handleSaveTemp = async (correctedData) => {
try { try {
await fetch( await fetch(
"https://bot.kediritechnopark.com/webhook/mastersnapper/save", "https://bot.kediritechnopark.com/webhook/mastersnapper/save",
{ {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fileTemp }), body: JSON.stringify({ correctedData }),
} }
); );
@@ -397,13 +381,10 @@ const CameraCanvas = () => {
muted muted
style={{ display: "none" }} style={{ display: "none" }}
/> />
<canvas <canvas ref={canvasRef} style={{ maxWidth: "100%", height: "auto" }} />
ref={canvasRef}
style={{ border: "1px solid black", maxWidth: "100%", height: "auto" }}
/>
<canvas ref={hiddenCanvasRef} style={{ display: "none" }} /> <canvas ref={hiddenCanvasRef} style={{ display: "none" }} />
{!isFreeze ? ( {!isFreeze && !fileTemp ? (
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<button onClick={shootImage}>Shoot</button> <button onClick={shootImage}>Shoot</button>
<div style={{ marginBottom: 10 }}> <div style={{ marginBottom: 10 }}>
@@ -415,7 +396,7 @@ const CameraCanvas = () => {
/> />
</div> </div>
</div> </div>
) : ( ) : !fileTemp ? (
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<button onClick={() => setIsFreeze(false)}>Hapus</button> <button onClick={() => setIsFreeze(false)}>Hapus</button>
<button <button
@@ -423,52 +404,15 @@ const CameraCanvas = () => {
ReadImage(capturedImage); ReadImage(capturedImage);
}} }}
> >
Simpan Upload
</button> </button>
</div> </div>
)} ) : (
<FormComponent
{fileTemp && ( fileTemp={fileTemp}
<div onSave={handleSaveTemp}
style={{ onDelete={handleDeleteTemp}
marginTop: 20, />
padding: 10,
border: "1px solid #ccc",
borderRadius: 8,
}}
>
<h4>Verifikasi Data</h4>
<table>
<tbody>
{Object.entries(fileTemp).map(([key, value]) => (
<tr key={key}>
<td style={{ paddingRight: 10, fontWeight: "bold" }}>
{key}
</td>
<td>{value}</td>
</tr>
))}
</tbody>
</table>
<div style={{ marginTop: 10 }}>
<button
onClick={handleSaveTemp}
style={{
marginRight: 10,
backgroundColor: "green",
color: "white",
}}
>
Simpan ke Galeri
</button>
<button
onClick={handleDeleteTemp}
style={{ backgroundColor: "red", color: "white" }}
>
Hapus
</button>
</div>
</div>
)} )}
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
@@ -536,6 +480,15 @@ const CameraCanvas = () => {
</div> </div>
</div> </div>
</div> </div>
<Modal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
loading={loading}
fileTemp={fileTemp}
onSave={handleSaveTemp}
onDelete={handleDeleteTemp}
/>
</div> </div>
); );
}; };

62
src/Modal.js Normal file
View File

@@ -0,0 +1,62 @@
import React from "react";
const spinnerStyle = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
export default function Modal({ isOpen, onClose, loading, children }) {
if (!isOpen) return null;
return (
<div style={styles.overlay} onClick={onClose}>
<div style={styles.modal} onClick={(e) => e.stopPropagation()}>
{loading ? (
<div style={styles.spinnerContainer}>
<div style={styles.spinner} />
<style>{spinnerStyle}</style>
</div>
) : (
children
)}
</div>
</div>
);
}
const styles = {
overlay: {
position: "fixed",
inset: 0,
backgroundColor: "rgba(0,0,0,0.5)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: 1000,
},
modal: {
backgroundColor: "white",
borderRadius: 8,
padding: 20,
minWidth: 350,
maxWidth: "90vw",
maxHeight: "80vh",
overflowY: "auto",
boxShadow: "0 2px 10px rgba(0,0,0,0.3)",
},
spinnerContainer: {
textAlign: "center",
padding: 40,
},
spinner: {
border: "4px solid #f3f3f3",
borderTop: "4px solid #3498db",
borderRadius: "50%",
width: 40,
height: 40,
animation: "spin 1s linear infinite",
margin: "0 auto",
},
};

View File

@@ -3,6 +3,7 @@ import React from "react";
import ReactDOM from "react-dom/client"; // ✅ use 'react-dom/client' in React 18+ import ReactDOM from "react-dom/client"; // ✅ use 'react-dom/client' in React 18+
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import App from "./App"; import App from "./App";
import "./index.css";
// ✅ createRoot instead of render // ✅ createRoot instead of render
const root = ReactDOM.createRoot(document.getElementById("root")); const root = ReactDOM.createRoot(document.getElementById("root"));