linkitylink 0.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/CLAUDE.md +238 -0
- package/Dockerfile +20 -0
- package/Dockerfile.local +24 -0
- package/LICENSE +674 -0
- package/MANUAL-TESTING.md +399 -0
- package/README.md +119 -0
- package/TEMPLATE-FEDERATION-GO-LIVE.md +269 -0
- package/USER-TESTING-GUIDE.md +420 -0
- package/docker-compose.standalone.yml +14 -0
- package/docker-compose.yml +42 -0
- package/lib/app-handoff.js +315 -0
- package/lib/relevant-bdos-middleware.js +381 -0
- package/package.json +33 -0
- package/public/create.html +1468 -0
- package/public/index.html +117 -0
- package/public/moderate.html +465 -0
- package/public/my-tapestries.html +351 -0
- package/public/relevant-bdos.js +267 -0
- package/public/styles.css +1004 -0
- package/server.js +2914 -0
|
@@ -0,0 +1,351 @@
|
|
|
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>My Tapestries - Glyphenge</title>
|
|
7
|
+
<link rel="stylesheet" href="/styles.css">
|
|
8
|
+
<style>
|
|
9
|
+
.tapestries-container {
|
|
10
|
+
max-width: 1200px;
|
|
11
|
+
width: 100%;
|
|
12
|
+
padding: 40px 20px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.tapestries-header {
|
|
16
|
+
text-align: center;
|
|
17
|
+
margin-bottom: 40px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.tapestries-header h1 {
|
|
21
|
+
font-size: 2.5rem;
|
|
22
|
+
font-weight: 800;
|
|
23
|
+
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
24
|
+
-webkit-background-clip: text;
|
|
25
|
+
-webkit-text-fill-color: transparent;
|
|
26
|
+
margin-bottom: 10px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.tapestries-header p {
|
|
30
|
+
color: #666;
|
|
31
|
+
font-size: 1.1rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.tapestries-grid {
|
|
35
|
+
display: grid;
|
|
36
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
37
|
+
gap: 30px;
|
|
38
|
+
margin-bottom: 40px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.tapestry-card {
|
|
42
|
+
background: white;
|
|
43
|
+
border-radius: 16px;
|
|
44
|
+
padding: 24px;
|
|
45
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
46
|
+
transition: all 0.3s ease;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.tapestry-card:hover {
|
|
51
|
+
transform: translateY(-4px);
|
|
52
|
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.tapestry-card h3 {
|
|
56
|
+
font-size: 1.3rem;
|
|
57
|
+
margin-bottom: 8px;
|
|
58
|
+
color: #2c3e50;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.tapestry-meta {
|
|
62
|
+
display: flex;
|
|
63
|
+
gap: 15px;
|
|
64
|
+
margin-bottom: 12px;
|
|
65
|
+
font-size: 0.9rem;
|
|
66
|
+
color: #666;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.tapestry-meta span {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 5px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.tapestry-emojicode {
|
|
76
|
+
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
77
|
+
color: white;
|
|
78
|
+
padding: 8px 16px;
|
|
79
|
+
border-radius: 8px;
|
|
80
|
+
font-size: 1.2rem;
|
|
81
|
+
margin-bottom: 12px;
|
|
82
|
+
text-align: center;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
transition: all 0.3s ease;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.tapestry-emojicode:hover {
|
|
88
|
+
transform: scale(1.05);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.tapestry-actions {
|
|
92
|
+
display: flex;
|
|
93
|
+
gap: 10px;
|
|
94
|
+
margin-top: 15px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.action-button {
|
|
98
|
+
flex: 1;
|
|
99
|
+
padding: 8px 16px;
|
|
100
|
+
border: none;
|
|
101
|
+
border-radius: 8px;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
font-weight: 600;
|
|
104
|
+
transition: all 0.3s ease;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.action-button.view {
|
|
108
|
+
background: linear-gradient(135deg, #10b981, #059669);
|
|
109
|
+
color: white;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.action-button.copy {
|
|
113
|
+
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
|
114
|
+
color: white;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.action-button:hover {
|
|
118
|
+
transform: translateY(-2px);
|
|
119
|
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.empty-state {
|
|
123
|
+
text-align: center;
|
|
124
|
+
padding: 80px 20px;
|
|
125
|
+
background: rgba(255, 255, 255, 0.95);
|
|
126
|
+
border-radius: 24px;
|
|
127
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.empty-state h2 {
|
|
131
|
+
font-size: 2rem;
|
|
132
|
+
margin-bottom: 20px;
|
|
133
|
+
color: #2c3e50;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.empty-state p {
|
|
137
|
+
font-size: 1.1rem;
|
|
138
|
+
color: #666;
|
|
139
|
+
margin-bottom: 30px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.create-first-button {
|
|
143
|
+
display: inline-block;
|
|
144
|
+
padding: 15px 40px;
|
|
145
|
+
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
146
|
+
color: white;
|
|
147
|
+
border-radius: 30px;
|
|
148
|
+
text-decoration: none;
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
font-size: 1.1rem;
|
|
151
|
+
transition: all 0.3s ease;
|
|
152
|
+
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.create-first-button:hover {
|
|
156
|
+
transform: translateY(-2px);
|
|
157
|
+
box-shadow: 0 8px 30px rgba(102, 126, 234, 0.6);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.back-button {
|
|
161
|
+
display: inline-block;
|
|
162
|
+
padding: 12px 30px;
|
|
163
|
+
background: rgba(255, 255, 255, 0.2);
|
|
164
|
+
color: white;
|
|
165
|
+
border-radius: 8px;
|
|
166
|
+
text-decoration: none;
|
|
167
|
+
font-weight: 600;
|
|
168
|
+
transition: all 0.3s ease;
|
|
169
|
+
margin-bottom: 20px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.back-button:hover {
|
|
173
|
+
background: rgba(255, 255, 255, 0.3);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@media (max-width: 768px) {
|
|
177
|
+
.tapestries-grid {
|
|
178
|
+
grid-template-columns: 1fr;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
</style>
|
|
182
|
+
</head>
|
|
183
|
+
<body style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; display: flex; align-items: center; justify-content: center;">
|
|
184
|
+
<div class="tapestries-container">
|
|
185
|
+
<a href="/" class="back-button">โ Back to Home</a>
|
|
186
|
+
|
|
187
|
+
<div class="tapestries-header">
|
|
188
|
+
<h1>My Tapestries</h1>
|
|
189
|
+
<p id="tapestry-count">Loading your magical creations...</p>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div class="tapestries-grid" id="tapestries-grid"></div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<script>
|
|
196
|
+
async function loadTapestries() {
|
|
197
|
+
try {
|
|
198
|
+
const response = await fetch('/my-tapestries');
|
|
199
|
+
const data = await response.json();
|
|
200
|
+
|
|
201
|
+
if (!data.success) {
|
|
202
|
+
showError('Failed to load tapestries');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const tapestries = data.tapestries || [];
|
|
207
|
+
const grid = document.getElementById('tapestries-grid');
|
|
208
|
+
const countEl = document.getElementById('tapestry-count');
|
|
209
|
+
|
|
210
|
+
if (tapestries.length === 0) {
|
|
211
|
+
countEl.textContent = 'No tapestries yet';
|
|
212
|
+
grid.innerHTML = `
|
|
213
|
+
<div class="empty-state" style="grid-column: 1 / -1;">
|
|
214
|
+
<h2>โจ No Tapestries Yet</h2>
|
|
215
|
+
<p>You haven't created any magical link tapestries yet.<br>Create your first one and start sharing!</p>
|
|
216
|
+
<a href="/" class="create-first-button">Create Your First Tapestry</a>
|
|
217
|
+
</div>
|
|
218
|
+
`;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
countEl.textContent = `${tapestries.length} ${tapestries.length === 1 ? 'tapestry' : 'tapestries'} created`;
|
|
223
|
+
|
|
224
|
+
grid.innerHTML = tapestries.map((tapestry, index) => `
|
|
225
|
+
<div class="tapestry-card">
|
|
226
|
+
<h3>${tapestry.title || 'Untitled Tapestry'}</h3>
|
|
227
|
+
<div class="tapestry-meta">
|
|
228
|
+
<span>๐ ${tapestry.linkCount} ${tapestry.linkCount === 1 ? 'link' : 'links'}</span>
|
|
229
|
+
<span>๐
${formatDate(tapestry.createdAt)}</span>
|
|
230
|
+
</div>
|
|
231
|
+
<div class="tapestry-emojicode" onclick="copyEmojicode('${tapestry.emojicode}')" title="Click to copy">
|
|
232
|
+
${tapestry.emojicode}
|
|
233
|
+
</div>
|
|
234
|
+
<div class="tapestry-actions">
|
|
235
|
+
<button class="action-button view" onclick="viewTapestry('${tapestry.emojicode}')">
|
|
236
|
+
๐๏ธ View
|
|
237
|
+
</button>
|
|
238
|
+
<button class="action-button copy" onclick="copyLink('${tapestry.emojicode}')">
|
|
239
|
+
๐ Copy Link
|
|
240
|
+
</button>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
`).join('');
|
|
244
|
+
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error('Failed to load tapestries:', error);
|
|
247
|
+
showError('Failed to load tapestries');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function formatDate(dateString) {
|
|
252
|
+
const date = new Date(dateString);
|
|
253
|
+
const now = new Date();
|
|
254
|
+
const diffMs = now - date;
|
|
255
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
256
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
257
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
258
|
+
|
|
259
|
+
if (diffMins < 60) {
|
|
260
|
+
return `${diffMins}m ago`;
|
|
261
|
+
} else if (diffHours < 24) {
|
|
262
|
+
return `${diffHours}h ago`;
|
|
263
|
+
} else if (diffDays < 7) {
|
|
264
|
+
return `${diffDays}d ago`;
|
|
265
|
+
} else {
|
|
266
|
+
return date.toLocaleDateString();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function viewTapestry(emojicode) {
|
|
271
|
+
window.location.href = `/?emojicode=${encodeURIComponent(emojicode)}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function copyEmojicode(emojicode) {
|
|
275
|
+
navigator.clipboard.writeText(emojicode).then(() => {
|
|
276
|
+
showToast('Emojicode copied!');
|
|
277
|
+
}).catch(err => {
|
|
278
|
+
console.error('Failed to copy:', err);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function copyLink(emojicode) {
|
|
283
|
+
const link = `${window.location.origin}/?emojicode=${encodeURIComponent(emojicode)}`;
|
|
284
|
+
navigator.clipboard.writeText(link).then(() => {
|
|
285
|
+
showToast('Link copied to clipboard!');
|
|
286
|
+
}).catch(err => {
|
|
287
|
+
console.error('Failed to copy:', err);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function showToast(message) {
|
|
292
|
+
const toast = document.createElement('div');
|
|
293
|
+
toast.textContent = message;
|
|
294
|
+
toast.style.cssText = `
|
|
295
|
+
position: fixed;
|
|
296
|
+
bottom: 20px;
|
|
297
|
+
right: 20px;
|
|
298
|
+
background: linear-gradient(135deg, #10b981, #059669);
|
|
299
|
+
color: white;
|
|
300
|
+
padding: 15px 25px;
|
|
301
|
+
border-radius: 8px;
|
|
302
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
303
|
+
z-index: 1000;
|
|
304
|
+
animation: slideIn 0.3s ease;
|
|
305
|
+
`;
|
|
306
|
+
document.body.appendChild(toast);
|
|
307
|
+
setTimeout(() => {
|
|
308
|
+
toast.style.animation = 'slideOut 0.3s ease';
|
|
309
|
+
setTimeout(() => toast.remove(), 300);
|
|
310
|
+
}, 2000);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function showError(message) {
|
|
314
|
+
document.getElementById('tapestries-grid').innerHTML = `
|
|
315
|
+
<div class="empty-state" style="grid-column: 1 / -1;">
|
|
316
|
+
<h2>โ ๏ธ Error</h2>
|
|
317
|
+
<p>${message}</p>
|
|
318
|
+
<a href="/" class="create-first-button">Back to Home</a>
|
|
319
|
+
</div>
|
|
320
|
+
`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Load tapestries on page load
|
|
324
|
+
loadTapestries();
|
|
325
|
+
</script>
|
|
326
|
+
|
|
327
|
+
<style>
|
|
328
|
+
@keyframes slideIn {
|
|
329
|
+
from {
|
|
330
|
+
transform: translateX(400px);
|
|
331
|
+
opacity: 0;
|
|
332
|
+
}
|
|
333
|
+
to {
|
|
334
|
+
transform: translateX(0);
|
|
335
|
+
opacity: 1;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@keyframes slideOut {
|
|
340
|
+
from {
|
|
341
|
+
transform: translateX(0);
|
|
342
|
+
opacity: 1;
|
|
343
|
+
}
|
|
344
|
+
to {
|
|
345
|
+
transform: translateX(400px);
|
|
346
|
+
opacity: 0;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
</style>
|
|
350
|
+
</body>
|
|
351
|
+
</html>
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* relevantBDOs - Client-side module for Planet Nine Advancement purchases
|
|
3
|
+
*
|
|
4
|
+
* Manages BDO identifiers that are relevant to a purchase transaction.
|
|
5
|
+
* These BDOs represent products, affiliates, or other entities that should
|
|
6
|
+
* be associated with the payment.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* // On page load - reads from URL and stores in localStorage
|
|
10
|
+
* RelevantBDOs.init();
|
|
11
|
+
*
|
|
12
|
+
* // Get current relevantBDOs for API calls
|
|
13
|
+
* const bdos = RelevantBDOs.get();
|
|
14
|
+
* // { emojicodes: ['๐๐๐...'], pubKeys: ['02a1b2...'] }
|
|
15
|
+
*
|
|
16
|
+
* // Add a BDO programmatically
|
|
17
|
+
* RelevantBDOs.addEmojicode('๐๐๐๐จ๐๐๐๐');
|
|
18
|
+
* RelevantBDOs.addPubKey('02a1b2c3d4e5f6...');
|
|
19
|
+
*
|
|
20
|
+
* // Clear after successful purchase
|
|
21
|
+
* RelevantBDOs.clear();
|
|
22
|
+
*
|
|
23
|
+
* URL Parameters:
|
|
24
|
+
* ?relevantBDOs=๐๐๐,๐จ๐๐ (comma-separated emojicodes)
|
|
25
|
+
* ?bdoPubKeys=02a1b2,02c3d4 (comma-separated pubKeys)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const RelevantBDOs = (function() {
|
|
29
|
+
const STORAGE_KEY = 'relevantBDOs';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Initialize - read from URL params and merge with existing localStorage
|
|
33
|
+
* Call this on every page load
|
|
34
|
+
*/
|
|
35
|
+
function init() {
|
|
36
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
37
|
+
const relevantBDOsParam = urlParams.get('relevantBDOs');
|
|
38
|
+
const bdoPubKeysParam = urlParams.get('bdoPubKeys');
|
|
39
|
+
|
|
40
|
+
// Get existing data
|
|
41
|
+
const existing = get();
|
|
42
|
+
|
|
43
|
+
let updated = false;
|
|
44
|
+
|
|
45
|
+
// Add emojicodes from URL
|
|
46
|
+
if (relevantBDOsParam) {
|
|
47
|
+
const newEmojicodes = relevantBDOsParam
|
|
48
|
+
.split(',')
|
|
49
|
+
.map(e => decodeURIComponent(e.trim()))
|
|
50
|
+
.filter(e => e.length > 0);
|
|
51
|
+
|
|
52
|
+
newEmojicodes.forEach(emojicode => {
|
|
53
|
+
if (!existing.emojicodes.includes(emojicode)) {
|
|
54
|
+
existing.emojicodes.push(emojicode);
|
|
55
|
+
updated = true;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Add pubKeys from URL
|
|
61
|
+
if (bdoPubKeysParam) {
|
|
62
|
+
const newPubKeys = bdoPubKeysParam
|
|
63
|
+
.split(',')
|
|
64
|
+
.map(k => k.trim())
|
|
65
|
+
.filter(k => k.length > 0);
|
|
66
|
+
|
|
67
|
+
newPubKeys.forEach(pubKey => {
|
|
68
|
+
if (!existing.pubKeys.includes(pubKey)) {
|
|
69
|
+
existing.pubKeys.push(pubKey);
|
|
70
|
+
updated = true;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Save if we added anything
|
|
76
|
+
if (updated) {
|
|
77
|
+
save(existing);
|
|
78
|
+
console.log('๐ฆ RelevantBDOs initialized:', existing);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return existing;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get current relevantBDOs from localStorage
|
|
86
|
+
* @returns {{ emojicodes: string[], pubKeys: string[] }}
|
|
87
|
+
*/
|
|
88
|
+
function get() {
|
|
89
|
+
try {
|
|
90
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
91
|
+
if (stored) {
|
|
92
|
+
const parsed = JSON.parse(stored);
|
|
93
|
+
return {
|
|
94
|
+
emojicodes: parsed.emojicodes || [],
|
|
95
|
+
pubKeys: parsed.pubKeys || []
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error('Failed to parse relevantBDOs:', e);
|
|
100
|
+
}
|
|
101
|
+
return { emojicodes: [], pubKeys: [] };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Save relevantBDOs to localStorage
|
|
106
|
+
* @param {{ emojicodes: string[], pubKeys: string[] }} data
|
|
107
|
+
*/
|
|
108
|
+
function save(data) {
|
|
109
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Add an emojicode to relevantBDOs
|
|
114
|
+
* @param {string} emojicode
|
|
115
|
+
*/
|
|
116
|
+
function addEmojicode(emojicode) {
|
|
117
|
+
const data = get();
|
|
118
|
+
if (!data.emojicodes.includes(emojicode)) {
|
|
119
|
+
data.emojicodes.push(emojicode);
|
|
120
|
+
save(data);
|
|
121
|
+
console.log('๐ฆ Added emojicode:', emojicode);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Add a pubKey to relevantBDOs
|
|
127
|
+
* @param {string} pubKey
|
|
128
|
+
*/
|
|
129
|
+
function addPubKey(pubKey) {
|
|
130
|
+
const data = get();
|
|
131
|
+
if (!data.pubKeys.includes(pubKey)) {
|
|
132
|
+
data.pubKeys.push(pubKey);
|
|
133
|
+
save(data);
|
|
134
|
+
console.log('๐ฆ Added pubKey:', pubKey);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Remove an emojicode from relevantBDOs
|
|
140
|
+
* @param {string} emojicode
|
|
141
|
+
*/
|
|
142
|
+
function removeEmojicode(emojicode) {
|
|
143
|
+
const data = get();
|
|
144
|
+
const index = data.emojicodes.indexOf(emojicode);
|
|
145
|
+
if (index > -1) {
|
|
146
|
+
data.emojicodes.splice(index, 1);
|
|
147
|
+
save(data);
|
|
148
|
+
console.log('๐ฆ Removed emojicode:', emojicode);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Remove a pubKey from relevantBDOs
|
|
154
|
+
* @param {string} pubKey
|
|
155
|
+
*/
|
|
156
|
+
function removePubKey(pubKey) {
|
|
157
|
+
const data = get();
|
|
158
|
+
const index = data.pubKeys.indexOf(pubKey);
|
|
159
|
+
if (index > -1) {
|
|
160
|
+
data.pubKeys.splice(index, 1);
|
|
161
|
+
save(data);
|
|
162
|
+
console.log('๐ฆ Removed pubKey:', pubKey);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Clear all relevantBDOs (call after successful purchase)
|
|
168
|
+
*/
|
|
169
|
+
function clear() {
|
|
170
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
171
|
+
console.log('๐งน RelevantBDOs cleared');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if there are any relevantBDOs
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
function hasAny() {
|
|
179
|
+
const data = get();
|
|
180
|
+
return data.emojicodes.length > 0 || data.pubKeys.length > 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get count of all relevantBDOs
|
|
185
|
+
* @returns {number}
|
|
186
|
+
*/
|
|
187
|
+
function count() {
|
|
188
|
+
const data = get();
|
|
189
|
+
return data.emojicodes.length + data.pubKeys.length;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Build URL with relevantBDOs as query params
|
|
194
|
+
* Useful for preserving BDOs when navigating to a new page
|
|
195
|
+
* @param {string} baseUrl
|
|
196
|
+
* @returns {string}
|
|
197
|
+
*/
|
|
198
|
+
function buildUrl(baseUrl) {
|
|
199
|
+
const data = get();
|
|
200
|
+
const url = new URL(baseUrl, window.location.origin);
|
|
201
|
+
|
|
202
|
+
if (data.emojicodes.length > 0) {
|
|
203
|
+
url.searchParams.set('relevantBDOs', data.emojicodes.map(e => encodeURIComponent(e)).join(','));
|
|
204
|
+
}
|
|
205
|
+
if (data.pubKeys.length > 0) {
|
|
206
|
+
url.searchParams.set('bdoPubKeys', data.pubKeys.join(','));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return url.toString();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Format for Stripe metadata (flattened key-value pairs)
|
|
214
|
+
* Stripe metadata has limits: 50 keys, 500 char values
|
|
215
|
+
* @returns {Object}
|
|
216
|
+
*/
|
|
217
|
+
function toStripeMetadata() {
|
|
218
|
+
const data = get();
|
|
219
|
+
const metadata = {};
|
|
220
|
+
|
|
221
|
+
// Add emojicodes
|
|
222
|
+
data.emojicodes.forEach((emojicode, i) => {
|
|
223
|
+
metadata[`bdo_emoji_${i}`] = emojicode;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Add pubKeys
|
|
227
|
+
data.pubKeys.forEach((pubKey, i) => {
|
|
228
|
+
metadata[`bdo_pubkey_${i}`] = pubKey;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Add counts for easy parsing
|
|
232
|
+
metadata.bdo_emoji_count = String(data.emojicodes.length);
|
|
233
|
+
metadata.bdo_pubkey_count = String(data.pubKeys.length);
|
|
234
|
+
|
|
235
|
+
return metadata;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Public API
|
|
239
|
+
return {
|
|
240
|
+
init,
|
|
241
|
+
get,
|
|
242
|
+
addEmojicode,
|
|
243
|
+
addPubKey,
|
|
244
|
+
removeEmojicode,
|
|
245
|
+
removePubKey,
|
|
246
|
+
clear,
|
|
247
|
+
hasAny,
|
|
248
|
+
count,
|
|
249
|
+
buildUrl,
|
|
250
|
+
toStripeMetadata
|
|
251
|
+
};
|
|
252
|
+
})();
|
|
253
|
+
|
|
254
|
+
// Auto-initialize if in browser
|
|
255
|
+
if (typeof window !== 'undefined') {
|
|
256
|
+
// Initialize on DOMContentLoaded if not already loaded
|
|
257
|
+
if (document.readyState === 'loading') {
|
|
258
|
+
document.addEventListener('DOMContentLoaded', () => RelevantBDOs.init());
|
|
259
|
+
} else {
|
|
260
|
+
RelevantBDOs.init();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Export for module systems
|
|
265
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
266
|
+
module.exports = RelevantBDOs;
|
|
267
|
+
}
|