initial commit
This commit is contained in:
Executable
+398
@@ -0,0 +1,398 @@
|
||||
|
||||
(() => {
|
||||
'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('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
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…');
|
||||
})();
|
||||
Reference in New Issue
Block a user