959 lines
50 KiB
PHP
959 lines
50 KiB
PHP
<?php
|
|
session_start();
|
|
include 'koneksi.php';
|
|
include 'run_background.php';
|
|
|
|
$token = isset($_GET['token']) ? trim($_GET['token']) : '';
|
|
if (empty($token)) {
|
|
header("Location: 404.php");
|
|
exit;
|
|
}
|
|
|
|
// Validasi Token
|
|
$manifestPath = __DIR__ . '/manifest_edit_tokens.json';
|
|
if (!file_exists($manifestPath)) {
|
|
header("Location: 404.php");
|
|
exit;
|
|
}
|
|
|
|
$tokens = json_decode(file_get_contents($manifestPath), true) ?: [];
|
|
if (!isset($tokens[$token])) {
|
|
header("Location: 404.php");
|
|
exit;
|
|
}
|
|
|
|
$tokenData = $tokens[$token];
|
|
if (time() > $tokenData['expires_at']) {
|
|
unset($tokens[$token]);
|
|
file_put_contents($manifestPath, json_encode($tokens, JSON_PRETTY_PRINT));
|
|
header("Location: 404.php");
|
|
exit;
|
|
}
|
|
|
|
$id_occupant = $tokenData['id_occupant'];
|
|
|
|
// Ambil data occupant
|
|
$stmt = $pdo->prepare("SELECT name, nik_nip, no_hp, foto FROM occupant WHERE id_occupant = ?");
|
|
$stmt->execute([$id_occupant]);
|
|
$occupant = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$occupant) {
|
|
header("Location: 404.php");
|
|
exit;
|
|
}
|
|
|
|
// Proses jika form disubmit via AJAX
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
header('Content-Type: application/json');
|
|
$name = trim($_POST['name']);
|
|
$nik_nip = isset($_POST['nik_nip']) ? trim($_POST['nik_nip']) : '';
|
|
|
|
if (empty($name)) {
|
|
echo json_encode(["status" => "error", "message" => "Name is required"]);
|
|
exit;
|
|
}
|
|
|
|
$fotoPath = $occupant['foto']; // Default bawaan
|
|
if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
|
|
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
|
|
|
|
if (!in_array($ext, $allowed)) {
|
|
echo json_encode(["status" => "error", "message" => "Photo format not supported"]);
|
|
exit;
|
|
}
|
|
|
|
if ($_FILES['foto']['size'] > 5 * 1024 * 1024) {
|
|
echo json_encode(["status" => "error", "message" => "Max photo size is 5MB"]);
|
|
exit;
|
|
}
|
|
|
|
$uploadDir = __DIR__ . '/uploads/';
|
|
if (!is_dir($uploadDir)) {
|
|
mkdir($uploadDir, 0755, true);
|
|
}
|
|
|
|
$fotoName = 'occ_' . time() . '_' . random_int(1000, 9999) . '.' . $ext;
|
|
$targetPath = $uploadDir . $fotoName;
|
|
|
|
if (move_uploaded_file($_FILES['foto']['tmp_name'], $targetPath)) {
|
|
$fotoPath = 'uploads/' . $fotoName;
|
|
|
|
// Hapus foto lama jika ada dan bukan bawaan sistem
|
|
if (!empty($occupant['foto']) && file_exists(__DIR__ . '/' . $occupant['foto'])) {
|
|
@unlink(__DIR__ . '/' . $occupant['foto']);
|
|
}
|
|
} else {
|
|
echo json_encode(["status" => "error", "message" => "Failed to upload photo. Check folder permissions."]);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// Handle base64 foto_data from camera capture
|
|
$fotoData = isset($_POST['foto_data']) ? $_POST['foto_data'] : '';
|
|
if (!empty($fotoData) && strpos($fotoData, 'data:image') === 0) {
|
|
$uploadDir = __DIR__ . '/uploads/';
|
|
if (!is_dir($uploadDir)) {
|
|
mkdir($uploadDir, 0755, true);
|
|
}
|
|
|
|
// Decode base64
|
|
$parts = explode(',', $fotoData, 2);
|
|
if (count($parts) === 2) {
|
|
$imgBinary = base64_decode($parts[1]);
|
|
if ($imgBinary !== false) {
|
|
$fotoName = 'occ_' . time() . '_' . random_int(1000, 9999) . '.jpg';
|
|
$targetPath = $uploadDir . $fotoName;
|
|
|
|
if (file_put_contents($targetPath, $imgBinary)) {
|
|
// Hapus foto lama
|
|
if (!empty($occupant['foto']) && file_exists(__DIR__ . '/' . $occupant['foto'])) {
|
|
@unlink(__DIR__ . '/' . $occupant['foto']);
|
|
}
|
|
$fotoPath = 'uploads/' . $fotoName;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
$stmtUpdate = $pdo->prepare("UPDATE occupant SET name = ?, nik_nip = ?, foto = ? WHERE id_occupant = ?");
|
|
$stmtUpdate->execute([$name, $nik_nip, $fotoPath, $id_occupant]);
|
|
|
|
// Success, invalidate token dan update pesan telegram
|
|
if (isset($tokenData['msg_id'])) {
|
|
$botToken = "8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho";
|
|
$chatId = '';
|
|
$tstmt = $pdo->prepare("SELECT telegram_id FROM occupant WHERE id_occupant = ?");
|
|
$tstmt->execute([$id_occupant]);
|
|
$tdata = $tstmt->fetch(PDO::FETCH_ASSOC);
|
|
if ($tdata && $tdata['telegram_id']) {
|
|
$chatId = $tdata['telegram_id'];
|
|
@file_get_contents("https://api.telegram.org/bot$botToken/deleteMessage?chat_id=$chatId&message_id={$tokenData['msg_id']}");
|
|
if (isset($tokenData['user_msg_id'])) {
|
|
@file_get_contents("https://api.telegram.org/bot$botToken/deleteMessage?chat_id=$chatId&message_id={$tokenData['user_msg_id']}");
|
|
}
|
|
|
|
// Send success message that auto-deletes after 5 seconds with live countdown
|
|
$successLabel = "✅ Data updated successfully";
|
|
$successText = "<b>" . $successLabel . "</b>";
|
|
$successPost = ['chat_id' => $chatId, 'text' => $successText, 'parse_mode' => 'HTML'];
|
|
$res = @file_get_contents("https://api.telegram.org/bot$botToken/sendMessage?" . http_build_query($successPost));
|
|
$resData = json_decode($res, true);
|
|
if (isset($resData['result']['message_id'])) {
|
|
$successMsgId = $resData['result']['message_id'];
|
|
runBackground(__DIR__ . '/auto_delete_quick.php', [$chatId, $successMsgId, $successLabel]);
|
|
}
|
|
}
|
|
}
|
|
|
|
unset($tokens[$token]);
|
|
file_put_contents($manifestPath, json_encode($tokens, JSON_PRETTY_PRINT));
|
|
|
|
echo json_encode(["status" => "success", "message" => "Data updated successfully!"]);
|
|
} catch (PDOException $e) {
|
|
echo json_encode(["status" => "error", "message" => "Failed to save: " . $e->getMessage()]);
|
|
}
|
|
exit;
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1, user-scalable=0">
|
|
<title>Profile Update - Identia</title>
|
|
<!-- CSS & Plugins (mirip manage_occupant) -->
|
|
<link rel="stylesheet" href="vendors/feather/feather.css">
|
|
<link rel="stylesheet" href="vendors/ti-icons/css/themify-icons.css">
|
|
<link rel="stylesheet" href="vendors/css/vendor.bundle.base.css">
|
|
<link rel="stylesheet" href="css/vertical-layout-light/style.css">
|
|
<link rel="stylesheet" href="vendors/mdi/css/materialdesignicons.min.css">
|
|
<style>
|
|
.auth .brand-logo img { width: 140px; max-width: 100%; }
|
|
.auth-form-light { border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
|
|
.custom-alert {
|
|
position: fixed; top: 20px; right: 20px; background: #fff;
|
|
border-left: 4px solid #fcd34d; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
padding: 15px 20px; border-radius: 4px; z-index: 9999;
|
|
display: none; align-items: center; gap: 12px; min-width: 300px;
|
|
max-width: 400px; animation: slideIn 0.3s ease-out;
|
|
}
|
|
.custom-alert.error { border-left-color: #ef4444; }
|
|
.custom-alert.success { border-left-color: #10b981; }
|
|
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
|
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
|
|
.custom-alert-icon { font-size: 24px; }
|
|
.custom-alert.error .custom-alert-icon { color: #ef4444; }
|
|
.custom-alert.success .custom-alert-icon { color: #10b981; }
|
|
.custom-alert.warning .custom-alert-icon { color: #f59e0b; }
|
|
.custom-alert-content { flex: 1; text-align: left; }
|
|
.custom-alert-title { font-weight: bold; font-size: 14px; margin-bottom: 2px; color: #333; }
|
|
.custom-alert-msg { font-size: 13px; color: #666; line-height: 1.4; }
|
|
.custom-alert-close { cursor: pointer; color: #999; font-size: 18px; }
|
|
.custom-alert-close:hover { color: #333; }
|
|
.info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 10px 12px; font-size: 13px; color: #1e293b; }
|
|
.submit-btn {
|
|
background: var(--primary); color: #fff; width: 100%; padding: 16px; border-radius: 16px; font-size: 16px; font-weight: 700; border: none; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 10px;
|
|
}
|
|
.info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
.info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }
|
|
body, .container-scroller, .page-body-wrapper, .content-wrapper { background-color: #f8f9fa !important; }
|
|
@media (max-width: 480px) {
|
|
.custom-alert { top: 10px; right: 10px; left: 10px; min-width: unset; max-width: unset; }
|
|
}
|
|
.success-checkmark { text-align: center; padding: 30px 10px; animation: scaleIn 0.5s ease; }
|
|
@keyframes scaleIn { 0% { transform: scale(0.5); opacity: 0; } 50% { transform: scale(1.1); } 100% { transform: scale(1); opacity: 1; } }
|
|
|
|
.foto-wrapper:hover .foto-circle { border-color: #4747a1 !important; }
|
|
.foto-wrapper:active { transform: scale(0.95); }
|
|
/* Session Timer Pill */
|
|
.session-timer-pill {
|
|
position: fixed; top: 12px; right: 12px; z-index: 10000;
|
|
display: flex; align-items: center; gap: 6px;
|
|
padding: 6px 14px; border-radius: 30px;
|
|
background: rgba(255,255,255,0.95); border: 1px solid #e2e8f0;
|
|
font-size: 12px; font-weight: 700; color: #64748b;
|
|
backdrop-filter: blur(8px); box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
transition: all 0.3s ease;
|
|
}
|
|
.session-timer-pill i { font-size: 16px; color: #f59e0b; }
|
|
.session-timer-pill.urgent { background: #fef2f2; border-color: #fecaca; color: #dc2626; }
|
|
.session-timer-pill.urgent i { color: #dc2626; }
|
|
/* Session Expired Overlay */
|
|
.session-expired-overlay {
|
|
display: none; position: fixed; inset: 0; z-index: 99999;
|
|
background: #f8f9fa;
|
|
flex-direction: column; align-items: center; justify-content: center;
|
|
text-align: center; padding: 40px 30px;
|
|
animation: fadeInOverlay 0.4s ease;
|
|
}
|
|
@keyframes fadeInOverlay { from { opacity: 0; } to { opacity: 1; } }
|
|
.expired-icon {
|
|
width: 80px; height: 80px; border-radius: 50%;
|
|
background: #fef2f2; border: 1px solid #fecaca;
|
|
display: flex; align-items: center; justify-content: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
.expired-icon i { font-size: 40px; color: #ef4444; }
|
|
.session-expired-overlay h3 { font-size: 22px; font-weight: 800; color: #1e293b; margin-bottom: 8px; }
|
|
.session-expired-overlay p { font-size: 14px; color: #64748b; line-height: 1.6; max-width: 300px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container-scroller">
|
|
<div class="container-fluid page-body-wrapper full-page-wrapper">
|
|
<div class="content-wrapper d-flex align-items-center auth px-0" style="padding-top: 2rem; padding-bottom: 2rem; align-items: flex-start !important;">
|
|
<div class="row w-100 mx-0">
|
|
<div class="col-lg-4 mx-auto pe-0 ps-0">
|
|
<div class="auth-form-light text-left py-4 px-4 px-sm-5 bg-white" style="min-height: 10vh; padding-bottom: 80px !important;">
|
|
<div class="brand-logo mb-4 text-center">
|
|
<img src="images/ko.png" alt="logo" style="width: 140px;">
|
|
</div>
|
|
|
|
<h4 class="font-weight-bold mb-1" style="color: #1e293b;">Edit Profile</h4>
|
|
<p class="font-weight-light text-muted mb-4" style="font-size: 13px;">Update your personal identity data</p>
|
|
|
|
<div class="info-alert mb-4">
|
|
<div class="info-alert-icon"><i class="mdi mdi-information-variant"></i></div>
|
|
<div class="info-alert-text" style="flex:1; font-size:12px;">
|
|
<span>Phone Number (Telegram) cannot be changed on this page.</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- STEP 1: Edit Data -->
|
|
<div id="step1-data">
|
|
<form class="pt-2" id="editForm">
|
|
<input type="hidden" name="action" value="update_data">
|
|
<div class="form-group mb-3">
|
|
<label class="font-weight-bold text-dark" style="font-size: 12px; margin-bottom: 6px;">Full Name</label>
|
|
<input type="text" class="form-control form-control-sm" name="name" id="name" value="<?= htmlspecialchars($occupant['name'] ?? '') ?>" required style="border-radius: 6px;">
|
|
</div>
|
|
<div class="form-group mb-3">
|
|
<label class="font-weight-bold text-dark" style="font-size: 12px; margin-bottom: 6px;">NIK / NIP</label>
|
|
<input type="text" class="form-control form-control-sm" name="nik_nip" id="nik_nip" value="<?= htmlspecialchars($occupant['nik_nip'] ?? '') ?>" style="border-radius: 6px;" required>
|
|
</div>
|
|
<div class="form-group mb-4">
|
|
<label class="font-weight-bold text-dark" style="font-size: 12px; margin-bottom: 6px;">Phone Number</label>
|
|
<input type="text" class="form-control form-control-sm bg-light" name="no_hp" id="no_hp" value="<?= htmlspecialchars($occupant['no_hp'] ?? '') ?>" readonly style="border-radius: 6px; cursor: not-allowed; color: #6c757d;">
|
|
</div>
|
|
|
|
<div class="mt-3">
|
|
<button type="button" id="btnLanjut" class="btn btn-block btn-primary btn-lg font-weight-medium auth-form-btn" style="border-radius: 8px; font-size: 14px; padding: 12px;">Next <i class="mdi mdi-arrow-right ml-1"></i></button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- STEP 2: Edit Foto -->
|
|
<div id="step2-foto" style="display: none;">
|
|
<div class="text-center mb-4">
|
|
<?php
|
|
$fotoSrc = (!empty($occupant['foto']) && file_exists($occupant['foto'])) ? $occupant['foto'] : 'images/faces/user.jpg';
|
|
?>
|
|
<div class="foto-wrapper" style="cursor: pointer; display: inline-block; position: relative;" onclick="startCamera()">
|
|
<img id="previewFoto" src="<?= htmlspecialchars($fotoSrc) ?>" class="foto-circle" style="width: 120px; height: 120px; border-radius: 50%; object-fit: cover; border: 3px solid #e0e7ff; box-shadow: 0 4px 10px rgba(0,0,0,0.05); transition: transform 0.2s;">
|
|
<div class="foto-badge" style="position: absolute; bottom: 5px; right: 5px; width: 32px; height: 32px; border-radius: 50%; background: #4747a1; display: flex; align-items: center; justify-content: center; border: 3px solid #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.2);">
|
|
<i class="mdi mdi-camera text-white" style="font-size: 16px;"></i>
|
|
</div>
|
|
</div>
|
|
<p class="mt-3 text-muted" style="font-size:12px; font-weight:500;"><i class="mdi mdi-cursor-default-click text-primary mr-1"></i>Click the photo above to update</p>
|
|
</div>
|
|
|
|
<input type="hidden" name="foto_data" id="fotoData" value="" form="editForm">
|
|
|
|
<div class="d-flex justify-content-between mt-4">
|
|
<button type="button" id="btnBack" class="btn btn-light" style="border-radius: 8px; font-size: 14px; padding: 12px 20px;"><i class="mdi mdi-arrow-left mr-1"></i> Back</button>
|
|
<button type="button" id="btnSimpan" class="btn btn-primary" style="border-radius: 8px; font-size: 14px; padding: 12px 30px; flex: 1; margin-left: 10px;"><i class="mdi mdi-content-save mr-1"></i> Save Data</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="step3-success" class="success-checkmark" style="display: none; padding-top: 30px;">
|
|
<div class="checkmark-circle text-center">
|
|
<div style="width: 80px; height: 80px; border-radius: 50%; background: #10b981; display: inline-flex; justify-content: center; align-items: center; margin-bottom: 20px; box-shadow: 0 10px 15px -3px rgba(16, 185, 129, 0.3);">
|
|
<i class="mdi mdi-check text-white" style="font-size: 40px; line-height: 1;"></i>
|
|
</div>
|
|
|
|
<h4 class="font-weight-bold text-success mb-2 text-center">Success!</h4>
|
|
<p class="text-muted text-center" style="font-size: 14px;">Your profile data has been updated.</p>
|
|
<hr class="my-4">
|
|
<p class="text-muted mb-4 text-center" style="font-size: 13px;">This page will automatically close in <span id="countdownText" class="font-weight-bold text-primary">3</span> seconds to keep your session secure.</p>
|
|
<button type="button" onclick="Telegram.WebApp.close(); window.close()" class="btn btn-block btn-outline-primary fw-bold" style="border-radius: 8px; padding: 12px;">Close Now</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- FULL CAMERA OVERLAY (registrasi_foto.php style) -->
|
|
<style>
|
|
.edit-cam-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; z-index:9999; background:#1e2223; flex-direction:column; font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif; }
|
|
.edit-cam-overlay * { box-sizing:border-box; }
|
|
.ecam-header { display:flex; justify-content:space-between; align-items:center; padding:20px; z-index:30; position:absolute; top:0; left:0; right:0; }
|
|
.ecam-area { flex:1; position:relative; overflow:hidden; background:#1e2223; }
|
|
.ecam-area video { width:100%; height:100%; object-fit:cover; transform:scaleX(-1); }
|
|
.ecam-area canvas.ecam-overlay-cv { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; }
|
|
.ecam-face-oval { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index:10; }
|
|
.ecam-face-oval svg { width:100%; height:100%; }
|
|
#editBgRect { fill: #ffffff; }
|
|
|
|
.ecam-face-oval #editOvalRing { stroke:#e2e8f0; stroke-width:6; display:block; }
|
|
.ecam-face-oval #editOvalRingOut { stroke:none; display:block; }
|
|
|
|
.ecam-face-oval.detecting #editBgRect { animation: bankBgGlow 0.5s infinite; filter: brightness(1.2); }
|
|
.ecam-face-oval.success #editBgRect { fill: #00FF00; animation: none; filter: brightness(1.2); }
|
|
.ecam-face-oval.error #editBgRect { fill: #FF0000; animation: none; filter: brightness(1.2); }
|
|
|
|
@keyframes bankBgGlow {
|
|
0% { fill: #FF0000; }
|
|
25% { fill: #FFFF00; }
|
|
50% { fill: #00FF00; }
|
|
75% { fill: #00FFFF; }
|
|
100% { fill: #FF0000; }
|
|
}
|
|
|
|
.ecam-bar-after {
|
|
position:absolute; bottom:0; left:0; right:0; z-index:40;
|
|
padding:30px 0; background: rgba(30,34,35,0.95); backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);
|
|
display:flex; align-items:center; justify-content:center; gap:40px; border-top:1px solid rgba(226, 232, 240, 0.2);
|
|
border-top-left-radius:24px; border-top-right-radius:24px;
|
|
pointer-events:auto; display:none;
|
|
}
|
|
.ecam-act-btn {
|
|
background:none; border:none; display:flex; flex-direction:column; align-items:center; gap:8px; cursor:pointer;
|
|
}
|
|
.ecam-act-btn .ring {
|
|
width:64px; height:64px; border-radius:50%; display:flex; align-items:center; justify-content:center;
|
|
font-size:28px; color:#fff; transition:transform 0.2s;
|
|
}
|
|
.ecam-act-btn:active .ring { transform:scale(0.95); }
|
|
.ecam-act-btn.retry .ring { background:#f59e0b; box-shadow:0 8px 20px rgba(245, 158, 11, 0.3); }
|
|
.ecam-act-btn.save .ring { background:#10b981; box-shadow:0 8px 20px rgba(16, 185, 129, 0.3); }
|
|
.ecam-act-btn span { font-size:14px; font-weight:600; color:#fff; }
|
|
|
|
/* Hide elements */
|
|
.ecam-hud, .ecam-bm, .ecam-lm, .ecam-st-pill { display: none !important; }
|
|
</style>
|
|
|
|
<div id="cameraOverlay" class="edit-cam-overlay">
|
|
<div class="ecam-header">
|
|
<button type="button" onclick="stopEditCamera()" style="background:rgba(255,255,255,0.2); border:none; width:40px; height:40px; border-radius:50%; color:white; display:flex; align-items:center; justify-content:center; backdrop-filter:blur(4px);">
|
|
<i class="mdi mdi-close" style="font-size:24px;"></i>
|
|
</button>
|
|
<div style="color:white; font-weight:700; font-size:14px; text-shadow:0 1px 3px rgba(0,0,0,0.5);">Photo Registration</div>
|
|
<div style="width:40px;"></div>
|
|
</div>
|
|
|
|
<!-- Camera Area -->
|
|
<div class="ecam-area" id="editCamArea">
|
|
<video id="editVid" autoplay playsinline muted></video>
|
|
<canvas class="ecam-overlay-cv" id="editOvCanvas"></canvas>
|
|
|
|
<div class="ecam-face-oval" id="editOval">
|
|
<svg viewBox="0 0 400 800" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">
|
|
<defs>
|
|
<mask id="editSilhouetteMask">
|
|
<rect width="100%" height="100%" fill="white"/>
|
|
<path d="M 200 200 C 110 200 70 270 70 360 C 40 350 30 460 70 450 C 90 550 140 580 200 580 C 260 580 310 550 330 450 C 370 460 360 350 330 360 C 330 270 290 200 200 200 Z" fill="black" />
|
|
</mask>
|
|
</defs>
|
|
<rect id="editBgRect" width="100%" height="100%" mask="url(#editSilhouetteMask)"/>
|
|
<path d="M 200 200 C 110 200 70 270 70 360 C 40 350 30 460 70 450 C 90 550 140 580 200 580 C 260 580 310 550 330 450 C 370 460 360 350 330 360 C 330 270 290 200 200 200 Z" fill="none" id="editOvalRingOut"/>
|
|
<path d="M 200 200 C 110 200 70 270 70 360 C 40 350 30 460 70 450 C 90 550 140 580 200 580 C 260 580 310 550 330 450 C 370 460 360 350 330 360 C 330 270 290 200 200 200 Z" fill="none" id="editOvalRing"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<!-- Brightness Meter -->
|
|
<div class="ecam-bm" id="editBrightMeter" style="display:none;">
|
|
<i class="mdi mdi-white-balance-sunny" id="editBmIcon"></i>
|
|
<div class="ecam-meter-bar"><div class="ecam-meter-fill" id="editBmFill" style="height:50%;background:#f59e0b;"></div></div>
|
|
</div>
|
|
|
|
<!-- Liveness Meter -->
|
|
<div class="ecam-lm" id="editLiveMeter" style="display:none;">
|
|
<i class="mdi mdi-shield-check" id="editLmIcon"></i>
|
|
<div class="ecam-meter-bar"><div class="ecam-meter-fill" id="editLmFill" style="height:0%;background:#f59e0b;"></div></div>
|
|
</div>
|
|
|
|
<!-- Loading -->
|
|
<div class="ecam-loading" id="editCamLoading">
|
|
<div class="ecam-ldring"></div>
|
|
<p id="editLoadText">Loading camera & face detection models...</p>
|
|
<div class="ecam-load-progress"><div class="ecam-load-progress-fill" id="editLoadProgress"></div></div>
|
|
</div>
|
|
|
|
<!-- Status Pill -->
|
|
<div class="ecam-st-pill searching" id="editStPill" style="display:none;">
|
|
<i class="mdi mdi-face-recognition" id="editStIcon"></i>
|
|
<span id="editStText">Preparing...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview -->
|
|
<div class="ecam-preview" id="editPreviewWrap">
|
|
<canvas id="editCapCanvas"></canvas>
|
|
<div class="ecam-preview-tag"><i class="mdi mdi-check-circle"></i> Photo Captured</div>
|
|
</div>
|
|
|
|
<!-- HUD: Shutter -->
|
|
<div class="ecam-hud">
|
|
<div style="margin-bottom:20px;">
|
|
<button class="ecam-shutter" id="editBtnShutter" disabled></button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bar After (retake/save) -->
|
|
<div class="ecam-bar-after" id="editBarAfter">
|
|
<button class="ecam-act-btn retry" id="editBtnRetake">
|
|
<div class="ring"><i class="mdi mdi-refresh"></i></div>
|
|
<span>Retake</span>
|
|
</button>
|
|
<button class="ecam-act-btn save" id="editBtnUseFoto">
|
|
<div class="ring"><i class="mdi mdi-check"></i></div>
|
|
<span>Save</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Alert Container -->
|
|
<div id="customAlert" class="custom-alert warning" style="display: none;">
|
|
<i id="customAlertIcon" class="mdi mdi-alert-circle custom-alert-icon"></i>
|
|
<div class="custom-alert-content">
|
|
<div id="customAlertTitle" class="custom-alert-title">Notice</div>
|
|
<div id="customAlertMsg" class="custom-alert-msg">Warning message here.</div>
|
|
</div>
|
|
<i class="mdi mdi-close custom-alert-close" onclick="closeCustomAlert()"></i>
|
|
</div>
|
|
|
|
<!-- Session Timer Pill -->
|
|
<div class="session-timer-pill" id="sessionTimerPill">
|
|
<i class="mdi mdi-timer-outline"></i>
|
|
<span id="sessionTimerText">5:00</span>
|
|
</div>
|
|
|
|
<!-- Session Expired Overlay -->
|
|
<div class="session-expired-overlay" id="sessionExpiredOverlay">
|
|
<div class="expired-icon"><i class="mdi mdi-timer-off-outline"></i></div>
|
|
<h3>Session Expired</h3>
|
|
<p>Your 5-minute access window has ended. Please use the <b>/edit_data</b> command in Telegram to request a new session.</p>
|
|
</div>
|
|
|
|
<script src="vendors/js/vendor.bundle.base.js"></script>
|
|
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
|
<script>
|
|
// Session expiry timestamp from server
|
|
var SESSION_EXPIRES_AT = <?= (int)$tokenData['expires_at'] ?>;
|
|
|
|
window.Telegram.WebApp.ready();
|
|
window.Telegram.WebApp.expand();
|
|
|
|
function previewImage(input) {
|
|
if (input.files && input.files[0]) {
|
|
var reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
document.getElementById('previewFoto').src = e.target.result;
|
|
}
|
|
reader.readAsDataURL(input.files[0]);
|
|
}
|
|
}
|
|
|
|
function showCustomAlert(title, message, type='warning') {
|
|
const alertBox = document.getElementById('customAlert');
|
|
const icon = document.getElementById('customAlertIcon');
|
|
alertBox.className = 'custom-alert ' + type;
|
|
if(type === 'error') icon.className = 'mdi mdi-close-circle custom-alert-icon';
|
|
else if(type === 'success') icon.className = 'mdi mdi-check-circle custom-alert-icon';
|
|
else icon.className = 'mdi mdi-alert-circle custom-alert-icon';
|
|
document.getElementById('customAlertTitle').innerText = title;
|
|
document.getElementById('customAlertMsg').innerText = message;
|
|
alertBox.style.display = 'flex';
|
|
alertBox.style.animation = 'slideIn 0.3s ease-out';
|
|
setTimeout(() => closeCustomAlert(), 5000);
|
|
}
|
|
function closeCustomAlert() {
|
|
const alertBox = document.getElementById('customAlert');
|
|
alertBox.style.animation = 'fadeOut 0.3s ease-out forwards';
|
|
setTimeout(() => { alertBox.style.display = 'none'; }, 300);
|
|
}
|
|
|
|
// Navigation Handlers
|
|
const infoAlertText = document.querySelector('.info-alert-text');
|
|
const originalInfoText = infoAlertText.innerHTML;
|
|
|
|
document.getElementById('btnLanjut').addEventListener('click', function() {
|
|
const name = document.getElementById('name').value.trim();
|
|
if(name === '') {
|
|
showCustomAlert('Incomplete Form', 'Full Name is required.', 'warning');
|
|
return;
|
|
}
|
|
document.getElementById('step1-data').style.display = 'none';
|
|
document.getElementById('step2-foto').style.display = 'block';
|
|
infoAlertText.innerHTML = 'Click the photo below to update it. If you do <b>not</b> want to update the photo, click <b>Save Data</b>';
|
|
});
|
|
|
|
document.getElementById('btnBack').addEventListener('click', function() {
|
|
document.getElementById('step2-foto').style.display = 'none';
|
|
document.getElementById('step1-data').style.display = 'block';
|
|
infoAlertText.innerHTML = originalInfoText;
|
|
});
|
|
</script>
|
|
<script src="js/face-api/face-api.min.js"></script>
|
|
<script>
|
|
// ── Full Camera System (registrasi_foto.php style) ──
|
|
let editStream = null, editDetectLoop = null, editFaceOk = false, editDataURL = null;
|
|
let editBrightInterval = null, editLivenessScore = 0;
|
|
let editPrevLandmarks = null, editLivenessFrames = 0;
|
|
let editModelsLoaded = false;
|
|
|
|
const editVid = document.getElementById('editVid');
|
|
const editOvCanvas = document.getElementById('editOvCanvas');
|
|
const editOval = document.getElementById('editOval');
|
|
const editCamLoading = document.getElementById('editCamLoading');
|
|
const editStPill = document.getElementById('editStPill');
|
|
const editStIcon = document.getElementById('editStIcon');
|
|
const editStText = document.getElementById('editStText');
|
|
const editBtnShutter = document.getElementById('editBtnShutter');
|
|
const editCamArea = document.getElementById('editCamArea');
|
|
const editPreviewWrap = document.getElementById('editPreviewWrap');
|
|
const editCapCanvas = document.getElementById('editCapCanvas');
|
|
const editBarAfter = document.getElementById('editBarAfter');
|
|
const editBmFill = document.getElementById('editBmFill');
|
|
const editBmIcon = document.getElementById('editBmIcon');
|
|
const editLmFill = document.getElementById('editLmFill');
|
|
const editLmIcon = document.getElementById('editLmIcon');
|
|
const editLoadProgress = document.getElementById('editLoadProgress');
|
|
const editLoadText = document.getElementById('editLoadText');
|
|
|
|
function editSetProgress(pct) { editLoadProgress.style.width = Math.min(100, pct) + '%'; }
|
|
function editSetStatus(type, txt) {
|
|
editStPill.className = 'ecam-st-pill';
|
|
if (type === 'search') { editStPill.classList.add('searching'); editStIcon.className = 'mdi mdi-face-recognition'; }
|
|
if (type === 'ok') { editStPill.classList.add('found'); editStIcon.className = 'mdi mdi-check-circle'; }
|
|
if (type === 'err') { editStPill.classList.add('warn'); editStIcon.className = 'mdi mdi-alert-circle'; }
|
|
editStText.textContent = txt;
|
|
}
|
|
|
|
function startCamera() {
|
|
document.getElementById('cameraOverlay').style.display = 'flex';
|
|
editCamLoading.style.display = 'flex';
|
|
editLoadText.textContent = 'Loading camera & face detection models...';
|
|
editSetProgress(5);
|
|
editCamArea.style.display = 'block';
|
|
editPreviewWrap.style.display = 'none';
|
|
editBarAfter.style.display = 'none';
|
|
document.querySelector('.ecam-hud').style.display = 'flex';
|
|
editFaceOk = false;
|
|
editDataURL = null;
|
|
editLivenessScore = 0;
|
|
editLivenessFrames = 0;
|
|
editPrevLandmarks = null;
|
|
editLmFill.style.height = '0%';
|
|
|
|
initEditCamera();
|
|
}
|
|
|
|
async function initEditCamera() {
|
|
try {
|
|
if (!editModelsLoaded) {
|
|
editSetProgress(20);
|
|
editLoadText.textContent = 'Loading face detection models...';
|
|
|
|
const loadModelsPromise = Promise.all([
|
|
faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models'),
|
|
faceapi.nets.faceLandmark68Net.loadFromUri('js/face-api/models')
|
|
]);
|
|
await Promise.race([
|
|
loadModelsPromise,
|
|
new Promise((_, reject) => setTimeout(() => reject(new Error("Model loading timed out after 15 seconds. Make sure your internet connection is stable.")), 15000))
|
|
]);
|
|
|
|
editModelsLoaded = true;
|
|
}
|
|
editSetProgress(50);
|
|
editLoadText.textContent = 'Starting camera...';
|
|
|
|
// Force Standard Definition (SD) settings for camera to improve performance
|
|
const videoConstraints = {
|
|
facingMode: 'user',
|
|
width: { ideal: 480 }
|
|
};
|
|
|
|
editStream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints, audio: false });
|
|
editVid.srcObject = editStream;
|
|
|
|
await new Promise((resolve, reject) => {
|
|
const timeoutId = setTimeout(() => reject(new Error("Camera stream initialized but video playback timed out. Device may not support video playback.")), 15000);
|
|
editVid.onloadedmetadata = () => {
|
|
clearTimeout(timeoutId);
|
|
editVid.play().then(resolve).catch((e) => reject(new Error("Failed to play video stream: " + e.message)));
|
|
};
|
|
});
|
|
|
|
editSetProgress(100);
|
|
setTimeout(() => {
|
|
editCamLoading.style.display = 'none';
|
|
editStPill.style.display = 'flex';
|
|
document.getElementById('editBrightMeter').style.display = 'flex';
|
|
document.getElementById('editLiveMeter').style.display = 'flex';
|
|
editSetStatus('search', 'Position your face...');
|
|
startEditDetection();
|
|
startEditBrightness();
|
|
}, 400);
|
|
|
|
} catch(err) {
|
|
let msg = err.message;
|
|
if (err.name === 'NotAllowedError') msg = 'Camera access denied by browser.';
|
|
else if (err.name === 'NotFoundError') msg = 'Camera not found.';
|
|
stopEditCamera();
|
|
showCustomAlert('Camera Access Failed', msg, 'error');
|
|
}
|
|
}
|
|
|
|
let isDetecting = false;
|
|
let faceState = 'idle';
|
|
let scanStartTime = 0;
|
|
|
|
function startEditDetection() {
|
|
const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 224, scoreThreshold: 0.15 });
|
|
editDetectLoop = setInterval(async () => {
|
|
if (editVid.paused || editVid.ended || !editVid.videoWidth || isDetecting) return;
|
|
isDetecting = true;
|
|
|
|
editOvCanvas.width = editVid.videoWidth;
|
|
editOvCanvas.height = editVid.videoHeight;
|
|
|
|
try {
|
|
const results = await faceapi.detectAllFaces(editVid, opts).withFaceLandmarks().run();
|
|
if (results.length === 1) {
|
|
const det = results[0].detection;
|
|
const box = det.box;
|
|
const landmarks = results[0].landmarks;
|
|
|
|
if (faceState === 'idle') {
|
|
faceState = 'scanning';
|
|
scanStartTime = Date.now();
|
|
editOval.className = 'ecam-face-oval detecting';
|
|
editLivenessFrames = 0; editLivenessScore = 0;
|
|
if (landmarks) editPrevLandmarks = landmarks.positions.map(p => ({x: p.x, y: p.y}));
|
|
} else if (faceState === 'scanning') {
|
|
if (landmarks) {
|
|
const pts = landmarks.positions;
|
|
if (editPrevLandmarks && pts.length === editPrevLandmarks.length) {
|
|
let totalDiff = 0;
|
|
for (let i = 0; i < pts.length; i++) {
|
|
totalDiff += Math.abs(pts[i].x - editPrevLandmarks[i].x) + Math.abs(pts[i].y - editPrevLandmarks[i].y);
|
|
}
|
|
const avgDiff = totalDiff / pts.length;
|
|
if (avgDiff > 0.05 && avgDiff < 15) {
|
|
editLivenessFrames = Math.min(editLivenessFrames + 1, 20);
|
|
} else if (avgDiff >= 15) {
|
|
editLivenessFrames = Math.max(0, editLivenessFrames - 2);
|
|
} else {
|
|
editLivenessFrames = Math.min(editLivenessFrames + 0.5, 20);
|
|
}
|
|
}
|
|
editPrevLandmarks = pts.map(p => ({x: p.x, y: p.y}));
|
|
editLivenessScore = Math.min(100, (editLivenessFrames / 12) * 100);
|
|
}
|
|
|
|
if (Date.now() - scanStartTime > 1500) {
|
|
faceState = 'showing_result';
|
|
if (editLivenessScore >= 15) {
|
|
editOval.className = 'ecam-face-oval success';
|
|
clearInterval(editDetectLoop);
|
|
clearInterval(editBrightInterval);
|
|
|
|
// Countdown 3...2...1
|
|
let count = 3;
|
|
const cdText = document.createElement('div');
|
|
cdText.style = 'position:absolute; top:40%; left:50%; transform:translate(-50%, -50%); font-size:120px; font-weight:900; color:#10b981; z-index:999; text-shadow:0 4px 20px rgba(0,0,0,0.5);';
|
|
cdText.innerText = count;
|
|
editCamArea.appendChild(cdText);
|
|
|
|
const cdInt = setInterval(() => {
|
|
count--;
|
|
if (count > 0) {
|
|
cdText.innerText = count;
|
|
} else {
|
|
clearInterval(cdInt);
|
|
cdText.remove();
|
|
editFaceOk = true;
|
|
editCapture();
|
|
}
|
|
}, 1000);
|
|
|
|
} else {
|
|
editOval.className = 'ecam-face-oval error';
|
|
setTimeout(() => {
|
|
if (faceState === 'showing_result') {
|
|
faceState = 'idle';
|
|
editOval.className = 'ecam-face-oval';
|
|
editLivenessFrames = 0;
|
|
editLivenessScore = 0;
|
|
}
|
|
}, 1500);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (faceState !== 'showing_result') {
|
|
faceState = 'idle';
|
|
editOval.className = 'ecam-face-oval';
|
|
editLivenessFrames = 0; editLivenessScore = 0;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("Detection error:", e);
|
|
} finally {
|
|
isDetecting = false;
|
|
}
|
|
}, 400);
|
|
}
|
|
|
|
function startEditBrightness() {
|
|
const tmpCanvas = document.createElement('canvas');
|
|
const tmpCtx = tmpCanvas.getContext('2d');
|
|
editBrightInterval = setInterval(() => {
|
|
if (editVid.paused || editVid.ended || !editVid.videoWidth) return;
|
|
tmpCanvas.width = 80; tmpCanvas.height = 60;
|
|
tmpCtx.drawImage(editVid, 0, 0, 80, 60);
|
|
const data = tmpCtx.getImageData(0, 0, 80, 60).data;
|
|
let sum = 0; const count = data.length / 4;
|
|
for (let i = 0; i < data.length; i += 4) sum += (data[i] * 0.299 + data[i+1] * 0.587 + data[i+2] * 0.114);
|
|
const pct = Math.min(100, Math.max(5, (sum / count / 255) * 100));
|
|
editBmFill.style.height = pct + '%';
|
|
if (pct < 30) { editBmFill.style.background = '#ef4444'; editBmIcon.style.color = '#ef4444'; }
|
|
else if (pct < 55) { editBmFill.style.background = '#f59e0b'; editBmIcon.style.color = '#f59e0b'; }
|
|
else { editBmFill.style.background = '#22c55e'; editBmIcon.style.color = '#22c55e'; }
|
|
}, 800);
|
|
}
|
|
|
|
function editCapture() {
|
|
if (!editFaceOk) return;
|
|
clearInterval(editDetectLoop);
|
|
clearInterval(editBrightInterval);
|
|
const vw = editVid.videoWidth, vh = editVid.videoHeight;
|
|
editCapCanvas.width = vw; editCapCanvas.height = vh;
|
|
const ctx = editCapCanvas.getContext('2d');
|
|
ctx.save(); ctx.translate(vw, 0); ctx.scale(-1, 1);
|
|
ctx.drawImage(editVid, 0, 0, vw, vh);
|
|
ctx.restore();
|
|
editDataURL = editCapCanvas.toDataURL('image/jpeg', 0.8);
|
|
if (editStream) editStream.getTracks().forEach(t => t.stop());
|
|
editCamArea.style.display = 'none';
|
|
editPreviewWrap.style.display = 'block';
|
|
document.querySelector('.ecam-hud').style.display = 'none';
|
|
editBarAfter.style.display = 'flex';
|
|
}
|
|
|
|
function editRetake() {
|
|
editDataURL = null;
|
|
editLivenessScore = 0; editLivenessFrames = 0; editPrevLandmarks = null;
|
|
editLmFill.style.height = '0%';
|
|
editPreviewWrap.style.display = 'none';
|
|
editCamArea.style.display = 'block';
|
|
editBarAfter.style.display = 'none';
|
|
document.querySelector('.ecam-hud').style.display = 'flex';
|
|
editCamLoading.style.display = 'flex';
|
|
editLoadText.textContent = 'Reloading camera...';
|
|
editSetProgress(50);
|
|
|
|
faceState = 'idle';
|
|
editOval.className = 'ecam-face-oval';
|
|
editFaceOk = false;
|
|
|
|
initEditCamera();
|
|
}
|
|
|
|
function editUseFoto() {
|
|
if (!editDataURL) return;
|
|
document.getElementById('previewFoto').src = editDataURL;
|
|
document.getElementById('fotoData').value = editDataURL;
|
|
stopEditCamera();
|
|
showCustomAlert('Success', 'Photo captured successfully. Click Save Data to save.', 'success');
|
|
}
|
|
|
|
function stopEditCamera() {
|
|
document.getElementById('cameraOverlay').style.display = 'none';
|
|
clearInterval(editDetectLoop);
|
|
clearInterval(editBrightInterval);
|
|
if (editStream) { editStream.getTracks().forEach(t => t.stop()); editStream = null; }
|
|
}
|
|
|
|
editBtnShutter.addEventListener('click', editCapture);
|
|
document.getElementById('editBtnRetake').addEventListener('click', editRetake);
|
|
document.getElementById('editBtnUseFoto').addEventListener('click', editUseFoto);
|
|
|
|
document.getElementById('btnSimpan').addEventListener('click', function() {
|
|
// Validate step 1 data again just in case
|
|
const name = document.getElementById('name').value.trim();
|
|
if(name === '') {
|
|
showCustomAlert('Incomplete Form', 'Full Name is required.', 'warning');
|
|
document.getElementById('step2-foto').style.display = 'none';
|
|
document.getElementById('step1-data').style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('btnSimpan');
|
|
const originalText = btn.innerHTML;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm mr-2" role="status" aria-hidden="true"></span>Saving...';
|
|
btn.disabled = true;
|
|
document.getElementById('btnBack').disabled = true;
|
|
|
|
const form = document.getElementById('editForm');
|
|
const formData = new FormData(form);
|
|
|
|
fetch('', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
// Success
|
|
document.getElementById('step2-foto').style.display = 'none';
|
|
document.getElementById('step3-success').style.display = 'block';
|
|
|
|
let countdown = 3;
|
|
const cText = document.getElementById('countdownText');
|
|
if(cText) cText.innerText = countdown;
|
|
setInterval(() => {
|
|
countdown--;
|
|
if(cText) cText.innerText = countdown;
|
|
if(countdown <= 0) {
|
|
if (window.Telegram && window.Telegram.WebApp) {
|
|
window.Telegram.WebApp.close();
|
|
}
|
|
window.close();
|
|
}
|
|
}, 1000);
|
|
|
|
} else {
|
|
showCustomAlert('Failed', data.message, 'error');
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
document.getElementById('btnBack').disabled = false;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showCustomAlert('Network Error', 'A connection error occurred.', 'error');
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
document.getElementById('btnBack').disabled = false;
|
|
});
|
|
});
|
|
|
|
// ─── SESSION EXPIRY TIMER ───
|
|
(function() {
|
|
var timerPill = document.getElementById('sessionTimerPill');
|
|
var timerText = document.getElementById('sessionTimerText');
|
|
var expiredOverlay = document.getElementById('sessionExpiredOverlay');
|
|
var sessionCompleted = false;
|
|
|
|
function updateSessionTimer() {
|
|
if (sessionCompleted) return;
|
|
var now = Math.floor(Date.now() / 1000);
|
|
var remaining = SESSION_EXPIRES_AT - now;
|
|
|
|
if (remaining <= 0) {
|
|
// Session expired - stop camera if active, hide ALL content
|
|
timerPill.style.display = 'none';
|
|
// Stop camera if it's open
|
|
if (typeof stopEditCamera === 'function') stopEditCamera();
|
|
// Hide all main content
|
|
var mainContent = document.querySelector('.auth-form-light');
|
|
if (mainContent) mainContent.style.display = 'none';
|
|
var cameraOverlay = document.getElementById('cameraOverlay');
|
|
if (cameraOverlay) cameraOverlay.style.display = 'none';
|
|
// Show expired overlay
|
|
expiredOverlay.style.display = 'flex';
|
|
// Auto-close after 3 seconds
|
|
setTimeout(function() {
|
|
if (window.Telegram && window.Telegram.WebApp) {
|
|
window.Telegram.WebApp.close();
|
|
}
|
|
window.close();
|
|
}, 3000);
|
|
return;
|
|
}
|
|
|
|
var mins = Math.floor(remaining / 60);
|
|
var secs = remaining % 60;
|
|
timerText.textContent = mins + ':' + (secs < 10 ? '0' : '') + secs;
|
|
|
|
if (remaining <= 60) {
|
|
timerPill.classList.add('urgent');
|
|
} else {
|
|
timerPill.classList.remove('urgent');
|
|
}
|
|
|
|
setTimeout(updateSessionTimer, 1000);
|
|
}
|
|
|
|
// Stop timer if user saves data successfully
|
|
var origStep3 = document.getElementById('step3-success');
|
|
if (origStep3) {
|
|
var observer = new MutationObserver(function() {
|
|
if (origStep3.style.display !== 'none') {
|
|
sessionCompleted = true;
|
|
timerPill.style.display = 'none';
|
|
}
|
|
});
|
|
observer.observe(origStep3, { attributes: true, attributeFilter: ['style'] });
|
|
}
|
|
|
|
updateSessionTimer();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|