init commit

This commit is contained in:
Rizqika 2024-11-11 10:03:20 +07:00
parent d72887445e
commit bd3a3d42ce
53 changed files with 18089 additions and 182 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
REACT_APP_BASE_URL = https://kapi.absys.ninja/veri
REACT_APP_API_KEY = NGKrycQefQrRvYm3KYwCIQQGnllGKlDd

687
index.html Normal file
View File

@ -0,0 +1,687 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" />
<meta name="author" content="" />
<title>Dashboard - SB Admin</title>
<link href="https://cdn.jsdelivr.net/npm/simple-datatables@7.1.2/dist/style.min.css" rel="stylesheet" />
<link href="css/styles.css" rel="stylesheet" />
<script src="https://use.fontawesome.com/releases/v6.3.0/js/all.js" crossorigin="anonymous"></script>
</head>
<body class="sb-nav-fixed">
<nav class="sb-topnav navbar navbar-expand navbar-dark bg-dark">
<!-- Navbar Brand-->
<a class="navbar-brand ps-3" href="index.html">Start Bootstrap</a>
<!-- Sidebar Toggle-->
<button class="btn btn-link btn-sm order-1 order-lg-0 me-4 me-lg-0" id="sidebarToggle" href="#!"><i class="fas fa-bars"></i></button>
<!-- Navbar Search-->
<form class="d-none d-md-inline-block form-inline ms-auto me-0 me-md-3 my-2 my-md-0">
<div class="input-group">
<input class="form-control" type="text" placeholder="Search for..." aria-label="Search for..." aria-describedby="btnNavbarSearch" />
<button class="btn btn-primary" id="btnNavbarSearch" type="button"><i class="fas fa-search"></i></button>
</div>
</form>
<!-- Navbar-->
<ul class="navbar-nav ms-auto ms-md-0 me-3 me-lg-4">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" id="navbarDropdown" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"><i class="fas fa-user fa-fw"></i></a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="#!">Settings</a></li>
<li><a class="dropdown-item" href="#!">Activity Log</a></li>
<li><hr class="dropdown-divider" /></li>
<li><a class="dropdown-item" href="#!">Logout</a></li>
</ul>
</li>
</ul>
</nav>
<div id="layoutSidenav">
<div id="layoutSidenav_nav">
<nav class="sb-sidenav accordion sb-sidenav-dark" id="sidenavAccordion">
<div class="sb-sidenav-menu">
<div class="nav">
<div class="sb-sidenav-menu-heading">Core</div>
<a class="nav-link" href="index.html">
<div class="sb-nav-link-icon"><i class="fas fa-tachometer-alt"></i></div>
Dashboard
</a>
<div class="sb-sidenav-menu-heading">Interface</div>
<a class="nav-link collapsed" href="#" data-bs-toggle="collapse" data-bs-target="#collapseLayouts" aria-expanded="false" aria-controls="collapseLayouts">
<div class="sb-nav-link-icon"><i class="fas fa-columns"></i></div>
Layouts
<div class="sb-sidenav-collapse-arrow"><i class="fas fa-angle-down"></i></div>
</a>
<div class="collapse" id="collapseLayouts" aria-labelledby="headingOne" data-bs-parent="#sidenavAccordion">
<nav class="sb-sidenav-menu-nested nav">
<a class="nav-link" href="layout-static.html">Static Navigation</a>
<a class="nav-link" href="layout-sidenav-light.html">Light Sidenav</a>
</nav>
</div>
<a class="nav-link collapsed" href="#" data-bs-toggle="collapse" data-bs-target="#collapsePages" aria-expanded="false" aria-controls="collapsePages">
<div class="sb-nav-link-icon"><i class="fas fa-book-open"></i></div>
Pages
<div class="sb-sidenav-collapse-arrow"><i class="fas fa-angle-down"></i></div>
</a>
<div class="collapse" id="collapsePages" aria-labelledby="headingTwo" data-bs-parent="#sidenavAccordion">
<nav class="sb-sidenav-menu-nested nav accordion" id="sidenavAccordionPages">
<a class="nav-link collapsed" href="#" data-bs-toggle="collapse" data-bs-target="#pagesCollapseAuth" aria-expanded="false" aria-controls="pagesCollapseAuth">
Authentication
<div class="sb-sidenav-collapse-arrow"><i class="fas fa-angle-down"></i></div>
</a>
<div class="collapse" id="pagesCollapseAuth" aria-labelledby="headingOne" data-bs-parent="#sidenavAccordionPages">
<nav class="sb-sidenav-menu-nested nav">
<a class="nav-link" href="login.html">Login</a>
<a class="nav-link" href="register.html">Register</a>
<a class="nav-link" href="password.html">Forgot Password</a>
</nav>
</div>
<a class="nav-link collapsed" href="#" data-bs-toggle="collapse" data-bs-target="#pagesCollapseError" aria-expanded="false" aria-controls="pagesCollapseError">
Error
<div class="sb-sidenav-collapse-arrow"><i class="fas fa-angle-down"></i></div>
</a>
<div class="collapse" id="pagesCollapseError" aria-labelledby="headingOne" data-bs-parent="#sidenavAccordionPages">
<nav class="sb-sidenav-menu-nested nav">
<a class="nav-link" href="401.html">401 Page</a>
<a class="nav-link" href="404.html">404 Page</a>
<a class="nav-link" href="500.html">500 Page</a>
</nav>
</div>
</nav>
</div>
<div class="sb-sidenav-menu-heading">Addons</div>
<a class="nav-link" href="charts.html">
<div class="sb-nav-link-icon"><i class="fas fa-chart-area"></i></div>
Charts
</a>
<a class="nav-link" href="tables.html">
<div class="sb-nav-link-icon"><i class="fas fa-table"></i></div>
Tables
</a>
</div>
</div>
<div class="sb-sidenav-footer">
<div class="small">Logged in as:</div>
Start Bootstrap
</div>
</nav>
</div>
<div id="layoutSidenav_content">
<main>
<div class="container-fluid px-4">
<h1 class="mt-4">Dashboard</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">Dashboard</li>
</ol>
<div class="row">
<div class="col-xl-3 col-md-6">
<div class="card bg-primary text-white mb-4">
<div class="card-body">Primary Card</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="#">View Details</a>
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-warning text-white mb-4">
<div class="card-body">Warning Card</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="#">View Details</a>
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-success text-white mb-4">
<div class="card-body">Success Card</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="#">View Details</a>
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-danger text-white mb-4">
<div class="card-body">Danger Card</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="#">View Details</a>
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xl-6">
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-chart-area me-1"></i>
Area Chart Example
</div>
<div class="card-body"><canvas id="myAreaChart" width="100%" height="40"></canvas></div>
</div>
</div>
<div class="col-xl-6">
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-chart-bar me-1"></i>
Bar Chart Example
</div>
<div class="card-body"><canvas id="myBarChart" width="100%" height="40"></canvas></div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-table me-1"></i>
DataTable Example
</div>
<div class="card-body">
<table id="datatablesSimple">
<thead>
<tr>
<th>Name</th>
<th>Position</th>
<th>Office</th>
<th>Age</th>
<th>Start date</th>
<th>Salary</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Name</th>
<th>Position</th>
<th>Office</th>
<th>Age</th>
<th>Start date</th>
<th>Salary</th>
</tr>
</tfoot>
<tbody>
<tr>
<td>Tiger Nixon</td>
<td>System Architect</td>
<td>Edinburgh</td>
<td>61</td>
<td>2011/04/25</td>
<td>$320,800</td>
</tr>
<tr>
<td>Garrett Winters</td>
<td>Accountant</td>
<td>Tokyo</td>
<td>63</td>
<td>2011/07/25</td>
<td>$170,750</td>
</tr>
<tr>
<td>Ashton Cox</td>
<td>Junior Technical Author</td>
<td>San Francisco</td>
<td>66</td>
<td>2009/01/12</td>
<td>$86,000</td>
</tr>
<tr>
<td>Cedric Kelly</td>
<td>Senior Javascript Developer</td>
<td>Edinburgh</td>
<td>22</td>
<td>2012/03/29</td>
<td>$433,060</td>
</tr>
<tr>
<td>Airi Satou</td>
<td>Accountant</td>
<td>Tokyo</td>
<td>33</td>
<td>2008/11/28</td>
<td>$162,700</td>
</tr>
<tr>
<td>Brielle Williamson</td>
<td>Integration Specialist</td>
<td>New York</td>
<td>61</td>
<td>2012/12/02</td>
<td>$372,000</td>
</tr>
<tr>
<td>Herrod Chandler</td>
<td>Sales Assistant</td>
<td>San Francisco</td>
<td>59</td>
<td>2012/08/06</td>
<td>$137,500</td>
</tr>
<tr>
<td>Rhona Davidson</td>
<td>Integration Specialist</td>
<td>Tokyo</td>
<td>55</td>
<td>2010/10/14</td>
<td>$327,900</td>
</tr>
<tr>
<td>Colleen Hurst</td>
<td>Javascript Developer</td>
<td>San Francisco</td>
<td>39</td>
<td>2009/09/15</td>
<td>$205,500</td>
</tr>
<tr>
<td>Sonya Frost</td>
<td>Software Engineer</td>
<td>Edinburgh</td>
<td>23</td>
<td>2008/12/13</td>
<td>$103,600</td>
</tr>
<tr>
<td>Jena Gaines</td>
<td>Office Manager</td>
<td>London</td>
<td>30</td>
<td>2008/12/19</td>
<td>$90,560</td>
</tr>
<tr>
<td>Quinn Flynn</td>
<td>Support Lead</td>
<td>Edinburgh</td>
<td>22</td>
<td>2013/03/03</td>
<td>$342,000</td>
</tr>
<tr>
<td>Charde Marshall</td>
<td>Regional Director</td>
<td>San Francisco</td>
<td>36</td>
<td>2008/10/16</td>
<td>$470,600</td>
</tr>
<tr>
<td>Haley Kennedy</td>
<td>Senior Marketing Designer</td>
<td>London</td>
<td>43</td>
<td>2012/12/18</td>
<td>$313,500</td>
</tr>
<tr>
<td>Tatyana Fitzpatrick</td>
<td>Regional Director</td>
<td>London</td>
<td>19</td>
<td>2010/03/17</td>
<td>$385,750</td>
</tr>
<tr>
<td>Michael Silva</td>
<td>Marketing Designer</td>
<td>London</td>
<td>66</td>
<td>2012/11/27</td>
<td>$198,500</td>
</tr>
<tr>
<td>Paul Byrd</td>
<td>Chief Financial Officer (CFO)</td>
<td>New York</td>
<td>64</td>
<td>2010/06/09</td>
<td>$725,000</td>
</tr>
<tr>
<td>Gloria Little</td>
<td>Systems Administrator</td>
<td>New York</td>
<td>59</td>
<td>2009/04/10</td>
<td>$237,500</td>
</tr>
<tr>
<td>Bradley Greer</td>
<td>Software Engineer</td>
<td>London</td>
<td>41</td>
<td>2012/10/13</td>
<td>$132,000</td>
</tr>
<tr>
<td>Dai Rios</td>
<td>Personnel Lead</td>
<td>Edinburgh</td>
<td>35</td>
<td>2012/09/26</td>
<td>$217,500</td>
</tr>
<tr>
<td>Jenette Caldwell</td>
<td>Development Lead</td>
<td>New York</td>
<td>30</td>
<td>2011/09/03</td>
<td>$345,000</td>
</tr>
<tr>
<td>Yuri Berry</td>
<td>Chief Marketing Officer (CMO)</td>
<td>New York</td>
<td>40</td>
<td>2009/06/25</td>
<td>$675,000</td>
</tr>
<tr>
<td>Caesar Vance</td>
<td>Pre-Sales Support</td>
<td>New York</td>
<td>21</td>
<td>2011/12/12</td>
<td>$106,450</td>
</tr>
<tr>
<td>Doris Wilder</td>
<td>Sales Assistant</td>
<td>Sidney</td>
<td>23</td>
<td>2010/09/20</td>
<td>$85,600</td>
</tr>
<tr>
<td>Angelica Ramos</td>
<td>Chief Executive Officer (CEO)</td>
<td>London</td>
<td>47</td>
<td>2009/10/09</td>
<td>$1,200,000</td>
</tr>
<tr>
<td>Gavin Joyce</td>
<td>Developer</td>
<td>Edinburgh</td>
<td>42</td>
<td>2010/12/22</td>
<td>$92,575</td>
</tr>
<tr>
<td>Jennifer Chang</td>
<td>Regional Director</td>
<td>Singapore</td>
<td>28</td>
<td>2010/11/14</td>
<td>$357,650</td>
</tr>
<tr>
<td>Brenden Wagner</td>
<td>Software Engineer</td>
<td>San Francisco</td>
<td>28</td>
<td>2011/06/07</td>
<td>$206,850</td>
</tr>
<tr>
<td>Fiona Green</td>
<td>Chief Operating Officer (COO)</td>
<td>San Francisco</td>
<td>48</td>
<td>2010/03/11</td>
<td>$850,000</td>
</tr>
<tr>
<td>Shou Itou</td>
<td>Regional Marketing</td>
<td>Tokyo</td>
<td>20</td>
<td>2011/08/14</td>
<td>$163,000</td>
</tr>
<tr>
<td>Michelle House</td>
<td>Integration Specialist</td>
<td>Sidney</td>
<td>37</td>
<td>2011/06/02</td>
<td>$95,400</td>
</tr>
<tr>
<td>Suki Burks</td>
<td>Developer</td>
<td>London</td>
<td>53</td>
<td>2009/10/22</td>
<td>$114,500</td>
</tr>
<tr>
<td>Prescott Bartlett</td>
<td>Technical Author</td>
<td>London</td>
<td>27</td>
<td>2011/05/07</td>
<td>$145,000</td>
</tr>
<tr>
<td>Gavin Cortez</td>
<td>Team Leader</td>
<td>San Francisco</td>
<td>22</td>
<td>2008/10/26</td>
<td>$235,500</td>
</tr>
<tr>
<td>Martena Mccray</td>
<td>Post-Sales support</td>
<td>Edinburgh</td>
<td>46</td>
<td>2011/03/09</td>
<td>$324,050</td>
</tr>
<tr>
<td>Unity Butler</td>
<td>Marketing Designer</td>
<td>San Francisco</td>
<td>47</td>
<td>2009/12/09</td>
<td>$85,675</td>
</tr>
<tr>
<td>Howard Hatfield</td>
<td>Office Manager</td>
<td>San Francisco</td>
<td>51</td>
<td>2008/12/16</td>
<td>$164,500</td>
</tr>
<tr>
<td>Hope Fuentes</td>
<td>Secretary</td>
<td>San Francisco</td>
<td>41</td>
<td>2010/02/12</td>
<td>$109,850</td>
</tr>
<tr>
<td>Vivian Harrell</td>
<td>Financial Controller</td>
<td>San Francisco</td>
<td>62</td>
<td>2009/02/14</td>
<td>$452,500</td>
</tr>
<tr>
<td>Timothy Mooney</td>
<td>Office Manager</td>
<td>London</td>
<td>37</td>
<td>2008/12/11</td>
<td>$136,200</td>
</tr>
<tr>
<td>Jackson Bradshaw</td>
<td>Director</td>
<td>New York</td>
<td>65</td>
<td>2008/09/26</td>
<td>$645,750</td>
</tr>
<tr>
<td>Olivia Liang</td>
<td>Support Engineer</td>
<td>Singapore</td>
<td>64</td>
<td>2011/02/03</td>
<td>$234,500</td>
</tr>
<tr>
<td>Bruno Nash</td>
<td>Software Engineer</td>
<td>London</td>
<td>38</td>
<td>2011/05/03</td>
<td>$163,500</td>
</tr>
<tr>
<td>Sakura Yamamoto</td>
<td>Support Engineer</td>
<td>Tokyo</td>
<td>37</td>
<td>2009/08/19</td>
<td>$139,575</td>
</tr>
<tr>
<td>Thor Walton</td>
<td>Developer</td>
<td>New York</td>
<td>61</td>
<td>2013/08/11</td>
<td>$98,540</td>
</tr>
<tr>
<td>Finn Camacho</td>
<td>Support Engineer</td>
<td>San Francisco</td>
<td>47</td>
<td>2009/07/07</td>
<td>$87,500</td>
</tr>
<tr>
<td>Serge Baldwin</td>
<td>Data Coordinator</td>
<td>Singapore</td>
<td>64</td>
<td>2012/04/09</td>
<td>$138,575</td>
</tr>
<tr>
<td>Zenaida Frank</td>
<td>Software Engineer</td>
<td>New York</td>
<td>63</td>
<td>2010/01/04</td>
<td>$125,250</td>
</tr>
<tr>
<td>Zorita Serrano</td>
<td>Software Engineer</td>
<td>San Francisco</td>
<td>56</td>
<td>2012/06/01</td>
<td>$115,000</td>
</tr>
<tr>
<td>Jennifer Acosta</td>
<td>Junior Javascript Developer</td>
<td>Edinburgh</td>
<td>43</td>
<td>2013/02/01</td>
<td>$75,650</td>
</tr>
<tr>
<td>Cara Stevens</td>
<td>Sales Assistant</td>
<td>New York</td>
<td>46</td>
<td>2011/12/06</td>
<td>$145,600</td>
</tr>
<tr>
<td>Hermione Butler</td>
<td>Regional Director</td>
<td>London</td>
<td>47</td>
<td>2011/03/21</td>
<td>$356,250</td>
</tr>
<tr>
<td>Lael Greer</td>
<td>Systems Administrator</td>
<td>London</td>
<td>21</td>
<td>2009/02/27</td>
<td>$103,500</td>
</tr>
<tr>
<td>Jonas Alexander</td>
<td>Developer</td>
<td>San Francisco</td>
<td>30</td>
<td>2010/07/14</td>
<td>$86,500</td>
</tr>
<tr>
<td>Shad Decker</td>
<td>Regional Director</td>
<td>Edinburgh</td>
<td>51</td>
<td>2008/11/13</td>
<td>$183,000</td>
</tr>
<tr>
<td>Michael Bruce</td>
<td>Javascript Developer</td>
<td>Singapore</td>
<td>29</td>
<td>2011/06/27</td>
<td>$183,000</td>
</tr>
<tr>
<td>Donna Snider</td>
<td>Customer Support</td>
<td>New York</td>
<td>27</td>
<td>2011/01/25</td>
<td>$112,000</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
<footer class="py-4 bg-light mt-auto">
<div class="container-fluid px-4">
<div class="d-flex align-items-center justify-content-between small">
<div class="text-muted">Copyright &copy; Your Website 2023</div>
<div>
<a href="#">Privacy Policy</a>
&middot;
<a href="#">Terms &amp; Conditions</a>
</div>
</div>
</div>
</footer>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="js/scripts.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.min.js" crossorigin="anonymous"></script>
<script src="assets/demo/chart-area-demo.js"></script>
<script src="assets/demo/chart-bar-demo.js"></script>
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@7.1.2/dist/umd/simple-datatables.min.js" crossorigin="anonymous"></script>
<script src="js/datatables-simple-demo.js"></script>
</body>
</html>

