feat: mobile face attendance integration
This commit is contained in:
60
README.md
Normal file
60
README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# SMAN1 Mobile Device Client (Web/PWA)
|
||||
|
||||
Aplikasi web mobile-first untuk **perangkat absensi** yang terhubung ke backend CodeIgniter 4.
|
||||
|
||||
> Fokusnya: mempermudah testing dan penggunaan awal di Android (Chrome) tanpa perlu build APK native dulu. Nantinya bisa dibungkus jadi aplikasi Android (WebView / TWA) jika dibutuhkan.
|
||||
|
||||
## Struktur
|
||||
|
||||
- `index.html` — halaman utama device absensi
|
||||
- `styles.css` — styling mobile-first (tombol besar, UI simpel untuk layar sentuh)
|
||||
- `app.js` — logic pemanggilan API backend `/api/attendance/checkin`
|
||||
|
||||
## Fitur
|
||||
|
||||
- Simpan **Backend URL**, `device_code`, dan `api_key` di localStorage.
|
||||
- Ambil lokasi GPS lewat `navigator.geolocation` dan kirim ke backend.
|
||||
- Form sederhana untuk input `student_id` (sementara, menunggu integrasi Face Recognition / QR).
|
||||
- Tampilkan hasil status:
|
||||
- `PRESENT`, `LATE`, `OUTSIDE_ZONE`, `NO_SCHEDULE`, `INVALID_DEVICE`, dll.
|
||||
- Desain UI:
|
||||
- Satu kolom, tombol besar, teks jelas, nyaman dipakai di HP Android (Chrome).
|
||||
|
||||
## Cara Menjalankan (dev)
|
||||
|
||||
1. Pastikan backend jalan di misalnya:
|
||||
- `http://localhost/sman1/backend/public`
|
||||
- atau IP di jaringan lokal, misal `http://192.168.1.10/sman1/backend/public`
|
||||
|
||||
2. Buka folder `mobile` di VSCode / editor.
|
||||
|
||||
3. Jalankan static server sederhana (opsi):
|
||||
|
||||
- Dengan PHP:
|
||||
|
||||
```bash
|
||||
cd mobile
|
||||
php -S localhost:8001
|
||||
```
|
||||
|
||||
Lalu buka `http://localhost:8001` di browser Android.
|
||||
|
||||
- Atau cukup buka `index.html` langsung di browser (double click), tapi lebih baik lewat HTTP.
|
||||
|
||||
4. Di HP Android (Chrome):
|
||||
|
||||
- Akses: `http://IP_LAPTOP:8001` (misal `http://192.168.1.10:8001`).
|
||||
- Masukkan:
|
||||
- **Backend URL**: `http://IP_LAPTOP/sman1/backend/public`
|
||||
- **Device Code** dan **API Key** dari halaman **Device Absen** di dashboard.
|
||||
- Isi `ID Siswa`, pastikan ada jadwal aktif dan zona device sudah diatur.
|
||||
- Tekan **Check-in Sekarang**.
|
||||
|
||||
## Integrasi Lanjutan
|
||||
|
||||
Ke depan, layar ini bisa dikembangkan menjadi:
|
||||
|
||||
- Integrasi kamera + Face Recognition (panggil external engine, hanya kirim `student_id` ke backend).
|
||||
- Scan QR code untuk `student_id`.
|
||||
- Pembatasan UI (mode kiosk) untuk dipasang di tablet di gerbang sekolah.
|
||||
|
||||
BIN
assets/img/logo_sman1garut.png
Normal file
BIN
assets/img/logo_sman1garut.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 958 KiB |
BIN
assets/webfonts/Outfit-Black.woff2
Normal file
BIN
assets/webfonts/Outfit-Black.woff2
Normal file
Binary file not shown.
BIN
assets/webfonts/Outfit-Bold.woff2
Normal file
BIN
assets/webfonts/Outfit-Bold.woff2
Normal file
Binary file not shown.
BIN
assets/webfonts/Outfit-ExtraBold.woff2
Normal file
BIN
assets/webfonts/Outfit-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
assets/webfonts/Outfit-ExtraLight.woff2
Normal file
BIN
assets/webfonts/Outfit-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
assets/webfonts/Outfit-Light.woff2
Normal file
BIN
assets/webfonts/Outfit-Light.woff2
Normal file
Binary file not shown.
BIN
assets/webfonts/Outfit-Medium.woff2
Normal file
BIN
assets/webfonts/Outfit-Medium.woff2
Normal file
Binary file not shown.
BIN
assets/webfonts/Outfit-Regular.woff2
Normal file
BIN
assets/webfonts/Outfit-Regular.woff2
Normal file
Binary file not shown.
BIN
assets/webfonts/Outfit-SemiBold.woff2
Normal file
BIN
assets/webfonts/Outfit-SemiBold.woff2
Normal file
Binary file not shown.
BIN
assets/webfonts/Outfit-Thin.woff2
Normal file
BIN
assets/webfonts/Outfit-Thin.woff2
Normal file
Binary file not shown.
329
index.html
Normal file
329
index.html
Normal file
@@ -0,0 +1,329 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<title>SMAN 1 Garut - Presensi Siswa</title>
|
||||
<link rel="stylesheet" href="./styles.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Peringatan: jangan buka dari file:// (browser blokir koneksi ke server) -->
|
||||
<div id="file-protocol-warning" class="file-protocol-warning hidden">
|
||||
<div class="file-protocol-warning-inner">
|
||||
<strong>⚠️ Buka aplikasi lewat HTTP</strong>
|
||||
<p>Aplikasi ini dibuka dari <strong>file://</strong>. Browser memblokir koneksi ke server. Agar bisa terhubung:</p>
|
||||
<ol>
|
||||
<li>Buka CMD / Terminal.</li>
|
||||
<li>Masuk ke folder <code>mobile</code>: <code>cd c:\laragon\www\sman1\mobile</code></li>
|
||||
<li>Jalankan: <code>php -S localhost:8001</code></li>
|
||||
<li>Buka di browser: <a href="http://localhost:8001" target="_blank" rel="noopener">http://localhost:8001</a></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast container (untuk notifikasi dari app.js) -->
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
|
||||
<!-- Modal Settings (Backend URL) -->
|
||||
<div id="settings-modal" class="modal hidden">
|
||||
<div class="modal-backdrop" id="modal-backdrop"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Pengaturan Server</h2>
|
||||
<button type="button" id="btn-close-settings" class="modal-close" aria-label="Tutup">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="modal-hint">Pastikan aplikasi dibuka lewat <strong>http://</strong> (bukan file://), misalnya <code>http://localhost:8001</code> setelah menjalankan <code>php -S localhost:8001</code> di folder mobile.</p>
|
||||
<div class="form-group">
|
||||
<label class="input-label" for="backend-url">URL Backend (SMAN 1)</label>
|
||||
<input id="backend-url" type="text" class="input-android input-full" placeholder="http://localhost/sman1/backend/public" autocomplete="off">
|
||||
</div>
|
||||
<p id="config-status" class="config-status"></p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="btn-test-connection" class="secondary-button">Cek koneksi</button>
|
||||
<button type="button" id="btn-save-config" class="login-button">Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Header (tombol settings untuk buka Backend URL) -->
|
||||
<div class="app-header">
|
||||
<button type="button" id="btn-open-settings" class="back-button btn-settings" aria-label="Pengaturan">
|
||||
<svg viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
|
||||
</button>
|
||||
<span class="app-title">Presensi Online</span>
|
||||
</div>
|
||||
|
||||
<!-- ========== SCREEN WELCOME ========== -->
|
||||
<div id="screen-welcome" class="screen">
|
||||
<div class="android-illustration">
|
||||
<div class="android-logo">
|
||||
<img src="./assets/img/logo_sman1garut.png" alt="Logo SMAN 1 Garut">
|
||||
</div>
|
||||
<h1 class="welcome-text">Presensi Online</h1>
|
||||
<p class="sub-text">SMAN 1 Garut</p>
|
||||
</div>
|
||||
<button type="button" id="btn-go-login" class="login-button ripple">Masuk</button>
|
||||
<div class="signup-link">
|
||||
Belum punya akun? <a href="#" id="btn-go-register">Daftar</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== SCREEN LOGIN ========== -->
|
||||
<div id="screen-login" class="screen hidden">
|
||||
<div class="android-illustration android-illustration-small">
|
||||
<h1 class="welcome-text">Masuk</h1>
|
||||
<p class="sub-text">NISN & PIN</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-label">NISN</div>
|
||||
<div class="input-android">
|
||||
<svg viewBox="0 0 24 24"><path d="M20 3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H4V5h16v14z"/><path d="M9 8c-1.38 0-2.5 1.12-2.5 2.5S7.62 13 9 13s2.5-1.12 2.5-2.5S10.38 8 9 8z"/></svg>
|
||||
<input id="login-nisn" type="text" inputmode="numeric" placeholder="Masukkan NISN">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-label">PIN</div>
|
||||
<div class="input-android">
|
||||
<svg viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
|
||||
<input id="login-pin" type="password" inputmode="numeric" maxlength="6" placeholder="Masukkan PIN">
|
||||
</div>
|
||||
</div>
|
||||
<p id="login-status" class="form-status"></p>
|
||||
<div class="forgot-section">
|
||||
<a href="#" id="link-forgot-pin" class="forgot-link">Lupa PIN?</a>
|
||||
</div>
|
||||
<button type="button" id="btn-login" class="login-button ripple">MASUK</button>
|
||||
<div class="signup-link">
|
||||
Belum punya akun? <a href="#" id="link-go-register">Daftar</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== SCREEN LUPA PIN ========== -->
|
||||
<div id="screen-forgot-pin" class="screen hidden">
|
||||
<div class="android-illustration android-illustration-small">
|
||||
<h1 class="welcome-text">Lupa PIN</h1>
|
||||
<p class="sub-text">Reset PIN dengan NISN Anda</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-label">NISN</div>
|
||||
<div class="input-android">
|
||||
<input id="forgot-nisn" type="text" inputmode="numeric" placeholder="Masukkan NISN">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-label">PIN Baru (min 4 digit)</div>
|
||||
<div class="input-android">
|
||||
<input id="forgot-new-pin" type="password" inputmode="numeric" maxlength="6" placeholder="PIN baru">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-label">Ulangi PIN Baru</div>
|
||||
<div class="input-android">
|
||||
<input id="forgot-new-pin2" type="password" inputmode="numeric" maxlength="6" placeholder="Ulangi PIN baru">
|
||||
</div>
|
||||
</div>
|
||||
<p id="forgot-pin-status" class="form-status"></p>
|
||||
<button type="button" id="btn-reset-pin" class="login-button ripple">Reset PIN</button>
|
||||
<div class="signup-link">
|
||||
<a href="#" id="link-back-to-login">Kembali ke Masuk</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== SCREEN REGISTER NISN ========== -->
|
||||
<div id="screen-register-nisn" class="screen hidden">
|
||||
<div class="android-illustration android-illustration-small">
|
||||
<h1 class="welcome-text">Daftar</h1>
|
||||
<p class="sub-text">Cek NISN dulu</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-label">NISN</div>
|
||||
<div class="input-android">
|
||||
<input id="reg-nisn" type="text" inputmode="numeric" placeholder="Masukkan NISN">
|
||||
</div>
|
||||
</div>
|
||||
<p id="reg-nisn-status" class="form-status"></p>
|
||||
<button type="button" id="btn-check-nisn" class="login-button ripple">Lanjutkan</button>
|
||||
<div class="signup-link">
|
||||
Sudah punya akun? <a href="#" id="link-back-login-1">Masuk</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== SCREEN REGISTER PIN ========== -->
|
||||
<div id="screen-register-pin" class="screen hidden">
|
||||
<div class="android-illustration android-illustration-small">
|
||||
<h1 class="welcome-text">Lengkapi Data</h1>
|
||||
<p class="sub-text">Pilih kelas & buat PIN</p>
|
||||
</div>
|
||||
<div id="reg-student-summary" class="student-summary"></div>
|
||||
<div class="form-group">
|
||||
<div class="input-label">Kelas</div>
|
||||
<select id="reg-class" class="input-android input-full">
|
||||
<option value="">-- Pilih kelas --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-label">PIN (min 4 digit)</div>
|
||||
<div class="input-android">
|
||||
<input id="reg-pin" type="password" inputmode="numeric" maxlength="6" placeholder="PIN">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-label">Ulangi PIN</div>
|
||||
<div class="input-android">
|
||||
<input id="reg-pin2" type="password" inputmode="numeric" maxlength="6" placeholder="Ulangi PIN">
|
||||
</div>
|
||||
</div>
|
||||
<p id="reg-pin-status" class="form-status"></p>
|
||||
<button type="button" id="btn-complete-register" class="login-button ripple">Simpan & Mulai</button>
|
||||
<div class="signup-link">
|
||||
<a href="#" id="btn-register-pin-batal">Batal</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== SCREEN HOME (setelah login) ========== -->
|
||||
<div id="screen-home" class="screen hidden">
|
||||
<div class="home-greeting">
|
||||
<h2 id="home-greeting-text" class="home-greeting-title">Presensi</h2>
|
||||
<p class="home-greeting-sub" id="home-date-label"></p>
|
||||
</div>
|
||||
<div id="home-student-summary" class="student-summary student-summary-compact"></div>
|
||||
|
||||
<!-- Kartu status / aksi presensi (enterprise style) -->
|
||||
<div class="home-presence-grid">
|
||||
<!-- Masuk -->
|
||||
<div class="presence-card" id="card-masuk">
|
||||
<div class="presence-card-icon presence-card-icon-masuk">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v18M5 12l7 7 7-7"/></svg>
|
||||
</div>
|
||||
<div class="presence-card-body">
|
||||
<div class="presence-card-label">Absen Masuk</div>
|
||||
<div id="masuk-status" class="presence-card-status hidden">Sudah masuk</div>
|
||||
<button type="button" id="btn-absen-masuk" class="presence-card-btn presence-card-btn-primary ripple">Absen Masuk</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Check-in Mapel -->
|
||||
<div class="presence-card presence-card-featured" id="card-mapel">
|
||||
<div class="presence-card-icon presence-card-icon-mapel">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||
</div>
|
||||
<div class="presence-card-body">
|
||||
<div class="presence-card-label">Jam Belajar</div>
|
||||
<p class="presence-card-hint">Scan QR dari guru untuk absen mapel</p>
|
||||
<button type="button" id="btn-scan-qr" class="presence-card-btn presence-card-btn-featured ripple">Check-in Mapel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pulang -->
|
||||
<div class="presence-card" id="card-pulang">
|
||||
<div class="presence-card-icon presence-card-icon-pulang">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 21V3M5 12l7-7 7 7"/></svg>
|
||||
</div>
|
||||
<div class="presence-card-body">
|
||||
<div class="presence-card-label">Absen Pulang</div>
|
||||
<div id="pulang-status" class="presence-card-status hidden">Sudah pulang</div>
|
||||
<button type="button" id="btn-absen-pulang" class="presence-card-btn presence-card-btn-secondary ripple">Absen Pulang</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lokasi ringkas -->
|
||||
<div class="home-location-bar">
|
||||
<span id="location-status" class="location-status-inline">Mengambil lokasi…</span>
|
||||
<button type="button" id="btn-refresh-location" class="link-button" aria-label="Refresh">↻</button>
|
||||
</div>
|
||||
|
||||
<!-- Hasil absen (popup rapi, bisa ditutup) -->
|
||||
<div id="result-box" class="result-popup hidden">
|
||||
<div class="result-popup-inner">
|
||||
<div id="result-icon" class="result-popup-icon"></div>
|
||||
<div id="result-status" class="result-popup-status">-</div>
|
||||
<p id="result-message" class="result-popup-message"></p>
|
||||
<button type="button" id="result-close-btn" class="btn-outline btn-sm">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="home-footer">
|
||||
<a href="#" id="btn-logout" class="logout-link">Keluar</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Scan QR Absen Mapel -->
|
||||
<div id="scan-qr-modal" class="modal modal-overlay hidden">
|
||||
<div class="modal-backdrop" id="scan-qr-backdrop"></div>
|
||||
<div class="modal-panel">
|
||||
<div class="modal-panel-header">
|
||||
<h2 class="modal-panel-title">Check-in Mapel</h2>
|
||||
<button type="button" id="scan-qr-close" class="modal-panel-close" aria-label="Tutup">×</button>
|
||||
</div>
|
||||
<div class="modal-panel-body">
|
||||
<p class="modal-panel-hint">Arahkan kamera ke QR yang ditampilkan guru di kelas.</p>
|
||||
<div class="qr-reader-wrap">
|
||||
<div id="qr-reader"></div>
|
||||
</div>
|
||||
<p id="scan-qr-status" class="modal-panel-status"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Konfirmasi PIN (setelah scan QR) -->
|
||||
<div id="qr-pin-modal" class="modal hidden">
|
||||
<div class="modal-backdrop" id="qr-pin-backdrop"></div>
|
||||
<div class="modal-panel">
|
||||
<div class="modal-panel-header">
|
||||
<h2 class="modal-panel-title">Konfirmasi PIN</h2>
|
||||
<button type="button" id="qr-pin-close" class="modal-panel-close">×</button>
|
||||
</div>
|
||||
<div class="modal-panel-body">
|
||||
<p class="modal-panel-hint">Masukkan PIN untuk mengirim absen mapel.</p>
|
||||
<div class="form-group">
|
||||
<label class="input-label" for="qr-pin-input">PIN</label>
|
||||
<input id="qr-pin-input" type="password" inputmode="numeric" maxlength="6" placeholder="Masukkan PIN" class="input-android input-full">
|
||||
</div>
|
||||
<div class="modal-panel-actions modal-panel-actions-row">
|
||||
<button type="button" id="qr-pin-cancel" class="btn-outline">Batal</button>
|
||||
<button type="button" id="qr-pin-submit" class="btn-primary">Kirim Absen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Kamera (verifikasi wajah sebelum check-in) -->
|
||||
<div id="camera-modal" class="modal modal-overlay hidden">
|
||||
<div class="camera-modal-backdrop" id="camera-backdrop"></div>
|
||||
<div class="modal-panel modal-panel-camera">
|
||||
<div class="modal-panel-header">
|
||||
<h2 class="modal-panel-title">Verifikasi Wajah - Smart Presensi</h2>
|
||||
<button type="button" id="camera-modal-close" class="modal-panel-close" aria-label="Tutup">×</button>
|
||||
</div>
|
||||
<div class="modal-panel-body">
|
||||
<p class="modal-panel-hint">Smart Presensi: arahkan wajah ke kamera. Setelah terverifikasi, Anda akan diminta PIN untuk menyelesaikan absen.</p>
|
||||
<p id="camera-face-status" class="camera-face-status hidden"></p>
|
||||
<div class="camera-frame">
|
||||
<video id="camera-video" class="camera-video" playsinline autoplay muted></video>
|
||||
<div id="camera-error" class="camera-frame-error hidden">Kamera tidak tersedia atau izin ditolak.</div>
|
||||
</div>
|
||||
<div class="modal-panel-actions">
|
||||
<button type="button" id="camera-btn-cancel" class="btn-outline">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>
|
||||
<script src="./app.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
if (window.location.protocol === 'file:') {
|
||||
var el = document.getElementById('file-protocol-warning');
|
||||
if (el) el.classList.remove('hidden');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
7
run.bat
Normal file
7
run.bat
Normal file
@@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
echo Menjalankan server mobile di http://localhost:8001
|
||||
echo.
|
||||
echo Buka di browser: http://localhost:8001
|
||||
echo Tekan Ctrl+C untuk stop.
|
||||
echo.
|
||||
php -S localhost:8001
|
||||
1225
styles.css
Normal file
1225
styles.css
Normal file
File diff suppressed because it is too large
Load Diff
504
test.html
Normal file
504
test.html
Normal file
@@ -0,0 +1,504 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<title>Login Native Android</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Status bar ala Android */
|
||||
.status-bar {
|
||||
background: #f5f5f5;
|
||||
padding: 12px 24px 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #1e1e1e;
|
||||
}
|
||||
|
||||
.status-bar .time {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-bar .icons {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Container utama - full screen tanpa card */
|
||||
.container {
|
||||
flex: 1;
|
||||
padding: 24px 24px 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header dengan tombol back khas Android */
|
||||
.app-header {
|
||||
margin-bottom: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
color: #1e1e1e;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.back-button:active {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.back-button svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: #1e1e1e;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
color: #1e1e1e;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* Ilustrasi/logo khas Android */
|
||||
.android-illustration {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.android-logo {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
background: #3ddc84;
|
||||
border-radius: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 12px rgba(61, 220, 132, 0.3);
|
||||
}
|
||||
|
||||
.android-logo svg {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: #1e1e1e;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
font-size: 16px;
|
||||
color: #5e5e5e;
|
||||
}
|
||||
|
||||
/* Form input ala Android native */
|
||||
.form-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #3c3c3c;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.input-android {
|
||||
background: #eeeeee;
|
||||
border-radius: 28px;
|
||||
padding: 4px 20px;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.input-android:focus-within {
|
||||
background: #ffffff;
|
||||
border-color: #3ddc84;
|
||||
box-shadow: 0 2px 8px rgba(61, 220, 132, 0.2);
|
||||
}
|
||||
|
||||
.input-android svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: #5e5e5e;
|
||||
}
|
||||
|
||||
.input-android:focus-within svg {
|
||||
fill: #3ddc84;
|
||||
}
|
||||
|
||||
.input-android input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 16px 0;
|
||||
font-size: 16px;
|
||||
color: #1e1e1e;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-android input::placeholder {
|
||||
color: #9e9e9e;
|
||||
}
|
||||
|
||||
/* Lupa password */
|
||||
.forgot-section {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: 8px 16px 32px;
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
color: #3ddc84;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.forgot-link:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Tombol login ala Android */
|
||||
.login-button {
|
||||
background: #3ddc84;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
padding: 18px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1e1e1e;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 12px rgba(61, 220, 132, 0.3);
|
||||
cursor: pointer;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
transform: scale(0.98);
|
||||
background: #2cb56b;
|
||||
box-shadow: 0 2px 6px rgba(61, 220, 132, 0.4);
|
||||
}
|
||||
|
||||
.login-button svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: #1e1e1e;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 24px 0;
|
||||
color: #9e9e9e;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
/* Opsi login lain */
|
||||
.other-login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.other-btn {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 28px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.other-btn:active {
|
||||
background: #f5f5f5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.other-btn svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
/* Biometric section */
|
||||
.biometric-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.biometric-hint {
|
||||
font-size: 14px;
|
||||
color: #5e5e5e;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.fingerprint-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.fingerprint-icon:active {
|
||||
background: #e4e4e4;
|
||||
border-color: #3ddc84;
|
||||
}
|
||||
|
||||
.fingerprint-icon svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
fill: #5e5e5e;
|
||||
}
|
||||
|
||||
/* Link daftar */
|
||||
.signup-link {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
font-size: 15px;
|
||||
color: #5e5e5e;
|
||||
}
|
||||
|
||||
.signup-link a {
|
||||
color: #3ddc84;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.signup-link a:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Navigation bar ala Android */
|
||||
.nav-bar {
|
||||
background: #ffffff;
|
||||
padding: 8px 24px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #9e9e9e;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: #3ddc84;
|
||||
}
|
||||
|
||||
.nav-item.active svg {
|
||||
fill: #3ddc84;
|
||||
}
|
||||
|
||||
.nav-item svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: #9e9e9e;
|
||||
}
|
||||
|
||||
/* Animasi */
|
||||
@keyframes ripple {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(0.98); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.ripple:active {
|
||||
animation: ripple 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Main content - full screen -->
|
||||
<div class="container">
|
||||
<!-- Header dengan back button -->
|
||||
<div class="app-header">
|
||||
</div>
|
||||
|
||||
<!-- Logo Android -->
|
||||
<div class="android-illustration">
|
||||
<div class="android-logo">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M17.5 15.5c0 1.11-.89 2-2 2s-2-.89-2-2 .89-2 2-2 2 .89 2 2zm-7 0c0 1.11-.89 2-2 2s-2-.89-2-2 .89-2 2-2 2 .89 2 2zm9-5l1.95-3.38c.28-.48.11-1.09-.37-1.37-.48-.28-1.09-.11-1.37.37L18.34 9H5.66L3.29 5.62c-.28-.48-.89-.65-1.37-.37-.48.28-.65.89-.37 1.37L3.5 10.5c-.31.43-.5.96-.5 1.5v5c0 1.11.89 2 2 2h14c1.11 0 2-.89 2-2v-5c0-.54-.19-1.07-.5-1.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="welcome-text">Selamat datang</h1>
|
||||
<p class="sub-text">Masuk ke akun Android kamu</p>
|
||||
</div>
|
||||
|
||||
<!-- Form login -->
|
||||
<div class="form-group">
|
||||
<div class="input-label">NISN</div>
|
||||
<div class="input-android">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>
|
||||
</svg>
|
||||
<input type="nisn" placeholder="nama@email.com" value="012345678">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="input-label">Kata sandi</div>
|
||||
<div class="input-android">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
|
||||
</svg>
|
||||
<input type="password" placeholder="Masukkan sandi" value="12345678">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lupa password -->
|
||||
<div class="forgot-section">
|
||||
<a href="#" class="forgot-link">Lupa kata sandi?</a>
|
||||
</div>
|
||||
|
||||
<!-- Tombol login -->
|
||||
<button class="login-button ripple" onclick="alert('Demo: Login berhasil!')">
|
||||
MASUK
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="divider">
|
||||
<div class="divider-line"></div>
|
||||
<span class="divider-text">atau</span>
|
||||
<div class="divider-line"></div>
|
||||
</div>
|
||||
|
||||
<!-- Login lain -->
|
||||
<div class="other-login">
|
||||
<button class="other-btn">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="#4285F4" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="other-btn">
|
||||
<svg viewBox="0 0 24 24" fill="#1877F2">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Fingerprint -->
|
||||
<div class="biometric-section">
|
||||
<div class="biometric-hint">Atau gunakan sidik jari</div>
|
||||
<div class="fingerprint-icon ripple" onclick="alert('Demo: Autentikasi biometrik')">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M17.81 4.47c-.08 0-.16-.02-.23-.06C15.66 3.42 14 3 12.01 3c-1.98 0-3.86.47-5.57 1.41-.24.13-.54.04-.68-.2-.13-.24-.04-.54.2-.68C7.9 2.52 9.94 2 12.01 2c2.07 0 3.98.5 5.74 1.44.24.13.35.43.22.67-.09.18-.26.28-.45.28zM4.32 7.09c-.19 0-.38-.1-.48-.27-.13-.24-.04-.54.2-.68.77-.43 1.57-.76 2.39-.99.23-.06.47.07.54.3.07.23-.07.47-.3.54-.75.21-1.48.51-2.2.91-.17.12-.36.19-.55.19zM3 13.5c-.28 0-.5-.22-.5-.5 0-1.36.21-2.69.64-3.96.08-.23.33-.35.56-.27.23.08.35.33.27.56-.38 1.14-.58 2.35-.58 3.67 0 .28-.22.5-.5.5zM21 13.5c-.28 0-.5-.22-.5-.5 0-3.75-2.92-6.86-6.64-7.15-.24-.02-.43-.22-.42-.46.02-.24.22-.43.46-.42C18.47 5.27 22 8.97 22 13c0 .28-.22.5-.5.5zM11.5 22c-.28 0-.5-.22-.5-.5v-4c0-.28.22-.5.5-.5s.5.22.5.5v4c0 .28-.22.5-.5.5zM15.5 22c-.28 0-.5-.22-.5-.5v-6c0-.28.22-.5.5-.5s.5.22.5.5v6c0 .28-.22.5-.5.5zM19.5 22c-.28 0-.5-.22-.5-.5v-2c0-.28.22-.5.5-.5s.5.22.5.5v2c0 .28-.22.5-.5.5zM7.5 22c-.28 0-.5-.22-.5-.5v-4c0-.28.22-.5.5-.5s.5.22.5.5v4c0 .28-.22.5-.5.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Link daftar -->
|
||||
<div class="signup-link">
|
||||
Belum punya akun?<a href="#">Daftar</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- footer -->
|
||||
|
||||
|
||||
<script>
|
||||
// Simulasi interaksi native
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
document.querySelectorAll('.nav-item').forEach(nav => nav.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Efek ripple sederhana
|
||||
document.querySelectorAll('.ripple').forEach(el => {
|
||||
el.addEventListener('mousedown', function(e) {
|
||||
this.style.transform = 'scale(0.98)';
|
||||
});
|
||||
el.addEventListener('mouseup', function(e) {
|
||||
this.style.transform = 'scale(1)';
|
||||
});
|
||||
el.addEventListener('mouseleave', function(e) {
|
||||
this.style.transform = 'scale(1)';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user