// ==UserScript==
// @name MZ - NT Player Search
// @namespace douglaskampl
// @version 2.99
// @description Searches for players who match specific requirements (NC/NCA only)
// @author Douglas Vieira
// @match https://www.managerzone.com/?p=national_teams&type=senior
// @match https://www.managerzone.com/?p=national_teams&type=u21
// @icon https://yt3.googleusercontent.com/ytc/AIdro_mDHaJkwjCgyINFM7cdUV2dWPPnL9Q58vUsrhOmRqkatg=s160-c-k-c0x00ffffff-no-rj
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect mzlive.eu
// @connect pub-02de1c06eac643f992bb26daeae5c7a0.r2.dev
// @connect https://www.managerzone.com/
// @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
GM_addStyle(`@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');.nt-search-open-btn{display:inline-block;padding:4px 8px;color:white;font-weight:bold;text-decoration:none;font-size:12px;font-family:'Space Mono',monospace;background:linear-gradient(135deg, #ff6e40, #ff5252, #448aff);border-radius:4px;transition:all .2s ease-in-out;border:1px solid rgba(138,43,226,.1);text-shadow:1px 1px 2px rgba(0,0,0,.3);margin-left:10px;vertical-align:middle}.nt-search-open-btn:hover{background:linear-gradient(145deg,rgba(40,40,70,.9),rgba(50,50,90,.9));color:lightgray;text-decoration:none;box-shadow:inset 0 0 5px rgba(138,43,226,.2)}.nt-search-open-btn i{margin-left:5px;color:violet}.nt-search-container{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(.95);background:linear-gradient(135deg,#0a0a0a 0%,#1a1a2e 100%);color:#f0f0f0;padding:2rem;border-radius:12px;box-shadow:0 8px 32px rgba(83,11,237,.3),0 4px 8px rgba(0,0,0,.2);z-index:9999;visibility:hidden;width:800px;max-width:99%;opacity:0;transition:all .3s cubic-bezier(0.4,0,0.2,1);border:1px solid rgba(138,43,226,.1)}.nt-search-container.visible{visibility:visible;opacity:1;transform:translate(-50%,-50%) scale(1)}.nt-search-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:2rem;padding-bottom:1rem;border-bottom:1px solid rgba(138,43,226,.2)}.nt-search-header h2{font-family:'Space Mono',monospace;margin:0;color:violet;font-size:1.5rem;text-shadow:0 0 10px rgba(138,43,226,.5)}.nt-search-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;margin-bottom:1.5rem}.nt-search-field{display:flex;flex-direction:column;gap:.5rem}.nt-search-field label{color:#ff9966;font-size:.875rem;text-transform:uppercase;letter-spacing:1px}.nt-search-field select{padding:.75rem;border:1px solid rgba(138,43,226,.3);border-radius:8px;background:#1a1a2e;color:#f0f0f0;font-size:1rem;transition:all .2s;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23ff9966' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right .75rem center;background-size:1rem}.nt-search-field select:focus{outline:none;border-color:#ff9966;box-shadow:0 0 0 2px rgba(138,43,226,.2)}.nt-search-field select:disabled{opacity:0.5;cursor:not-allowed;background:#333}.nt-search-buttons{display:flex;justify-content:center;align-items:center;gap:1rem;margin-top:1rem}.nt-search-button{width:auto;max-width:300px;padding:0.5rem 1rem;background:#009b3a;color:#ffdf00;border:none;border-radius:8px;font-weight:500;font-size:0.9rem;cursor:pointer;transition:all .2s;text-transform:uppercase;letter-spacing:2px;box-shadow:0 4px 6px rgba(0,0,0,.1)}.nt-search-button:not(:disabled):hover{transform:translateY(-2px);box-shadow:0 6px 8px rgba(0,0,0,.2)}.nt-search-button:disabled{opacity:0.5;cursor:not-allowed;background:#666}.nt-search-log{margin-top:1rem;padding:1rem;background:rgba(26,26,46,.3);border-radius:8px;font-family:monospace;font-size:.875rem;max-height:150px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#6366f1 #1a1a2e}.nt-search-log::-webkit-scrollbar{width:8px;height:8px}.nt-search-log::-webkit-scrollbar-track{background:#1a1a2e;border-radius:4px}.nt-search-log::-webkit-scrollbar-thumb{background:#6366f1;border-radius:4px}.nt-search-log::-webkit-scrollbar-thumb:hover{background:#4834d4}.nt-search-log-entry{margin-bottom:.5rem;padding:.5rem;background:rgba(26,26,46,.5);border-radius:4px;color:#00ffff;animation:slideIn 0.3s ease-out forwards;opacity:0;transform:translateX(-20px)}@keyframes slideIn{from{opacity:0;transform:translateX(-20px)}to{opacity:1;transform:translateX(0)}}.nt-search-guestbook-link{position:fixed;bottom:1rem;right:1rem;color:#ff9966;transition:all .2s}.nt-search-guestbook-link:hover{color:#6366f1;transform:scale(1.1)}.nt-search-country-select{width:200px}.nt-search-country-select select{width:100%}.nt-search-loading{display:flex;justify-content:center;align-items:center;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(10,10,20,.9);z-index:10000;visibility:hidden;opacity:0;transition:opacity .3s}.nt-search-loading.visible{visibility:visible;opacity:1}.nt-orbital-spinner{position:relative;width:60px;height:60px}.nt-orbiter{position:absolute;width:10px;height:10px;border-radius:50%;top:50%;left:50%;margin:-5px}.nt-orbiter:nth-child(1){background:violet;animation:orbit1 2s linear infinite}.nt-orbiter:nth-child(2){background:#ff9966;animation:orbit2 2s linear infinite 0.2s}.nt-orbiter:nth-child(3){background:#00ffff;animation:orbit3 2s linear infinite 0.4s}@keyframes orbit1{0%{transform:rotate(0deg) translateX(25px) rotate(0deg) scale(1)}50%{transform:rotate(180deg) translateX(25px) rotate(-180deg) scale(0.7)}100%{transform:rotate(360deg) translateX(25px) rotate(-360deg) scale(1)}}@keyframes orbit2{0%{transform:rotate(120deg) translateX(25px) rotate(-120deg) scale(1)}50%{transform:rotate(300deg) translateX(25px) rotate(-300deg) scale(0.7)}100%{transform:rotate(480deg) translateX(25px) rotate(-480deg) scale(1)}}@keyframes orbit3{0%{transform:rotate(240deg) translateX(25px) rotate(-240deg) scale(1)}50%{transform:rotate(420deg) translateX(25px) rotate(-420deg) scale(0.7)}100%{transform:rotate(600deg) translateX(25px) rotate(-600deg) scale(1)}}.nt-search-results-button{width:auto;max-width:300px;padding:0.5rem 1rem;background:#009b3a;color:#ffdf00;border:none;border-radius:8px;font-weight:500;font-size:0.9rem;cursor:pointer;transition:all .2s;text-transform:uppercase;letter-spacing:2px;box-shadow:0 4px 6px rgba(0,0,0,.1);display:none}.nt-search-results-button:hover{transform:translateY(-2px);box-shadow:0 6px 8px rgba(0,0,0,.2)}.nt-search-results-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:linear-gradient(135deg,#0a0a0a 0%,#1a1a2e 100%);color:#f0f0f0;padding:0;border-radius:12px;z-index:10001;width:90%;height:90vh;overflow:hidden;box-shadow:0 8px 32px rgba(83,11,237,.3);animation:modalSlideIn 0.3s ease-out forwards}@keyframes modalSlideIn{from{opacity:0;transform:translate(-50%,-48%)}to{opacity:1;transform:translate(-50%,-50%)}}.nt-search-results-header{position:sticky;top:0;display:flex;justify-content:space-between;align-items:center;padding:1.5rem;background:inherit;border-bottom:1px solid rgba(138,43,226,.2);z-index:1}.nt-search-results-title{font-family:'Space Mono',monospace;margin:0;font-size:1.5rem;color:#fff;text-shadow:0 0 10px rgba(138,43,226,.5)}.nt-search-results-close{background:none;border:none;color:#ff9966;font-size:1.5rem;cursor:pointer;transition:all 0.2s;padding:0.5rem}.nt-search-results-close:hover{color:#6366f1;transform:scale(1.1)}.nt-search-results-content{padding:1.5rem;height:calc(90vh - 5rem);overflow-y:auto;scrollbar-width:thin;scrollbar-color:#6366f1 #1a1a2e}.nt-search-results-content::-webkit-scrollbar{width:8px}.nt-search-results-content::-webkit-scrollbar-track{background:#1a1a2e}.nt-search-results-content::-webkit-scrollbar-thumb{background:#6366f1;border-radius:4px}.nt-search-results-content::-webkit-scrollbar-thumb:hover{background:#4834d4}.nt-search-players-container{display:flex;flex-wrap:wrap;gap:1.5rem;margin:1rem 0}.nt-search-player-card{background:rgba(26,26,46,.5);border-radius:8px;padding:1rem;transition:all 0.2s;border:1px solid rgba(138,43,226,.1);flex:1 1 calc(50% - 1rem);box-sizing:border-box;display:flex;flex-direction:column;min-width:350px}.nt-search-player-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(83,11,237,.2)}.nt-search-player-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.nt-search-player-info{flex:1}.nt-search-player-name{font-size:1.1rem;font-weight:bold;color:#fff;margin:0 0 0.5rem 0}.nt-search-player-name a{color:inherit;text-decoration:none}.nt-search-player-name a:hover{color:violet}.nt-search-player-details{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.5rem;color:#ff9966;font-size:0.875rem}.nt-search-skills-list{margin-top:1rem;display:flex;flex-direction:column;gap:4px}.nt-search-skill-row{display:flex;align-items:center;background:transparent;padding:0;box-shadow:none;min-height:24px}.nt-search-skill-name{font-size:.8rem;color:#f0f0f0;flex-basis:80px;margin-right:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.nt-search-skill-value{display:flex;align-items:center;gap:4px;color:#ff9966;flex-shrink:0}.nt-search-skill-value img{height:.9em;width:auto;vertical-align:middle}.nt-search-skill-value-text{font-size:.8rem;white-space:nowrap}.nt-search-player-total-balls{margin-top:0.75rem;font-weight:bold;color:#ffcc66;text-align:right;font-size:0.9rem}.nt-search-results-pagination{display:flex;justify-content:center;align-items:center;gap:1rem;padding:1rem 0;border-top:1px solid rgba(138,43,226,.1);border-bottom:1px solid rgba(138,43,226,.1);margin:0 -1.5rem 1rem -1.5rem}.nt-search-results-pagination.bottom{border-top:1px solid rgba(138,43,226,.1);border-bottom:none;margin-top:1rem;margin-bottom:0}.nt-search-results-pagination.top{border-bottom:1px solid rgba(138,43,226,.1);border-top:none;margin-bottom:1rem;margin-top:0}.nt-search-pagination-button{background:#1a1a2e;color:#f0f0f0;border:1px solid rgba(138,43,226,.3);border-radius:4px;padding:0.5rem 1rem;cursor:pointer;transition:all 0.2s}.nt-search-pagination-button:not(:disabled):hover{background:#2a2a4e;transform:translateY(-1px)}.nt-search-pagination-button:disabled{opacity:0.5;cursor:not-allowed}.nt-search-pagination-info{color:#ff9966;font-size:0.875rem}.nt-search-export-button{background:#1a1a2e;color:#f0f0f0;border:1px solid rgba(138,43,226,.3);border-radius:4px;padding:0.5rem 1rem;cursor:pointer;transition:all 0.2s;margin-left:1rem}.nt-search-export-button:hover{background:#2a2a4e;transform:translateY(-1px)}.nt-search-export-button:active{transform:translateY(1px)}.nt-search-header-controls{display:flex;align-items:center;gap:1rem}`);
const MASSIVE_COUNTRIES = ['BR', 'CN', 'AR', 'SE', 'PL', 'TR'];
const PLAYERS_PER_PAGE = 10;
const ORDERED_SKILL_KEYS = [
"speed", "stamina", "playIntelligence", "passing", "shooting", "heading",
"keeping", "ballControl", "tackling", "aerialPassing", "setPlays", "experience"
];
class Logger {
constructor(container, flushInterval = 400) {
this.container = container;
this.flushInterval = flushInterval;
this.queue = [];
this.timeout = null;
this.scheduled = false;
}
getTimestamp() {
const now = new Date();
return `[${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}]`;
}
log(message, type = 'info') {
this.queue.push({ message: `${this.getTimestamp()} ${message}`, type });
if (!this.scheduled) {
this.scheduled = true;
this.timeout = setTimeout(() => this.flush(), this.flushInterval);
}
}
flush() {
if (!this.queue.length || !this.container) {
this.scheduled = false;
return;
}
const fragment = document.createDocumentFragment();
this.queue.forEach(({ message, type }) => {
const entry = document.createElement('div');
entry.className = `nt-search-log-entry ${type}`;
entry.textContent = message;
fragment.appendChild(entry);
});
this.container.appendChild(fragment);
this.container.scrollTop = this.container.scrollHeight;
this.queue = [];
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
this.scheduled = false;
}
}
class RequestQueue {
constructor(maxConcurrent = 5, delay = 100) {
this.queue = [];
this.maxConcurrent = maxConcurrent;
this.delay = delay;
this.running = 0;
this.processed = 0;
}
add(request) {
return new Promise((resolve, reject) => {
const wrappedRequest = async () => {
try {
await new Promise(res => setTimeout(res, this.delay));
const result = await request();
this.processed++;
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
this.processNext();
}
};
this.queue.push(wrappedRequest);
this.processNext();
});
}
processNext() {
while (this.running < this.maxConcurrent && this.queue.length > 0) {
this.running++;
const request = this.queue.shift();
request();
}
}
reset() {
this.queue = [];
this.running = 0;
this.processed = 0;
}
}
class ChunkProcessor {
constructor(chunkSize = 25) {
this.chunkSize = chunkSize;
}
async process(items, processFn, onChunkComplete) {
const chunks = this.createChunks(items);
let processed = 0;
for (const chunk of chunks) {
await Promise.all(chunk.map(processFn));
processed += chunk.length;
if (onChunkComplete) {
onChunkComplete(processed, items.length);
}
await new Promise(res => setTimeout(res, 50));
}
}
createChunks(items) {
const chunks = [];
for (let i = 0; i < items.length; i += this.chunkSize) {
chunks.push(items.slice(i, i + this.chunkSize));
}
return chunks;
}
}
class NTPlayerParser {
constructor(minRequirements) {
this.minRequirements = minRequirements;
this.logger = null;
}
parseSkills(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const rows = doc.querySelectorAll('.player_skills tr');
if (!rows.length) return null;
const skills = {};
let totalBalls = 0;
const totalBallsElement = doc.querySelector('td[title] span.bold');
if (totalBallsElement) {
totalBalls = parseInt(totalBallsElement.textContent, 10) || 0;
}
let skillRows = Array.from(rows);
if (skillRows.length > ORDERED_SKILL_KEYS.length) { skillRows = skillRows.slice(0, ORDERED_SKILL_KEYS.length); }
skillRows.forEach((row, index) => {
const valueCell = row.querySelector('.skillval');
if (!valueCell) return;
const rawValue = valueCell.textContent.replace(/[()]/g, "").trim();
const value = parseInt(rawValue, 10);
if (!isNaN(value)) {
skills[ORDERED_SKILL_KEYS[index]] = value;
}
});
if (Object.keys(skills).length === 0) return null;
ORDERED_SKILL_KEYS.forEach(key => {
if (!(key in skills)) {
skills[key] = 0;
}
});
if (!this.validateSkills(skills)) return null;
return { skills, totalBalls };
}
validateSkills(skills) {
return Object.entries(this.minRequirements)
.filter(([key]) => key in skills && typeof skills[key] === 'number')
.every(([key, minValue]) => skills[key] >= minValue);
}
async fetchAndParsePlayer(playerId, ntid, cid) {
const url = `https://www.managerzone.com/ajax.php?p=nationalTeams&sub=search&ntid=${ntid}&cid=${cid}&type=national_team&pid=${playerId}&sport=soccer`;
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
const html = await response.text();
return this.parseSkills(html);
} catch (error) {
if (this.logger) this.logger.log(`Error parsing player ${playerId}: ${error.message}`, 'error');
return null;
}
}
}
class PlayerData {
constructor(id, name, teamName, teamId, age, value, salary, totalBalls, skills) {
this.id = id;
this.name = name;
this.teamName = teamName;
this.teamId = teamId || null;
this.age = age;
this.value = value;
this.salary = salary;
this.totalBalls = totalBalls;
this.skills = skills;
}
toExcelRow() {
const row = {
'ID': this.id,
'Name': this.name,
'Team': this.teamName,
'Age': this.age,
'Value': this.value,
'Salary': this.salary,
'Total Balls': this.totalBalls,
};
ORDERED_SKILL_KEYS.forEach(key => {
row[NTPlayerSearcher.prototype.formatSkillName(key)] = this.skills[key] || 0;
});
return row;
}
}
class NTPlayerSearcher {
constructor() {
this.requestQueue = new RequestQueue(5, 100);
this.chunkProcessor = new ChunkProcessor(25);
this.searchValues = {
speed: 0,
stamina: 0,
playIntelligence: 0,
passing: 0,
shooting: 0,
heading: 0,
keeping: 0,
ballControl: 0,
tackling: 0,
aerialPassing: 0,
setPlays: 0,
experience: 0,
minAge: 16,
maxAge: 40,
totalBalls: 9,
country: '',
countryData: null
};
this.isSearching = false;
this.teamIds = new Set();
this.playerIds = new Map();
this.processedLeagues = 0;
this.totalLeagues = 0;
this.validPlayers = new Map();
this.loadingElement = null;
this.logger = null;
this.countries = [];
this.userCountry = null;
this.username = null;
this.currentResultsPage = 1;
this.resultsListeners = { prev: null, next: null, esc: null };
}
async fetchTopPlayers(country, page = 0, isU21 = false) {
try {
const baseUrl = `https://mzlive.eu/mzlive.php?action=list&type=top100&mode=players&country=${country}&cy=EUR`;
const url = isU21 ? `${baseUrl}&age=u21&page=${page}` : `${baseUrl}&page=${page}`;
const response = await this.requestQueue.add(() =>
new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
onload: res => resolve(res),
onerror: err => reject(err),
ontimeout: () => reject(new Error(`Timeout fetching Top100 page ${page}`))
});
})
);
const data = JSON.parse(response.responseText);
const players = (data.players || []).filter(player => {
return player.age >= this.searchValues.minAge && player.age <= this.searchValues.maxAge;
});
const playerEntries = players.map(player => [
player.id.toString(),
{
id: player.id.toString(),
name: player.name,
teamName: player.team_name,
teamId: player.team_id?.toString() || null,
age: player.age,
value: parseInt(player.value) || 0,
salary: 0
}
]);
this.playerIds = new Map([...this.playerIds, ...playerEntries]);
return players.map(player => player.id.toString());
} catch (error) {
if (this.logger) this.logger.log(`Error fetching Top100 players (page ${page}): ${error.message}`, 'error');
return [];
}
}
async fetchAllTop100Players(country) {
const maxPages = MASSIVE_COUNTRIES.includes(country) ? 20 : 5;
const isU21 = this.searchValues.maxAge <= 21;
const pages = Array.from({ length: maxPages + 1 }, (_, i) => i);
const chunkSize = 5;
const results = [];
if (this.logger) this.logger.log(`Fetching Top100 players...`);
for (let i = 0; i < pages.length; i += chunkSize) {
const chunk = pages.slice(i, i + chunkSize);
const chunkResults = await Promise.all(
chunk.map(page => this.fetchTopPlayers(country, page, isU21))
);
results.push(...chunkResults);
await new Promise(res => setTimeout(res, 100));
}
if (this.logger) this.logger.log(`Finished fetching Top100 players.`);
return results.flat();
}
async fetchCountriesList() {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://pub-02de1c06eac643f992bb26daeae5c7a0.r2.dev/json/countries.json',
onload: res => resolve(JSON.parse(res.responseText)),
onerror: err => reject(err),
ontimeout: () => reject(new Error('Timeout fetching countries list'))
});
});
}
async fetchUserCountry() {
const usernameElem = document.querySelector('#header-username');
if (!usernameElem) return { userCountry: null, username: null };
const username = usernameElem.textContent.trim();
try {
const response = await fetch(`https://www.managerzone.com/xml/manager_data.php?sport_id=1&username=${username}`);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const text = await response.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(text, "text/xml");
const parserError = xmlDoc.querySelector('parsererror');
if (parserError) {
console.error('XML parsing error:', parserError.textContent);
return { userCountry: null, username: username };
}
const countryCode = xmlDoc.querySelector('UserData')?.getAttribute('countryShortname') || null;
return { userCountry: countryCode, username: username };
} catch (error) {
console.error("Error fetching user country:", error);
if (this.logger) this.logger.log(`Error fetching user country: ${error.message}`, 'error');
return { userCountry: null, username: username };
}
}
async checkUserRole(ntid, cid, username) {
if (!ntid || !cid || !username) {
if (this.logger) this.logger.log("Missing ntid, cid, or username for role check.", "warn");
return false;
}
const url = `https://www.managerzone.com/ajax.php?p=nationalTeams&sub=team&ntid=${ntid}&cid=${cid}&type=national_team&sport=soccer`;
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const profileLinks = doc.querySelectorAll('table.padding a[href*="/?p=profile&uid="]');
for (const link of profileLinks) {
if (link.textContent.trim() === username) {
if (this.logger) this.logger.log(`User ${username} confirmed as NC/NCA.`, 'info');
return true;
}
}
if (this.logger) this.logger.log(`User ${username} is not NC or NCA for this country.`, 'info');
return false;
} catch (error) {
console.error(`Error checking user role: ${error.message}`);
if (this.logger) this.logger.log(`Error checking user role: ${error.message}`, 'error');
return false;
}
}
async init() {
this.createLoadingElement();
this.showLoading();
try {
const [countries, { userCountry, username }] = await Promise.all([
this.fetchCountriesList(),
this.fetchUserCountry()
]);
this.countries = countries || [];
this.userCountry = userCountry;
this.username = username;
let isAuthorized = false;
let userCountryData = null;
if (this.userCountry && this.username && this.countries.length > 0) {
userCountryData = this.countries.find(c => c.code === this.userCountry);
if (userCountryData) {
this.searchValues.country = this.userCountry;
this.searchValues.countryData = { ntid: userCountryData.ntid, cid: userCountryData.cid };
}
}
const tempLogContainer = document.createElement('div');
this.logger = new Logger(tempLogContainer);
if (userCountryData && this.username) {
isAuthorized = await this.checkUserRole(userCountryData.ntid, userCountryData.cid, this.username);
} else {
if (this.logger) this.logger.log("Could not find user country data or username.", "warn");
}
if (isAuthorized) {
const appended = await this.appendSearchTab();
if (!appended) {
throw new Error("Failed to append search tab elements.");
}
const logContainer = document.querySelector('.nt-search-log');
if (logContainer) {
this.logger.container = logContainer;
if (!this.userCountry) {
this.logger.log("Could not determine user country data.", "warn");
}
} else {
console.error("Log container not found after appending tab.");
this.logger = { log: console.log, flush: () => {} };
}
this.setUpEvents();
} else {
console.log("User is not authorized (NC/NCA) or required data missing. NT Search UI not added.");
if (this.logger) this.logger.log("User not authorized (NC/NCA) or essential data missing. Search tool disabled.", "warn");
}
} catch (error) {
console.error("Initialization failed:", error);
if (this.logger) {
this.logger.log(`Initialization failed: ${error.message}`, 'error');
} else {
alert(`Initialization failed: ${error.message}`);
}
}
finally {
if (this.logger) this.logger.flush();
this.hideLoading();
}
}
createLoadingElement() {
if (this.loadingElement) return;
this.loadingElement = document.createElement('div');
this.loadingElement.className = 'nt-search-loading';
const spinnerContainer = document.createElement('div');
spinnerContainer.className = 'nt-orbital-spinner';
spinnerContainer.innerHTML = `
<div class="nt-orbiter"></div>
<div class="nt-orbiter"></div>
<div class="nt-orbiter"></div>
`;
this.loadingElement.appendChild(spinnerContainer);
document.body.appendChild(this.loadingElement);
}
showLoading() {
if (this.loadingElement) {
this.loadingElement.classList.add('visible');
}
}
hideLoading() {
if (this.loadingElement) {
this.loadingElement.classList.remove('visible');
}
}
async getLeagueIds(countryCode) {
try {
const response = await this.requestQueue.add(() =>
new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://mzlive.eu/mzlive.php?action=list&type=leagues&country=${countryCode}`,
onload: res => resolve(res),
onerror: err => reject(err),
ontimeout: () => reject(new Error('Timeout fetching leagues'))
});
})
);
const leagues = JSON.parse(response.responseText);
const maxDivision = MASSIVE_COUNTRIES.includes(countryCode) ? 6 : 3;
return leagues.filter(league => {
const name = league.name.toLowerCase();
if (name.startsWith('div')) {
const divLevel = parseInt(name.split('.')[0].replace('div', ''));
if(isNaN(divLevel)) return true;
return divLevel <= maxDivision;
}
return true;
}).map(league => league.id);
} catch (error) {
if (this.logger) this.logger.log(`Error fetching leagues: ${error.message}`, 'error');
return [];
}
}
async getTeamIds(leagueId) {
try {
const response = await this.requestQueue.add(() =>
fetch(`https://www.managerzone.com/xml/team_league.php?sport_id=1&league_id=${leagueId}`)
);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for league ${leagueId}`);
const text = await response.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(text, "text/xml");
const parserError = xmlDoc.querySelector('parsererror');
if (parserError) {
console.error(`XML parsing error for league ${leagueId}:`, parserError.textContent);
throw new Error(`XML parsing error for league ${leagueId}`);
}
const teams = xmlDoc.getElementsByTagName('Team');
return Array.from(teams).map(team => team.getAttribute('teamId'));
} catch (error) {
if (this.logger) this.logger.log(`Error fetching teams for league ${leagueId}: ${error.message}`, 'error');
return [];
}
}
async processLeagueBatch(leagueIds) {
if (!leagueIds || leagueIds.length === 0) {
if (this.logger) this.logger.log("No league IDs to process.", "warn");
return;
}
if (this.logger) this.logger.log(`Processing ${leagueIds.length} leagues...`);
await this.chunkProcessor.process(
leagueIds,
async (leagueId) => {
try {
const teamIds = await this.getTeamIds(leagueId);
if (teamIds && teamIds.length > 0) {
teamIds.forEach(id => this.teamIds.add(id));
}
this.processedLeagues++;
} catch (error) {
if (this.logger) this.logger.log(`Failed to process league ${leagueId}: ${error}`, 'error');
}
}
);
if (this.logger) this.logger.log(`Finished processing leagues. Found ${this.teamIds.size} unique teams.`);
}
async fetchPlayerList(teamId) {
try {
const response = await this.requestQueue.add(() =>
fetch(`https://www.managerzone.com/xml/team_playerlist.php?sport_id=1&team_id=${teamId}`)
);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for team ${teamId}`);
const text = await response.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(text, "text/xml");
const parserError = xmlDoc.querySelector('parsererror');
if (parserError) {
console.error(`XML parsing error for team ${teamId}:`, parserError.textContent);
throw new Error(`XML parsing error for team ${teamId}`);
}
const teamPlayers = xmlDoc.querySelector('TeamPlayers');
if (!teamPlayers) {
if (this.logger) this.logger.log(`No TeamPlayers data found for team ${teamId}`, 'warn');
return;
}
const teamName = teamPlayers.getAttribute('teamName') || `Team ${teamId}`;
const actualTeamId = teamPlayers.getAttribute('teamId') || teamId;
const players = xmlDoc.getElementsByTagName('Player');
const targetCountry = this.searchValues.country.toLowerCase();
Array.from(players).forEach(player => {
const age = parseInt(player.getAttribute('age'));
const countryCodeAttr = player.getAttribute('countryShortname');
if(!countryCodeAttr) return;
const countryCode = countryCodeAttr.toLowerCase();
if (age >= this.searchValues.minAge && age <= this.searchValues.maxAge && countryCode === targetCountry) {
const playerId = player.getAttribute('id');
const playerName = player.getAttribute('name');
const value = parseInt(player.getAttribute('value')) || 0;
const salary = parseInt(player.getAttribute('salary')) || 0;
if (playerId && playerName) {
this.playerIds.set(playerId, {
id: playerId,
name: playerName,
teamName: teamName,
teamId: actualTeamId,
age: age,
value: value,
salary: salary
});
}
}
});
} catch (error) {
if (this.logger) this.logger.log(`Error fetching players for team ${teamId}: ${error.message}`, 'error');
}
}
async processTeamBatch(teamIds) {
if (!teamIds || teamIds.length === 0) {
if (this.logger) this.logger.log("No team IDs to process.", "warn");
return;
}
const totalTeams = teamIds.length;
let processedTeams = 0;
if (this.logger) this.logger.log(`Processing ${totalTeams} teams...`);
await this.chunkProcessor.process(
teamIds,
async (teamId) => {
await this.fetchPlayerList(teamId);
processedTeams++;
if (processedTeams % 100 === 0 || processedTeams === totalTeams) {
if (this.logger) this.logger.log(`Team processing: ${processedTeams}/${totalTeams}`);
}
}
);
if (this.logger) this.logger.log(`Finished processing teams. Found ${this.playerIds.size} potential players.`);
}
async searchForPlayers() {
if (!this.searchValues.country || !this.searchValues.countryData) {
if (this.logger) this.logger.log('Country not selected or country data missing.', 'error');
alert('Please ensure a country is selected.');
return;
}
this.teamIds = new Set();
this.playerIds = new Map();
this.processedLeagues = 0;
this.totalLeagues = 0;
this.validPlayers = new Map();
this.requestQueue.reset();
this.currentResultsPage = 1;
const countryCode = this.searchValues.country;
if (this.logger) this.logger.log(`Starting search for country: ${countryCode}`);
try {
if (this.searchValues.maxAge > 18) {
await this.fetchAllTop100Players(countryCode);
if (this.logger) this.logger.log(`Found ${this.playerIds.size} players from Top100.`);
}
const leagueIds = await this.getLeagueIds(countryCode);
this.totalLeagues = leagueIds.length;
if(this.totalLeagues === 0 && this.playerIds.size === 0){
if (this.logger) this.logger.log(`No leagues found and no top players matched. Stopping search.`, 'warn');
return;
}
await this.processLeagueBatch(leagueIds);
await this.processTeamBatch(Array.from(this.teamIds));
const { ntid, cid } = this.searchValues.countryData;
const ntPlayerParser = new NTPlayerParser(this.searchValues);
ntPlayerParser.logger = this.logger;
const playerEntries = Array.from(this.playerIds.entries());
if(playerEntries.length === 0) {
if (this.logger) this.logger.log('No potential players found after gathering IDs.', 'warn');
return;
}
if (this.logger) this.logger.log(`Processing skills for ${playerEntries.length} players...`);
let processedCount = 0;
let validCount = 0;
const totalPlayersToParse = playerEntries.length;
const yieldFrequency = 25;
await this.chunkProcessor.process(
playerEntries,
async ([playerId, playerData]) => {
try {
const parsedData = await ntPlayerParser.fetchAndParsePlayer(playerId, ntid, cid);
if (parsedData && parsedData.totalBalls >= this.searchValues.totalBalls) {
this.validPlayers.set(playerId, new PlayerData(
playerId,
playerData.name,
playerData.teamName,
playerData.teamId,
playerData.age,
playerData.value,
playerData.salary,
parsedData.totalBalls,
parsedData.skills
));
validCount++;
}
} catch (parseError) {
if (this.logger) this.logger.log(`Error processing player ${playerId} (${playerData.name}): ${parseError.message}`, 'error');
} finally {
processedCount++;
if (processedCount % 100 === 0 || processedCount === totalPlayersToParse) {
if (this.logger) this.logger.log(`Skill check progress: ${processedCount}/${totalPlayersToParse}`);
}
if (processedCount % yieldFrequency === 0) {
await new Promise(res => setTimeout(res, 0));
}
}
}
);
if (this.logger) this.logger.log('Finishing skill processing...');
await new Promise(resolve => setTimeout(resolve, 200));
const finalCount = this.validPlayers.size;
if (this.logger) this.logger.log(`Search complete: found ${finalCount} players matching all criteria.`);
const resultsButton = document.querySelector('.nt-search-results-button');
if(resultsButton) resultsButton.style.display = finalCount > 0 ? "inline-block" : "none";
return Array.from(this.validPlayers.keys());
} catch (error) {
if (this.logger) this.logger.log(`Critical error during search: ${error.message}`, 'error');
console.error('Search failed critically:', error);
alert(`An error occurred during the search: ${error.message}`);
} finally {
if (this.logger) this.logger.flush();
}
}
async performSearch() {
if (this.isSearching) {
if(this.logger) this.logger.log("Search already in progress.", "warn");
return;
}
if (!this.searchValues.country || !this.searchValues.countryData) {
alert("Please select a country before searching.");
return;
}
this.isSearching = true;
const internalSearchButton = document.querySelector('.nt-search-container .nt-search-button');
if(internalSearchButton) internalSearchButton.disabled = true;
const logContainer = document.querySelector('.nt-search-log');
const resultsButton = document.querySelector('.nt-search-results-button');
this.showLoading();
if(logContainer) logContainer.innerHTML = '';
if(resultsButton) resultsButton.style.display = 'none';
if (!this.logger || !this.logger.container) {
console.error("Logger not initialized before search start.");
const logCont = document.querySelector('.nt-search-log');
if(logCont) this.logger = new Logger(logCont);
else this.logger = { log: console.log, flush: () => {} };
}
try {
await this.searchForPlayers();
} catch (error) {
if (this.logger) this.logger.log(`Unhandled error during search execution: ${error.message}`, 'error');
console.error('Search execution failed:', error);
alert(`An unexpected error occurred: ${error.message}`);
} finally {
this.isSearching = false;
if(internalSearchButton) internalSearchButton.disabled = false;
this.hideLoading();
if(this.logger) this.logger.flush();
}
}
getFiltersAppliedText() {
const filters = [];
const countryName = this.countries.find(c => c.code === this.searchValues.country)?.name || this.searchValues.country;
if (countryName) {
filters.push(`Country: ${countryName}`);
}
filters.push(`Age: ${this.searchValues.minAge} - ${this.searchValues.maxAge}`);
filters.push(`Min Total Balls: ${this.searchValues.totalBalls}`);
ORDERED_SKILL_KEYS.forEach(skill => {
if (this.searchValues[skill] > 0) {
filters.push(`Min ${this.formatSkillName(skill)}: ${this.searchValues[skill]}`);
}
});
return filters.join('; ');
}
createPaginationControls(page, totalPages) {
const container = document.createElement('div');
container.className = 'nt-search-results-pagination';
if (totalPages > 1) {
const prevBtn = document.createElement('button');
prevBtn.className = 'nt-search-pagination-button';
prevBtn.textContent = 'Previous';
prevBtn.disabled = page === 1;
prevBtn.dataset.action = "prev";
const pageInfo = document.createElement('span');
pageInfo.className = 'nt-search-pagination-info';
pageInfo.textContent = `Page ${page} of ${totalPages}`;
const nextBtn = document.createElement('button');
nextBtn.className = 'nt-search-pagination-button';
nextBtn.textContent = 'Next';
nextBtn.disabled = page === totalPages;
nextBtn.dataset.action = "next";
container.appendChild(prevBtn);
container.appendChild(pageInfo);
container.appendChild(nextBtn);
}
return container;
}
renderResultsPage(page) {
const playersContainer = document.querySelector('.nt-search-players-container');
const paginationTopContainer = document.querySelector('.nt-search-results-pagination.top');
const paginationBottomContainer = document.querySelector('.nt-search-results-pagination.bottom');
const modalContent = document.querySelector('.nt-search-results-content');
if (!playersContainer || !paginationTopContainer || !paginationBottomContainer || !modalContent) return;
this.currentResultsPage = page;
playersContainer.textContent = '';
paginationTopContainer.textContent = '';
paginationBottomContainer.textContent = '';
const playersArray = Array.from(this.validPlayers.values())
.sort((a, b) => b.totalBalls - a.totalBalls);
const totalPages = Math.ceil(playersArray.length / PLAYERS_PER_PAGE);
const startIndex = (page - 1) * PLAYERS_PER_PAGE;
const pagePlayers = playersArray.slice(startIndex, startIndex + PLAYERS_PER_PAGE);
this.removePaginationListeners();
this.resultsListeners.prev = () => {
if (this.currentResultsPage > 1) {
this.renderResultsPage(this.currentResultsPage - 1);
if(modalContent) modalContent.scrollTop = 0;
}
};
this.resultsListeners.next = () => {
if (this.currentResultsPage < totalPages) {
this.renderResultsPage(this.currentResultsPage + 1);
if(modalContent) modalContent.scrollTop = 0;
}
};
const topControls = this.createPaginationControls(page, totalPages);
const bottomControls = this.createPaginationControls(page, totalPages);
paginationTopContainer.appendChild(topControls);
paginationBottomContainer.appendChild(bottomControls);
this.addPaginationListeners(paginationTopContainer);
this.addPaginationListeners(paginationBottomContainer);
const fragment = document.createDocumentFragment();
pagePlayers.forEach(player => {
let skillsHTML = '';
ORDERED_SKILL_KEYS.forEach((skillKey) => {
const value = player.skills[skillKey] || 0;
const skillName = this.formatSkillName(skillKey);
skillsHTML += `
<div class="nt-search-skill-row" title="${skillName}: ${value}">
<span class="nt-search-skill-name">${skillName}</span>
<div class="nt-search-skill-value">
<img src="/img/soccer/wlevel_${value}.gif" alt="${value}">
<span class="nt-search-skill-value-text">(${value})</span>
</div>
</div>
`;
});
const skillsContainerHTML = `<div class="nt-search-skills-list">${skillsHTML}</div>`;
const playerCard = document.createElement('div');
playerCard.className = 'nt-search-player-card';
playerCard.innerHTML = `
<div class="nt-search-player-header">
<div class="nt-search-player-info">
<h3 class="nt-search-player-name">
<a href="https://www.managerzone.com/?p=players&pid=${player.id}" target="_blank" title="View player profile (ID: ${player.id})">
${player.name}
</a>
</h3>
<div class="nt-search-player-details">
<span>Team: ${player.teamId ? `<a href="https://www.managerzone.com/?p=team&tid=${player.teamId}" target="_blank" title="View team profile">${player.teamName}</a>` : player.teamName}</span>
<span>Age: ${player.age}</span>
<span>Value: ${new Intl.NumberFormat('en-US').format(player.value)} USD</span>
</div>
</div>
</div>
${skillsContainerHTML}
<div class="nt-search-player-total-balls">Total Balls: ${player.totalBalls}</div>
`;
fragment.appendChild(playerCard);
});
playersContainer.appendChild(fragment);
}
addPaginationListeners(container) {
const prevBtn = container.querySelector('[data-action="prev"]');
const nextBtn = container.querySelector('[data-action="next"]');
if (prevBtn && this.resultsListeners.prev) prevBtn.addEventListener('click', this.resultsListeners.prev);
if (nextBtn && this.resultsListeners.next) nextBtn.addEventListener('click', this.resultsListeners.next);
}
removePaginationListeners() {
const containers = document.querySelectorAll('.nt-search-results-pagination');
containers.forEach(container => {
const prevBtn = container.querySelector('[data-action="prev"]');
const nextBtn = container.querySelector('[data-action="next"]');
if (prevBtn && this.resultsListeners.prev) prevBtn.removeEventListener('click', this.resultsListeners.prev);
if (nextBtn && this.resultsListeners.next) nextBtn.removeEventListener('click', this.resultsListeners.next);
});
}
showResults() {
if (this.validPlayers.size === 0) {
alert("No valid players found matching the criteria.");
return;
}
const existingModal = document.querySelector('.nt-search-results-modal');
if (existingModal) existingModal.remove();
this.removePaginationListeners();
if (this.resultsListeners.esc) {
document.removeEventListener('keydown', this.resultsListeners.esc);
this.resultsListeners.esc = null;
}
const modal = document.createElement('div');
modal.className = 'nt-search-results-modal';
const modalHeader = document.createElement('div');
modalHeader.className = 'nt-search-results-header';
const headerControls = document.createElement('div');
headerControls.className = 'nt-search-header-controls';
const closeButton = document.createElement('button');
closeButton.className = 'nt-search-results-close';
closeButton.innerHTML = '×';
closeButton.title = 'Close Results (Esc)';
const exportButton = document.createElement('button');
exportButton.className = 'nt-search-export-button';
exportButton.textContent = 'Export';
exportButton.title = 'Export results to Excel (.xlsx)';
exportButton.onclick = () => this.exportToExcel();
headerControls.appendChild(exportButton);
headerControls.appendChild(closeButton);
modalHeader.innerHTML = `
<div>
<h2 class="nt-search-results-title">Search Results (${this.validPlayers.size})</h2>
<div class="nt-search-results-filters" style="font-size: 0.8rem; color: #bbb; margin-top: 0.5rem; max-width: 700px; line-height: 1.4;">
<strong style="color: #ff9966;">Filters:</strong> ${this.getFiltersAppliedText()}
</div>
</div>`;
modalHeader.appendChild(headerControls);
const modalContent = document.createElement('div');
modalContent.className = 'nt-search-results-content';
const paginationTopContainer = document.createElement('div');
paginationTopContainer.className = 'nt-search-results-pagination top';
const playersContainer = document.createElement('div');
playersContainer.className = 'nt-search-players-container';
const paginationBottomContainer = document.createElement('div');
paginationBottomContainer.className = 'nt-search-results-pagination bottom';
modalContent.appendChild(paginationTopContainer);
modalContent.appendChild(playersContainer);
modalContent.appendChild(paginationBottomContainer);
modal.appendChild(modalHeader);
modal.appendChild(modalContent);
document.body.appendChild(modal);
this.renderResultsPage(this.currentResultsPage);
const closeModal = () => {
this.removePaginationListeners();
if (this.resultsListeners.esc) {
document.removeEventListener('keydown', this.resultsListeners.esc);
this.resultsListeners.esc = null;
}
modal.remove();
};
closeButton.addEventListener('click', closeModal);
this.resultsListeners.esc = (e) => {
if (e.key === 'Escape') {
closeModal();
}
};
document.addEventListener('keydown', this.resultsListeners.esc);
}
formatSkillName(skill) {
const names = {
speed: 'Speed',
stamina: 'Stamina',
playIntelligence: 'Play Int',
passing: 'Passing',
shooting: 'Shooting',
heading: 'Heading',
keeping: 'Keeping',
ballControl: 'Ball Ctrl',
tackling: 'Tackling',
aerialPassing: 'Aerial Pass',
setPlays: 'Set Plays',
experience: 'Experience'
};
return names[skill] || skill.charAt(0).toUpperCase() + skill.slice(1);
}
exportToExcel() {
if (this.validPlayers.size === 0) {
alert("No players to export.");
return;
}
try {
const dataToExport = Array.from(this.validPlayers.values())
.sort((a,b) => b.totalBalls - a.totalBalls)
.map(player => player.toExcelRow());
if(dataToExport.length === 0) {
alert("No data formatted for export.");
return;
}
const worksheet = XLSX.utils.json_to_sheet(dataToExport);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Players");
const date = new Date().toISOString().slice(0, 10);
const countryCode = this.searchValues.country || 'export';
const filename = `MZ_NT_Search_${countryCode}_${date}.xlsx`;
XLSX.writeFile(workbook, filename);
if (this.logger) this.logger.log(`Exported ${dataToExport.length} players to ${filename}`);
} catch (error) {
console.error("Excel export failed:", error);
alert(`Excel export failed: ${error.message}`);
if (this.logger) this.logger.log(`Excel export failed: ${error.message}`, 'error');
}
}
async appendSearchTab() {
const targetSelect = document.querySelector('#menuForm select#type');
if (!targetSelect) {
console.error('Target select element (#menuForm select#type) not found.');
return false;
}
const existingButton = document.querySelector('.nt-search-open-btn');
if (existingButton) existingButton.remove();
const existingContainer = document.querySelector('.nt-search-container');
if (existingContainer) existingContainer.remove();
const openButton = document.createElement('a');
openButton.href = '#';
openButton.className = 'nt-search-open-btn';
openButton.innerHTML = 'NTPlayerSearch <i class="fa fa-search" style="margin-left: 5px;"></i>';
openButton.title = 'Open National Team Player Search';
targetSelect.insertAdjacentElement('afterend', openButton);
const searchContainer = document.createElement('div');
searchContainer.className = 'nt-search-container';
const goText = ((lang) => ({
pt: 'Buscar', es: 'Buscar', fr: 'Chercher', de: 'Suchen', it: 'Cerca',
pl: 'Szukaj', tr: 'Ara', ro: 'Caută', sv: 'Sök', nl: 'Zoeken'
}[lang.slice(0, 2)] || 'Search'))(navigator.language);
const skillFields = ORDERED_SKILL_KEYS.map(key => [key, this.formatSkillName(key)]);
const skillsHTML = skillFields.map(([field, label]) => `
<div class="nt-search-field">
<label title="Minimum ${label}">${label}</label>
<select name="${field}" title="Select minimum ${label}">
${this.generateOptions(10, 0, field)}
</select>
</div>
`).join('');
const countryOptionsHTML = this.countries && this.countries.length > 0
? this.generateCountryOptions()
: `<option value="" disabled selected>Could not load countries</option>`;
searchContainer.innerHTML = `
<div class="nt-search-header">
<h2>NT Player Search</h2>
<button class="nt-search-results-close" title="Close Panel (Esc)" style="font-size: 1.2rem; padding: 0.3rem 0.6rem;">×</button>
</div>
<div class="nt-search-grid">
${skillsHTML}
<div class="nt-search-field">
<label title="Minimum Total Balls">Total Balls</label>
<select name="totalBalls" title="Select minimum Total Balls">
${this.generateOptions(110, 9, 'totalBalls')}
</select>
</div>
<div class="nt-search-field">
<label title="Minimum Age">Min Age</label>
<select name="minAge" title="Select minimum Age">
${this.generateOptions(96, 16, 'minAge')}
</select>
</div>
<div class="nt-search-field">
<label title="Maximum Age">Max Age</label>
<select name="maxAge" title="Select maximum Age">
${this.generateOptions(96, 16, 'maxAge')}
</select>
</div>
<div class="nt-search-country-select nt-search-field">
<label>Country</label>
<select name="country" required title="Select country (only your country is enabled)">
${countryOptionsHTML}
</select>
</div>
</div>
<div class="nt-search-buttons">
<button class="nt-search-button" title="Start searching">${goText}</button>
<button class="nt-search-results-button" style="display: none;" title="Show found players">Show Results</button>
</div>
<div class="nt-search-log" title="Search process log"></div>
<a href="https://www.managerzone.com/?p=guestbook&uid=8577497"
class="nt-search-guestbook-link"
target="_blank"
title="Visit Author's Guestbook">
<i class="fa fa-book" aria-hidden="true"></i>
</a>`;
document.body.appendChild(searchContainer);
const panelCloseButton = searchContainer.querySelector('.nt-search-header .nt-search-results-close');
if (panelCloseButton) {
panelCloseButton.addEventListener('click', () => {
searchContainer.classList.remove('visible');
});
}
return true;
}
generateCountryOptions() {
if (!this.countries || this.countries.length === 0) {
return `<option value="" disabled selected>Error loading countries</option>`;
}
const placeholder = `<option value="" disabled ${!this.userCountry ? 'selected' : ''}>Select country</option>`;
const options = this.countries
.sort((a, b) => a.name.localeCompare(b.name))
.map(country => {
const isUserCountry = country.code === this.userCountry;
const displayName = country.name === 'Czech Republic' ? 'Czechia' :
country.name === 'Macedonia' ? 'North Macedonia' : country.name;
const selectedAttr = isUserCountry ? 'selected' : '';
const disabledAttr = !isUserCountry ? 'disabled' : '';
return `
<option value="${country.code}"
data-ntid="${country.ntid}"
data-cid="${country.cid}"
${selectedAttr}
${disabledAttr}>
${displayName}
</option>`;
}).join('');
return placeholder + options;
}
generateOptions(max, min = 0, name) {
let optionsHTML = '';
const defaultValue = this.searchValues[name];
for (let i = min; i <= max; i++) {
const selected = (defaultValue === i) ? 'selected' : '';
optionsHTML += `<option value="${i}" ${selected}>${i}</option>`;
}
return optionsHTML;
}
handleSelectChange(e) {
const select = e.target;
const value = select.value;
if (select.name === 'country') {
const option = select.selectedOptions[0];
if (option && option.value) {
this.searchValues.country = value;
this.searchValues.countryData = {
ntid: option.dataset.ntid,
cid: option.dataset.cid
};
if(this.logger) this.logger.log(`Country set to: ${option.textContent.trim()}`);
} else {
this.searchValues.country = '';
this.searchValues.countryData = null;
if(this.logger) this.logger.log(`Country selection cleared.`, 'warn');
}
} else {
const numValue = parseInt(value);
if (!isNaN(numValue)) {
this.searchValues[select.name] = numValue;
if (select.name === 'minAge' && numValue > this.searchValues.maxAge) {
this.searchValues.maxAge = numValue;
const maxAgeSelect = document.querySelector('select[name="maxAge"]');
if (maxAgeSelect) maxAgeSelect.value = numValue;
} else if (select.name === 'maxAge' && numValue < this.searchValues.minAge) {
this.searchValues.minAge = numValue;
const minAgeSelect = document.querySelector('select[name="minAge"]');
if (minAgeSelect) minAgeSelect.value = numValue;
}
}
}
}
setUpEvents() {
const openButton = document.querySelector('.nt-search-open-btn');
const searchContainer = document.querySelector('.nt-search-container');
if (!searchContainer) {
console.error("Search container not found for event setup.");
return;
}
const internalSearchButton = searchContainer.querySelector('.nt-search-button');
const resultsButton = searchContainer.querySelector('.nt-search-results-button');
const selects = searchContainer.querySelectorAll('select');
const panelCloseButton = searchContainer.querySelector('.nt-search-header .nt-search-results-close');
if (openButton) {
openButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (searchContainer) {
searchContainer.classList.add('visible');
} else {
console.error("Search container not found when trying to open.");
}
});
} else {
console.error("Open button not found during event setup.");
}
if (!panelCloseButton?.onclick && panelCloseButton) {
panelCloseButton.addEventListener('click', () => {
if (searchContainer) searchContainer.classList.remove('visible');
});
}
document.addEventListener('click', (e) => {
if (searchContainer?.classList.contains('visible') &&
!searchContainer.contains(e.target) &&
!openButton?.contains(e.target)) {
searchContainer.classList.remove('visible');
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && searchContainer?.classList.contains('visible')) {
searchContainer.classList.remove('visible');
}
});
if(selects.length > 0){
selects.forEach(select => {
if (this.searchValues.hasOwnProperty(select.name)) {
select.value = this.searchValues[select.name];
}
select.addEventListener('change', (e) => this.handleSelectChange(e));
});
const countrySelect = searchContainer.querySelector('select[name="country"]');
if(countrySelect && this.searchValues.country){
countrySelect.value = this.searchValues.country;
} else if(countrySelect && !this.userCountry) {
countrySelect.selectedIndex = 0;
}
} else {
console.error("Select elements not found inside search container.");
}
if (internalSearchButton) {
internalSearchButton.addEventListener('click', () => this.performSearch());
} else {
console.error("Internal search button not found.");
}
if (resultsButton) {
resultsButton.addEventListener('click', () => this.showResults());
} else {
console.error("Results button not found.");
}
}
}
try {
const searcher = new NTPlayerSearcher();
if (document.readyState === 'interactive' || document.readyState === 'complete') {
searcher.init();
} else {
document.addEventListener('DOMContentLoaded', () => searcher.init());
}
} catch (e) {
console.error(e);
alert("Failed to initialize NTPlayerSearch. Check the console for details.");
}
})();