@bsb/registry 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -0
- package/bsb-plugin.json +47 -0
- package/lib/.bsb/clients/service-bsb-registry.d.ts +1118 -0
- package/lib/.bsb/clients/service-bsb-registry.d.ts.map +1 -0
- package/lib/.bsb/clients/service-bsb-registry.js +393 -0
- package/lib/.bsb/clients/service-bsb-registry.js.map +1 -0
- package/lib/plugins/service-bsb-registry/auth.d.ts +87 -0
- package/lib/plugins/service-bsb-registry/auth.d.ts.map +1 -0
- package/lib/plugins/service-bsb-registry/auth.js +197 -0
- package/lib/plugins/service-bsb-registry/auth.js.map +1 -0
- package/lib/plugins/service-bsb-registry/db/file.d.ts +73 -0
- package/lib/plugins/service-bsb-registry/db/file.d.ts.map +1 -0
- package/lib/plugins/service-bsb-registry/db/file.js +588 -0
- package/lib/plugins/service-bsb-registry/db/file.js.map +1 -0
- package/lib/plugins/service-bsb-registry/db/index.d.ts +75 -0
- package/lib/plugins/service-bsb-registry/db/index.d.ts.map +1 -0
- package/lib/plugins/service-bsb-registry/db/index.js +24 -0
- package/lib/plugins/service-bsb-registry/db/index.js.map +1 -0
- package/lib/plugins/service-bsb-registry/index.d.ts +1228 -0
- package/lib/plugins/service-bsb-registry/index.d.ts.map +1 -0
- package/lib/plugins/service-bsb-registry/index.js +661 -0
- package/lib/plugins/service-bsb-registry/index.js.map +1 -0
- package/lib/plugins/service-bsb-registry/types.d.ts +559 -0
- package/lib/plugins/service-bsb-registry/types.d.ts.map +1 -0
- package/lib/plugins/service-bsb-registry/types.js +235 -0
- package/lib/plugins/service-bsb-registry/types.js.map +1 -0
- package/lib/plugins/service-bsb-registry-ui/http-server.d.ts +138 -0
- package/lib/plugins/service-bsb-registry-ui/http-server.d.ts.map +1 -0
- package/lib/plugins/service-bsb-registry-ui/http-server.js +1660 -0
- package/lib/plugins/service-bsb-registry-ui/http-server.js.map +1 -0
- package/lib/plugins/service-bsb-registry-ui/index.d.ts +62 -0
- package/lib/plugins/service-bsb-registry-ui/index.d.ts.map +1 -0
- package/lib/plugins/service-bsb-registry-ui/index.js +101 -0
- package/lib/plugins/service-bsb-registry-ui/index.js.map +1 -0
- package/lib/plugins/service-bsb-registry-ui/static/assets/images/apple-touch-icon.png +0 -0
- package/lib/plugins/service-bsb-registry-ui/static/assets/images/favicon-16x16.png +0 -0
- package/lib/plugins/service-bsb-registry-ui/static/assets/images/favicon-32x32.png +0 -0
- package/lib/plugins/service-bsb-registry-ui/static/assets/images/favicon.ico +0 -0
- package/lib/plugins/service-bsb-registry-ui/static/css/style.css +1849 -0
- package/lib/plugins/service-bsb-registry-ui/static/js/app.js +336 -0
- package/lib/plugins/service-bsb-registry-ui/templates/layouts/main.hbs +39 -0
- package/lib/plugins/service-bsb-registry-ui/templates/pages/error.hbs +13 -0
- package/lib/plugins/service-bsb-registry-ui/templates/pages/home.hbs +62 -0
- package/lib/plugins/service-bsb-registry-ui/templates/pages/not-found.hbs +13 -0
- package/lib/plugins/service-bsb-registry-ui/templates/pages/plugin-detail.hbs +537 -0
- package/lib/plugins/service-bsb-registry-ui/templates/pages/plugins.hbs +40 -0
- package/lib/plugins/service-bsb-registry-ui/templates/partials/pagination.hbs +41 -0
- package/lib/plugins/service-bsb-registry-ui/templates/partials/plugin-card.hbs +40 -0
- package/lib/plugins/service-bsb-registry-ui/templates/partials/search-form.hbs +31 -0
- package/lib/schemas/service-bsb-registry-ui.json +57 -0
- package/lib/schemas/service-bsb-registry-ui.plugin.json +73 -0
- package/lib/schemas/service-bsb-registry.json +1883 -0
- package/lib/schemas/service-bsb-registry.plugin.json +68 -0
- package/package.json +60 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BSB Registry UI Client-Side Application
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// State
|
|
6
|
+
const state = {
|
|
7
|
+
plugins: [],
|
|
8
|
+
filteredPlugins: [],
|
|
9
|
+
currentPage: 1,
|
|
10
|
+
pageSize: 12,
|
|
11
|
+
searchQuery: '',
|
|
12
|
+
category: null,
|
|
13
|
+
sort: 'recent',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// API Base URL (proxied through UI server to avoid CORS)
|
|
17
|
+
const API_BASE = '/api';
|
|
18
|
+
|
|
19
|
+
// Initialize app
|
|
20
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
21
|
+
initEventListeners();
|
|
22
|
+
loadStats();
|
|
23
|
+
loadPlugins();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Event Listeners
|
|
27
|
+
function initEventListeners() {
|
|
28
|
+
// Search
|
|
29
|
+
document.getElementById('searchBtn').addEventListener('click', handleSearch);
|
|
30
|
+
document.getElementById('searchInput').addEventListener('keypress', (e) => {
|
|
31
|
+
if (e.key === 'Enter') handleSearch();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Category filters
|
|
35
|
+
document.querySelectorAll('.filter-tag').forEach(tag => {
|
|
36
|
+
tag.addEventListener('click', (e) => {
|
|
37
|
+
const category = e.target.dataset.category;
|
|
38
|
+
handleCategoryFilter(category);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Sort
|
|
43
|
+
document.getElementById('sortSelect').addEventListener('change', (e) => {
|
|
44
|
+
state.sort = e.target.value;
|
|
45
|
+
sortPlugins();
|
|
46
|
+
renderPlugins();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Modal close
|
|
50
|
+
document.querySelector('.modal-close').addEventListener('click', closeModal);
|
|
51
|
+
document.getElementById('pluginModal').addEventListener('click', (e) => {
|
|
52
|
+
if (e.target.id === 'pluginModal') closeModal();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Load stats from API
|
|
57
|
+
async function loadStats() {
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(`${API_BASE}/stats`);
|
|
60
|
+
if (!response.ok) throw new Error('Failed to fetch stats');
|
|
61
|
+
|
|
62
|
+
const stats = await response.json();
|
|
63
|
+
|
|
64
|
+
document.getElementById('totalPlugins').textContent = stats.totalPlugins || 0;
|
|
65
|
+
document.getElementById('totalLanguages').textContent = Object.keys(stats.byLanguage || {}).length || 0;
|
|
66
|
+
document.getElementById('totalDownloads').textContent = formatNumber(stats.totalDownloads || 0);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Error loading stats:', error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Load plugins from API
|
|
73
|
+
async function loadPlugins() {
|
|
74
|
+
showLoading();
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(`${API_BASE}/plugins?limit=1000`);
|
|
78
|
+
if (!response.ok) throw new Error('Failed to fetch plugins');
|
|
79
|
+
|
|
80
|
+
const data = await response.json();
|
|
81
|
+
state.plugins = data.results || [];
|
|
82
|
+
state.filteredPlugins = [...state.plugins];
|
|
83
|
+
|
|
84
|
+
sortPlugins();
|
|
85
|
+
renderPlugins();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('Error loading plugins:', error);
|
|
88
|
+
showError('Failed to load plugins. Make sure the registry API is running.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Search plugins
|
|
93
|
+
async function handleSearch() {
|
|
94
|
+
const query = document.getElementById('searchInput').value.trim();
|
|
95
|
+
state.searchQuery = query;
|
|
96
|
+
state.currentPage = 1;
|
|
97
|
+
|
|
98
|
+
if (!query) {
|
|
99
|
+
state.filteredPlugins = [...state.plugins];
|
|
100
|
+
renderPlugins();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
showLoading();
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const response = await fetch(`${API_BASE}/plugins/search?q=${encodeURIComponent(query)}`);
|
|
108
|
+
if (!response.ok) throw new Error('Search failed');
|
|
109
|
+
|
|
110
|
+
const data = await response.json();
|
|
111
|
+
state.filteredPlugins = data.results || [];
|
|
112
|
+
|
|
113
|
+
document.getElementById('sectionTitle').textContent = `Search Results for "${query}"`;
|
|
114
|
+
renderPlugins();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('Error searching:', error);
|
|
117
|
+
showError('Search failed');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Filter by category
|
|
122
|
+
function handleCategoryFilter(category) {
|
|
123
|
+
state.category = category;
|
|
124
|
+
state.currentPage = 1;
|
|
125
|
+
|
|
126
|
+
if (!category) {
|
|
127
|
+
state.filteredPlugins = [...state.plugins];
|
|
128
|
+
} else {
|
|
129
|
+
state.filteredPlugins = state.plugins.filter(p => p.category === category);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
document.getElementById('sectionTitle').textContent = `${capitalize(category)} Plugins`;
|
|
133
|
+
|
|
134
|
+
// Update active state
|
|
135
|
+
document.querySelectorAll('.filter-tag').forEach(tag => {
|
|
136
|
+
tag.classList.toggle('active', tag.dataset.category === category);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
sortPlugins();
|
|
140
|
+
renderPlugins();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Sort plugins
|
|
144
|
+
function sortPlugins() {
|
|
145
|
+
state.filteredPlugins.sort((a, b) => {
|
|
146
|
+
switch (state.sort) {
|
|
147
|
+
case 'name':
|
|
148
|
+
return a.id.localeCompare(b.id);
|
|
149
|
+
case 'popular':
|
|
150
|
+
return (b.downloads || 0) - (a.downloads || 0);
|
|
151
|
+
case 'recent':
|
|
152
|
+
default:
|
|
153
|
+
return new Date(b.updatedAt) - new Date(a.updatedAt);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Render plugins
|
|
159
|
+
function renderPlugins() {
|
|
160
|
+
const container = document.getElementById('pluginList');
|
|
161
|
+
const plugins = getPaginatedPlugins();
|
|
162
|
+
|
|
163
|
+
if (plugins.length === 0) {
|
|
164
|
+
container.innerHTML = '<div class="loading">No plugins found</div>';
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
container.innerHTML = plugins.map(plugin => `
|
|
169
|
+
<div class="plugin-card" onclick="showPluginDetail('${plugin.id}')">
|
|
170
|
+
<div class="plugin-header">
|
|
171
|
+
<div>
|
|
172
|
+
<div class="plugin-name">${escapeHtml(plugin.name)}</div>
|
|
173
|
+
<div class="plugin-id">${escapeHtml(plugin.id)}</div>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="plugin-version">v${escapeHtml(plugin.version)}</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<p class="plugin-description">${escapeHtml(plugin.description || 'No description')}</p>
|
|
179
|
+
|
|
180
|
+
<div class="plugin-meta">
|
|
181
|
+
<span>${plugin.language}</span>
|
|
182
|
+
<span>·</span>
|
|
183
|
+
<span>${formatNumber(plugin.downloads || 0)} downloads</span>
|
|
184
|
+
<span>·</span>
|
|
185
|
+
<span>${plugin.eventCount || 0} events</span>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="plugin-tags">
|
|
189
|
+
<span class="tag category">${escapeHtml(plugin.category)}</span>
|
|
190
|
+
<span class="tag language">${escapeHtml(plugin.language)}</span>
|
|
191
|
+
${(plugin.tags || []).slice(0, 3).map(tag =>
|
|
192
|
+
`<span class="tag">${escapeHtml(tag)}</span>`
|
|
193
|
+
).join('')}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
`).join('');
|
|
197
|
+
|
|
198
|
+
renderPagination();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Get paginated plugins
|
|
202
|
+
function getPaginatedPlugins() {
|
|
203
|
+
const start = (state.currentPage - 1) * state.pageSize;
|
|
204
|
+
const end = start + state.pageSize;
|
|
205
|
+
return state.filteredPlugins.slice(start, end);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Render pagination
|
|
209
|
+
function renderPagination() {
|
|
210
|
+
const container = document.getElementById('pagination');
|
|
211
|
+
const totalPages = Math.ceil(state.filteredPlugins.length / state.pageSize);
|
|
212
|
+
|
|
213
|
+
if (totalPages <= 1) {
|
|
214
|
+
container.innerHTML = '';
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const pages = [];
|
|
219
|
+
for (let i = 1; i <= totalPages; i++) {
|
|
220
|
+
if (i === 1 || i === totalPages || (i >= state.currentPage - 2 && i <= state.currentPage + 2)) {
|
|
221
|
+
pages.push(i);
|
|
222
|
+
} else if (pages[pages.length - 1] !== '...') {
|
|
223
|
+
pages.push('...');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
container.innerHTML = pages.map(page => {
|
|
228
|
+
if (page === '...') {
|
|
229
|
+
return '<span class="page-btn disabled">...</span>';
|
|
230
|
+
}
|
|
231
|
+
return `
|
|
232
|
+
<button
|
|
233
|
+
class="page-btn ${page === state.currentPage ? 'active' : ''}"
|
|
234
|
+
onclick="goToPage(${page})"
|
|
235
|
+
>
|
|
236
|
+
${page}
|
|
237
|
+
</button>
|
|
238
|
+
`;
|
|
239
|
+
}).join('');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Navigate to page
|
|
243
|
+
function goToPage(page) {
|
|
244
|
+
state.currentPage = page;
|
|
245
|
+
renderPlugins();
|
|
246
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Show plugin detail modal
|
|
250
|
+
async function showPluginDetail(pluginId) {
|
|
251
|
+
const modal = document.getElementById('pluginModal');
|
|
252
|
+
const modalBody = document.getElementById('modalBody');
|
|
253
|
+
|
|
254
|
+
modalBody.innerHTML = '<div class="loading">Loading plugin details...</div>';
|
|
255
|
+
modal.classList.add('active');
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const response = await fetch(`${API_BASE}/plugins/${pluginId}`);
|
|
259
|
+
if (!response.ok) throw new Error('Failed to fetch plugin details');
|
|
260
|
+
|
|
261
|
+
const plugin = await response.json();
|
|
262
|
+
|
|
263
|
+
modalBody.innerHTML = `
|
|
264
|
+
<h2>${escapeHtml(plugin.name)}</h2>
|
|
265
|
+
<p class="plugin-id">${escapeHtml(plugin.id)} @ v${escapeHtml(plugin.version)}</p>
|
|
266
|
+
|
|
267
|
+
<p>${escapeHtml(plugin.description)}</p>
|
|
268
|
+
|
|
269
|
+
<div class="plugin-meta" style="margin: 1rem 0;">
|
|
270
|
+
<span><strong>Language:</strong> ${escapeHtml(plugin.language)}</span>
|
|
271
|
+
<span>·</span>
|
|
272
|
+
<span><strong>Category:</strong> ${escapeHtml(plugin.category)}</span>
|
|
273
|
+
<span>·</span>
|
|
274
|
+
<span><strong>Events:</strong> ${plugin.eventCount || 0}</span>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
${plugin.author ? `<p><strong>Author:</strong> ${escapeHtml(plugin.author)}</p>` : ''}
|
|
278
|
+
${plugin.license ? `<p><strong>License:</strong> ${escapeHtml(plugin.license)}</p>` : ''}
|
|
279
|
+
${plugin.homepage ? `<p><strong>Homepage:</strong> <a href="${escapeHtml(plugin.homepage)}" target="_blank">${escapeHtml(plugin.homepage)}</a></p>` : ''}
|
|
280
|
+
${plugin.repository ? `<p><strong>Repository:</strong> <a href="${escapeHtml(plugin.repository)}" target="_blank">${escapeHtml(plugin.repository)}</a></p>` : ''}
|
|
281
|
+
|
|
282
|
+
<h3 style="margin-top: 2rem;">Installation</h3>
|
|
283
|
+
<pre style="background: var(--bg); padding: 1rem; border-radius: 0.5rem; overflow-x: auto;">npx bsb client install ${escapeHtml(plugin.id)}</pre>
|
|
284
|
+
|
|
285
|
+
${plugin.package?.nodejs ? `
|
|
286
|
+
<h3 style="margin-top: 2rem;">NPM Package</h3>
|
|
287
|
+
<pre style="background: var(--bg); padding: 1rem; border-radius: 0.5rem; overflow-x: auto;">npm install ${escapeHtml(plugin.package.nodejs)}</pre>
|
|
288
|
+
` : ''}
|
|
289
|
+
|
|
290
|
+
<div style="margin-top: 2rem;">
|
|
291
|
+
<h3>Tags</h3>
|
|
292
|
+
<div class="plugin-tags">
|
|
293
|
+
${(plugin.tags || []).map(tag =>
|
|
294
|
+
`<span class="tag">${escapeHtml(tag)}</span>`
|
|
295
|
+
).join('')}
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
`;
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error('Error loading plugin details:', error);
|
|
301
|
+
modalBody.innerHTML = '<div class="error">Failed to load plugin details</div>';
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Close modal
|
|
306
|
+
function closeModal() {
|
|
307
|
+
document.getElementById('pluginModal').classList.remove('active');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Show loading state
|
|
311
|
+
function showLoading() {
|
|
312
|
+
document.getElementById('pluginList').innerHTML = '<div class="loading">Loading plugins...</div>';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Show error
|
|
316
|
+
function showError(message) {
|
|
317
|
+
document.getElementById('pluginList').innerHTML =
|
|
318
|
+
`<div class="error">${escapeHtml(message)}</div>`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Utility functions
|
|
322
|
+
function formatNumber(num) {
|
|
323
|
+
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
|
|
324
|
+
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
|
|
325
|
+
return num.toString();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function capitalize(str) {
|
|
329
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function escapeHtml(text) {
|
|
333
|
+
const div = document.createElement('div');
|
|
334
|
+
div.textContent = text;
|
|
335
|
+
return div.innerHTML;
|
|
336
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{{title}}</title>
|
|
7
|
+
<link rel="icon" type="image/x-icon" href="/static/assets/images/favicon.ico">
|
|
8
|
+
<link rel="icon" type="image/png" sizes="16x16" href="/static/assets/images/favicon-16x16.png">
|
|
9
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/static/assets/images/favicon-32x32.png">
|
|
10
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/static/assets/images/apple-touch-icon.png">
|
|
11
|
+
<link rel="stylesheet" href="/static/css/style.css">
|
|
12
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css">
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<header class="header">
|
|
16
|
+
<div class="header-content container">
|
|
17
|
+
<div class="logo">
|
|
18
|
+
<a href="/">BSB Registry</a>
|
|
19
|
+
</div>
|
|
20
|
+
<nav class="nav">
|
|
21
|
+
<a href="/" class="nav-link{{#if (eq activePage 'home')}} active{{/if}}">Home</a>
|
|
22
|
+
<a href="/plugins" class="nav-link{{#if (eq activePage 'browse')}} active{{/if}}">Browse</a>
|
|
23
|
+
</nav>
|
|
24
|
+
</div>
|
|
25
|
+
</header>
|
|
26
|
+
|
|
27
|
+
<main>
|
|
28
|
+
{{{body}}}
|
|
29
|
+
</main>
|
|
30
|
+
|
|
31
|
+
<footer class="footer">
|
|
32
|
+
<div class="container">
|
|
33
|
+
<p>BSB Framework - Multi-Language Plugin Registry</p>
|
|
34
|
+
</div>
|
|
35
|
+
</footer>
|
|
36
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
|
|
37
|
+
<script>hljs.highlightAll();</script>
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<section class="plugins">
|
|
2
|
+
<div class="container">
|
|
3
|
+
<div class="empty-state">
|
|
4
|
+
<h1 class="empty-state-code">{{statusCode}}</h1>
|
|
5
|
+
<h2 class="empty-state-title">{{title}}</h2>
|
|
6
|
+
<p class="empty-state-text">{{message}}</p>
|
|
7
|
+
<div class="empty-state-actions">
|
|
8
|
+
<a href="/" class="btn">Go Home</a>
|
|
9
|
+
<a href="/plugins" class="btn btn-secondary">Browse Plugins</a>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</section>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<section class="hero">
|
|
2
|
+
<div class="container">
|
|
3
|
+
<h1 class="hero-title">BSB Plugin Registry</h1>
|
|
4
|
+
<p class="hero-subtitle">Discover and publish multi-language plugins for the BSB Framework</p>
|
|
5
|
+
|
|
6
|
+
<form method="GET" action="/plugins" class="search-box">
|
|
7
|
+
<input
|
|
8
|
+
type="search"
|
|
9
|
+
name="query"
|
|
10
|
+
class="search-input"
|
|
11
|
+
placeholder="Search plugins by name, description, or tags..."
|
|
12
|
+
>
|
|
13
|
+
<button type="submit" class="search-btn">Search</button>
|
|
14
|
+
</form>
|
|
15
|
+
|
|
16
|
+
<div class="quick-filters">
|
|
17
|
+
<span class="filter-label">Popular:</span>
|
|
18
|
+
<a href="/plugins?language=nodejs" class="filter-tag">Node.js</a>
|
|
19
|
+
<a href="/plugins?language=csharp" class="filter-tag">C#</a>
|
|
20
|
+
<a href="/plugins?language=go" class="filter-tag">Go</a>
|
|
21
|
+
<a href="/plugins?language=java" class="filter-tag">Java</a>
|
|
22
|
+
<a href="/plugins?language=python" class="filter-tag">Python</a>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</section>
|
|
26
|
+
|
|
27
|
+
{{#if stats}}
|
|
28
|
+
<section class="stats">
|
|
29
|
+
<div class="stats-grid container">
|
|
30
|
+
<div class="stat-card">
|
|
31
|
+
<div class="stat-value">{{stats.totalPlugins}}</div>
|
|
32
|
+
<div class="stat-label">Total Plugins</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="stat-card">
|
|
35
|
+
<div class="stat-value">{{stats.totalDownloads}}</div>
|
|
36
|
+
<div class="stat-label">Total Downloads</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
40
|
+
{{/if}}
|
|
41
|
+
|
|
42
|
+
<section class="plugins">
|
|
43
|
+
<div class="container">
|
|
44
|
+
{{#if plugins}}
|
|
45
|
+
<div class="section-header">
|
|
46
|
+
<h2 class="section-title">Recent Plugins</h2>
|
|
47
|
+
<a href="/plugins" class="nav-link">Browse All →</a>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="plugin-grid">
|
|
51
|
+
{{#each plugins}}
|
|
52
|
+
{{> plugin-card}}
|
|
53
|
+
{{/each}}
|
|
54
|
+
</div>
|
|
55
|
+
{{else}}
|
|
56
|
+
<div class="empty-state">
|
|
57
|
+
<h2 class="empty-state-title">No plugins published yet</h2>
|
|
58
|
+
<p class="empty-state-text">Be the first to publish a plugin to the registry.</p>
|
|
59
|
+
</div>
|
|
60
|
+
{{/if}}
|
|
61
|
+
</div>
|
|
62
|
+
</section>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<section class="plugins">
|
|
2
|
+
<div class="container">
|
|
3
|
+
<div class="empty-state">
|
|
4
|
+
<h1 class="empty-state-code">404</h1>
|
|
5
|
+
<h2 class="empty-state-title">{{message}}</h2>
|
|
6
|
+
<p class="empty-state-text">The plugin you're looking for doesn't exist in the registry.</p>
|
|
7
|
+
<div class="empty-state-actions">
|
|
8
|
+
<a href="/" class="btn">Go Home</a>
|
|
9
|
+
<a href="/plugins" class="btn btn-secondary">Browse Plugins</a>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</section>
|