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,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, '&')
|
|
474
|
+
.replace(/</g, '<')
|
|
475
|
+
.replace(/>/g, '>')
|
|
476
|
+
.replace(/"/g, '"')
|
|
477
|
+
.replace(/'/g, ''');
|
|
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>
|