feat: mobile face attendance integration

This commit is contained in:
mwpn
2026-03-05 14:19:22 +07:00
commit b61f6579ee
16 changed files with 3459 additions and 0 deletions

60
README.md Normal file
View 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.

1334
app.js Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

329
index.html Normal file
View 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">&times;</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 &amp; 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 &amp; 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 &amp; 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">&times;</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">&times;</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">&times;</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
View 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

File diff suppressed because it is too large Load Diff

504
test.html Normal file
View 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>