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,1468 @@
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>Create Your Glyphenge</title>
7
+ <link rel="stylesheet" href="/styles.css">
8
+ </head>
9
+ <body>
10
+ <div class="create-page">
11
+ <!-- Header -->
12
+ <div class="create-header">
13
+ <h1>Create Your Link Tapestry</h1>
14
+ <span class="style-badge" id="style-badge">Stunning</span>
15
+ </div>
16
+
17
+ <!-- Main Content: Three Columns -->
18
+ <div class="create-content">
19
+ <!-- Left Column: Link Management -->
20
+ <div class="create-column">
21
+ <div class="input-section">
22
+ <h2>Your Links</h2>
23
+
24
+ <div class="input-tabs">
25
+ <button class="tab-button active" onclick="switchTab('import')">Import Links</button>
26
+ <button class="tab-button" onclick="switchTab('manual')">Add Manually</button>
27
+ </div>
28
+
29
+ <!-- Import Tab -->
30
+ <div class="tab-content active" id="import-tab">
31
+ <div class="form-group">
32
+ <label for="import-url">Import from Linktree or similar</label>
33
+ <input type="url" id="import-url" placeholder="https://linktr.ee/username">
34
+ </div>
35
+ <button class="add-link-button" onclick="importLinks(event)">Import Links</button>
36
+ </div>
37
+
38
+ <!-- Manual Tab -->
39
+ <div class="tab-content" id="manual-tab">
40
+ <div class="form-group">
41
+ <label for="link-title">Link Title</label>
42
+ <input type="text" id="link-title" placeholder="GitHub">
43
+ </div>
44
+ <div class="form-group">
45
+ <label for="link-url">Link URL</label>
46
+ <input type="url" id="link-url" placeholder="https://github.com/username">
47
+ </div>
48
+ <button class="add-link-button" onclick="addLink()">Add Link</button>
49
+ </div>
50
+
51
+ <!-- Link List -->
52
+ <div class="link-list" id="link-list"></div>
53
+ </div>
54
+
55
+ <!-- Buy Buttons (Below Left Panel) -->
56
+ <div class="purchase-options">
57
+ <button class="buy-button web-purchase" onclick="handlePurchase()">
58
+ <span class="glow"></span>
59
+ <span class="price">$20</span>
60
+ <span class="action">Buy Now</span>
61
+ </button>
62
+ <button class="buy-button app-purchase" id="app-purchase-button" onclick="handleAppPurchase()">
63
+ <span class="glow"></span>
64
+ <span class="price">$15</span>
65
+ <span class="discount-badge">25% OFF</span>
66
+ <span class="action">Buy in App</span>
67
+ </button>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Middle Column: Live Preview -->
72
+ <div class="create-column">
73
+ <div class="preview-section">
74
+ <h2>Preview</h2>
75
+ <div class="preview-container" id="preview-container">
76
+ <div class="preview-placeholder">
77
+ Add links to see your tapestry preview
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ <!-- Right Column: Template Carousel -->
84
+ <div class="create-column">
85
+ <div class="carousel-section">
86
+ <h2>Choose Template</h2>
87
+ <div class="carousel-container">
88
+ <div class="carousel-track" id="carousel-track"></div>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+
95
+ <script src="https://js.stripe.com/v3/"></script>
96
+ <script src="/relevant-bdos.js"></script>
97
+ <script>
98
+ // Get style from URL parameter
99
+ const urlParams = new URLSearchParams(window.location.search);
100
+ const selectedStyle = urlParams.get('style') || 'stunning';
101
+
102
+ // Update badge
103
+ const badge = document.getElementById('style-badge');
104
+ badge.textContent = selectedStyle.charAt(0).toUpperCase() + selectedStyle.slice(1);
105
+ badge.className = `style-badge ${selectedStyle}`;
106
+
107
+ // RelevantBDOs is auto-initialized by the module
108
+ // Available methods: RelevantBDOs.get(), RelevantBDOs.clear(), etc.
109
+
110
+ // State
111
+ let links = [];
112
+ let selectedTemplate = 0;
113
+ let selectedTemplateEmojicode = null; // Track template emojicode for payment
114
+ let stripe = null;
115
+ let elements = null;
116
+ let userSubmittedTemplates = []; // Will hold templates from server
117
+
118
+ // Tab switching
119
+ function switchTab(tab) {
120
+ const buttons = document.querySelectorAll('.tab-button');
121
+ const contents = document.querySelectorAll('.tab-content');
122
+
123
+ buttons.forEach(btn => btn.classList.remove('active'));
124
+ contents.forEach(content => content.classList.remove('active'));
125
+
126
+ if (tab === 'import') {
127
+ buttons[0].classList.add('active');
128
+ document.getElementById('import-tab').classList.add('active');
129
+ } else {
130
+ buttons[1].classList.add('active');
131
+ document.getElementById('manual-tab').classList.add('active');
132
+ }
133
+ }
134
+
135
+ // Import links
136
+ async function importLinks(event) {
137
+ const url = document.getElementById('import-url').value;
138
+ if (!url) {
139
+ alert('Please enter a URL to import from');
140
+ return;
141
+ }
142
+
143
+ try {
144
+ // Show loading state
145
+ const importButton = event.target;
146
+ const originalText = importButton.textContent;
147
+ importButton.textContent = 'Importing...';
148
+ importButton.disabled = true;
149
+
150
+ // Call the parse endpoint
151
+ const response = await fetch('/parse-linktree', {
152
+ method: 'POST',
153
+ headers: { 'Content-Type': 'application/json' },
154
+ body: JSON.stringify({ url })
155
+ });
156
+
157
+ const data = await response.json();
158
+
159
+ if (data.success) {
160
+ // Add all imported links to the links array
161
+ data.links.forEach(link => {
162
+ links.push(link);
163
+ });
164
+
165
+ // Update UI
166
+ renderLinkList();
167
+ updatePreview();
168
+
169
+ // Clear input and show success
170
+ document.getElementById('import-url').value = '';
171
+ alert(`✅ Successfully imported ${data.links.length} links from @${data.username}'s Linktree!`);
172
+ } else {
173
+ alert(`❌ ${data.error}`);
174
+ }
175
+
176
+ // Restore button
177
+ importButton.textContent = originalText;
178
+ importButton.disabled = false;
179
+
180
+ } catch (error) {
181
+ console.error('Import error:', error);
182
+ alert('Failed to import links. Please try again.');
183
+
184
+ // Restore button on error
185
+ const importButton = event.target;
186
+ importButton.textContent = 'Import Links';
187
+ importButton.disabled = false;
188
+ }
189
+ }
190
+
191
+ // Add link manually
192
+ function addLink() {
193
+ const title = document.getElementById('link-title').value;
194
+ const url = document.getElementById('link-url').value;
195
+
196
+ if (!title || !url) {
197
+ alert('Please enter both title and URL');
198
+ return;
199
+ }
200
+
201
+ links.push({ title, url });
202
+ renderLinkList();
203
+ updatePreview();
204
+
205
+ // Clear inputs
206
+ document.getElementById('link-title').value = '';
207
+ document.getElementById('link-url').value = '';
208
+ }
209
+
210
+ // Remove link
211
+ function removeLink(index) {
212
+ links.splice(index, 1);
213
+ renderLinkList();
214
+ updatePreview();
215
+ }
216
+
217
+ // Render link list
218
+ function renderLinkList() {
219
+ const listContainer = document.getElementById('link-list');
220
+ if (links.length === 0) {
221
+ listContainer.innerHTML = '<p style="color: #999; text-align: center; padding: 20px;">No links added yet</p>';
222
+ return;
223
+ }
224
+
225
+ listContainer.innerHTML = links.map((link, index) => `
226
+ <div class="link-item">
227
+ <div>
228
+ <strong>${link.title}</strong>
229
+ <br>
230
+ <small>${link.url}</small>
231
+ </div>
232
+ <button class="remove-link" onclick="removeLink(${index})">Remove</button>
233
+ </div>
234
+ `).join('');
235
+ }
236
+
237
+ // Template definitions for each style
238
+ const templates = {
239
+ stunning: [
240
+ { name: 'Sunset', colors: ['#ff6b6b', '#ee5a6f', '#feca57'], linkColors: ['#10b981', '#3b82f6', '#8b5cf6', '#ec4899'] }
241
+ ],
242
+ dazzling: [
243
+ { name: 'Rainbow', colors: ['#ff6b6b', '#feca57', '#48dbfb'], linkColors: ['#ff6b6b', '#feca57', '#48dbfb', '#ff9ff3'] }
244
+ ],
245
+ electric: [
246
+ { name: 'Lightning', colors: ['#00d2ff', '#3a47d5', '#4facfe'], linkColors: ['#00d2ff', '#3a47d5', '#4facfe', '#00f2fe'] }
247
+ ],
248
+ polished: [
249
+ { name: 'Steel', colors: ['#2c3e50', '#4ca1af', '#536976'], linkColors: ['#4ca1af', '#536976', '#bdc3c7', '#95a5a6'] }
250
+ ],
251
+ professional: [
252
+ { name: 'Corporate', colors: ['#141e30', '#243b55', '#2c3e50'], linkColors: ['#243b55', '#2c3e50', '#34495e', '#7f8c8d'] }
253
+ ],
254
+ captivating: [
255
+ { name: 'Passion', colors: ['#fc466b', '#3f5efb', '#f093fb'], linkColors: ['#fc466b', '#3f5efb', '#f093fb', '#4facfe'] }
256
+ ],
257
+ delightful: [
258
+ { name: 'Sunshine', colors: ['#fdbb2d', '#22c1c3', '#feca57'], linkColors: ['#fdbb2d', '#22c1c3', '#feca57', '#ee5a6f'] }
259
+ ],
260
+ magical: [
261
+ { name: 'Unicorn', colors: ['#a8edea', '#fed6e3', '#f093fb'], linkColors: ['#a8edea', '#fed6e3', '#f093fb', '#ffecd2'] }
262
+ ],
263
+ basic: [
264
+ { name: 'Clean', colors: ['#6a85b6', '#bac8e0', '#95a5a6'], linkColors: ['#6a85b6', '#bac8e0', '#95a5a6', '#7f8c8d'] }
265
+ ]
266
+ };
267
+
268
+ // Generate live preview SVG
269
+ function generatePreviewSVG(template, userLinks) {
270
+ const linkCount = userLinks.length || 1;
271
+ const height = Math.max(400, linkCount * 110 + 180);
272
+
273
+ const gradientStops = template.colors.map((color, index) => {
274
+ const offset = (index / (template.colors.length - 1)) * 100;
275
+ return `<stop offset="${offset}%" style="stop-color:${color};stop-opacity:1" />`;
276
+ }).join('');
277
+
278
+ const linkCards = userLinks.map((link, i) => {
279
+ const color = template.linkColors[i % template.linkColors.length];
280
+ const colorDark = shadeColor(color, -20);
281
+
282
+ return `
283
+ <defs>
284
+ <linearGradient id="linkGrad${i}" x1="0%" y1="0%" x2="100%" y2="0%">
285
+ <stop offset="0%" style="stop-color:${color};stop-opacity:1" />
286
+ <stop offset="100%" style="stop-color:${colorDark};stop-opacity:1" />
287
+ </linearGradient>
288
+ </defs>
289
+ <g transform="translate(50, ${140 + i * 100})">
290
+ <rect width="500" height="80" fill="url(#linkGrad${i})" rx="12" opacity="0.95"/>
291
+ <circle cx="40" cy="40" r="25" fill="white" opacity="0.3"/>
292
+ <text x="80" y="35" font-family="system-ui" font-size="16" font-weight="600" fill="white">${escapeXML(link.title)}</text>
293
+ <text x="80" y="55" font-family="system-ui" font-size="14" fill="white" opacity="0.9">${escapeXML(getDomain(link.url))}</text>
294
+ <text x="460" y="45" font-family="system-ui" font-size="24" fill="white" opacity="0.6">→</text>
295
+ </g>
296
+ `;
297
+ }).join('');
298
+
299
+ return `
300
+ <svg viewBox="0 0 600 ${height}" xmlns="http://www.w3.org/2000/svg">
301
+ <defs>
302
+ <linearGradient id="bg-${template.name}" x1="0%" y1="0%" x2="0%" y2="100%">
303
+ ${gradientStops}
304
+ </linearGradient>
305
+ </defs>
306
+ <rect width="600" height="${height}" fill="url(#bg-${template.name})" rx="20"/>
307
+ <text x="300" y="80" font-family="system-ui" font-size="32" font-weight="bold" fill="white" text-anchor="middle">
308
+ Your Awesome Links
309
+ </text>
310
+ ${linkCards}
311
+ <text x="300" y="${height - 50}" font-size="48" text-anchor="middle" opacity="0.7">✨</text>
312
+ </svg>
313
+ `;
314
+ }
315
+
316
+ // Generate template card SVG (smaller, for carousel)
317
+ function generateTemplateSVG(template) {
318
+ const gradientStops = template.colors.map((color, index) => {
319
+ const offset = (index / (template.colors.length - 1)) * 100;
320
+ return `<stop offset="${offset}%" style="stop-color:${color};stop-opacity:1" />`;
321
+ }).join('');
322
+
323
+ return `
324
+ <svg viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
325
+ <defs>
326
+ <linearGradient id="tmpl-${template.name}" x1="0%" y1="0%" x2="0%" y2="100%">
327
+ ${gradientStops}
328
+ </linearGradient>
329
+ </defs>
330
+ <rect width="600" height="400" fill="url(#tmpl-${template.name})" rx="20"/>
331
+ <text x="300" y="200" font-family="system-ui" font-size="28" font-weight="bold" fill="white" text-anchor="middle" opacity="0.9">
332
+ ${template.name}
333
+ </text>
334
+ </svg>
335
+ `;
336
+ }
337
+
338
+ // Update live preview
339
+ function updatePreview() {
340
+ const container = document.getElementById('preview-container');
341
+
342
+ // Get template from either built-in or user-submitted
343
+ let template;
344
+ if (selectedTemplateEmojicode) {
345
+ // User template selected
346
+ const userTemplateIndex = userSubmittedTemplates.findIndex(t => t.emojicode === selectedTemplateEmojicode);
347
+ template = userSubmittedTemplates[userTemplateIndex];
348
+ } else {
349
+ // Built-in template selected
350
+ const styleTemplates = templates[selectedStyle] || templates.stunning;
351
+ template = styleTemplates[selectedTemplate];
352
+ }
353
+
354
+ if (links.length === 0) {
355
+ container.innerHTML = '<div class="preview-placeholder">Add links to see your tapestry preview</div>';
356
+ } else {
357
+ container.innerHTML = generatePreviewSVG(template, links);
358
+ }
359
+ }
360
+
361
+ // Render carousel
362
+ function renderCarousel() {
363
+ const track = document.getElementById('carousel-track');
364
+ const styleTemplates = templates[selectedStyle] || templates.stunning;
365
+
366
+ // Render built-in templates
367
+ const builtInTemplateCards = styleTemplates.map((template, index) => `
368
+ <div class="template-card ${index === selectedTemplate && !selectedTemplateEmojicode ? 'selected' : ''}" onclick="selectBuiltInTemplate(${index})">
369
+ ${generateTemplateSVG(template)}
370
+ </div>
371
+ `).join('');
372
+
373
+ // Render user-submitted templates
374
+ const userTemplateCards = userSubmittedTemplates.map((template, index) => {
375
+ const isSelected = selectedTemplateEmojicode === template.emojicode;
376
+ return `
377
+ <div class="template-card ${isSelected ? 'selected' : ''}" onclick='selectUserTemplate(${index}, "${template.emojicode}")'>
378
+ ${generateTemplateSVG(template)}
379
+ <div style="position: absolute; top: 10px; right: 10px; background: rgba(16, 185, 129, 0.9); color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 600;">
380
+ Community
381
+ </div>
382
+ </div>
383
+ `;
384
+ }).join('');
385
+
386
+ const customTemplateCard = `
387
+ <div class="template-card" onclick="showTemplateOptions()" style="cursor: pointer;">
388
+ <svg viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
389
+ <defs>
390
+ <linearGradient id="custom-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
391
+ <stop offset="0%" style="stop-color:#8b5cf6;stop-opacity:1" />
392
+ <stop offset="100%" style="stop-color:#ec4899;stop-opacity:1" />
393
+ </linearGradient>
394
+ </defs>
395
+ <rect width="600" height="400" fill="url(#custom-gradient)" rx="20"/>
396
+ <text x="300" y="180" font-family="system-ui" font-size="24" font-weight="bold" fill="white" text-anchor="middle" opacity="0.9">
397
+ Custom Templates
398
+ </text>
399
+ <text x="300" y="220" font-family="system-ui" font-size="16" fill="white" text-anchor="middle" opacity="0.8">
400
+ Download spec or upload your own
401
+ </text>
402
+ <text x="300" y="250" font-family="system-ui" font-size="32" fill="white" text-anchor="middle" opacity="0.9">
403
+ ⚙️
404
+ </text>
405
+ </svg>
406
+ </div>
407
+ `;
408
+
409
+ track.innerHTML = builtInTemplateCards + userTemplateCards + customTemplateCard;
410
+ }
411
+
412
+ // Select built-in template
413
+ function selectBuiltInTemplate(index) {
414
+ selectedTemplate = index;
415
+ selectedTemplateEmojicode = null; // Clear user template selection
416
+ renderCarousel();
417
+ updatePreview();
418
+
419
+ // Clear template from relevantBDOs
420
+ RelevantBDOs.removeByEmojicode(selectedTemplateEmojicode);
421
+ }
422
+
423
+ // Select user-submitted template
424
+ function selectUserTemplate(index, emojicode) {
425
+ selectedTemplate = index;
426
+ selectedTemplateEmojicode = emojicode;
427
+ renderCarousel();
428
+
429
+ // Get template from userSubmittedTemplates
430
+ const template = userSubmittedTemplates[index];
431
+ updatePreview();
432
+
433
+ // Add template emojicode to relevantBDOs for payment sharing
434
+ console.log(`🎨 Selected user template: ${template.name} (${emojicode})`);
435
+ RelevantBDOs.addEmojicode(emojicode);
436
+ }
437
+
438
+ // Scroll carousel
439
+ function scrollCarousel(direction) {
440
+ const track = document.getElementById('carousel-track');
441
+ const scrollAmount = 320;
442
+ track.scrollBy({
443
+ left: direction * scrollAmount,
444
+ behavior: 'smooth'
445
+ });
446
+ }
447
+
448
+ // Helper: Shade color
449
+ function shadeColor(color, percent) {
450
+ const num = parseInt(color.replace("#",""), 16);
451
+ const amt = Math.round(2.55 * percent);
452
+ const R = (num >> 16) + amt;
453
+ const G = (num >> 8 & 0x00FF) + amt;
454
+ const B = (num & 0x0000FF) + amt;
455
+ return "#" + (0x1000000 + (R<255?R<1?0:R:255)*0x10000 +
456
+ (G<255?G<1?0:G:255)*0x100 + (B<255?B<1?0:B:255))
457
+ .toString(16).slice(1);
458
+ }
459
+
460
+ // Helper: Get domain from URL
461
+ function getDomain(url) {
462
+ try {
463
+ const domain = new URL(url).hostname.replace('www.', '');
464
+ return domain;
465
+ } catch {
466
+ return url;
467
+ }
468
+ }
469
+
470
+ // Helper: Escape XML
471
+ function escapeXML(str) {
472
+ return String(str)
473
+ .replace(/&/g, '&amp;')
474
+ .replace(/</g, '&lt;')
475
+ .replace(/>/g, '&gt;')
476
+ .replace(/"/g, '&quot;')
477
+ .replace(/'/g, '&apos;');
478
+ }
479
+
480
+ // Fetch user-submitted templates from server
481
+ async function fetchUserSubmittedTemplates() {
482
+ try {
483
+ console.log('🎨 Fetching user-submitted templates...');
484
+ const response = await fetch('/templates');
485
+ const data = await response.json();
486
+
487
+ if (data.success && data.templates) {
488
+ userSubmittedTemplates = data.templates;
489
+ console.log(`✅ Loaded ${userSubmittedTemplates.length} user-submitted templates`);
490
+ } else {
491
+ console.log('ℹ️ No user-submitted templates available yet');
492
+ }
493
+ } catch (error) {
494
+ console.error('❌ Error fetching user templates:', error);
495
+ }
496
+ }
497
+
498
+ // Handle purchase
499
+ async function handlePurchase() {
500
+ if (links.length === 0) {
501
+ alert('Please add at least one link before purchasing');
502
+ return;
503
+ }
504
+
505
+ try {
506
+ console.log('💳 Creating payment intent...');
507
+
508
+ // Get relevantBDOs from module to include in payment
509
+ const relevantBDOs = RelevantBDOs.get();
510
+ console.log('📦 Including relevantBDOs in payment:', relevantBDOs);
511
+
512
+ const response = await fetch('/create-payment-intent', {
513
+ method: 'POST',
514
+ headers: { 'Content-Type': 'application/json' },
515
+ body: JSON.stringify({ relevantBDOs })
516
+ });
517
+
518
+ if (!response.ok) {
519
+ throw new Error('Failed to create payment intent');
520
+ }
521
+
522
+ const { clientSecret, publishableKey } = await response.json();
523
+
524
+ console.log('✅ Payment intent created');
525
+
526
+ stripe = Stripe(publishableKey);
527
+ elements = stripe.elements({ clientSecret: clientSecret });
528
+
529
+ const paymentElement = elements.create('payment');
530
+ showPaymentModal(paymentElement);
531
+
532
+ } catch (error) {
533
+ console.error('❌ Payment error:', error);
534
+ alert('Payment failed. Please try again.');
535
+ }
536
+ }
537
+
538
+ function showPaymentModal(paymentElement) {
539
+ const modal = document.createElement('div');
540
+ modal.style.cssText = `
541
+ position: fixed;
542
+ top: 0;
543
+ left: 0;
544
+ width: 100%;
545
+ height: 100%;
546
+ background: rgba(0,0,0,0.8);
547
+ display: flex;
548
+ align-items: flex-start;
549
+ justify-content: center;
550
+ z-index: 10000;
551
+ overflow-y: auto;
552
+ padding: 40px 20px;
553
+ `;
554
+
555
+ const formContainer = document.createElement('div');
556
+ formContainer.style.cssText = `
557
+ background: white;
558
+ padding: 40px;
559
+ border-radius: 20px;
560
+ max-width: 500px;
561
+ width: 90%;
562
+ max-height: calc(100vh - 80px);
563
+ overflow-y: auto;
564
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
565
+ margin: auto;
566
+ `;
567
+
568
+ formContainer.innerHTML = `
569
+ <h2 style="margin: 0 0 10px 0; color: #333;">Complete Purchase</h2>
570
+ <p style="margin: 0 0 30px 0; color: #666;">$20.00 for your Glyphenge Tapestry</p>
571
+ <div id="payment-element" style="margin-bottom: 20px;"></div>
572
+ <button id="submit-payment" style="
573
+ width: 100%;
574
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
575
+ color: white;
576
+ border: none;
577
+ padding: 15px;
578
+ border-radius: 10px;
579
+ font-size: 16px;
580
+ font-weight: 600;
581
+ cursor: pointer;
582
+ margin-bottom: 10px;
583
+ ">Pay $20</button>
584
+ <button id="cancel-payment" style="
585
+ width: 100%;
586
+ background: #e5e7eb;
587
+ color: #666;
588
+ border: none;
589
+ padding: 15px;
590
+ border-radius: 10px;
591
+ font-size: 16px;
592
+ cursor: pointer;
593
+ ">Cancel</button>
594
+ <div id="payment-message" style="
595
+ margin-top: 20px;
596
+ padding: 10px;
597
+ border-radius: 5px;
598
+ display: none;
599
+ "></div>
600
+ `;
601
+
602
+ modal.appendChild(formContainer);
603
+ document.body.appendChild(modal);
604
+
605
+ paymentElement.mount('#payment-element');
606
+
607
+ document.getElementById('submit-payment').addEventListener('click', async () => {
608
+ const submitButton = document.getElementById('submit-payment');
609
+ submitButton.disabled = true;
610
+ submitButton.textContent = 'Processing...';
611
+
612
+ try {
613
+ // Confirm payment (handle success inline, no redirect)
614
+ const { error } = await stripe.confirmPayment({
615
+ elements,
616
+ redirect: 'if_required'
617
+ });
618
+
619
+ if (error) {
620
+ showMessage(error.message, 'error');
621
+ submitButton.disabled = false;
622
+ submitButton.textContent = 'Pay $20';
623
+ } else {
624
+ // Payment succeeded! Now create the tapestry
625
+ submitButton.textContent = 'Creating Tapestry...';
626
+
627
+ try {
628
+ const createResponse = await fetch('/create', {
629
+ method: 'POST',
630
+ headers: { 'Content-Type': 'application/json' },
631
+ body: JSON.stringify({
632
+ title: 'My Glyphenge',
633
+ links: links,
634
+ source: 'create-page',
635
+ style: selectedStyle
636
+ })
637
+ });
638
+
639
+ if (!createResponse.ok) {
640
+ throw new Error('Failed to create tapestry');
641
+ }
642
+
643
+ const tapestryData = await createResponse.json();
644
+
645
+ // Close payment modal
646
+ document.body.removeChild(modal);
647
+
648
+ // Clear relevantBDOs after successful purchase
649
+ RelevantBDOs.clear();
650
+
651
+ // Show success with emojicode
652
+ showSuccessPage(tapestryData);
653
+
654
+ } catch (createError) {
655
+ showMessage('Payment successful but tapestry creation failed. Please contact support.', 'error');
656
+ submitButton.disabled = false;
657
+ submitButton.textContent = 'Pay $20';
658
+ }
659
+ }
660
+ } catch (err) {
661
+ showMessage('Payment failed. Please try again.', 'error');
662
+ submitButton.disabled = false;
663
+ submitButton.textContent = 'Pay $20';
664
+ }
665
+ });
666
+
667
+ document.getElementById('cancel-payment').addEventListener('click', () => {
668
+ document.body.removeChild(modal);
669
+ });
670
+ }
671
+
672
+ // Handle app purchase (triggers authteam flow)
673
+ async function handleAppPurchase() {
674
+ if (links.length === 0) {
675
+ alert('Please add at least one link before purchasing');
676
+ return;
677
+ }
678
+
679
+ try {
680
+ console.log('📱 Creating handoff for app purchase...');
681
+
682
+ // Get relevantBDOs to pass to the handoff
683
+ const relevantBDOs = RelevantBDOs.get();
684
+ const styleTemplates = templates[selectedStyle] || templates.stunning;
685
+ const template = styleTemplates[selectedTemplate];
686
+
687
+ // Create the BDO data that will be passed to the app
688
+ const bdoData = {
689
+ title: 'My Glyphenge',
690
+ links: links,
691
+ source: 'create-page',
692
+ style: selectedStyle,
693
+ template: template.name
694
+ };
695
+
696
+ const response = await fetch('/handoff/create', {
697
+ method: 'POST',
698
+ headers: { 'Content-Type': 'application/json' },
699
+ body: JSON.stringify({
700
+ bdoData,
701
+ relevantBDOs,
702
+ productType: 'linkitylink'
703
+ })
704
+ });
705
+
706
+ if (!response.ok) {
707
+ throw new Error('Failed to create handoff');
708
+ }
709
+
710
+ const handoffData = await response.json();
711
+ console.log('✅ Handoff created:', handoffData);
712
+
713
+ // Show authteam modal
714
+ showAuthteamModal(handoffData);
715
+
716
+ } catch (error) {
717
+ console.error('❌ Handoff error:', error);
718
+ alert('Failed to start app handoff. Please try again.');
719
+ }
720
+ }
721
+
722
+ // Authteam color sequence modal
723
+ function showAuthteamModal(handoffData) {
724
+ const { token, sequence, webPrice, appPrice, discount, discountPercent, expiresAt } = handoffData;
725
+
726
+ // Color definitions
727
+ const colorMap = {
728
+ red: '#ef4444',
729
+ blue: '#3b82f6',
730
+ green: '#22c55e',
731
+ yellow: '#eab308',
732
+ purple: '#a855f7',
733
+ orange: '#f97316'
734
+ };
735
+
736
+ const modal = document.createElement('div');
737
+ modal.className = 'authteam-modal';
738
+ modal.style.cssText = `
739
+ position: fixed;
740
+ top: 0;
741
+ left: 0;
742
+ width: 100%;
743
+ height: 100%;
744
+ background: rgba(0,0,0,0.9);
745
+ display: flex;
746
+ align-items: center;
747
+ justify-content: center;
748
+ z-index: 10000;
749
+ padding: 20px;
750
+ `;
751
+
752
+ const content = document.createElement('div');
753
+ content.style.cssText = `
754
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
755
+ padding: 40px;
756
+ border-radius: 20px;
757
+ max-width: 500px;
758
+ width: 100%;
759
+ box-shadow: 0 20px 60px rgba(0,0,0,0.5);
760
+ text-align: center;
761
+ color: white;
762
+ `;
763
+
764
+ content.innerHTML = `
765
+ <h2 style="margin: 0 0 10px 0; color: #fff; font-size: 1.8rem;">Connect to The Advancement</h2>
766
+ <p style="margin: 0 0 20px 0; color: #94a3b8; font-size: 1rem;">
767
+ Match the color sequence shown in the app to verify your device
768
+ </p>
769
+
770
+ <!-- Discount banner -->
771
+ <div style="
772
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
773
+ padding: 15px;
774
+ border-radius: 10px;
775
+ margin-bottom: 25px;
776
+ ">
777
+ <span style="font-size: 1.2rem; font-weight: bold;">Save $${(discount / 100).toFixed(2)}</span>
778
+ <span style="opacity: 0.9;"> (${discountPercent}% off)</span>
779
+ </div>
780
+
781
+ <!-- Sequence display -->
782
+ <div style="margin-bottom: 25px;">
783
+ <p style="color: #94a3b8; margin-bottom: 10px; font-size: 0.9rem;">Enter this sequence in the app:</p>
784
+ <div id="sequence-display" style="
785
+ display: flex;
786
+ justify-content: center;
787
+ gap: 10px;
788
+ padding: 20px;
789
+ background: rgba(255,255,255,0.05);
790
+ border-radius: 15px;
791
+ ">
792
+ ${sequence.map(color => `
793
+ <div style="
794
+ width: 50px;
795
+ height: 50px;
796
+ background: ${colorMap[color]};
797
+ border-radius: 10px;
798
+ box-shadow: 0 4px 15px ${colorMap[color]}40;
799
+ " title="${color}"></div>
800
+ `).join('')}
801
+ </div>
802
+ </div>
803
+
804
+ <!-- Status -->
805
+ <div id="handoff-status" style="
806
+ padding: 15px;
807
+ background: rgba(255,255,255,0.05);
808
+ border-radius: 10px;
809
+ margin-bottom: 20px;
810
+ ">
811
+ <p style="margin: 0; color: #fbbf24;">
812
+ ⏳ Waiting for app verification...
813
+ </p>
814
+ <p style="margin: 5px 0 0 0; color: #94a3b8; font-size: 0.8rem;">
815
+ Open The Advancement app and tap "Connect to Web"
816
+ </p>
817
+ </div>
818
+
819
+ <!-- Instructions -->
820
+ <div style="
821
+ text-align: left;
822
+ padding: 15px;
823
+ background: rgba(255,255,255,0.05);
824
+ border-radius: 10px;
825
+ margin-bottom: 20px;
826
+ ">
827
+ <p style="margin: 0 0 10px 0; color: #fff; font-weight: 600;">How it works:</p>
828
+ <ol style="margin: 0; padding-left: 20px; color: #94a3b8; font-size: 0.9rem;">
829
+ <li style="margin-bottom: 8px;">Open The Advancement app on your phone</li>
830
+ <li style="margin-bottom: 8px;">Tap "Connect to Web" or scan the QR code</li>
831
+ <li style="margin-bottom: 8px;">Enter the color sequence shown above</li>
832
+ <li style="margin-bottom: 0;">Complete purchase in the app for $${(appPrice / 100).toFixed(2)}</li>
833
+ </ol>
834
+ </div>
835
+
836
+ <!-- Token for manual entry -->
837
+ <div style="margin-bottom: 20px;">
838
+ <p style="color: #94a3b8; font-size: 0.8rem; margin-bottom: 5px;">Handoff code (for manual entry):</p>
839
+ <code style="
840
+ background: rgba(255,255,255,0.1);
841
+ padding: 8px 15px;
842
+ border-radius: 5px;
843
+ font-family: monospace;
844
+ font-size: 0.9rem;
845
+ user-select: all;
846
+ cursor: pointer;
847
+ " onclick="navigator.clipboard.writeText('${token}')" title="Click to copy">${token.substring(0, 16)}...</code>
848
+ </div>
849
+
850
+ <!-- Buttons -->
851
+ <div style="display: flex; gap: 10px;">
852
+ <button id="pay-web-instead" style="
853
+ flex: 1;
854
+ background: #374151;
855
+ color: white;
856
+ border: none;
857
+ padding: 15px;
858
+ border-radius: 10px;
859
+ font-size: 14px;
860
+ cursor: pointer;
861
+ ">Pay $${(webPrice / 100).toFixed(2)} on Web Instead</button>
862
+ <button id="cancel-handoff" style="
863
+ flex: 1;
864
+ background: #4b5563;
865
+ color: white;
866
+ border: none;
867
+ padding: 15px;
868
+ border-radius: 10px;
869
+ font-size: 14px;
870
+ cursor: pointer;
871
+ ">Cancel</button>
872
+ </div>
873
+ `;
874
+
875
+ modal.appendChild(content);
876
+ document.body.appendChild(modal);
877
+
878
+ // Store handoff data for polling
879
+ window.currentHandoff = {
880
+ token,
881
+ modal,
882
+ pollInterval: null
883
+ };
884
+
885
+ // Start polling for handoff completion
886
+ startHandoffPolling(token, modal);
887
+
888
+ // Button handlers
889
+ document.getElementById('pay-web-instead').addEventListener('click', () => {
890
+ stopHandoffPolling();
891
+ document.body.removeChild(modal);
892
+ handlePurchase(); // Switch to web purchase
893
+ });
894
+
895
+ document.getElementById('cancel-handoff').addEventListener('click', () => {
896
+ stopHandoffPolling();
897
+ document.body.removeChild(modal);
898
+ });
899
+ }
900
+
901
+ // Poll for handoff completion
902
+ function startHandoffPolling(token, modal) {
903
+ const statusEl = modal.querySelector('#handoff-status');
904
+
905
+ window.currentHandoff.pollInterval = setInterval(async () => {
906
+ try {
907
+ const response = await fetch(`/handoff/${token}/status`);
908
+ if (!response.ok) {
909
+ throw new Error('Handoff expired or not found');
910
+ }
911
+
912
+ const data = await response.json();
913
+
914
+ if (data.sequenceCompleted && data.appPubKey) {
915
+ // App has verified and associated
916
+ statusEl.innerHTML = `
917
+ <p style="margin: 0; color: #22c55e;">
918
+ ✅ App connected successfully!
919
+ </p>
920
+ <p style="margin: 5px 0 0 0; color: #94a3b8; font-size: 0.8rem;">
921
+ Complete purchase in The Advancement app
922
+ </p>
923
+ `;
924
+ } else if (data.sequenceCompleted) {
925
+ // Sequence verified, waiting for app association
926
+ statusEl.innerHTML = `
927
+ <p style="margin: 0; color: #3b82f6;">
928
+ 🔄 Sequence verified, connecting app...
929
+ </p>
930
+ `;
931
+ }
932
+
933
+ if (data.completedAt) {
934
+ // Purchase completed in app!
935
+ stopHandoffPolling();
936
+ document.body.removeChild(modal);
937
+
938
+ // Clear relevantBDOs
939
+ RelevantBDOs.clear();
940
+
941
+ // Show success
942
+ showAppPurchaseSuccess(data);
943
+ }
944
+
945
+ } catch (error) {
946
+ console.log('Handoff polling error:', error.message);
947
+ // Don't stop polling on minor errors
948
+ }
949
+ }, 2000); // Poll every 2 seconds
950
+ }
951
+
952
+ function stopHandoffPolling() {
953
+ if (window.currentHandoff && window.currentHandoff.pollInterval) {
954
+ clearInterval(window.currentHandoff.pollInterval);
955
+ window.currentHandoff.pollInterval = null;
956
+ }
957
+ }
958
+
959
+ // Show success after app purchase
960
+ function showAppPurchaseSuccess(data) {
961
+ const { emojicode, bdoPubKey } = data;
962
+
963
+ // Construct URLs
964
+ const emojicodeUrl = emojicode
965
+ ? `${window.location.origin}?emojicode=${encodeURIComponent(emojicode)}`
966
+ : null;
967
+ const alphanumericUrl = bdoPubKey
968
+ ? `${window.location.origin}/t/${bdoPubKey.substring(0, 16)}`
969
+ : null;
970
+
971
+ document.body.innerHTML = `
972
+ <div style="
973
+ min-height: 100vh;
974
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
975
+ display: flex;
976
+ align-items: center;
977
+ justify-content: center;
978
+ padding: 20px;
979
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
980
+ ">
981
+ <div style="
982
+ background: white;
983
+ border-radius: 20px;
984
+ padding: 60px 40px;
985
+ max-width: 600px;
986
+ width: 100%;
987
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
988
+ text-align: center;
989
+ ">
990
+ <div style="font-size: 64px; margin-bottom: 20px;">📱✨</div>
991
+ <h1 style="color: #333; margin-bottom: 10px; font-size: 2rem;">Purchase Complete!</h1>
992
+ <p style="color: #666; margin-bottom: 20px; font-size: 1.1rem;">
993
+ Your tapestry was purchased in The Advancement app
994
+ </p>
995
+ <p style="color: #10b981; margin-bottom: 40px; font-size: 1rem; font-weight: 600;">
996
+ You saved 25% with the app discount!
997
+ </p>
998
+
999
+ ${emojicode ? `
1000
+ <!-- Emojicode -->
1001
+ <div style="
1002
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1003
+ color: white;
1004
+ padding: 20px;
1005
+ border-radius: 15px;
1006
+ margin-bottom: 30px;
1007
+ ">
1008
+ <div style="font-size: 0.9rem; opacity: 0.9; margin-bottom: 8px;">Your Emojicode</div>
1009
+ <div style="
1010
+ font-size: 2rem;
1011
+ font-family: monospace;
1012
+ letter-spacing: 2px;
1013
+ ">${emojicode}</div>
1014
+ </div>
1015
+ ` : ''}
1016
+
1017
+ ${emojicodeUrl ? `
1018
+ <div style="margin-bottom: 30px;">
1019
+ <a href="${emojicodeUrl}" style="
1020
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
1021
+ color: white;
1022
+ padding: 15px 30px;
1023
+ border-radius: 10px;
1024
+ text-decoration: none;
1025
+ font-weight: 600;
1026
+ display: inline-block;
1027
+ ">View Your Tapestry</a>
1028
+ </div>
1029
+ ` : ''}
1030
+
1031
+ <p style="color: #666; font-size: 0.9rem;">
1032
+ Your tapestry is now in your carrierBag in The Advancement app.
1033
+ </p>
1034
+
1035
+ <a href="/create" style="
1036
+ color: #10b981;
1037
+ text-decoration: none;
1038
+ font-weight: 600;
1039
+ margin-top: 20px;
1040
+ display: inline-block;
1041
+ ">Create Another Tapestry</a>
1042
+ </div>
1043
+ </div>
1044
+ `;
1045
+ }
1046
+
1047
+ function showMessage(message, type) {
1048
+ const messageElement = document.getElementById('payment-message');
1049
+ messageElement.textContent = message;
1050
+ messageElement.style.display = 'block';
1051
+ messageElement.style.background = type === 'error' ? '#fee2e2' : '#d1fae5';
1052
+ messageElement.style.color = type === 'error' ? '#991b1b' : '#065f46';
1053
+ }
1054
+
1055
+ // Show success page with emojicode and URLs
1056
+ function showSuccessPage(tapestryData) {
1057
+ const { emojicode, pubKey, uuid } = tapestryData;
1058
+
1059
+ // Construct URLs (client constructs, server doesn't)
1060
+ const emojicodeUrl = `${window.location.origin}?emojicode=${encodeURIComponent(emojicode)}`;
1061
+ const alphanumericUrl = `${window.location.origin}/t/${pubKey.substring(0, 16)}`;
1062
+ const bdoUrl = `http://localhost:3003/emoji/${encodeURIComponent(emojicode)}`;
1063
+
1064
+ // Replace entire page with success view
1065
+ document.body.innerHTML = `
1066
+ <div style="
1067
+ min-height: 100vh;
1068
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1069
+ display: flex;
1070
+ align-items: center;
1071
+ justify-content: center;
1072
+ padding: 20px;
1073
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1074
+ ">
1075
+ <div style="
1076
+ background: white;
1077
+ border-radius: 20px;
1078
+ padding: 60px 40px;
1079
+ max-width: 600px;
1080
+ width: 100%;
1081
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
1082
+ text-align: center;
1083
+ ">
1084
+ <div style="font-size: 64px; margin-bottom: 20px;">✨</div>
1085
+ <h1 style="color: #333; margin-bottom: 10px; font-size: 2rem;">Your Tapestry is Ready!</h1>
1086
+ <p style="color: #666; margin-bottom: 40px; font-size: 1.1rem;">Share your magical link tapestry with the world</p>
1087
+
1088
+ <!-- Emojicode -->
1089
+ <div style="
1090
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1091
+ color: white;
1092
+ padding: 20px;
1093
+ border-radius: 15px;
1094
+ margin-bottom: 30px;
1095
+ ">
1096
+ <div style="font-size: 0.9rem; opacity: 0.9; margin-bottom: 8px;">Your Emojicode</div>
1097
+ <div style="
1098
+ font-size: 2rem;
1099
+ font-family: monospace;
1100
+ letter-spacing: 2px;
1101
+ cursor: pointer;
1102
+ padding: 10px;
1103
+ background: rgba(255,255,255,0.1);
1104
+ border-radius: 8px;
1105
+ " onclick="copyToClipboard('${emojicode}', this)" title="Click to copy">
1106
+ ${emojicode}
1107
+ </div>
1108
+ <div style="font-size: 0.8rem; opacity: 0.8; margin-top: 8px;">👆 Click to copy</div>
1109
+ </div>
1110
+
1111
+ <!-- URLs -->
1112
+ <div style="margin-bottom: 30px;">
1113
+ <h3 style="color: #333; margin-bottom: 15px; font-size: 1.2rem;">Share Your Links</h3>
1114
+
1115
+ <!-- Emojicode URL -->
1116
+ <div style="margin-bottom: 15px;">
1117
+ <label style="display: block; color: #666; font-size: 0.9rem; margin-bottom: 5px;">Emojicode URL (Persistent)</label>
1118
+ <div style="
1119
+ background: #f3f4f6;
1120
+ padding: 12px;
1121
+ border-radius: 8px;
1122
+ font-family: monospace;
1123
+ font-size: 0.85rem;
1124
+ word-break: break-all;
1125
+ cursor: pointer;
1126
+ border: 2px solid transparent;
1127
+ transition: border-color 0.2s;
1128
+ " onclick="copyToClipboard('${emojicodeUrl}', this)">
1129
+ ${emojicodeUrl}
1130
+ </div>
1131
+ </div>
1132
+
1133
+ <!-- Alphanumeric URL -->
1134
+ <div style="margin-bottom: 15px;">
1135
+ <label style="display: block; color: #666; font-size: 0.9rem; margin-bottom: 5px;">Browser-Friendly URL</label>
1136
+ <div style="
1137
+ background: #f3f4f6;
1138
+ padding: 12px;
1139
+ border-radius: 8px;
1140
+ font-family: monospace;
1141
+ font-size: 0.85rem;
1142
+ word-break: break-all;
1143
+ cursor: pointer;
1144
+ border: 2px solid transparent;
1145
+ transition: border-color 0.2s;
1146
+ " onclick="copyToClipboard('${alphanumericUrl}', this)">
1147
+ ${alphanumericUrl}
1148
+ </div>
1149
+ </div>
1150
+ </div>
1151
+
1152
+ <!-- Action Buttons -->
1153
+ <div style="display: flex; gap: 15px; flex-wrap: wrap;">
1154
+ <a href="${emojicodeUrl}" style="
1155
+ flex: 1;
1156
+ min-width: 200px;
1157
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
1158
+ color: white;
1159
+ padding: 15px 30px;
1160
+ border-radius: 10px;
1161
+ text-decoration: none;
1162
+ font-weight: 600;
1163
+ display: inline-block;
1164
+ transition: transform 0.2s, box-shadow 0.2s;
1165
+ " onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 30px rgba(16, 185, 129, 0.4)'"
1166
+ onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 4px 20px rgba(16, 185, 129, 0.3)'">
1167
+ View Your Tapestry
1168
+ </a>
1169
+ <a href="/create" style="
1170
+ flex: 1;
1171
+ min-width: 200px;
1172
+ background: #e5e7eb;
1173
+ color: #333;
1174
+ padding: 15px 30px;
1175
+ border-radius: 10px;
1176
+ text-decoration: none;
1177
+ font-weight: 600;
1178
+ display: inline-block;
1179
+ transition: transform 0.2s;
1180
+ " onmouseover="this.style.transform='translateY(-2px)'"
1181
+ onmouseout="this.style.transform='translateY(0)'">
1182
+ Create Another
1183
+ </a>
1184
+ </div>
1185
+
1186
+ <!-- Footer Note -->
1187
+ <p style="color: #999; font-size: 0.85rem; margin-top: 30px;">
1188
+ 💾 Your tapestry is saved to your session and can be accessed anytime
1189
+ </p>
1190
+ </div>
1191
+ </div>
1192
+
1193
+ <script>
1194
+ function copyToClipboard(text, element) {
1195
+ navigator.clipboard.writeText(text).then(() => {
1196
+ const originalBorder = element.style.border;
1197
+ element.style.border = '2px solid #10b981';
1198
+ setTimeout(() => {
1199
+ element.style.border = originalBorder;
1200
+ }, 1000);
1201
+ });
1202
+ }
1203
+ <\/script>
1204
+ `;
1205
+ }
1206
+
1207
+ // Template Options Modal
1208
+ function showTemplateOptions() {
1209
+ const modal = document.createElement('div');
1210
+ modal.style.cssText = `
1211
+ position: fixed;
1212
+ top: 0;
1213
+ left: 0;
1214
+ width: 100%;
1215
+ height: 100%;
1216
+ background: rgba(0,0,0,0.8);
1217
+ display: flex;
1218
+ align-items: center;
1219
+ justify-content: center;
1220
+ z-index: 10000;
1221
+ `;
1222
+
1223
+ const content = document.createElement('div');
1224
+ content.style.cssText = `
1225
+ background: white;
1226
+ padding: 40px;
1227
+ border-radius: 20px;
1228
+ max-width: 500px;
1229
+ width: 90%;
1230
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
1231
+ `;
1232
+
1233
+ content.innerHTML = `
1234
+ <h2 style="margin: 0 0 10px 0; color: #333;">Template Options</h2>
1235
+ <p style="margin: 0 0 30px 0; color: #666;">Download template spec or upload your own</p>
1236
+
1237
+ <button onclick="downloadTemplateSpec()" style="
1238
+ width: 100%;
1239
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
1240
+ color: white;
1241
+ border: none;
1242
+ padding: 15px;
1243
+ border-radius: 10px;
1244
+ font-size: 16px;
1245
+ font-weight: 600;
1246
+ cursor: pointer;
1247
+ margin-bottom: 10px;
1248
+ ">Download Template Spec</button>
1249
+
1250
+ <button onclick="showUploadTemplateForm()" style="
1251
+ width: 100%;
1252
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
1253
+ color: white;
1254
+ border: none;
1255
+ padding: 15px;
1256
+ border-radius: 10px;
1257
+ font-size: 16px;
1258
+ font-weight: 600;
1259
+ cursor: pointer;
1260
+ margin-bottom: 10px;
1261
+ ">Upload Custom Template</button>
1262
+
1263
+ <button onclick="this.closest('.modal').remove()" style="
1264
+ width: 100%;
1265
+ background: #e5e7eb;
1266
+ color: #666;
1267
+ border: none;
1268
+ padding: 15px;
1269
+ border-radius: 10px;
1270
+ font-size: 16px;
1271
+ cursor: pointer;
1272
+ ">Cancel</button>
1273
+ `;
1274
+
1275
+ content.querySelector('button[onclick*="remove"]').onclick = () => {
1276
+ document.body.removeChild(modal);
1277
+ };
1278
+
1279
+ modal.className = 'modal';
1280
+ modal.appendChild(content);
1281
+ document.body.appendChild(modal);
1282
+ }
1283
+
1284
+ // Download Template Specification
1285
+ function downloadTemplateSpec() {
1286
+ const templateSpec = {
1287
+ _documentation: {
1288
+ description: "Glyphenge Custom Template Specification",
1289
+ version: "1.0",
1290
+ revenue: "Earn 2% of every $20 purchase using your template",
1291
+ requirements: {
1292
+ container: "SVG container with dynamic \\${height} placeholder",
1293
+ linkTemplate: "Must include <a href=\\\"\\${url}\\\" target=\\\"_blank\\\"> wrapper",
1294
+ placeholders: [
1295
+ "\\${height} - Dynamic height based on link count",
1296
+ "\\${url} - Link URL",
1297
+ "\\${title} - Link title",
1298
+ "\\${domain} - Extracted domain name",
1299
+ "\\${index} or \\${i} - Link index (0-based)"
1300
+ ]
1301
+ }
1302
+ },
1303
+ name: "My Custom Template",
1304
+ colors: ["#ff6b6b", "#ee5a6f", "#feca57"],
1305
+ linkColors: ["#10b981", "#3b82f6", "#8b5cf6", "#ec4899"],
1306
+ background: `<svg viewBox="0 0 600 \${height}" xmlns="http://www.w3.org/2000/svg">
1307
+ <defs>
1308
+ <linearGradient id="bg" x1="0%" y1="0%" x2="0%" y2="100%">
1309
+ <stop offset="0%" style="stop-color:#ff6b6b;stop-opacity:1" />
1310
+ <stop offset="50%" style="stop-color:#ee5a6f;stop-opacity:1" />
1311
+ <stop offset="100%" style="stop-color:#feca57;stop-opacity:1" />
1312
+ </linearGradient>
1313
+ </defs>
1314
+ <rect width="600" height="\${height}" fill="url(#bg)" rx="20"/>
1315
+ </svg>`,
1316
+ linkTemplate: `<a href="\${url}" target="_blank">
1317
+ <rect x="50" y="\${y}" width="500" height="80" fill="\${color}" rx="12" opacity="0.95"/>
1318
+ <text x="80" y="\${y + 35}" font-family="system-ui" font-size="16" fill="white">\${title}</text>
1319
+ <text x="80" y="\${y + 55}" font-family="system-ui" font-size="14" fill="white" opacity="0.9">\${domain}</text>
1320
+ </a>`
1321
+ };
1322
+
1323
+ const blob = new Blob([JSON.stringify(templateSpec, null, 2)], { type: 'application/json' });
1324
+ const url = URL.createObjectURL(blob);
1325
+ const a = document.createElement('a');
1326
+ a.href = url;
1327
+ a.download = 'glyphenge-template.json';
1328
+ a.click();
1329
+ URL.revokeObjectURL(url);
1330
+
1331
+ // Close modal
1332
+ document.querySelector('.modal').remove();
1333
+ }
1334
+
1335
+ // Show Upload Template Form
1336
+ function showUploadTemplateForm() {
1337
+ // Close current modal
1338
+ document.querySelector('.modal').remove();
1339
+
1340
+ const modal = document.createElement('div');
1341
+ modal.style.cssText = `
1342
+ position: fixed;
1343
+ top: 0;
1344
+ left: 0;
1345
+ width: 100%;
1346
+ height: 100%;
1347
+ background: rgba(0,0,0,0.8);
1348
+ display: flex;
1349
+ align-items: center;
1350
+ justify-content: center;
1351
+ z-index: 10000;
1352
+ `;
1353
+
1354
+ const content = document.createElement('div');
1355
+ content.style.cssText = `
1356
+ background: white;
1357
+ padding: 40px;
1358
+ border-radius: 20px;
1359
+ max-width: 500px;
1360
+ width: 90%;
1361
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
1362
+ `;
1363
+
1364
+ content.innerHTML = `
1365
+ <h2 style="margin: 0 0 10px 0; color: #333;">Upload Custom Template</h2>
1366
+ <p style="margin: 0 0 20px 0; color: #666;">Upload your template JSON file</p>
1367
+
1368
+ <div style="margin-bottom: 20px;">
1369
+ <input type="file" id="template-file" accept=".json" style="
1370
+ width: 100%;
1371
+ padding: 10px;
1372
+ border: 2px dashed #d1d5db;
1373
+ border-radius: 10px;
1374
+ cursor: pointer;
1375
+ ">
1376
+ </div>
1377
+
1378
+ <button id="upload-template-btn" style="
1379
+ width: 100%;
1380
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
1381
+ color: white;
1382
+ border: none;
1383
+ padding: 15px;
1384
+ border-radius: 10px;
1385
+ font-size: 16px;
1386
+ font-weight: 600;
1387
+ cursor: pointer;
1388
+ margin-bottom: 10px;
1389
+ ">Upload Template</button>
1390
+
1391
+ <button onclick="this.closest('.modal').remove()" style="
1392
+ width: 100%;
1393
+ background: #e5e7eb;
1394
+ color: #666;
1395
+ border: none;
1396
+ padding: 15px;
1397
+ border-radius: 10px;
1398
+ font-size: 16px;
1399
+ cursor: pointer;
1400
+ ">Cancel</button>
1401
+ `;
1402
+
1403
+ content.querySelector('button[onclick*="remove"]').onclick = () => {
1404
+ document.body.removeChild(modal);
1405
+ };
1406
+
1407
+ content.querySelector('#upload-template-btn').onclick = () => {
1408
+ uploadTemplate();
1409
+ };
1410
+
1411
+ modal.className = 'modal';
1412
+ modal.appendChild(content);
1413
+ document.body.appendChild(modal);
1414
+ }
1415
+
1416
+ // Upload Template
1417
+ async function uploadTemplate() {
1418
+ const fileInput = document.getElementById('template-file');
1419
+ const file = fileInput.files[0];
1420
+
1421
+ if (!file) {
1422
+ alert('Please select a template file');
1423
+ return;
1424
+ }
1425
+
1426
+ try {
1427
+ const text = await file.text();
1428
+ const template = JSON.parse(text);
1429
+
1430
+ // Validate template structure
1431
+ if (!template.name || !template.colors || !template.linkColors || !template.background || !template.linkTemplate) {
1432
+ alert('Invalid template format. Please check the specification.');
1433
+ return;
1434
+ }
1435
+
1436
+ // Add to templates (would normally upload to server)
1437
+ console.log('Custom template uploaded:', template);
1438
+ alert('Template uploaded successfully! (Server upload not yet implemented)');
1439
+
1440
+ // Close modal
1441
+ document.querySelector('.modal').remove();
1442
+
1443
+ } catch (error) {
1444
+ console.error('Upload error:', error);
1445
+ alert('Failed to parse template file. Please check the format.');
1446
+ }
1447
+ }
1448
+
1449
+
1450
+ // Initialize - fetch user templates then render UI
1451
+ async function initialize() {
1452
+ await fetchUserSubmittedTemplates();
1453
+ renderCarousel();
1454
+ renderLinkList();
1455
+ updatePreview();
1456
+
1457
+ // Hide app purchase button if disabled
1458
+ if (window.LINKITYLINK_CONFIG && !window.LINKITYLINK_CONFIG.enableAppPurchase) {
1459
+ const appButton = document.getElementById('app-purchase-button');
1460
+ if (appButton) {
1461
+ appButton.style.display = 'none';
1462
+ }
1463
+ }
1464
+ }
1465
+ initialize();
1466
+ </script>
1467
+ </body>
1468
+ </html>