mirror of
https://github.com/dyzulk/twinpath-hotspot-themes.git
synced 2026-01-26 13:31:54 +07:00
Feature: Implement Voucher Check (Opsi A) with 3-tab UI
This commit is contained in:
@@ -2,12 +2,12 @@ const brandConfig = {
|
|||||||
brandName: "TwinpathNet",
|
brandName: "TwinpathNet",
|
||||||
portalUrl: "http://welcome.dyzulk.com/login",
|
portalUrl: "http://welcome.dyzulk.com/login",
|
||||||
allowedDomains: [
|
allowedDomains: [
|
||||||
"welcome.dyzulk.com", // Main Portal
|
"welcome.dyzulk.com"
|
||||||
"10.0.0.1", // Default Gateway (Local IP)
|
|
||||||
"dyzulk.com" // Custom Domain
|
|
||||||
],
|
],
|
||||||
creditName: "dyzulk.com",
|
creditName: "dyzulk.com",
|
||||||
creditUrl: "https://dyzulk.com",
|
creditUrl: "https://dyzulk.com",
|
||||||
|
mikhmonUrl: "https://mikhmon.dyzulk.com",
|
||||||
|
mikhmonSession: "Twinpath-Net",
|
||||||
assets: {
|
assets: {
|
||||||
logo: "img/logo-twinpath.svg",
|
logo: "img/logo-twinpath.svg",
|
||||||
icon_ticket: "svg/ticket.svg",
|
icon_ticket: "svg/ticket.svg",
|
||||||
@@ -23,7 +23,8 @@ const brandConfig = {
|
|||||||
icon_clock: "svg/clock.svg",
|
icon_clock: "svg/clock.svg",
|
||||||
icon_upload: "svg/upload-cloud.svg",
|
icon_upload: "svg/upload-cloud.svg",
|
||||||
icon_download: "svg/download-cloud.svg",
|
icon_download: "svg/download-cloud.svg",
|
||||||
icon_wifi: "svg/wifi.svg"
|
icon_wifi: "svg/wifi.svg",
|
||||||
|
icon_search: "svg/search.svg"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,15 @@ const translations = {
|
|||||||
qr_err_unauthorized: "Domain Tidak Sah",
|
qr_err_unauthorized: "Domain Tidak Sah",
|
||||||
qr_err_invalid_url: "URL Login Hotspot Tidak Sah",
|
qr_err_invalid_url: "URL Login Hotspot Tidak Sah",
|
||||||
qr_err_invalid_content: "Konten Tidak Sah (Hanya URL)",
|
qr_err_invalid_content: "Konten Tidak Sah (Hanya URL)",
|
||||||
qr_err_parse: "Gagal Membaca QR"
|
qr_err_parse: "Gagal Membaca QR",
|
||||||
|
tab_info: "Info",
|
||||||
|
info_label: "Cek Masa Aktif",
|
||||||
|
check_btn: "Cek Status",
|
||||||
|
check_loading: "Mengecek voucher...",
|
||||||
|
check_not_found: "Voucher tidak ditemukan atau belum aktif.",
|
||||||
|
check_expired: "Voucher sudah kadaluarsa.",
|
||||||
|
check_valid_until: "Aktif sampai",
|
||||||
|
check_quota_remaining: "Sisa Kuota"
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
lang_name: "English",
|
lang_name: "English",
|
||||||
@@ -111,7 +119,15 @@ const translations = {
|
|||||||
qr_err_unauthorized: "Unauthorized Domain",
|
qr_err_unauthorized: "Unauthorized Domain",
|
||||||
qr_err_invalid_url: "Invalid Hotspot Login URL",
|
qr_err_invalid_url: "Invalid Hotspot Login URL",
|
||||||
qr_err_invalid_content: "Invalid Content (Only URL allowed)",
|
qr_err_invalid_content: "Invalid Content (Only URL allowed)",
|
||||||
qr_err_parse: "QR Parse Error"
|
qr_err_parse: "QR Parse Error",
|
||||||
|
tab_info: "Info",
|
||||||
|
info_label: "Check Validity",
|
||||||
|
check_btn: "Check Status",
|
||||||
|
check_loading: "Checking voucher...",
|
||||||
|
check_not_found: "Voucher not found or not active.",
|
||||||
|
check_expired: "Voucher has expired.",
|
||||||
|
check_valid_until: "Valid until",
|
||||||
|
check_quota_remaining: "Quota Remaining"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ function handleDecodedText(decodedText) {
|
|||||||
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"]');
|
||||||
|
|
||||||
|
// 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) {
|
||||||
@@ -94,7 +99,16 @@ function handleDecodedText(decodedText) {
|
|||||||
if (connectBtn) connectBtn.style.display = 'none';
|
if (connectBtn) connectBtn.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
confirmUser.innerText = username;
|
confirmUser.innerText = username;
|
||||||
if (connectBtn) connectBtn.style.display = 'block';
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
overlay.classList.remove('hidden');
|
overlay.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
@@ -111,6 +125,17 @@ function cancelConfirm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function proceedSubmit() {
|
function proceedSubmit() {
|
||||||
|
// Determine mode from active tab
|
||||||
|
const activeTab = document.querySelector('.tab-btn.active');
|
||||||
|
const isInfoMode = activeTab && activeTab.onclick.toString().includes('info');
|
||||||
|
|
||||||
|
if (isInfoMode) {
|
||||||
|
const username = document.getElementById('confirm-user').innerText;
|
||||||
|
checkVoucher(username);
|
||||||
|
closeQR();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If it's a URL, redirect directly
|
// If it's a URL, redirect directly
|
||||||
if (scannedUrl) {
|
if (scannedUrl) {
|
||||||
console.log("Redirecting to scanned URL:", scannedUrl);
|
console.log("Redirecting to scanned URL:", scannedUrl);
|
||||||
|
|||||||
100
js/script.js
100
js/script.js
@@ -1,38 +1,43 @@
|
|||||||
function setMode(mode) {
|
function setMode(mode) {
|
||||||
const voucherMode = document.getElementById('voucher-mode');
|
const voucherMode = document.getElementById('voucher-mode');
|
||||||
const memberMode = document.getElementById('member-mode');
|
const memberMode = document.getElementById('member-mode');
|
||||||
const voucherTab = document.querySelector('.tab-btn:nth-child(1)');
|
const infoMode = document.getElementById('info-mode');
|
||||||
const memberTab = document.querySelector('.tab-btn:nth-child(2)');
|
const tabs = document.querySelectorAll('.tab-btn');
|
||||||
|
const loginBtn = document.getElementById('login-btn');
|
||||||
|
const checkBtn = document.getElementById('check-btn');
|
||||||
const form = document.login;
|
const form = document.login;
|
||||||
|
|
||||||
if (mode === 'voucher') {
|
// Reset visibility
|
||||||
voucherMode.classList.remove('hidden');
|
[voucherMode, memberMode, infoMode, loginBtn, checkBtn].forEach(el => {
|
||||||
memberMode.classList.add('hidden');
|
if (el) el.classList.add('hidden');
|
||||||
voucherTab.classList.add('active');
|
});
|
||||||
memberTab.classList.remove('active');
|
tabs.forEach(t => t.classList.remove('active'));
|
||||||
|
|
||||||
// Sync to form immediately
|
if (mode === 'voucher') {
|
||||||
|
if (voucherMode) voucherMode.classList.remove('hidden');
|
||||||
|
if (loginBtn) loginBtn.classList.remove('hidden');
|
||||||
|
tabs[0].classList.add('active');
|
||||||
if (form) {
|
if (form) {
|
||||||
const code = document.getElementById('voucher-input').value;
|
const code = document.getElementById('voucher-input').value;
|
||||||
form.username.value = code;
|
form.username.value = code;
|
||||||
form.password.value = document.getElementById('voucher-pass').value || code;
|
form.password.value = document.getElementById('voucher-pass').value || code;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (mode === 'member') {
|
||||||
voucherMode.classList.add('hidden');
|
if (memberMode) memberMode.classList.remove('hidden');
|
||||||
memberMode.classList.remove('hidden');
|
if (loginBtn) loginBtn.classList.remove('hidden');
|
||||||
voucherTab.classList.remove('active');
|
tabs[1].classList.add('active');
|
||||||
memberTab.classList.add('active');
|
|
||||||
|
|
||||||
// Sync to form immediately
|
|
||||||
if (form) {
|
if (form) {
|
||||||
form.username.value = document.getElementById('member-user').value;
|
form.username.value = document.getElementById('member-user').value;
|
||||||
form.password.value = document.getElementById('member-pass').value;
|
form.password.value = document.getElementById('member-pass').value;
|
||||||
}
|
}
|
||||||
|
} else if (mode === 'info') {
|
||||||
|
if (infoMode) infoMode.classList.remove('hidden');
|
||||||
|
if (checkBtn) checkBtn.classList.remove('hidden');
|
||||||
|
tabs[2].classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update login button text based on mode
|
// Update login button text based on mode (only for voucher/member)
|
||||||
const loginBtn = document.getElementById('login-btn');
|
if (loginBtn && (mode === 'voucher' || mode === 'member')) {
|
||||||
if (loginBtn) {
|
|
||||||
const lang = localStorage.getItem('twinpath_lang') || 'en';
|
const lang = localStorage.getItem('twinpath_lang') || 'en';
|
||||||
const key = mode === 'voucher' ? 'login_voucher' : 'login_member';
|
const key = mode === 'voucher' ? 'login_voucher' : 'login_member';
|
||||||
loginBtn.setAttribute('data-i18n', key);
|
loginBtn.setAttribute('data-i18n', key);
|
||||||
@@ -176,6 +181,65 @@ function updateProgressBars() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkVoucher(forceCode = null) {
|
||||||
|
const input = document.getElementById('info-input');
|
||||||
|
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()"]');
|
||||||
|
|
||||||
|
// Show loading state in overlay
|
||||||
|
if (overlay && confirmUser) {
|
||||||
|
confirmUser.innerText = getTranslation('check_loading');
|
||||||
|
if (connectBtn) connectBtn.style.display = 'none';
|
||||||
|
overlay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mikhmonUrl = brandConfig.mikhmonUrl;
|
||||||
|
const session = brandConfig.mikhmonSession;
|
||||||
|
const url = `${mikhmonUrl}/status/index.php?session=${session}&nama=${code}&json=true`;
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'active') {
|
||||||
|
confirmUser.innerHTML = `
|
||||||
|
<div style="font-size: 0.85rem; text-align: left; margin-top: 5px;">
|
||||||
|
<div style="margin-bottom: 8px; border-bottom: 1px solid #333; padding-bottom: 4px; color: #50e3c2; font-weight: bold;">${data.profile}</div>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||||
|
<div>
|
||||||
|
<div style="font-size: 0.65rem; color: #888;">UPTIME</div>
|
||||||
|
<div style="color: #fff;">${data.uptime}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size: 0.65rem; color: #888;">QUOTA</div>
|
||||||
|
<div style="color: #fff;">${data.data_left}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<div style="font-size: 0.65rem; color: #888;">EXPIRED AT</div>
|
||||||
|
<div style="color: #ff4d4d; font-family: monospace;">${data.expired_at}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
if (confirmMsg) confirmMsg.innerText = getTranslation('success_title');
|
||||||
|
} else if (data.status === 'expired') {
|
||||||
|
confirmUser.innerHTML = `<span style="color: #ff4d4d;">${getTranslation('check_expired')}</span>`;
|
||||||
|
if (confirmMsg) confirmMsg.innerText = getTranslation('failed_title');
|
||||||
|
} else {
|
||||||
|
confirmUser.innerHTML = `<span style="color: #aaa;">${getTranslation('check_not_found')}</span>`;
|
||||||
|
if (confirmMsg) confirmMsg.innerText = getTranslation('failed_title');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(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>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function initDashboard() {
|
function initDashboard() {
|
||||||
// 1. Set Greetings
|
// 1. Set Greetings
|
||||||
const greetingEl = document.getElementById('greeting-text');
|
const greetingEl = document.getElementById('greeting-text');
|
||||||
|
|||||||
17
login.html
17
login.html
@@ -53,6 +53,9 @@
|
|||||||
<div class="tab-btn" onclick="setMode('member')">
|
<div class="tab-btn" onclick="setMode('member')">
|
||||||
<img src="svg/user.svg" alt="" data-asset="icon_user"> <span data-i18n="tab_member">Member</span>
|
<img src="svg/user.svg" alt="" data-asset="icon_user"> <span data-i18n="tab_member">Member</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tab-btn" onclick="setMode('info')">
|
||||||
|
<img src="svg/clock.svg" alt="" data-asset="icon_clock"> <span data-i18n="tab_info">Info</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Login Form -->
|
<!-- Login Form -->
|
||||||
@@ -94,11 +97,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Info/Check Mode -->
|
||||||
|
<div id="info-mode" class="hidden">
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label" data-i18n="info_label">Check Validity</label>
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<img src="svg/search.svg" class="input-icon-img" alt="" data-asset="icon_search">
|
||||||
|
<input type="text" id="info-input" class="input-field input-with-icon" data-i18n="voucher_placeholder" placeholder="Enter code to check...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div style="display: grid; gap: 0.75rem;">
|
<div style="display: grid; gap: 0.75rem;">
|
||||||
<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" class="btn btn-outline" onclick="openQR()">
|
<button type="button" class="btn btn-outline" id="scan-btn" onclick="openQR()">
|
||||||
<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>
|
||||||
|
|||||||
1
svg/search.svg
Normal file
1
svg/search.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||||
|
After Width: | Height: | Size: 278 B |
Reference in New Issue
Block a user