ok
This commit is contained in:
267
src/FormComponent.js
Normal file
267
src/FormComponent.js
Normal 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,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
< 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 >
|
||||||
|
</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;
|
||||||
@@ -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
62
src/Modal.js
Normal 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",
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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"));
|
||||||
|
|||||||
Reference in New Issue
Block a user