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,73 +931,43 @@ 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']:
for lst in sector['lists']:
if q in lst['title'].lower() or q in lst.get('description', '').lower():
results.append({
'type': 'list',
'sector_code': sector['code'],
'sector_name': sector['name'],
'slug': lst['slug'],
'title': lst['title'],
'description': lst.get('description', ''),
'stars': lst.get('stars', ''),
'entry_count': lst['entry_count']
})
if len(results) >= limit:
break
if len(results) >= limit:
break
# Also search individual list entries if few results
if len(results) < limit:
al_dir = DATA_DIR / 'awesomelist' al_dir = DATA_DIR / 'awesomelist'
if al_dir.exists(): if not al_dir.exists():
return jsonify({'query': q, 'results': [], 'total': 0})
for fp in sorted(al_dir.iterdir()): for fp in sorted(al_dir.iterdir()):
if not fp.name.endswith('.json'): if not fp.name.startswith('sector_'):
continue continue
try: try:
with open(fp) as f: with open(fp) as f:
lst = json.load(f) sector = json.load(f)
for sub in lst.get('subcategories', []): for sub in sector.get('subcategories', []):
for entry in sub.get('entries', []): for entry in sub.get('entries', []):
if q in entry.get('name', '').lower() or q in entry.get('description', '').lower(): if q in entry.get('name', '').lower() or q in entry.get('description', '').lower():
results.append({ results.append({
'type': 'entry', 'sector_code': sector['code'],
'list_title': lst['title'], 'sector_name': sector['name'],
'list_slug': lst['slug'],
'subcategory': sub['name'], 'subcategory': sub['name'],
'name': entry['name'], 'name': entry.get('name', ''),
'url': entry.get('url', ''), 'url': entry.get('url', ''),
'description': entry.get('description', ''), 'description': entry.get('description', ''),
'stars': entry.get('stars', '') 'starred': entry.get('starred', False)
}) })
if len(results) >= limit: if len(results) >= limit:
break break
@ -1008,6 +978,5 @@ def awesomelist_search():
if len(results) >= limit: if len(results) >= limit:
break 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>`;
let html = `<div class="al-list-detail">`; html += `<div class="crt-entry-content">`;
html += `<div class="al-list-detail-header">${esc(listData.title)}</div>`;
if (listData.description) {
html += `<div class="al-list-detail-desc">${md(listData.description)}</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 (listData.subcategories && listData.subcategories.length > 0) {
html += `<div class="al-sub-grid" id="alSubGrid">`;
for (let i = 0; i < listData.subcategories.length; i++) {
const sub = listData.subcategories[i];
if (sub.entries.length === 0) continue;
html += `<div class="al-sub-card" data-sub-idx="${i}">
<div class="al-sub-card-name">${esc(sub.name)}</div>
<div class="al-sub-card-count">${sub.entries.length} items</div>
</div>`;
}
html += `</div>`;
html += `<div id="alEntriesPanel" style="display:none"></div>`;
}
html += `</div>`;
panel.innerHTML = 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) { if (entry.url) {
html += `<div class="al-entry-name"><a href="${esc(entry.url)}" target="_blank" rel="noopener">${esc(entry.name || entry.url)}</a>`; html += `<div class="crt-entry-name"><a href="${esc(entry.url)}" target="_blank" rel="noopener">${esc(entry.name || entry.url)}</a></div>`;
} else { } else if (entry.name) {
html += `<div class="al-entry-name">${md(entry.name || '')}`; html += `<div class="crt-entry-name">${md(entry.name)}</div>`;
} }
if (entry.stars) {
html += `<span class="al-entry-stars">⭐ ${esc(entry.stars)}</span>`;
}
html += `</div>`;
if (entry.description) { if (entry.description) {
html += `<div class="al-entry-desc">${md(entry.description)}</div>`; html += `<div class="crt-entry-desc">${md(entry.description)}</div>`;
}
if (entry.extra_links && entry.extra_links.length > 0) {
html += `<div class="crt-entry-extra">`;
for (const link of entry.extra_links) {
html += `<a href="${esc(link.url)}" target="_blank" rel="noopener">${esc(link.name)}</a>`;
}
html += `</div>`;
} }
html += `</div></div>`; html += `</div></div>`;
} return html;
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>`;
html += `</div>`;
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 {
html += `<div class="al-result-breadcrumb">${esc(r.list_title)} / ${esc(r.subcategory)}</div>`;
html += `<div class="al-result-name">`;
if (r.url) { if (r.url) {
html += `<a href="${esc(r.url)}" target="_blank" rel="noopener">${esc(r.name || r.url)}</a>`; html += `<a href="${esc(r.url)}" target="_blank" rel="noopener">${esc(r.name || r.url)}</a>`;
} else { } else {
html += md(r.name); html += md(r.name);
} }
if (r.stars) html += `<span class="al-result-stars">⭐ ${esc(r.stars)}</span>`;
html += `</div>`; html += `</div>`;
if (r.description) html += `<div class="al-result-desc">${md(r.description)}</div>`; if (r.description) {
html += `<div class="crt-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';
// Populate notes
if (sub.notes && sub.notes.length > 0) {
let nh = '';
for (const note of sub.notes) nh += `${esc(note)}<br>`;
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' });
});
});
// Category search (local filter on subcategory cards)
const catSearch = document.getElementById('crtCatSearch');
if (catSearch) {
catSearch.addEventListener('input', () => {
const q = catSearch.value.trim().toLowerCase();
document.querySelectorAll('.crt-sub-card').forEach(card => {
if (!q) {
card.style.display = '';
return;
}
const text = card.textContent.toLowerCase();
card.style.display = text.includes(q) ? '' : 'none';
});
});
}
}
// ─── Load Functions ──────────────────────────────────
async function loadIndex() {
root.innerHTML = '<div class="crt-loading">ACCESSING RECON DATABASE...</div>';
try { try {
const listData = await api(`/list/${slug}`); const data = await api('');
renderListDetail(listData); renderIndex(data);
history.pushState({ view: 'index' }, '', '/recon/awesomelist');
} catch (e) { } catch (e) {
panel.innerHTML = '<div class="al-empty">FAILED TO LOAD DOSSIER</div>'; root.innerHTML = `<div class="crt-empty">DATABASE ACCESS DENIED // ${esc(e.message)}</div>`;
}
});
});
// Filter
const filterInput = document.getElementById('alSectorFilter');
if (filterInput) {
filterInput.addEventListener('input', () => {
const q = filterInput.value.trim().toLowerCase();
document.querySelectorAll('.al-list-card').forEach(card => {
const title = card.querySelector('.al-list-title').textContent.toLowerCase();
const desc = card.querySelector('.al-list-desc').textContent.toLowerCase();
card.style.display = (!q || title.includes(q) || desc.includes(q)) ? '' : 'none';
});
});
} }
} }
// ─── History Navigation ────────────────────────────── 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) => { window.addEventListener('popstate', (e) => {
if (e.state && e.state.view === 'sector' && state.data) { if (e.state && e.state.view === 'detail' && e.state.slug) {
const sector = state.data.sectors.find(s => s.code === e.state.code); loadSector(e.state.slug);
if (sector) { renderSector(sector); return; } } else {
loadIndex();
} }
if (state.data) renderSectors(state.data);
}); });
// ─── Init ──────────────────────────────────────────── // ─── Init ────────────────────────────────────────────
async function init() { function init() {
root.innerHTML = '<div class="al-loading">ACCESSING PROPAGANDA DATABASE...</div>';
try {
const data = await api('');
state.data = data;
// Check URL params
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const sectorCode = params.get('sector'); const sector = params.get('sector');
if (sectorCode) { if (sector) {
const sector = data.sectors.find(s => s.code.toLowerCase() === sectorCode.toLowerCase()); loadSector(sector);
if (sector) { renderSector(sector); return; } } else {
} loadIndex();
renderSectors(data);
} catch (e) {
root.innerHTML = '<div class="al-empty">FAILED TO ACCESS DATABASE — RETRY LATER</div>';
console.error(e);
} }
} }
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init(); 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>