mirror of
https://github.com/dyzulk/twinpath-hotspot-themes.git
synced 2026-01-26 13:31:54 +07:00
feat: isolate voucher check ui and qr scanner modes
This commit is contained in:
@@ -664,10 +664,30 @@ footer {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
#qr-confirm-overlay.hidden {
|
#qr-confirm-overlay.hidden, .modal.hidden {
|
||||||
display: none;
|
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 {
|
.confirm-card {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|||||||
27
deploy.ps1
Normal file
27
deploy.ps1
Normal file
@@ -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
|
||||||
@@ -77,22 +77,25 @@ function handleDecodedText(decodedText) {
|
|||||||
|
|
||||||
// Fill inputs (only if authorized)
|
// Fill inputs (only if authorized)
|
||||||
if (!isUnauthorized && username) {
|
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 voucherInput = document.getElementById('voucher-input');
|
||||||
const passField = document.getElementById('voucher-pass');
|
const passField = document.getElementById('voucher-pass');
|
||||||
if (voucherInput) voucherInput.value = username;
|
if (voucherInput) voucherInput.value = username;
|
||||||
if (passField) passField.value = password || username;
|
if (passField) passField.value = password || username;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show confirmation overlay
|
// Show confirmation overlay (Login Mode only)
|
||||||
const overlay = document.getElementById('qr-confirm-overlay');
|
const overlay = document.getElementById('qr-confirm-overlay');
|
||||||
const confirmUser = document.getElementById('confirm-user');
|
const confirmUser = document.getElementById('confirm-user');
|
||||||
const connectBtn = document.querySelector('button[onclick="proceedSubmit()"]');
|
const connectBtn = document.querySelector('button[onclick="proceedSubmit()"]');
|
||||||
const confirmMsg = document.querySelector('[data-i18n="confirm_msg"]');
|
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 (overlay && confirmUser) {
|
||||||
if (isUnauthorized) {
|
if (isUnauthorized) {
|
||||||
confirmUser.innerHTML = `<span style="color: #ff4d4d;">Blocked: ${blockReason}</span>`;
|
confirmUser.innerHTML = `<span style="color: #ff4d4d;">Blocked: ${blockReason}</span>`;
|
||||||
@@ -101,15 +104,10 @@ function handleDecodedText(decodedText) {
|
|||||||
confirmUser.innerText = username;
|
confirmUser.innerText = username;
|
||||||
if (connectBtn) {
|
if (connectBtn) {
|
||||||
connectBtn.style.display = 'block';
|
connectBtn.style.display = 'block';
|
||||||
if (isInfoMode) {
|
|
||||||
connectBtn.innerText = getTranslation('check_btn');
|
|
||||||
if (confirmMsg) confirmMsg.innerText = getTranslation('info_label');
|
|
||||||
} else {
|
|
||||||
connectBtn.innerText = getTranslation('connect_btn');
|
connectBtn.innerText = getTranslation('connect_btn');
|
||||||
if (confirmMsg) confirmMsg.innerText = getTranslation('confirm_msg');
|
if (confirmMsg) confirmMsg.innerText = getTranslation('confirm_msg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
overlay.classList.remove('hidden');
|
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');
|
const modal = document.getElementById('qr-scanner-modal');
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
|
|
||||||
|
|||||||
107
js/script.js
107
js/script.js
@@ -45,6 +45,20 @@ function setMode(mode) {
|
|||||||
loginBtn.innerText = translations[lang][key];
|
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() {
|
function doLogin() {
|
||||||
@@ -187,10 +201,33 @@ function updateProgressBars() {
|
|||||||
const isExpiredTime = limitUptime > 0 && uptime >= limitUptime;
|
const isExpiredTime = limitUptime > 0 && uptime >= limitUptime;
|
||||||
const isExpiredQuota = limitBytes > 0 && bytesOut >= limitBytes;
|
const isExpiredQuota = limitBytes > 0 && bytesOut >= limitBytes;
|
||||||
|
|
||||||
if (isExpiredTime || isExpiredQuota) {
|
const timeRemainingContainer = document.querySelector('[data-i18n="time_left"]')?.parentElement?.querySelector('.value');
|
||||||
console.warn("User has reached limit!");
|
const quotaRemainingContainer = document.querySelector('[data-i18n="quota_left"]')?.parentElement?.querySelector('.value');
|
||||||
// We could add a label here, but MikroTik usually redirects/logs out automatically
|
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) {
|
function checkVoucher(forceCode = null) {
|
||||||
@@ -198,16 +235,13 @@ function checkVoucher(forceCode = null) {
|
|||||||
const code = forceCode || (input ? input.value.trim() : "");
|
const code = forceCode || (input ? input.value.trim() : "");
|
||||||
if (!code) return;
|
if (!code) return;
|
||||||
|
|
||||||
const overlay = document.getElementById('qr-confirm-overlay');
|
const infoModal = document.getElementById('voucher-info-modal');
|
||||||
const confirmUser = document.getElementById('confirm-user');
|
const infoContent = document.getElementById('voucher-info-content');
|
||||||
const confirmMsg = document.querySelector('[data-i18n="confirm_msg"]');
|
|
||||||
const connectBtn = document.querySelector('button[onclick="proceedSubmit()"]');
|
|
||||||
|
|
||||||
// Show loading state in overlay
|
// Show loading state
|
||||||
if (overlay && confirmUser) {
|
if (infoContent && infoModal) {
|
||||||
confirmUser.innerText = getTranslation('check_loading');
|
infoContent.innerHTML = `<div style="text-align:center; padding: 20px;">${getTranslation('check_loading')}</div>`;
|
||||||
if (connectBtn) connectBtn.style.display = 'none';
|
infoModal.classList.remove('hidden');
|
||||||
overlay.classList.remove('hidden');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mikhmonUrl = brandConfig.mikhmonUrl;
|
const mikhmonUrl = brandConfig.mikhmonUrl;
|
||||||
@@ -218,37 +252,52 @@ function checkVoucher(forceCode = null) {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.status === 'active') {
|
if (data.status === 'active') {
|
||||||
confirmUser.innerHTML = `
|
infoContent.innerHTML = `
|
||||||
<div style="font-size: 0.85rem; text-align: left; margin-top: 5px;">
|
<div class="confirm-item" style="margin-bottom: 12px; border-bottom: 1px solid var(--border); padding-bottom: 8px;">
|
||||||
<div style="margin-bottom: 8px; border-bottom: 1px solid #333; padding-bottom: 4px; color: #50e3c2; font-weight: bold;">${data.profile}</div>
|
<span class="confirm-label" data-i18n="user_label">${getTranslation('user_label')}</span>
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
<span class="confirm-value">${data.user}</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.85rem; text-align: left;">
|
||||||
|
<div style="margin-bottom: 12px; color: #50e3c2; font-weight: bold; font-family: var(--font-mono);">${data.profile}</div>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size: 0.65rem; color: #888;">UPTIME</div>
|
<div class="confirm-label">UPTIME</div>
|
||||||
<div style="color: #fff;">${data.uptime}</div>
|
<div style="color: #fff; font-weight: 600;">${data.uptime}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size: 0.65rem; color: #888;">QUOTA</div>
|
<div class="confirm-label">QUOTA</div>
|
||||||
<div style="color: #fff;">${data.data_left}</div>
|
<div style="color: #fff; font-weight: 600;">${data.data_left}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 10px;">
|
<div style="margin-top: 15px; background: rgba(255,77,77,0.1); padding: 8px; border-radius: 4px; border: 1px solid rgba(255,77,77,0.2);">
|
||||||
<div style="font-size: 0.65rem; color: #888;">EXPIRED AT</div>
|
<div class="confirm-label" style="color: #ff4d4d;">EXPIRED AT</div>
|
||||||
<div style="color: #ff4d4d; font-family: monospace;">${data.expired_at}</div>
|
<div style="color: #ff4d4d; font-family: monospace; font-weight: bold;">${data.expired_at}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
if (confirmMsg) confirmMsg.innerText = getTranslation('success_title');
|
|
||||||
} else if (data.status === 'expired') {
|
} else if (data.status === 'expired') {
|
||||||
confirmUser.innerHTML = `<span style="color: #ff4d4d;">${getTranslation('check_expired')}</span>`;
|
infoContent.innerHTML = `
|
||||||
if (confirmMsg) confirmMsg.innerText = getTranslation('failed_title');
|
<div style="text-align:center; padding: 20px;">
|
||||||
|
<span style="color: #ff4d4d; font-weight: bold; font-size: 1.1rem;">${getTranslation('check_expired')}</span>
|
||||||
|
<div style="font-size: 0.8rem; color: #888; margin-top: 5px;">Reason: ${data.reason || 'Unknown'}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
} else {
|
} else {
|
||||||
confirmUser.innerHTML = `<span style="color: #aaa;">${getTranslation('check_not_found')}</span>`;
|
infoContent.innerHTML = `
|
||||||
if (confirmMsg) confirmMsg.innerText = getTranslation('failed_title');
|
<div style="text-align:center; padding: 20px;">
|
||||||
|
<span style="color: #aaa; font-weight: bold;">${getTranslation('check_not_found')}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error("AJAX Error:", err);
|
console.error("AJAX Error:", err);
|
||||||
confirmUser.innerHTML = `<span style="color: #ff4d4d;">CORS/Connection Error</span><br><small style="font-size: 10px; color: #666;">Check Mikhmon Address & CORS</small>`;
|
infoContent.innerHTML = `
|
||||||
|
<div style="text-align:center; padding: 20px; color: #ff4d4d;">
|
||||||
|
<strong>Connection Error</strong><br>
|
||||||
|
<small style="font-size: 10px; color: #888;">Cannot reach Mikhmon API. Check CORS settings.</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
login.html
16
login.html
@@ -113,7 +113,7 @@
|
|||||||
<button type="submit" id="login-btn" class="btn btn-primary" data-i18n="login_voucher">Use Voucher</button>
|
<button type="submit" id="login-btn" class="btn btn-primary" data-i18n="login_voucher">Use Voucher</button>
|
||||||
<button type="button" id="check-btn" class="btn btn-primary hidden" onclick="checkVoucher()" data-i18n="check_btn">Check Status</button>
|
<button type="button" id="check-btn" class="btn btn-primary hidden" onclick="checkVoucher()" data-i18n="check_btn">Check Status</button>
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline" id="scan-btn" onclick="openQR()">
|
<button type="button" class="btn btn-outline" id="scan-btn" onclick="openQR(getActiveMode())">
|
||||||
<img src="svg/scan-line.svg" width="16" height="16" alt="" data-asset="icon_scan" style="margin-right: 0.5rem; vertical-align: text-bottom;">
|
<img src="svg/scan-line.svg" width="16" height="16" alt="" data-asset="icon_scan" style="margin-right: 0.5rem; vertical-align: text-bottom;">
|
||||||
<span data-i18n="scan_btn">Scan QR Code</span>
|
<span data-i18n="scan_btn">Scan QR Code</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -207,6 +207,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Voucher Info Modal (Dedicated for results) -->
|
||||||
|
<div id="voucher-info-modal" class="modal hidden">
|
||||||
|
<div class="modal-content confirm-card">
|
||||||
|
<button class="close-modal" onclick="closeVoucherInfo()">×</button>
|
||||||
|
<h3 data-i18n="info_title">Voucher Details</h3>
|
||||||
|
<div id="voucher-info-content" class="confirm-details">
|
||||||
|
<!-- Dynamic content here -->
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 1.5rem;">
|
||||||
|
<button type="button" class="btn btn-primary" style="width: 100%" onclick="closeVoucherInfo()">OK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Hidden element for file scanning -->
|
<!-- Hidden element for file scanning -->
|
||||||
<div id="qr-file-reader" style="display:none"></div>
|
<div id="qr-file-reader" style="display:none"></div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user