init commit
2
.env
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
REACT_APP_BASE_URL = https://kapi.absys.ninja/veri
|
||||||
|
REACT_APP_API_KEY = NGKrycQefQrRvYm3KYwCIQQGnllGKlDd
|
687
index.html
Normal 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 © Your Website 2023</div>
|
||||||
|
<div>
|
||||||
|
<a href="#">Privacy Policy</a>
|
||||||
|
·
|
||||||
|
<a href="#">Terms & 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
17
package.json
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
38
src/App.css
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
103
src/App.js
@ -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;
|
||||||
|
@ -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
9
src/assets/icon/index.js
Normal 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
After Width: | Height: | Size: 513 B |
BIN
src/assets/icon/sms-otp.png
Normal file
After Width: | Height: | Size: 899 B |
BIN
src/assets/icon/sms.png
Normal file
After Width: | Height: | Size: 403 B |
BIN
src/assets/images/Dummy-Ktp.png
Normal file
After Width: | Height: | Size: 106 KiB |
BIN
src/assets/images/Logo.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
src/assets/images/Profile.jpeg
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
src/assets/images/dashboard-img.png
Normal file
After Width: | Height: | Size: 14 KiB |
11
src/assets/images/index.js
Normal 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
|
||||||
|
}
|
70
src/assets/js/externalScript.js
Normal 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
@ -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
@ -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 © Your Website 2023</div>
|
||||||
|
<div>
|
||||||
|
<a href="#">Privacy Policy</a> ·
|
||||||
|
<a href="#">Terms & Conditions</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
16
src/components/Main.jsx
Normal 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
@ -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;
|
50
src/components/Sidebar/DeepMenu.jsx
Normal 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;
|
15
src/components/Sidebar/DeeperMenu.jsx
Normal 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;
|
70
src/components/Sidebar/Menu.jsx
Normal 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;
|
37
src/components/Sidebar/Profile.jsx
Normal 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',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
50
src/components/Sidebar/SearchBar.jsx
Normal 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',
|
||||||
|
},
|
||||||
|
};
|
25
src/components/Sidebar/Sidebar.jsx
Normal 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;
|
41
src/components/Sidebar/SubMenu.jsx
Normal 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;
|
263
src/components/Sidebar/dataMenu.js
Normal 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
@ -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
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
22
src/index.js
@ -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();
|
|
||||||
|
@ -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 |
@ -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;
|
|
720
src/screens/Biometric/FaceRecognition/Section/Compare.jsx
Normal 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',
|
||||||
|
},
|
||||||
|
};
|
841
src/screens/Biometric/FaceRecognition/Section/Enroll.jsx
Normal 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;
|
||||||
|
|
||||||
|
|
||||||
|
|
11
src/screens/Biometric/FaceRecognition/Section/Liveness.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const Liveness = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Face Recognition - Liveness</h1>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Liveness
|
662
src/screens/Biometric/FaceRecognition/Section/Search.jsx
Normal 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
|
||||||
|
|
724
src/screens/Biometric/FaceRecognition/Section/Verify.jsx
Normal 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;
|
13
src/screens/Biometric/FaceRecognition/Section/index.js
Normal 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
|
||||||
|
}
|
11
src/screens/Biometric/FaceRecognition/Summary.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const Summary = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Summary</h1>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Summary
|
11
src/screens/Biometric/FaceRecognition/Transaction.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const Transaction = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Transaction</h1>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Transaction
|
123
src/screens/Biometric/FaceRecognition/Verify.jsx
Normal 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',
|
||||||
|
},
|
||||||
|
};
|
9
src/screens/Biometric/FaceRecognition/index.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import FaceVerify from "./Verify";
|
||||||
|
import FaceSummary from "./Summary";
|
||||||
|
import FaceTransaction from "./Transaction";
|
||||||
|
|
||||||
|
export {
|
||||||
|
FaceVerify,
|
||||||
|
FaceSummary,
|
||||||
|
FaceTransaction
|
||||||
|
}
|
511
src/screens/Biometric/OcrKtp/Verify.jsx
Normal 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',
|
||||||
|
},
|
||||||
|
};
|
5
src/screens/Biometric/OcrKtp/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import VerifyKtp from "./Verify";
|
||||||
|
|
||||||
|
export {
|
||||||
|
VerifyKtp
|
||||||
|
}
|
246
src/screens/Home/Applications/Applications.jsx
Normal 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',
|
||||||
|
},
|
||||||
|
};
|
220
src/screens/Home/Applications/CreateApps.jsx
Normal 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;
|
200
src/screens/Home/Dashboard/Dashboard.jsx
Normal 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',
|
||||||
|
},
|
||||||
|
};
|
137
src/screens/Home/GettingStarted/GettingStarted.jsx
Normal 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
@ -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
|
||||||
|
}
|
@ -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';
|
|