initial commit
This commit is contained in:
Executable
+228
@@ -0,0 +1,228 @@
|
||||
|
||||
// trader-resets.js (v2.2.0) — Local JSON version with ONE unified "data last updated" bubble
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
// ===== Config =====
|
||||
const REFRESH_MS = 5 * 60 * 1000; // auto-refresh every 5 minutes
|
||||
const STORAGE_KEY = 'eft.traders.mode'; // 'regular' or 'pve'
|
||||
const DEBUG_TS = false;
|
||||
|
||||
// Local JSON files (served from /var/www/html via cron)
|
||||
const DATA_URL = {
|
||||
regular: '/traders_pvp.json',
|
||||
pve: '/traders_pve.json',
|
||||
};
|
||||
|
||||
// ===== DOM refs =====
|
||||
const els = {
|
||||
grid: document.getElementById('traderGrid'),
|
||||
refresh: document.getElementById('refreshBtn'),
|
||||
modeToggle: document.getElementById('modeToggle'),
|
||||
dataUpdated: document.getElementById('dataUpdated'),
|
||||
};
|
||||
|
||||
if (!els.grid) {
|
||||
console.warn('[trader-resets] Missing #traderGrid.');
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== Utilities =====
|
||||
// Normalize resetTime (seconds, ms, ISO) -> ms epoch or null
|
||||
function toMsEpoch(resetTime) {
|
||||
if (resetTime == null) return null;
|
||||
if (typeof resetTime === 'number') return resetTime < 1e12 ? resetTime * 1000 : resetTime;
|
||||
if (typeof resetTime === 'string') {
|
||||
const parsed = Date.parse(resetTime);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function currentMode() {
|
||||
return els.modeToggle && els.modeToggle.checked ? 'pve' : 'regular';
|
||||
}
|
||||
|
||||
function fmtHMS(ms) {
|
||||
if (!Number.isFinite(ms) || ms <= 0) return '00:00:00';
|
||||
const sec = Math.floor(ms / 1000);
|
||||
const h = String(Math.floor(sec / 3600)).padStart(2, '0');
|
||||
const m = String(Math.floor((sec % 3600) / 60)).padStart(2, '0');
|
||||
const s = String(sec % 60).padStart(2, '0');
|
||||
return `${h}:${m}:${s}`;
|
||||
}
|
||||
|
||||
async function getLocalTraders(mode) {
|
||||
const url = DATA_URL[mode] || DATA_URL.regular;
|
||||
const v = Math.floor(Date.now() / 60000); // per-minute cache-bust
|
||||
const res = await fetch(`${url}?v=${v}`, { cache: 'no-store' });
|
||||
if (!res.ok) throw new Error(`Failed to load ${url}: ${res.status} ${res.statusText}`);
|
||||
return res.json(); // array of traders
|
||||
}
|
||||
|
||||
// ===== Last-updated (from static file Last-Modified headers) =====
|
||||
async function fetchLastModified(url) {
|
||||
try {
|
||||
const head = await fetch(url, { method: 'HEAD' });
|
||||
const lm = head.headers.get('Last-Modified');
|
||||
if (lm) return new Date(lm);
|
||||
} catch {}
|
||||
try {
|
||||
const get = await fetch(url + '?stamp=' + Date.now(), { cache: 'no-store' });
|
||||
const lm = get.headers.get('Last-Modified');
|
||||
if (lm) return new Date(lm);
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set the *single* bubble to the newest (max) of PvP/PvE file timestamps
|
||||
async function refreshUnifiedUpdatedBubble() {
|
||||
const [pvp, pve] = await Promise.all([
|
||||
fetchLastModified(DATA_URL.regular),
|
||||
fetchLastModified(DATA_URL.pve),
|
||||
]);
|
||||
|
||||
const newest = (!pvp && !pve)
|
||||
? null
|
||||
: (pvp && pve ? (pvp > pve ? pvp : pve) : (pvp || pve));
|
||||
|
||||
if (els.dataUpdated) {
|
||||
els.dataUpdated.textContent =
|
||||
`Data last updated: ${newest ? newest.toLocaleString() : '—'}`;
|
||||
els.dataUpdated.title = newest ? newest.toISOString() : '';
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Rendering =====
|
||||
function renderUnknownCard(trader) {
|
||||
const card = document.createElement('article');
|
||||
card.className = 'trader-card';
|
||||
card.setAttribute('role', 'listitem');
|
||||
|
||||
const figure = document.createElement('figure');
|
||||
figure.className = 'trader-figure';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.className = 'trader-img';
|
||||
img.alt = `${trader.name || 'Trader'}`;
|
||||
img.loading = 'lazy';
|
||||
img.src = trader.imageLink || 'https://tarkov.dev/images/traders/default.jpg';
|
||||
figure.appendChild(img);
|
||||
|
||||
const name = document.createElement('div');
|
||||
name.className = 'trader-name';
|
||||
name.textContent = trader.name || 'Unknown trader';
|
||||
|
||||
const timer = document.createElement('div');
|
||||
timer.className = 'trader-countdown';
|
||||
timer.textContent = 'Unknown';
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'trader-meta';
|
||||
meta.textContent = 'Next reset: —';
|
||||
|
||||
card.appendChild(figure);
|
||||
card.appendChild(name);
|
||||
card.appendChild(timer);
|
||||
card.appendChild(meta);
|
||||
els.grid.appendChild(card);
|
||||
}
|
||||
|
||||
function render(traders) {
|
||||
els.grid.innerHTML = '';
|
||||
const now = Date.now();
|
||||
|
||||
traders.forEach(t => {
|
||||
const nextReset = toMsEpoch(t.resetTime);
|
||||
if (DEBUG_TS) {
|
||||
console.debug('[trader-resets] raw resetTime:', t.name, t.resetTime, '→', nextReset && new Date(nextReset).toISOString());
|
||||
}
|
||||
if (!Number.isFinite(nextReset)) {
|
||||
renderUnknownCard(t);
|
||||
return;
|
||||
}
|
||||
|
||||
const msLeft = nextReset - now;
|
||||
|
||||
const card = document.createElement('article');
|
||||
card.className = 'trader-card';
|
||||
card.setAttribute('role', 'listitem');
|
||||
|
||||
const figure = document.createElement('figure');
|
||||
figure.className = 'trader-figure';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.className = 'trader-img';
|
||||
img.alt = t.name || 'Trader';
|
||||
img.loading = 'lazy';
|
||||
img.src = t.imageLink || 'https://tarkov.dev/images/traders/default.jpg';
|
||||
figure.appendChild(img);
|
||||
|
||||
const name = document.createElement('div');
|
||||
name.className = 'trader-name';
|
||||
name.textContent = t.name || 'Unknown trader';
|
||||
|
||||
const timer = document.createElement('div');
|
||||
timer.className = 'trader-countdown';
|
||||
timer.textContent = fmtHMS(msLeft);
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'trader-meta';
|
||||
meta.textContent = `Next reset: ${new Date(nextReset).toLocaleTimeString()}`;
|
||||
|
||||
card.appendChild(figure);
|
||||
card.appendChild(name);
|
||||
card.appendChild(timer);
|
||||
card.appendChild(meta);
|
||||
els.grid.appendChild(card);
|
||||
|
||||
// Live ticking
|
||||
let tickId = setInterval(() => {
|
||||
const left = nextReset - Date.now();
|
||||
if (left <= 0) {
|
||||
timer.textContent = '00:00:00';
|
||||
setTimeout(() => { timer.textContent = 'Restocking…'; }, 500);
|
||||
clearInterval(tickId);
|
||||
return;
|
||||
}
|
||||
timer.textContent = fmtHMS(left);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Load from local JSON =====
|
||||
async function load() {
|
||||
try {
|
||||
const mode = currentMode(); // 'regular' or 'pve'
|
||||
const traders = await getLocalTraders(mode);
|
||||
render(traders);
|
||||
|
||||
// Update the unified "data last updated" bubble
|
||||
refreshUnifiedUpdatedBubble().catch(() => {});
|
||||
} catch (err) {
|
||||
console.error('[trader-resets] load failed:', err);
|
||||
if (els.dataUpdated) {
|
||||
els.dataUpdated.textContent = 'Data last updated: — (failed to read local JSON)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Persistence: restore & save mode =====
|
||||
if (els.modeToggle) {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved === 'pve') els.modeToggle.checked = true;
|
||||
if (saved === 'regular') els.modeToggle.checked = false;
|
||||
|
||||
els.modeToggle.addEventListener('change', () => {
|
||||
localStorage.setItem(STORAGE_KEY, currentMode());
|
||||
load(); // re-render immediately when mode flips
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Events =====
|
||||
els.refresh?.addEventListener('click', load);
|
||||
|
||||
// ===== Kickoff =====
|
||||
load();
|
||||
setInterval(load, REFRESH_MS);
|
||||
})();
|
||||
Reference in New Issue
Block a user