Optimized caching - only fetch on page load, instant tab switching
// ==UserScript==
// @name Nitro Type Startrack Leaderboard Integration
// @version 8.0
// @description Optimized caching - only fetch on page load, instant tab switching
// @author Combined Logic (SuperJoelzy + Captain.Loveridge)
// @license MIT
// @match https://www.nitrotype.com/*
// @grant GM_xmlhttpRequest
// @run-at document-idle
// @namespace https://greasyfork.org/users/1443935
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION ---
const TAB_CLASS = 'nt-custom-leaderboards';
const LEADERBOARD_PATH = '/leaderboards';
const CACHE_KEY = 'ntStartrackCache_';
const CACHE_TIMESTAMP_KEY = 'ntStartrackCacheTime_';
const CACHE_DURATION = 60 * 60 * 1000; // 60 minutes (1 hour)
const ASYNC_DELAY = 50;
// --- CACHE QUEUE STATE ---
let cacheQueue = [];
let isCaching = false;
let initialCacheComplete = false;
// --- STATE & DATA ---
let state = {
view: 'individual',
timeframe: 'season',
currentDate: new Date(),
dateRange: { start: null, end: null }
};
const SEASON_START = '2025-11-02 06:00:00';
const SEASON_END = '2025-11-30 08:00:00';
const timeframes = [
{ key: 'season', label: 'Season', hasNav: false },
{ key: '24hr', label: 'Last 24 Hours', hasNav: false },
{ key: '60min', label: '60 Minutes', hasNav: false },
{ key: '7day', label: 'Last 7 Days', hasNav: false },
{ key: 'daily', label: 'Daily', hasNav: true },
{ key: 'weekly', label: 'Weekly', hasNav: true },
{ key: 'monthly', label: 'Monthly', hasNav: true },
{ key: 'custom', label: 'Custom', hasNav: false }
];
// --- UTILITIES ---
function formatDate(date) {
if (!date) return '';
const d = new Date(date);
const y = d.getFullYear();
const m = ('0' + (d.getMonth() + 1)).slice(-2);
const day = ('0' + d.getDate()).slice(-2);
const h = ('0' + d.getHours()).slice(-2);
const min = ('0' + d.getMinutes()).slice(-2);
const s = ('0' + d.getSeconds()).slice(-2);
return `${y}-${m}-${day} ${h}:${min}:${s}`;
}
function getStartOfDay(date) {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
return d;
}
function getEndOfDay(date) {
const d = new Date(date);
d.setHours(23, 59, 59, 999);
return d;
}
function calculateDateRange(tempState) {
let start, end;
const current = new Date(tempState.currentDate);
const timeframe = tempState.timeframe || state.timeframe;
const now = new Date();
if (timeframe === 'season') {
return { start: SEASON_START, end: SEASON_END };
} else if (timeframe === '60min') {
end = now;
start = new Date(now.getTime() - (60 * 60 * 1000));
} else if (timeframe === '24hr') {
end = now;
start = new Date(now.getTime() - (24 * 60 * 60 * 1000));
} else if (timeframe === '7day') {
end = now;
start = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
} else if (timeframe === 'daily') {
start = getStartOfDay(current);
end = getEndOfDay(current);
} else if (timeframe === 'weekly') {
const dayOfWeek = current.getDay();
start = getStartOfDay(current);
start.setDate(start.getDate() - dayOfWeek);
end = new Date(start);
end.setDate(end.getDate() + 6);
end = getEndOfDay(end);
} else if (timeframe === 'monthly') {
start = new Date(current.getFullYear(), current.getMonth(), 1);
end = new Date(current.getFullYear(), current.getMonth() + 1, 0);
end = getEndOfDay(end);
} else if (timeframe === 'custom') {
start = tempState.dateRange?.start || getStartOfDay(now);
end = tempState.dateRange?.end || getEndOfDay(now);
}
return {
start: formatDate(start),
end: formatDate(end)
};
}
function navigateDate(direction) {
const current = state.currentDate;
const date = new Date(current);
if (state.timeframe === 'daily') {
date.setDate(current.getDate() + direction);
} else if (state.timeframe === 'weekly') {
date.setDate(current.getDate() + (7 * direction));
} else if (state.timeframe === 'monthly') {
date.setMonth(current.getMonth() + direction);
}
state.currentDate = date;
fetchLeaderboardData();
}
// --- PROGRESS INDICATOR ---
function setIndicator(message, isUpdating = true) {
const indicatorEl = document.getElementById('update-indicator');
if (indicatorEl) {
indicatorEl.textContent = message;
indicatorEl.style.color = isUpdating ? '#FFC107' : '#28A745';
document.querySelectorAll('[data-timeframe]').forEach(btn => {
btn.classList.remove('is-active', 'is-frozen');
if (btn.dataset.timeframe === state.timeframe) {
btn.classList.add('is-active', 'is-frozen');
}
});
document.querySelectorAll('[data-view]').forEach(btn => {
btn.classList.remove('is-active');
if (btn.dataset.view === state.view) {
btn.classList.add('is-active');
}
});
}
}
function getCacheKey(tempState) {
const s = tempState || state;
const ranges = calculateDateRange(s);
let startKey = ranges.start;
let endKey = ranges.end;
if (s.timeframe === '60min' || s.timeframe === '24hr' || s.timeframe === '7day') {
const roundToHour = (dateStr) => {
const date = new Date(dateStr.replace(' ', 'T'));
date.setMinutes(0, 0, 0);
return formatDate(date);
};
startKey = roundToHour(ranges.start);
endKey = roundToHour(ranges.end);
}
return `${CACHE_KEY}${s.view}_${s.timeframe}_${startKey}_${endKey}`;
}
// --- BUILD HTML ---
function buildLeaderboardHTML() {
const currentTF = timeframes.find(t => t.key === state.timeframe);
const hasNav = currentTF?.hasNav || false;
const isCustom = state.timeframe === 'custom';
return `
<section class="card card--b card--o card--shadow card--f card--grit well well--b well--l">
<div class="card-cap bg--gradient">
<h1 class="h2 tbs">Startrack Leaderboards</h1>
</div>
<div class="well--p well--l_p">
<div class="row row--o well well--b well--l">
<div class="tabs tabs--a tabs--leaderboards">
<button class="tab" data-view="individual">
<div class="bucket bucket--c bucket--xs"><div class="bucket-media"><svg class="icon icon-racer"><use xlink:href="/dist/site/images/icons/icons.css.svg#icon-racer"></use></svg></div><div class="bucket-content">Top Racers</div></div>
</button>
<button class="tab" data-view="team">
<div class="bucket bucket--c bucket--xs"><div class="bucket-media"><svg class="icon icon-team"><use xlink:href="/dist/site/images/icons/icons.css.svg#icon-team"></use></svg></div><div class="bucket-content">Top Teams</div></div>
</button>
</div>
<div class="card card--d card--o card--sq card--f">
<div class="well--p well--pt">
<div class="row row--o has-btn">
${timeframes.map(tf => `<button type="button" class="btn btn--dark btn--outline btn--thin" data-timeframe="${tf.key}">${tf.label}</button>`).join('')}
</div>
<div class="divider divider--a mbf"></div>
<div class="seasonLeader seasonLeader--default">
<div class="split split--start row">
<div class="split-cell">
<h1 class="seasonLeader-title" id="date-title">Loading...</h1>
<p class="seasonLeader-date" id="date-range"></p>
</div>
</div>
<div class="split-cell tac tsm" style="align-self: flex-end; margin-bottom: 5px;">
<span id="update-indicator">Loading...</span>
</div>
</div>
${hasNav ? `
<div class="row row--o mtm">
<button class="btn btn--secondary btn--thin" id="nav-prev">< Previous</button>
<button class="btn btn--secondary btn--thin" id="nav-today">Today</button>
<button class="btn btn--secondary btn--thin" id="nav-next">Next ></button>
</div>
` : ''}
${isCustom ? `
<div class="row row--o mtm">
<label class="tsm tc-ts">Start:</label>
<input type="date" id="start-date" class="input input--mini mlxs mrm" value="${state.dateRange.start ? state.dateRange.start.toISOString().split('T')[0] : ''}">
<label class="tsm tc-ts">End:</label>
<input type="date" id="end-date" class="input input--mini mlxs mrm" value="${state.dateRange.end ? state.dateRange.end.toISOString().split('T')[0] : ''}">
<button class="btn btn--primary btn--thin" id="update-custom">Update</button>
</div>
` : ''}
<div id="leaderboard-table-container">
<div class="tac pxl mtl">
<div class="loading-spinner loading-spinner--ts" style="margin: 0 auto;"></div>
<div class="mtm">Loading content...</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
`;
}
// --- CACHE MANAGEMENT ---
function cleanOldCache() {
try {
const keys = Object.keys(localStorage);
const cacheKeys = keys.filter(k => k.startsWith(CACHE_KEY));
if (cacheKeys.length > 50) {
console.log(`Cleaning cache: ${cacheKeys.length} items found, keeping newest 50`);
cacheKeys.sort().slice(0, cacheKeys.length - 50).forEach(key => {
localStorage.removeItem(key);
});
}
} catch (e) {
console.error('Error cleaning cache:', e);
}
}
function isCacheFresh(cacheKey) {
const timestampKey = CACHE_TIMESTAMP_KEY + cacheKey;
const timestamp = localStorage.getItem(timestampKey);
if (!timestamp) return false;
const age = Date.now() - parseInt(timestamp);
return age < CACHE_DURATION;
}
function saveToCache(cacheKey, data) {
try {
localStorage.setItem(cacheKey, data);
localStorage.setItem(CACHE_TIMESTAMP_KEY + cacheKey, Date.now().toString());
console.log('✓ Cached:', cacheKey);
} catch (quotaError) {
if (quotaError.name === 'QuotaExceededError') {
console.log('⚠ Quota exceeded - clearing old cache and retrying');
cleanOldCache();
try {
localStorage.setItem(cacheKey, data);
localStorage.setItem(CACHE_TIMESTAMP_KEY + cacheKey, Date.now().toString());
console.log('✓ Cached after cleanup:', cacheKey);
} catch (e2) {
console.error('✗ Still cannot save - cache data too large, skipping cache');
}
}
}
}
// --- RENDER TABLE ---
function renderTable(data) {
const container = document.getElementById('leaderboard-table-container');
if (!container) return;
if (!data || data.length === 0) {
container.innerHTML = '<div class="tac pxl tsm tc-ts">No data available.</div>';
return;
}
const top100 = data.slice(0, 100);
const isIndividual = state.view === 'individual';
let html = '<table class="table table--selectable table--striped table--fixed table--leaderboard">';
html += '<thead class="table-head"><tr class="table-row">';
if (isIndividual) {
html += `<th scope="col" class="table-cell table-cell--place"></th><th scope="col" class="table-cell table-cell--racer">Team</th><th scope="col" class="table-cell table-cell--racer">Display Name</th><th scope="col" class="table-cell table-cell--speed">WPM</th><th scope="col" class="table-cell table-cell--races">Accuracy</th><th scope="col" class="table-cell table-cell--races">Races</th><th scope="col" class="table-cell table-cell--points">Points</th>`;
} else {
html += `<th scope="col" class="table-cell table-cell--place"></th><th scope="col" class="table-cell table-cell--racer">Team Tag</th><th scope="col" class="table-cell table-cell--speed">WPM</th><th scope="col" class="table-cell table-cell--races">Accuracy</th><th scope="col" class="table-cell table-cell--races">Races</th><th scope="col" class="table-cell table-cell--points">Points</th>`;
}
html += '</tr></thead><tbody class="table-body">';
top100.forEach((item, index) => {
const rank = index + 1;
let rowClass = 'table-row';
let medalHTML = `<div class="mhc"><span class="h3 tc-ts">${rank}</span></div>`;
if (rank === 1) {
rowClass = 'table-row table-row--gold';
medalHTML = '<img class="db" src="/dist/site/images/medals/gold-sm.png">';
} else if (rank === 2) {
rowClass = 'table-row table-row--silver';
medalHTML = '<img class="db" src="/dist/site/images/medals/silver-sm.png">';
} else if (rank === 3) {
rowClass = 'table-row table-row--bronze';
medalHTML = '<img class="db" src="/dist/site/images/medals/bronze-sm.png">';
}
const wpm = parseFloat(item.WPM).toFixed(1);
const acc = (parseFloat(item.Accuracy) * 100).toFixed(2);
const points = Math.round(parseFloat(item.Points)).toLocaleString();
html += `<tr class="${rowClass}"><td class="table-cell table-cell--place tac">${medalHTML}</td>`;
if (isIndividual) {
const teamTag = item.TeamTag || '--';
const displayName = item.CurrentDisplayName || item.Username;
const tagColor = item.tagColor || 'fff';
html += `<td class="table-cell table-cell--racer" style="color: #${tagColor}">${teamTag}</td><td class="table-cell table-cell--racer"><a href="https://www.nitrotype.com/racer/${item.Username}" target="_blank" class="link">${displayName}</a></td><td class="table-cell table-cell--speed">${wpm}</td><td class="table-cell table-cell--races">${acc}%</td><td class="table-cell table-cell--races">${item.Races}</td><td class="table-cell table-cell--points">${points}</td>`;
} else {
const teamTag = item.TeamTag || '----';
const teamName = item.TeamName || `${item.TeamTag || 'Unknown'} Team`;
const tagColor = item.tagColor || 'B3C8DD';
html += `
<td class="table-cell table-cell--racer">
<a href="https://www.nitrotype.com/team/${teamTag}" target="_blank" class="link" style="color: #${tagColor};">[${teamTag}]</a>
${teamName}
</td>
<td class="table-cell table-cell--speed">${wpm}</td>
<td class="table-cell table-cell--races">${acc}%</td>
<td class="table-cell table-cell--races">${item.Races}</td>
<td class="table-cell table-cell--points">${points}</td>
`;
}
html += '</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
// Show "Last updated" timestamp
const cacheKey = getCacheKey();
const timestamp = localStorage.getItem(CACHE_TIMESTAMP_KEY + cacheKey);
if (timestamp) {
const updateTime = new Date(parseInt(timestamp));
const timeString = updateTime.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
setIndicator(`Last updated: ${timeString}`, false);
} else {
setIndicator('Updated', false);
}
// Start background pre-caching only once on initial page load
if (!initialCacheComplete && !isCaching) {
initialCacheComplete = true;
isCaching = true;
setTimeout(() => {
populateCacheQueue();
cacheAllViews();
}, 1000);
}
}
function updateDateDisplay() {
const titleEl = document.getElementById('date-title');
const rangeEl = document.getElementById('date-range');
if (!titleEl || !rangeEl) return;
const ranges = calculateDateRange(state);
const start = new Date(ranges.start.replace(' ', 'T'));
const end = new Date(ranges.end.replace(' ', 'T'));
if (state.timeframe === 'season') {
titleEl.textContent = 'Season';
rangeEl.textContent = 'Nov 2 - Nov 30, 2025';
} else if (state.timeframe === 'daily') {
titleEl.textContent = 'Daily';
rangeEl.textContent = start.toLocaleDateString();
} else if (state.timeframe === 'weekly') {
titleEl.textContent = 'Weekly';
rangeEl.textContent = `${start.toLocaleDateString()} - ${end.toLocaleDateString()}`;
} else if (state.timeframe === 'monthly') {
titleEl.textContent = 'Monthly';
rangeEl.textContent = start.toLocaleDateString(undefined, { month: 'long', year: 'numeric' });
} else if (state.timeframe === 'custom') {
titleEl.textContent = 'Custom Range';
rangeEl.textContent = `${start.toLocaleDateString()} - ${end.toLocaleDateString()}`;
} else {
titleEl.textContent = timeframes.find(t => t.key === state.timeframe)?.label || 'Leaderboards';
rangeEl.textContent = '';
}
}
// --- CACHE QUEUE ---
function populateCacheQueue() {
cacheQueue = [];
const views = ['individual', 'team'];
const currentYear = state.currentDate.getFullYear();
const currentMonth = state.currentDate.getMonth();
const currentDay = state.currentDate.getDate();
const priorityTimeframes = ['season', '24hr', '7day'];
const now = new Date();
now.setMinutes(0, 0, 0);
timeframes.filter(t => priorityTimeframes.includes(t.key)).forEach(tf => {
views.forEach(view => {
cacheQueue.push({ view: view, timeframe: tf.key, currentDate: now });
});
});
const dynamicTFs = timeframes.filter(t => t.hasNav);
views.forEach(view => {
dynamicTFs.forEach(tf => {
let date = new Date(currentYear, currentMonth, currentDay);
cacheQueue.push({ view: view, timeframe: tf.key, currentDate: date });
});
});
const currentKey = getCacheKey();
cacheQueue = cacheQueue.filter(item => getCacheKey(item) !== currentKey);
console.log(`Cache queue populated with ${cacheQueue.length} items`);
}
function cacheAllViews() {
if (cacheQueue.length === 0) {
console.log('✓ All views pre-cached successfully!');
isCaching = false;
return;
}
const nextItem = cacheQueue.shift();
const nextKey = getCacheKey(nextItem);
if (localStorage.getItem(nextKey) && isCacheFresh(nextKey)) {
console.log(`✓ ${nextItem.view} ${nextItem.timeframe} already cached (${cacheQueue.length} remaining)`);
cacheAllViews();
return;
}
console.log(`⟳ Pre-caching ${nextItem.view} ${nextItem.timeframe} (${cacheQueue.length} remaining)`);
fetchFreshData(nextKey, nextItem.view, nextItem.timeframe, nextItem.currentDate, cacheAllViews);
}
// --- FETCH DATA ---
function fetchLeaderboardData(forceRefresh = false) {
const cacheKey = getCacheKey();
const cachedData = localStorage.getItem(cacheKey);
const isFresh = isCacheFresh(cacheKey);
updateDateDisplay();
// If we have fresh cache AND it's not a page reload, just show it - NO API call
if (cachedData && isFresh && !forceRefresh) {
try {
const data = JSON.parse(cachedData);
renderTable(data);
console.log('✓ Loaded from cache (no API call):', state.view, state.timeframe);
return;
} catch (e) {
console.error("Error parsing cached data:", e);
localStorage.removeItem(cacheKey);
}
}
// Cache is expired or missing - fetch fresh data
const container = document.getElementById('leaderboard-table-container');
if (container) {
container.innerHTML = `<div class="tac pxl mtl"><div class="loading-spinner loading-spinner--ts" style="margin: 0 auto;"></div><div class="mtm">Loading data...</div></div>`;
}
setIndicator('Updating...', true);
console.log('⟳ Fetching fresh data:', state.view, state.timeframe);
fetchFreshData(cacheKey);
}
function fetchFreshData(cacheKey, view = state.view, timeframe = state.timeframe, currentDate = state.currentDate, callback) {
const tempState = { view, timeframe, currentDate };
const ranges = calculateDateRange(tempState);
const apiUrl = view === 'individual'
? 'https://ntstartrack.org/api/individual-leaderboard'
: 'https://ntstartrack.org/api/team-leaderboard';
const cacheBuster = `cb=${new Date().getTime()}`;
const url = `${apiUrl}?start_time=${encodeURIComponent(ranges.start)}&end_time=${encodeURIComponent(ranges.end)}&showbot=FALSE&${cacheBuster}`;
if (view === state.view && timeframe === state.timeframe) {
setIndicator('Updating...', true);
}
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: function(response) {
if (response.status === 200) {
setTimeout(() => {
try {
if (!response.responseText || response.responseText.trim().length === 0) {
throw new Error('Empty response from API');
}
const trimmed = response.responseText.trim();
if (trimmed.startsWith('<') || trimmed.startsWith('<!DOCTYPE')) {
throw new Error('API returned HTML instead of JSON');
}
const data = JSON.parse(response.responseText);
if (!Array.isArray(data)) {
throw new Error('Invalid data format - expected array');
}
console.log('✓ Successfully parsed', data.length, 'items');
const top100 = data.slice(0, 100);
saveToCache(cacheKey, JSON.stringify(top100));
if (view === state.view && timeframe === state.timeframe) {
renderTable(data);
}
if (callback) callback();
} catch (e) {
console.error('Parse Error:', e);
if (view === state.view && timeframe === state.timeframe) {
setIndicator(`Update failed`, false);
}
if (callback) callback();
}
}, ASYNC_DELAY);
} else {
console.error('API Error:', response.status, response.statusText);
if (view === state.view && timeframe === state.timeframe) {
setIndicator(`Update failed`, false);
}
if (callback) callback();
}
},
onerror: function(error) {
console.error('Network Error:', error);
if (view === state.view && timeframe === state.timeframe) {
setIndicator('Update failed', false);
}
if (callback) callback();
}
});
}
// --- EVENT LISTENERS ---
function attachListeners() {
document.querySelectorAll('[data-view]').forEach(btn => {
btn.addEventListener('click', (e) => {
const view = e.currentTarget.dataset.view;
if (state.view !== view) {
state.view = view;
fetchLeaderboardData(false);
}
});
});
document.querySelectorAll('[data-timeframe]').forEach(btn => {
btn.addEventListener('click', (e) => {
const tf = e.currentTarget.dataset.timeframe;
if (state.timeframe !== tf) {
state.timeframe = tf;
state.currentDate = new Date();
fetchLeaderboardData(false);
}
});
});
document.getElementById('nav-prev')?.addEventListener('click', () => navigateDate(-1));
document.getElementById('nav-next')?.addEventListener('click', () => navigateDate(1));
document.getElementById('nav-today')?.addEventListener('click', () => {
state.currentDate = new Date();
fetchLeaderboardData(true);
});
document.getElementById('update-custom')?.addEventListener('click', () => {
const startInput = document.getElementById('start-date');
const endInput = document.getElementById('end-date');
const startVal = startInput?.value;
const endVal = endInput?.value;
if (startVal && endVal) {
state.dateRange.start = getStartOfDay(new Date(startVal + 'T00:00:00'));
state.dateRange.end = getEndOfDay(new Date(endVal + 'T00:00:00'));
fetchLeaderboardData(true);
}
});
}
// --- PAGE INTEGRATION ---
function renderLeaderboardPage(forceRefresh = false) {
const mainContent = document.querySelector('main.structure-content');
if (!mainContent) return;
mainContent.innerHTML = buildLeaderboardHTML();
attachListeners();
fetchLeaderboardData(forceRefresh);
setActiveTab();
setTabTitle();
}
function setActiveTab() {
document.querySelectorAll('.nav-list-item').forEach(li => li.classList.remove('is-current'));
const tab = document.querySelector('.' + TAB_CLASS);
if (tab) {
tab.classList.add('is-current');
}
}
function setTabTitle() {
if (window.location.pathname === LEADERBOARD_PATH) {
document.title = 'Leaderboards | Nitro Type';
}
}
function insertLeaderboardTab() {
if (document.querySelector(`a[href="${LEADERBOARD_PATH}"]`)) return;
const navList = document.querySelector('.nav-list');
if (!navList) return;
const li = document.createElement('li');
li.className = `nav-list-item ${TAB_CLASS}`;
li.innerHTML = `<a href="${LEADERBOARD_PATH}" class="nav-link"><span class="has-notify">Leaderboards</span></a>`;
const news = Array.from(navList.children).find(li =>
li.textContent.trim().includes('News')
);
if (news) news.before(li);
else navList.appendChild(li);
}
function handlePage() {
insertLeaderboardTab();
if (location.pathname === LEADERBOARD_PATH) {
if (document.getElementById('leaderboard-table-container')) {
setActiveTab();
return;
}
const observer = new MutationObserver((mutationsList, observer) => {
const main = document.querySelector('main.structure-content');
if (main && (main.children.length === 0 || main.querySelector('.error'))) {
renderLeaderboardPage();
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
const main = document.querySelector('main.structure-content');
if (main && (main.children.length === 0 || main.querySelector('.error'))) {
renderLeaderboardPage();
}
} else {
document.querySelector('.' + TAB_CLASS)?.classList.remove('is-current');
}
}
// --- INIT ---
const obs = new MutationObserver(handlePage);
obs.observe(document.body, { childList: true, subtree: true });
handlePage();
})();