feat: flatten awesomelist to 2-level nav like contraband, reuse crt- CSS classes

This commit is contained in:
jae 2026-04-04 03:14:27 +00:00
parent dd1d5adef5
commit f00daafaba
32 changed files with 534 additions and 303 deletions

View file

@ -931,83 +931,52 @@ def awesomelist_index():
return jsonify({'error': 'Data not available'}), 503 return jsonify({'error': 'Data not available'}), 503
return jsonify(db) return jsonify(db)
@app.route('/api/awesomelist/sector/<code>') @app.route('/api/awesomelist/<code>')
def awesomelist_sector(code): def awesomelist_sector_detail(code):
db = _load_awesomelist_index() """Serve flattened sector data (like contraband categories)"""
if not db: p = DATA_DIR / 'awesomelist' / f'sector_{code.upper()}.json'
return jsonify({'error': 'Data not available'}), 503
for s in db['sectors']:
if s['code'].lower() == code.lower():
return jsonify(s)
return jsonify({'error': 'Sector not found'}), 404
@app.route('/api/awesomelist/list/<slug>')
def awesomelist_detail(slug):
p = DATA_DIR / 'awesomelist' / f'{slug}.json'
if not p.exists(): if not p.exists():
return jsonify({'error': 'List not found'}), 404 return jsonify({'error': 'Sector not found'}), 404
with open(p, 'r') as f: with open(p, 'r') as f:
return json.load(f) data = json.load(f)
return jsonify(data)
@app.route('/api/awesomelist/search') @app.route('/api/awesomelist/search')
def awesomelist_search(): def awesomelist_search():
q = request.args.get('q', '').strip().lower() q = request.args.get('q', '').strip().lower()
if len(q) < 2: if len(q) < 2:
return jsonify({'query': q, 'results': [], 'total': 0}) return jsonify({'query': q, 'results': [], 'total': 0})
db = _load_awesomelist_index()
if not db:
return jsonify({'error': 'Data not available'}), 503
results = [] results = []
limit = 100 limit = 100
for sector in db['sectors']: al_dir = DATA_DIR / 'awesomelist'
for lst in sector['lists']: if not al_dir.exists():
if q in lst['title'].lower() or q in lst.get('description', '').lower(): return jsonify({'query': q, 'results': [], 'total': 0})
results.append({ for fp in sorted(al_dir.iterdir()):
'type': 'list', if not fp.name.startswith('sector_'):
'sector_code': sector['code'], continue
'sector_name': sector['name'], try:
'slug': lst['slug'], with open(fp) as f:
'title': lst['title'], sector = json.load(f)
'description': lst.get('description', ''), for sub in sector.get('subcategories', []):
'stars': lst.get('stars', ''), for entry in sub.get('entries', []):
'entry_count': lst['entry_count'] if q in entry.get('name', '').lower() or q in entry.get('description', '').lower():
}) results.append({
if len(results) >= limit: 'sector_code': sector['code'],
break 'sector_name': sector['name'],
if len(results) >= limit: 'subcategory': sub['name'],
break 'name': entry.get('name', ''),
# Also search individual list entries if few results 'url': entry.get('url', ''),
if len(results) < limit: 'description': entry.get('description', ''),
al_dir = DATA_DIR / 'awesomelist' 'starred': entry.get('starred', False)
if al_dir.exists(): })
for fp in sorted(al_dir.iterdir()): if len(results) >= limit:
if not fp.name.endswith('.json'): break
continue
try:
with open(fp) as f:
lst = json.load(f)
for sub in lst.get('subcategories', []):
for entry in sub.get('entries', []):
if q in entry.get('name', '').lower() or q in entry.get('description', '').lower():
results.append({
'type': 'entry',
'list_title': lst['title'],
'list_slug': lst['slug'],
'subcategory': sub['name'],
'name': entry['name'],
'url': entry.get('url', ''),
'description': entry.get('description', ''),
'stars': entry.get('stars', '')
})
if len(results) >= limit:
break
if len(results) >= limit:
break
except:
continue
if len(results) >= limit: if len(results) >= limit:
break break
except:
continue
if len(results) >= limit:
break
return jsonify({'query': q, 'results': results, 'total': len(results)}) return jsonify({'query': q, 'results': results, 'total': len(results)})
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False) app.run(host='0.0.0.0', port=5000, debug=False)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
/* /*
AWESOME LISTS // Propaganda Resource Controller RECON // Curated Lists Controller (flattened)
*/ */
(function () { (function () {
'use strict'; 'use strict';
@ -8,7 +8,7 @@
const root = document.getElementById('awesomelistRoot'); const root = document.getElementById('awesomelistRoot');
if (!root) return; if (!root) return;
let state = { view: 'sectors', data: null, searchTimeout: null, activeListSlug: null, activeSubIdx: null }; let state = { view: 'index', sectors: [], searchTimeout: null };
// ─── Utilities ─────────────────────────────────────── // ─── Utilities ───────────────────────────────────────
function esc(s) { function esc(s) {
@ -31,346 +31,322 @@
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
} }
// ─── API Fetch ───────────────────────────────────────
async function api(endpoint) { async function api(endpoint) {
const res = await fetch(API + endpoint); const res = await fetch(API + endpoint);
if (!res.ok) throw new Error(`API ${res.status}`); if (!res.ok) throw new Error(`API ${res.status}`);
return res.json(); return res.json();
} }
// ─── Render: Sectors (Level 1) ─────────────────────── // ─── Render: Index View ──────────────────────────────
function renderSectors(data) { function renderIndex(data) {
state.view = 'sectors'; state.view = 'index';
state.data = data; state.sectors = data.sectors || [];
let html = ''; let html = '';
// Stats bar // Stats bar
html += `<div class="al-stats-bar"> html += `<div class="crt-stats-bar">
<div class="al-stat"><span class="al-stat-label">DATABASE</span><span class="al-stat-value green"> ONLINE</span></div> <div class="crt-stat"><span class="crt-stat-label">DATABASE</span><span class="crt-stat-value"> ONLINE</span></div>
<div class="al-stat"><span class="al-stat-label">TOTAL LISTS</span><span class="al-stat-value amber">${fmt(data.total_lists)}</span></div> <div class="crt-stat"><span class="crt-stat-label">TOTAL ASSETS</span><span class="crt-stat-value">${fmt(data.total_entries)}</span></div>
<div class="al-stat"><span class="al-stat-label">TOTAL ENTRIES</span><span class="al-stat-value">${fmt(data.total_entries)}</span></div> <div class="crt-stat"><span class="crt-stat-label">SECTORS</span><span class="crt-stat-value">${data.total_sectors}</span></div>
<div class="al-stat"><span class="al-stat-label">SECTORS</span><span class="al-stat-value">${data.total_sectors}</span></div> <div class="crt-stat"><span class="crt-stat-label">STATUS</span><span class="crt-stat-value"> DECLASSIFIED</span></div>
<div class="al-stat"><span class="al-stat-label">STATUS</span><span class="al-stat-value green"> DECLASSIFIED</span></div>
</div>`; </div>`;
// Search // Search
html += `<div class="al-search-container"> html += `<div class="crt-search-container">
<span class="al-search-icon"></span> <span class="crt-search-icon"></span>
<input type="text" class="al-search-input" id="alSearch" placeholder="SEARCH ALL LISTS & ENTRIES..." autocomplete="off"> <input type="text" class="crt-search-input" id="crtSearch" placeholder="SEARCH ALL ASSETS..." autocomplete="off">
<span class="al-search-count" id="alSearchCount"></span> <span class="crt-search-count" id="crtSearchCount"></span>
</div>`; </div>`;
html += `<div class="al-search-results" id="alSearchResults"></div>`;
// Search results container
html += `<div class="crt-search-results" id="crtSearchResults"></div>`;
// Sector grid // Sector grid
html += `<div class="al-sector-grid" id="alSectorGrid">`; html += `<div class="crt-grid" id="crtGrid">`;
for (const sector of data.sectors) { for (const sec of state.sectors) {
html += `<div class="al-sector-card" data-code="${esc(sector.code)}"> if (sec.entry_count === 0) continue;
<div class="al-sector-icon">${sector.icon}</div> html += `<div class="crt-card" data-slug="${esc(sec.slug)}">
<div class="al-sector-code">${esc(sector.code)}</div> <span class="crt-card-icon">${sec.icon}</span>
<div class="al-sector-name">${esc(sector.name)}</div> <div class="crt-card-code">${esc(sec.code)}</div>
<div class="al-sector-meta"> <div class="crt-card-name">${esc(sec.name)}</div>
<span class="amber">${sector.list_count} LISTS</span> <div class="crt-card-meta">
<span>${fmt(sector.total_entries)} ENTRIES</span> <span class="crt-card-count">${fmt(sec.entry_count)} assets</span>
<span class="crt-card-stars">${sec.subcategory_count} sections</span>
</div> </div>
<div class="crt-card-status"> ACCESSIBLE</div>
</div>`; </div>`;
} }
html += `</div>`; html += `</div>`;
root.innerHTML = html; root.innerHTML = html;
bindSectorEvents(); bindIndexEvents();
} }
// ─── Render: Lists in Sector (Level 2) ────────────── // ─── Render: Sector Detail ───────────────────────────
function renderSector(sector) { function renderSector(sec) {
state.view = 'sector'; state.view = 'detail';
state.activeListSlug = null;
let html = ''; let html = '';
html += `<div class="al-back" id="alBackSectors">◄ BACK TO SECTORS</div>`; // Back button
html += `<div class="crt-back" id="crtBack">◄ BACK TO INDEX</div>`;
html += `<div class="al-detail-header"> // Detail header
<div class="al-detail-code">${esc(sector.code)}</div> html += `<div class="crt-detail-header">
<div class="al-detail-title">${sector.icon} ${esc(sector.name)}</div> <div class="crt-detail-code">${esc(sec.code)}</div>
<div class="al-detail-meta"> <div class="crt-detail-title">${esc(sec.icon)} ${esc(sec.name)}</div>
<span class="amber">${sector.list_count} LISTS</span> <div class="crt-detail-meta">
<span>${fmt(sector.total_entries)} ENTRIES</span> <span>${fmt(sec.total_entries)} ASSETS</span>
<span>${sec.subcategory_count} SECTIONS</span>
</div> </div>
</div>`; </div>`;
// Filter search // Search within sector
html += `<div class="al-search-container"> html += `<div class="crt-search-container">
<span class="al-search-icon"></span> <span class="crt-search-icon"></span>
<input type="text" class="al-search-input" id="alSectorFilter" placeholder="FILTER LISTS IN THIS SECTOR..." autocomplete="off"> <input type="text" class="crt-search-input" id="crtCatSearch" placeholder="FILTER THIS SECTOR..." autocomplete="off">
</div>`; </div>`;
// List cards // Subcategory grid (3-col cards)
html += `<div class="al-list-grid" id="alListGrid">`; html += `<div class="crt-sub-grid" id="crtSubGrid">`;
for (const lst of sector.lists) { for (let i = 0; i < sec.subcategories.length; i++) {
html += `<div class="al-list-card" data-slug="${esc(lst.slug)}"> const sub = sec.subcategories[i];
<div class="al-list-title">${esc(lst.title)}</div> if (sub.entries.length === 0) continue;
<div class="al-list-desc">${esc(lst.description || '')}</div> html += `<div class="crt-sub-card" data-sub-idx="${i}">
<div class="al-list-stats"> <div class="crt-sub-card-name">${esc(sub.name)}</div>
<span class="amber">${fmt(lst.entry_count)} entries</span> <div class="crt-sub-card-stats">
<span>${lst.subcategory_count} sections</span> <span>${sub.entries.length} items</span>
${lst.stars ? `<span>⭐ ${esc(lst.stars)}</span>` : ''}
</div> </div>
</div>`; </div>`;
} }
html += `</div>`; html += `</div>`;
// Detail panel // Shared detail panel
html += `<div id="alListDetail" style="display:none"></div>`; html += `<div class="crt-sub-detail" id="crtSubDetail" style="display:none">
<div class="crt-sub-detail-header" id="crtSubDetailHeader"></div>
<div class="crt-sub-detail-notes" id="crtSubDetailNotes"></div>
<div class="crt-sub-detail-entries" id="crtSubDetailEntries"></div>
</div>`;
root.innerHTML = html; root.innerHTML = html;
bindListEvents(sector); bindDetailEvents(sec);
} }
// ─── Render: Single List Detail (Level 3) ─────────── // ─── Render: Single Entry ────────────────────────────
function renderListDetail(listData) { function renderEntry(entry) {
state.activeSubIdx = null; const starClass = entry.starred ? ' starred' : '';
const panel = document.getElementById('alListDetail'); const starIcon = entry.starred ? '⭐' : '·';
if (!panel) return; let html = `<div class="crt-entry${starClass}">`;
html += `<span class="crt-entry-star">${starIcon}</span>`;
html += `<div class="crt-entry-content">`;
let html = `<div class="al-list-detail">`; if (entry.url) {
html += `<div class="al-list-detail-header">${esc(listData.title)}</div>`; html += `<div class="crt-entry-name"><a href="${esc(entry.url)}" target="_blank" rel="noopener">${esc(entry.name || entry.url)}</a></div>`;
if (listData.description) { } else if (entry.name) {
html += `<div class="al-list-detail-desc">${md(listData.description)}</div>`; html += `<div class="crt-entry-name">${md(entry.name)}</div>`;
}
if (listData.github_url) {
html += `<a href="${esc(listData.github_url)}" target="_blank" rel="noopener" class="al-list-detail-github">📂 ${esc(listData.github_url)}</a>`;
} }
// Subcategory cards if (entry.description) {
if (listData.subcategories && listData.subcategories.length > 0) { html += `<div class="crt-entry-desc">${md(entry.description)}</div>`;
html += `<div class="al-sub-grid" id="alSubGrid">`; }
for (let i = 0; i < listData.subcategories.length; i++) {
const sub = listData.subcategories[i]; if (entry.extra_links && entry.extra_links.length > 0) {
if (sub.entries.length === 0) continue; html += `<div class="crt-entry-extra">`;
html += `<div class="al-sub-card" data-sub-idx="${i}"> for (const link of entry.extra_links) {
<div class="al-sub-card-name">${esc(sub.name)}</div> html += `<a href="${esc(link.url)}" target="_blank" rel="noopener">${esc(link.name)}</a>`;
<div class="al-sub-card-count">${sub.entries.length} items</div>
</div>`;
} }
html += `</div>`; html += `</div>`;
html += `<div id="alEntriesPanel" style="display:none"></div>`;
} }
html += `</div>`; html += `</div></div>`;
panel.innerHTML = html; return html;
panel.style.display = 'block';
// Bind sub card clicks
panel.querySelectorAll('.al-sub-card').forEach(card => {
card.addEventListener('click', () => {
const idx = parseInt(card.dataset.subIdx);
if (state.activeSubIdx === idx) {
state.activeSubIdx = null;
card.classList.remove('active');
document.getElementById('alEntriesPanel').style.display = 'none';
return;
}
state.activeSubIdx = idx;
panel.querySelectorAll('.al-sub-card').forEach(c => c.classList.remove('active'));
card.classList.add('active');
renderEntries(listData.subcategories[idx]);
});
});
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// ─── Render: Entries ─────────────────────────────────
function renderEntries(sub) {
const panel = document.getElementById('alEntriesPanel');
if (!panel) return;
let html = `<div class="al-entries-panel">`;
html += `<div class="al-entries-header">${esc(sub.name)}${sub.entries.length} ITEMS</div>`;
for (const entry of sub.entries) {
html += `<div class="al-entry">`;
html += `<span class="al-entry-bullet">▸</span>`;
html += `<div class="al-entry-content">`;
if (entry.url) {
html += `<div class="al-entry-name"><a href="${esc(entry.url)}" target="_blank" rel="noopener">${esc(entry.name || entry.url)}</a>`;
} else {
html += `<div class="al-entry-name">${md(entry.name || '')}`;
}
if (entry.stars) {
html += `<span class="al-entry-stars">⭐ ${esc(entry.stars)}</span>`;
}
html += `</div>`;
if (entry.description) {
html += `<div class="al-entry-desc">${md(entry.description)}</div>`;
}
html += `</div></div>`;
}
html += `</div>`;
panel.innerHTML = html;
panel.style.display = 'block';
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
} }
// ─── Render: Search Results ────────────────────────── // ─── Render: Search Results ──────────────────────────
function renderSearchResults(data) { function renderSearchResults(data) {
const container = document.getElementById('alSearchResults'); const container = document.getElementById('crtSearchResults');
const countEl = document.getElementById('alSearchCount'); const countEl = document.getElementById('crtSearchCount');
const grid = document.getElementById('alSectorGrid'); const grid = document.getElementById('crtGrid');
if (!data.results || data.results.length === 0) { if (!data.results || data.results.length === 0) {
container.innerHTML = data.query ? '<div class="al-empty">NO MATCHING RESULTS FOUND</div>' : ''; container.innerHTML = data.query ? '<div class="crt-empty">NO MATCHING ASSETS FOUND</div>' : '';
countEl.textContent = data.query ? '0 RESULTS' : ''; countEl.textContent = data.query ? '0 RESULTS' : '';
if (grid) grid.style.display = data.query ? 'none' : ''; if (grid) grid.style.display = data.query ? 'none' : '';
return; return;
} }
countEl.textContent = `${data.total} RESULTS`; countEl.textContent = `${data.results.length} RESULTS`;
if (grid) grid.style.display = 'none'; if (grid) grid.style.display = 'none';
let html = ''; let html = '';
for (const r of data.results) { for (const r of data.results) {
html += `<div class="al-search-result">`; const starIcon = r.starred ? '⭐ ' : '';
if (r.type === 'list') { html += `<div class="crt-search-result">`;
html += `<div class="al-result-breadcrumb">${esc(r.sector_code)} // ${esc(r.sector_name)}</div>`; html += `<div class="crt-result-breadcrumb">${esc(r.sector_code)} // ${esc(r.sector_name)} / ${esc(r.subcategory)}</div>`;
html += `<div class="al-result-name">${esc(r.title)}`; html += `<div class="crt-result-name">${starIcon}`;
if (r.stars) html += `<span class="al-result-stars">⭐ ${esc(r.stars)}</span>`; if (r.url) {
html += `</div>`; html += `<a href="${esc(r.url)}" target="_blank" rel="noopener">${esc(r.name || r.url)}</a>`;
if (r.description) html += `<div class="al-result-desc">${md(r.description)}</div>`;
html += `<div style="font-family:'JetBrains Mono';font-size:0.6rem;color:#555;margin-top:0.3rem">${fmt(r.entry_count)} entries</div>`;
} else { } else {
html += `<div class="al-result-breadcrumb">${esc(r.list_title)} / ${esc(r.subcategory)}</div>`; html += md(r.name);
html += `<div class="al-result-name">`; }
if (r.url) { html += `</div>`;
html += `<a href="${esc(r.url)}" target="_blank" rel="noopener">${esc(r.name || r.url)}</a>`; if (r.description) {
} else { html += `<div class="crt-result-desc">${md(r.description)}</div>`;
html += md(r.name);
}
if (r.stars) html += `<span class="al-result-stars">⭐ ${esc(r.stars)}</span>`;
html += `</div>`;
if (r.description) html += `<div class="al-result-desc">${md(r.description)}</div>`;
} }
html += `</div>`; html += `</div>`;
} }
container.innerHTML = html; container.innerHTML = html;
} }
// ─── Event Bindings ────────────────────────────────── // ─── Event Bindings: Index ───────────────────────────
function bindSectorEvents() { function bindIndexEvents() {
document.querySelectorAll('.al-sector-card').forEach(card => { // Card clicks
document.querySelectorAll('.crt-card').forEach(card => {
card.addEventListener('click', () => { card.addEventListener('click', () => {
const code = card.dataset.code; const slug = card.dataset.slug;
const sector = state.data.sectors.find(s => s.code === code); loadSector(slug);
if (sector) {
history.pushState({ view: 'sector', code }, '', `?sector=${code}`);
renderSector(sector);
}
}); });
}); });
const searchInput = document.getElementById('alSearch'); // Global search
const searchInput = document.getElementById('crtSearch');
if (searchInput) { if (searchInput) {
searchInput.addEventListener('input', () => { searchInput.addEventListener('input', () => {
clearTimeout(state.searchTimeout); clearTimeout(state.searchTimeout);
const q = searchInput.value.trim(); const q = searchInput.value.trim();
if (q.length < 2) { if (q.length < 2) {
document.getElementById('alSearchResults').innerHTML = ''; renderSearchResults({ query: '', results: [] });
document.getElementById('alSearchCount').textContent = '';
const grid = document.getElementById('alSectorGrid');
if (grid) grid.style.display = '';
return; return;
} }
state.searchTimeout = setTimeout(async () => { state.searchTimeout = setTimeout(async () => {
try { try {
const data = await api(`/search?q=${encodeURIComponent(q)}`); const data = await api(`/search?q=${encodeURIComponent(q)}&limit=100`);
renderSearchResults(data); renderSearchResults(data);
} catch (e) { } catch (e) {
console.error(e); console.warn('Search failed:', e);
} }
}, 300); }, 300);
}); });
} }
} }
function bindListEvents(sector) { // ─── Event Bindings: Detail ──────────────────────────
function bindDetailEvents(sec) {
// Back button // Back button
document.getElementById('alBackSectors').addEventListener('click', () => { document.getElementById('crtBack').addEventListener('click', () => {
history.pushState({ view: 'sectors' }, '', window.location.pathname); loadIndex();
renderSectors(state.data);
}); });
// List card clicks // Subcategory card clicks
document.querySelectorAll('.al-list-card').forEach(card => { document.querySelectorAll('.crt-sub-card').forEach(card => {
card.addEventListener('click', async () => { card.addEventListener('click', () => {
const slug = card.dataset.slug; const idx = parseInt(card.dataset.subIdx);
if (state.activeListSlug === slug) { const sub = sec.subcategories[idx];
state.activeListSlug = null; const panel = document.getElementById('crtSubDetail');
card.classList.remove('active'); const headerEl = document.getElementById('crtSubDetailHeader');
document.getElementById('alListDetail').style.display = 'none'; const notesEl = document.getElementById('crtSubDetailNotes');
const entriesEl = document.getElementById('crtSubDetailEntries');
// If clicking the already active card, collapse
const wasActive = card.classList.contains('active');
document.querySelectorAll('.crt-sub-card').forEach(c => c.classList.remove('active'));
if (wasActive) {
panel.style.display = 'none';
return; return;
} }
state.activeListSlug = slug;
document.querySelectorAll('.al-list-card').forEach(c => c.classList.remove('active'));
card.classList.add('active'); card.classList.add('active');
const panel = document.getElementById('alListDetail'); // Populate header
panel.innerHTML = '<div class="al-loading">LOADING DOSSIER...</div>'; headerEl.innerHTML = `<span class="crt-sub-toggle">▾</span> <span class="crt-sub-detail-name">${esc(sub.name)}</span> <span class="crt-sub-count">${sub.entries.length} items</span>`;
panel.style.display = 'block';
try { // Populate notes
const listData = await api(`/list/${slug}`); if (sub.notes && sub.notes.length > 0) {
renderListDetail(listData); let nh = '';
} catch (e) { for (const note of sub.notes) nh += `${esc(note)}<br>`;
panel.innerHTML = '<div class="al-empty">FAILED TO LOAD DOSSIER</div>'; notesEl.innerHTML = nh;
notesEl.style.display = '';
} else {
notesEl.innerHTML = '';
notesEl.style.display = 'none';
} }
// Populate entries
let eh = '';
for (const entry of sub.entries) eh += renderEntry(entry);
entriesEl.innerHTML = eh;
panel.style.display = '';
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
}); });
}); });
// Filter // Category search (local filter on subcategory cards)
const filterInput = document.getElementById('alSectorFilter'); const catSearch = document.getElementById('crtCatSearch');
if (filterInput) { if (catSearch) {
filterInput.addEventListener('input', () => { catSearch.addEventListener('input', () => {
const q = filterInput.value.trim().toLowerCase(); const q = catSearch.value.trim().toLowerCase();
document.querySelectorAll('.al-list-card').forEach(card => { document.querySelectorAll('.crt-sub-card').forEach(card => {
const title = card.querySelector('.al-list-title').textContent.toLowerCase(); if (!q) {
const desc = card.querySelector('.al-list-desc').textContent.toLowerCase(); card.style.display = '';
card.style.display = (!q || title.includes(q) || desc.includes(q)) ? '' : 'none'; return;
}
const text = card.textContent.toLowerCase();
card.style.display = text.includes(q) ? '' : 'none';
}); });
}); });
} }
} }
// ─── History Navigation ────────────────────────────── // ─── Load Functions ──────────────────────────────────
window.addEventListener('popstate', (e) => { async function loadIndex() {
if (e.state && e.state.view === 'sector' && state.data) { root.innerHTML = '<div class="crt-loading">ACCESSING RECON DATABASE...</div>';
const sector = state.data.sectors.find(s => s.code === e.state.code);
if (sector) { renderSector(sector); return; }
}
if (state.data) renderSectors(state.data);
});
// ─── Init ────────────────────────────────────────────
async function init() {
root.innerHTML = '<div class="al-loading">ACCESSING PROPAGANDA DATABASE...</div>';
try { try {
const data = await api(''); const data = await api('');
state.data = data; renderIndex(data);
history.pushState({ view: 'index' }, '', '/recon/awesomelist');
// Check URL params
const params = new URLSearchParams(window.location.search);
const sectorCode = params.get('sector');
if (sectorCode) {
const sector = data.sectors.find(s => s.code.toLowerCase() === sectorCode.toLowerCase());
if (sector) { renderSector(sector); return; }
}
renderSectors(data);
} catch (e) { } catch (e) {
root.innerHTML = '<div class="al-empty">FAILED TO ACCESS DATABASE — RETRY LATER</div>'; root.innerHTML = `<div class="crt-empty">DATABASE ACCESS DENIED // ${esc(e.message)}</div>`;
console.error(e);
} }
} }
init(); async function loadSector(slug) {
root.innerHTML = '<div class="crt-loading">DECRYPTING SECTOR DATA...</div>';
try {
const data = await api(`/${slug}`);
renderSector(data);
history.pushState({ view: 'detail', slug }, '', `/recon/awesomelist?sector=${slug}`);
window.scrollTo({ top: 0, behavior: 'smooth' });
} catch (e) {
root.innerHTML = `<div class="crt-empty">SECTOR NOT FOUND // ${esc(e.message)}</div>`;
}
}
// ─── Handle Back/Forward ─────────────────────────────
window.addEventListener('popstate', (e) => {
if (e.state && e.state.view === 'detail' && e.state.slug) {
loadSector(e.state.slug);
} else {
loadIndex();
}
});
// ─── Init ────────────────────────────────────────────
function init() {
const params = new URLSearchParams(window.location.search);
const sector = params.get('sector');
if (sector) {
loadSector(sector);
} else {
loadIndex();
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})(); })();

View file

@ -3,12 +3,12 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JAESWIFT // AWESOME LISTS</title> <title>JAESWIFT // CURATED LISTS</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/section.css"> <link rel="stylesheet" href="/css/section.css">
<link rel="stylesheet" href="/css/awesomelist.css"> <link rel="stylesheet" href="/css/contraband.css">
</head> </head>
<body> <body>
<div class="scanline-overlay"></div> <div class="scanline-overlay"></div>
@ -36,16 +36,16 @@
<span class="separator">/</span> <span class="separator">/</span>
<a href="/depot/recon">RECON</a> <a href="/depot/recon">RECON</a>
<span class="separator">/</span> <span class="separator">/</span>
<span class="current">AWESOME LISTS</span> <span class="current">CURATED LISTS</span>
</div> </div>
<section class="section-header" style="padding-top: calc(var(--nav-height) + 1.5rem);"> <section class="section-header" style="padding-top: calc(var(--nav-height) + 1.5rem);">
<div class="section-header-label">RECON // CURATED INTELLIGENCE</div> <div class="section-header-label">RECON // CURATED INTELLIGENCE</div>
<h1 class="section-header-title">AWESOME LISTS</h1> <h1 class="section-header-title">CURATED LISTS</h1>
<p class="section-header-sub">&gt; 660+ curated dossiers covering 135,000+ resources across 28 sectors. Select a sector to begin.</p> <p class="section-header-sub">&gt; Curated dossiers covering resources across 28 sectors. Select a sector to begin.</p>
</section> </section>
<section class="al-container"> <section class="crt-container">
<div id="awesomelistRoot"></div> <div id="awesomelistRoot"></div>
</section> </section>