736
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,16 +3,26 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"ajv": "^8.17.1",
"ajv-keywords": "^5.1.0",
"bootstrap": "^5.3.3",
"font-awesome": "^4.7.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-scripts": "5.0.1", "react-drag-drop-files": "^2.4.0",
"react-router-dom": "^6.28.0",
"react-scripts": "^5.0.1",
"react-select": "^5.8.2",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "cross-env PORT=3001 react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
@ -34,5 +44,8 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"cross-env": "^7.0.3"
} }
} }

View File

@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -1,25 +1,90 @@
import logo from './logo.svg'; import React from 'react';
import './App.css'; import {
Navbar,
Sidebar,
Main,
Footer
} from './components';
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
import {
GettingStarted,
Dashboard,
Applications,
CreateApps
} from './screens/Home';
import {
FaceVerify,
FaceSummary,
FaceTransaction
} from './screens/Biometric/FaceRecognition';
function App() { import {
Enroll,
VerifySection,
Liveness,
Compare,
Search
} from './screens/Biometric/FaceRecognition/Section';
import {
VerifyKtp
} from './screens/Biometric/OcrKtp';
// Import all other components following the dataMenu structure...
const App = () => {
return ( return (
<div className="App"> <Router>
<header className="App-header"> <body className="sb-nav-fixed">
<img src={logo} className="App-logo" alt="logo" /> <Navbar />
<p> <div id="layoutSidenav">
Edit <code>src/App.js</code> and save to reload. <Sidebar />
</p> <div id="layoutSidenav_content">
<a <Routes>
className="App-link" {/* Main Dashboard */}
href="https://reactjs.org" <Route path="/" element={<GettingStarted />} />
target="_blank" <Route path="/getting-started" element={<GettingStarted />} />
rel="noopener noreferrer" <Route path="/dashboard" element={<Dashboard />} />
> <Route path="/application" element={<Applications />} />
Learn React <Route path="/createApps" element={<CreateApps />} />
</a>
</header> {/* Biometric - Face Recognition (Verify) */}
<Route path="/face-verify/*" element={<FaceVerify />}>
{/* Anak rute */}
<Route path="face-enroll" element={<Enroll />} />
<Route path="face-verifysection" element={<VerifySection />} />
<Route path="face-liveness" element={<Liveness />} />
<Route path="face-compare" element={<Compare />} />
<Route path="face-search" element={<Search />} />
{/* Default route */}
<Route index element={<Navigate to="face-enroll" />} />
</Route>
{/* Add routes for the verify section */}
<Route path="/face-enroll" element={<Enroll />} />
<Route path="/face-verifysection" element={<VerifySection />} />
<Route path="/face-liveness" element={<Liveness />} />
<Route path="/face-compare" element={<Compare />} />
<Route path="/face-search" element={<Search />} />
{/* Biometric - Face Recognition (Summary) */}
<Route path="/face-summary" element={<FaceSummary />} />
<Route path="/face-transaction" element={<FaceTransaction />} />
{/* Biometric - KTP */}
<Route path="/ktp-verify" element={<VerifyKtp />} />
{/* <Route path="/sms-otp-settings" element={<SmsOtpSettings />} /> */}
{/* Continue for each link */}
</Routes>
<Footer />
</div> </div>
</div>
</body>
</Router>
); );
} };
export default App; export default App;

View File

@ -1,8 +0,0 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

11289
src/assets/css/app.css Normal file

File diff suppressed because it is too large Load Diff

9
src/assets/icon/index.js Normal file
View File

@ -0,0 +1,9 @@
import OCR from './ocr.png';
import SmsAnnounce from './sms.png';
import OTP from './sms-otp.png'
export {
OCR,
SmsAnnounce,
OTP
}

BIN
src/assets/icon/ocr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

BIN
src/assets/icon/sms-otp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

BIN
src/assets/icon/sms.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
src/assets/images/Logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,11 @@
import ProfileImage from './Profile.jpeg';
import Logo from './Logo.png';
import DashboardImg from './dashboard-img.png';
import DummyKtp from './Dummy-Ktp.png';
export {
ProfileImage,
Logo,
DashboardImg,
DummyKtp
}

View File

@ -0,0 +1,70 @@
// src/components/ExternalScripts.js
import React, { useEffect } from 'react';
const ExternalScripts = () => {
useEffect(() => {
// Menambahkan FontAwesome script secara dinamis
const fontAwesomeScript = document.createElement('script');
fontAwesomeScript.src = "https://use.fontawesome.com/releases/v6.3.0/js/all.js";
fontAwesomeScript.crossOrigin = "anonymous";
fontAwesomeScript.async = true;
document.body.appendChild(fontAwesomeScript);
// Menambahkan Chart.js script secara dinamis
const chartJsScript = document.createElement('script');
chartJsScript.src = "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.min.js";
chartJsScript.crossOrigin = "anonymous";
chartJsScript.async = true;
document.body.appendChild(chartJsScript);
// Menambahkan Simple DataTables JS script secara dinamis
const simpleDatatablesScript = document.createElement('script');
simpleDatatablesScript.src = "https://cdn.jsdelivr.net/npm/simple-datatables@7.1.2/dist/umd/simple-datatables.min.js";
simpleDatatablesScript.crossOrigin = "anonymous";
simpleDatatablesScript.async = true;
document.body.appendChild(simpleDatatablesScript);
// Fungsi untuk menangani event DOMContentLoaded
const handleDOMContentLoaded = () => {
console.log('JS Activated');
// Toggle the side navigation
const sidebarToggle = document.body.querySelector('#sidebarToggle');
if (sidebarToggle) {
sidebarToggle.addEventListener('click', event => {
event.preventDefault();
document.body.classList.toggle('sb-sidenav-toggled');
localStorage.setItem('sb|sidebar-toggle', document.body.classList.contains('sb-sidenav-toggled'));
});
}
};
// Menunggu dokumen siap sebelum menambahkan event listener
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', handleDOMContentLoaded);
} else {
handleDOMContentLoaded();
}
// Cleanup function untuk menghapus script dan event listener saat komponen unmount
return () => {
document.body.removeChild(fontAwesomeScript);
document.body.removeChild(chartJsScript);
document.body.removeChild(simpleDatatablesScript);
document.removeEventListener('DOMContentLoaded', handleDOMContentLoaded);
};
}, []);
return (
<>
{/* Link CSS untuk Simple DataTables */}
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/simple-datatables@7.1.2/dist/style.min.css"
/>
</>
);
};
export default ExternalScripts;

28
src/assets/js/scripts.js Normal file
View File

@ -0,0 +1,28 @@
/*!
* Start Bootstrap - SB Admin v7.0.7 (https://startbootstrap.com/template/sb-admin)
* Copyright 2013-2023 Start Bootstrap
* Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-sb-admin/blob/master/LICENSE)
*/
//
// Scripts
//
window.addEventListener('DOMContentLoaded', event => {
console.log('JS Activated');
// Toggle the side navigation
const sidebarToggle = document.body.querySelector('#sidebarToggle');
if (sidebarToggle) {
// Uncomment Below to persist sidebar toggle between refreshes
// if (localStorage.getItem('sb|sidebar-toggle') === 'true') {
// document.body.classList.toggle('sb-sidenav-toggled');
// }
sidebarToggle.addEventListener('click', event => {
event.preventDefault();
document.body.classList.toggle('sb-sidenav-toggled');
localStorage.setItem('sb|sidebar-toggle', document.body.classList.contains('sb-sidenav-toggled'));
});
}
});

19
src/components/Footer.jsx Normal file
View File

@ -0,0 +1,19 @@
import React from 'react';
const Footer = () => {
return (
<footer className="py-4 bg-light mt-auto">
<div className="container-fluid px-4">
<div className="d-flex align-items-center justify-content-between small">
<div className="text-muted">Copyright &copy; Your Website 2023</div>
<div>
<a href="#">Privacy Policy</a> &middot;
<a href="#">Terms & Conditions</a>
</div>
</div>
</div>
</footer>
);
};
export default Footer;

16
src/components/Main.jsx Normal file
View File

@ -0,0 +1,16 @@
import React from 'react';
const Main = () => {
return (
<main>
<div className="container-fluid px-4">
<h1 className="mt-4">Dashboard</h1>
<ol className="breadcrumb mb-4">
<li className="breadcrumb-item active">Dashboard</li>
</ol>
</div>
</main>
);
};
export default Main;

165
src/components/Navbar.jsx Normal file
View File

@ -0,0 +1,165 @@
import React, { useState, useEffect } from 'react';
import { Logo } from '../assets/images';
import { Link } from 'react-router-dom';
const Navbar = () => {
const messageCount = 5; // Example count for messages
const notificationCount = 3; // Example count for notifications
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 600);
};
// Check the initial screen size
handleResize();
// Add event listener for window resize
window.addEventListener('resize', handleResize);
// Cleanup event listener on component unmount
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<nav className="sb-topnav navbar navbar-expand" style={{ backgroundColor: '#0542cc' }}>
<Link to="/getting-started">
<a className="navbar-brand ps-3 text-white d-flex align-items-center">
{/* Logo Image */}
<img
src={Logo}
alt="Logo"
style={{ width: '30px', height: '30px', marginRight: '10px' }} // Adjust size as needed
/>
Rekan Veri
</a>
</Link>
<button
className={`btn btn-link btn-sm order-1 order-lg-0 me-4 me-lg-0 text-white ${isMobile ? 'ms-auto' : ''}`}
id="sidebarToggle"
href="#!"
>
<i className="fas fa-bars"></i>
</button>
{/* Show navigation and search only on larger screens */}
{!isMobile && (
<>
<div className="navbar-nav me-auto">
<a className="nav-link text-white" href="/">Home</a>
<a className="nav-link text-white" href="/">Contact</a>
</div>
<form className="d-none d-md-inline-block form-inline ms-auto me-0 me-md-3 my-2 my-md-0">
<div className="input-group">
<input
className="form-control"
type="text"
placeholder="Search for..."
aria-label="Search for..."
aria-describedby="btnNavbarSearch"
/>
<button className="btn btn-primary" id="btnNavbarSearch" type="button">
<i className="fas fa-search"></i>
</button>
</div>
</form>
<ul className="navbar-nav ms-auto ms-md-0 me-3 me-lg-4">
{/* Messages Dropdown with Count */}
<li className="nav-item dropdown me-3 position-relative">
<a
className="nav-link dropdown-toggle text-white"
id="messagesDropdown"
href="#"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className="fas fa-envelope fa-fw"></i>
{messageCount > 0 && (
<span className="badge bg-danger badge-circle custom-badge">
{messageCount}
</span>
)}
</a>
<ul className="dropdown-menu dropdown-menu-end" aria-labelledby="messagesDropdown">
<li><a className="dropdown-item" href="#!">New Message</a></li>
<li><a className="dropdown-item" href="#!">Inbox</a></li>
<li><a className="dropdown-item" href="#!">Sent Messages</a></li>
</ul>
</li>
{/* Notifications Dropdown with Count */}
<li className="nav-item dropdown me-3 position-relative">
<a
className="nav-link dropdown-toggle text-white"
id="notificationsDropdown"
href="#"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className="fas fa-bell fa-fw"></i>
{notificationCount > 0 && (
<span className="badge bg-danger badge-circle custom-badge">
<strong>{notificationCount}</strong>
</span>
)}
</a>
<ul className="dropdown-menu dropdown-menu-end" aria-labelledby="notificationsDropdown">
<li><a className="dropdown-item" href="#!">New Notification</a></li>
<li><a className="dropdown-item" href="#!">Alerts</a></li>
<li><a className="dropdown-item" href="#!">Updates</a></li>
</ul>
</li>
{/* Expand Dropdown */}
<li className="nav-item dropdown me-3">
<a
className="nav-link dropdown-toggle text-white"
id="expandDropdown"
href="#"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className="fas fa-expand fa-fw"></i>
</a>
<ul className="dropdown-menu dropdown-menu-end" aria-labelledby="expandDropdown">
<li><a className="dropdown-item" href="#!">Full Screen</a></li>
<li><a className="dropdown-item" href="#!">Windowed Mode</a></li>
</ul>
</li>
{/* Apps Dropdown */}
<li className="nav-item dropdown me-3">
<a
className="nav-link dropdown-toggle text-white"
id="appsDropdown"
href="#"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className="fas fa-th fa-fw"></i>
</a>
<ul className="dropdown-menu dropdown-menu-end" aria-labelledby="appsDropdown">
<li><a className="dropdown-item" href="#!">App 1</a></li>
<li><a className="dropdown-item" href="#!">App 2</a></li>
<li><a className="dropdown-item" href="#!">App 3</a></li>
</ul>
</li>
</ul>
</>
)}
</nav>
);
};
export default Navbar;

View File

@ -0,0 +1,50 @@
import React from 'react';
import DeeperMenu from './DeeperMenu';
const DeepMenu = ({ name, link, target, subMenus, activeMenu, onMenuClick }) => {
return (
<>
{subMenus ? (
<a
className={`nav-link collapsed ${activeMenu === name ? 'active' : ''}`} // Menambahkan kelas 'active' pada DeepMenu
href="#"
data-bs-toggle="collapse"
data-bs-target={`#${target}`} // Menggunakan target untuk menghubungkan dengan collapse
aria-expanded="false"
aria-controls={target} // Memastikan ID yang sesuai digunakan untuk kontrol collapse
onClick={() => onMenuClick(name)} // Menangani klik pada DeepMenu
>
{name}
<div className="sb-sidenav-collapse-arrow">
<i className="fas fa-angle-down"></i>
</div>
</a>
) : (
<a
className={`nav-link ${activeMenu === name ? 'active' : ''}`} // Menambahkan kelas 'active' pada DeepMenu
href={link}
onClick={() => onMenuClick(name)} // Menangani klik pada DeepMenu
>
{name}
</a>
)}
{subMenus && (
<div className="collapse" id={target}>
<nav className="sb-sidenav-menu-nested nav">
{subMenus.map((deepSubMenu, index) => (
<DeeperMenu
key={index}
{...deepSubMenu}
activeMenu={activeMenu} // Menyediakan state activeMenu ke DeeperMenu
onMenuClick={onMenuClick} // Fungsi untuk menangani klik
/>
))}
</nav>
</div>
)}
</>
);
};
export default DeepMenu;

View File

@ -0,0 +1,15 @@
import { Link } from 'react-router-dom';
const DeeperMenu = ({ name, link, target, activeMenu, onMenuClick }) => {
return (
<Link
className={`nav-link ${activeMenu === name ? 'active' : ''}`} // Menambahkan kelas 'active' pada DeeperMenu
to={link} // Menggunakan `to` dari `Link` untuk navigasi
onClick={() => onMenuClick(name)} // Menangani klik pada DeeperMenu
>
{name}
</Link>
);
};
export default DeeperMenu;

View File

@ -0,0 +1,70 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import SubMenu from './SubMenu';
import dataMenu from './dataMenu';
const Menu = ({ searchQuery }) => {
const [activeMenu, setActiveMenu] = useState(null);
const handleMenuClick = (name) => {
setActiveMenu(name); // Menyimpan menu yang aktif
};
const filteredMenu = dataMenu.map((menuSection) => {
const filteredItems = menuSection.items.filter((item) => {
const itemName = item.name.toLowerCase();
const matches = itemName.includes(searchQuery);
const subMatches = item.subMenus?.some((subMenu) =>
subMenu.name.toLowerCase().includes(searchQuery)
);
return matches || subMatches;
});
return {
...menuSection,
items: filteredItems,
};
}).filter(section => section.items.length > 0); // Filter out sections without items
return (
<div className="sb-sidenav-menu mb-2">
<div className="nav">
{filteredMenu.map((menuSection, index) => (
<React.Fragment key={index}>
{menuSection.items.map((item, idx) => (
// Jika item memiliki submenu, render SubMenu
item.subMenus ? (
<SubMenu
key={idx}
heading={item.name}
target={item.target}
iconClass={menuSection.iconClass}
subMenus={item.subMenus.filter(subMenu =>
subMenu.name.toLowerCase().includes(searchQuery)
)}
activeMenu={activeMenu}
onMenuClick={handleMenuClick}
/>
) : (
// Jika item tidak memiliki submenu, gunakan <Link> untuk navigasi
<Link
key={idx}
className={`nav-link ${activeMenu === item.name ? 'active' : ''}`} // Menambahkan kelas 'active' pada item yang dipilih
to={item.link}
onClick={() => handleMenuClick(item.name)} // Memperbarui state activeMenu saat item dipilih
>
<div className="sb-nav-link-icon">
{item.iconClass && <i className={item.iconClass}></i>}
</div>
{item.name}
</Link>
)
))}
</React.Fragment>
))}
</div>
</div>
);
};
export default Menu;

View File

@ -0,0 +1,37 @@
// src/components/Profile.js
import React from 'react';
import { ProfileImage } from '../../assets/images';
const Profile = () => (
<div className="profile-section" style={styles.container}>
<img
src={ProfileImage}
alt="Profile"
className="profile-image"
style={styles.image}
/>
<span className="profile-name" style={styles.name}>Alexander Pierce</span>
</div>
);
export default Profile;
const styles = {
container: {
padding: '10px',
display: 'flex',
alignItems: 'center',
marginTop: '2vh'
},
image: {
width: '40px',
height: '40px',
borderRadius: '50%',
marginRight: '10px',
},
name: {
fontWeight: 'bold',
},
};

View File

@ -0,0 +1,50 @@
// src/components/SearchBar.js
import React from 'react';
const SearchBar = ({ onSearch }) => (
<div className="search-bar" style={styles.container}>
<input
type="text"
className="search-input"
placeholder="Search"
onChange={(e) => onSearch(e.target.value)}
style={styles.input}
/>
<button className="search-button" style={styles.button}>
<i className="fa fa-search" style={styles.icon}></i>
</button>
</div>
);
export default SearchBar;
const styles = {
container: {
padding: '0.1rem',
display: 'flex',
alignItems: 'center',
margin: '0 0.5rem 0 0.6rem'
},
input: {
width: '100%',
padding: '6px',
borderRadius: '4px 0 0 4px',
border: '1px solid #ddd',
outline: 'none',
fontSize: '0.9em',
},
button: {
padding: '6px',
border: '1px solid #ddd',
borderLeft: 'none',
borderRadius: '0 4px 4px 0',
backgroundColor: '#fff',
cursor: 'pointer',
},
icon: {
color: '#555',
fontSize: '0.9em',
},
};

View File

@ -0,0 +1,25 @@
import React, { useState } from 'react';
import Menu from './Menu';
import Profile from './Profile';
import SearchBar from './SearchBar';
const Sidebar = () => {
const [searchQuery, setSearchQuery] = useState('');
return (
<div id="layoutSidenav_nav">
<nav className="sb-sidenav accordion sb-sidenav-light" id="sidenavAccordion">
<Profile />
<SearchBar onSearch={(query) => setSearchQuery(query.toLowerCase())} />
<Menu searchQuery={searchQuery} />
<div className="sb-sidenav-footer">
<div className="small">Logged in as:</div>
Start Bootstrap
</div>
</nav>
</div>
);
};
export default Sidebar;

View File

