From 98a6f4304c530eab5864e027585e518463f8bf06 Mon Sep 17 00:00:00 2001 From: dyzulk <66510723+dyzulk@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:47:22 +0700 Subject: [PATCH] feat: isolate voucher check ui and qr scanner modes --- css/style.css | 22 +++++++++- deploy.ps1 | 27 ++++++++++++ js/qr-scanner.js | 32 ++++++++------ js/script.js | 107 ++++++++++++++++++++++++++++++++++------------- login.html | 16 ++++++- 5 files changed, 160 insertions(+), 44 deletions(-) create mode 100644 deploy.ps1 diff --git a/css/style.css b/css/style.css index 6ce5927..c354fc2 100644 --- a/css/style.css +++ b/css/style.css @@ -664,10 +664,30 @@ footer { z-index: 10; } -#qr-confirm-overlay.hidden { +#qr-confirm-overlay.hidden, .modal.hidden { display: none; } +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.85); + backdrop-filter: blur(8px); + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + z-index: 2000; +} + +.modal-content { + width: 100%; + max-width: 320px; +} + .confirm-card { background: var(--bg-secondary); border: 1px solid var(--border); diff --git a/deploy.ps1 b/deploy.ps1 new file mode 100644 index 0000000..d4652ba --- /dev/null +++ b/deploy.ps1 @@ -0,0 +1,27 @@ +# Deploy Script for Twinpath Hotspot +# This script copies production files to a 'dist' folder for easy MikroTik upload. + +$Source = Get-Location +$Dest = Join-Path (Split-Path $Source -Parent) "dist\hotspot" + +# Files/Folders to exclude from production +$ExcludeFiles = @("README.md", "LICENSE", "deploy.ps1", ".gitignore") +$ExcludeDirs = @(".git") + +Write-Host "🚀 Preparing production folder: $Dest" -ForegroundColor Cyan + +# Create destination if it doesn't exist +if (!(Test-Path $Dest)) { + New-Item -ItemType Directory -Path $Dest -Force | Out-Null +} + +# Use Robocopy for efficient syncing (it's faster and handles exclusions well) +# /MIR: Mirror directory tree +# /XD: Exclude Directories +# /XF: Exclude Files +# /NFL: No File List (cleaner output) +# /NDL: No Directory List (cleaner output) +robocopy $Source $Dest /MIR /XD $ExcludeDirs /XF $ExcludeFiles /R:3 /W:5 /NFL /NDL /NP + +$TargetName = Split-Path $Dest -Leaf +Write-Host "✅ Deployment ready! You can now drag & drop 'dist\$TargetName' to MikroTik." -ForegroundColor Green diff --git a/js/qr-scanner.js b/js/qr-scanner.js index 6591379..5540024 100644 --- a/js/qr-scanner.js +++ b/js/qr-scanner.js @@ -77,21 +77,24 @@ function handleDecodedText(decodedText) { // Fill inputs (only if authorized) if (!isUnauthorized && username) { + if (activeScannerMode === 'check') { + console.log("Check mode: Direct fetch for", username); + closeQR(); + checkVoucher(username); + return; + } + const voucherInput = document.getElementById('voucher-input'); const passField = document.getElementById('voucher-pass'); if (voucherInput) voucherInput.value = username; if (passField) passField.value = password || username; } - // Show confirmation overlay + // Show confirmation overlay (Login Mode only) const overlay = document.getElementById('qr-confirm-overlay'); const confirmUser = document.getElementById('confirm-user'); const connectBtn = document.querySelector('button[onclick="proceedSubmit()"]'); const confirmMsg = document.querySelector('[data-i18n="confirm_msg"]'); - - // Determine mode from active tab - const activeTab = document.querySelector('.tab-btn.active'); - const isInfoMode = activeTab && activeTab.onclick.toString().includes('info'); if (overlay && confirmUser) { if (isUnauthorized) { @@ -101,13 +104,8 @@ function handleDecodedText(decodedText) { confirmUser.innerText = username; if (connectBtn) { connectBtn.style.display = 'block'; - if (isInfoMode) { - connectBtn.innerText = getTranslation('check_btn'); - if (confirmMsg) confirmMsg.innerText = getTranslation('info_label'); - } else { - connectBtn.innerText = getTranslation('connect_btn'); - if (confirmMsg) confirmMsg.innerText = getTranslation('confirm_msg'); - } + connectBtn.innerText = getTranslation('connect_btn'); + if (confirmMsg) confirmMsg.innerText = getTranslation('confirm_msg'); } } overlay.classList.remove('hidden'); @@ -237,7 +235,15 @@ function scanFromFile(event) { -function openQR() { +let activeScannerMode = 'login'; // 'login' or 'check' + +async function openQR(mode = 'login') { + activeScannerMode = mode; + console.log(`Opening QR Scanner in ${mode} mode`); + + // Hide confirmation overlay when opening scanner to prevent legacy results from showing + document.getElementById('qr-confirm-overlay').classList.add('hidden'); + const modal = document.getElementById('qr-scanner-modal'); modal.style.display = 'flex'; diff --git a/js/script.js b/js/script.js index 457dd00..4bd3c06 100644 --- a/js/script.js +++ b/js/script.js @@ -45,6 +45,20 @@ function setMode(mode) { loginBtn.innerText = translations[lang][key]; } } + + // Close any open modals when switching tabs + if (typeof closeQR === 'function') closeQR(); + if (typeof closeVoucherInfo === 'function') closeVoucherInfo(); +} + +function getActiveMode() { + const tabs = document.querySelectorAll('.tab-btn'); + if (tabs.length >= 3) { + if (tabs[0].classList.contains('active')) return 'voucher'; + if (tabs[1].classList.contains('active')) return 'member'; + if (tabs[2].classList.contains('active')) return 'info'; + } + return 'voucher'; } function doLogin() { @@ -187,10 +201,33 @@ function updateProgressBars() { const isExpiredTime = limitUptime > 0 && uptime >= limitUptime; const isExpiredQuota = limitBytes > 0 && bytesOut >= limitBytes; - if (isExpiredTime || isExpiredQuota) { - console.warn("User has reached limit!"); - // We could add a label here, but MikroTik usually redirects/logs out automatically + const timeRemainingContainer = document.querySelector('[data-i18n="time_left"]')?.parentElement?.querySelector('.value'); + const quotaRemainingContainer = document.querySelector('[data-i18n="quota_left"]')?.parentElement?.querySelector('.value'); + const lang = localStorage.getItem('twinpath_lang') || 'en'; + const unlimitedLabel = (translations[lang] && translations[lang]['unlimited']) || 'Unlimited'; + + if (timeRemainingContainer) { + if (limitUptime === 0) { + timeRemainingContainer.innerText = unlimitedLabel; + } else if (isExpiredTime) { + timeRemainingContainer.innerText = "Reached Limit"; // Or add a translation key + timeRemainingContainer.style.color = "#ff4d4d"; + } } + + if (quotaRemainingContainer) { + if (limitBytes === 0) { + quotaRemainingContainer.innerText = unlimitedLabel; + } else if (isExpiredQuota) { + quotaRemainingContainer.innerText = "Reached Limit"; + quotaRemainingContainer.style.color = "#ff4d4d"; + } + } +} + +function closeVoucherInfo() { + const modal = document.getElementById('voucher-info-modal'); + if (modal) modal.classList.add('hidden'); } function checkVoucher(forceCode = null) { @@ -198,16 +235,13 @@ function checkVoucher(forceCode = null) { const code = forceCode || (input ? input.value.trim() : ""); if (!code) return; - const overlay = document.getElementById('qr-confirm-overlay'); - const confirmUser = document.getElementById('confirm-user'); - const confirmMsg = document.querySelector('[data-i18n="confirm_msg"]'); - const connectBtn = document.querySelector('button[onclick="proceedSubmit()"]'); + const infoModal = document.getElementById('voucher-info-modal'); + const infoContent = document.getElementById('voucher-info-content'); - // Show loading state in overlay - if (overlay && confirmUser) { - confirmUser.innerText = getTranslation('check_loading'); - if (connectBtn) connectBtn.style.display = 'none'; - overlay.classList.remove('hidden'); + // Show loading state + if (infoContent && infoModal) { + infoContent.innerHTML = `