|
| 1 | +/* ── EoS Marketplace – Dynamic App Loader ── */ |
| 2 | +(function () { |
| 3 | + 'use strict'; |
| 4 | + |
| 5 | + const DATA_URL = 'data/apps.json'; |
| 6 | + let allApps = []; |
| 7 | + let categories = []; |
| 8 | + let activeCategory = 'all'; |
| 9 | + let searchQuery = ''; |
| 10 | + |
| 11 | + const CATEGORY_ICONS = { |
| 12 | + extensions: '🧩', |
| 13 | + desktop: '🖥️', |
| 14 | + mobile: '📱', |
| 15 | + service: '☁️', |
| 16 | + native: '⚙️', |
| 17 | + web: '🌐' |
| 18 | + }; |
| 19 | + |
| 20 | + const PLATFORM_LABELS = { |
| 21 | + chrome: 'Chrome', firefox: 'Firefox', safari: 'Safari', |
| 22 | + vscode: 'VS Code', jetbrains: 'JetBrains', obsidian: 'Obsidian', |
| 23 | + slack: 'Slack', raycast: 'Raycast', github: 'GitHub', |
| 24 | + 'google-workspace': 'Google WS', office365: 'Office 365', |
| 25 | + windows: 'Windows', macos: 'macOS', linux: 'Linux', |
| 26 | + android: 'Android', ios: 'iOS', eos: 'EoS', |
| 27 | + web: 'Web', firebase: 'Firebase', cloud: 'Cloud', docker: 'Docker' |
| 28 | + }; |
| 29 | + |
| 30 | + async function init() { |
| 31 | + try { |
| 32 | + const res = await fetch(DATA_URL); |
| 33 | + const data = await res.json(); |
| 34 | + categories = data.categories || []; |
| 35 | + allApps = data.apps || []; |
| 36 | + renderStats(data); |
| 37 | + renderFilters(); |
| 38 | + renderGrid(); |
| 39 | + bindEvents(); |
| 40 | + } catch (err) { |
| 41 | + console.error('Failed to load apps:', err); |
| 42 | + document.getElementById('app-grid').innerHTML = |
| 43 | + '<div class="empty-state"><div class="icon">⚠️</div><p>Failed to load app catalog. Check console for details.</p></div>'; |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + function renderStats(data) { |
| 48 | + const el = document.getElementById('stats'); |
| 49 | + if (!el) return; |
| 50 | + const counts = {}; |
| 51 | + allApps.forEach(a => { counts[a.category] = (counts[a.category] || 0) + 1; }); |
| 52 | + el.innerHTML = ` |
| 53 | + <div class="stat"><div class="stat-number">${allApps.length}</div><div class="stat-label">Total Apps</div></div> |
| 54 | + <div class="stat"><div class="stat-number">${counts.extensions || 0}</div><div class="stat-label">Extensions</div></div> |
| 55 | + <div class="stat"><div class="stat-number">${counts.desktop || 0}</div><div class="stat-label">Desktop</div></div> |
| 56 | + <div class="stat"><div class="stat-number">${counts.mobile || 0}</div><div class="stat-label">Mobile</div></div> |
| 57 | + <div class="stat"><div class="stat-number">${counts.service || 0}</div><div class="stat-label">Services</div></div> |
| 58 | + <div class="stat"><div class="stat-number">${counts.native || 0}</div><div class="stat-label">Native</div></div> |
| 59 | + <div class="stat"><div class="stat-number">${counts.web || 0}</div><div class="stat-label">Web</div></div> |
| 60 | + `; |
| 61 | + } |
| 62 | + |
| 63 | + function renderFilters() { |
| 64 | + const el = document.getElementById('filters'); |
| 65 | + if (!el) return; |
| 66 | + let html = '<button class="pill active" data-cat="all"><span class="pill-icon">📦</span>All</button>'; |
| 67 | + categories.forEach(c => { |
| 68 | + html += `<button class="pill" data-cat="${c.id}"><span class="pill-icon">${c.icon}</span>${c.name}</button>`; |
| 69 | + }); |
| 70 | + el.innerHTML = html; |
| 71 | + } |
| 72 | + |
| 73 | + function getDownloadUrl(app) { |
| 74 | + if (app.downloadUrl) return app.downloadUrl; |
| 75 | + if (app.downloads) { |
| 76 | + return app.downloads.windows || app.downloads.android || app.downloads.macos || app.downloads.linux || app.releaseUrl; |
| 77 | + } |
| 78 | + return app.releaseUrl; |
| 79 | + } |
| 80 | + |
| 81 | + function renderCard(app) { |
| 82 | + const icon = CATEGORY_ICONS[app.category] || '📦'; |
| 83 | + const platforms = (app.platform || []).map(p => |
| 84 | + `<span class="platform-badge">${PLATFORM_LABELS[p] || p}</span>` |
| 85 | + ).join(''); |
| 86 | + const tags = (app.tags || []).slice(0, 4).map(t => |
| 87 | + `<span class="tag">#${t}</span>` |
| 88 | + ).join(''); |
| 89 | + |
| 90 | + const downloadUrl = getDownloadUrl(app); |
| 91 | + let downloadBtn = ''; |
| 92 | + if (app.downloads && Object.keys(app.downloads).length > 1) { |
| 93 | + const entries = Object.entries(app.downloads); |
| 94 | + downloadBtn = entries.map(([plat, url]) => |
| 95 | + `<a href="${url}" class="btn btn-primary" target="_blank">⬇ ${PLATFORM_LABELS[plat] || plat}</a>` |
| 96 | + ).join(''); |
| 97 | + } else if (downloadUrl) { |
| 98 | + downloadBtn = `<a href="${downloadUrl}" class="btn btn-primary" target="_blank">⬇ Download</a>`; |
| 99 | + } |
| 100 | + |
| 101 | + return ` |
| 102 | + <div class="card" data-category="${app.category}"> |
| 103 | + <div class="card-header"> |
| 104 | + <div class="card-icon">${icon}</div> |
| 105 | + <div class="card-title">${app.name}</div> |
| 106 | + <span class="card-version">v${app.version}</span> |
| 107 | + </div> |
| 108 | + <div class="card-desc">${app.description}</div> |
| 109 | + <div class="card-platforms">${platforms}</div> |
| 110 | + <div class="card-tags">${tags}</div> |
| 111 | + <div class="card-actions"> |
| 112 | + ${downloadBtn} |
| 113 | + <a href="${app.repo}" class="btn btn-outline" target="_blank">📂 Source</a> |
| 114 | + </div> |
| 115 | + </div>`; |
| 116 | + } |
| 117 | + |
| 118 | + function renderGrid() { |
| 119 | + const el = document.getElementById('app-grid'); |
| 120 | + if (!el) return; |
| 121 | + const filtered = allApps.filter(app => { |
| 122 | + const catMatch = activeCategory === 'all' || app.category === activeCategory; |
| 123 | + const q = searchQuery.toLowerCase(); |
| 124 | + const searchMatch = !q || |
| 125 | + app.name.toLowerCase().includes(q) || |
| 126 | + app.description.toLowerCase().includes(q) || |
| 127 | + (app.tags || []).some(t => t.toLowerCase().includes(q)); |
| 128 | + return catMatch && searchMatch; |
| 129 | + }); |
| 130 | + |
| 131 | + if (filtered.length === 0) { |
| 132 | + el.innerHTML = '<div class="empty-state"><div class="icon">🔎</div><p>No apps found matching your criteria.</p></div>'; |
| 133 | + return; |
| 134 | + } |
| 135 | + el.innerHTML = filtered.map(renderCard).join(''); |
| 136 | + } |
| 137 | + |
| 138 | + function bindEvents() { |
| 139 | + document.getElementById('filters').addEventListener('click', e => { |
| 140 | + const pill = e.target.closest('.pill'); |
| 141 | + if (!pill) return; |
| 142 | + document.querySelectorAll('.pill').forEach(p => p.classList.remove('active')); |
| 143 | + pill.classList.add('active'); |
| 144 | + activeCategory = pill.dataset.cat; |
| 145 | + renderGrid(); |
| 146 | + }); |
| 147 | + |
| 148 | + const searchInput = document.getElementById('search'); |
| 149 | + if (searchInput) { |
| 150 | + searchInput.addEventListener('input', e => { |
| 151 | + searchQuery = e.target.value; |
| 152 | + renderGrid(); |
| 153 | + }); |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + document.addEventListener('DOMContentLoaded', init); |
| 158 | +})(); |
0 commit comments