@ -0,0 +1,41 @@
import React from 'react';
import DeepMenu from './DeepMenu';
const SubMenu = ({ heading, target, iconClass, subMenus, activeMenu, onMenuClick }) => {
return (
<>
<div className="sb-sidenav-menu-heading">{heading}</div>
<a
className={`nav-link collapsed ${activeMenu === heading ? 'active' : ''}`} // Menggunakan kelas active
href="#"
data-bs-toggle="collapse"
data-bs-target={`#${target}`}
aria-expanded="false"
aria-controls={target}
onClick={() => onMenuClick(heading)} // Memperbarui activeMenu
>
<div className="sb-nav-link-icon">
<i className={iconClass}></i>
</div>
{heading}
<div className="sb-sidenav-collapse-arrow">
<i className="fas fa-angle-down"></i>
</div>
</a>
<div className="collapse" id={target} aria-labelledby="headingOne">
<nav className="sb-sidenav-menu-nested nav">
{subMenus.map((subMenu, index) => (
<DeepMenu
key={index}
{...subMenu}
activeMenu={activeMenu}
onMenuClick={onMenuClick}
/>
))}
</nav>
</div>
</>
);
};
export default SubMenu;

View File

@ -0,0 +1,263 @@
// src/components/dataMenu.js
const dataMenu = [
{
items: [
{
name: 'Main Dashboard', // Changed the name
target: 'collapseHome',
subMenus: [
{
name: 'Getting Started',
link: '/getting-started'
},
{
name: 'Dashboard Overview', // Changed the name
link: '/dashboard'
},
{
name: 'Application Settings', // Changed the name
link: '/application'
},
],
},
],
iconClass: 'fas fa-tachometer-alt',
},
{
items: [
{
name: 'Biometric Systems', // Changed the name
target: 'collapseBiometric',
subMenus: [
{
name: 'Face Recognition System', // Changed the name
target: 'collapseFaceRecog',
subMenus: [
{ name: 'Verify Identity', link: '/face-verify'}, // Changed the name
{ name: 'Summary Report', link: '/face-summary'}, // Changed the name
{ name: 'Transaction Log', link: '/face-transaction'}, // Changed the name
],
},
{
name: 'KTP OCR', // Changed the name
target: 'collapseOcrKtp',
subMenus: [
{ name: 'Verify KTP', link: '/ktp-verify'}, // Changed the name
{ name: 'Manage Basic Auth', link: '/ktp-manage'},
{ name: 'Summary of KTPs', link: '/ktp-summary'}, // Changed the name
{ name: 'KTP Transaction History', link: '/ktp-transaction'}, // Changed the name
],
},
{
name: 'NPWP OCR', // Changed the name
target: 'collapseOcrNpwp',
subMenus: [
{ name: 'Verify NPWP', link: '/npwp-verify'}, // Changed the name
{ name: 'NPWP Summary', link: '/npwp-summary'}, // Changed the name
{ name: 'NPWP Transaction Log', link: '/npwp-transaction'}, // Changed the name
],
},
{
name: 'SIM OCR', // Changed the name
target: 'collapseOcrSim',
subMenus: [
{ name: 'Verify SIM', link: '/sim-verify'}, // Changed the name
{ name: 'SIM Summary', link: '/sim-summary'}, // Changed the name
{ name: 'SIM Transaction Log', link: '/sim-transaction'}, // Changed the name
],
},
{
name: 'Document OCR', // Changed the name
target: 'collapseOcrDocument',
subMenus: [
{ name: 'Verify Document', link: '/document-verify'}, // Changed the name
{ name: 'Document Summary', link: '/document-summary'}, // Changed the name
{ name: 'Document Transaction History', link: '/document-transaction'}, // Changed the name
],
},
],
},
],
iconClass: 'fas fa-user',
},
{
items: [
{
name: 'SMS Services', // Changed the name
target: 'collapseSms',
subMenus: [
{
name: 'SMS Verification', // Changed the name
link: '/sms-verify'
},
{
name: 'SMS OTP Management', // Changed the name
target: 'collapseSmsOtp',
subMenus: [
{ name: 'Settings', link: '/sms-otp-settings'},
{ name: 'Summary Report', link: '/sms-otp-summary'}, // Changed the name
{ name: 'Transaction Log', link: '/sms-otp-transaction'}, // Changed the name
{ name: 'Detail View', link: '/sms-otp-detail'}, // Changed the name
],
},
{
name: 'SMS Announcements', // Changed the name
target: 'collapseAnnouncement',
subMenus: [
{ name: 'Bulk Message', link: '/sms-announcement-bulk'}, // Changed the name
{ name: 'Announcement Summary', link: '/sms-announcement-summary'}, // Changed the name
{ name: 'Transaction Logs', link: '/sms-announcement-transaction'},
],
},
{
name: 'Blocked Numbers', // Changed the name
link: '/sms-block'
},
{
name: 'SMS Anomaly Report', // Changed the name
link: '/sms-anomaly'
},
],
},
],
iconClass: 'fas fa-phone',
},
{
items: [
{
name: 'WhatsApp Communication', // Changed the name
target: 'collapseWa',
subMenus: [
{
name: 'Verify WhatsApp Account', // Changed the name
link: '/wa-verify'
},
{
name: 'WhatsApp Management', // Changed the name
target: 'collapseWaManage',
subMenus: [
{ name: 'Register Business Account', link: '/wa-manage-register'}, // Changed the name
{ name: 'WhatsApp Profile Settings', link: '/wa-manage-profile'}, // Changed the name
{ name: 'Message Templates', link: '/wa-manage-template'}, // Changed the name
{ name: 'Integration Settings', link: '/wa-manage-integration'}, // Changed the name
],
},
{
name: 'WhatsApp Activity', // Changed the name
target: 'collapseActivity',
subMenus: [
{ name: 'Settings', link: '/wa-activity-settings'}, // Changed the name
{ name: 'Activity Summary', link: '/wa-activity-summary'}, // Changed the name
{ name: 'Transaction Logs', link: '/wa-activity-transaction'},
{ name: 'Bulk Sending', link: '/wa-activity-bulk'}, // Changed the name
],
},
{
name: 'WhatsApp Inbox', // Changed the name
link: '/wa-inbox'
},
{
name: 'Blocked WhatsApp Numbers', // Changed the name
link: '/wa-block'
},
],
},
],
iconClass: 'fab fa-whatsapp',
},
{
items: [
{
name: 'Identity Verification', // Changed the name
target: 'collapseIdentify',
subMenus: [
{
name: 'Electronic Certificate Verification', // Changed the name
target: 'collapseElectro',
subMenus: [
{ name: 'Verify Certificate', link: '/identify-electro-verify'}, // Changed the name
{ name: 'Transaction Logs', link: '/identify-electro-transaction'},
],
},
{
name: 'NPWP Verification', // Changed the name
target: 'collapseIdentifyNpwp',
subMenus: [
{ name: 'Transaction Logs', link: '/identify-npwp-transaction'}
],
},
{
name: 'Tax Number Verification', // Changed the name
target: 'collapseTax',
subMenus: [
{ name: 'Verify Tax Number', link: '/identify-tax-verify'}, // Changed the name
{ name: 'Transaction Logs', link: '/identify-tax-transaction'}
],
},
{
name: 'Income Verification', // Changed the name
target: 'collapseIncome',
subMenus: [
{ name: 'Verify Income', link: '/identify-income-verify'}, // Changed the name
{ name: 'Transaction Logs', link: '/identify-income-transaction'}
],
},
{
name: 'ID Verification', // Changed the name
target: 'collapseIdVerification',
subMenus: [
{ name: 'Verify ID', link: '/identify-id-verify'}, // Changed the name
{ name: 'Transaction Logs', link: '/identify-id-transaction'}
],
},
],
},
],
iconClass: 'fas fa-edit',
},
{
items: [
{
name: 'Watchlist Management', // Changed the name
target: 'collapseWatchlist',
subMenus: [
{
name: 'Watchlist Screening', // Changed the name
target: 'collapseScreening',
subMenus: [
{ name: 'Verify Watchlist', link: '/watchlist-screening-verify'}, // Changed the name
{ name: 'Admin Settings', link: '/watchlist-screening-admin'},
{ name: 'Search Watchlist', link: '/watchlist-screening-search'}, // Changed the name
{ name: 'Transaction Logs', link: '/watchlist-screening-transaction'},
{ name: 'Monitor Watchlist', link: '/watchlist-screening-monitor'},
],
},
],
},
],
iconClass: 'fas fa-calendar',
},
{
items: [
{
name: 'File Management', // Changed the name
target: 'collapseFiles',
subMenus: [
{
name: 'File Screening', // Changed the name
target: 'collapseScreening',
subMenus: [
{ name: 'Verify File', link: '/files-screening-verify'}, // Changed the name
{ name: 'Search Files', link: '/files-screening-search'}, // Changed the name
{ name: 'File Management Settings', link: '/files-screening-admin'},
],
},
],
},
],
iconClass: 'fas fa-cogs',
}
];
export default dataMenu;

11
src/components/index.js Normal file
View File

@ -0,0 +1,11 @@
import Navbar from "./Navbar";
import Sidebar from "./Sidebar/Sidebar";
import Main from "./Main";
import Footer from "./Footer";
export {
Navbar,
Sidebar,
Main,
Footer
}

