wiki-plugin-shoppe 0.0.2 → 0.0.4

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.
@@ -0,0 +1,267 @@
1
+ (function() {
2
+
3
+ window.plugins.shoppe = {
4
+ emit: function($item, item) {
5
+ const div = $item[0];
6
+ div.innerHTML = `
7
+ <div class="sw">
8
+ <style>
9
+ .sw { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 680px; margin: 0 auto; color: #1d1d1f; }
10
+ .sw h2 { font-size: 22px; font-weight: 700; margin: 0 0 4px; }
11
+ .sw h3 { font-size: 13px; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin: 0 0 10px; }
12
+ .sw-section { margin-bottom: 24px; }
13
+ .sw-card { background: #f5f5f7; border-radius: 12px; padding: 18px 20px; margin-bottom: 10px; }
14
+ .sw-step { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 12px; }
15
+ .sw-step:last-child { margin-bottom: 0; }
16
+ .sw-step-num { background: #0066cc; color: white; border-radius: 50%; width: 24px; height: 24px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; margin-top: 1px; }
17
+ .sw-step-body { font-size: 14px; line-height: 1.5; color: #333; }
18
+ .sw-step-body strong { color: #1d1d1f; }
19
+ .sw-step-body code { background: #e8e8ed; border-radius: 4px; padding: 1px 5px; font-size: 12px; }
20
+ .sw-tree { font-family: monospace; font-size: 12px; background: #1d1d1f; color: #a8f0a8; border-radius: 8px; padding: 14px 16px; line-height: 1.7; white-space: pre; overflow-x: auto; margin-top: 8px; }
21
+ .sw-shoppe { display: flex; align-items: center; justify-content: space-between; background: white; border: 1px solid #e5e5ea; border-radius: 10px; padding: 12px 16px; margin-bottom: 8px; }
22
+ .sw-shoppe-left { display: flex; flex-direction: column; gap: 2px; }
23
+ .sw-shoppe-name { font-weight: 600; font-size: 15px; }
24
+ .sw-shoppe-code { font-size: 18px; letter-spacing: 4px; }
25
+ .sw-link { font-size: 13px; color: #0066cc; text-decoration: none; white-space: nowrap; }
26
+ .sw-link:hover { text-decoration: underline; }
27
+ .sw-empty { font-size: 13px; color: #999; font-style: italic; }
28
+ .sw-drop { border: 2px dashed #ccc; border-radius: 12px; padding: 28px 20px; text-align: center; background: #fafafa; transition: border-color 0.2s, background 0.2s; cursor: pointer; }
29
+ .sw-drop.dragover { border-color: #0066cc; background: #e8f0fe; }
30
+ .sw-drop p { font-size: 13px; color: #888; margin: 6px 0 14px; }
31
+ .sw-btn { display: inline-block; padding: 9px 20px; border-radius: 18px; font-size: 13px; font-weight: 600; cursor: pointer; border: none; transition: background 0.15s; }
32
+ .sw-btn-blue { background: #0066cc; color: white; }
33
+ .sw-btn-blue:hover { background: #0055aa; }
34
+ .sw-btn-green { background: #10b981; color: white; }
35
+ .sw-btn-green:hover { background: #059669; }
36
+ .sw-register { display: flex; gap: 8px; margin-top: 10px; }
37
+ .sw-register input { flex: 1; padding: 9px 13px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; outline: none; }
38
+ .sw-register input:focus { border-color: #0066cc; }
39
+ .sw-status { margin-top: 12px; border-radius: 10px; padding: 13px 16px; font-size: 14px; line-height: 1.5; display: none; }
40
+ .sw-status.info { background: #e8f0fe; color: #1a56db; display: block; }
41
+ .sw-status.success { background: #d1fae5; color: #065f46; display: block; }
42
+ .sw-status.error { background: #fee2e2; color: #991b1b; display: block; }
43
+ .sw-status code { background: rgba(0,0,0,0.08); border-radius: 4px; padding: 1px 5px; font-size: 12px; }
44
+ </style>
45
+
46
+ <!-- Directory -->
47
+ <div class="sw-section">
48
+ <h2>🛍️ Shoppe</h2>
49
+ <p style="font-size:14px;color:#555;margin:4px 0 16px">A multi-tenant digital goods marketplace. Browse the shoppes below, or open one of your own.</p>
50
+ <h3>Shoppes on this server</h3>
51
+ <div id="sw-directory"><em class="sw-empty">Loading...</em></div>
52
+ </div>
53
+
54
+ <!-- How to join -->
55
+ <div class="sw-section">
56
+ <h3>How to open a shoppe</h3>
57
+ <div class="sw-card">
58
+ <div class="sw-step">
59
+ <div class="sw-step-num">1</div>
60
+ <div class="sw-step-body"><strong>Ask the wiki owner to register you.</strong> They'll use the form at the bottom of this page and give you a <code>uuid</code> and <code>emojicode</code> — your shoppe's identity.</div>
61
+ </div>
62
+ <div class="sw-step">
63
+ <div class="sw-step-num">2</div>
64
+ <div class="sw-step-body"><strong>Build your shoppe folder</strong> with this structure, then zip the whole thing:
65
+ <div class="sw-tree">my-shoppe.zip
66
+ manifest.json ← { "uuid": "…", "emojicode": "…", "name": "My Shoppe" }
67
+ books/ ← .epub .pdf .mobi
68
+ music/
69
+ My Album/ ← subfolder = album (add cover.jpg inside)
70
+ cover.jpg
71
+ 01-track.mp3
72
+ standalone.mp3 ← file directly here = single track
73
+ posts/ ← .md files
74
+ albums/
75
+ Vacation 2025/ ← subfolder of images = photo album
76
+ photo1.jpg
77
+ products/
78
+ T-Shirt/ ← subfolder = physical product
79
+ cover.jpg
80
+ info.json ← { "title": "…", "description": "…", "price": 25, "shipping": 5 }</div>
81
+ </div>
82
+ </div>
83
+ <div class="sw-step">
84
+ <div class="sw-step-num">3</div>
85
+ <div class="sw-step-body"><strong>Drag your .zip onto the upload zone below.</strong> Your goods will be registered and your shoppe will go live immediately.</div>
86
+ </div>
87
+ <div class="sw-step">
88
+ <div class="sw-step-num">4</div>
89
+ <div class="sw-step-body"><strong>To update your shoppe</strong>, just rebuild your folder and upload a new archive — existing items will be overwritten and new ones added.</div>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ <!-- Upload -->
95
+ <div class="sw-section">
96
+ <h3>Upload your archive</h3>
97
+ <div class="sw-drop" id="sw-drop">
98
+ <div style="font-size:40px">📦</div>
99
+ <p>Drag and drop your .zip here, or click to browse.<br>Your <code>manifest.json</code> must contain the <code>uuid</code> and <code>emojicode</code> you were given.</p>
100
+ <button class="sw-btn sw-btn-blue" id="sw-browse-btn">Choose Archive</button>
101
+ <input type="file" id="sw-file-input" accept=".zip" style="display:none">
102
+ </div>
103
+ <div id="sw-upload-status" class="sw-status"></div>
104
+ </div>
105
+
106
+ <!-- Owner: register -->
107
+ <div class="sw-section" id="sw-owner-section" style="display:none">
108
+ <h3>Register a new shoppe (owner only)</h3>
109
+ <div class="sw-register">
110
+ <input type="text" id="sw-name-input" placeholder="Shoppe name (e.g. Zach's Art Store)">
111
+ <button class="sw-btn sw-btn-green" id="sw-register-btn">Register</button>
112
+ </div>
113
+ <div id="sw-register-status" class="sw-status"></div>
114
+ </div>
115
+
116
+ </div>
117
+ `;
118
+
119
+ setupListeners(div);
120
+ loadDirectory(div);
121
+ checkOwner(div);
122
+ },
123
+
124
+ bind: function($item, item) {}
125
+ };
126
+
127
+ // ── Directory (public) ──────────────────────────────────────────────────────
128
+
129
+ async function loadDirectory(container) {
130
+ const el = container.querySelector('#sw-directory');
131
+ try {
132
+ const resp = await fetch('/plugin/shoppe/directory');
133
+ const result = await resp.json();
134
+ if (!result.success || result.shoppes.length === 0) {
135
+ el.innerHTML = '<em class="sw-empty">No shoppes yet — be the first!</em>';
136
+ return;
137
+ }
138
+ el.innerHTML = result.shoppes.map(s => `
139
+ <div class="sw-shoppe">
140
+ <div class="sw-shoppe-left">
141
+ <span class="sw-shoppe-name">${s.name}</span>
142
+ <span class="sw-shoppe-code">${s.emojicode}</span>
143
+ </div>
144
+ <a class="sw-link" href="${s.url}" target="_blank">Visit shoppe →</a>
145
+ </div>
146
+ `).join('');
147
+ } catch (err) {
148
+ el.innerHTML = '<em class="sw-empty">Could not load directory.</em>';
149
+ }
150
+ }
151
+
152
+ // ── Owner check ─────────────────────────────────────────────────────────────
153
+
154
+ async function checkOwner(container) {
155
+ try {
156
+ const resp = await fetch('/plugin/shoppe/tenants');
157
+ if (resp.ok) {
158
+ container.querySelector('#sw-owner-section').style.display = 'block';
159
+ }
160
+ } catch (err) { /* not owner, stay hidden */ }
161
+ }
162
+
163
+ // ── Listeners ───────────────────────────────────────────────────────────────
164
+
165
+ function setupListeners(container) {
166
+ const drop = container.querySelector('#sw-drop');
167
+ const fileInput = container.querySelector('#sw-file-input');
168
+ const browseBtn = container.querySelector('#sw-browse-btn');
169
+ const registerBtn = container.querySelector('#sw-register-btn');
170
+ const nameInput = container.querySelector('#sw-name-input');
171
+
172
+ browseBtn.addEventListener('click', () => fileInput.click());
173
+ fileInput.addEventListener('change', e => {
174
+ if (e.target.files[0]) uploadArchive(e.target.files[0], container);
175
+ });
176
+
177
+ drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('dragover'); });
178
+ drop.addEventListener('dragleave', () => drop.classList.remove('dragover'));
179
+ drop.addEventListener('drop', e => {
180
+ e.preventDefault();
181
+ drop.classList.remove('dragover');
182
+ const file = e.dataTransfer.files[0];
183
+ if (file && file.name.endsWith('.zip')) {
184
+ uploadArchive(file, container);
185
+ } else {
186
+ showStatus(container, '#sw-upload-status', 'Please drop a .zip archive', 'error');
187
+ }
188
+ });
189
+
190
+ if (registerBtn) {
191
+ registerBtn.addEventListener('click', () => registerShoppe(container));
192
+ }
193
+ }
194
+
195
+ // ── Upload ──────────────────────────────────────────────────────────────────
196
+
197
+ async function uploadArchive(file, container) {
198
+ showStatus(container, '#sw-upload-status', `⏳ Uploading <strong>${file.name}</strong>…`, 'info');
199
+ const form = new FormData();
200
+ form.append('archive', file);
201
+ try {
202
+ const resp = await fetch('/plugin/shoppe/upload', { method: 'POST', body: form });
203
+ const result = await resp.json();
204
+ if (!result.success) throw new Error(result.error || 'Upload failed');
205
+
206
+ const r = result.results;
207
+ const counts = [
208
+ r.books.length && `📚 ${r.books.length} book${r.books.length !== 1 ? 's' : ''}`,
209
+ r.music.length && `🎵 ${r.music.length} music item${r.music.length !== 1 ? 's' : ''}`,
210
+ r.posts.length && `📝 ${r.posts.length} post${r.posts.length !== 1 ? 's' : ''}`,
211
+ r.albums.length && `🖼️ ${r.albums.length} album${r.albums.length !== 1 ? 's' : ''}`,
212
+ r.products.length && `📦 ${r.products.length} product${r.products.length !== 1 ? 's' : ''}`
213
+ ].filter(Boolean).join(' · ') || 'no items found';
214
+
215
+ showStatus(container, '#sw-upload-status',
216
+ `✅ <strong>${result.tenant.name}</strong> ${result.tenant.emojicode} updated — ${counts}<br>
217
+ <a href="/plugin/shoppe/${result.tenant.uuid}" target="_blank" class="sw-link" style="display:inline-block;margin-top:8px;">View your shoppe →</a>`,
218
+ 'success');
219
+ loadDirectory(container);
220
+ } catch (err) {
221
+ showStatus(container, '#sw-upload-status', `❌ ${err.message}`, 'error');
222
+ }
223
+ }
224
+
225
+ // ── Register (owner) ────────────────────────────────────────────────────────
226
+
227
+ async function registerShoppe(container) {
228
+ const nameInput = container.querySelector('#sw-name-input');
229
+ const registerBtn = container.querySelector('#sw-register-btn');
230
+ const name = nameInput.value.trim();
231
+ if (!name) { showStatus(container, '#sw-register-status', 'Enter a shoppe name first', 'error'); return; }
232
+
233
+ registerBtn.disabled = true;
234
+ registerBtn.textContent = 'Registering…';
235
+ try {
236
+ const resp = await fetch('/plugin/shoppe/register', {
237
+ method: 'POST',
238
+ headers: { 'Content-Type': 'application/json' },
239
+ body: JSON.stringify({ name })
240
+ });
241
+ const result = await resp.json();
242
+ if (!result.success) throw new Error(result.error || 'Registration failed');
243
+
244
+ nameInput.value = '';
245
+ showStatus(container, '#sw-register-status',
246
+ `✅ Registered! Give these to the shoppe owner:<br>
247
+ UUID: <code>${result.tenant.uuid}</code><br>
248
+ Emojicode: <strong>${result.tenant.emojicode}</strong>`,
249
+ 'success');
250
+ loadDirectory(container);
251
+ } catch (err) {
252
+ showStatus(container, '#sw-register-status', `❌ ${err.message}`, 'error');
253
+ } finally {
254
+ registerBtn.disabled = false;
255
+ registerBtn.textContent = 'Register';
256
+ }
257
+ }
258
+
259
+ // ── Helpers ─────────────────────────────────────────────────────────────────
260
+
261
+ function showStatus(container, selector, html, type) {
262
+ const el = container.querySelector(selector);
263
+ el.className = `sw-status ${type}`;
264
+ el.innerHTML = html;
265
+ }
266
+
267
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-plugin-shoppe",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Multi-tenant digital goods shoppe for federated wiki, powered by Sanora",
5
5
  "keywords": [
6
6
  "wiki",
package/server/server.js CHANGED
@@ -557,7 +557,7 @@ async function startServer(params) {
557
557
  }
558
558
  });
