Полный перевод на русский язык
This commit is contained in:
@@ -414,7 +414,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- JS -->
|
<!-- JS -->
|
||||||
<script src="js/app.js?v=4"></script>
|
<script src="js/app.js?v=5"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -81,7 +81,72 @@ const translations = {
|
|||||||
status_inactive: "Inactive",
|
status_inactive: "Inactive",
|
||||||
status_expired: "Expired",
|
status_expired: "Expired",
|
||||||
status_no_sub: "No active subscription",
|
status_no_sub: "No active subscription",
|
||||||
unit_gb: "GB"
|
unit_gb: "GB",
|
||||||
|
|
||||||
|
// New Keys
|
||||||
|
err_plans_load: "Failed to load plans.",
|
||||||
|
err_open_tg: "Open in Telegram",
|
||||||
|
btn_creating: "Creating..",
|
||||||
|
plan_unlimited: "Unlimited",
|
||||||
|
unit_days: "Days",
|
||||||
|
msg_paid: "Successful Payment!",
|
||||||
|
err_network: "Network error",
|
||||||
|
msg_link_copied: "Link copied!",
|
||||||
|
err_no_sub: "No subscription found.",
|
||||||
|
|
||||||
|
modal_dl_title: "Download App",
|
||||||
|
modal_dl_subtitle: "Choose a client for your device:",
|
||||||
|
btn_dl_for: "Download for",
|
||||||
|
|
||||||
|
msg_promo_applied: "Promo Applied! {0}% OFF.",
|
||||||
|
err_promo_invalid: "Invalid Promo Code",
|
||||||
|
err_promo_check: "Error checking promo",
|
||||||
|
|
||||||
|
about_slogan: "Premium V2Ray service.<br>Fast, Secure, Reliable.",
|
||||||
|
|
||||||
|
msg_support_sent: "Message sent! Admin will contact you.",
|
||||||
|
err_send_fail: "Failed to send.",
|
||||||
|
|
||||||
|
adm_no_users: "No users found",
|
||||||
|
confirm_toggle: "Change user active/disabled state?",
|
||||||
|
confirm_reset: "Reset used traffic to zero?",
|
||||||
|
confirm_delete_sub: "Are you sure? User will lose VPN access immediately.",
|
||||||
|
title_add_days: "Add Days",
|
||||||
|
subtitle_add_days: "Enter days to ADD (negative to subtract):",
|
||||||
|
btn_apply: "Apply",
|
||||||
|
title_set_limit: "Set Traffic Limit",
|
||||||
|
subtitle_set_limit: "Enter new limit in GB (0 for ∞):",
|
||||||
|
title_set_exp: "Set Expiration",
|
||||||
|
subtitle_set_exp: "Days from NOW (0 = expire, 36500 = ∞):",
|
||||||
|
|
||||||
|
confirm_broadcast: "Send this message to ALL users?",
|
||||||
|
msg_broadcast_sent: "Broadcast sent to {0} users!",
|
||||||
|
|
||||||
|
title_new_promo: "New Promo",
|
||||||
|
ph_code: "CODE (e.g. SUMMER2024)",
|
||||||
|
ph_discount: "Discount %",
|
||||||
|
ph_uses: "Uses Limit",
|
||||||
|
ph_days: "Validity Days",
|
||||||
|
ph_bonus: "Bonus Days",
|
||||||
|
lbl_unlim_usage: "Unlimited Usage",
|
||||||
|
lbl_sticky: "Permanent (Sticky)",
|
||||||
|
note_sticky: "* Sticky promo locks the discount for the user forever.",
|
||||||
|
btn_create_promo: "Create Promo",
|
||||||
|
msg_promo_created: "Promo Created!",
|
||||||
|
err_promo_create: "Error creating promo",
|
||||||
|
msg_promo_deleted: "Promo deleted!",
|
||||||
|
confirm_delete_promo: "Are you sure you want to delete promo code <b>{0}</b>?",
|
||||||
|
|
||||||
|
app_desc_sleek: "Sleek & Fast",
|
||||||
|
app_desc_premium: "Premium Choice",
|
||||||
|
app_desc_standard: "Standard Client",
|
||||||
|
app_desc_modern: "Powerful & Modern",
|
||||||
|
app_desc_aio: "Universal All-in-One",
|
||||||
|
app_desc_opensource: "Powerful & Open Source",
|
||||||
|
app_desc_simple: "Simple & Modern",
|
||||||
|
app_desc_classic: "Classic Client",
|
||||||
|
app_desc_native: "Native M1/M2 Support",
|
||||||
|
app_desc_elegant: "Elegant Desktop Client"
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
nav_home: "Главная",
|
nav_home: "Главная",
|
||||||
@@ -153,7 +218,72 @@ const translations = {
|
|||||||
status_inactive: "Неактивна",
|
status_inactive: "Неактивна",
|
||||||
status_expired: "Истекла",
|
status_expired: "Истекла",
|
||||||
status_no_sub: "Нет активной подписки",
|
status_no_sub: "Нет активной подписки",
|
||||||
unit_gb: "ГБ"
|
unit_gb: "ГБ",
|
||||||
|
|
||||||
|
// New Keys RU
|
||||||
|
err_plans_load: "Не удалось загрузить планы.",
|
||||||
|
err_open_tg: "Откройте в Telegram",
|
||||||
|
btn_creating: "Создание..",
|
||||||
|
plan_unlimited: "Безлимит",
|
||||||
|
unit_days: "Дней",
|
||||||
|
msg_paid: "Успешная оплата!",
|
||||||
|
err_network: "Ошибка сети",
|
||||||
|
msg_link_copied: "Ссылка скопирована!",
|
||||||
|
err_no_sub: "Подписка не найдена.",
|
||||||
|
|
||||||
|
modal_dl_title: "Скачать приложение",
|
||||||
|
modal_dl_subtitle: "Выберите клиент для устройства:",
|
||||||
|
btn_dl_for: "Скачать для",
|
||||||
|
|
||||||
|
msg_promo_applied: "Промо применено! Скидка {0}%.",
|
||||||
|
err_promo_invalid: "Неверный промокод",
|
||||||
|
err_promo_check: "Ошибка проверки промо",
|
||||||
|
|
||||||
|
about_slogan: "Премиум V2Ray сервис.<br>Быстро, Надежно, Безопасно.",
|
||||||
|
|
||||||
|
msg_support_sent: "Отправлено! Админ свяжется с вами.",
|
||||||
|
err_send_fail: "Ошибка отправки.",
|
||||||
|
|
||||||
|
adm_no_users: "Пользователи не найдены",
|
||||||
|
confirm_toggle: "Изменить статус (вкл/выкл)?",
|
||||||
|
confirm_reset: "Сбросить трафик в ноль?",
|
||||||
|
confirm_delete_sub: "Удалить подписку? Пользователь потеряет доступ.",
|
||||||
|
title_add_days: "Добавить дни",
|
||||||
|
subtitle_add_days: "Дней для добавления (отрицат. = убавить):",
|
||||||
|
btn_apply: "Применить",
|
||||||
|
title_set_limit: "Установить лимит",
|
||||||
|
subtitle_set_limit: "Лимит в ГБ (0 = безлимит):",
|
||||||
|
title_set_exp: "Срок действия",
|
||||||
|
subtitle_set_exp: "Дней от СЕЙЧАС (0 = истекло, 36500 = ∞):",
|
||||||
|
|
||||||
|
confirm_broadcast: "Отправить сообщение ВСЕМ пользователям?",
|
||||||
|
msg_broadcast_sent: "Рассылка отправлена {0} пользователям!",
|
||||||
|
|
||||||
|
title_new_promo: "Новый Промокод",
|
||||||
|
ph_code: "КОД (напр. SUMMER2024)",
|
||||||
|
ph_discount: "Скидка %",
|
||||||
|
ph_uses: "Лимит испол.",
|
||||||
|
ph_days: "Дней действия",
|
||||||
|
ph_bonus: "Бонусные дни",
|
||||||
|
lbl_unlim_usage: "Безлимит использований",
|
||||||
|
lbl_sticky: "Перманентный (Sticky)",
|
||||||
|
note_sticky: "* Sticky закрепляет скидку за пользователем навсегда.",
|
||||||
|
btn_create_promo: "Создать",
|
||||||
|
msg_promo_created: "Промокод создан!",
|
||||||
|
err_promo_create: "Ошибка создания",
|
||||||
|
msg_promo_deleted: "Промокод удален!",
|
||||||
|
confirm_delete_promo: "Удалить промокод <b>{0}</b>?",
|
||||||
|
|
||||||
|
app_desc_sleek: "Быстрый и удобный",
|
||||||
|
app_desc_premium: "Премиум выбор",
|
||||||
|
app_desc_standard: "Стандартный клиент",
|
||||||
|
app_desc_modern: "Мощный и современный",
|
||||||
|
app_desc_aio: "Универсальный комбайн",
|
||||||
|
app_desc_opensource: "Мощный Open Source",
|
||||||
|
app_desc_simple: "Простой и современный",
|
||||||
|
app_desc_classic: "Классика",
|
||||||
|
app_desc_native: "Нативная поддержка M1/M2",
|
||||||
|
app_desc_elegant: "Элегантный клиент"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -523,25 +653,25 @@ async function loadShop() {
|
|||||||
<span class="plan-price">${plan.price} ⭐️</span>
|
<span class="plan-price">${plan.price} ⭐️</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="plan-specs">
|
<div class="plan-specs">
|
||||||
<span><i data-lucide="database"></i> ${plan.data_limit > 0 ? plan.data_limit + ' GB' : 'Unlimited'}</span>
|
<span><i data-lucide="database"></i> ${plan.data_limit > 0 ? plan.data_limit + ' ' + t('unit_gb') : t('plan_unlimited')}</span>
|
||||||
<span><i data-lucide="clock"></i> ${plan.days} Days</span>
|
<span><i data-lucide="clock"></i> ${plan.days} ${t('unit_days')}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" onclick="initPayment('${plan.id}')">Purchase</button>
|
<button class="btn-primary" onclick="initPayment('${plan.id}')">${t('btn_purchase')}</button>
|
||||||
`;
|
`;
|
||||||
container.appendChild(card);
|
container.appendChild(card);
|
||||||
});
|
});
|
||||||
try { if (window.lucide) lucide.createIcons(); } catch (e) { }
|
try { if (window.lucide) lucide.createIcons(); } catch (e) { }
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
container.textContent = "Failed to load plans.";
|
container.textContent = t('err_plans_load');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initPayment(planId) {
|
async function initPayment(planId) {
|
||||||
if (!tg) return alert("Open in Telegram");
|
if (!tg) return alert(t('err_open_tg'));
|
||||||
const btn = event.target;
|
const btn = event.target;
|
||||||
const oldText = btn.innerText;
|
const oldText = btn.innerText;
|
||||||
btn.innerText = 'Creating..';
|
btn.innerText = t('btn_creating');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -563,7 +693,7 @@ async function initPayment(planId) {
|
|||||||
if (data.invoice_link) {
|
if (data.invoice_link) {
|
||||||
tg.openInvoice(data.invoice_link, (status) => {
|
tg.openInvoice(data.invoice_link, (status) => {
|
||||||
if (status === 'paid') {
|
if (status === 'paid') {
|
||||||
showToast('Successful Payment!', 'success');
|
showToast(t('msg_paid'), 'success');
|
||||||
router('dashboard');
|
router('dashboard');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -571,7 +701,7 @@ async function initPayment(planId) {
|
|||||||
showToast('Error: ' + (data.error || 'Unknown'), 'error');
|
showToast('Error: ' + (data.error || 'Unknown'), 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('Network error', 'error');
|
showToast(t('err_network'), 'error');
|
||||||
} finally {
|
} finally {
|
||||||
btn.innerText = oldText;
|
btn.innerText = oldText;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
@@ -603,7 +733,7 @@ async function loadSubscription() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (linkEl) linkEl.textContent = "No active subscription";
|
if (linkEl) linkEl.textContent = t('status_no_sub');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic Apps based on Device
|
// Dynamic Apps based on Device
|
||||||
@@ -616,10 +746,10 @@ async function loadSubscription() {
|
|||||||
appsContainer.innerHTML = `
|
appsContainer.innerHTML = `
|
||||||
<div class="sub-actions">
|
<div class="sub-actions">
|
||||||
<button class="btn-primary" onclick="openDownloadModal()">
|
<button class="btn-primary" onclick="openDownloadModal()">
|
||||||
<i data-lucide="download"></i> Download for ${myOS}
|
<i data-lucide="download"></i> ${t('btn_dl_for')} ${myOS}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-secondary" onclick="copyConfig()">
|
<button class="btn-secondary" onclick="copyConfig()">
|
||||||
<i data-lucide="copy"></i> Copy Link
|
<i data-lucide="copy"></i> ${t('btn_copy')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -633,25 +763,25 @@ function openDownloadModal() {
|
|||||||
|
|
||||||
if (device === 'ios') {
|
if (device === 'ios') {
|
||||||
apps = [
|
apps = [
|
||||||
{ name: 'V2Box', desc: 'Sleek & Fast', link: 'https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690' },
|
{ name: 'V2Box', desc: t('app_desc_sleek'), link: 'https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690' },
|
||||||
{ name: 'Streisand', desc: 'Premium Choice', link: 'https://apps.apple.com/us/app/streisand/id6450534064' }
|
{ name: 'Streisand', desc: t('app_desc_premium'), link: 'https://apps.apple.com/us/app/streisand/id6450534064' }
|
||||||
];
|
];
|
||||||
} else if (device === 'android') {
|
} else if (device === 'android') {
|
||||||
apps = [
|
apps = [
|
||||||
{ name: 'v2rayNG', desc: 'Standard Client', link: 'https://play.google.com/store/apps/details?id=com.v2ray.ang' },
|
{ name: 'v2rayNG', desc: t('app_desc_standard'), link: 'https://play.google.com/store/apps/details?id=com.v2ray.ang' },
|
||||||
{ name: 'NekoBox', desc: 'Powerful & Modern', link: 'https://github.com/MatsuriDayo/NekoBoxForAndroid/releases' },
|
{ name: 'NekoBox', desc: t('app_desc_modern'), link: 'https://github.com/MatsuriDayo/NekoBoxForAndroid/releases' },
|
||||||
{ name: 'Hiddify', desc: 'Universal All-in-One', link: 'https://play.google.com/store/apps/details?id=app.hiddify.com' }
|
{ name: 'Hiddify', desc: t('app_desc_aio'), link: 'https://play.google.com/store/apps/details?id=app.hiddify.com' }
|
||||||
];
|
];
|
||||||
} else if (device === 'windows') {
|
} else if (device === 'windows') {
|
||||||
apps = [
|
apps = [
|
||||||
{ name: 'NekoRay', desc: 'Powerful & Open Source', link: 'https://github.com/MatsuriDayo/nekoray/releases' },
|
{ name: 'NekoRay', desc: t('app_desc_opensource'), link: 'https://github.com/MatsuriDayo/nekoray/releases' },
|
||||||
{ name: 'Hiddify Next', desc: 'Simple & Modern', link: 'https://github.com/hiddify/hiddify-next/releases' },
|
{ name: 'Hiddify Next', desc: t('app_desc_simple'), link: 'https://github.com/hiddify/hiddify-next/releases' },
|
||||||
{ name: 'v2rayN', desc: 'Classic Client', link: 'https://github.com/2many986/v2rayN/releases' }
|
{ name: 'v2rayN', desc: t('app_desc_classic'), link: 'https://github.com/2many986/v2rayN/releases' }
|
||||||
];
|
];
|
||||||
} else if (device === 'macos') {
|
} else if (device === 'macos') {
|
||||||
apps = [
|
apps = [
|
||||||
{ name: 'V2Box', desc: 'Native M1/M2 Support', link: 'https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690' },
|
{ name: 'V2Box', desc: t('app_desc_native'), link: 'https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690' },
|
||||||
{ name: 'Hiddify Next', desc: 'Elegant Desktop Client', link: 'https://github.com/hiddify/hiddify-next/releases' }
|
{ name: 'Hiddify Next', desc: t('app_desc_elegant'), link: 'https://github.com/hiddify/hiddify-next/releases' }
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
// Linux or fallback
|
// Linux or fallback
|
||||||
@@ -663,7 +793,7 @@ function openDownloadModal() {
|
|||||||
|
|
||||||
let html = `
|
let html = `
|
||||||
<div class="download-options">
|
<div class="download-options">
|
||||||
<p class="subtitle" style="margin-bottom:20px;">Choose a client for your device:</p>
|
<p class="subtitle" style="margin-bottom:20px;">${t('modal_dl_subtitle')}</p>
|
||||||
<div class="admin-list">
|
<div class="admin-list">
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -681,16 +811,16 @@ function openDownloadModal() {
|
|||||||
|
|
||||||
html += `</div></div>`;
|
html += `</div></div>`;
|
||||||
|
|
||||||
openModal("Download App", html);
|
openModal(t('modal_dl_title'), html);
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyConfig() {
|
function copyConfig() {
|
||||||
if (currentState.subUrl) {
|
if (currentState.subUrl) {
|
||||||
navigator.clipboard.writeText(currentState.subUrl);
|
navigator.clipboard.writeText(currentState.subUrl);
|
||||||
showToast("Link copied!", "success");
|
showToast(t('msg_link_copied'), "success");
|
||||||
} else {
|
} else {
|
||||||
showToast("No subscription found.", "error");
|
showToast(t('err_no_sub'), "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,13 +869,13 @@ async function checkPromo() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
currentState.promoCode = data.code;
|
currentState.promoCode = data.code;
|
||||||
showToast(`Promo Applied! ${data.discount}% OFF.`, "success");
|
showToast(t('msg_promo_applied').replace('{0}', data.discount), "success");
|
||||||
closeModal(); // Optional: close modal on success
|
closeModal(); // Optional: close modal on success
|
||||||
} else {
|
} else {
|
||||||
showToast("Invalid Promo Code", "error");
|
showToast(t('err_promo_invalid'), "error");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast("Error checking promo", "error");
|
showToast(t('err_promo_check'), "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -792,22 +922,22 @@ function openSupport() {
|
|||||||
const html = `
|
const html = `
|
||||||
<div class="support-form">
|
<div class="support-form">
|
||||||
<p style="color:var(--md-sys-color-outline); margin-bottom:12px;">
|
<p style="color:var(--md-sys-color-outline); margin-bottom:12px;">
|
||||||
Describe your issue. We will contact you via Telegram.
|
${t('msg_support_sent')}
|
||||||
</p>
|
</p>
|
||||||
<textarea id="support-msg" class="glass-input" style="height:100px; padding-top:10px; resize:none;" placeholder="Your message..."></textarea>
|
<textarea id="support-msg" class="glass-input" style="height:100px; padding-top:10px; resize:none;" placeholder="${t('ph_message')}"></textarea>
|
||||||
<button class="btn-primary" style="margin-top:16px;" onclick="sendSupport()">Send Message</button>
|
<button class="btn-primary" style="margin-top:16px;" onclick="sendSupport()">${t('adm_send')}</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
openModal("Support", html);
|
openModal(t('nav_support'), html);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAbout() {
|
function openAbout() {
|
||||||
openModal("About", `
|
openModal(t('btn_about'), `
|
||||||
<div style="text-align:center">
|
<div style="text-align:center">
|
||||||
<i data-lucide="rocket" style="width:48px;height:48px;color:var(--md-sys-color-primary)"></i>
|
<i data-lucide="rocket" style="width:48px;height:48px;color:var(--md-sys-color-primary)"></i>
|
||||||
<h3 style="margin-top:12px">Stellarisei VPN v1.2</h3>
|
<h3 style="margin-top:12px">Stellarisei VPN v1.2</h3>
|
||||||
<p style="color:var(--md-sys-color-outline); margin-top:8px">
|
<p style="color:var(--md-sys-color-outline); margin-top:8px">
|
||||||
Premium V2Ray service.<br>Fast, Secure, Reliable.
|
${t('about_slogan')}
|
||||||
</p>
|
</p>
|
||||||
<p style="margin-top:16px; font-size:12px; opacity:0.5">© 2026 Stellarisei</p>
|
<p style="margin-top:16px; font-size:12px; opacity:0.5">© 2026 Stellarisei</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -837,12 +967,12 @@ async function sendSupport() {
|
|||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
closeModal();
|
closeModal();
|
||||||
showToast("Message sent! Admin will contact you.", "success");
|
showToast(t('msg_support_sent'), "success");
|
||||||
} else {
|
} else {
|
||||||
showToast("Failed to send.", "error");
|
showToast(t('err_send_fail'), "error");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast("Error sending message.", "error");
|
showToast(t('err_network'), "error");
|
||||||
} finally {
|
} finally {
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.innerText = oldText;
|
btn.innerText = oldText;
|
||||||
@@ -952,7 +1082,7 @@ async function adminSearchUsers() {
|
|||||||
list.innerHTML = `
|
list.innerHTML = `
|
||||||
<div class="card glass" style="text-align:center; padding:24px; opacity:0.6;">
|
<div class="card glass" style="text-align:center; padding:24px; opacity:0.6;">
|
||||||
<i data-lucide="search-x" style="width:40px; height:40px; margin-bottom:12px;"></i>
|
<i data-lucide="search-x" style="width:40px; height:40px; margin-bottom:12px;"></i>
|
||||||
<p>No users found</p>
|
<p>${t('adm_no_users')}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
@@ -1000,7 +1130,7 @@ async function showAdminUserDetail(targetId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const m = data.marzban || {};
|
const m = data.marzban || {};
|
||||||
document.getElementById('adm-user-status').textContent = m.status || 'Inactive';
|
document.getElementById('adm-user-status').textContent = m.status || t('status_inactive');
|
||||||
|
|
||||||
const subUntil = data.user.subscription_until ? new Date(data.user.subscription_until) : null;
|
const subUntil = data.user.subscription_until ? new Date(data.user.subscription_until) : null;
|
||||||
let expStr = 'None';
|
let expStr = 'None';
|
||||||
@@ -1020,9 +1150,9 @@ async function showAdminUserDetail(targetId) {
|
|||||||
async function adminUserAction(action) {
|
async function adminUserAction(action) {
|
||||||
if (action === 'toggle_status' || action === 'reset_traffic' || action === 'delete_sub') {
|
if (action === 'toggle_status' || action === 'reset_traffic' || action === 'delete_sub') {
|
||||||
const config = {
|
const config = {
|
||||||
'toggle_status': { title: 'Toggle Status', msg: 'Change user active/disabled state?', btnClass: 'btn-primary' },
|
'toggle_status': { title: t('adm_btn_toggle'), msg: t('confirm_toggle'), btnClass: 'btn-primary' },
|
||||||
'reset_traffic': { title: 'Reset Traffic', msg: 'Reset used traffic to zero?', btnClass: 'btn-primary' },
|
'reset_traffic': { title: t('adm_btn_reset'), msg: t('confirm_reset'), btnClass: 'btn-primary' },
|
||||||
'delete_sub': { title: 'Delete Subscription', msg: 'Are you sure? User will lose VPN access immediately.', btnClass: 'btn-error' }
|
'delete_sub': { title: t('adm_btn_delete'), msg: t('confirm_delete_sub'), btnClass: 'btn-error' }
|
||||||
};
|
};
|
||||||
const cfg = config[action];
|
const cfg = config[action];
|
||||||
openModal(cfg.title, `
|
openModal(cfg.title, `
|
||||||
@@ -1041,34 +1171,34 @@ async function adminUserAction(action) {
|
|||||||
let html = "";
|
let html = "";
|
||||||
|
|
||||||
if (action === 'add_days') {
|
if (action === 'add_days') {
|
||||||
title = "Add Days";
|
title = t('title_add_days');
|
||||||
html = `
|
html = `
|
||||||
<div class="modal-form">
|
<div class="modal-form">
|
||||||
<p class="subtitle" style="margin-bottom:12px;">Enter days to ADD (negative to subtract):</p>
|
<p class="subtitle" style="margin-bottom:12px;">${t('subtitle_add_days')}</p>
|
||||||
<input type="number" id="adm-action-val" class="glass-input" value="30" style="text-align:center;">
|
<input type="number" id="adm-action-val" class="glass-input" value="30" style="text-align:center;">
|
||||||
<button class="btn-primary" style="margin-top:20px;" onclick="submitAdminAction('add_days')">Apply</button>
|
<button class="btn-primary" style="margin-top:20px;" onclick="submitAdminAction('add_days')">${t('btn_apply')}</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (action === 'set_limit') {
|
} else if (action === 'set_limit') {
|
||||||
title = "Set Traffic Limit";
|
title = t('title_set_limit');
|
||||||
html = `
|
html = `
|
||||||
<div class="modal-form">
|
<div class="modal-form">
|
||||||
<p class="subtitle" style="margin-bottom:12px;">Enter new limit in GB (0 for ∞):</p>
|
<p class="subtitle" style="margin-bottom:12px;">${t('subtitle_set_limit')}</p>
|
||||||
<input type="number" id="adm-action-val" class="glass-input" value="50" style="text-align:center;">
|
<input type="number" id="adm-action-val" class="glass-input" value="50" style="text-align:center;">
|
||||||
<button class="btn-primary" style="margin-top:20px;" onclick="submitAdminAction('set_limit')">Set Limit</button>
|
<button class="btn-primary" style="margin-top:20px;" onclick="submitAdminAction('set_limit')">${t('title_set_limit')}</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (action === 'set_expiry') {
|
} else if (action === 'set_expiry') {
|
||||||
title = "Set Expiration";
|
title = t('title_set_exp');
|
||||||
html = `
|
html = `
|
||||||
<div class="modal-form">
|
<div class="modal-form">
|
||||||
<p class="subtitle" style="margin-bottom:12px;">Days from NOW (0 = expire, 36500 = ∞):</p>
|
<p class="subtitle" style="margin-bottom:12px;">${t('subtitle_set_exp')}</p>
|
||||||
<input type="number" id="adm-action-val" class="glass-input" value="30" style="text-align:center;">
|
<input type="number" id="adm-action-val" class="glass-input" value="30" style="text-align:center;">
|
||||||
<button class="btn-primary" style="margin-top:20px;" onclick="submitAdminAction('set_expiry')">Set Expiry</button>
|
<button class="btn-primary" style="margin-top:20px;" onclick="submitAdminAction('set_expiry')">${t('title_set_exp')}</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (action === 'set_plan') {
|
} else if (action === 'set_plan') {
|
||||||
title = "Select Plan";
|
title = t('shop_title');
|
||||||
const res = await fetch(`${API_BASE}/admin/plans_full?user_id=${currentState.user.id}`);
|
const res = await fetch(`${API_BASE}/admin/plans_full?user_id=${currentState.user.id}`);
|
||||||
const allPlans = await res.json();
|
const allPlans = await res.json();
|
||||||
|
|
||||||
@@ -1179,10 +1309,10 @@ async function loadAdminPromos() {
|
|||||||
async function deleteAdminPromo(code) {
|
async function deleteAdminPromo(code) {
|
||||||
openModal("Delete Promo", `
|
openModal("Delete Promo", `
|
||||||
<div class="confirm-modal">
|
<div class="confirm-modal">
|
||||||
<p class="subtitle" style="margin-bottom:24px;">Are you sure you want to delete promo code <b>${code}</b>?</p>
|
<p class="subtitle" style="margin-bottom:24px;">${t('confirm_delete_promo').replace('{0}', code)}</p>
|
||||||
<div style="display:flex; gap:12px;">
|
<div style="display:flex; gap:12px;">
|
||||||
<button class="btn-secondary" style="flex:1" onclick="closeModal()">Cancel</button>
|
<button class="btn-secondary" style="flex:1" onclick="closeModal()">${t('btn_back')}</button>
|
||||||
<button class="btn-error" style="flex:1" onclick="submitDeletePromo('${code}')">Delete</button>
|
<button class="btn-error" style="flex:1" onclick="submitDeletePromo('${code}')">${t('adm_btn_delete')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
@@ -1203,31 +1333,31 @@ async function submitDeletePromo(code) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openCreatePromoModal() {
|
function openCreatePromoModal() {
|
||||||
openModal("New Promo", `
|
openModal(t('title_new_promo'), `
|
||||||
<div class="promo-create-form" style="display:flex; flex-direction:column; gap:12px;">
|
<div class="promo-create-form" style="display:flex; flex-direction:column; gap:12px;">
|
||||||
<input type="text" id="new-promo-code" placeholder="CODE (e.g. SUMMER2024)" class="glass-input" style="text-transform:uppercase;">
|
<input type="text" id="new-promo-code" placeholder="${t('ph_code')}" class="glass-input" style="text-transform:uppercase;">
|
||||||
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px;">
|
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px;">
|
||||||
<input type="number" id="new-promo-discount" placeholder="Discount %" class="glass-input">
|
<input type="number" id="new-promo-discount" placeholder="${t('ph_discount')}" class="glass-input">
|
||||||
<input type="number" id="new-promo-uses" placeholder="Uses Limit" class="glass-input">
|
<input type="number" id="new-promo-uses" placeholder="${t('ph_uses')}" class="glass-input">
|
||||||
</div>
|
</div>
|
||||||
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px;">
|
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px;">
|
||||||
<input type="number" id="new-promo-days" placeholder="Validity Days" class="glass-input">
|
<input type="number" id="new-promo-days" placeholder="${t('ph_days')}" class="glass-input">
|
||||||
<input type="number" id="new-promo-bonus" placeholder="Bonus Days" class="glass-input">
|
<input type="number" id="new-promo-bonus" placeholder="${t('ph_bonus')}" class="glass-input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="list-item glass" style="margin:0; padding:8px 12px; cursor:pointer;">
|
<label class="list-item glass" style="margin:0; padding:8px 12px; cursor:pointer;">
|
||||||
<span style="flex:1">Unlimited Usage</span>
|
<span style="flex:1">${t('lbl_unlim_usage')}</span>
|
||||||
<input type="checkbox" id="new-promo-unlim" style="width:20px; height:20px;">
|
<input type="checkbox" id="new-promo-unlim" style="width:20px; height:20px;">
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="list-item glass" style="margin:0; padding:8px 12px; cursor:pointer;">
|
<label class="list-item glass" style="margin:0; padding:8px 12px; cursor:pointer;">
|
||||||
<span style="flex:1">Permanent (Sticky)</span>
|
<span style="flex:1">${t('lbl_sticky')}</span>
|
||||||
<input type="checkbox" id="new-promo-sticky" style="width:20px; height:20px;">
|
<input type="checkbox" id="new-promo-sticky" style="width:20px; height:20px;">
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<p style="font-size:11px; opacity:0.6; padding:0 4px;">* Sticky promo locks the discount for the user forever.</p>
|
<p style="font-size:11px; opacity:0.6; padding:0 4px;">${t('note_sticky')}</p>
|
||||||
|
|
||||||
<button class="btn-primary" style="margin-top:8px;" onclick="createPromo()">Create Promo</button>
|
<button class="btn-primary" style="margin-top:8px;" onclick="createPromo()">${t('btn_create_promo')}</button>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
@@ -1263,7 +1393,7 @@ async function sendBroadcast() {
|
|||||||
const msg = document.getElementById('broadcast-msg').value;
|
const msg = document.getElementById('broadcast-msg').value;
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
|
|
||||||
if (!confirm("Send this message to ALL users?")) return;
|
if (!confirm(t('confirm_broadcast'))) return;
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}/admin/broadcast`, {
|
const res = await fetch(`${API_BASE}/admin/broadcast`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -1272,7 +1402,7 @@ async function sendBroadcast() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
showToast(`Broadcast sent to ${data.sent} users!`);
|
showToast(t('msg_broadcast_sent').replace('{0}', data.sent));
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPromoModal() {
|
function openPromoModal() {
|
||||||
|
|||||||
Reference in New Issue
Block a user