View File

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -1,17 +1,17 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom';
import './index.css'; import './assets/css/app.css';
import App from './App'; import App from './App';
import reportWebVitals from './reportWebVitals'; import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
import ExternalScripts from './assets/js/externalScript'; // Mengimpor komponen ExternalScripts
const root = ReactDOM.createRoot(document.getElementById('root')); // Menyisipkan ExternalScripts di luar App
root.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
{/* Menambahkan ExternalScripts ke dalam aplikasi */}
<ExternalScripts />
<App /> <App />
</React.StrictMode> </React.StrictMode>,
document.getElementById('root')
); );
// 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();

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,13 +0,0 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -0,0 +1,720 @@
import React, { useState, useRef, useEffect } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronLeft, faChevronDown, faTimes, faImage } from '@fortawesome/free-solid-svg-icons';
import { FileUploader } from 'react-drag-drop-files';
import Select from 'react-select'
import { height } from '@fortawesome/free-solid-svg-icons/fa0';
const Compare = () => {
const BASE_URL = process.env.REACT_APP_BASE_URL
const API_KEY = process.env.REACT_APP_API_KEY
const [isSelectOpen, setIsSelectOpen] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [selectedImageName, setSelectedImageName] = useState('');
const [selectedCompareImageName, setSelectedCompareImageName] = useState('');
const fileInputRef = useRef(null);
const fileCompareInputRef = useRef(null);
const [showResult, setShowResult] = useState(false);
const [applicationId, setApplicationId] = useState('');
const [selectedQuota, setSelectedQuota] = useState(0);
const [thresholdId, setTresholdId] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [imageUrl, setImageUrl] = useState('');
const [imageCompareUrl, setImageCompareUrl] = useState('');
const [verified, setVerified] = useState(null);
const fileTypes = ["JPG", "JPEG", "PNG"];
const [file, setFile] = useState(null); // For the first image
const [compareFile, setCompareFile] = useState(null); // For the second imag
const [applicationIds, setApplicationIds] = useState([]);
const [inputValueApplication, setInputValueApplication] = useState(''); // Controlled input value for Application ID
const thresholdIds = [
{ id: 1, name: 'cosine', displayName: 'Basic' },
{ id: 2, name: 'euclidean', displayName: 'Medium' },
{ id: 3, name: 'euclidean_l2', displayName: 'High' },
];
const [applicationError, setApplicationError] = useState('');
const [thresholdError, setThresholdError] = useState('');
const [uploadError, setUploadError] = useState('');
const [compareUploadError, setCompareUploadError] = useState('');
useEffect(() => {
const fetchApplicationIds = async () => {
try {
setIsLoading(true)
const url = `${BASE_URL}/application/list`;
console.log('Fetching URL:', url); // Log the URL
const response = await fetch(url, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': `${API_KEY}`,
},
});
const data = await response.json();
if (data.status_code === 200) {
const ids = data.details.data.map(app => app.id);
console.log('Application Id: ' + ids); // Log the IDs
setApplicationIds(data.details.data); // Update state with the fetched data
} else {
console.error('Failed to fetch data:', data.details.message);
}
} catch (error) {
console.error('Error fetching application IDs:', error);
} finally {
setIsLoading(false)
}
};
fetchApplicationIds();
}, []);
const handleApplicationChange = (selectedOption) => {
if (selectedOption) {
const selectedId = selectedOption.value;
const selectedApp = applicationIds.find(app => app.id === parseInt(selectedId));
if (selectedApp) {
setApplicationId(selectedId);
setSelectedQuota(selectedApp.quota); // Set the selected quota
}
}
};
const handleInputChangeApplication = (newInputValue) => {
// Limit input to 15 characters for Application ID
if (newInputValue.length <= 15) {
setInputValueApplication(newInputValue);
}
};
const handleFocus = () => {
setIsSelectOpen(true);
};
const handleBlur = () => {
setIsSelectOpen(false);
};
const handleImageUpload = (file) => {
if (file && fileTypes.includes(file.name.split('.').pop().toUpperCase())) {
setSelectedImageName(file.name);
setFile(file); // Store the file directly in state
setUploadError(''); // Clear error if valid
}
};
const handleCompareImageUpload = (file) => {
if (file && fileTypes.includes(file.name.split('.').pop().toUpperCase())) {
setSelectedCompareImageName(file.name);
setCompareFile(file); // Store the compare file directly in state
setCompareUploadError(''); // Clear error if valid
}
};
const handleImageCancel = () => {
setSelectedImageName('');
setImageUrl('');
if (fileInputRef.current) fileInputRef.current.value = '';
};
const handleCompareImageCancel = () => {
setSelectedCompareImageName('');
setImageCompareUrl('');
if (fileCompareInputRef.current) fileCompareInputRef.current.value = '';
};
const handleCheckClick = async () => {
// Reset error messages
setApplicationError('');
setThresholdError('');
setUploadError('');
setCompareUploadError('');
setErrorMessage('');
// Initialize a flag to check for errors
let hasError = false;
// Validate Application ID
if (!applicationId) {
setApplicationError('Please select an Application ID before compare.');
hasError = true;
}
// Validate Threshold ID
const selectedThreshold = thresholdIds.find(threshold => threshold.name === thresholdId)?.name;
if (!selectedThreshold) {
setThresholdError('Invalid threshold selected.');
hasError = true;
}
// Validate Image Uploads
if (!selectedImageName) {
setUploadError('Please upload a face photo before compare.');
hasError = true;
}
if (!selectedCompareImageName) {
setCompareUploadError('Please upload a compare face photo before compare.');
hasError = true;
}
// If there are any errors, return early
if (hasError) {
return;
}
// Prepare FormData and log inputs
const formData = new FormData();
formData.append('application_id', applicationId);
formData.append('file1', file); // Use the state variable directly
formData.append('file2', compareFile); // Use the state variable directly
formData.append('threshold', selectedThreshold);
// Log the inputs
console.log('Inputs:', {
applicationId,
threshold: selectedThreshold,
file1: selectedImageName,
file2: selectedCompareImageName,
});
setIsLoading(true);
setErrorMessage('');
try {
const response = await fetch(`${BASE_URL}/face_recognition/compare`, {
method: 'POST',
headers: {
'accept': 'application/json',
'x-api-key': `${API_KEY}`,
},
body: formData,
});
const data = await response.json();
if (response.ok) {
// Fetch image URLs from response
const imageUrl1 = data.details.data.result.image_url1;
const imageUrl2 = data.details.data.result.image_url2;
await fetchImage(imageUrl1, setImageUrl);
await fetchImage(imageUrl2, setImageCompareUrl);
setVerified(data.details.data.result.verified);
setShowResult(true);
console.log('Comparison successful:', data);
} else {
console.error('Error response:', data);
const errorMessage = data.message || data.detail || data.details?.message || 'An unknown error occurred.';
setErrorMessage(errorMessage);
}
} catch (error) {
console.error('Error:', error);
setErrorMessage('An error occurred while making the request.');
} finally {
setIsLoading(false);
}
};
const fetchImage = async (imageUrl, setImageUrl) => {
setIsLoading(true);
try {
const response = await fetch(imageUrl, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': `${API_KEY}`, // Ensure this is valid
}
});
if (!response.ok) {
const errorDetails = await response.json();
console.error('Image fetch error details:', errorDetails);
setErrorMessage('Failed to fetch image, please try again.');
return;
}
const imageBlob = await response.blob();
const imageData = URL.createObjectURL(imageBlob);
console.log('Fetched image URL:', imageData);
setImageUrl(imageData); // Set the state with the blob URL
} catch (error) {
console.error('Error fetching image:', error);
setErrorMessage(error.message);
} finally {
setIsLoading(false);
}
};
const applicationOptions = applicationIds.map(app => ({
value: app.id,
label: app.name
}));
const ResultsSection = ({ showResult, verified, imageUrl, selectedImageName, imageCompareUrl, selectedCompareImageName }) => (
showResult && (
<div style={styles.containerResultStyle}>
<h1 style={{ color: '#0542cc', fontSize: '1.5rem', textAlign: 'center' }}>Results</h1>
<div style={styles.resultContainer}>
<table style={styles.tableStyle}>
<tbody>
<tr>
<td style={{ border: '0.1px solid gray', padding: '8px', width: '30%' }}>Similarity</td>
<td style={styles.similarityText(verified)}>
{verified !== null ? (verified ? 'True' : 'False') : 'N/A'}
</td>
</tr>
</tbody>
</table>
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: '20px' }}>
<div style={styles.imageContainer}>
<img
src={imageUrl || "path-to-your-image"}
alt="Original Foto"
style={styles.imageStyle}
/>
<p style={{ marginTop: '1rem', textAlign: 'center' }}>{selectedImageName}</p>
</div>
<div style={styles.imageCompareContainer}>
<img
src={imageCompareUrl || "path-to-your-image"}
alt="Compare Foto"
style={styles.imageStyle}
/>
<p style={{ marginTop: '1rem', textAlign: 'center' }}>{selectedCompareImageName}</p>
</div>
</div>
</div>
</div>
)
);
const formatFileSize = (sizeInBytes) => {
if (sizeInBytes < 1024) {
return `${sizeInBytes} bytes`; // Jika ukuran lebih kecil dari 1 KB
} else if (sizeInBytes < 1048576) {
return `${(sizeInBytes / 1024).toFixed(2)} KB`; // Jika ukuran lebih kecil dari 1 MB
} else {
return `${(sizeInBytes / 1048576).toFixed(2)} MB`; // Jika ukuran lebih besar dari 1 MB
}
};
return (
<div>
{/* Inject keyframes for the spinner */}
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
},
@media (max-width: 768px) {
.resultContainer {
flex-direction: column;
align-items: center;
}
}
`}
</style>
{isLoading && (
<div style={styles.loadingOverlay}>
<div style={styles.spinner}></div>
<p style={styles.loadingText}>Loading...</p>
</div>
)}
{/* Application ID Selection */}
<div className="form-group row align-items-center">
<div className="col-md-6">
<div className="select-wrapper">
<Select
id="applicationId"
value={applicationOptions.find(option => option.value === applicationId)}
onChange={handleApplicationChange} // Pass selected option directly
options={applicationOptions}
placeholder="Select Application ID"
isSearchable
menuPortalTarget={document.body}
menuPlacement="auto"
inputValue={inputValueApplication}
onInputChange={handleInputChangeApplication}
/>
</div>
{applicationError && <small style={styles.uploadError}>{applicationError}</small>}
</div>
<div className="col-md-6">
<p className="text-secondary" style={{ fontSize: '16px', fontWeight: '400', margin: '0', marginTop: '8px' }}>
Remaining Quota
</p>
<div style={styles.remainingQuota}>
<span style={styles.quotaText}>{selectedQuota}</span> {/* Display selected quota */}
<span style={styles.timesText}>(times)</span>
</div>
</div>
</div>
{/* Subject ID Input and Threshold Selection */}
<div className="form-group row align-items-center">
<div className="col-md-6">
<div style={styles.selectWrapper}>
<select
id="thresholdId"
className="form-control"
style={styles.select}
value={thresholdId}
onChange={(e) => setTresholdId(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
>
<option value="">Select Threshold</option>
{thresholdIds.map((app) => (
<option key={app.id} value={app.name}>
{app.displayName}
</option>
))}
</select>
<FontAwesomeIcon
icon={isSelectOpen ? faChevronDown : faChevronLeft}
style={styles.chevronIcon}
/>
{thresholdError && <small style={styles.uploadError}>{thresholdError}</small>}
</div>
</div>
</div>
<div className="row">
{/* Upload Image #1 */}
<div className="col-md-6">
<div className="row form-group mt-4">
<label style={{ fontWeight: 600, fontSize: '14px', color: '#212529' }}>Upload Face Photo</label>
<FileUploader
handleChange={handleImageUpload}
name="file"
types={fileTypes}
multiple={false}
children={
<div style={styles.uploadArea}>
<i className="fas fa-cloud-upload-alt" style={styles.uploadIcon}></i>
<p style={styles.uploadText}>Drag and Drop Here</p>
<p>Or</p>
<a href="#">Browse</a>
<p className="text-muted">Recommended size: 250x250 (Max File Size: 2MB)</p>
<p className="text-muted">Supported file types: JPG, JPEG</p>
</div>
}
/>
{uploadError && <small style={styles.uploadError}>{uploadError}</small>}
</div>
{/* Display uploaded image name */}
{selectedImageName && (
<div className="mt-4" style={styles.wrapper}>
<div style={styles.fileWrapper}>
<FontAwesomeIcon icon={faImage} style={styles.imageIcon} />
<div style={styles.textContainer}>
<h5>Uploaded File:</h5>
<p>{selectedImageName}</p>
{file && (
<p style={styles.fileSize}>
Size: {formatFileSize(file.size)}
</p>
)}
</div>
<div style={styles.closeButtonContainer}>
<button
style={styles.closeButton}
onClick={handleImageCancel}
aria-label="Remove uploaded image"
>
<FontAwesomeIcon
icon={faTimes}
style={styles.closeIcon}
/>
</button>
</div>
</div>
</div>
)}
</div>
{/* Upload Image #2 */}
<div className="col-md-6">
<div className="row form-group mt-4">
<label style={{ fontWeight: 600, fontSize: '14px', color: '#212529' }}>Upload Compare Photo</label>
<FileUploader
handleChange={handleCompareImageUpload}
name="file"
types={fileTypes}
multiple={false}
children={
<div style={styles.uploadArea}>
<i className="fas fa-cloud-upload-alt" style={styles.uploadIcon}></i>
<p style={styles.uploadText}>Drag and Drop Here</p>
<p>Or</p>
<a href="#">Browse</a>
<p className="text-muted">Recommended size: 250x250 (Max File Size: 2MB)</p>
<p className="text-muted">Supported file types: JPG, JPEG</p>
</div>
}
/>
{compareUploadError && <small style={styles.uploadError}>{compareUploadError}</small>}
</div>
{/* Display uploaded image name */}
{selectedCompareImageName && (
<div className="mt-4" style={styles.wrapper}>
<div style={styles.fileWrapper}>
<FontAwesomeIcon icon={faImage} style={styles.imageIcon} />
<div style={styles.textContainer}>
<h5>Uploaded File:</h5>
<p>{selectedCompareImageName}</p>
{file && (
<p style={styles.fileSize}>
Size: {formatFileSize(file.size)}
</p>
)}
</div>
<div style={styles.closeButtonContainer}>
<button
style={styles.closeButton}
onClick={handleImageCancel}
aria-label="Remove uploaded image"
>
<FontAwesomeIcon
icon={faTimes}
style={styles.closeIcon}
/>
</button>
</div>
</div>
</div>
)}
</div>
</div>
{/* Submit Button */}
<div style={styles.submitButton}>
<button onClick={handleCheckClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Check Now</p>
</button>
</div>
{/* Results Section */}
{showResult && (
<ResultsSection/>
)}
</div>
)
}
export default Compare
const styles = {
formGroup: {
marginTop: '-45px',
},
selectWrapper: {
position: 'relative',
marginTop: '0',
},
select: {
width: '100%',
paddingRight: '30px',
},
chevronIcon: {
position: 'absolute',
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'none',
},
remainingQuota: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginTop: '4px',
},
quotaText: {
fontSize: '40px',
color: '#0542cc',
fontWeight: '600',
},
timesText: {
marginLeft: '8px',
verticalAlign: 'super',
fontSize: '20px',
},
uploadArea: {
backgroundColor: '#e6f2ff',
height: '40svh',
cursor: 'pointer',
marginTop: '1rem',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
border: '1px solid #ced4da',
borderRadius: '0.25rem',
padding: '25px 10px 10px 10px'
},
uploadIcon: {
fontSize: '40px',
color: '#0542cc',
marginBottom: '7px',
},
uploadText: {
color: '#1f2d3d',
fontWeight: '400',
fontSize: '16px',
lineHeight: '13px',
},
wrapper: {
border: '1px solid #ddd',
borderRadius: '6px',
padding: '18px 10px 0 8px', // Padding lebih seragam
height: '13svh', // Tinggi lebih kecil untuk menyesuaikan tampilan
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#f9f9f9',
overflow: 'hidden',
},
fileWrapper: {
display: 'flex',
alignItems: 'center',
flex: '1',
},
textContainer: {
flex: '1',
fontSize: '16px', // Ukuran font lebih kecil
marginLeft: '6px',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
marginTop: '1rem'
},
fileSize: {
fontSize: '12px',
color: '#555',
marginBottom: '2rem',
},
closeButtonContainer: {
display: 'flex',
alignItems: 'center',
marginLeft: 'auto',
},
closeButton: {
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: '0',
},
imageIcon: {
color: '#0542cc',
fontSize: '18px', // Ukuran ikon sedikit lebih kecil
marginRight: '6px',
},
closeIcon: {
color: 'red',
fontSize: '18px',
},
submitButton: {
marginLeft: 'auto',
marginTop: '4rem',
textAlign: 'start',
position: 'relative',
zIndex: 1,
},
uploadError: {
color: 'red',
fontSize: '12px',
marginTop: '5px',
},
containerResultStyle: {
margin: '20px 0',
padding: '10px',
border: '1px solid #e0e0e0',
borderRadius: '8px',
backgroundColor: '#f9f9f9',
},
resultContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
tableStyle: {
width: '100%',
borderCollapse: 'collapse',
marginBottom: '20px',
},
imageContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
flex: 1,
padding: '10px',
},
imageCompareContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
flex: 1,
padding: '10px',
},
imageStyle: {
width: '100%',
height: 'auto',
maxWidth: '150px', // Limit image width
borderRadius: '8px',
},
similarityText: (verified) => ({
border: '0.1px solid gray',
padding: '8px',
color: verified ? 'green' : 'red',
fontWeight: 'bold',
}),
loadingOverlay: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.2)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
},
spinner: {
border: '4px solid rgba(0, 0, 0, 0.1)',
borderLeftColor: '#0542cc',
borderRadius: '50%',
width: '90px',
height: '90px',
animation: 'spin 1s ease-in-out infinite',
},
loadingText: {
marginTop: '10px',
fontSize: '1.2rem',
color: '#fff',
textAlign: 'center',
},
};

View File

@ -0,0 +1,841 @@
import React, { useState, useRef, useEffect } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTimes, faImage } from '@fortawesome/free-solid-svg-icons';
import { FileUploader } from 'react-drag-drop-files';
import Select from 'react-select'
const Enroll = () => {
const BASE_URL = process.env.REACT_APP_BASE_URL
const API_KEY = process.env.REACT_APP_API_KEY
const fileTypes = ["JPG", "JPEG", "PNG"];
const [file, setFile] = useState(null);
const [errorMessage, setErrorMessage] = useState('');
const [selectedImageName, setSelectedImageName] = useState('');
const fileInputRef = useRef(null);
const [showResult, setShowResult] = useState(false);
const [applicationId, setApplicationId] = useState('');
const [applicationIds, setApplicationIds] = useState([]);
const [selectedQuota, setSelectedQuota] = useState(0);
const [subjectId, setSubjectId] = useState('');
const [subjectIds, setSubjectIds] = useState([]);
const [imageUrl, setImageUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [applicationError, setApplicationError] = useState('');
const [subjectError, setSubjectError] = useState('');
const [imageError, setImageError] = useState('');
const [subjectAvailabilityMessage, setSubjectAvailabilityMessage] = useState(''); // Message for subject availability
const [inputValueApplication, setInputValueApplication] = useState(''); // Controlled input value for Application ID
const [options, setOptions] = useState([]);
const [isMobile, setIsMobile] = useState(false);
const styles = {
// Existing styles
formGroup: {
marginTop: '-45px',
},
selectWrapper: {
position: 'relative',
marginTop: '0',
},
select: {
width: '100%',
paddingRight: '30px',
},
chevronIcon: {
position: 'absolute',
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'none',
},
remainingQuota: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginTop: '4px',
},
quotaText: {
fontSize: '40px',
color: '#0542cc',
fontWeight: '600',
},
timesText: {
marginLeft: '8px',
verticalAlign: 'super',
fontSize: '20px',
},
uploadArea: {
backgroundColor: '#e6f2ff',
height: '250px', // Default height for non-mobile devices
cursor: 'pointer',
marginTop: '1rem',
paddingTop: '22px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
border: '1px solid #ced4da',
borderRadius: '0.25rem',
},
// Mobile responsive styles for upload area
uploadAreaMobile: {
backgroundColor: '#e6f2ff',
height: '50svh', // Reduced height for mobile
cursor: 'pointer',
marginTop: '1rem',
paddingTop: '18px', // Adjusted padding for mobile
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
border: '1px solid #ced4da',
borderRadius: '0.25rem',
padding: '20px'
},
uploadIcon: {
fontSize: '40px',
color: '#0542cc',
marginBottom: '7px',
},
uploadText: {
color: '#1f2d3d',
fontWeight: '400',
fontSize: '16px',
lineHeight: '13px',
},
wrapper: {
border: '1px solid #ddd',
borderRadius: '6px',
padding: '18px 10px 0 8px', // Padding lebih seragam
height: '13svh', // Tinggi lebih kecil untuk menyesuaikan tampilan
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#f9f9f9',
overflow: 'hidden',
},
fileWrapper: {
display: 'flex',
alignItems: 'center',
flex: '1',
},
textContainer: {
flex: '1',
fontSize: '16px', // Ukuran font lebih kecil
marginLeft: '6px',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
marginTop: '1rem'
},
fileSize: {
fontSize: '12px',
color: '#555',
marginBottom: '2rem',
},
closeButtonContainer: {
display: 'flex',
alignItems: 'center',
marginLeft: 'auto',
},
closeButton: {
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: '0',
},
imageIcon: {
color: '#0542cc',
fontSize: '18px', // Ukuran ikon sedikit lebih kecil
marginRight: '6px',
},
closeIcon: {
color: 'red',
fontSize: '18px',
},
submitButton: {
marginLeft: 'auto',
marginTop: '4rem',
textAlign: 'start',
position: 'relative',
zIndex: 1,
},
uploadError: {
color: 'red',
fontSize: '12px',
marginTop: '5px',
},
// New styles added and merged
containerResultStyle: {
padding: '20px',
border: '1px solid #0053b3',
borderRadius: '5px',
width: '100%',
margin: '20px auto',
},
resultContainer: {
display: 'flex',
justifyContent: 'space-between', // Horizontal alignment
alignItems: 'flex-start', // Align items at the top
flexDirection: isMobile ? 'column' : 'row', // Stack vertically on mobile
width: '100%',
},
resultsTable: {
width: '60%',
borderCollapse: 'collapse',
},
resultsTableMobile: {
width: '100%',
borderCollapse: 'collapse',
},
resultsCell: {
padding: '8px',
width: '30%',
fontSize: isMobile ? '14px' : '16px',
},
resultsValueCell: {
padding: '8px',
width: '70%',
fontSize: isMobile ? '14px' : '16px',
color: 'red',
},
resultsTrueValue: {
color: 'inherit',
},
imageContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: isMobile ? 'center' : 'flex-start', // Center image on mobile
width: '100%',
marginTop: isMobile ? '10px' : '0', // Add margin for spacing on mobile
},
imageStyle: {
width: '300px',
height: '300px',
borderRadius: '5px',
},
imageStyleMobile: {
width: '100%', // Make image responsive on mobile
height: 'auto',
borderRadius: '5px',
},
imageDetails: {
marginTop: '10px',
fontSize: isMobile ? '14px' : '16px', // Adjust font size on mobile
color: '#1f2d3d',
},
loadingOverlay: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.2)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
},
spinner: {
border: '4px solid rgba(0, 0, 0, 0.1)',
borderLeftColor: '#0542cc',
borderRadius: '50%',
width: '90px',
height: '90px',
animation: 'spin 1s ease-in-out infinite',
},
loadingText: {
marginTop: '10px',
fontSize: '1.2rem',
color: '#fff',
textAlign: 'center',
},
uploadedFileWrapper: {
backgroundColor: '#fff',
border: '0.2px solid gray',
padding: '15px 0 0 17px',
borderRadius: '5px',
display: 'flex',
alignItems: 'center',
gap: '10px',
justifyContent: 'space-between',
},
uploadedFileInfo: {
marginRight: '18rem',
marginTop: '0.2rem',
},
uploadedFileText: {
fontSize: '16px',
color: '#1f2d3d',
},
resultsTable: {
width: '60%',
borderCollapse: 'collapse',
},
resultsRow: {
border: '0.1px solid gray',
padding: '8px',
},
resultsCell: {
padding: '8px',
width: '30%',
},
resultsValueCell: {
padding: '8px',
width: '70%',
color: 'red',
},
resultsTrueValue: {
color: 'inherit',
},
customLabel: {
fontWeight: 600, fontSize: '14px', color: '#212529'
},
// Mobile responsiveness adjustments (if necessary)
responsiveImageStyle: {
width: '100%',
maxHeight: '250px',
objectFit: 'cover',
marginTop: '20px',
},
responsiveResultContainer: {
padding: '1rem',
border: '1px solid #ccc',
borderRadius: '8px',
marginTop: '20px',
},
responsiveImageContainer: {
marginTop: '20px',
textAlign: 'center',
},
responsiveSubmitButton: {
marginTop: '1rem',
},
responsiveLoadingOverlay: {
position: 'absolute',
top: '0',
left: '0',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: '10',
},
responsiveSpinner: {
border: '4px solid #f3f3f3',
borderTop: '4px solid #3498db',
borderRadius: '50%',
width: '50px',
height: '50px',
animation: 'spin 2s linear infinite',
},
responsiveLoadingText: {
color: 'white',
marginTop: '10px',
},
}
useEffect(() => {
const fetchApplicationIds = async () => {
setIsLoading(true);
const url = `${BASE_URL}/application/list`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': API_KEY,
},
});
const data = await response.json();
if (data.status_code === 200) {
setApplicationIds(data.details.data);
} else {
console.error('Failed to fetch data:', data.details.message);
}
} catch (error) {
console.error('Error fetching application IDs:', error);
} finally {
setIsLoading(false);
}
};
fetchApplicationIds();
setOptions(subjectIds.map(id => ({ value: id, label: id })));
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Deteksi apakah layar kecil (mobile)
};
window.addEventListener('resize', handleResize);
handleResize(); // Initial check
return () => window.removeEventListener('resize', handleResize);
}, [subjectIds]);
const handleApplicationChange = async (selectedOption) => {
const selectedId = selectedOption.value;
const selectedApp = applicationIds.find(app => app.id === parseInt(selectedId));
if (!selectedOption) {
console.error("Selected option is undefined");
return;
}
if (selectedApp) {
setSelectedQuota(selectedApp.quota);
}
setApplicationId(selectedId);
// Fetch subjects related to the application
await fetchSubjectIds(selectedId);
};
const handleInputChangeApplication = (newInputValue) => {
// Limit input to 15 characters for Application ID
if (newInputValue.length <= 15) {
setInputValueApplication(newInputValue);
}
};
const fetchSubjectIds = async (appId) => {
setIsLoading(true);
try {
const response = await fetch(`${BASE_URL}/trx_face/list/subject?application_id=${appId}&search=${subjectId}&limit=10`, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': API_KEY,
},
});
const data = await response.json();
console.log("Fetched Subject IDs:", data); // Log data fetched from API
if (data.status_code === 200) {
setSubjectIds(data.details.data);
} else {
console.error('Failed to fetch subject IDs:', data.details.message);
}
} catch (error) {
console.error('Error fetching subject IDs:', error);
} finally {
setIsLoading(false);
}
};
const handleImageUpload = (file) => {
// Ensure the file is not undefined or null before accessing its properties
if (file && file.name) {
const fileExtension = file.name.split('.').pop().toUpperCase();
if (fileTypes.includes(fileExtension)) {
setSelectedImageName(file.name);
setFile(file);
setImageError(''); // Clear any previous errors
} else {
alert('Image format is not supported');
setImageError('Image format is not supported');
setFile(null);
}
} else {
console.error('No file selected or invalid file object.');
}
};
const handleImageCancel = () => {
setSelectedImageName('');
setFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleEnrollClick = async () => {
let hasError = false; // Track if there are any errors
// Validate inputs and set corresponding errors
const validationErrors = {
imageError: !selectedImageName ? 'Please upload a face photo before enrolling.' : '',
applicationError: !applicationId ? 'Please select an Application ID before enrolling.' : '',
subjectError: !subjectId ? 'Please enter a Subject ID before enrolling.' : '',
};
// Update state with errors
if (validationErrors.imageError) {
setImageError(validationErrors.imageError);
hasError = true;
} else {
setImageError(''); // Clear error if valid
}
if (validationErrors.applicationError) {
setApplicationError(validationErrors.applicationError);
hasError = true;
} else {
setApplicationError(''); // Clear error if valid
}
if (validationErrors.subjectError) {
setSubjectError(validationErrors.subjectError);
hasError = true;
} else {
setSubjectError(''); // Clear error if valid
}
// If there are errors, return early
if (hasError) return;
if (!file) {
setImageError('No file selected. Please upload a valid image file.');
return;
}
const formData = new FormData();
formData.append('application_id', String(applicationId));
formData.append('subject_id', subjectId);
formData.append('file', file);
console.log('Inputs:', {
applicationId,
subjectId,
file: file.name,
});
setIsLoading(true);
setErrorMessage(''); // Clear previous error message
try {
const response = await fetch(`${BASE_URL}/face_recognition/enroll`, {
method: 'POST',
body: formData,
headers: {
'accept': 'application/json',
'x-api-key': `${API_KEY}`,
}
});
if (!response.ok) {
const errorDetails = await response.json();
console.error('Response error details:', errorDetails);
// Periksa jika detail error terkait dengan Subject ID
if (errorDetails.detail && errorDetails.detail.includes('Subject ID')) {
setSubjectError(errorDetails.detail); // Tampilkan error di bawah input Subject ID
} else {
setErrorMessage(errorDetails.detail || 'Failed to enroll, please try again');
}
return;
}
const result = await response.json();
console.log('Enrollment response:', result);
if (result.details && result.details.data && result.details.data.image_url) {
const imageFileName = result.details.data.image_url.split('/').pop();
console.log('Image URL:', result.details.data.image_url);
await fetchImage(imageFileName);
} else {
console.error('Image URL not found in response:', result);
setErrorMessage('Image URL not found in response. Please try again.');
}
setShowResult(true);
console.log('Enrollment successful:', result);
} catch (error) {
console.error('Error during API call:', error);
setErrorMessage('An unexpected error occurred. Please try again.');
} finally {
setIsLoading(false);
}
};
const fetchImage = async (imageFileName) => {
setIsLoading(true);
try {
const response = await fetch(`${BASE_URL}/preview/image/${imageFileName}`, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': API_KEY,
}
});
if (!response.ok) {
const errorDetails = await response.json();
console.error('Image fetch error details:', errorDetails);
setErrorMessage('Failed to fetch image, please try again.');
return;
}
const imageBlob = await response.blob();
const imageData = URL.createObjectURL(imageBlob);
console.log('Fetched image URL:', imageData);
setImageUrl(imageData);
} catch (error) {
console.error('Error fetching image:', error);
setErrorMessage(error.message);
} finally {
setIsLoading(false);
}
};
const CustomLabel = ({ overRide, children, ...props }) => {
// We intentionally don't pass `overRide` to the label
return (
<label {...props}>
{children}
</label>
);
};
const applicationOptions = applicationIds.map(app => ({
value: app.id,
label: app.name
}));
const handleSubjectIdChange = async (e) => {
const id = e.target.value;
setSubjectId(id);
console.log("Current Subject ID Input:", id); // Debugging: Log input
if (id) {
const exists = subjectIds.includes(id);
console.log("Subject IDs:", subjectIds); // Debugging: Log existing Subject IDs
if (exists) {
setSubjectAvailabilityMessage('Subject already exists.'); // Error message
setSubjectError(''); // Clear any subject error
} else {
setSubjectAvailabilityMessage('This subject ID is available.'); // Success message
setSubjectError('');
}
} else {
setSubjectAvailabilityMessage(''); // Clear message if input is empty
}
};
// Fungsi untuk mengonversi ukuran file dari byte ke KB/MB
const formatFileSize = (sizeInBytes) => {
if (sizeInBytes < 1024) {
return `${sizeInBytes} bytes`; // Jika ukuran lebih kecil dari 1 KB
} else if (sizeInBytes < 1048576) {
return `${(sizeInBytes / 1024).toFixed(2)} KB`; // Jika ukuran lebih kecil dari 1 MB
} else {
return `${(sizeInBytes / 1048576).toFixed(2)} MB`; // Jika ukuran lebih besar dari 1 MB
}
};
return (
<div>
{/* Inject keyframes for the spinner */}
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.select-wrapper {
max-height: 200px; /* Limit the height of the container */
overflow-y: auto; /* Enable vertical scroll if the content overflows */
}
`}
</style>
{isLoading && (
<div style={styles.loadingOverlay}>
<div style={styles.spinner}></div>
<p style={styles.loadingText}>Loading...</p>
</div>
)}
{/* Application ID Selection */}
<div className="form-group row align-items-center">
<div className="col-md-6">
<div className="select-wrapper">
<Select
id="applicationId"
value={applicationOptions.find(option => option.value === applicationId)}
onChange={handleApplicationChange}
options={applicationOptions}
placeholder="Select Application ID"
isSearchable
menuPortalTarget={document.body}
menuPlacement="auto"
inputValue={inputValueApplication}
onInputChange={handleInputChangeApplication} // Limit input length for Application ID
/>
</div>
{applicationError && <small style={{ color: 'red' }}>{applicationError}</small>}
</div>
<div className="col-md-6">
<p className="text-secondary" style={{ fontSize: '16px', fontWeight: '400', margin: '0', marginTop: '8px' }}>
Remaining Quota
</p>
<div style={styles.remainingQuota}>
<span style={styles.quotaText}>{selectedQuota}</span>
<span style={styles.timesText}>(times)</span>
</div>
</div>
</div>
{/* Subject ID Input */}
<div className="form-group row align-items-center">
<div className="col-md-6">
<input
type="text"
id="subjectId"
className="form-control"
placeholder="Enter Subject ID"
value={subjectId}
onChange={handleSubjectIdChange}
onFocus={() => fetchSubjectIds(applicationId)}
maxLength={15}
/>
{subjectError && <small style={{ color: 'red' }}>{subjectError}</small>}
{subjectAvailabilityMessage && (
<small style={{ color: subjectAvailabilityMessage.includes('available') ? 'green' : 'red' }}>
{subjectAvailabilityMessage}
</small>
)}
</div>
</div>
{/* Upload Section */}
<div className='col-md-6'>
<div className="row form-group mt-4">
<CustomLabel htmlFor="uploadPhoto" style={styles.customLabel}>
Upload Face Photo
</CustomLabel>
<FileUploader
handleChange={handleImageUpload}
name="file"
types={fileTypes}
multiple={false}
onDrop={(files) => {
if (files && files[0]) {
handleImageUpload(files[0]);
} else {
console.error('No valid files dropped');
}
}}
children={(
<div style={isMobile ? styles.uploadAreaMobile : styles.uploadArea}>
<i className="fas fa-cloud-upload-alt" style={styles.uploadIcon}></i>
<p style={styles.uploadText}>Drag and Drop Here</p>
<p>Or</p>
<a href="#" onClick={() => fileInputRef.current.click()}>Browse</a>
<p className="text-muted">Recommended size: 300x300 (Max File Size: 2MB)</p>
<p className="text-muted">Supported file types: JPG, JPEG</p>
</div>
)}
/>
<input
type="file"
id="fileUpload"
ref={fileInputRef}
style={{ display: 'none' }}
accept="image/jpeg, image/jpg"
onChange={e => handleImageUpload(e.target.files[0])}
/>
{imageError && <small style={{ color: 'red' }}>{imageError}</small>}
</div>
</div>
{/* Display uploaded image name */}
{selectedImageName && (
<div className="col-md-6 mt-4" style={styles.wrapper}>
<div style={styles.fileWrapper}>
<FontAwesomeIcon icon={faImage} style={styles.imageIcon} />
<div style={styles.textContainer}>
<h5>Uploaded File:</h5>
<p>{selectedImageName}</p>
{file && (
<p style={styles.fileSize}>
Size: {formatFileSize(file.size)}
</p>
)}
</div>
<div style={styles.closeButtonContainer}>
<button
style={styles.closeButton}
onClick={handleImageCancel}
aria-label="Remove uploaded image"
>
<FontAwesomeIcon
icon={faTimes}
style={styles.closeIcon}
/>
</button>
</div>
</div>
</div>
)}
{/* Submit Button */}
<div style={styles.submitButton}>
<button onClick={handleEnrollClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Enroll Now</p>
</button>
</div>
{/* Result Section */}
{showResult && (
<div style={styles.containerResultStyle}>
<h1 style={{ color: '#0542cc', fontSize: isMobile ? '1.5rem' : '2rem' }}>Results</h1>
<div style={styles.resultContainer}>
{/* Table Styling: responsive */}
<table style={isMobile ? styles.resultsTableMobile : styles.resultsTable}>
<tbody>
<tr>
<td style={{ ...styles.resultsCell, width: '40%' }}>Similarity</td>
<td style={{ ...styles.resultsValueCell, ...styles.resultsTrueValue }}>True</td>
</tr>
{/* More rows can go here */}
</tbody>
</table>
{/* Image and Details Container */}
<div style={styles.imageContainer}>
<img
src={imageUrl || "path-to-your-image"}
alt="Contoh Foto"
style={isMobile ? styles.imageStyleMobile : styles.imageStyle}
/>
<p style={isMobile ? { ...styles.imageDetails, fontSize: '14px' } : styles.imageDetails}>
{selectedImageName}
</p>
</div>
</div>
</div>
)}
</div>
);
}
export default Enroll;

