633 lines
20 KiB
JavaScript
633 lines
20 KiB
JavaScript
// Game dashboard: load character profile, zones, hunt, combat log, and inventory.
|
|
|
|
let energyInterval = null;
|
|
let energyData = null; // store latest server energy data for sync
|
|
let isHunting = false;
|
|
let currentTab = 'hunt';
|
|
let inventoryCache = null;
|
|
|
|
/**
|
|
* Switch active tab in the action panel.
|
|
*/
|
|
function switchTab(tab) {
|
|
currentTab = tab;
|
|
document.querySelectorAll('.tab').forEach((t) => {
|
|
const isActive = t.dataset.tab === tab;
|
|
t.classList.toggle('active', isActive);
|
|
t.setAttribute('aria-selected', isActive);
|
|
});
|
|
document.getElementById('hunt-tab').style.display = tab === 'hunt' ? '' : 'none';
|
|
document.getElementById('inventory-tab').style.display = tab === 'inventory' ? '' : 'none';
|
|
}
|
|
|
|
/**
|
|
* Refresh energy state from the server at a fixed interval.
|
|
* Keeps client-side countdown in sync with the authoritative server.
|
|
*/
|
|
function startEnergySync() {
|
|
if (energyInterval) clearInterval(energyInterval);
|
|
// Sync every 60s (2/3 of a regen tick) to stay in sync with server
|
|
energyInterval = setInterval(() => {
|
|
loadCharacter();
|
|
}, 60000);
|
|
}
|
|
|
|
/**
|
|
* Update the energy bar and countdown display.
|
|
* @param {number} current - current energy value
|
|
* @param {number} max - max energy
|
|
* @param {number} nextTickSec - seconds until next +1 energy tick (server-derived)
|
|
*/
|
|
function updateEnergyBar(current, max, nextTickSec) {
|
|
energyData = { current, max, nextTickSec };
|
|
|
|
// Update text
|
|
const energyEl = document.getElementById('char-energy');
|
|
if (energyEl) energyEl.textContent = `${current} / ${max}`;
|
|
|
|
// Update bar fill
|
|
const barEl = document.getElementById('energy-bar');
|
|
if (barEl) {
|
|
const pct = Math.round((current / max) * 100);
|
|
barEl.style.width = `${pct}%`;
|
|
barEl.title = `${current}/${max} energy`;
|
|
}
|
|
|
|
// Update countdown
|
|
const timerEl = document.getElementById('energy-timer');
|
|
if (timerEl) {
|
|
if (current >= max) {
|
|
timerEl.textContent = 'Energy full';
|
|
} else {
|
|
const mins = Math.floor(nextTickSec / 60);
|
|
const secs = nextTickSec % 60;
|
|
timerEl.textContent = `Next +1 in ${mins}:${secs.toString().padStart(2, '0')}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start a client-side countdown that decrements until the next energy tick.
|
|
* When it reaches zero, refetch from server to get authoritative state.
|
|
*/
|
|
function startCountdown() {
|
|
if (!energyData || energyData.current >= energyData.max || energyData.nextTickSec <= 0) {
|
|
return;
|
|
}
|
|
|
|
let remaining = energyData.nextTickSec;
|
|
|
|
if (energyInterval) clearInterval(energyInterval);
|
|
energyInterval = setInterval(() => {
|
|
remaining--;
|
|
if (remaining <= 0) {
|
|
clearInterval(energyInterval);
|
|
energyInterval = null;
|
|
loadCharacter();
|
|
return;
|
|
}
|
|
const mins = Math.floor(remaining / 60);
|
|
const secs = remaining % 60;
|
|
const timerEl = document.getElementById('energy-timer');
|
|
if (timerEl) {
|
|
timerEl.textContent = `Next +1 in ${mins}:${secs.toString().padStart(2, '0')}`;
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
async function loadCharacter() {
|
|
try {
|
|
const data = await api.get('/character');
|
|
if (!data) {
|
|
// No character yet — redirect to create-hero
|
|
window.location.href = '/create-hero.html';
|
|
return;
|
|
}
|
|
|
|
const { character, derived, energy } = data;
|
|
|
|
// Show stats, hide loading
|
|
document.getElementById('hero-loading').style.display = 'none';
|
|
document.getElementById('hero-stats').style.display = 'block';
|
|
|
|
// Basic info
|
|
document.getElementById('player-name').textContent = character.name;
|
|
document.getElementById('char-name').textContent = character.name;
|
|
document.getElementById('char-class').textContent = character.class.charAt(0).toUpperCase() + character.class.slice(1);
|
|
document.getElementById('char-level').textContent = character.level;
|
|
document.getElementById('char-hp').textContent = `${character.hp_current} / ${character.maxHp}`;
|
|
document.getElementById('char-energy').textContent = `${energy.current} / ${energy.max}`;
|
|
document.getElementById('char-gold').textContent = character.gold;
|
|
document.getElementById('char-xp').textContent = `${character.xp} / ${xpToNext(character.level)}`;
|
|
document.getElementById('char-str').textContent = character.str;
|
|
document.getElementById('char-agi').textContent = character.agi;
|
|
document.getElementById('char-int').textContent = character.int;
|
|
document.getElementById('char-vit').textContent = character.vit;
|
|
document.getElementById('derived-atk').textContent = derived.attack;
|
|
document.getElementById('derived-def').textContent = derived.defense;
|
|
document.getElementById('derived-spd').textContent = derived.speed;
|
|
document.getElementById('derived-crit').textContent = derived.critChance;
|
|
|
|
// Update energy bar and start countdown
|
|
updateEnergyBar(energy.current, energy.max, energy.nextTickInSec);
|
|
startCountdown();
|
|
} catch (err) {
|
|
// Not authenticated — redirect to login
|
|
window.location.href = '/login.html';
|
|
}
|
|
}
|
|
|
|
function xpToNext(level) {
|
|
return Math.floor(100 * Math.pow(level, 1.5));
|
|
}
|
|
|
|
// ===== Zone list =====
|
|
async function loadZones() {
|
|
const zoneList = document.getElementById('zone-list');
|
|
try {
|
|
const data = await api.get('/zones');
|
|
if (!data || !data.ok) {
|
|
zoneList.innerHTML = '';
|
|
const p = document.createElement('p');
|
|
p.className = 'placeholder';
|
|
p.textContent = 'Failed to load zones.';
|
|
zoneList.appendChild(p);
|
|
return;
|
|
}
|
|
|
|
const zones = data.data;
|
|
zoneList.innerHTML = '';
|
|
|
|
for (const zone of zones) {
|
|
const card = document.createElement('div');
|
|
card.className = 'zone-card';
|
|
|
|
const zoneName = document.createElement('div');
|
|
zoneName.className = 'zone-name';
|
|
zoneName.textContent = zone.name;
|
|
|
|
const zoneDesc = document.createElement('div');
|
|
zoneDesc.className = 'zone-desc';
|
|
zoneDesc.textContent = zone.description;
|
|
|
|
const zoneMeta = document.createElement('div');
|
|
zoneMeta.className = 'zone-meta';
|
|
zoneMeta.textContent = `Lv ${zone.min_level}+ · ${zone.energy_cost}⚡`;
|
|
|
|
const btn = document.createElement('button');
|
|
btn.className = 'btn btn-hunt';
|
|
btn.textContent = `Hunt (${zone.energy_cost}⚡)`;
|
|
btn.disabled = !zone.unlocked;
|
|
btn.dataset.zoneId = zone.id;
|
|
|
|
if (!zone.unlocked) {
|
|
const lockMsg = document.createElement('div');
|
|
lockMsg.className = 'zone-locked-msg';
|
|
lockMsg.textContent = `Reach level ${zone.min_level}`;
|
|
card.appendChild(zoneName);
|
|
card.appendChild(lockMsg);
|
|
} else {
|
|
card.appendChild(zoneName);
|
|
card.appendChild(zoneDesc);
|
|
card.appendChild(zoneMeta);
|
|
card.appendChild(btn);
|
|
}
|
|
|
|
zoneList.appendChild(card);
|
|
}
|
|
|
|
// Attach hunt handlers
|
|
zoneList.querySelectorAll('.btn-hunt').forEach((btn) => {
|
|
btn.addEventListener('click', handleHunt);
|
|
});
|
|
} catch (_) {
|
|
zoneList.innerHTML = '';
|
|
const p = document.createElement('p');
|
|
p.className = 'placeholder';
|
|
p.textContent = 'Failed to load zones.';
|
|
zoneList.appendChild(p);
|
|
}
|
|
}
|
|
|
|
// ===== Hunt =====
|
|
async function handleHunt(e) {
|
|
if (isHunting) return;
|
|
const btn = e.target;
|
|
const zoneId = Number(btn.dataset.zoneId);
|
|
if (isNaN(zoneId)) return;
|
|
|
|
isHunting = true;
|
|
btn.disabled = true;
|
|
btn.textContent = 'Hunting...';
|
|
|
|
// Hide previous result
|
|
document.getElementById('result-panel').style.display = 'none';
|
|
|
|
try {
|
|
const data = await api.post('/hunt', { zoneId });
|
|
if (!data || !data.ok) {
|
|
showToast(data?.error?.message || 'Hunt failed.', 'error');
|
|
return;
|
|
}
|
|
|
|
const result = data.data;
|
|
displayHuntResult(result);
|
|
loadCharacter();
|
|
loadCombatLog();
|
|
loadZones(); // refresh energy cost buttons
|
|
} catch (err) {
|
|
showToast('Hunt failed.', 'error');
|
|
} finally {
|
|
isHunting = false;
|
|
// Re-enable all hunt buttons
|
|
document.querySelectorAll('.btn-hunt').forEach((b) => {
|
|
b.disabled = false;
|
|
// Restore text from zone card labels
|
|
const zoneCard = b.closest('.zone-card');
|
|
if (zoneCard) {
|
|
const meta = zoneCard.querySelector('.zone-meta');
|
|
if (meta) {
|
|
const cost = meta.textContent.match(/(\d+)⚡/);
|
|
if (cost) b.textContent = `Hunt (${cost[1]}⚡)`;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ===== Display hunt result =====
|
|
function displayHuntResult(result) {
|
|
const panel = document.getElementById('result-panel');
|
|
const title = document.getElementById('result-title');
|
|
const body = document.getElementById('result-body');
|
|
|
|
panel.style.display = 'block';
|
|
|
|
const won = result.winner === 'hero';
|
|
title.textContent = won ? 'Victory!' : 'Defeat';
|
|
title.style.color = won ? '' : '';
|
|
|
|
body.innerHTML = '';
|
|
|
|
// Monster info
|
|
const monsterLine = document.createElement('p');
|
|
monsterLine.textContent = `Fought: ${result.monster.name} (Lv.${result.monster.level})`;
|
|
body.appendChild(monsterLine);
|
|
|
|
// Rewards
|
|
const rewards = document.createElement('p');
|
|
rewards.textContent = [];
|
|
const parts = [];
|
|
if (result.xpGained) parts.push(`+${result.xpGained} XP`);
|
|
if (result.goldGained) parts.push(`+${result.goldGained} Gold`);
|
|
if (parts.length) rewards.textContent = parts.join(', ');
|
|
if (result.loot && result.loot.length > 0) {
|
|
const lootParts = result.loot.map((l) => `${l.name}${l.quantity > 1 ? `x${l.quantity}` : ''}`);
|
|
const lootLine = document.createElement('p');
|
|
lootLine.textContent = `Loot: ${lootParts.join(', ')}`;
|
|
body.appendChild(lootLine);
|
|
}
|
|
body.appendChild(rewards);
|
|
|
|
// Level up
|
|
if (result.leveledUp) {
|
|
const lvLine = document.createElement('p');
|
|
lvLine.style.fontWeight = 'bold';
|
|
lvLine.textContent = `⬆ Level up! Now level ${result.newLevel}`;
|
|
body.appendChild(lvLine);
|
|
}
|
|
|
|
// HP remaining
|
|
const hpLine = document.createElement('p');
|
|
hpLine.textContent = `HP: ${result.hpRemaining} / ${result.hpMax}`;
|
|
body.appendChild(hpLine);
|
|
|
|
// Energy remaining
|
|
const energyLine = document.createElement('p');
|
|
energyLine.textContent = `Energy: ${result.energy}`;
|
|
body.appendChild(energyLine);
|
|
|
|
// Gold lost on defeat
|
|
if (result.goldLost > 0) {
|
|
const lostLine = document.createElement('p');
|
|
lostLine.style.color = '#c0392b';
|
|
lostLine.textContent = `Lost ${result.goldLost} gold to defeat.`;
|
|
body.appendChild(lostLine);
|
|
}
|
|
}
|
|
|
|
// ===== Combat log =====
|
|
async function loadCombatLog() {
|
|
const logList = document.getElementById('combat-log-list');
|
|
try {
|
|
const data = await api.get('/combat-log?limit=10');
|
|
if (!data || !data.ok) {
|
|
logList.innerHTML = '';
|
|
const p = document.createElement('p');
|
|
p.className = 'placeholder';
|
|
p.textContent = 'No combat history.';
|
|
logList.appendChild(p);
|
|
return;
|
|
}
|
|
|
|
const logs = data.data;
|
|
logList.innerHTML = '';
|
|
|
|
if (logs.length === 0) {
|
|
const p = document.createElement('p');
|
|
p.className = 'placeholder';
|
|
p.textContent = 'No combat history.';
|
|
logList.appendChild(p);
|
|
return;
|
|
}
|
|
|
|
for (const log of logs) {
|
|
const entry = document.createElement('div');
|
|
entry.className = 'log-entry';
|
|
|
|
const resultTag = document.createElement('span');
|
|
resultTag.className = `log-result ${log.won ? 'win' : 'loss'}`;
|
|
resultTag.textContent = log.won ? '✓' : '✗';
|
|
|
|
const logText = document.createElement('span');
|
|
logText.className = 'log-text';
|
|
const parts = [`${log.monsterName} (Lv.${log.monsterLevel})`];
|
|
if (log.won) {
|
|
if (log.xpGained) parts.push(`+${log.xpGained} XP`);
|
|
if (log.goldGained) parts.push(`+${log.goldGained} G`);
|
|
} else {
|
|
parts.push('defeated');
|
|
}
|
|
logText.textContent = parts.join(' · ');
|
|
|
|
entry.appendChild(resultTag);
|
|
entry.appendChild(logText);
|
|
logList.appendChild(entry);
|
|
}
|
|
} catch (_) {
|
|
logList.innerHTML = '';
|
|
const p = document.createElement('p');
|
|
p.className = 'placeholder';
|
|
p.textContent = 'Failed to load combat log.';
|
|
logList.appendChild(p);
|
|
}
|
|
}
|
|
|
|
// ===== Item display helpers =====
|
|
const ITEM_EMOJI = { weapon: '⚔️', armor: '🛡️', trinket: '💍', consumable: '🧪', material: '📦' };
|
|
|
|
function formatBonuses(bonuses) {
|
|
const parts = [];
|
|
if (bonuses.bonus_attack) parts.push(`+${bonuses.bonus_attack} ATK`);
|
|
if (bonuses.bonus_defense) parts.push(`+${bonuses.bonus_defense} DEF`);
|
|
if (bonuses.bonus_str) parts.push(`+${bonuses.bonus_str} STR`);
|
|
if (bonuses.bonus_agi) parts.push(`+${bonuses.bonus_agi} AGI`);
|
|
if (bonuses.bonus_int) parts.push(`+${bonuses.bonus_int} INT`);
|
|
if (bonuses.bonus_vit) parts.push(`+${bonuses.bonus_vit} VIT`);
|
|
return parts.join(', ');
|
|
}
|
|
|
|
function formatEffect(item) {
|
|
if (!item.effect) return '';
|
|
const match = item.effect.match(/^(heal|energy):(\d+)$/);
|
|
if (!match) return '';
|
|
const [_full, type, value] = match;
|
|
return type === 'heal' ? `Heals ${value} HP` : `Restores ${value} Energy`;
|
|
}
|
|
|
|
// ===== Inventory =====
|
|
const EQUIP_SLOTS = ['weapon', 'armor', 'trinket'];
|
|
|
|
async function loadInventory() {
|
|
try {
|
|
const data = await api.get('/inventory');
|
|
if (!data) return;
|
|
inventoryCache = data;
|
|
renderEquipmentPanel(data.items);
|
|
renderInventoryGrid(data.items);
|
|
} catch (_) {
|
|
// Character doesn't exist yet or not authenticated — skip.
|
|
}
|
|
}
|
|
|
|
function renderEquipmentPanel(items) {
|
|
const container = document.getElementById('equipment-slots');
|
|
container.innerHTML = '';
|
|
|
|
for (const slot of EQUIP_SLOTS) {
|
|
const slotDiv = document.createElement('div');
|
|
slotDiv.className = 'equip-slot';
|
|
|
|
const label = document.createElement('span');
|
|
label.className = 'slot-label';
|
|
label.textContent = slot;
|
|
|
|
const equipped = items.find((i) => i.equipped_slot === slot);
|
|
const itemInfo = document.createElement('span');
|
|
itemInfo.className = 'slot-item';
|
|
|
|
if (equipped) {
|
|
const nameSpan = document.createElement('span');
|
|
nameSpan.className = `item-name rarity-${equipped.rarity}`;
|
|
nameSpan.textContent = equipped.name;
|
|
itemInfo.appendChild(nameSpan);
|
|
|
|
const statsText = formatBonuses(equipped.bonuses);
|
|
if (statsText) {
|
|
const statsSpan = document.createElement('span');
|
|
statsSpan.className = 'item-stats';
|
|
statsSpan.textContent = statsText;
|
|
itemInfo.appendChild(statsSpan);
|
|
}
|
|
|
|
const unequipBtn = document.createElement('button');
|
|
unequipBtn.className = 'btn-unequip';
|
|
unequipBtn.textContent = '✕';
|
|
unequipBtn.title = `Unequip ${equipped.name}`;
|
|
unequipBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
handleUnequip(slot);
|
|
});
|
|
itemInfo.appendChild(unequipBtn);
|
|
} else {
|
|
const emptySpan = document.createElement('span');
|
|
emptySpan.className = 'slot-empty';
|
|
emptySpan.textContent = 'Empty';
|
|
itemInfo.appendChild(emptySpan);
|
|
}
|
|
|
|
slotDiv.appendChild(label);
|
|
slotDiv.appendChild(itemInfo);
|
|
container.appendChild(slotDiv);
|
|
}
|
|
}
|
|
|
|
function renderInventoryGrid(items) {
|
|
const container = document.getElementById('inventory-grid');
|
|
container.innerHTML = '';
|
|
|
|
const unequipped = items.filter((i) => !i.equipped);
|
|
if (unequipped.length === 0) {
|
|
const p = document.createElement('p');
|
|
p.className = 'placeholder';
|
|
p.textContent = 'No items.';
|
|
container.appendChild(p);
|
|
return;
|
|
}
|
|
|
|
for (const item of unequipped) {
|
|
const card = document.createElement('div');
|
|
card.className = 'inv-item';
|
|
|
|
// Tooltip (always shown on hover)
|
|
const tooltip = document.createElement('div');
|
|
tooltip.className = 'tooltip';
|
|
|
|
const tooltipName = document.createElement('div');
|
|
tooltipName.className = `tooltip-name rarity-${item.rarity}`;
|
|
tooltipName.textContent = item.name;
|
|
tooltip.appendChild(tooltipName);
|
|
|
|
const tooltipType = document.createElement('div');
|
|
tooltipType.className = 'tooltip-type';
|
|
tooltipType.textContent = `${item.type}${item.quantity > 1 ? ` (x${item.quantity})` : ''}`;
|
|
tooltip.appendChild(tooltipType);
|
|
|
|
const statsText = formatBonuses(item.bonuses);
|
|
if (statsText) {
|
|
const tooltipStats = document.createElement('div');
|
|
tooltipStats.className = 'tooltip-stats';
|
|
tooltipStats.textContent = statsText;
|
|
tooltip.appendChild(tooltipStats);
|
|
}
|
|
|
|
const effectText = formatEffect(item);
|
|
if (effectText) {
|
|
const tooltipEffect = document.createElement('div');
|
|
tooltipEffect.className = 'tooltip-effect';
|
|
tooltipEffect.textContent = effectText;
|
|
tooltip.appendChild(tooltipEffect);
|
|
}
|
|
|
|
if (item.min_level > 1) {
|
|
const tooltipLevel = document.createElement('div');
|
|
tooltipLevel.className = 'tooltip-level';
|
|
tooltipLevel.textContent = `Requires Lv. ${item.min_level}`;
|
|
tooltip.appendChild(tooltipLevel);
|
|
}
|
|
|
|
card.appendChild(tooltip);
|
|
|
|
// Compact card content
|
|
const header = document.createElement('div');
|
|
header.className = 'inv-item-header';
|
|
|
|
const icon = document.createElement('span');
|
|
icon.className = 'inv-item-icon';
|
|
icon.textContent = ITEM_EMOJI[item.type] || '❓';
|
|
header.appendChild(icon);
|
|
|
|
const name = document.createElement('span');
|
|
name.className = 'inv-item-name rarity-' + item.rarity;
|
|
name.textContent = item.name;
|
|
header.appendChild(name);
|
|
|
|
const rarityBadge = document.createElement('span');
|
|
rarityBadge.className = `inv-item-rarity bg-rarity-${item.rarity}`;
|
|
rarityBadge.textContent = item.rarity;
|
|
header.appendChild(rarityBadge);
|
|
|
|
card.appendChild(header);
|
|
|
|
const stats = document.createElement('div');
|
|
stats.className = 'inv-item-stats';
|
|
stats.textContent = formatBonuses(item.bonuses) || formatEffect(item) || '—';
|
|
card.appendChild(stats);
|
|
|
|
if (item.quantity > 1) {
|
|
const qty = document.createElement('span');
|
|
qty.className = 'inv-item-qty';
|
|
qty.textContent = `x${item.quantity}`;
|
|
card.appendChild(qty);
|
|
}
|
|
|
|
card.addEventListener('click', () => handleItemClick(item));
|
|
container.appendChild(card);
|
|
}
|
|
}
|
|
|
|
async function handleItemClick(item) {
|
|
try {
|
|
let data;
|
|
if (item.type === 'consumable') {
|
|
data = await api.post('/inventory/use', { itemId: item.itemId });
|
|
} else {
|
|
data = await api.post('/inventory/equip', { itemId: item.itemId, slot: item.type });
|
|
}
|
|
if (!data || !data.ok) {
|
|
showToast(data?.error?.message || 'Action failed.', 'error');
|
|
return;
|
|
}
|
|
inventoryCache = data;
|
|
renderEquipmentPanel(data.items);
|
|
renderInventoryGrid(data.items);
|
|
loadCharacter();
|
|
} catch (err) {
|
|
showToast(err.message || 'Action failed.', 'error');
|
|
}
|
|
}
|
|
|
|
async function handleUnequip(slot) {
|
|
try {
|
|
const data = await api.post('/inventory/unequip', { slot });
|
|
if (!data || !data.ok) {
|
|
showToast(data?.error?.message || 'Unequip failed.', 'error');
|
|
return;
|
|
}
|
|
inventoryCache = data;
|
|
renderEquipmentPanel(data.items);
|
|
renderInventoryGrid(data.items);
|
|
loadCharacter();
|
|
} catch (err) {
|
|
showToast(err.message || 'Unequip failed.', 'error');
|
|
}
|
|
}
|
|
|
|
// ===== Toast notifications =====
|
|
function showToast(message, type = 'info') {
|
|
// Remove existing toasts
|
|
document.querySelectorAll('.toast').forEach((t) => t.remove());
|
|
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast toast-${type}`;
|
|
toast.textContent = message;
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.remove();
|
|
}, 4000);
|
|
}
|
|
|
|
// ===== Init =====
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
await loadCharacter();
|
|
await loadZones();
|
|
await loadCombatLog();
|
|
await loadInventory();
|
|
startEnergySync();
|
|
|
|
// Tab click handlers
|
|
document.querySelectorAll('.tab').forEach((tab) => {
|
|
tab.addEventListener('click', () => switchTab(tab.dataset.tab));
|
|
});
|
|
});
|
|
|
|
// Logout handler
|
|
document.getElementById('logout-btn')?.addEventListener('click', async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
await api.post('/auth/logout', {});
|
|
} catch (_) {}
|
|
window.location.href = '/login.html';
|
|
});
|