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.
@@ -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
+ }