View File

@ -0,0 +1,11 @@
import React from 'react'
const Liveness = () => {
return (
<div>
<h1>Face Recognition - Liveness</h1>
</div>
)
}
export default Liveness

View File

@ -0,0 +1,662 @@
import React, { useState, useRef, useEffect } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronLeft, faChevronDown, faTimes, faImage } from '@fortawesome/free-solid-svg-icons';
import { FileUploader } from 'react-drag-drop-files';
import Select from 'react-select'
const fileTypes = ["JPG", "JPEG", "PNG"]; // Allowed file types
const Search = () => {
const BASE_URL = process.env.REACT_APP_BASE_URL
const API_KEY = process.env.REACT_APP_API_KEY
const [isSelectOpen, setIsSelectOpen] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [selectedImageName, setSelectedImageName] = useState('');
const fileInputRef = useRef(null);
const [showResult, setShowResult] = useState(false);
const [applicationId, setApplicationId] = useState('');
const [selectedQuota, setSelectedQuota] = useState(0);
const [limitId, setLimitId] = useState('');
const [imageUrls, setImageUrls] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [results, setResults] = useState([]);
const [file, setFile] = useState(null);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const [applicationIds, setApplicationIds] = useState([]);
const [inputValueApplication, setInputValueApplication] = useState(''); // Controlled input value for Application ID
const [limitIds] = useState(
Array.from({ length: 10 }, (_, index) => ({
id: index + 1,
name: index + 1,
}))
);
const [applicationIdError, setApplicationIdError] = useState('');
const [limitIdError, setLimitIdError] = useState('');
const [imageError, setImageError] = useState('');
const [uploadedFile, setUploadedFile] = useState(null);
const formatFileSize = (sizeInBytes) => {
if (sizeInBytes < 1024) {
return `${sizeInBytes} bytes`; // Jika ukuran lebih kecil dari 1 KB
} else if (sizeInBytes < 1048576) {
return `${(sizeInBytes / 1024).toFixed(2)} KB`; // Jika ukuran lebih kecil dari 1 MB
} else {
return `${(sizeInBytes / 1048576).toFixed(2)} MB`; // Jika ukuran lebih besar dari 1 MB
}
};
useEffect(() => {
const fetchApplicationIds = async () => {
try {
setIsLoading(true)
const url = `${BASE_URL}/application/list`;
console.log('Fetching URL:', url); // Log the URL
const response = await fetch(url, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': `${API_KEY}`,
},
});
const data = await response.json();
if (data.status_code === 200) {
const ids = data.details.data.map(app => app.id);
console.log('Application Id: ' + ids); // Log the IDs
setApplicationIds(data.details.data); // Update state with the fetched data
} else {
console.error('Failed to fetch data:', data.details.message);
}
} catch (error) {
console.error('Error fetching application IDs:', error);
} finally {
setIsLoading(false)
}
};
fetchApplicationIds();
const handleResize = () => setIsMobile(window.innerWidth <= 768);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const applicationOptions = applicationIds.map(app => ({
value: app.id,
label: app.name
}));
const handleApplicationChange = (selectedOption) => {
if (selectedOption) {
const selectedId = selectedOption.value;
const selectedApp = applicationIds.find(app => app.id === parseInt(selectedId));
if (selectedApp) {
setSelectedQuota(selectedApp.quota); // Set the selected quota
setApplicationId(selectedId); // Set the selected application ID
}
}
};
const handleInputChangeApplication = (newInputValue) => {
// Limit input to 15 characters for Application ID
if (newInputValue.length <= 15) {
setInputValueApplication(newInputValue);
}
};
const handleFocus = () => {
setIsSelectOpen(true);
};
const handleBlur = () => {
setIsSelectOpen(false);
};
const handleImageUpload = (file) => {
// Ensure file exists before accessing its properties
if (!file) {
console.error('File is undefined');
setImageError('Please upload a valid image file.');
return;
}
const fileExtension = file.name.split('.').pop().toUpperCase();
if (fileTypes.includes(fileExtension)) {
setSelectedImageName(file.name);
setFile(file);
setImageError(''); // Clear any previous errors
} else {
// Show an alert if the file type is not supported
alert('Image format is not supported');
setImageError('Image format is not supported'); // Optionally set error message to display on the UI
setFile(null); // Optionally clear the selected file
}
};
const handleImageCancel = () => {
setSelectedImageName('');
setFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleCheckClick = async () => {
// Clear existing errors
setApplicationIdError('');
setLimitIdError('');
setImageError('');
setErrorMessage('');
// Initialize validation flags
let hasError = false;
// Validate Application ID
if (!applicationId) {
setApplicationIdError('Please select an Application ID before searching.');
hasError = true; // Set error flag
}
// Validate Limit ID
if (!limitId) {
setLimitIdError('Please select a Limit before searching.');
hasError = true; // Set error flag
}
// Validate Image Upload
if (!selectedImageName) {
setImageError('Please upload an image file.');
hasError = true; // Set error flag
}
// Check if the file is uploaded
if (!uploadedFile) {
setErrorMessage('Please upload an image file.');
hasError = true; // Set error flag
}
// If any errors were found, do not proceed
if (hasError) {
return;
}
const parsedLimitId = parseInt(limitId, 10);
const formData = new FormData();
formData.append('application_id', applicationId);
formData.append('threshold', 1);
formData.append('limit', parsedLimitId);
formData.append('file', uploadedFile, uploadedFile.name); // Use the uploaded file
setIsLoading(true);
try {
const response = await fetch(`${BASE_URL}/face_recognition/search`, {
method: 'POST',
headers: {
'accept': 'application/json',
'x-api-key': `${API_KEY}`,
},
body: formData,
});
const data = await response.json();
console.log('Response Data:', data); // Log the response
if (response.ok) {
const resultsArray = Array.isArray(data.details.data) ? data.details.data : [];
const processedResults = resultsArray.map(item => ({
identity: item.identity,
similarity: item.similarity,
imageUrl: item.image_url,
distance: item.distance,
}));
// Fetch images using their URLs
await Promise.all(processedResults.map(async result => {
const imageFileName = result.imageUrl.split('/').pop(); // Extract file name if needed
await fetchImage(imageFileName); // Fetch image
console.log('multiple image data: ', result.imageUrl); // Log the URL
}));
setResults(processedResults);
setShowResult(true);
} else {
console.error('Error response:', JSON.stringify(data, null, 2));
const errorMessage = data.message || data.detail || data.details?.message || 'An unknown error occurred.';
setErrorMessage(errorMessage);
}
} catch (error) {
console.error('Error:', error);
setErrorMessage('An error occurred while making the request.');
} finally {
setIsLoading(false);
}
};
const fetchImage = async (imageFileName) => {
setIsLoading(true);
try {
const response = await fetch(`${BASE_URL}/preview/image/${imageFileName}`, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': API_KEY,
},
});
if (!response.ok) {
const errorDetails = await response.json();
console.error('Image fetch error details:', errorDetails);
setErrorMessage('Failed to fetch image, please try again.');
return;
}
const imageBlob = await response.blob();
const imageData = URL.createObjectURL(imageBlob);
console.log('Fetched image URL:', imageData);
setImageUrls(prevUrls => [...prevUrls, imageData]); // Store the blob URL
} catch (error) {
console.error('Error fetching image:', error);
setErrorMessage(error.message);
} finally {
setIsLoading(false);
}
};
const CustomLabel = ({ overRide, children, ...props }) => {
// We intentionally don't pass `overRide` to the label
return (
<label {...props}>
{children}
</label>
);
};
const styles = {
formGroup: {
marginTop: '-45px',
},
selectWrapper: {
position: 'relative',
marginTop: '0',
},
select: {
width: '100%',
paddingRight: '30px',
},
chevronIcon: {
position: 'absolute',
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'none',
},
remainingQuota: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginTop: '4px',
},
quotaText: {
fontSize: '40px',
color: '#0542cc',
fontWeight: '600',
},
timesText: {
marginLeft: '8px',
verticalAlign: 'super',
fontSize: '20px',
},
uploadArea: {
backgroundColor: '#e6f2ff',
height: '50svh',
cursor: 'pointer',
marginTop: '1rem',
padding: '1rem',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
border: '1px solid #ced4da',
borderRadius: '0.25rem',
},
uploadIcon: {
fontSize: '40px',
color: '#0542cc',
marginBottom: '7px',
},
uploadText: {
color: '#1f2d3d',
fontWeight: '400',
fontSize: '16px',
lineHeight: '13px',
},
wrapper: {
border: '1px solid #ddd',
borderRadius: '6px',
padding: '18px 10px 0 8px', // Padding lebih seragam
height: '13svh', // Tinggi lebih kecil untuk menyesuaikan tampilan
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#f9f9f9',
overflow: 'hidden',
},
fileWrapper: {
display: 'flex',
alignItems: 'center',
flex: '1',
},
textContainer: {
flex: '1',
fontSize: '16px', // Ukuran font lebih kecil
marginLeft: '6px',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
marginTop: '1rem'
},
fileSize: {
fontSize: '12px',
color: '#555',
marginBottom: '2rem',
},
closeButtonContainer: {
display: 'flex',
alignItems: 'center',
marginLeft: 'auto',
},
closeButton: {
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: '0',
},
imageIcon: {
color: '#0542cc',
fontSize: '18px', // Ukuran ikon sedikit lebih kecil
marginRight: '6px',
},
closeIcon: {
color: 'red',
fontSize: '18px',
},
submitButton: {
marginLeft: 'auto',
marginTop: '4rem',
textAlign: 'start',
position: 'relative',
zIndex: 1,
},
uploadError: {
color: 'red',
fontSize: '12px',
marginTop: '5px',
},
loadingOverlay: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.2)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
},
spinner: {
border: '4px solid rgba(0, 0, 0, 0.1)',
borderLeftColor: '#0542cc',
borderRadius: '50%',
width: '90px',
height: '90px',
animation: 'spin 1s ease-in-out infinite',
},
loadingText: {
marginTop: '10px',
fontSize: '1.2rem',
color: '#fff',
textAlign: 'center',
},
containerResultStyle: {
padding: '1rem',
backgroundColor: '#f7f7f7',
borderRadius: '8px',
margin: '1rem',
width: isMobile ? '100%' : '50%',
},
resultContainer: {
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
flexWrap: 'wrap',
gap: '1rem',
justifyContent: 'center',
},
resultItem: {
display: 'flex',
flexDirection: isMobile ? 'row' : 'column',
alignItems: 'center',
textAlign: 'center',
padding: '0.5rem',
backgroundColor: '#fff',
border: '1px solid #ddd',
borderRadius: '8px',
boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.1)',
width: isMobile ? '100%' : '150px',
},
resultTextContainer: {
marginBottom: isMobile ? '0' : '0.5rem',
},
resultText: {
fontSize: '0.9rem',
color: '#333',
margin: '0.2rem 0',
},
resultImage: {
width: '80px',
height: '80px',
borderRadius: '50%',
marginTop: isMobile ? '0' : '0.5rem',
},
};
return (
<div>
{/* Inject keyframes for the spinner */}
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
{isLoading && (
<div style={styles.loadingOverlay}>
<div style={styles.spinner}></div>
<p style={styles.loadingText}>Loading...</p>
</div>
)}
{/* Application ID Selection */}
<div className="form-group row align-items-center">
<div className="col-md-6">
<div className="select-wrapper">
<Select
id="applicationId"
value={applicationOptions.find(option => option.value === applicationId)}
onChange={handleApplicationChange}
options={applicationOptions}
placeholder="Select Application ID"
isSearchable
menuPortalTarget={document.body}
menuPlacement="auto"
inputValue={inputValueApplication}
onInputChange={handleInputChangeApplication}
/>
</div>
{applicationIdError && (
<small style={styles.uploadError}>{applicationIdError}</small>
)}
</div>
<div className="col-md-6">
<p className="text-secondary" style={{ fontSize: '16px', fontWeight: '400', margin: '0', marginTop: '8px' }}>
Remaining Quota
</p>
<div style={styles.remainingQuota}>
<span style={styles.quotaText}>{selectedQuota}</span> {/* Display selected quota */}
<span style={styles.timesText}>(times)</span>
</div>
</div>
</div>
{/* limit ID Input and Threshold Selection */}
<div className="form-group row align-items-center">
<div className="col-md-6">
<div style={styles.selectWrapper}>
<select
id="limitId"
className="form-control"
style={styles.select}
value={limitId}
onChange={(e) => setLimitId(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
>
<option value="">Select Limit</option>
{limitIds.map((app) => (
<option key={app.id} value={app.id}>
{app.name}
</option>
))}
</select>
<FontAwesomeIcon
icon={isSelectOpen ? faChevronDown : faChevronLeft}
style={styles.chevronIcon}
/>
{limitIdError && (
<small style={styles.uploadError}>{limitIdError}</small>
)}
</div>
</div>
</div>
{/* Upload Section */}
{/* Drag and Drop File Uploader */}
<div className='col-md-6'>
<div className="row form-group mt-4">
<CustomLabel htmlFor="uploadPhoto" style={styles.customLabel}>
Upload Face Photo
</CustomLabel>
<FileUploader
handleChange={handleImageUpload}
name="file"
types={fileTypes}
multiple={false}
onDrop={(files) => handleImageUpload(files[0])}
children={
<div style={styles.uploadArea}>
<i className="fas fa-cloud-upload-alt" style={styles.uploadIcon}></i>
<p style={styles.uploadText}>Drag and Drop Here</p>
<p>Or</p>
<a href="#" onClick={() => fileInputRef.current.click()}>Browse</a>
<p className="text-muted">Recommended size: 300x300 (Max File Size: 2MB)</p>
<p className="text-muted">Supported file types: JPG, JPEG</p>
</div>
}
/>
<input
type="file"
id="fileUpload"
ref={fileInputRef}
style={{ display: 'none' }}
accept="image/jpeg, image/jpg"
onChange={e => handleImageUpload(e.target.files[0])}
/>
{(imageError || errorMessage) && (
<small style={{ color: 'red' }}>
{imageError || errorMessage}
</small>
)}
</div>
</div>
{selectedImageName && (
<div className="col-md-6 mt-4" style={styles.wrapper}>
<div style={styles.fileWrapper}>
<FontAwesomeIcon icon={faImage} style={styles.imageIcon} />
<div style={styles.textContainer}>
<h5>Uploaded File:</h5>
<p>{selectedImageName}</p>
{file && (
<p style={styles.fileSize}>
Size: {formatFileSize(file.size)}
</p>
)}
</div>
<div style={styles.closeButtonContainer}>
<button
style={styles.closeButton}
onClick={handleImageCancel}
aria-label="Remove uploaded image"
>
<FontAwesomeIcon
icon={faTimes}
style={styles.closeIcon}
/>
</button>
</div>
</div>
</div>
)}
{/* Submit Button */}
<div style={styles.submitButton}>
<button onClick={handleCheckClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Check Now</p>
</button>
</div>
{/* Results Section */}
{
showResult && results.length > 0 && (
<div style={styles.containerResultStyle}>
<h1 style={{ color: '#0542cc', textAlign: 'center' }}>Results</h1>
<div style={styles.resultContainer}>
{results.slice(0, limitId).map((result, index) => (
<div key={index} style={styles.resultItem}>
<div style={styles.resultTextContainer}>
<p style={styles.resultText}>Image Name: image_{index + 1}</p>
<p style={styles.resultText}>Similarity: {result.similarity}%</p>
<p style={styles.resultText}>Distance: {result.distance}</p>
</div>
<img src={imageUrls[index]} alt={`Result ${index + 1}`} style={styles.resultImage} />
</div>
))}
</div>
</div>
)}
</div>
);
}
export default Search

View File

@ -0,0 +1,724 @@
import React, { useState, useRef, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronLeft, faChevronDown, faTimes, faImage } from '@fortawesome/free-solid-svg-icons';
import { FileUploader } from 'react-drag-drop-files';
import Select from 'react-select'
const Verify = () => {
const BASE_URL = process.env.REACT_APP_BASE_URL;
const API_KEY = process.env.REACT_APP_API_KEY;
const fileTypes = ["JPG", "JPEG", "PNG"];
const [file, setFile] = useState(null);
const [isSelectOpen, setIsSelectOpen] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [uploadError, setUploadError] = useState('');
const [applicationError, setApplicationError] = useState('');
const [subjectError, setSubjectError] = useState('');
const [thresholdError, setThresholdError] = useState('');
const [selectedImageName, setSelectedImageName] = useState('');
const fileInputRef = useRef(null);
const [showResult, setShowResult] = useState(false);
const [applicationId, setApplicationId] = useState('');
const [thresholdId, setThresholdId] = useState('');
const [selectedQuota, setSelectedQuota] = useState(0);
const [subjectId, setSubjectId] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [verified, setVerified] = useState(null);
const [imageUrl, setImageUrl] = useState('');
const [applicationIds, setApplicationIds] = useState([]);
const [subjectIds, setSubjectIds] = useState([]);
const [subjectAvailabilityMessage, setSubjectAvailabilityMessage] = useState(''); // Message for subject availability
const [inputValueApplication, setInputValueApplication] = useState(''); // Controlled input value for Application ID
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const thresholdIds = [
{ id: 1, name: 'cosine', displayName: 'Basic' },
{ id: 2, name: 'euclidean', displayName: 'Medium' },
{ id: 3, name: 'euclidean_l2', displayName: 'High' },
];
const options = subjectIds.map(id => ({ value: id, label: id }));
const [inputValue, setInputValue] = useState('');
const applicationOptions = applicationIds.map(app => ({
value: app.id,
label: app.name
}));
useEffect(() => {
const fetchApplicationIds = async () => {
try {
setIsLoading(true);
const url = `${BASE_URL}/application/list`;
const response = await fetch(url, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': `${API_KEY}`,
},
});
const data = await response.json();
if (data.status_code === 200) {
setApplicationIds(data.details.data);
} else {
console.error('Failed to fetch data:', data.details.message);
}
} catch (error) {
console.error('Error fetching application IDs:', error);
} finally {
setIsLoading(false);
}
};
fetchApplicationIds();
const handleResize = () => setIsMobile(window.innerWidth <= 768);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const styles = {
formGroup: {
marginTop: '-45px',
},
selectWrapper: {
position: 'relative',
marginTop: '0',
},
select: {
width: '100%',
paddingRight: '30px',
},
chevronIcon: {
position: 'absolute',
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'none',
},
remainingQuota: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginTop: '4px',
},
quotaText: {
fontSize: '40px',
color: '#0542cc',
fontWeight: '600',
},
timesText: {
marginLeft: '8px',
verticalAlign: 'super',
fontSize: '20px',
},
uploadArea: {
backgroundColor: '#e6f2ff',
height: '250px',
cursor: 'pointer',
marginTop: '1rem',
padding: '22px 10px 0 20px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
border: '1px solid #ced4da',
borderRadius: '0.25rem',
},
uploadIcon: {
fontSize: '40px',
color: '#0542cc',
marginBottom: '7px',
},
uploadText: {
color: '#1f2d3d',
fontWeight: '400',
fontSize: '16px',
lineHeight: '13px',
},
wrapper: {
border: '1px solid #ddd',
borderRadius: '6px',
padding: '18px 10px 0 8px', // Padding lebih seragam
height: '13svh', // Tinggi lebih kecil untuk menyesuaikan tampilan
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#f9f9f9',
overflow: 'hidden',
},
fileWrapper: {
display: 'flex',
alignItems: 'center',
flex: '1',
},
textContainer: {
flex: '1',
fontSize: '16px', // Ukuran font lebih kecil
marginLeft: '6px',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
marginTop: '1rem'
},
fileSize: {
fontSize: '12px',
color: '#555',
marginBottom: '2rem',
},
closeButtonContainer: {
display: 'flex',
alignItems: 'center',
marginLeft: 'auto',
},
closeButton: {
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: '0',
},
imageIcon: {
color: '#0542cc',
fontSize: '18px', // Ukuran ikon sedikit lebih kecil
marginRight: '6px',
},
closeIcon: {
color: 'red',
fontSize: '18px',
},
submitButton: {
marginLeft: 'auto',
marginTop: '4rem',
textAlign: 'start',
position: 'relative',
zIndex: 1,
},
uploadError: {
color: 'red',
fontSize: '12px',
marginTop: '5px',
},
containerResultStyle: {
padding: '20px',
border: '1px solid #0053b3',
borderRadius: '5px',
width: '100%',
maxWidth: '600px',
margin: '20px auto',
},
resultContainer: {
display: 'flex',
alignItems: 'center',
width: '100%',
},
tableStyle: {
width: '60%',
borderCollapse: 'collapse',
},
resultsTableMobile: {
width: '100%',
borderCollapse: 'collapse',
fontSize: '14px',
},
resultsCell: {
border: '0.1px solid gray',
padding: '8px',
},
resultsValueCell: {
border: '0.1px solid gray',
padding: '8px',
width: '60%',
},
imageContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginTop: isMobile ? '20px' : '0',
},
imageStyle: {
width: '300px',
height: '300px',
borderRadius: '5px',
objectFit: 'cover',
},
imageStyleMobile: {
width: '100%',
height: 'auto',
},
loadingOverlay: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.2)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
},
spinner: {
border: '4px solid rgba(0, 0, 0, 0.1)',
borderLeftColor: '#0542cc',
borderRadius: '50%',
width: '90px',
height: '90px',
animation: 'spin 1s ease-in-out infinite',
},
loadingText: {
marginTop: '10px',
fontSize: '1.2rem',
color: '#fff',
textAlign: 'center',
},
};
const handleApplicationChange = async (selectedOption) => {
if (!selectedOption) {
console.error("Selected option is undefined");
return;
}
const selectedId = selectedOption.value;
const selectedApp = applicationIds.find(app => app.id === parseInt(selectedId));
if (selectedApp) {
setSelectedQuota(selectedApp.quota);
}
setApplicationId(selectedId);
if (selectedId) {
await fetchSubjectIds(selectedId);
} else {
setSubjectIds([]);
setSubjectAvailabilityMessage('');
}
};
const fetchSubjectIds = async (appId) => {
setIsLoading(true);
try {
const response = await fetch(`${BASE_URL}/trx_face/list/subject?application_id=${appId}&search=${subjectId}&limit=99`, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': API_KEY,
},
});
const data = await response.json();
console.log("Fetched Subject IDs:", data); // Log data fetched from API
if (data.status_code === 200) {
setSubjectIds(data.details.data);
} else {
console.error('Failed to fetch subject IDs:', data.details.message);
}
} catch (error) {
console.error('Error fetching subject IDs:', error);
} finally {
setIsLoading(false);
}
};
const handleFocus = () => {
setIsSelectOpen(true);
};
const handleBlur = () => {
setIsSelectOpen(false);
};
const handleImageUpload = (file) => {
// Ensure file exists before accessing its properties
if (!file) {
console.error('File is undefined');
setUploadError('Please upload a valid image file.');
return;
}
const fileExtension = file.name.split('.').pop().toUpperCase();
if (fileTypes.includes(fileExtension)) {
setSelectedImageName(file.name);
setFile(file);
setUploadError(''); // Clear any previous errors
} else {
// Show an alert if the file type is not supported
alert('Image format is not supported');
setUploadError('Image format is not supported'); // Optionally set error message to display on the UI
setFile(null); // Optionally clear the selected file
}
};
const handleImageCancel = () => {
setSelectedImageName('');
setFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleCheckClick = async () => {
// Reset previous error messages
setErrorMessage('');
setApplicationError('');
setSubjectError('');
setThresholdError('');
setUploadError('');
let hasError = false; // Track if any errors occur
if (!applicationId) {
setApplicationError('Please select an Application ID before enrolling.');
hasError = true; // Mark that an error occurred
}
if (!subjectId) {
setSubjectError('Please enter a Subject ID before enrolling.');
hasError = true; // Mark that an error occurred
}
const selectedThreshold = thresholdIds.find(threshold => threshold.name === thresholdId)?.name;
if (!selectedThreshold) {
setThresholdError('Invalid threshold selected.');
hasError = true; // Mark that an error occurred
}
if (!selectedImageName) {
setUploadError('Please upload a face photo before verifying.');
hasError = true; // Mark that an error occurred
}
// If there are any errors, stop the function
if (hasError) {
return;
}
// Log the input values
console.log('Selected Image Name:', selectedImageName);
console.log('Application ID:', applicationId);
console.log('Subject ID:', subjectId);
console.log('Selected Threshold:', selectedThreshold);
const formData = new FormData();
formData.append('application_id', applicationId);
formData.append('threshold', selectedThreshold);
formData.append('subject_id', subjectId);
// const file = fileInputRef.current.files[0];
if (file) {
formData.append('file', file, file.name);
} else {
setUploadError('Please upload an image file.');
return;
}
setIsLoading(true);
try {
const response = await fetch(`${BASE_URL}/face_recognition/verifiy`, {
method: 'POST',
headers: {
'accept': 'application/json',
'x-api-key': `${API_KEY}`,
},
body: formData,
});
const data = await response.json();
if (response.ok) {
if (data.details && data.details.data && data.details.data.result && data.details.data.result.image_url) {
const imageFileName = data.details.data.result.image_url.split('/').pop();
await fetchImage(imageFileName);
}
setShowResult(true);
setVerified(data.details.data.result.verified);
} else {
const errorMessage = data.message || data.detail || data.details?.message || 'An unknown error occurred.';
setErrorMessage(errorMessage);
}
} catch (error) {
console.error('Error:', error);
setErrorMessage('An error occurred while making the request.');
} finally {
setIsLoading(false);
}
};
const fetchImage = async (imageFileName) => {
setIsLoading(true);
try {
const response = await fetch(`${BASE_URL}/preview/image/${imageFileName}`, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': API_KEY,
},
});
if (!response.ok) {
setErrorMessage('Failed to fetch image, please try again.');
return;
}
const imageBlob = await response.blob();
const imageData = URL.createObjectURL(imageBlob);
setImageUrl(imageData);
} catch (error) {
console.error('Error fetching image:', error);
setErrorMessage(error.message);
} finally {
setIsLoading(false);
}
};
const CustomLabel = ({ overRide, children, ...props }) => {
// We intentionally don't pass `overRide` to the label
return (
<label {...props}>
{children}
</label>
);
};
const handleInputChangeApplication = (newInputValue) => {
// Limit input to 15 characters for Application ID
if (newInputValue.length <= 15) {
setInputValueApplication(newInputValue);
}
};
// Fungsi untuk mengonversi ukuran file dari byte ke KB/MB
const formatFileSize = (sizeInBytes) => {
if (sizeInBytes < 1024) {
return `${sizeInBytes} bytes`; // Jika ukuran lebih kecil dari 1 KB
} else if (sizeInBytes < 1048576) {
return `${(sizeInBytes / 1024).toFixed(2)} KB`; // Jika ukuran lebih kecil dari 1 MB
} else {
return `${(sizeInBytes / 1048576).toFixed(2)} MB`; // Jika ukuran lebih besar dari 1 MB
}
};
return (
<div>
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
{isLoading && (
<div style={styles.loadingOverlay}>
<div style={styles.spinner}></div>
<p style={styles.loadingText}>Loading...</p>
</div>
)}
{/* Application ID Selection */}
<div className="form-group row align-items-center">
<div className="col-md-6">
<div className="select-wrapper">
<Select
id="applicationId"
value={applicationOptions.find(option => option.value === applicationId)}
onChange={handleApplicationChange}
options={applicationOptions}
placeholder="Select Application ID"
isSearchable
menuPortalTarget={document.body} // Use this for scroll behavior
menuPlacement="auto"
inputValue={inputValueApplication}
onInputChange={handleInputChangeApplication} // Limit input length for Application ID
/>
</div>
{applicationError && <small style={styles.uploadError}>{applicationError}</small>}
</div>
<div className="col-md-6">
<p className="text-secondary" style={{ fontSize: '16px', fontWeight: '400', margin: '0', marginTop: '8px' }}>
Remaining Quota
</p>
<div style={styles.remainingQuota}>
<span style={styles.quotaText}>{selectedQuota}</span> {/* Display selected quota */}
<span style={styles.timesText}>(times)</span>
</div>
</div>
</div>
{/* Subject ID Input */}
<div className="form-group row align-items-center">
<div className="col-md-6">
<Select
id="subjectId"
options={options}
value={options.find(option => option.value === subjectId)}
onChange={selectedOption => setSubjectId(selectedOption ? selectedOption.value : '')}
onInputChange={(value) => {
// Check if input value length is within the 15-character limit
if (value.length <= 15) {
setInputValue(value); // Set the input value if within limit
}
}}
onFocus={() => fetchSubjectIds(applicationId)} // Fetch subject IDs on focus
placeholder="Enter Subject ID"
isClearable
noOptionsMessage={() => (
<div style={{ color: 'red' }}>Subject ID not registered.</div>
)}
inputValue={inputValue} // Bind the inputValue state to control the input
/>
{subjectError && <small style={{ color: 'red' }}>{subjectError}</small>}
{subjectAvailabilityMessage && (
<small style={{ color: subjectAvailabilityMessage.includes('available') ? 'green' : 'red' }}>
{subjectAvailabilityMessage}
</small>
)}
</div>
<div className="col-md-6">
<div style={styles.selectWrapper}>
<select
id="thresholdId"
className="form-control"
style={styles.select}
value={thresholdId}
onChange={(e) => {
setThresholdId(e.target.value);
setThresholdError(''); // Clear error if valid
}}
onFocus={handleFocus}
onBlur={handleBlur}
>
<option value="">Select Threshold</option>
{thresholdIds.map((app) => (
<option key={app.id} value={app.name}>
{app.displayName}
</option>
))}
</select>
<FontAwesomeIcon
icon={isSelectOpen ? faChevronDown : faChevronLeft}
style={styles.chevronIcon}
/>
{thresholdError && <small style={styles.uploadError}>{thresholdError}</small>}
</div>
</div>
</div>
{/* Upload Section */}
<div className="col-md-6">
<div className="row form-group mt-4">
<CustomLabel htmlFor="uploadPhoto" style={styles.customLabel}>
Upload Face Photo
</CustomLabel>
<FileUploader
handleChange={handleImageUpload}
name="file"
types={fileTypes}
multiple={false}
onDrop={(files) => handleImageUpload(files[0])}
children={
<div style={styles.uploadArea}>
<i className="fas fa-cloud-upload-alt" style={styles.uploadIcon}></i>
<p style={styles.uploadText}>Drag and Drop Here</p>
<p>Or</p>
<a href="#" onClick={() => fileInputRef.current.click()}>Browse</a>
<p className="text-muted">Recommended size: 300x300 (Max File Size: 2MB)</p>
<p className="text-muted">Supported file types: JPG, JPEG</p>
</div>
}
/>
{uploadError && <small style={styles.uploadError}>{uploadError}</small>}
</div>
</div>
{/* Display uploaded image name */}
{selectedImageName && (
<div className="col-md-6 mt-4" style={styles.wrapper}>
<div style={styles.fileWrapper}>
<FontAwesomeIcon icon={faImage} style={styles.imageIcon} />
<div style={styles.textContainer}>
<h5>Uploaded File:</h5>
<p>{selectedImageName}</p>
{file && (
<p style={styles.fileSize}>
Size: {formatFileSize(file.size)}
</p>
)}
</div>
<div style={styles.closeButtonContainer}>
<button
style={styles.closeButton}
onClick={handleImageCancel}
aria-label="Remove uploaded image"
>
<FontAwesomeIcon
icon={faTimes}
style={styles.closeIcon}
/>
</button>
</div>
</div>
</div>
)}
{/* Submit Button */}
<div style={styles.submitButton}>
<button onClick={handleCheckClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Check Now</p>
</button>
</div>
{/* Results Section */}
{showResult && (
<div style={styles.containerResultStyle}>
<h1 style={{ color: '#0542cc', fontSize: isMobile ? '1.5rem' : '2rem' }}>Results</h1>
<div style={{ ...styles.resultContainer, flexDirection: isMobile ? 'column' : 'row' }}>
<table style={isMobile ? styles.resultsTableMobile : styles.tableStyle}>
<tbody>
<tr>
<td style={{ ...styles.resultsCell, width: '40%' }}>Similarity</td>
<td
style={{
...styles.resultsValueCell,
color: verified ? 'green' : verified === false ? 'red' : 'black',
}}
>
<strong>{verified !== null ? (verified ? 'True' : 'False') : 'N/A'}</strong>
</td>
</tr>
</tbody>
</table>
<div style={{ ...styles.imageContainer, width: isMobile ? '100%' : '30%' }}>
<img
src={imageUrl || "path-to-your-image"}
alt="Example Image"
style={isMobile ? styles.imageStyleMobile : styles.imageStyle}
/>
<p style={{ marginTop: '10px', fontSize: isMobile ? '12px' : '16px' }}>
File Name: {selectedImageName}
</p>
</div>
</div>
</div>
)}
</div>
);
}
export default Verify;

