Skip to content

Commit 31d2bab

Browse files
committed
fix: force-add JS files that were blocked by *.js in .gitignore
The .gitignore had '*.js' which was intended for node_modules but was blocking js/marketplace.js and shared/js/*.js from being tracked. This caused the marketplace page to show empty (404 on JS).
1 parent 9e5abe8 commit 31d2bab

4 files changed

Lines changed: 324 additions & 0 deletions

File tree

js/marketplace.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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+
})();

shared/js/api.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* EoS Shared — API Client
3+
* Used by: extensions, desktop (Electron), web apps
4+
*/
5+
6+
import auth from './auth.js';
7+
8+
export class EosApiClient {
9+
constructor(config = {}) {
10+
this.baseUrl = config.baseUrl || 'https://api.embeddedos.org';
11+
}
12+
13+
async request(endpoint, options = {}) {
14+
const url = `${this.baseUrl}${endpoint}`;
15+
const headers = {
16+
'Content-Type': 'application/json',
17+
...auth.getAuthHeaders(),
18+
...options.headers,
19+
};
20+
21+
const res = await fetch(url, { ...options, headers });
22+
23+
if (!res.ok) {
24+
const error = new Error(`API ${res.status}: ${res.statusText}`);
25+
error.status = res.status;
26+
try { error.body = await res.json(); } catch {}
27+
throw error;
28+
}
29+
30+
return res.json();
31+
}
32+
33+
get(endpoint) {
34+
return this.request(endpoint, { method: 'GET' });
35+
}
36+
37+
post(endpoint, body) {
38+
return this.request(endpoint, { method: 'POST', body: JSON.stringify(body) });
39+
}
40+
41+
put(endpoint, body) {
42+
return this.request(endpoint, { method: 'PUT', body: JSON.stringify(body) });
43+
}
44+
45+
delete(endpoint) {
46+
return this.request(endpoint, { method: 'DELETE' });
47+
}
48+
}
49+
50+
export default new EosApiClient();

shared/js/auth.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* EoS Shared — Authentication Helpers
3+
* Used by: extensions, desktop (Electron), web apps
4+
*/
5+
6+
export class EosAuth {
7+
constructor(config = {}) {
8+
this.baseUrl = config.baseUrl || 'https://api.embeddedos.org';
9+
this.tokenKey = config.tokenKey || 'eos_auth_token';
10+
}
11+
12+
async login(email, password) {
13+
const res = await fetch(`${this.baseUrl}/auth/login`, {
14+
method: 'POST',
15+
headers: { 'Content-Type': 'application/json' },
16+
body: JSON.stringify({ email, password }),
17+
});
18+
if (!res.ok) throw new Error(`Login failed: ${res.status}`);
19+
const data = await res.json();
20+
this.setToken(data.token);
21+
return data;
22+
}
23+
24+
async logout() {
25+
this.clearToken();
26+
}
27+
28+
getToken() {
29+
if (typeof localStorage !== 'undefined') {
30+
return localStorage.getItem(this.tokenKey);
31+
}
32+
return null;
33+
}
34+
35+
setToken(token) {
36+
if (typeof localStorage !== 'undefined') {
37+
localStorage.setItem(this.tokenKey, token);
38+
}
39+
}
40+
41+
clearToken() {
42+
if (typeof localStorage !== 'undefined') {
43+
localStorage.removeItem(this.tokenKey);
44+
}
45+
}
46+
47+
isAuthenticated() {
48+
return !!this.getToken();
49+
}
50+
51+
getAuthHeaders() {
52+
const token = this.getToken();
53+
return token ? { Authorization: `Bearer ${token}` } : {};
54+
}
55+
}
56+
57+
export default new EosAuth();

shared/js/utils.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* EoS Shared — Common Utilities
3+
* Used by: all JavaScript-based platforms
4+
*/
5+
6+
export function debounce(fn, ms = 300) {
7+
let timer;
8+
return (...args) => {
9+
clearTimeout(timer);
10+
timer = setTimeout(() => fn(...args), ms);
11+
};
12+
}
13+
14+
export function throttle(fn, ms = 300) {
15+
let last = 0;
16+
return (...args) => {
17+
const now = Date.now();
18+
if (now - last >= ms) {
19+
last = now;
20+
fn(...args);
21+
}
22+
};
23+
}
24+
25+
export function formatBytes(bytes, decimals = 2) {
26+
if (bytes === 0) return '0 B';
27+
const k = 1024;
28+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
29+
const i = Math.floor(Math.log(bytes) / Math.log(k));
30+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`;
31+
}
32+
33+
export function formatDate(date, locale = 'en-US') {
34+
return new Date(date).toLocaleDateString(locale, {
35+
year: 'numeric', month: 'short', day: 'numeric',
36+
});
37+
}
38+
39+
export function generateId(prefix = 'eos') {
40+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
41+
}
42+
43+
export function deepClone(obj) {
44+
return structuredClone ? structuredClone(obj) : JSON.parse(JSON.stringify(obj));
45+
}
46+
47+
export function isElectron() {
48+
return typeof window !== 'undefined' &&
49+
typeof window.process === 'object' &&
50+
window.process.type === 'renderer';
51+
}
52+
53+
export function isBrowser() {
54+
return typeof window !== 'undefined' && !isElectron();
55+
}
56+
57+
export function isMobile() {
58+
return typeof navigator !== 'undefined' && /Mobi|Android/i.test(navigator.userAgent);
59+
}

0 commit comments

Comments
 (0)