chore: release v1.1.0

This commit is contained in:
dyzulk
2026-01-12 12:10:25 +07:00
parent 98a6f4304c
commit 8f62c68a69
6 changed files with 112 additions and 48 deletions

View File

@@ -584,7 +584,7 @@ footer {
transform: translateY(-50%);
width: 16px;
height: 16px;
opacity: 0.5;
opacity: 0.9;
pointer-events: none;
}

View File

@@ -62,7 +62,8 @@ const translations = {
check_not_found: "Voucher tidak ditemukan atau belum aktif.",
check_expired: "Voucher sudah kadaluarsa.",
check_valid_until: "Aktif sampai",
check_quota_remaining: "Sisa Kuota"
check_quota_remaining: "Sisa Kuota",
status_label: "STATUS"
},
en: {
lang_name: "English",
@@ -127,7 +128,8 @@ const translations = {
check_not_found: "Voucher not found or not active.",
check_expired: "Voucher has expired.",
check_valid_until: "Valid until",
check_quota_remaining: "Quota Remaining"
check_quota_remaining: "Quota Remaining",
status_label: "STATUS"
}
};

View File

@@ -24,13 +24,33 @@ function safeResume() {
function handleDecodedText(decodedText) {
console.log(`Scan result: ${decodedText}`);
// 1. Fast Path for CHECK MODE: Allow plain text and skip confirmation
if (activeScannerMode === 'check' || activeScannerMode === 'info') {
let code = decodedText;
try {
if (decodedText.startsWith('http://') || decodedText.startsWith('https://')) {
const url = new URL(decodedText);
code = url.searchParams.get('username') || decodedText;
}
} catch (e) {
code = decodedText;
}
if (code) {
console.log("Check/Info mode: Direct completion for", code);
closeQR();
checkVoucher(code); // Trigger the API check modal
return;
}
}
let username = "";
let password = "";
let isUnauthorized = false;
let blockReason = ""; // Track why it was blocked
scannedUrl = "";
// Check if result is a URL
// Check if result is a URL (STRICT for Login Mode)
try {
if (decodedText.startsWith('http://') || decodedText.startsWith('https://')) {
const url = new URL(decodedText);
@@ -75,15 +95,8 @@ function handleDecodedText(decodedText) {
blockReason = getTranslation('qr_err_parse');
}
// Fill inputs (only if authorized)
// Fill inputs (only if authorized - Login Mode)
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;

View File

@@ -5,10 +5,11 @@ function setMode(mode) {
const tabs = document.querySelectorAll('.tab-btn');
const loginBtn = document.getElementById('login-btn');
const checkBtn = document.getElementById('check-btn');
const trialContainer = document.getElementById('trial-container');
const form = document.login;
// Reset visibility
[voucherMode, memberMode, infoMode, loginBtn, checkBtn].forEach(el => {
[voucherMode, memberMode, infoMode, loginBtn, checkBtn, trialContainer].forEach(el => {
if (el) el.classList.add('hidden');
});
tabs.forEach(t => t.classList.remove('active'));
@@ -16,6 +17,7 @@ function setMode(mode) {
if (mode === 'voucher') {
if (voucherMode) voucherMode.classList.remove('hidden');
if (loginBtn) loginBtn.classList.remove('hidden');
if (trialContainer) trialContainer.classList.remove('hidden');
tabs[0].classList.add('active');
if (form) {
const code = document.getElementById('voucher-input').value;
@@ -25,6 +27,7 @@ function setMode(mode) {
} else if (mode === 'member') {
if (memberMode) memberMode.classList.remove('hidden');
if (loginBtn) loginBtn.classList.remove('hidden');
if (trialContainer) trialContainer.classList.remove('hidden');
tabs[1].classList.add('active');
if (form) {
form.username.value = document.getElementById('member-user').value;
@@ -33,6 +36,7 @@ function setMode(mode) {
} else if (mode === 'info') {
if (infoMode) infoMode.classList.remove('hidden');
if (checkBtn) checkBtn.classList.remove('hidden');
// trialContainer remains hidden in info mode
tabs[2].classList.add('active');
}
@@ -252,6 +256,36 @@ function checkVoucher(forceCode = null) {
.then(response => response.json())
.then(data => {
if (data.status === 'active') {
const lang = localStorage.getItem('twinpath_lang') || 'en';
let statusText = data.expired_at;
let statusColor = '#ff4d4d'; // Default red for expiration dates
let statusBg = 'rgba(255,77,77,0.1)';
let statusBorder = 'rgba(255,77,77,0.2)';
// Format logic
if (statusText === 'Active') {
statusColor = '#50e3c2'; // Green
statusBg = 'rgba(80, 227, 194, 0.1)';
statusBorder = 'rgba(80, 227, 194, 0.2)';
} else {
// Try to parse date: "jan/15/2026 00:37:47"
const dateParts = statusText.match(/([a-z]+)\/(\d+)\/(\d+)\s+(\d+:\d+:\d+)/i);
if (dateParts) {
try {
const [_, mStr, d, y, time] = dateParts;
const months = { 'jan': 0, 'feb': 1, 'mar': 2, 'apr': 3, 'may': 4, 'jun': 5, 'jul': 6, 'aug': 7, 'sep': 8, 'oct': 9, 'nov': 10, 'dec': 11 };
const dateObj = new Date(y, months[mStr.toLowerCase()], d);
// Set time components manually if needed or just use date
const [hh, mm, ss] = time.split(':');
dateObj.setHours(hh, mm, ss);
const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' };
const formattedDate = new Intl.DateTimeFormat(lang === 'id' ? 'id-ID' : 'en-US', options).format(dateObj);
statusText = formattedDate; // Just the date
} catch (e) { console.error("Date parse error", e); }
}
}
infoContent.innerHTML = `
<div class="confirm-item" style="margin-bottom: 12px; border-bottom: 1px solid var(--border); padding-bottom: 8px;">
<span class="confirm-label" data-i18n="user_label">${getTranslation('user_label')}</span>
@@ -269,9 +303,9 @@ function checkVoucher(forceCode = null) {
<div style="color: #fff; font-weight: 600;">${data.data_left}</div>
</div>
</div>
<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 class="confirm-label" style="color: #ff4d4d;">EXPIRED AT</div>
<div style="color: #ff4d4d; font-family: monospace; font-weight: bold;">${data.expired_at}</div>
<div style="margin-top: 15px; background: ${statusBg}; padding: 8px; border-radius: 4px; border: 1px solid ${statusBorder};">
<div class="confirm-label" style="color: ${statusColor};" data-i18n="status_label">STATUS</div>
<div style="color: ${statusColor}; font-family: monospace; font-weight: bold;">${statusText}</div>
</div>
</div>
`;
@@ -361,6 +395,16 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
});
// Handle Enter key for Voucher Check
const infoInput = document.getElementById('info-input');
if (infoInput) {
infoInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
checkVoucher(); // No form to prevent anymore
}
});
}
});
function togglePassword(inputId, button) {

View File

@@ -59,9 +59,12 @@
</div>
<!-- Login Form -->
<!-- Login Form (Voucher & Member Only) -->
<form name="login" action="$(link-login-only)" method="post" onsubmit="return doLogin()">
<input type="hidden" name="dst" value="$(link-orig)">
<input type="hidden" name="popup" value="true">
<!-- Hidden submit to capture Enter key on inputs -->
<input type="submit" style="display:none" />
<!-- Voucher Mode -->
<div id="voucher-mode">
@@ -96,38 +99,40 @@
</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 -->
<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="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(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;">
<span data-i18n="scan_btn">Scan QR Code</span>
</button>
$(if trial == 'yes')
<div style="text-align: center; margin-top: 0.5rem; font-size: 0.8rem; color: var(--fg-secondary);">
<span data-i18n="or_text">Or</span>
<button type="button" onclick="location.href='$(link-login-only)?dst=$(link-orig-esc)&amp;username=T-$(mac-esc)'" class="btn btn-outline" style="margin-top: 0.5rem" data-i18n="trial_btn">
Free Trial Access
</button>
</div>
$(endif)
</div>
</form>
<!-- Info/Check Mode (Outside Form) -->
<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 (Shared Grid) -->
<div style="display: grid; gap: 0.75rem;">
<!-- Login Button: Triggers Form Submit via JS -->
<button type="button" id="login-btn" class="btn btn-primary" data-i18n="login_voucher" onclick="if(doLogin()) document.login.submit()">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" 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;">
<span data-i18n="scan_btn">Scan QR Code</span>
</button>
$(if trial == 'yes')
<div id="trial-container" style="text-align: center; margin-top: 0.5rem; font-size: 0.8rem; color: var(--fg-secondary);">
<span data-i18n="or_text">Or</span>
<button type="button" onclick="location.href='$(link-login-only)?dst=$(link-orig-esc)&amp;username=T-$(mac-esc)'" class="btn btn-outline" style="margin-top: 0.5rem" data-i18n="trial_btn">
Free Trial Access
</button>
</div>
$(endif)
</div>
</div>
<!-- Pricing Section -->

View File

@@ -1 +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>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ededed" 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>

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 273 B