View File

@ -0,0 +1,13 @@
import Enroll from "./Enroll";
import VerifySection from "./Verify";
import Compare from "./Compare"
import Liveness from "./Liveness"
import Search from "./Search"
export {
Enroll,
VerifySection,
Compare,
Liveness,
Search
}

View File

@ -0,0 +1,11 @@
import React from 'react'
const Summary = () => {
return (
<div>
<h1>Summary</h1>
</div>
)
}
export default Summary

View File

@ -0,0 +1,11 @@
import React from 'react'
const Transaction = () => {
return (
<div>
<h1>Transaction</h1>
</div>
)
}
export default Transaction

View File

@ -0,0 +1,123 @@
import React, { useEffect, useState } from 'react';
import { Link, Routes, Route, useNavigate } from 'react-router-dom';
import {
Enroll,
Compare,
Liveness,
Search,
VerifySection
} from './Section';
const Verify = () => {
const verifyTabs = [
{ name: 'Enroll', link: 'face-enroll' },
{ name: 'Verify', link: 'face-verifysection' },
{ name: 'Liveness', link: 'face-liveness' },
{ name: 'Compare', link: 'face-compare' },
{ name: 'Search', link: 'face-search' },
];
const [isMobile, setIsMobile] = useState(false);
const navigate = useNavigate();
// Redirect otomatis ke rute default saat akses ke /face-verify
useEffect(() => {
if (window.location.pathname === '/face-verify') {
navigate('face-enroll', { replace: true });
}
}, [navigate]);
// Update state isMobile berdasarkan ukuran layar
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div className="container" style={styles.container}>
{/* Static Content */}
<div className="row-card border-left border-primary shadow mb-4" style={styles.welcomeCard}>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
<div className="d-flex flex-row mt-3">
<Link to="/createApps" style={{ textDecoration: 'none' }}>
<button className="btn d-flex justify-content-center align-items-center me-2" style={styles.createButton}>
<i className="fas fa-plus text-white me-2"></i>
<p className="text-white mb-0">Create New App ID</p>
</button>
</Link>
</div>
</div>
</div>
{/* Tab Navigation */}
<div style={styles.section}>
<div className={`d-flex ${isMobile ? 'flex-column' : 'flex-row'} justify-content-between align-items-center mb-3`}>
<div className="d-flex flex-wrap">
{verifyTabs.map((tab) => (
<Link
key={tab.link}
to={tab.link}
className={`btn ${window.location.pathname.includes(tab.link) ? 'btn-primary' : 'btn-light'} me-2 mb-2`}
style={styles.tabLink}
>
{tab.name}
</Link>
))}
</div>
</div>
{/* Dynamic Tab Content */}
<div className="tab-content">
<Routes>
<Route path="face-enroll" element={<Enroll />} />
<Route path="face-verifysection" element={<VerifySection />} />
<Route path="face-liveness" element={<Liveness />} />
<Route path="face-compare" element={<Compare />} />
<Route path="face-search" element={<Search />} />
</Routes>
</div>
</div>
</div>
);
};
export default Verify;
const styles = {
container: {
marginTop: '3%',
padding: '0 15px',
},
welcomeCard: {
backgroundColor: '#E2FBEA',
borderLeft: '4px solid #0542CC',
borderRadius: '5px',
marginBottom: '20px',
},
createButton: {
backgroundColor: '#0542CC',
},
section: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542CC',
borderRadius: '10px',
width: '100%',
},
tabLink: {
padding: '10px 20px',
},
};

