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.
- package/client/shoppe.js +267 -0
- package/package.json +1 -1
- package/server/server.js +11 -1
- package/client/client.js +0 -207
package/client/shoppe.js
ADDED
|
@@ -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
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
|
-
})();
|