559
559
 
560
- // List all tenants (owner only)
560
+ // List all tenants (owner only — includes uuid for management)
561
561
  app.get('/plugin/shoppe/tenants', owner, (req, res) => {
562
562
  const tenants = loadTenants();
563
563
  const safe = Object.values(tenants).map(({ uuid, emojicode, name, createdAt }) => ({
@@ -567,6 +567,16 @@ async function startServer(params) {
567
567
  res.json({ success: true, tenants: safe });
568
568
  });
569
569
 
570
+ // Public directory — name, emojicode, and shoppe URL only
571
+ app.get('/plugin/shoppe/directory', (req, res) => {
572
+ const tenants = loadTenants();
573
+ const listing = Object.values(tenants).map(({ uuid, emojicode, name }) => ({
574
+ name, emojicode,
575
+ url: `/plugin/shoppe/${uuid}`
576
+ }));
577
+ res.json({ success: true, shoppes: listing });
578
+ });
579
+
570
580
  // Upload goods archive (auth via manifest uuid+emojicode)
571
581
  app.post('/plugin/shoppe/upload', upload.single('archive'), async (req, res) => {
572
582
  try {
package/client/client.js DELETED
@@ -1,207 +0,0 @@
1
- (function() {
2
-
3
- window.plugins.shoppe = {
4
- emit: function($item, item) {
5
- const div = $item[0];
6
- div.innerHTML = `
7
- <div class="shoppe-widget">
8
- <style>
9
- .shoppe-widget { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 700px; margin: 0 auto; }
10
- .shoppe-drop { border: 2px dashed #ccc; border-radius: 14px; padding: 40px 24px; text-align: center; cursor: pointer; transition: border-color 0.2s, background 0.2s; background: #fafafa; }
11
- .shoppe-drop.dragover { border-color: #0066cc; background: #e8f0fe; }
12
- .shoppe-drop-icon { font-size: 48px; margin-bottom: 12px; }
13
- .shoppe-drop h3 { font-size: 18px; font-weight: 600; margin-bottom: 6px; color: #1d1d1f; }
14
- .shoppe-drop p { font-size: 13px; color: #888; margin-bottom: 16px; }
15
- .shoppe-btn { display: inline-block; padding: 10px 22px; border-radius: 20px; font-size: 14px; font-weight: 600; cursor: pointer; border: none; transition: background 0.15s; }
16
- .shoppe-btn-blue { background: #0066cc; color: white; }
17
- .shoppe-btn-blue:hover { background: #0055aa; }
18
- .shoppe-btn-green { background: #10b981; color: white; }
19
- .shoppe-btn-green:hover { background: #059669; }
20
- .shoppe-section { margin-top: 20px; }
21
- .shoppe-section h4 { font-size: 13px; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
22
- .shoppe-tenant { display: flex; align-items: center; justify-content: space-between; background: white; border: 1px solid #eee; border-radius: 10px; padding: 12px 16px; margin-bottom: 8px; }
23
- .shoppe-tenant-info { display: flex; flex-direction: column; gap: 2px; }
24
- .shoppe-tenant-name { font-weight: 600; font-size: 14px; }
25
- .shoppe-tenant-code { font-size: 16px; letter-spacing: 3px; }
26
- .shoppe-tenant-link { font-size: 13px; color: #0066cc; text-decoration: none; }
27
- .shoppe-tenant-link:hover { text-decoration: underline; }
28
- .shoppe-status { margin-top: 14px; border-radius: 10px; padding: 14px 18px; font-size: 14px; }
29
- .shoppe-status.info { background: #e8f0fe; color: #1a56db; }
30
- .shoppe-status.success { background: #d1fae5; color: #065f46; }
31
- .shoppe-status.error { background: #fee2e2; color: #991b1b; }
32
- .shoppe-register { display: flex; gap: 8px; margin-top: 12px; }
33
- .shoppe-register input { flex: 1; padding: 9px 14px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; }
34
- .shoppe-result-counts { margin-top: 8px; font-size: 13px; opacity: 0.8; }
35
- </style>
36
-
37
- <!-- Drop zone -->
38
- <div class="shoppe-drop" id="shoppe-drop">
39
- <div class="shoppe-drop-icon">🛍️</div>
40
- <h3>Drop your shoppe archive here</h3>
41
- <p>Drag and drop a .zip archive, or click to browse.<br>Archive must contain manifest.json with your uuid and emojicode.</p>
42
- <button class="shoppe-btn shoppe-btn-blue" id="shoppe-browse-btn">Choose Archive</button>
43
- <input type="file" id="shoppe-file-input" accept=".zip" style="display:none">
44
- </div>
45
-
46
- <!-- Register section -->
47
- <div class="shoppe-section">
48
- <h4>Register New Shoppe</h4>
49
- <div class="shoppe-register">
50
- <input type="text" id="shoppe-name-input" placeholder="Shoppe name (e.g. Zach's Art Store)">
51
- <button class="shoppe-btn shoppe-btn-green" id="shoppe-register-btn">Register</button>
52
- </div>
53
- </div>
54
-
55
- <!-- Tenant list -->
56
- <div class="shoppe-section" id="shoppe-tenants-section">
57
- <h4>Active Shoppes</h4>
58
- <div id="shoppe-tenants-list"><em style="font-size:13px;color:#999">Loading...</em></div>
59
- </div>
60
-
61
- <!-- Status -->
62
- <div id="shoppe-status" style="display:none"></div>
63
- </div>
64
- `;
65
-
66
- setupShoppeListeners(div);
67
- loadTenants(div);
68
- },
69
-
70
- bind: function($item, item) {}
71
- };
72
-
73
- function setupShoppeListeners(container) {
74
- const drop = container.querySelector('#shoppe-drop');
75
- const fileInput = container.querySelector('#shoppe-file-input');
76
- const browseBtn = container.querySelector('#shoppe-browse-btn');
77
- const registerBtn = container.querySelector('#shoppe-register-btn');
78
- const nameInput = container.querySelector('#shoppe-name-input');
79
-
80
- // Browse button
81
- browseBtn.addEventListener('click', () => fileInput.click());
82
-
83
- // File selected via browser
84
- fileInput.addEventListener('change', e => {
85
- const file = e.target.files[0];
86
- if (file) uploadArchive(file, container);
87
- });
88
-
89
- // Drag and drop
90
- drop.addEventListener('dragover', e => {
91
- e.preventDefault();
92
- drop.classList.add('dragover');
93
- });
94
- drop.addEventListener('dragleave', () => drop.classList.remove('dragover'));
95
- drop.addEventListener('drop', e => {
96
- e.preventDefault();
97
- drop.classList.remove('dragover');
98
- const file = e.dataTransfer.files[0];
99
- if (file && file.name.endsWith('.zip')) {
100
- uploadArchive(file, container);
101
- } else {
102
- showStatus(container, 'Please drop a .zip archive', 'error');
103
- }
104
- });
105
-
106
- // Register
107
- registerBtn.addEventListener('click', async () => {
108
- const name = nameInput.value.trim();
109
- if (!name) { showStatus(container, 'Enter a shoppe name first', 'error'); return; }
110
- registerBtn.disabled = true;
111
- registerBtn.textContent = 'Registering...';
112
- try {
113
- const resp = await fetch('/plugin/shoppe/register', {
114
- method: 'POST',
115
- headers: { 'Content-Type': 'application/json' },
116
- body: JSON.stringify({ name })
117
- });
118
- const result = await resp.json();
119
- if (result.success) {
120
- nameInput.value = '';
121
- showStatus(container,
122
- `✅ Registered! UUID: <code>${result.tenant.uuid}</code><br>Emojicode: <strong>${result.tenant.emojicode}</strong><br>Put these in your manifest.json`,
123
- 'success');
124
- loadTenants(container);
125
- } else {
126
- throw new Error(result.error || 'Registration failed');
127
- }
128
- } catch (err) {
129
- showStatus(container, `❌ ${err.message}`, 'error');
130
- } finally {
131
- registerBtn.disabled = false;
132
- registerBtn.textContent = 'Register';
133
- }
134
- });
135
- }
136
-
137
- async function uploadArchive(file, container) {
138
- showStatus(container, `⏳ Uploading and processing <strong>${file.name}</strong>...`, 'info');
139
-
140
- const form = new FormData();
141
- form.append('archive', file);
142
-
143
- try {
144
- const resp = await fetch('/plugin/shoppe/upload', { method: 'POST', body: form });
145
- const result = await resp.json();
146
-
147
- if (result.success) {
148
- const r = result.results;
149
- const counts = [
150
- r.books.length && `📚 ${r.books.length} book${r.books.length !== 1 ? 's' : ''}`,
151
- r.music.length && `🎵 ${r.music.length} music item${r.music.length !== 1 ? 's' : ''}`,
152
- r.posts.length && `📝 ${r.posts.length} post${r.posts.length !== 1 ? 's' : ''}`,
153
- r.albums.length && `🖼️ ${r.albums.length} album${r.albums.length !== 1 ? 's' : ''}`,
154
- r.products.length && `📦 ${r.products.length} product${r.products.length !== 1 ? 's' : ''}`
155
- ].filter(Boolean).join(', ') || 'nothing uploaded';
156
-
157
- showStatus(container,
158
- `✅ Uploaded for <strong>${result.tenant.name}</strong> ${result.tenant.emojicode}<br>
159
- <div class="shoppe-result-counts">${counts}</div>
160
- <a href="/plugin/shoppe/${result.tenant.uuid}" target="_blank" style="display:inline-block;margin-top:10px;color:#0066cc;">View shoppe →</a>`,
161
- 'success');
162
- loadTenants(container);
163
- } else {
164
- throw new Error(result.error || 'Upload failed');
165
- }
166
- } catch (err) {
167
- showStatus(container, `❌ ${err.message}`, 'error');
168
- }
169
- }
170
-
171
- async function loadTenants(container) {
172
- const list = container.querySelector('#shoppe-tenants-list');
173
- if (!list) return;
174
- try {
175
- const resp = await fetch('/plugin/shoppe/tenants');
176
- if (!resp.ok) {
177
- list.innerHTML = '<em style="font-size:13px;color:#999">Not available (owner only)</em>';
178
- return;
179
- }
180
- const result = await resp.json();
181
- if (!result.success || result.tenants.length === 0) {
182
- list.innerHTML = '<em style="font-size:13px;color:#999">No shoppes registered yet.</em>';
183
- return;
184
- }
185
- list.innerHTML = result.tenants.map(t => `
186
- <div class="shoppe-tenant">
187
- <div class="shoppe-tenant-info">
188
- <span class="shoppe-tenant-name">${t.name}</span>
189
- <span class="shoppe-tenant-code">${t.emojicode}</span>
190
- <code style="font-size:11px;color:#888">${t.uuid}</code>
191
- </div>
192
- <a class="shoppe-tenant-link" href="${t.url}" target="_blank">View shoppe →</a>
193
- </div>
194
- `).join('');
195
- } catch (err) {
196
- list.innerHTML = '<em style="font-size:13px;color:#999">Could not load tenants.</em>';
197
- }
198
- }
199
-
200
- function showStatus(container, html, type) {
201
- const el = container.querySelector('#shoppe-status');
202
- el.className = `shoppe-status ${type}`;
203
- el.innerHTML = html;
204
- el.style.display = 'block';
205
- }
206
-
207
- })();