View File

@ -0,0 +1,9 @@
import FaceVerify from "./Verify";
import FaceSummary from "./Summary";
import FaceTransaction from "./Transaction";
export {
FaceVerify,
FaceSummary,
FaceTransaction
}

View File

@ -0,0 +1,511 @@
import React, { useState, useRef, useEffect } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronLeft, faChevronDown, faTimes, faImage } from '@fortawesome/free-solid-svg-icons';
import { DummyKtp } from '../../../assets/images'
import { Link } from 'react-router-dom';
const Verify = () => {
const BASE_URL = process.env.REACT_APP_BASE_URL
const API_KEY = process.env.REACT_APP_API_KEY
const [isSelectOpen, setIsSelectOpen] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [selectedImageName, setSelectedImageName] = useState('');
const fileInputRef = useRef(null);
const [showResult, setShowResult] = useState(false);
const [applicationId, setApplicationId] = useState('');
const [imageUrl, setImageUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [applicationIds, setApplicationIds] = useState([]);
// Example usage:
const data = {
nik: "21710121748901",
district: "BATAM KOTA",
name: "HANDOKO",
city: "KOTA BATAM",
dob: "BANJARMASIN, 12-12-1974",
state: "PROVINSI KEPULAUAN RIAU",
gender: "LAKI-LAKI",
religion: "KRISTEN",
bloodType: "A",
maritalStatus: "KAWIN",
address: "GOLDEN LAND BLOK FN NO.39",
occupation: "WIRASWASTA",
rtRw: "002/013",
nationality: "WNI",
village: "TAMAN BALOI",
imageUrl: DummyKtp, // Replace this with the actual image path
dark: false,
blur: false,
grayscale: false,
flashlight: false,
};
useEffect(() => {
const fetchApplicationIds = async () => {
try {
setIsLoading(true)
const url = `${BASE_URL}/application/list`;
console.log('Fetching URL:', url);
const response = await fetch(url, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': `${API_KEY}`,
},
});
const data = await response.json();
if (data.status_code === 200) {
const ids = data.details.data.map(app => app.id);
console.log('Application Id: ' + ids);
setApplicationIds(data.details.data);
} else {
console.error('Failed to fetch data:', data.details.message);
}
} catch (error) {
console.error('Error fetching application IDs:', error);
} finally {
setIsLoading(false)
}
};
fetchApplicationIds();
}, []);
const handleFocus = () => {
setIsSelectOpen(true);
};
const handleBlur = () => {
setIsSelectOpen(false);
};
const handleImageUpload = (event) => {
const file = event.target.files[0];
if (file && (file.type === 'image/jpeg' || file.type === 'image/jpg')) {
setSelectedImageName(file.name);
setErrorMessage('');
} else {
alert('Please upload a valid image file (JPG, JPEG).');
}
};
const handleImageCancel = () => {
setSelectedImageName('');
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleCheckClick = async () => {
console.log('Verify - OCR Ktp')
setShowResult(true)
};
const getValueStyle = (value) => ({
color: value ? 'green' : 'red',
});
return (
<div className='container' style={{ marginTop: '3%' }}>
{/* Inject keyframes for the spinner */}
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
{isLoading && (
<div style={styles.loadingOverlay}>
<div style={styles.spinner}></div>
<p style={styles.loadingText}>Loading...</p>
</div>
)}
{/* Welcome Message */}
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<div>
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
</div>
{/* Tombol di bawah teks */}
<div className="d-flex flex-row mt-3">
<Link to="/createApps" style={{ textDecoration: 'none' }}>
<button className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<i className="fas fa-plus text-white me-2"></i>
<p className="text-white mb-0">Create New App ID</p>
</button>
</Link>
</div>
</div>
</div>
{/* Section */}
<div style={
{
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
}
}>
{/* Application ID Selection */}
<div className="form-group row align-items-center"> {/* Added align-items-center for vertical alignment */}
<div className="col-md-6">
<div style={styles.selectWrapper}>
<select
id="applicationId"
className="form-control"
style={styles.select}
value={applicationId}
onChange={(e) => setApplicationId(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
>
<option value="">Select Application ID</option>
{applicationIds.map((app) => (
<option key={app.id} value={app.id}>
{app.name}
</option>
))}
</select>
<FontAwesomeIcon
icon={isSelectOpen ? faChevronDown : faChevronLeft}
style={styles.chevronIcon}
/>
</div>
</div>
<div className="col-md-6">
<p className="text-secondary" style={{ fontSize: '16px', fontWeight: '400', margin: '0', marginTop: '8px' }}> {/* Adjusted margins */}
Remaining Quota
</p>
<div style={styles.remainingQuota}>
<span style={styles.quotaText}>0</span>
<span style={styles.timesText}>(times)</span>
</div>
</div>
</div>
{/* Upload Section */}
<div className='col-md-6'>
<div className="form-group mt-4">
<label htmlFor="uploadPhoto" style={{ fontWeight: 600, fontSize: '14px', color: '#212529' }}>Upload your e-KTP Photo</label>
<div
style={styles.uploadArea}
onClick={() => document.getElementById('fileUpload').click()}
>
<i className="fas fa-cloud-upload-alt" style={styles.uploadIcon}></i>
<p style={styles.uploadText}>Drag and Drop Here</p>
<p>Or</p>
<a href="#">Browse</a>
<p className="text-muted">Recommended size: 300x300 (Max File Size: 2MB)</p>
<p className="text-muted">Supported file types: JPG, JPEG</p>
</div>
<input
type="file"
id="fileUpload"
ref={fileInputRef}
style={{ display: 'none' }}
accept="image/jpeg, image/png, image/jpg"
onChange={handleImageUpload}
/>
{errorMessage && (
<small style={styles.uploadError}>{errorMessage}</small>
)}
</div>
</div>
{/* Display uploaded image name */}
{selectedImageName && (
<div className="col-md-6 mt-4">
<div style={styles.fileWrapper}>
<FontAwesomeIcon icon={faImage} style={styles.imageIcon} />
<div style={{ marginRight: '18rem', marginTop: '0.2rem' }}>
<h5>Uploaded File:</h5>
<p>{selectedImageName}</p>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<FontAwesomeIcon
icon={faTimes}
style={styles.closeIcon}
onClick={handleImageCancel}
/>
</div>
</div>
</div>
)}
{/* Submit Button */}
<div style={styles.submitButton}>
<button onClick={handleCheckClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Check Now</p>
</button>
</div>
{/* Result Section */}
{showResult && (
<div style={{ display: 'flex', flexDirection: 'row', marginTop: '20px' }}>
<div style={{ flex: 1, marginRight: '10px' }}>
<h1 style={{ color: '#0542cc' }}>Results</h1>
<div style={styles.resultContainer}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<table style={{ ...styles.tableStyle, marginRight: '10px' }}>
<tbody>
<tr>
<td style={styles.tableCell}>NIK</td>
<td style={styles.tableCell}>{data.nik}</td>
</tr>
<tr>
<td style={styles.tableCell}>District</td>
<td style={styles.tableCell}>{data.district}</td>
</tr>
<tr>
<td style={styles.tableCell}>Name</td>
<td style={styles.tableCell}>{data.name}</td>
</tr>
<tr>
<td style={styles.tableCell}>City</td>
<td style={styles.tableCell}>{data.city}</td>
</tr>
<tr>
<td style={styles.tableCell}>Date of Birth</td>
<td style={styles.tableCell}>{data.dob}</td>
</tr>
<tr>
<td style={styles.tableCell}>State</td>
<td style={styles.tableCell}>{data.state}</td>
</tr>
<tr>
<td style={styles.tableCell}>Gender</td>
<td style={styles.tableCell}>{data.gender}</td>
</tr>
</tbody>
</table>
<table style={styles.tableStyle}>
<tbody>
<tr>
<td style={styles.tableCell}>Religion</td>
<td style={styles.tableCell}>{data.religion}</td>
</tr>
<tr>
<td style={styles.tableCell}>Blood Type</td>
<td style={styles.tableCell}>{data.bloodType}</td>
</tr>
<tr>
<td style={styles.tableCell}>Marital Status</td>
<td style={styles.tableCell}>{data.maritalStatus}</td>
</tr>
<tr>
<td style={styles.tableCell}>Address</td>
<td style={styles.tableCell}>{data.address}</td>
</tr>
<tr>
<td style={styles.tableCell}>Occupation</td>
<td style={styles.tableCell}>{data.occupation}</td>
</tr>
<tr>
<td style={styles.tableCell}>RT/RW</td>
<td style={styles.tableCell}>{data.rtRw}</td>
</tr>
<tr>
<td style={styles.tableCell}>Nationality</td>
<td style={styles.tableCell}>{data.nationality}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div style={{ flex: 1, textAlign: 'center', marginTop: '3rem' }}>
<img
src={data.imageUrl}
alt="KTP"
style={{ width: '292px', height: '172px', borderRadius: '8px', marginBottom: '10px' }}
/>
<div style={{ width: '292px', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', marginLeft: '8.2rem' }}>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', width: '100%' }}>
<p style={getValueStyle(data.dark)}>Dark: {data.dark.toString()}</p>
<p style={getValueStyle(data.blur)}>Blur: {data.blur.toString()}</p>
</div>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', width: '100%' }}>
<p style={getValueStyle(data.grayscale)}>Grayscale: {data.grayscale.toString()}</p>
<p style={getValueStyle(data.flashlight)}>Flashlight: {data.flashlight.toString()}</p>
</div>
</div>
</div>
</div>
)}
</div>
</div>
)
}
export default Verify
const styles = {
formGroup: {
marginTop: '-45px',
},
selectWrapper: {
position: 'relative',
marginTop: '0', // Adjusted to remove excessive spacing
},
select: {
width: '100%',
paddingRight: '30px', // Ensures padding for the icon
},
chevronIcon: {
position: 'absolute',
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'none',
},
remainingQuota: {
display: 'flex', // Ensures the text aligns properly
flexDirection: 'row',
alignItems: 'center',
marginTop: '4px', // Adjust spacing from the label
},
quotaText: {
fontSize: '40px',
color: '#0542cc',
fontWeight: '600',
},
timesText: {
marginLeft: '8px',
verticalAlign: 'super',
fontSize: '20px', // Adjust font size if necessary
},
uploadArea: {
backgroundColor: '#e6f2ff',
height: '250px',
cursor: 'pointer',
marginTop: '1rem',
paddingTop: '22px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
border: '1px solid #ced4da',
borderRadius: '0.25rem',
},
uploadIcon: {
fontSize: '40px',
color: '#0542cc',
marginBottom: '7px',
},
uploadText: {
color: '#1f2d3d',
fontWeight: '400',
fontSize: '16px',
lineHeight: '13px',
},
fileWrapper: {
backgroundColor: '#fff',
border: '0.2px solid gray',
padding: '15px 0 0 17px',
borderRadius: '5px',
position: 'relative',
display: 'flex',
alignItems: 'center',
gap: '10px',
justifyContent: 'space-between',
},
fileInfo: {
marginTop: '4rem',
display: 'flex',
alignItems: 'center',
},
imageIcon: {
color: '#0542cc',
fontSize: '24px',
marginBottom: '1rem'
},
closeIcon: {
color: 'red',
cursor: 'pointer',
fontSize: '26px',
marginRight: '1rem',
marginBottom: '1rem'
},
submitButton: {
marginLeft: 'auto',
marginTop: '4rem',
textAlign: 'start',
position: 'relative', // Menambahkan posisi relative
zIndex: 1, // Menambah z-index jika ada elemen yang menutupi
},
uploadError: {
color: 'red',
fontSize: '12px',
marginTop: '5px',
},
containerResultStyle: {
padding: '20px',
border: '1px solid #ccc',
borderRadius: '8px',
backgroundColor: '#f9f9f9',
marginTop: '20px',
},
resultContainer: {
overflowX: 'auto', // Allows horizontal scrolling if the table is too wide
},
tableStyle: {
width: '100%',
borderCollapse: 'collapse', // Ensures that table borders are merged
},
tableCell: {
padding: '10px',
border: '1px solid #ddd', // Light gray border around each cell
textAlign: 'left',
},
loadingOverlay: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.2)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
},
spinner: {
border: '4px solid rgba(0, 0, 0, 0.1)',
borderLeftColor: '#0542cc',
borderRadius: '50%',
width: '90px',
height: '90px',
animation: 'spin 1s ease-in-out infinite',
},
loadingText: {
marginTop: '10px',
fontSize: '1.2rem',
color: '#fff',
textAlign: 'center',
},
};

View File

@ -0,0 +1,5 @@
import VerifyKtp from "./Verify";
export {
VerifyKtp
}

View File

@ -0,0 +1,246 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
const Applications = () => {
const BASE_URL = process.env.REACT_APP_BASE_URL;
const API_KEY = process.env.REACT_APP_API_KEY;
const [applications, setApplications] = useState([]);
const [buttonStates, setButtonStates] = useState([]);
const [copiedIndex, setCopiedIndex] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
const [errorMessage, setErrorMessage] = useState(null);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); // Check if mobile
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchTerm(searchTerm);
}, 500);
return () => clearTimeout(timer);
}, [searchTerm]);
useEffect(() => {
const fetchApplications = async () => {
if (debouncedSearchTerm) {
try {
const response = await fetch(`${BASE_URL}/application/get-by-name/${debouncedSearchTerm}`, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': API_KEY,
},
});
const result = await response.json();
if (response.ok) {
if (result.details && result.details.data) {
setApplications([result.details.data]);
setButtonStates([{ isHovered: false, isActive: false }]);
setErrorMessage(null);
} else {
setApplications([]);
setErrorMessage('Data is not found.');
}
} else {
setApplications([]);
setErrorMessage('Data is not found.');
}
} catch (error) {
console.error('Error fetching applications:', error);
setApplications([]);
setErrorMessage('Data is not found.');
}
} else {
const fetchAllApplications = async () => {
try {
const response = await fetch(`${BASE_URL}/application/list`, {
method: 'GET',
headers: {
'x-api-key': API_KEY,
},
});
const result = await response.json();
if (response.ok && result.status_code === 200) {
setApplications(result.details.data);
setButtonStates(result.details.data.map(() => ({ isHovered: false, isActive: false })));
setErrorMessage(null);
} else {
setApplications([]);
setErrorMessage('Error fetching data. Please try again.');
}
} catch (error) {
console.error('Error fetching all applications:', error);
setApplications([]);
setErrorMessage('Error fetching data. Please try again.');
}
};
fetchAllApplications();
}
};
fetchApplications();
}, [debouncedSearchTerm]);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Update state based on window size
};
window.addEventListener('resize', handleResize); // Add resize event listener
return () => {
window.removeEventListener('resize', handleResize); // Cleanup event listener on unmount
};
}, []);
const handleMouseEnter = (index) => {
setButtonStates((prevStates) => {
const newStates = [...prevStates];
newStates[index].isHovered = true;
return newStates;
});
};
const handleMouseLeave = (index) => {
setButtonStates((prevStates) => {
const newStates = [...prevStates];
newStates[index].isHovered = false;
return newStates;
});
};
const handleCopy = (index, appName) => {
navigator.clipboard.writeText(appName)
.then(() => {
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null), 2000);
})
.catch(err => console.error('Failed to copy: ', err));
};
return (
<div style={styles.container}>
<h1 style={styles.title}>List of Applications</h1>
<input
type="text"
placeholder="Type your keywords here"
style={styles.searchInput}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<div style={styles.wrapperCard(isMobile)}>
<div style={styles.createButtonContainer}>
<Link to="/create-new-application" style={styles.link}>
<button style={styles.createButton}>
<FontAwesomeIcon icon={faPlus} style={{ marginRight: '8px' }} />
Create New App ID
</button>
</Link>
</div>
{errorMessage ? (
<p>{errorMessage}</p>
) : (
<div style={styles.cardsContainer(isMobile)}>
{applications.map((application, index) => (
<div
key={application.id || index}
style={styles.card}
onMouseEnter={() => handleMouseEnter(index)}
onMouseLeave={() => handleMouseLeave(index)}
>
<h2>{application.name}</h2>
<p><strong>Application ID:</strong> {application.id}</p>
<p><strong>Date & Time:</strong>
{application.created_at ? new Date(application.created_at).toLocaleString() : "Date not available"}
</p>
<button
onClick={() => handleCopy(index, application.name)}
style={styles.copyButton}
>
{copiedIndex === index ? 'Copied!' : 'Copy'}
</button>
</div>
))}
</div>
)}
</div>
</div>
);
};
export default Applications;
const styles = {
container: {
padding: '20px',
fontFamily: 'Arial, sans-serif',
color: '#333',
},
title: {
fontSize: '24px',
color: '#003399',
marginBottom: '10px',
},
searchInput: {
width: '100%',
padding: '10px',
marginBottom: '20px',
borderRadius: '5px',
border: '1px solid #ddd',
},
wrapperCard: (isMobile) => ({
border: '0.2px solid gray',
borderLeft: '5px solid #0542cc',
borderRadius: '5px',
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.1)',
padding: isMobile ? '15px' : '22px', // Adjust padding dynamically based on device
overflow: 'hidden',
margin: '0 auto',
maxWidth: '1200px',
}),
createButtonContainer: {
textAlign: 'start',
marginBottom: '20px',
},
createButton: {
padding: '10px 20px',
backgroundColor: '#003399',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
},
link: {
textDecoration: 'none',
},
cardsContainer: (isMobile) => ({
display: 'grid',
gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', // Stack cards on mobile
gap: isMobile ? '15px' : '20px', // Adjust gap based on device
width: '100%',
boxSizing: 'border-box',
}),
card: {
padding: '15px',
backgroundColor: '#f9f9f9',
borderRadius: '8px',
boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.1)',
border: '1px solid #ddd',
maxWidth: '100%',
boxSizing: 'border-box',
},
copyButton: {
marginTop: '10px',
padding: '8px 12px',
backgroundColor: '#003399',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
},
};

