feat: flatten awesomelist to 2-level nav like contraband, reuse crt- CSS classes
This commit is contained in:
parent
dd1d5adef5
commit
f00daafaba
32 changed files with 534 additions and 303 deletions
63
api/app.py
63
api/app.py
|
|
@ -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)
|
||||
|
|
|
|||
1
api/data/awesomelist/sector_PRP-001.json
Normal file
1
api/data/awesomelist/sector_PRP-001.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-002.json
Normal file
1
api/data/awesomelist/sector_PRP-002.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-003.json
Normal file
1
api/data/awesomelist/sector_PRP-003.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-004.json
Normal file
1
api/data/awesomelist/sector_PRP-004.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-005.json
Normal file
1
api/data/awesomelist/sector_PRP-005.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-006.json
Normal file
1
api/data/awesomelist/sector_PRP-006.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-007.json
Normal file
1
api/data/awesomelist/sector_PRP-007.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-008.json
Normal file
1
api/data/awesomelist/sector_PRP-008.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-009.json
Normal file
1
api/data/awesomelist/sector_PRP-009.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-010.json
Normal file
1
api/data/awesomelist/sector_PRP-010.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-011.json
Normal file
1
api/data/awesomelist/sector_PRP-011.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-012.json
Normal file
1
api/data/awesomelist/sector_PRP-012.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-013.json
Normal file
1
api/data/awesomelist/sector_PRP-013.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-014.json
Normal file
1
api/data/awesomelist/sector_PRP-014.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-015.json
Normal file
1
api/data/awesomelist/sector_PRP-015.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-016.json
Normal file
1
api/data/awesomelist/sector_PRP-016.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-017.json
Normal file
1
api/data/awesomelist/sector_PRP-017.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-018.json
Normal file
1
api/data/awesomelist/sector_PRP-018.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-019.json
Normal file
1
api/data/awesomelist/sector_PRP-019.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-020.json
Normal file
1
api/data/awesomelist/sector_PRP-020.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-021.json
Normal file
1
api/data/awesomelist/sector_PRP-021.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-022.json
Normal file
1
api/data/awesomelist/sector_PRP-022.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-023.json
Normal file
1
api/data/awesomelist/sector_PRP-023.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-024.json
Normal file
1
api/data/awesomelist/sector_PRP-024.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-025.json
Normal file
1
api/data/awesomelist/sector_PRP-025.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-026.json
Normal file
1
api/data/awesomelist/sector_PRP-026.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-027.json
Normal file
1
api/data/awesomelist/sector_PRP-027.json
Normal file
File diff suppressed because one or more lines are too long
1
api/data/awesomelist/sector_PRP-099.json
Normal file
1
api/data/awesomelist/sector_PRP-099.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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();
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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">> 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">> 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>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue