Files
EFTCOMPANION/quest-search-local.js
2026-06-25 21:26:53 +00:00

399 lines
13 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
'use strict';
const els = {
input: document.getElementById('questInput'),
button: document.getElementById('questBtn'),
status: document.getElementById('questStatus'),
results: document.getElementById('questResults'),
toolbar: document.querySelector('.quest-toolbar')
};
function showStatus(msg, ok = false) {
if (!els.status) return;
els.status.textContent = msg;
els.status.style.color = ok ? 'var(--fg)' : 'var(--fg-soft)';
}
function escapeHTML(s = '') {
return s.toString()
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
async function fetchJSON(url) {
try {
const res = await fetch(url, { cache: 'no-store' });
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
}
let allTasks = null;
async function loadLocalData() {
if (Array.isArray(allTasks)) return allTasks;
els.results?.setAttribute('aria-busy', 'true');
try {
const json = await fetchJSON('/quests.json');
const tasks =
(Array.isArray(json?.tasks) && json.tasks) ||
(Array.isArray(json?.data?.tasks) && json.data.tasks) ||
[];
// ✅ FIX: assign the cache
allTasks = tasks;
return allTasks;
} finally {
els.results?.removeAttribute('aria-busy');
}
}
function renderQuest(task) {
const wrap = document.createElement('article');
wrap.className = 'quest-card trader-card';
const h2 = document.createElement('h2');
h2.className = 'quest-name trader-name';
h2.textContent = task.name || 'Unknown Quest';
wrap.appendChild(h2);
if (task.trader || task.wikiLink) {
const meta = document.createElement('div');
meta.className = 'quest-meta';
if (task.trader) {
const box = document.createElement('div');
box.className = 'quest-trader';
box.style.display = 'flex';
box.style.alignItems = 'center';
if (task.trader.imageLink) {
const img = document.createElement('img');
img.src = task.trader.imageLink;
img.alt = task.trader.name || 'Trader';
img.loading = 'lazy';
img.width = 36;
img.height = 36;
img.style.borderRadius = '6px';
img.style.marginRight = '8px';
img.style.border = '1px solid rgba(0,255,102,0.2)';
box.appendChild(img);
}
const nm = document.createElement('span');
nm.textContent = task.trader.name || 'Trader';
box.appendChild(nm);
meta.appendChild(box);
}
if (task.wikiLink) {
const wiki = document.createElement('a');
wiki.href = task.wikiLink;
wiki.target = '_blank';
wiki.rel = 'noopener noreferrer';
wiki.className = 'btn';
wiki.style.marginLeft = 'auto';
wiki.textContent = 'Open Wiki';
meta.appendChild(wiki);
}
wrap.appendChild(meta);
}
const objectives = Array.isArray(task.objectives) ? task.objectives : [];
const ol = document.createElement('ol');
ol.className = 'quest-objectives';
if (!objectives.length) {
const li = document.createElement('li');
li.className = 'quest-objective';
li.textContent = 'No objectives listed.';
ol.appendChild(li);
} else {
for (const obj of objectives) {
const li = document.createElement('li');
li.className = 'quest-objective';
const lines = [];
const type = obj.type ? `<strong>${escapeHTML(obj.type)}</strong>` : '<strong>Objective</strong>';
const desc = escapeHTML(obj.description || '—');
const maps = (obj.maps && obj.maps.length)
? ` — Map: ${escapeHTML(obj.maps.map(m => m.normalizedName).join(', '))}`
: '';
lines.push(`${type}: ${desc}${maps}`);
if (typeof obj.count === 'number' && (obj.item || (obj.items && obj.items.length))) {
const baseName = obj.item?.name || obj.items?.[0]?.name || 'item';
const fir = obj.foundInRaid ? ' (FIR)' : '';
lines.push(`Hand in: ${escapeHTML(baseName)} × ${obj.count}${escapeHTML(fir)}`);
}
if (obj.targetNames && obj.targetNames.length && typeof obj.count === 'number') {
lines.push(`Eliminate: ${escapeHTML(obj.targetNames.join(', '))} × ${obj.count}`);
}
li.innerHTML = lines.map(l => `<div>${l}</div>`).join('');
ol.appendChild(li);
}
}
wrap.appendChild(ol);
const fr = task.finishRewards;
if (fr) {
const box = document.createElement('div');
box.className = 'quest-rewards';
function addRow(label, valueHTML) {
const row = document.createElement('div');
row.innerHTML = `<strong>${escapeHTML(label)}:</strong> ${valueHTML}`;
box.appendChild(row);
}
if (Array.isArray(fr.items) && fr.items.length) {
const grid = document.createElement('div');
grid.style.display = 'grid';
grid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(180px, 1fr))';
grid.style.gap = '8px';
for (const g of fr.items) {
const c = document.createElement('div');
c.style.display = 'flex';
c.style.alignItems = 'center';
c.style.gap = '8px';
if (g.item?.iconLink) {
const img = document.createElement('img');
img.src = g.item.iconLink;
img.alt = g.item.name || '';
img.width = 24;
img.height = 24;
img.style.border = '1px solid rgba(0,255,102,0.2)';
img.style.borderRadius = '4px';
c.appendChild(img);
}
const span = document.createElement('span');
const nm = g.item?.name || 'Item';
const ct = (typeof g.count === 'number') ? ` × ${g.count}` : '';
span.textContent = `${nm}${ct}`;
c.appendChild(span);
grid.appendChild(c);
}
const row = document.createElement('div');
row.innerHTML = `<strong>Items:</strong>`;
row.appendChild(grid);
box.appendChild(row);
}
if (Array.isArray(fr.traderStanding) && fr.traderStanding.length) {
const parts = fr.traderStanding.map(s => {
const t = s.trader?.name || 'Trader';
const v = typeof s.standing === 'number'
? `${s.standing > 0 ? '+' : ''}${s.standing}`
: (s.standing ?? '');
return `${t}: ${v}`;
});
addRow('Standing', escapeHTML(parts.join(', ')));
}
if (fr.offerUnlock) {
const list = Array.isArray(fr.offerUnlock) ? fr.offerUnlock : [fr.offerUnlock];
const ul = document.createElement('ul');
for (const u of list) {
const li = document.createElement('li');
const itemName = u?.item?.name || 'Item';
const traderName = u?.trader?.name || 'Trader';
li.textContent = `${itemName} @ ${traderName}`;
ul.appendChild(li);
}
const row = document.createElement('div');
row.innerHTML = `<strong>Offer unlocks:</strong>`;
row.appendChild(ul);
box.appendChild(row);
}
if (fr.skillLevelReward) {
const list = Array.isArray(fr.skillLevelReward) ? fr.skillLevelReward : [fr.skillLevelReward];
const ul = document.createElement('ul');
for (const s of list) {
const li = document.createElement('li');
const skillObj = s?.skill;
const skillName = skillObj?.name || skillObj?.id || 'Skill';
const lvl = typeof s?.level === 'number' ? ` +${s.level}` : '';
li.textContent = `${skillName}${lvl}`;
ul.appendChild(li);
}
const row = document.createElement('div');
row.innerHTML = `<strong>Skill rewards:</strong>`;
row.appendChild(ul);
box.appendChild(row);
}
if (fr.traderUnlock) {
const list = Array.isArray(fr.traderUnlock) ? fr.traderUnlock : [fr.traderUnlock];
const ul = document.createElement('ul');
for (const t of list) {
const li = document.createElement('li');
li.textContent = t?.name || t?.id || 'Trader unlock';
ul.appendChild(li);
}
const row = document.createElement('div');
row.innerHTML = `<strong>Trader unlocks:</strong>`;
row.appendChild(ul);
box.appendChild(row);
}
if (fr.craftUnlock) {
const list = Array.isArray(fr.craftUnlock) ? fr.craftUnlock : [fr.craftUnlock];
const ul = document.createElement('ul');
for (const p of list) {
const li = document.createElement('li');
li.textContent = p?.name || p?.id || 'Craft unlock';
ul.appendChild(li);
}
const row = document.createElement('div');
row.innerHTML = `<strong>Craft unlocks:</strong>`;
row.appendChild(ul);
box.appendChild(row);
}
if (fr.achievement) {
const list = Array.isArray(fr.achievement) ? fr.achievement : [fr.achievement];
const line = list.map(a => a?.name || a?.id || 'Achievement').join(', ');
addRow('Achievement', escapeHTML(line));
}
if (fr.customization) {
const list = Array.isArray(fr.customization) ? fr.customization : [fr.customization];
const line = list.map(c => c?.name || c?.id || 'Customization').join(', ');
addRow('Customization', escapeHTML(line));
}
if (box.children.length) wrap.appendChild(box);
}
return wrap;
}
function getOrCreateAltMatchesContainer() {
const id = 'questAltMatches';
let box = document.getElementById(id);
if (box) {
box.hidden = false;
return box;
}
const results = document.getElementById('questResults');
if (!results || !results.parentNode) return null;
box = document.createElement('div');
box.id = id;
box.className = 'quest-alt-matches';
results.parentNode.insertBefore(box, results);
return box;
}
function renderMatchList(matches) {
const altBox = getOrCreateAltMatchesContainer();
if (!altBox) return;
altBox.innerHTML = '';
if (!matches.length) {
altBox.hidden = true;
return;
}
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = `Show ${matches.length} match(es)`;
details.appendChild(summary);
const ul = document.createElement('ul');
ul.style.listStyle = 'disc';
ul.style.paddingLeft = '1.25rem';
for (const m of matches) {
const li = document.createElement('li');
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'linklike';
btn.textContent = m.trader?.name ? `${m.name}${m.trader.name}` : m.name;
btn.addEventListener('click', () => openTaskDetails(m.id));
li.appendChild(btn);
ul.appendChild(li);
}
details.appendChild(ul);
altBox.appendChild(details);
}
async function openTaskDetails(id) {
try {
showStatus('Loading task…');
els.results.innerHTML = '';
const list = await loadLocalData();
const task = list.find(t => t?.id === id);
if (!task) {
showStatus('Task not found', false);
return;
}
const card = renderQuest(task);
els.results.appendChild(card);
showStatus('Ready', true);
} catch (err) {
console.error('[QuestSearch] open details failed:', err);
showStatus('Failed to load task details.', false);
}
}
async function search() {
const q = (els.input?.value || '').trim();
const alt = getOrCreateAltMatchesContainer();
els.results.innerHTML = '';
if (alt) alt.innerHTML = '';
if (!q) {
if (alt) alt.hidden = true;
showStatus('Type a quest name to search.');
return;
}
try {
showStatus('Searching…');
const list = await loadLocalData();
const safe = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(safe, 'i');
const matches = (Array.isArray(list) ? list : []).filter(t => re.test(t.name || ''));
renderMatchList(matches);
showStatus(`Found ${matches.length} match(es)`, true);
if (matches.length === 1) await openTaskDetails(matches[0].id);
} catch (err) {
console.error('[QuestSearch] search failed:', err);
showStatus('Search failed. Check console.', false);
}
}
let lastEnterTs = 0;
const ENTER_DEBOUNCE_MS = 250;
els.button?.addEventListener('click', search);
els.input?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const now = Date.now();
if (now - lastEnterTs < ENTER_DEBOUNCE_MS) return;
lastEnterTs = now;
e.preventDefault();
search();
}
});
showStatus('Waiting to search…');
})();