View File

@ -0,0 +1,220 @@
import React, { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
const CreateApps = () => {
const BASE_URL = process.env.REACT_APP_BASE_URL;
const API_KEY = process.env.REACT_APP_API_KEY;
const location = useLocation();
const navigate = useNavigate();
const [buttonState, setButtonState] = useState({
isHovered: false,
isActive: false,
});
const [inputValue, setInputValue] = useState('');
const [error, setError] = useState('');
const Breadcrumb = ({ path }) => {
return (
<nav style={styles.breadcrumb}>
{path.map((item, index) => (
<span key={index}>
{index > 0 && ' > '}
<a
href={item.link}
style={{
...styles.breadcrumbLink,
fontWeight: location.pathname === item.link ? 'bold' : 'normal',
}}
>
{item.name}
</a>
</span>
))}
</nav>
);
};
const breadcrumbPath = [
{ name: 'Application', link: '/application' },
{ name: 'Create Application ID', link: '/createApps' },
];
const handleMouseEnter = () => setButtonState({ ...buttonState, isHovered: true });
const handleMouseLeave = () => setButtonState({ ...buttonState, isHovered: false, isActive: false });
const handleMouseDown = () => setButtonState({ ...buttonState, isActive: true });
const handleMouseUp = () => setButtonState({ ...buttonState, isActive: false });
const handleInputChange = (e) => {
setInputValue(e.target.value);
if (error) setError(''); // Clear error when user starts typing
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!inputValue) {
setError('Application name is required.');
} else {
const requestData = {
name: inputValue,
quota: 100,
mode_id: 9,
status_id: 1,
};
try {
const response = await fetch(`${BASE_URL}/application/add`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'accept': 'application/json',
'x-api-key': `${API_KEY}`,
},
body: JSON.stringify(requestData),
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
console.log('Form submitted successfully:', result);
// Tampilkan pesan sukses
alert('Application ID created successfully!');
// Navigasikan kembali ke halaman sebelumnya
navigate(-1); // Jika menggunakan react-router-dom
// Atau jika menggunakan React Native:
// navigation.goBack();
} catch (error) {
console.error('Error creating application ID:', error);
setError('Failed to create Application ID. Please try again.');
}
}
};
return (
<div style={styles.container}>
<div style={styles.wrapper}>
<Breadcrumb path={breadcrumbPath} />
<h2 style={styles.header}>Create Application ID</h2>
<form onSubmit={handleSubmit} style={styles.form}>
<div className='col mb-3'>
<input
type="text"
value={inputValue}
onChange={handleInputChange}
placeholder="Input your application name"
style={styles.input}
className="italic-placeholder"
maxLength={15}
/>
{error && <span style={styles.error}>{error}</span>}
</div>
<button
type="submit"
style={{
...styles.button,
backgroundColor: buttonState.isActive
? '#4BA5E7'
: buttonState.isHovered
? '#1A6FD9'
: '#0542cc',
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
>
<span style={styles.buttonText}>Create App ID</span>
</button>
</form>
</div>
<style>
{`
input::placeholder {
font-style: italic;
}
@media (max-width: 600px) {
.col {
padding: 0;
}
}
`}
</style>
</div>
);
};
const styles = {
container: {
padding: '1rem',
width: '100%',
margin: '0',
boxSizing: 'border-box',
},
wrapper: {
display: 'flex',
flexDirection: 'column',
border: '1px solid #ddd',
borderRadius: '8px',
padding: '1.5rem',
boxShadow: '0 2px 5px rgba(0, 0, 0, 0.1)',
borderLeft: '5px solid #0542cc',
backgroundColor: '#fff',
},
form: {
display: 'flex',
flexDirection: 'column',
},
header: {
marginBottom: '1rem',
fontSize: '1.5rem',
textAlign: 'flex-start',
},
input: {
width: '100%',
padding: '0.75rem',
marginBottom: '1rem',
borderRadius: '6px',
border: '1px solid #ccc',
boxSizing: 'border-box',
fontStyle: 'italic',
},
button: {
padding: '0.5rem 1rem', // Ubah ukuran padding tombol untuk resize
alignSelf: 'flex-start', // Menempatkan tombol di flex-start
color: '#fff',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
transition: 'background-color 0.3s ease',
},
buttonText: {
color: '#fff',
fontWeight: '500',
},
breadcrumb: {
marginBottom: '1rem',
fontSize: '14px',
display: 'flex',
justifyContent: 'flex-start',
},
breadcrumbLink: {
color: '#000',
textDecoration: 'none',
},
error: {
color: 'red',
fontSize: '12px',
marginTop: '-8px',
marginBottom: '10px',
},
};
export default CreateApps;

View File

@ -0,0 +1,200 @@
import React from 'react';
import {
OCR,
SmsAnnounce,
OTP
} from '../../../assets/icon';
import { DashboardImg } from '../../../assets/images';
function Dashboard() {
const cardData = [
{
icon: OCR,
title: 'Optical Character Recognition',
hits: '0',
percentage: '0%',
note: 'From Yesterday',
},
{
icon: SmsAnnounce,
title: 'SMS Announcement',
hits: '0',
percentage: '0%',
note: 'From Yesterday',
},
{
icon: OTP,
title: 'SMS OTP',
hits: '0',
percentage: '0%',
note: 'From Yesterday',
},
];
return (
<div className="container mt-5">
{/* Check Our Service Status Section */}
<div style={styles.wrapper}>
<div className="d-flex justify-content-between align-items-center">
<div style={styles.alert}>
<h5 className="mb-1" style={styles.heading(600, '#0542cc')}>Check Our Service Status</h5>
<p className="mb-0" style={styles.text}>
Stay updated on the current status of our services in real-time. Easily monitor performance, downtime, and maintenance schedules to ensure seamless integration and uninterrupted usage.
</p>
</div>
<button className="btn btn-primary">
<i className="fas fa-chevron-right"></i>
</button>
</div>
</div>
{/* Your Daily Usage Section */}
<div className="card shadow-sm p-3 mb-4">
<h5 className="mb-3">
<i className="fas fa-info me-2"></i>
<span style={styles.heading(400, '#212529')}>Your Daily Usage</span>
</h5>
<div className="row">
{cardData.map((card, index) => (
<div className="col-md-4 mb-3" key={index}>
<div className="card h-100" style={styles.card}>
<div className="card-body">
<div style={styles.flexContainer}>
<img src={card.icon} alt={card.title.toLowerCase()} style={styles.icon} />
<h6 className="card-title" style={styles.heading(600, '#0542cc')}>{card.title}</h6>
</div>
<div style={styles.valueContainer}>
<p className="card-text" style={styles.hits}>{card.hits}</p>
<p>Hits</p>
<p className="text-primary" style={styles.percentage}>
{card.percentage}
</p>
<p>{card.note}</p>
</div>
</div>
</div>
</div>
))}
</div>
{/* Subscribe Other Services Section */}
<div className="row">
<div className="col-12">
<div className="card mt-4" style={styles.cardBottom}>
<div className="card-body text-center">
<div style={styles.flexContainer} className="flex-column flex-md-row">
{/* Image on top for smaller screens, beside text on larger screens */}
<img
src={DashboardImg}
alt="dashImg"
className="mb-3 mb-md-0"
style={styles.responsiveImage}
/>
<div style={styles.contentBottom}>
<h3 style={styles.heading(600, '#000')}>Subscribe Other Services</h3>
<p className="card-text" style={styles.paragraph}>
Unlock access to more features by subscribing to our other services. Once subscribed, you'll gain insights through detailed statistics and data tailored to your needs.
</p>
<button className="btn" style={styles.button}>Contact Us</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default Dashboard;
const styles = {
wrapper: {
backgroundColor: '#e2fbea',
borderWidth: '1px',
borderStyle: 'solid',
borderColor: '#d2d6de',
borderRadius: '5px',
padding: '16px',
marginBottom: '2%',
},
alert: {
backgroundColor: '#e2fbea',
padding: '6px 22px 0 0',
marginBottom: '2%',
},
heading: (fontWeight, color) => ({
fontWeight: fontWeight,
color: color,
}),
text: {
fontWeight: 400,
color: '#212529',
marginTop: '2vh',
},
card: {
border: '0.1px solid gray',
borderRadius: '2%',
filter: 'drop-shadow(0 2px 5px rgba(0, 0, 0, 0.1))',
},
icon: {
marginBottom: '3%',
marginRight: '3%',
},
responsiveImage: {
maxWidth: '100%', // Ensure the image doesn't overflow its container
height: 'auto', // Maintain aspect ratio of the image
},
flexContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center', // Align items in the center by default
gap: '2%',
},
hits: {
fontWeight: 600,
fontSize: '40px',
color: '#0542cc',
},
percentage: {
fontWeight: 600,
fontSize: '40px',
color: '#0542cc',
},
valueContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: '4%',
margin: '10% 0 0 0',
},
cardBottom: {
border: '0.1px solid gray',
borderRadius: '2%',
filter: 'drop-shadow(0 2px 5px rgba(0, 0, 0, 0.1))',
},
contentBottom: {
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: '10px',
},
paragraph: {
textAlign: 'left',
fontWeight: 400,
},
button: {
backgroundColor: '#0542cc',
borderRadius: '5px',
color: 'white',
padding: '10px 20px',
border: 'none',
textAlign: 'center',
cursor: 'pointer',
},
};

View File

@ -0,0 +1,137 @@
import React from 'react';
import { Link } from 'react-router-dom';
const GettingStarted = () => {
const cardData = [
{
title: "Watchlist Screening",
subtitle: "Assess Potential Risk",
description: "Receive detailed insights into potential matches against a list of known or suspected individuals and entities. Our service analyzes the provided full name and other key information to help you assess risks effectively and make informed decisions.",
badge: "New",
buttonText: "Get Started",
},
{
title: "Watchlist Screening",
subtitle: "Assess Potential Risk",
description: "Receive detailed insights into potential matches against a list of known or suspected individuals and entities. Our service analyzes the provided full name and other key information to help you assess risks effectively and make informed decisions.",
badge: "New",
buttonText: "Get Started",
},
{
title: "Watchlist Screening",
subtitle: "Assess Potential Risk",
description: "Receive detailed insights into potential matches against a list of known or suspected individuals and entities. Our service analyzes the provided full name and other key information to help you assess risks effectively and make informed decisions.",
badge: "New",
buttonText: "Get Started",
},
{
title: "Watchlist Screening",
subtitle: "Assess Potential Risk",
description: "Receive detailed insights into potential matches against a list of known or suspected individuals and entities. Our service analyzes the provided full name and other key information to help you assess risks effectively and make informed decisions.",
badge: "New",
buttonText: "Get Started",
},
{
title: "Watchlist Screening",
subtitle: "Assess Potential Risk",
description: "Receive detailed insights into potential matches against a list of known or suspected individuals and entities. Our service analyzes the provided full name and other key information to help you assess risks effectively and make informed decisions.",
badge: "New",
buttonText: "Get Started",
},
{
title: "Watchlist Screening",
subtitle: "Assess Potential Risk",
description: "Receive detailed insights into potential matches against a list of known or suspected individuals and entities. Our service analyzes the provided full name and other key information to help you assess risks effectively and make informed decisions.",
badge: "New",
buttonText: "Get Started",
},
];
return (
<div className="container mt-5">
{/* Welcome Section */}
<div className="row-card" style={styles.welcomeCard}>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<div>
<h4 className="mb-3 text-start">
<i className="fas fa-info fa-bold me-3"></i>
Welcome Back, Murtadi
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
</div>
{/* Buttons */}
<div className="d-flex flex-column flex-md-row mt-3">
<Link to="/createApps" style={{ textDecoration: 'none' }} className="mb-3 mb-md-0">
<button className="btn d-flex justify-content-center align-items-center me-2" style={styles.buttonPrimary}>
<i className="fas fa-plus text-white me-2"></i>
<p className="text-white mb-0">Create New App ID</p>
</button>
</Link>
<button className="btn d-flex justify-content-center align-items-center" style={styles.buttonSecondary}>
<p className="text-primary mb-0 fw-bold">Read Our API Docs</p>
</button>
</div>
</div>
</div>
{/* Cards Section */}
<div className="row">
{cardData.map((card, index) => (
<div className="col-md-6 col-lg-4 mb-4" key={index}>
<div className="card h-100" style={styles.card}>
<div className="card-body text-start">
<h5 className="card-title d-flex justify-content-between align-items-center">
<span style={styles.cardTitle}>{card.title}</span>
<span className="badge" style={styles.badge}>{card.badge}</span>
</h5>
<h6 className="mb-3">{card.subtitle}</h6>
<p className="card-text">{card.description}</p>
<a href="/" className="btn btn-link" style={styles.linkButton}>
{card.buttonText}
</a>
</div>
</div>
</div>
))}
</div>
</div>
);
};
export default GettingStarted;
const styles = {
welcomeCard: {
backgroundColor: 'white',
borderLeft: '4px solid #007bff',
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)',
marginBottom: '1rem',
},
buttonPrimary: {
backgroundColor: '#0542CC',
},
buttonSecondary: {
backgroundColor: '#E2FBEA',
},
card: {
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
},
cardTitle: {
color: '#007bff',
},
badge: {
backgroundColor: 'green',
color: 'white',
padding: '0.25em 0.5em',
borderRadius: '0.25em',
},
linkButton: {
padding: '0',
fontWeight: 'bold',
color: '#007bff',
},
};

11
src/screens/Home/index.js Normal file
View File

@ -0,0 +1,11 @@
import GettingStarted from './GettingStarted/GettingStarted';
import Dashboard from './Dashboard/Dashboard';
import Applications from './Applications/Applications';
import CreateApps from './Applications/CreateApps';
export {
GettingStarted,
Dashboard,
Applications,
CreateApps
}

View File

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';