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(db)
@app.route('/api/awesomelist/sector/<code>')
def awesomelist_sector(code):
db = _load_awesomelist_index()
if not db:
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'
@app.route('/api/awesomelist/<code>')
def awesomelist_sector_detail(code):
"""Serve flattened sector data (like contraband categories)"""
p = DATA_DIR / 'awesomelist' / f'sector_{code.upper()}.json'
if not p.exists():
return jsonify({'error': 'List not found'}), 404
return jsonify({'error': 'Sector not found'}), 404
with open(p, 'r') as f:
return json.load(f)
data = json.load(f)
return jsonify(data)
@app.route('/api/awesomelist/search')
def awesomelist_search():
q = request.args.get('q', '').strip().lower()
if len(q) < 2:
return jsonify({'query': q, 'results': [], 'total': 0})
db = _load_awesomelist_index()
if not db:
return jsonify({'error': 'Data not available'}), 503
results = []
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'
if al_dir.exists():
if not al_dir.exists():
return jsonify({'query': q, 'results': [], 'total': 0})
for fp in sorted(al_dir.iterdir()):
if not fp.name.endswith('.json'):
if not fp.name.startswith('sector_'):
continue
try:
with open(fp) as f:
lst = json.load(f)
for sub in lst.get('subcategories', []):
sector = json.load(f)
for sub in sector.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'],
'sector_code': sector['code'],
'sector_name': sector['name'],
'subcategory': sub['name'],
'name': entry['name'],
'name': entry.get('name', ''),
'url': entry.get('url', ''),
'description': entry.get('description', ''),
'stars': entry.get('stars', '')
'starred': entry.get('starred', False)
})
if len(results) >= limit:
break
@ -1008,6 +978,5 @@ def awesomelist_search():
if len(results) >= limit:
break
return jsonify({'query': q, 'results': results, 'total': len(results)})
if __name__ == '__main__':
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 () {
'use strict';
@ -8,7 +8,7 @@
const root = document.getElementById('awesomelistRoot');
if (!root) return;
let state = { view: 'sectors', data: null, searchTimeout: null, activeListSlug: null, activeSubIdx: null };
let state = { view: 'index', sectors: [], searchTimeout: null };
// ─── Utilities ───────────────────────────────────────
function esc(s) {
@ -31,346 +31,322 @@
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
// ─── API Fetch ───────────────────────────────────────
async function api(endpoint) {
const res = await fetch(API + endpoint);
if (!res.ok) throw new Error(`API ${res.status}`);
return res.json();
}
// ─── Render: Sectors (Level 1) ───────────────────────
function renderSectors(data) {
state.view = 'sectors';
state.data = data;
// ─── Render: Index View ──────────────────────────────
function renderIndex(data) {
state.view = 'index';
state.sectors = data.sectors || [];
let html = '';
// Stats bar
html += `<div class="al-stats-bar">
<div class="al-stat"><span class="al-stat-label">DATABASE</span><span class="al-stat-value green"> 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="al-stat"><span class="al-stat-label">TOTAL ENTRIES</span><span class="al-stat-value">${fmt(data.total_entries)}</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="al-stat"><span class="al-stat-label">STATUS</span><span class="al-stat-value green"> DECLASSIFIED</span></div>
html += `<div class="crt-stats-bar">
<div class="crt-stat"><span class="crt-stat-label">DATABASE</span><span class="crt-stat-value"> ONLINE</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="crt-stat"><span class="crt-stat-label">SECTORS</span><span class="crt-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>`;
// Search
html += `<div class="al-search-container">
<span class="al-search-icon"></span>
<input type="text" class="al-search-input" id="alSearch" placeholder="SEARCH ALL LISTS & ENTRIES..." autocomplete="off">
<span class="al-search-count" id="alSearchCount"></span>
html += `<div class="crt-search-container">
<span class="crt-search-icon"></span>
<input type="text" class="crt-search-input" id="crtSearch" placeholder="SEARCH ALL ASSETS..." autocomplete="off">
<span class="crt-search-count" id="crtSearchCount"></span>
</div>`;
html += `<div class="al-search-results" id="alSearchResults"></div>`;
// Search results container
html += `<div class="crt-search-results" id="crtSearchResults"></div>`;
// Sector grid
html += `<div class="al-sector-grid" id="alSectorGrid">`;
for (const sector of data.sectors) {
html += `<div class="al-sector-card" data-code="${esc(sector.code)}">
<div class="al-sector-icon">${sector.icon}</div>
<div class="al-sector-code">${esc(sector.code)}</div>
<div class="al-sector-name">${esc(sector.name)}</div>
<div class="al-sector-meta">
<span class="amber">${sector.list_count} LISTS</span>
<span>${fmt(sector.total_entries)} ENTRIES</span>
html += `<div class="crt-grid" id="crtGrid">`;
for (const sec of state.sectors) {
if (sec.entry_count === 0) continue;
html += `<div class="crt-card" data-slug="${esc(sec.slug)}">
<span class="crt-card-icon">${sec.icon}</span>
<div class="crt-card-code">${esc(sec.code)}</div>
<div class="crt-card-name">${esc(sec.name)}</div>
<div class="crt-card-meta">
<span class="crt-card-count">${fmt(sec.entry_count)} assets</span>
<span class="crt-card-stars">${sec.subcategory_count} sections</span>
</div>
<div class="crt-card-status"> ACCESSIBLE</div>
</div>`;
}
html += `</div>`;
root.innerHTML = html;
bindSectorEvents();
bindIndexEvents();
}
// ─── Render: Lists in Sector (Level 2) ──────────────
function renderSector(sector) {
state.view = 'sector';
state.activeListSlug = null;
// ─── Render: Sector Detail ───────────────────────────
function renderSector(sec) {
state.view = 'detail';
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">
<div class="al-detail-code">${esc(sector.code)}</div>
<div class="al-detail-title">${sector.icon} ${esc(sector.name)}</div>
<div class="al-detail-meta">
<span class="amber">${sector.list_count} LISTS</span>
<span>${fmt(sector.total_entries)} ENTRIES</span>
// Detail header
html += `<div class="crt-detail-header">
<div class="crt-detail-code">${esc(sec.code)}</div>
<div class="crt-detail-title">${esc(sec.icon)} ${esc(sec.name)}</div>
<div class="crt-detail-meta">
<span>${fmt(sec.total_entries)} ASSETS</span>
<span>${sec.subcategory_count} SECTIONS</span>
</div>
</div>`;
// Filter search
html += `<div class="al-search-container">
<span class="al-search-icon"></span>
<input type="text" class="al-search-input" id="alSectorFilter" placeholder="FILTER LISTS IN THIS SECTOR..." autocomplete="off">
// Search within sector
html += `<div class="crt-search-container">
<span class="crt-search-icon"></span>
<input type="text" class="crt-search-input" id="crtCatSearch" placeholder="FILTER THIS SECTOR..." autocomplete="off">
</div>`;
// List cards
html += `<div class="al-list-grid" id="alListGrid">`;
for (const lst of sector.lists) {
html += `<div class="al-list-card" data-slug="${esc(lst.slug)}">
<div class="al-list-title">${esc(lst.title)}</div>
<div class="al-list-desc">${esc(lst.description || '')}</div>
<div class="al-list-stats">
<span class="amber">${fmt(lst.entry_count)} entries</span>
<span>${lst.subcategory_count} sections</span>
${lst.stars ? `<span>⭐ ${esc(lst.stars)}</span>` : ''}
// Subcategory grid (3-col cards)
html += `<div class="crt-sub-grid" id="crtSubGrid">`;
for (let i = 0; i < sec.subcategories.length; i++) {
const sub = sec.subcategories[i];
if (sub.entries.length === 0) continue;
html += `<div class="crt-sub-card" data-sub-idx="${i}">
<div class="crt-sub-card-name">${esc(sub.name)}</div>
<div class="crt-sub-card-stats">
<span>${sub.entries.length} items</span>
</div>
</div>`;
}
html += `</div>`;
// Detail panel
html += `<div id="alListDetail" style="display:none"></div>`;
// Shared detail panel
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;
bindListEvents(sector);
bindDetailEvents(sec);
}
// ─── Render: Single List Detail (Level 3) ───────────
function renderListDetail(listData) {
state.activeSubIdx = null;
const panel = document.getElementById('alListDetail');
if (!panel) return;
let html = `<div class="al-list-detail">`;
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">`;
// ─── Render: Single Entry ────────────────────────────
function renderEntry(entry) {
const starClass = entry.starred ? ' starred' : '';
const starIcon = entry.starred ? '⭐' : '·';
let html = `<div class="crt-entry${starClass}">`;
html += `<span class="crt-entry-star">${starIcon}</span>`;
html += `<div class="crt-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 || '')}`;
html += `<div class="crt-entry-name"><a href="${esc(entry.url)}" target="_blank" rel="noopener">${esc(entry.name || entry.url)}</a></div>`;
} else if (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) {
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>`;
panel.innerHTML = html;
panel.style.display = 'block';
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
return html;
}
// ─── Render: Search Results ──────────────────────────
function renderSearchResults(data) {
const container = document.getElementById('alSearchResults');
const countEl = document.getElementById('alSearchCount');
const grid = document.getElementById('alSectorGrid');
const container = document.getElementById('crtSearchResults');
const countEl = document.getElementById('crtSearchCount');
const grid = document.getElementById('crtGrid');
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' : '';
if (grid) grid.style.display = data.query ? 'none' : '';
return;
}
countEl.textContent = `${data.total} RESULTS`;
countEl.textContent = `${data.results.length} RESULTS`;
if (grid) grid.style.display = 'none';
let html = '';
for (const r of data.results) {
html += `<div class="al-search-result">`;
if (r.type === 'list') {
html += `<div class="al-result-breadcrumb">${esc(r.sector_code)} // ${esc(r.sector_name)}</div>`;
html += `<div class="al-result-name">${esc(r.title)}`;
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">`;
const starIcon = r.starred ? '⭐ ' : '';
html += `<div class="crt-search-result">`;
html += `<div class="crt-result-breadcrumb">${esc(r.sector_code)} // ${esc(r.sector_name)} / ${esc(r.subcategory)}</div>`;
html += `<div class="crt-result-name">${starIcon}`;
if (r.url) {
html += `<a href="${esc(r.url)}" target="_blank" rel="noopener">${esc(r.name || r.url)}</a>`;
} else {
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>`;
if (r.description) {
html += `<div class="crt-result-desc">${md(r.description)}</div>`;
}
html += `</div>`;
}
container.innerHTML = html;
}
// ─── Event Bindings ──────────────────────────────────
function bindSectorEvents() {
document.querySelectorAll('.al-sector-card').forEach(card => {
// ─── Event Bindings: Index ───────────────────────────
function bindIndexEvents() {
// Card clicks
document.querySelectorAll('.crt-card').forEach(card => {
card.addEventListener('click', () => {
const code = card.dataset.code;
const sector = state.data.sectors.find(s => s.code === code);
if (sector) {
history.pushState({ view: 'sector', code }, '', `?sector=${code}`);
renderSector(sector);
}
const slug = card.dataset.slug;
loadSector(slug);
});
});
const searchInput = document.getElementById('alSearch');
// Global search
const searchInput = document.getElementById('crtSearch');
if (searchInput) {
searchInput.addEventListener('input', () => {
clearTimeout(state.searchTimeout);
const q = searchInput.value.trim();
if (q.length < 2) {
document.getElementById('alSearchResults').innerHTML = '';
document.getElementById('alSearchCount').textContent = '';
const grid = document.getElementById('alSectorGrid');
if (grid) grid.style.display = '';
renderSearchResults({ query: '', results: [] });
return;
}
state.searchTimeout = setTimeout(async () => {
try {
const data = await api(`/search?q=${encodeURIComponent(q)}`);
const data = await api(`/search?q=${encodeURIComponent(q)}&limit=100`);
renderSearchResults(data);
} catch (e) {
console.error(e);
console.warn('Search failed:', e);
}
}, 300);
});
}
}
function bindListEvents(sector) {
// ─── Event Bindings: Detail ──────────────────────────
function bindDetailEvents(sec) {
// Back button
document.getElementById('alBackSectors').addEventListener('click', () => {
history.pushState({ view: 'sectors' }, '', window.location.pathname);
renderSectors(state.data);
document.getElementById('crtBack').addEventListener('click', () => {
loadIndex();
});
// List card clicks
document.querySelectorAll('.al-list-card').forEach(card => {
card.addEventListener('click', async () => {
const slug = card.dataset.slug;
if (state.activeListSlug === slug) {
state.activeListSlug = null;
card.classList.remove('active');
document.getElementById('alListDetail').style.display = 'none';
// Subcategory card clicks
document.querySelectorAll('.crt-sub-card').forEach(card => {
card.addEventListener('click', () => {
const idx = parseInt(card.dataset.subIdx);
const sub = sec.subcategories[idx];
const panel = document.getElementById('crtSubDetail');
const headerEl = document.getElementById('crtSubDetailHeader');
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;
}
state.activeListSlug = slug;
document.querySelectorAll('.al-list-card').forEach(c => c.classList.remove('active'));
card.classList.add('active');
const panel = document.getElementById('alListDetail');
panel.innerHTML = '<div class="al-loading">LOADING DOSSIER...</div>';
panel.style.display = 'block';
// Populate header
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>`;
// 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 {
const listData = await api(`/list/${slug}`);
renderListDetail(listData);
const data = await api('');
renderIndex(data);
history.pushState({ view: 'index' }, '', '/recon/awesomelist');
} catch (e) {
panel.innerHTML = '<div class="al-empty">FAILED TO LOAD DOSSIER</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';
});
});
root.innerHTML = `<div class="crt-empty">DATABASE ACCESS DENIED // ${esc(e.message)}</div>`;
}
}
// ─── 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) => {
if (e.state && e.state.view === 'sector' && state.data) {
const sector = state.data.sectors.find(s => s.code === e.state.code);
if (sector) { renderSector(sector); return; }
if (e.state && e.state.view === 'detail' && e.state.slug) {
loadSector(e.state.slug);
} else {
loadIndex();
}
if (state.data) renderSectors(state.data);
});
// ─── Init ────────────────────────────────────────────
async function init() {
root.innerHTML = '<div class="al-loading">ACCESSING PROPAGANDA DATABASE...</div>';
try {
const data = await api('');
state.data = data;
// Check URL params
function init() {
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) {
root.innerHTML = '<div class="al-empty">FAILED TO ACCESS DATABASE — RETRY LATER</div>';
console.error(e);
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>
<meta charset="UTF-8">
<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 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/section.css">
<link rel="stylesheet" href="/css/awesomelist.css">
<link rel="stylesheet" href="/css/contraband.css">
</head>
<body>
<div class="scanline-overlay"></div>
@ -36,16 +36,16 @@
<span class="separator">/</span>
<a href="/depot/recon">RECON</a>
<span class="separator">/</span>
<span class="current">AWESOME LISTS</span>
<span class="current">CURATED LISTS</span>
</div>
<section class="section-header" style="padding-top: calc(var(--nav-height) + 1.5rem);">
<div class="section-header-label">RECON // CURATED INTELLIGENCE</div>
<h1 class="section-header-title">AWESOME LISTS</h1>
<p class="section-header-sub">&gt; 660+ curated dossiers covering 135,000+ resources across 28 sectors. Select a sector to begin.</p>
<h1 class="section-header-title">CURATED LISTS</h1>
<p class="section-header-sub">&gt; Curated dossiers covering resources across 28 sectors. Select a sector to begin.</p>
</section>
<section class="al-container">
<section class="crt-container">
<div id="awesomelistRoot"></div>
</section>